Jelajahi Sumber

Merge branch 'feat/tags'

* feat/tags:
  Add x-mark, ensure proper loading from both Eclipse and jarfile
  Ensure that the save-dir is appropriately set.
  Fix sizing when loading something with a lot of tags due to FlowLayout
  Link tag panel to model
  Add set of strings for tags.
  Add panel for tag information.
Sam Jaffe 4 tahun lalu
induk
melakukan
328e2fe248

+ 16 - 0
src/main/lombok/org/leumasjaffe/recipe/config/Env.java

@@ -0,0 +1,16 @@
+package org.leumasjaffe.recipe.config;
+
+import com.google.common.base.MoreObjects;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class Env {
+	public final String RECIPE_SAVE_DIR;
+
+	static {
+		RECIPE_SAVE_DIR = MoreObjects.firstNonNull(
+				System.getenv("RECIPE_MANAGER_SAVE_DIR"),
+				System.getProperty("user.home") + "/.local/recipe-manager");
+	}
+}

+ 112 - 0
src/main/lombok/org/leumasjaffe/recipe/controller/GhostTextController.java

@@ -0,0 +1,112 @@
+package org.leumasjaffe.recipe.controller;
+
+import java.awt.Color;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.JTextComponent;
+
+/**
+ * Code taken from https://stackoverflow.com/questions/10506789/how-to-display-faint-gray-ghost-text-in-a-jtextfield/10507193
+ * @author Guillaume Polet / XaV
+ *
+ */
+public class GhostTextController implements FocusListener, DocumentListener, PropertyChangeListener {
+	private final JTextComponent textComp;
+	private boolean isEmpty;
+	private Color ghostColor;
+	private Color foregroundColor;
+	private final String ghostText;
+
+	public GhostTextController(final JTextComponent textComp, String ghostText) {
+		super();
+		this.textComp = textComp;
+		this.ghostText = ghostText;
+		this.ghostColor = Color.LIGHT_GRAY;
+		textComp.addFocusListener(this);
+		registerListeners();
+		updateState();
+		if (!this.textComp.hasFocus()) {
+			focusLost(null);
+		}
+	}
+
+	public void delete() {
+		unregisterListeners();
+		textComp.removeFocusListener(this);
+	}
+
+	private void registerListeners() {
+		textComp.getDocument().addDocumentListener(this);
+		textComp.addPropertyChangeListener("foreground", this);
+	}
+
+	private void unregisterListeners() {
+		textComp.getDocument().removeDocumentListener(this);
+		textComp.removePropertyChangeListener("foreground", this);
+	}
+
+	public Color getGhostColor() {
+		return ghostColor;
+	}
+
+	public void setGhostColor(Color ghostColor) {
+		this.ghostColor = ghostColor;
+	}
+
+	private void updateState() {
+		isEmpty = textComp.getText().length() == 0;
+		foregroundColor = textComp.getForeground();
+	}
+
+	@Override
+	public void focusGained(FocusEvent e) {
+		if (isEmpty) {
+			unregisterListeners();
+			try {
+				textComp.setText("");
+				textComp.setForeground(foregroundColor);
+			} finally {
+				registerListeners();
+			}
+		}
+	}
+
+	@Override
+	public void focusLost(FocusEvent e) {
+		if (isEmpty) {
+			unregisterListeners();
+			try {
+				textComp.setText(ghostText);
+				textComp.setForeground(ghostColor);
+			} finally {
+				registerListeners();
+			}
+		}
+	}
+
+	@Override
+	public void propertyChange(PropertyChangeEvent evt) {
+		updateState();
+	}
+
+	@Override
+	public void changedUpdate(DocumentEvent e) {
+		updateState();
+	}
+
+	@Override
+	public void insertUpdate(DocumentEvent e) {
+		updateState();
+	}
+
+	@Override
+	public void removeUpdate(DocumentEvent e) {
+		updateState();
+	}
+
+}

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

@@ -13,6 +13,8 @@ import java.util.Optional;
 import javax.swing.JFileChooser;
 import javax.swing.JOptionPane;
 
+import org.leumasjaffe.recipe.config.Env;
+
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
@@ -22,7 +24,7 @@ import lombok.experimental.NonFinal;
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 class SwingSaveLoadHandle implements SaveLoadHandle {
-	JFileChooser chooser = new JFileChooser();
+	JFileChooser chooser = new JFileChooser(Env.RECIPE_SAVE_DIR);
 	Component owner;
 	@NonFinal File filename = null;
 	

+ 3 - 0
src/main/lombok/org/leumasjaffe/recipe/model/RecipeCard.java

@@ -1,8 +1,10 @@
 package org.leumasjaffe.recipe.model;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Stream;
 
 import javax.swing.ImageIcon;
@@ -17,6 +19,7 @@ public class RecipeCard extends Observable.Instance implements CompoundRecipeCom
 	String title = "";
 	String description = "";
 	int servings = 1;
+	Set<String> tags = new HashSet<>();
 	// TODO: Nutrition information
 	Optional<ImageIcon> photo = Optional.empty(); // TODO JSONIZATION	
 	List<Element> elements = new ArrayList<>();

+ 108 - 0
src/main/lombok/org/leumasjaffe/recipe/view/TagInputPanel.java

@@ -0,0 +1,108 @@
+package org.leumasjaffe.recipe.view;
+
+import javax.imageio.ImageIO;
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.JTextField;
+
+import org.leumasjaffe.recipe.controller.GhostTextController;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class TagInputPanel extends JPanel {
+	private static final Icon X_MARK;
+	static {
+		BufferedImage tmp = null;
+		try {
+			tmp = ImageIO.read(TagInputPanel.class.getResourceAsStream("/x-mark.png"));
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		X_MARK = new ImageIcon(tmp);
+	}
+	
+	private final class TagField extends JPanel {
+		private TagField(final String value) {
+			setLayout(new FlowLayout(FlowLayout.CENTER));
+			add(new JLabel(value));
+			JButton btnRemove = new JButton(X_MARK);
+			btnRemove.setPreferredSize(new Dimension(10, 10));
+			add(btnRemove);
+			btnRemove.addActionListener(e -> {
+				setVisible(false);
+				TagInputPanel.this.model.remove(value);
+				TagInputPanel.this.remove(this);
+				TagInputPanel.this.revalidate();
+			});
+		}
+	}
+	
+	@NonFinal Set<String> model = new HashSet<>();
+	JTextField textField;
+
+	public TagInputPanel() {
+		setLayout(new FlowLayout(FlowLayout.LEADING, 0, 0));
+		
+		textField = new JTextField();
+		new GhostTextController(textField, "Tags...");
+		add(textField);
+		textField.setColumns(10);
+		
+		textField.addKeyListener(new KeyListener() {
+			@Override public void keyTyped(KeyEvent e) {}
+			@Override public void keyReleased(KeyEvent e) {}
+			
+			@Override
+			public void keyPressed(KeyEvent e) {
+				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+					pushTag(textField.getText());
+					textField.setText("");
+				}
+			}
+		});
+	}
+	
+	public void setModel(Set<String> model) {
+		this.model = model;
+		clear();
+		for (final String tag : this.model) {
+			pushTag(tag);
+		}
+	}
+
+	private void clear() {
+		for (Component comp : getComponents()) {
+			comp.setVisible(false);
+		}
+		removeAll();
+		textField.setVisible(true);
+		add(textField);
+	}
+
+	private void pushTag(final String tag) {
+		this.model.add(tag);
+		remove(textField);
+		add(new TagField(tag));
+		add(textField);
+		revalidate();
+		textField.requestFocus();
+	}
+}

+ 21 - 8
src/main/lombok/org/leumasjaffe/recipe/view/summary/SummaryPanel.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.recipe.view.summary;
 
+import java.awt.Dimension;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
@@ -20,6 +21,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.view.TagInputPanel;
 import org.leumasjaffe.recipe.viewmodel.ScaleFactor;
 
 import lombok.AccessLevel;
@@ -40,6 +42,7 @@ public class SummaryPanel extends JPanel {
 	
 	JTextField txtTitle;
 	JTextArea txaDescription;
+	TagInputPanel panelTags;
 	JSpinner spnServings;
 	JSpinner spnServingsToMake;
 	
@@ -53,9 +56,9 @@ public class SummaryPanel extends JPanel {
 		
 		GridBagLayout gridBagLayout = new GridBagLayout();
 		gridBagLayout.columnWidths = new int[]{0, 0, 0};
-		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
 		gridBagLayout.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
-		gridBagLayout.rowWeights = new double[]{0.0, 1.0, 1.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 1.0, 1.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
 		JPanel panelHeader = new JPanel();
@@ -121,7 +124,7 @@ public class SummaryPanel extends JPanel {
 		
 		JPanel panelIngredients = new JPanel();
 		GridBagConstraints gbc_panelIngredients = new GridBagConstraints();
-		gbc_panelIngredients.insets = new Insets(0, 0, 5, 5);
+		gbc_panelIngredients.insets = new Insets(0, 0, 0, 5);
 		gbc_panelIngredients.fill = GridBagConstraints.BOTH;
 		gbc_panelIngredients.gridx = 0;
 		gbc_panelIngredients.gridy = 2;
@@ -131,16 +134,15 @@ public class SummaryPanel extends JPanel {
 		JPanel panel = new JPanel();
 		GridBagConstraints gbc_panel = new GridBagConstraints();
 		gbc_panel.gridheight = 2;
-		gbc_panel.insets = new Insets(0, 0, 5, 0);
 		gbc_panel.fill = GridBagConstraints.BOTH;
 		gbc_panel.gridx = 1;
 		gbc_panel.gridy = 1;
 		add(panel, gbc_panel);
 		GridBagLayout gbl_panel = new GridBagLayout();
 		gbl_panel.columnWidths = new int[]{0, 0};
-		gbl_panel.rowHeights = new int[]{0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0, 0, 0};
 		gbl_panel.columnWeights = new double[]{1.0, Double.MIN_VALUE};
-		gbl_panel.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, 0.0, 1.0, Double.MIN_VALUE};
 		panel.setLayout(gbl_panel);
 		
 		JPanel panelPhoto = new ImagePanel();
@@ -151,14 +153,24 @@ public class SummaryPanel extends JPanel {
 		gbc_panelPhoto.gridy = 0;
 		panel.add(panelPhoto, gbc_panelPhoto);
 		
+		JPanel wrapper = new JPanel(new VerticalLayout());
+		panelTags = new TagInputPanel();
+		panelTags.setPreferredSize(new Dimension(200, 100));
+		GridBagConstraints gbc_panelTags = new GridBagConstraints();
+		gbc_panelTags.insets = new Insets(0, 0, 5, 0);
+		gbc_panelTags.fill = GridBagConstraints.BOTH;
+		gbc_panelTags.gridx = 0;
+		gbc_panelTags.gridy = 1;
+		panel.add(wrapper, gbc_panelTags);
+		wrapper.add(panelTags);
+		
 		txaDescription = new JTextArea(5, 20);
 		txaDescription.setWrapStyleWord(true);
 		txaDescription.setLineWrap(true);
 		GridBagConstraints gbc_txaDescription = new GridBagConstraints();
-		gbc_txaDescription.insets = new Insets(0, 0, 5, 0);
 		gbc_txaDescription.fill = GridBagConstraints.BOTH;
 		gbc_txaDescription.gridx = 0;
-		gbc_txaDescription.gridy = 1;
+		gbc_txaDescription.gridy = 2;
 		panel.add(txaDescription, gbc_txaDescription);
 		
 		titleBinding = new TextBinding<>(txtTitle,
@@ -191,6 +203,7 @@ public class SummaryPanel extends JPanel {
 	}
 	
 	public void setModel(final RecipeCard card) {
+		panelTags.setModel(card.getTags());
 		titleBinding.setModel(card);
 		servingsBinding.setModel(card);
 		descriptionBinding.setModel(card);

TEMPAT SAMPAH
src/main/resources/x-mark.png


File diff ditekan karena terlalu besar
+ 1 - 0
src/main/resources/x-mark.svg