diff --git a/sample.xml b/sample.xml index 98318a6f..27a84a6b 100644 --- a/sample.xml +++ b/sample.xml @@ -1,6 +1,7 @@ - + Spacely Sprockets @@ -24,11 +25,10 @@ The EventIDs for the consumers + + Blob to see if works in group element + - - Sample bit variable - Doesn't do anything - Sample integer variable Doesn't do anything @@ -36,18 +36,57 @@ 999 12 + + Sample integer slider + Doesn't do anything either + 0 + 1000 + 12 + + + + - Reset - Controls reloading and clearing node memory. Board must be restarted for this to take effect. + Reset via Map + + Controls reloading and clearing node memory. + Board must be restarted for this to take effect. + - 85(No reset) - 0Reset all to defaults - 170Reset just EventIDs to defaults + 0No reset (0) + 85Reset just EventIDs to defaults (85) + 170Reset all to defaults (170) + + Reset Directly + + This accesses the same memory location as the + mapped variable just above. + + + + Factory Reset via address 129 + Perform Reset + Do a factory reset? + 2 + + + Reboot via address 129 + Perform Reboot + + 9 + + + Blob defined at address 131 + + + Yet Another Reset + This should be stored at address 141. + diff --git a/sample2.xml b/sample2.xml index ae66532b..678f1e04 100644 --- a/sample2.xml +++ b/sample2.xml @@ -35,24 +35,6 @@ - - Regular bit variable - Demonstrate how a standard bit (boolean) variable can be shown - - - Bit variable with named states - Demonstrate how a map relabels the states of a bit (boolean) variable - - - true - Lit - - - false - Not Lit - - - Resets diff --git a/src/org/openlcb/cdi/CdiRep.java b/src/org/openlcb/cdi/CdiRep.java index 15d9c470..59540475 100644 --- a/src/org/openlcb/cdi/CdiRep.java +++ b/src/org/openlcb/cdi/CdiRep.java @@ -79,25 +79,52 @@ public static interface Map { * @return a list of all user-visible values. */ public java.util.List getValues(); + + /** + * Add an item to the map. Useful if e.g. a non-mapped + * value is found in a location. + */ + public void addItemToMap(String key, String entry); } public static interface EventID extends Item { } + public static interface IntegerRep extends Item { public int getDefault(); public long getMin(); public long getMax(); public int getSize(); + + public boolean isSliderHint(); + public int getSliderDivisions(); } + public static interface BitRep extends Item { public boolean getDefault(); public int getSize(); } + + public static interface UnknownRep extends Item { + public boolean getDefault(); + + public int getSize(); + } + public static interface StringRep extends Item { // "String" causes too many name conflicts public int getSize(); } + public static interface ActionButtonRep extends Item { + + public long getValue(); + public String getButtonText(); + public String getDialogText(); + + public int getSize(); + } + } diff --git a/src/org/openlcb/cdi/impl/ConfigRepresentation.java b/src/org/openlcb/cdi/impl/ConfigRepresentation.java index 47456b87..1c332dff 100644 --- a/src/org/openlcb/cdi/impl/ConfigRepresentation.java +++ b/src/org/openlcb/cdi/impl/ConfigRepresentation.java @@ -253,6 +253,10 @@ private long processGroup(String baseName, int segment, List items, entry = new EventEntry(name, (CdiRep.EventID) it, segment, origin); } else if (it instanceof CdiRep.StringRep) { entry = new StringEntry(name, (CdiRep.StringRep) it, segment, origin); + } else if (it instanceof CdiRep.ActionButtonRep) { + entry = new ActionButtonEntry(name, (CdiRep.ActionButtonRep) it, segment, origin); + } else if (it instanceof CdiRep.UnknownRep) { + entry = new UnknownEntry(name, (CdiRep.UnknownRep) it, segment, origin); } else { logger.log(Level.SEVERE, "could not process CDI entry type of {0}", it); } @@ -303,12 +307,16 @@ public void visitEntry(CdiEntry e) { visitInt((IntegerEntry) e); } else if (e instanceof EventEntry) { visitEvent((EventEntry) e); + } else if (e instanceof ActionButtonEntry) { + visitActionButton((ActionButtonEntry) e); } else if (e instanceof GroupRep) { visitGroupRep((GroupRep) e); } else if (e instanceof GroupEntry) { visitGroup((GroupEntry) e); } else if (e instanceof SegmentEntry) { visitSegment((SegmentEntry) e); + } else if (e instanceof UnknownEntry) { + visitUnknown((UnknownEntry) e); } else if (e instanceof CdiContainer) { visitContainer((CdiContainer) e); } else { @@ -331,6 +339,10 @@ public void visitEvent(EventEntry e) { visitLeaf(e); } + public void visitActionButton(ActionButtonEntry e) { + visitLeaf(e); + } + public void visitGroupRep(GroupRep e) { visitContainer(e); } @@ -343,6 +355,10 @@ public void visitSegment(SegmentEntry e) { visitContainer(e); } + public void visitUnknown(UnknownEntry e) { + visitLeaf(e); + } + public void visitContainer(CdiContainer c) { for (CdiEntry e : c.getEntries()) { visitEntry(e); @@ -700,4 +716,97 @@ public void setValue(String value) { } } + /** + * Represents an unknown variable, perhaps due to a more-recent schema + */ + public class UnknownEntry extends CdiEntry { + public CdiRep.UnknownRep rep; + + UnknownEntry(String name, CdiRep.UnknownRep rep, int segment, long origin) { + this.key = name; + this.space = segment; + this.origin = origin; + this.rep = rep; + this.size = rep.getSize(); + } + + @Override + public CdiRep.Item getCdiItem() { + return rep; + } + + @Override + protected void updateVisibleValue() { + lastVisibleValue = getValue(); + } + + @Override + public boolean isNullTerminated() { + return size > 64; + } + + public String getValue() { + MemorySpaceCache cache = getCacheForSpace(space); + byte[] b = cache.read(origin, size); + if (b == null) return null; + // We search for a terminating null byte and clip the string there. + int len = 0; + while (len < b.length && b[len] != 0) ++len; + byte[] rep = new byte[len]; + System.arraycopy(b, 0, rep, 0, len); + String ret = new String(rep, UTF8); + return ret; + } + + public void setValue(String value) { + MemorySpaceCache cache = getCacheForSpace(space); + byte[] f; + f = value.getBytes(UTF8); + byte[] b = new byte[Math.min(size, f.length + 1)]; + System.arraycopy(f, 0, b, 0, Math.min(f.length, b.length - 1)); + cache.write(this.origin, b, this); + } + } + + /** + * Represents an action button variable. + */ + public class ActionButtonEntry extends CdiEntry { + public CdiRep.ActionButtonRep rep; + + ActionButtonEntry(String name, CdiRep.ActionButtonRep rep, int segment, long origin) { + this.key = name; + this.space = segment; + this.origin = origin; + this.rep = rep; + this.size = rep.getSize(); + } + + @Override + public CdiRep.Item getCdiItem() { + return rep; + } + + @Override + protected void updateVisibleValue() { + // does nothing in this class + } + + public long getValue() { + // should not be called + logger.log(Level.SEVERE, "ActionButtonEntry.getValue should not be called"); + return -1; + } + + public void setValue(long value) { + MemorySpaceCache cache = getCacheForSpace(space); + byte[] b = new byte[size]; + for (int i = size - 1; i >= 0; --i) { + b[i] = (byte)(value & 0xff); + value >>= 8; + } + cache.write(origin, b, this); + } + } + } diff --git a/src/org/openlcb/cdi/jdom/JdomCdiRep.java b/src/org/openlcb/cdi/jdom/JdomCdiRep.java index 5e8d960a..1c69b9cc 100644 --- a/src/org/openlcb/cdi/jdom/JdomCdiRep.java +++ b/src/org/openlcb/cdi/jdom/JdomCdiRep.java @@ -102,11 +102,33 @@ public java.util.List getItems() { for (int i = 0; i getValues() { } return list; } - + + public void addItemToMap(String key, String entry) { + Element relation = new Element("relation"); + Element property = new Element("property"); + Element value = new Element("value"); + + property.addContent(key); + value.addContent(entry); + relation.addContent(property); + relation.addContent(value); + map.addContent(relation); + } + Element map; } @@ -229,7 +263,6 @@ public int getOffset() { public int getIndexInParent() { return e.getParent().indexOf(e); } - } public static class Group extends Nested implements CdiRep.Group { @@ -400,6 +433,47 @@ public int getSize() { else return a.getIntValue(); } catch (org.jdom2.DataConversionException e1) { return 0; } } + + @Override + public boolean isSliderHint() { + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element slider = hints.getChild("slider"); + if (slider == null) return false; + return true; + } + + @Override + public int getSliderDivisions() { + Element hints = e.getChild("hints"); + if (hints == null) return 1; + Element slider = hints.getChild("slider"); + if (slider == null) return 1; + Attribute divisions = slider.getAttribute("divisions"); + if (divisions == null) return 1; + try { + return divisions.getIntValue(); + } catch (org.jdom2.DataConversionException e) { return 1; } + } + + } + + public static class UnknownRep extends Item implements CdiRep.UnknownRep { + UnknownRep(Element e) { super(e); } + + @Override + public boolean getDefault() { return false; } + + @Override + public int getSize() { + Attribute a = e.getAttribute("size"); + try { + // the `size` attribute is required to allocate space, so the + // default value set here is zero + if (a == null) return 0; + else return a.getIntValue(); + } catch (org.jdom2.DataConversionException e1) { return 0; } + } } public static class BitRep extends Item implements CdiRep.BitRep { @@ -432,5 +506,56 @@ public int getSize() { } } + public static class ActionButtonRep extends Item implements CdiRep.ActionButtonRep { + + ActionButtonRep(Element e) { super(e); } + + @Override + public int getSize() { + Attribute a = e.getAttribute("size"); + try { + if (a == null) return 1; + else return a.getIntValue(); + } catch (org.jdom2.DataConversionException e1) { return 0; } + } + + @Override + public long getValue() { + Element target = e.getChild("value"); + if (target != null) { + String text = target.getTextNormalize(); + try { + return Integer.valueOf(text); + } catch (NumberFormatException ex) { + logger.severe("Invalid content for value element: "+text); + // and return the default value from length + } + } + // otherwise, return default value of 0 + return 0; + } + + @Override + public String getButtonText() { + Element target = e.getChild("buttonText"); + if (target != null) { + return target.getTextNormalize(); + } + // otherwise, return empty value + return ""; + } + + @Override + public String getDialogText() { + Element target = e.getChild("dialogText"); + if (target != null) { + return target.getTextNormalize(); + } + // otherwise, return empty value + return ""; + } + + } + Element root; } diff --git a/src/org/openlcb/cdi/swing/CdiPanel.java b/src/org/openlcb/cdi/swing/CdiPanel.java index 8677dcec..f1ff00ea 100644 --- a/src/org/openlcb/cdi/swing/CdiPanel.java +++ b/src/org/openlcb/cdi/swing/CdiPanel.java @@ -78,6 +78,7 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; +import javax.swing.JSlider; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; @@ -219,6 +220,7 @@ public void initComponents(ConfigRepresentation rep, GuiItemFactory factory) { bottomPanel = new JPanel(); //buttonBar.setAlignmentX(Component.LEFT_ALIGNMENT); bottomPanel.setLayout(new WrapLayout()); + JButton bb = new JButton("Refresh All"); bb.setToolTipText("Discards all changes and loads the freshest value from the hardware for all entries."); bb.addActionListener(actionEvent -> reloadAll()); @@ -1375,6 +1377,12 @@ public void visitString(ConfigRepresentation.StringEntry e) { super.visitString(e); } + @Override + public void visitUnknown(ConfigRepresentation.UnknownEntry e) { + currentLeaf = new UnknownPane(e); + super.visitUnknown(e); + } + @Override public void visitInt(ConfigRepresentation.IntegerEntry e) { currentLeaf = new IntPane(e); @@ -1387,6 +1395,12 @@ public void visitEvent(ConfigRepresentation.EventEntry e) { super.visitEvent(e); } + @Override + public void visitActionButton(ConfigRepresentation.ActionButtonEntry e) { + currentLeaf = new ActionButtonPane(e); + super.visitActionButton(e); + } + @Override public void visitLeaf(ConfigRepresentation.CdiEntry e) { allEntries.add(currentLeaf); @@ -1852,7 +1866,7 @@ private abstract class EntryPane extends JPanel { setAlignmentX(Component.LEFT_ALIGNMENT); String name = (item.getName() != null ? item.getName() : defaultName); setBorder(BorderFactory.createTitledBorder(name)); - + createDescriptionPane(this, item.getDescription()); p3 = new JPanel(); @@ -1982,25 +1996,28 @@ public void run() { }); entry.fireUpdate(); - JButton b; - b = factory.handleReadButton(new JButton("Refresh")); // was: read - b.addActionListener(new java.awt.event.ActionListener() { - @Override - public void actionPerformed(java.awt.event.ActionEvent e) { - entry.reload(); - } - }); - p3.add(b); - - writeButton = factory.handleWriteButton(new JButton("Write")); - writeButton.addActionListener(new java.awt.event.ActionListener() { - @Override - public void actionPerformed(java.awt.event.ActionEvent e) { - writeDisplayTextToNode(); - } - }); - p3.add(writeButton); + if (! (textComponent instanceof JButton ) // Buttons write themselves + && ! (textComponent instanceof JLabel ) ) { // labels don't need to write + JButton b; + b = factory.handleReadButton(new JButton("Refresh")); // was: read + b.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + entry.reload(); + } + }); + p3.add(b); + writeButton = factory.handleWriteButton(new JButton("Write")); + writeButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + writeDisplayTextToNode(); + } + }); + p3.add(writeButton); + } + additionalButtons(); p3.add(Box.createHorizontalGlue()); @@ -2334,7 +2351,8 @@ private void releaseListener() { private class IntPane extends EntryPane { JTextField textField = null; - JComboBox box = null; + JComboBox box = null; + JSlider slider = null; CdiRep.Map map = null; private final ConfigRepresentation.IntegerEntry entry; @@ -2355,16 +2373,48 @@ public java.awt.Dimension getMaximumSize() { }; textComponent = box; } else { - // map not present, just an entry box - textField = new JTextField(24) { - public java.awt.Dimension getMaximumSize() { - return getPreferredSize(); + // map not present - is it a slider? + if (entry.rep.isSliderHint()) { + // display a slider + slider = new JSlider((int)entry.rep.getMin(), (int)entry.rep.getMax()); + if (entry.rep.getSliderDivisions() > 1) { + // display divisions on the slider + int divisionSpacing = + ((int)(entry.rep.getMax()-entry.rep.getMin()))/entry.rep.getSliderDivisions(); + slider.setMajorTickSpacing(divisionSpacing); + slider.setLabelTable(slider.createStandardLabels(divisionSpacing)); + slider.setPaintTicks(true); + slider.setPaintLabels(true); } - }; - textComponent = textField; - textField.setToolTipText("Signed integer from " - +entry.rep.getMin()+" to "+entry.rep.getMax() - +" ("+entry.size+" bytes)"); + textComponent = slider; + if (entry.rep.getMin() < 0) { + slider.setToolTipText("Signed integer from " + +entry.rep.getMin()+" to "+entry.rep.getMax() + +" ("+entry.size+" bytes)"); + } else { + slider.setToolTipText("Unsigned integer from " + +entry.rep.getMin()+" to "+entry.rep.getMax() + +" ("+entry.size+" bytes)"); + } + + } else { + // display an entry box + textField = new JTextField(24) { + public java.awt.Dimension getMaximumSize() { + return getPreferredSize(); + } + }; + textComponent = textField; + if (entry.rep.getMin() < 0) { + textField.setToolTipText("Signed integer from " + +entry.rep.getMin()+" to "+entry.rep.getMax() + +" ("+entry.size+" bytes)"); + } else { + textField.setToolTipText("Unsigned integer from " + +entry.rep.getMin()+" to "+entry.rep.getMax() + +" ("+entry.size+" bytes)"); + } + } } init(); @@ -2375,8 +2425,11 @@ protected void writeDisplayTextToNode() { long value; if (textField != null) { value = Long.parseLong(textField.getText()); + } else if (slider != null) { + // get value from current slider position + value = slider.getValue(); } else { - // have to get key from stored value + // have to get key from stored map value String entry = (String) box.getSelectedItem(); String key = map.getKey(entry); value = Long.parseLong(key); @@ -2389,12 +2442,27 @@ protected void writeDisplayTextToNode() { @Override protected void updateDisplayText(@NonNull String value) { if (textField != null) textField.setText(value); - if (box != null) box.setSelectedItem(value); + if (slider != null) slider.setValue(Integer.parseInt(value)); + if (box != null) { + // check to see if item exists + box.setSelectedItem(value); + if (! box.getSelectedItem().equals(value)) { + // not present per-se, see if need to add as reserved? + String newValue = "Reserved value: "+value; + box.setSelectedItem(newValue); + if ( ! box.getSelectedItem().equals(newValue)) { + box.addItem(newValue); + box.setSelectedItem(newValue); + map.addItemToMap(newValue, value); + } + } + } } @NonNull @Override protected String getDisplayText() { + if (slider != null) return ""+slider.getValue(); String s = (box == null) ? (String) textField.getText() : (String) box.getSelectedItem(); return s == null ? "" : s; @@ -2409,6 +2477,8 @@ protected String getDisplayText() { */ @NonNull protected String getCurrentValue() { + if (slider != null) return ""+slider.getValue(); + String s; if (box==null) { s = (String) textField.getText(); @@ -2418,7 +2488,6 @@ protected String getCurrentValue() { } return s == null ? "" : s; } - boolean isDataInvalid() { try { @@ -2448,9 +2517,9 @@ void updateWriteButton() { writeButton.setEnabled( ! isDataInvalid()); } - } + // Define font to be used in certain types of StringPanes Font textAreaFont; { Font existingFont = UIManager.getFont("TextArea.font"); @@ -2458,7 +2527,8 @@ void updateWriteButton() { textAreaFont = new Font(Font.MONOSPACED, Font.PLAIN, size); } - private class StringPane extends EntryPane { + static final int MAX_SINGLE_LINE_ENTRY = 64; // somewhat arbitrary max length of single-line entry + private class StringPane extends EntryPane { JTextComponent textField; private final ConfigRepresentation.StringEntry entry; @@ -2476,8 +2546,8 @@ public void insertString(int offset, String str, AttributeSet a) throws BadLocat } }; - if (entry.size <= 64) { // somewhat arbitrary maximum length of single-line entry - + if (entry.size <= MAX_SINGLE_LINE_ENTRY) { + // This case is a single line in a JTextField JTextField jtf = new JTextField(doc, "", entry.size-1) { // -1 for trailing zero public Dimension getMaximumSize() { return getPreferredSize(); @@ -2485,6 +2555,7 @@ public Dimension getMaximumSize() { }; jtf.setFont(textAreaFont); jtf = factory.handleStringValue(jtf); + jtf.getDocument().putProperty("filterNewlines", Boolean.FALSE); // needed so 0x0A doesn't become space textField = jtf; } else { @@ -2522,6 +2593,86 @@ protected String getDisplayText() { } } + private class UnknownPane extends EntryPane { + private final ConfigRepresentation.UnknownEntry entry; + + UnknownPane(ConfigRepresentation.UnknownEntry e) { + super(e, "Unknown"); + this.entry = e; + + textComponent = new JLabel("Unknown Entry in the CDI Information"); + textComponent.setToolTipText("Unknown element, perhaps from a later version of the CDI format?"); + init(); + } + + @Override + protected void writeDisplayTextToNode() { + } + + @Override + protected void updateDisplayText(@NonNull String value) { + } + + @NonNull + @Override + protected String getDisplayText() { + return "Unknown Element"; + } + } + + private class ActionButtonPane extends EntryPane { + JButton actionButton; + private final ConfigRepresentation.ActionButtonEntry entry; + + ActionButtonPane(ConfigRepresentation.ActionButtonEntry e) { + super(e, "Action"); + this.entry = e; + + actionButton = new JButton(e.rep.getButtonText()); + textComponent = actionButton; + textComponent.setToolTipText("Writes to node when pressed."); + actionButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + writeDisplayTextToNode(); + } + }); + + init(); + } + + @Override + protected void writeDisplayTextToNode() { + if (entry.rep.getDialogText() == null || entry.rep.getDialogText().isEmpty()) { + entry.setValue((long)(entry.rep.getValue())); + _changeMade = true; + notifyTabColorRefresh(); + } else { + // first, show a dialog box + int result = javax.swing.JOptionPane.showConfirmDialog(null, + entry.rep.getDialogText(),"",javax.swing.JOptionPane.OK_CANCEL_OPTION); + // if not OK, silently skip; if OK, act + if (result == 0) { + entry.setValue((long)(entry.rep.getValue())); + System.out.println(entry.rep.getValue()); + _changeMade = true; + notifyTabColorRefresh(); + } + } + } + + @Override + protected void updateDisplayText(@NonNull String value) { + // does nothing + } + + @NonNull + @Override + protected String getDisplayText() { + return entry.rep.getButtonText(); + } + } + /** * Handle GUI hook requests if needed *