diff --git a/pom.xml b/pom.xml index 398a04d..45b434a 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,7 @@ res + true diff --git a/res/comment.png b/res/comment.png new file mode 100755 index 0000000..8880c9e Binary files /dev/null and b/res/comment.png differ diff --git a/res/disable.png b/res/disable.png old mode 100644 new mode 100755 index 60149af..0e80c76 Binary files a/res/disable.png and b/res/disable.png differ diff --git a/res/help.png b/res/help.png old mode 100644 new mode 100755 index e66abc6..28c16d3 Binary files a/res/help.png and b/res/help.png differ diff --git a/res/no_comment.png b/res/no_comment.png new file mode 100755 index 0000000..7ba13c9 Binary files /dev/null and b/res/no_comment.png differ diff --git a/res/operation.png b/res/operation.png old mode 100644 new mode 100755 index c1103f7..84c2f02 Binary files a/res/operation.png and b/res/operation.png differ diff --git a/res/remove.png b/res/remove.png old mode 100644 new mode 100755 index 406976c..fc42e25 Binary files a/res/remove.png and b/res/remove.png differ diff --git a/res/stop.png b/res/stop.png old mode 100644 new mode 100755 index e450c91..29ea6d9 Binary files a/res/stop.png and b/res/stop.png differ diff --git a/res/stop_active.png b/res/stop_active.png old mode 100644 new mode 100755 index 9ca2fee..a94eb52 Binary files a/res/stop_active.png and b/res/stop_active.png differ diff --git a/res/version.properties b/res/version.properties new file mode 100644 index 0000000..713c915 --- /dev/null +++ b/res/version.properties @@ -0,0 +1 @@ +version = ${project.version} \ No newline at end of file diff --git a/src/main/java/de/usd/cstchef/operations/Operation.java b/src/main/java/de/usd/cstchef/operations/Operation.java index fceb094..efd807b 100644 --- a/src/main/java/de/usd/cstchef/operations/Operation.java +++ b/src/main/java/de/usd/cstchef/operations/Operation.java @@ -9,6 +9,8 @@ import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.io.EOFException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -19,6 +21,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; @@ -28,6 +31,7 @@ import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JSpinner; @@ -68,6 +72,8 @@ public abstract class Operation extends JPanel { private static ImageIcon disableIcon = new ImageIcon(Operation.class.getResource("/disable.png")); private static ImageIcon removeIcon = new ImageIcon(Operation.class.getResource("/remove.png")); private static ImageIcon helpIcon = new ImageIcon(Operation.class.getResource("/help.png")); + private static ImageIcon commentIcon = new ImageIcon(Operation.class.getResource("/comment.png")); + private static ImageIcon noCommentIcon = new ImageIcon(Operation.class.getResource("/no_comment.png")); private NotifyChangeListener notifyChangeListener; @@ -80,6 +86,9 @@ public abstract class Operation extends JPanel { private Box contentBox; private Map uiElements; + private String comment; + private JButton commentBtn; + private int operationSkip = 0; private int laneSkip = 0; @@ -122,6 +131,19 @@ public Operation() { removeBtn.setToolTipText("Remove"); JButton helpBtn = createIconButton(Operation.helpIcon); helpBtn.setToolTipText(opInfos.description()); + commentBtn = createIconButton(noCommentIcon); + + commentBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + commentBtn.setToolTipText(getComment()); + String comment = JOptionPane.showInputDialog("Edit comment:", commentBtn.getToolTipText()); + commentBtn.setToolTipText(comment); + setComment(comment); + ImageIcon newIcon = comment.isEmpty() ? Operation.noCommentIcon : Operation.commentIcon; + commentBtn.setIcon(newIcon); + } + }); + disableBtn.addActionListener(new ActionListener() { @Override @@ -162,6 +184,8 @@ public void actionPerformed(ActionEvent e) { header.add(titleLbl); header.add(Box.createHorizontalStrut(6)); header.add(helpBtn); + header.add(Box.createHorizontalStrut(3)); + header.add(commentBtn); header.add(Box.createHorizontalGlue()); header.add(disableBtn); header.add(Box.createHorizontalStrut(3)); @@ -189,6 +213,18 @@ public void actionPerformed(ActionEvent e) { this.refreshColors(); } + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + if(comment != null) { + this.comment = comment; + commentBtn.setIcon(Operation.commentIcon); + commentBtn.setToolTipText(comment); + } + } + public Map getState() { Map properties = new HashMap<>(); for (String key : this.uiElements.keySet()) { diff --git a/src/main/java/de/usd/cstchef/view/RecipePanel.java b/src/main/java/de/usd/cstchef/view/RecipePanel.java index 8f4cd84..9647439 100644 --- a/src/main/java/de/usd/cstchef/view/RecipePanel.java +++ b/src/main/java/de/usd/cstchef/view/RecipePanel.java @@ -12,11 +12,13 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Timer; import java.util.TimerTask; @@ -350,10 +352,23 @@ public void setFormatMessage(HttpRequestResponse requestResponse, MessageType me } public void restoreState(String jsonState) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { - // TODO do we want to remove all existing operations before loading here? - this.clear(); // Yes! + this.clear(); ObjectMapper mapper = new ObjectMapper(); - JsonNode stepNodes = mapper.readTree(jsonState); + JsonNode rootNode = mapper.readTree(jsonState); + JsonNode stepNodes; + JsonNode versionNode; + + // check if "version" ObjectNode is there (since 1.3.2) + if(rootNode.get(0) != null && rootNode.get(0).get("version") == null) { + // recipes saved by CSTC <= 1.3.1 + stepNodes = rootNode; + } + else { + // currently 1.3.2 + versionNode = rootNode.get(0); + stepNodes = rootNode.get(1); + } + if (!stepNodes.isArray()) { throw new IOException("wrong data format"); } @@ -364,29 +379,81 @@ public void restoreState(String jsonState) throws IOException, ClassNotFoundExce throw new IOException("wrong data format"); } - for (int i = 0; i < operationNodes.size(); i++) { + RecipeStepPanel panel = (RecipeStepPanel) this.operationLines.getComponent(step); + + /* two types of ObjectNodes for every RecipeStepPanel: + Lane information (always at index 0, if set) and the Operations + + If there's a lane ObjectNode we need to tell the inner loop to begin at index 1. + The inner loop iterates over the Operations + */ + int index = 0; + if(operationNodes.get(0) != null) { + if(operationNodes.get(0).get("lane_title") != null) { + index = 1; + panel.setTitle(operationNodes.get(0).get("lane_title").asText()); + } + if(operationNodes.get(0).get("lane_comment") != null) { + index = 1; + panel.setComment(operationNodes.get(0).get("lane_comment").asText()); + } + } + + for (int i = index; i < operationNodes.size(); i++) { JsonNode operationNode = operationNodes.get(i); String operation = operationNode.get("operation").asText(); Map parameters = mapper.convertValue(operationNode.get("parameters"), Map.class); Class cls = (Class) Class.forName(operation); + // check if it is an operation Operation op = cls.newInstance(); op.load(parameters); op.setDisabled(!operationNode.get("is_enabled").asBoolean()); - RecipeStepPanel panel = (RecipeStepPanel) this.operationLines.getComponent(step); - panel.addComponent(op, i); + + // check if "comment" attribute is set (since 1.3.2) + if(operationNode.get("comment") != null) { + if(operationNode.get("comment").asText() != "null") { + op.setComment(operationNode.get("comment").asText()); + } + } + // depending on if lane name is set we may start the loop at index 1, but want to add the first component at index 0 + panel.addComponent(op, index == 1 ? i-1 : 0); } } } private String getStateAsJSON() throws IOException { ObjectMapper mapper = new ObjectMapper(); + ArrayNode rootNode = mapper.createArrayNode(); + ObjectNode versionNode = mapper.createObjectNode(); ArrayNode stepsNode = mapper.createArrayNode(); for (int step = 0; step < this.operationSteps; step++) { ArrayNode operationsNode = mapper.createArrayNode(); RecipeStepPanel stepPanel = (RecipeStepPanel) this.operationLines.getComponent(step); + + // save lane name in case it differs from the default + int laneNumber = step + 1; + boolean laneNodeCreated = false; + if(!stepPanel.getTitle().equals("Lane " + laneNumber)) { + laneNodeCreated = true; + ObjectNode laneNode = mapper.createObjectNode(); + laneNode.put("lane_title", stepPanel.getTitle()); + // save comment in same node in case it is set + if(!stepPanel.getComment().equals("")) { + laneNode.put("lane_comment", stepPanel.getComment()); + } + operationsNode.add(laneNode); + } + + // save comment in case it's not already + if(!laneNodeCreated && !stepPanel.getComment().equals("")) { + ObjectNode laneNode = mapper.createObjectNode(); + laneNode.put("lane_comment", stepPanel.getComment()); + operationsNode.add(laneNode); + } + List operations = stepPanel.getOperations(); for (Operation op : operations) { ObjectNode operationNode = mapper.createObjectNode(); @@ -394,10 +461,25 @@ private String getStateAsJSON() throws IOException { operationsNode.add(operationNode); operationNode.putPOJO("parameters", op.getState()); operationNode.putPOJO("is_enabled", !op.isDisabled()); + // "comment":null if empty + operationNode.put("comment", op.getComment()); } stepsNode.add(operationsNode); } - return mapper.writeValueAsString(stepsNode); + + /* maven performs a substitution at compile time in "/res/version.properties" + with the version from pom.xml and here it reads from this file + */ + Properties properties = new Properties(); + properties.load(RecipePanel.class.getResourceAsStream("/version.properties")); + String version = properties.getProperty("version"); + + versionNode.put("version", version); + + rootNode.add(versionNode); + rootNode.add(stepsNode); + + return mapper.writeValueAsString(rootNode); } private void save(File file) throws IOException { @@ -587,6 +669,9 @@ private void saveFilterState() { private void clear() { for (int step = 0; step < this.operationSteps; step++) { RecipeStepPanel stepPanel = (RecipeStepPanel) this.operationLines.getComponent(step); + int laneIndex = step + 1; + stepPanel.setTitle("Lane " + laneIndex); + stepPanel.clearComment(); stepPanel.clearOperations(); } } diff --git a/src/main/java/de/usd/cstchef/view/RecipeStepPanel.java b/src/main/java/de/usd/cstchef/view/RecipeStepPanel.java index 89b79da..f479220 100644 --- a/src/main/java/de/usd/cstchef/view/RecipeStepPanel.java +++ b/src/main/java/de/usd/cstchef/view/RecipeStepPanel.java @@ -3,14 +3,24 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; @@ -27,6 +37,13 @@ public class RecipeStepPanel extends JPanel { private JPanel operationsLine; private GridBagConstraints addContraints; private ChangeListener changeListener; + private JTextField contentTextField; + + private String comment; + private JButton commentBtn; + + private static ImageIcon commentIcon = new ImageIcon(Operation.class.getResource("/comment.png")); + private static ImageIcon noCommentIcon = new ImageIcon(Operation.class.getResource("/no_comment.png")); public RecipeStepPanel(String title, ChangeListener changelistener) { this.changeListener = changelistener; @@ -41,12 +58,39 @@ public RecipeStepPanel(String title, ChangeListener changelistener) { CompoundBorder border = new CompoundBorder(lineBorder, margin); headerBox.setBorder(border); - JTextField contentTextField = new JTextField(); + contentTextField = new JTextField(); contentTextField.setBorder(null); contentTextField.setBackground(new Color(0, 0, 0, 0)); contentTextField.setText(title); + contentTextField.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + String newTitle = JOptionPane.showInputDialog("Edit title:", getTitle()); + contentTextField.setText(newTitle.length() <= 50 ? newTitle : getTitle()); + setTitle(newTitle.length() <= 50 ? newTitle : getTitle()); // lane name should be leq 50 chars + } + }); headerBox.add(contentTextField); + JPanel panel = new JPanel(); + panel.setBackground(new Color(0, 0, 0, 0)); // transparent + + commentBtn = createIconButton(noCommentIcon); + + commentBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + commentBtn.setToolTipText(getComment()); + String comment = JOptionPane.showInputDialog("Edit comment:", commentBtn.getToolTipText()); + commentBtn.setToolTipText(comment); + setComment(comment); + ImageIcon newIcon = comment.isEmpty() ? RecipeStepPanel.noCommentIcon : RecipeStepPanel.commentIcon; + commentBtn.setIcon(newIcon); + } + }); + + panel.add(commentBtn); + headerBox.add(panel); + this.add(headerBox, BorderLayout.NORTH); // body @@ -124,4 +168,41 @@ public void clearOperations() { operationsLine.repaint(); this.changeListener.stateChanged(new ChangeEvent(this)); } + + public String getTitle() { + return contentTextField.getText(); + } + + public void setTitle(String title) { + contentTextField.setText(title); + } + + private JButton createIconButton(ImageIcon icon) { + JButton btn = new JButton(); + btn.setBorder(BorderFactory.createEmptyBorder()); + btn.setIcon(icon); + btn.setContentAreaFilled(false); + btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + btn.setAlignmentX(Component.RIGHT_ALIGNMENT); + + return btn; + } + + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + if(comment != null) { + this.comment = comment; + commentBtn.setIcon(RecipeStepPanel.commentIcon); + commentBtn.setToolTipText(comment); + } + } + + public void clearComment() { + this.comment = ""; + commentBtn.setToolTipText(""); + commentBtn.setIcon(RecipeStepPanel.noCommentIcon); + } }