diff --git a/sample.xml b/sample.xml index dc327a8f..1e49fc12 100644 --- a/sample.xml +++ b/sample.xml @@ -8,32 +8,78 @@ Model 123 Uniblab EC 415 1.2.3.4 + Link to OpenLCB.org documentation Size8 cm by 12 cm - + Link to OpenLCB.org documentation + Produced Events The EventIDs for the producers + Link to OpenLCB.org documentation + + + + + 1 + 250 + 12 + + + + + + + Status Field + As first string, this content will appear in the tab + + - + Consumed Events The EventIDs for the consumers + + Blob to see if works in group element - + Float to see if works in group element Int of size 4 so that each group is 32 long + + Hideable and Hidden Nested Group + + + + + + + Hideable and Not Hidden Nested Group + + + + + + + Non-hideable Nested Group + + + Sample integer variable @@ -42,14 +88,28 @@ 999 12 + + Sample float variable + Doesn't do anything + 1 + 999 + 12 + + + Same float variable + Overlaps previous + 0 + 10000 + 12 + Sample integer slider Doesn't do anything either - 0 - 1000 + 1 + 250 12 - + @@ -59,7 +119,27 @@ 1000 12 - + + + + + Sample integer slider with view + And another + 1 + 250 + 12 + + + + + + Immediate-write integer slider with view + You guessed it! + 0 + 1000 + 12 + + @@ -72,10 +152,13 @@ Board must be restarted for this to take effect. - 0No reset (0) + 10No reset (10) 85Reset just EventIDs to defaults (85) 170Reset all to defaults (170) + + + Reset Directly diff --git a/src/org/openlcb/ProducerConsumerEventReportMessage.java b/src/org/openlcb/ProducerConsumerEventReportMessage.java index e9ac7278..5c0f7739 100644 --- a/src/org/openlcb/ProducerConsumerEventReportMessage.java +++ b/src/org/openlcb/ProducerConsumerEventReportMessage.java @@ -88,9 +88,19 @@ public byte[] getPayloadArray() { @Override public String toString() { - return super.toString() - +" Producer/Consumer Event Report "+eventID.toString() - +" payload of "+getPayloadSize(); + String retval = " Producer/Consumer Event Report "+eventID.toString(); + + if ( getPayloadSize() > 0 ) { + retval = retval + " payload of "+getPayloadSize()+" : "; + int n = getPayloadSize(); + boolean first = true; + for (byte data : payload) { + if (!first) retval = retval + "."; + retval = retval + Integer.toHexString((int)(data&0xFF)).toUpperCase(); + first = false; + } + } + return retval; } public boolean equals(Object o) { diff --git a/src/org/openlcb/ProtocolIdentification.java b/src/org/openlcb/ProtocolIdentification.java index ee19f26a..2cdda57e 100644 --- a/src/org/openlcb/ProtocolIdentification.java +++ b/src/org/openlcb/ProtocolIdentification.java @@ -105,7 +105,7 @@ public List getProtocolNames() { * @param protocol enum representing the protocol bit to test * @return true if protocol is supported, false otherwise. */ - boolean hasProtocol(Protocol protocol) { + public boolean hasProtocol(Protocol protocol) { return protocol.supports(value); } } diff --git a/src/org/openlcb/cdi/CdiRep.java b/src/org/openlcb/cdi/CdiRep.java index e751e541..6864cb29 100644 --- a/src/org/openlcb/cdi/CdiRep.java +++ b/src/org/openlcb/cdi/CdiRep.java @@ -15,6 +15,8 @@ public static interface Identification { public String getModel(); public String getHardwareVersion(); public String getSoftwareVersion(); + public String getLinkText(); + public String getLinkURL(); public Map getMap(); } @@ -29,6 +31,8 @@ public static interface Segment { public String getName(); public String getDescription(); + public String getLinkText(); + public String getLinkURL(); public Map getMap(); public int getIndexInParent(); } @@ -44,7 +48,12 @@ public static interface Item { public static interface Group extends Item { public java.util.List getItems(); public int getReplication(); + public String getLinkText(); + public String getLinkURL(); public String getRepName(int index, int replications); + public boolean isHideable(); + public boolean isHidden(); + public boolean isReadOnly(); } public static interface Map { @@ -102,8 +111,12 @@ public static interface IntegerRep extends Item { // Should the slider itself immediately write its value on change? public boolean isSliderImmediate(); // Optionally specifies the 'distance' between tick marks on the slider. - // If 0 (default value), don't show tick marks. + // If 0 (default value) or 1, don't show tick marks. public int getSliderTickSpacing(); + // Optionally specifies if the slider value should be shown in text box + public boolean isSliderShowValue(); + // Did the CDI content hint that this value should be presented as a radio button? + public boolean isRadioButtonHint(); } public static interface FloatRep extends Item { diff --git a/src/org/openlcb/cdi/impl/ConfigRepresentation.java b/src/org/openlcb/cdi/impl/ConfigRepresentation.java index 7dd14342..cc22cadd 100644 --- a/src/org/openlcb/cdi/impl/ConfigRepresentation.java +++ b/src/org/openlcb/cdi/impl/ConfigRepresentation.java @@ -412,6 +412,10 @@ public void reload() { MemorySpaceCache cache = getCacheForSpace(space); cache.reload(origin, size, isNullTerminated()); } + + boolean flaggedReadOnly = false; + public boolean isFlaggedReadOnly() { return flaggedReadOnly; } + public void setFlaggedReadOnly(boolean state) {flaggedReadOnly = state; } } public class Root implements CdiContainer { @@ -574,6 +578,21 @@ public class GroupEntry extends GroupBase { } } } + + public boolean isHideable() { + return group.isHideable(); + } + + public boolean isHidden() { + return group.isHidden(); + } + + /** + * Does this entry carry the readOnly hint? + */ + public boolean isReadOnlyConfigured() { + return group.isReadOnly(); + } } /** diff --git a/src/org/openlcb/cdi/jdom/JdomCdiRep.java b/src/org/openlcb/cdi/jdom/JdomCdiRep.java index 261c78ae..86f8569f 100644 --- a/src/org/openlcb/cdi/jdom/JdomCdiRep.java +++ b/src/org/openlcb/cdi/jdom/JdomCdiRep.java @@ -6,6 +6,7 @@ import java.util.logging.Logger; import org.jdom2.Attribute; +import org.jdom2.DataConversionException; import org.jdom2.Element; import org.openlcb.cdi.CdiRep; @@ -49,6 +50,22 @@ public String getSoftwareVersion() { return c.getText(); } + @Override + public String getLinkText() { + Element c = id.getChild("link"); + if (c == null) return null; + return c.getText(); + } + + @Override + public String getLinkURL() { + Element c = id.getChild("link"); + if (c == null) return null; + Attribute a = c.getAttribute("ref"); + if (a == null) return null; + return a.getValue(); + } + @Override public Map getMap() { return new Map(id.getChild("map")); @@ -127,6 +144,8 @@ public java.util.List getItems() { case "repname": case "name": case "description": + case "link": + case "hints": break; default: list.add(new UnknownRep(element)); @@ -164,6 +183,23 @@ public int getOrigin() { else return a.getIntValue(); } catch (org.jdom2.DataConversionException e1) { return 0; } } + + @Override + public String getLinkText() { + Element c = e.getChild("link"); + if (c == null) return null; + return c.getText(); + } + + @Override + public String getLinkURL() { + Element c = e.getChild("link"); + if (c == null) return null; + Attribute a = c.getAttribute("ref"); + if (a == null) return null; + return a.getValue(); + } + } public static class Map implements CdiRep.Map { @@ -289,6 +325,65 @@ public int getOffset() { } catch (org.jdom2.DataConversionException e1) { return 0; } } + @Override + public String getLinkText() { + Element c = e.getChild("link"); + if (c == null) return null; + return c.getText(); + } + + @Override + public String getLinkURL() { + Element c = e.getChild("link"); + if (c == null) return null; + Attribute a = c.getAttribute("ref"); + if (a == null) return null; + return a.getValue(); + } + + @Override + public boolean isHideable() { + // defaults to false + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element visibility = hints.getChild("visibility"); + if (visibility == null) return false; + Attribute a = visibility.getAttribute("hideable"); + if (a == null) return false; + try { + boolean value = a.getBooleanValue(); + return value; + } catch (DataConversionException ex) { + return false; + } + } + + @Override + public boolean isHidden() { + // defaults to false + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element visibility = hints.getChild("visibility"); + if (visibility == null) return false; + Attribute a = visibility.getAttribute("hidden"); + if (a == null) return false; + try { + boolean value = a.getBooleanValue(); + return value; + } catch (DataConversionException ex) { + return false; + } + } + + @Override + public boolean isReadOnly() { + // defaults to false + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element readOnly = hints.getChild("readOnly"); + return readOnly != null; + } + /** * Provides the name for this replication. See the CDI TN for the * algorithm being used. @@ -455,8 +550,12 @@ public boolean isSliderImmediate() { if (slider == null) return false; Attribute immediate = slider.getAttribute("immediate"); if (immediate == null) return false; - if (! immediate.getValue().toLowerCase().equals("yes")) return false; - return true; + try { + boolean value = immediate.getBooleanValue(); + return value; + } catch (DataConversionException ex) { + return false; + } } @Override @@ -472,6 +571,31 @@ public int getSliderTickSpacing() { } catch (org.jdom2.DataConversionException e) { return 0; } } + @Override + public boolean isSliderShowValue() { + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element slider = hints.getChild("slider"); + if (slider == null) return false; + Attribute showValue = slider.getAttribute("showValue"); + if (showValue == null) return false; + try { + boolean value = showValue.getBooleanValue(); + return value; + } catch (DataConversionException ex) { + return false; + } + } + + @Override + public boolean isRadioButtonHint() { + Element hints = e.getChild("hints"); + if (hints == null) return false; + Element radiobutton = hints.getChild("radiobutton"); + if (radiobutton == null) return false; + return true; + } + } diff --git a/src/org/openlcb/cdi/swing/CdiPanel.java b/src/org/openlcb/cdi/swing/CdiPanel.java index d8291376..47a38d6f 100644 --- a/src/org/openlcb/cdi/swing/CdiPanel.java +++ b/src/org/openlcb/cdi/swing/CdiPanel.java @@ -32,7 +32,10 @@ import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; @@ -49,6 +52,7 @@ import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,10 +65,12 @@ import java.util.regex.Pattern; import javax.swing.AbstractAction; +import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.ComboBoxModel; import javax.swing.InputVerifier; import javax.swing.JButton; @@ -84,10 +90,13 @@ import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.KeyStroke; +import javax.swing.JRadioButton; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.WindowConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.AttributeSet; @@ -110,7 +119,7 @@ * * Works with a CDI reader. * - * @author Bob Jacobsen Copyright 2011 + * @author Bob Jacobsen Copyright 2011, 2024 * @author Paul Bender Copyright 2016 * @author Balazs Racz Copyright 2016 * @author Pete Cressman Copyright 2020 @@ -1098,17 +1107,31 @@ public void visitGroup(ConfigRepresentation.GroupEntry e) { } factory.handleGroupPaneStart(groupPane); + if (e.isReadOnlyConfigured()) { + // mark the direct children of these, except groups, as readOnly + // when the direct child is a grouprep (repetitions > 1), mark those children + for (ConfigRepresentation.CdiEntry entry : e.getEntries()) { + if (! (entry instanceof ConfigRepresentation.GroupEntry) ) { + entry.setFlaggedReadOnly(true); + if (entry instanceof ConfigRepresentation.GroupRep ) { + for (ConfigRepresentation.CdiEntry subentry : ((ConfigRepresentation.GroupRep)entry).getEntries()) { + subentry.setFlaggedReadOnly(true); + } + } + } + } + } super.visitGroup(e); factory.handleGroupPaneEnd(groupPane); - if (groupPane.getComponentCount() > 0) { - if (oldPane instanceof SegmentPane) { - // we make toplevel groups collapsible. + if (groupPane.getComponentCount() > 0) { // empty groups are not collabsible + if (oldPane instanceof SegmentPane || e.isHideable()) { // we only make toplevel groups collapsible unless hint requests groupPane.setBorder(null); CollapsiblePanel cPanel = new CollapsiblePanel(groupPane.getName(), groupPane); // cPanel.setBorder(BorderFactory.createLineBorder(java.awt.Color.RED)); //debugging cPanel.setAlignmentY(Component.TOP_ALIGNMENT); cPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + cPanel.setExpanded(!e.isHidden()); oldPane.add(cPanel); addNavigationActions(cPanel); } else { @@ -1559,7 +1582,7 @@ JPanel createIdentificationPane(CdiRep c) { JPanel p1 = new JPanel(); p.add(p1); - p1.setLayout(new util.javaworld.GridLayout2(4,2)); + p1.setLayout(new util.javaworld.GridLayout2(5,2)); p1.setAlignmentX(Component.LEFT_ALIGNMENT); p1.add(new JLabel("Manufacturer: ")); @@ -1574,6 +1597,10 @@ JPanel createIdentificationPane(CdiRep c) { p1.add(new JLabel("Software Version: ")); p1.add(new JLabel(id.getSoftwareVersion())); + if (id.getLinkText() != null && id.getLinkURL() != null) { + p1.add(new HtmlLabel(id.getLinkText(),id.getLinkURL())); + } + p1.setMaximumSize(p1.getPreferredSize()); // include map if present @@ -1629,7 +1656,8 @@ public class SegmentPane extends JPanel { //p.setBorder(BorderFactory.createTitledBorder(name)); createDescriptionPane(this, item.getDescription()); - + createLinkPane(this, item.segment.getLinkText(), item.segment.getLinkURL()); + // include map if present JPanel p2 = createPropertyPane(item.getMap()); if (p2 != null) p.add(p2); @@ -1655,6 +1683,12 @@ public Dimension getMaximumSize() { parent.add(area); } + void createLinkPane(JPanel parent, String text, String ref) { + if (text == null || ref == null) return; + parent.add(new HtmlLabel(text, ref)); + } + + private void addCopyPasteButtons(JPanel linePanel, JTextField textField) { final JButton b = new JButton("Copy"); final Color defaultColor = b.getBackground(); @@ -1858,6 +1892,7 @@ public class GroupPane extends JPanel { setName(name); createDescriptionPane(this, item.getDescription()); + createLinkPane(this, entry.group.getLinkText(), entry.group.getLinkURL()); // include map if present JPanel p2 = createPropertyPane(item.getMap()); @@ -1909,31 +1944,51 @@ void release() { protected void additionalButtons() {} protected void init() { + // a panel that may exist below the main component (textComponent) + // which, if present, gets the Refresh and Write buttons + JPanel subpanel = null; + if (textComponent instanceof JTextArea) { - JPanel lengthPanel = new JPanel(); + subpanel = new JPanel(); // include an auto-updating "remaining characters" field - lengthPanel.setLayout(new FlowLayout()); + subpanel.setLayout(new FlowLayout()); JLabel lengthLabel = new JLabel("Remaining characters: "); - lengthPanel.add(lengthLabel); + subpanel.add(lengthLabel); final JTextField countField = new JTextField(6); countField.setText(""+(entry.size-1)); - lengthPanel.add(countField); + subpanel.add(countField); lengthLabel.setFont(countAreaFont); countField.setFont(countAreaFont); JPanel combinedPanel = new JPanel(); combinedPanel.setLayout(new BoxLayout(combinedPanel, BoxLayout.Y_AXIS)); - combinedPanel.add(new JScrollPane(textComponent){ + JScrollPane spane = new JScrollPane(textComponent){ // Limit how small the layout will make the field public Dimension getMinimumSize() { Dimension superSize = super.getMinimumSize(); int width = superSize.width; - int height = Math.max(superSize.height, 200); + int height = Math.max(superSize.height, 50); return new Dimension(width, height); } - }); - combinedPanel.add(lengthPanel); + public Dimension getPreferredSize() { + Dimension superMin = super.getMinimumSize(); + Dimension superPref = super.getPreferredSize(); + int width = Math.max(superMin.width, superPref.width); + int height = Math.max(superMin.height, superPref.height); + return new Dimension(width, height); + } + public Dimension getMaximumSize() { + Dimension superMax = super.getMaximumSize(); + Dimension superPref = super.getPreferredSize(); + int width = Math.max(superMax.width, superPref.width); + int height = Math.max(superMax.height, superPref.height); + return new Dimension(width, height); + } + }; + spane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + combinedPanel.add(spane); + combinedPanel.add(subpanel); p3.add(combinedPanel); @@ -1966,6 +2021,8 @@ private void updateLength() { p3.add(textComponent); } textComponent.setMaximumSize(textComponent.getPreferredSize()); + + // Add color-setting listeners - this is here to avoid having lots of replicated code if (textComponent instanceof JTextComponent) { ((JTextComponent) textComponent).getDocument().addDocumentListener( new DocumentListener() { @@ -1989,20 +2046,8 @@ private void drawRed() { } } ); - } else if (textComponent instanceof JComboBox) { - ((JComboBox) textComponent).addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent actionEvent) { - updateColor(); - } - }); - } else if (textComponent instanceof JSlider) { - ((JSlider) textComponent).addChangeListener(new javax.swing.event.ChangeListener(){ - public void stateChanged(javax.swing.event.ChangeEvent e) { - updateColor(); - } - }); } + entryListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent propertyChangeEvent) { @@ -2037,16 +2082,27 @@ 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(); + if (subpanel != null ) { + subpanel.add(b); + } else { + p3.add(b); + } + + // write button is suppressed if flagged as read only + if (! entry.isFlaggedReadOnly()) { + writeButton = factory.handleWriteButton(new JButton("Write")); + writeButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + writeDisplayTextToNode(); + } + }); + if (subpanel != null ) { + subpanel.add(writeButton); + } else { + p3.add(writeButton); } - }); - p3.add(writeButton); + } } additionalButtons(); @@ -2168,6 +2224,7 @@ public void propertyChange(PropertyChangeEvent propertyChangeEvent) { if (eventTable != null) { add(eventNamesLabel); } + if (e.isFlaggedReadOnly()) textComponent.setEnabled(false); } /** @@ -2391,11 +2448,186 @@ private void releaseListener() { } } + // represent a slider with an optional text view + private class SliderWithView extends JPanel { + JSlider slider = null; + JTextField textField = null; + + SliderWithView(int min, int max, boolean showValue, int size) { + setLayout(new FlowLayout()); + + // define the slider + slider = new JSlider(min, max); + slider.setOpaque(true); // so you can color it + + // set a tooltip showing the valid range + if (min < 0) { + slider.setToolTipText("Signed integer from " + +min+" to "+max + +" ("+size+" bytes)"); + } else { + slider.setToolTipText("Unsigned integer from " + +min+" to "+max + +" ("+size+" bytes)"); + } + + add(slider); + + // optionally define the text field + if (showValue) { + textField = new JTextField(2+(int)Math.log10(Math.max(1., Math.abs(max)))) { + public java.awt.Dimension getMaximumSize() { + return getPreferredSize(); + } + }; + textField.setOpaque(true); // so you can color it + + // set a tooltip showing the valid range + if (min < 0) { + textField.setToolTipText("Signed integer from " + +min+" to "+max + +" ("+size+" bytes)"); + } else { + textField.setToolTipText("Unsigned integer from " + +min+" to "+max + +" ("+size+" bytes)"); + } + + // add a listener to the slider to fill this + slider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + textField.setText(""+slider.getValue()); + } + }); + + // Add listeners to set slider. Value is considered + // final when the field is exited or Enter is hit. + // We do this instead of listening for a value + // change to avoid a possible back-and-forth + // setting loop between the text field and slider. + textField.addFocusListener(new FocusListener(){ + public void focusLost(FocusEvent e) { + textToSlider(); + } + public void focusGained(FocusEvent e) { + } + }); + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent ke) { + if (ke.getKeyCode() == KeyEvent.VK_ENTER) { + textToSlider(); + } + } + }); + + add(textField); + } + } + + // setting background also colors slider + @Override + public void setBackground(Color color) { + // super.setBackground(..) would color whole block + if (slider != null) { + slider.setBackground(color); + } + if (textField != null) { + textField.setBackground(color); + } + } + + @Override + public void setEnabled(boolean state) { + super.setEnabled(state); + if (slider != null) { + slider.setEnabled(state); + } + if (textField != null) { + textField.setEnabled(state); + } + } + + // copies the textfield value to the slider, handling errors + void textToSlider() { + try { + int current = (int)Double.parseDouble(textField.getText().trim()); + slider.setValue(current); + } catch (NumberFormatException e) { + // don't set the value, load from current slider value + textField.setText(""+slider.getValue()); + } + } + } + + // represents a set of radio buttons + private class RadioButtonPane extends JPanel { + + CdiRep.Map map; + ButtonGroup group = new ButtonGroup(); + + RadioButtonPane(CdiRep.Map map, ActionListener action) { + this.map = map; + + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + // create the pane and fill with radio buttons + for (String v : map.getValues()) { + JRadioButton button = new JRadioButton(v); + button.setActionCommand(v); + add(button); + // add color listener + button.addActionListener(action); + + group.add(button); + } + } + + long getCurrentValue() { + return Long.parseLong(getCurrentValueString()); + } + + // value is a numeric string + void setCurrentValue(String value) { + String key = map.getKey(value); + + Enumeration e = group.getElements(); + while (e.hasMoreElements()) { + AbstractButton b = e.nextElement(); + if (b.getActionCommand().equals(value)) { + b.setSelected(true); + return; + } + } + // else set unselected + logger.log(Level.WARNING, "Value \""+value+"\" does not match a button value, taking 1st button"); + group.clearSelection(); + } + + String getCurrentValueString() { + String value = getDisplayText(); + if (map.getKey(value) != null) { + return map.getKey(value); + } else { + logger.severe("Value \""+value+"\" does not match a button name"); + return map.getKeys().get(0); + } + } + + String getDisplayText() { + if (group.getSelection() == null) { + return map.getValues().get(0); + } + return group.getSelection().getActionCommand(); + } + } + private class IntPane extends EntryPane { JTextField textField = null; JComboBox box = null; - JSlider slider = null; + SliderWithView sliderView = null; + RadioButtonPane radiobuttons = null; CdiRep.Map map = null; private final ConfigRepresentation.IntegerEntry entry; boolean suppressExternal = false; // used to suppress slider output when changed from read @@ -2410,32 +2642,52 @@ private class IntPane extends EntryPane { String[] labels; map = item.getMap(); if ((map != null) && (map.getKeys().size() > 0)) { - // map present, make selection box - box = new JComboBox(map.getValues().toArray(new String[]{""})) { - public java.awt.Dimension getMaximumSize() { - return getPreferredSize(); - } - }; - textComponent = box; + // map present, make selection box or radio buttons? + if (entry.rep.isRadioButtonHint()) { + ActionListener action = new ActionListener() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + updateColor(); + } + }; + radiobuttons = new RadioButtonPane(map, action); + textComponent = radiobuttons; + } else { + box = new JComboBox(map.getValues().toArray(new String[]{""})) { + public java.awt.Dimension getMaximumSize() { + return getPreferredSize(); + } + }; + + // add color listener + box.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + updateColor(); + } + }); + + textComponent = box; + } } else { // 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()); - slider.setOpaque(true); // so you can color it - if (entry.rep.getSliderTickSpacing() > 1) { + sliderView = new SliderWithView((int)entry.rep.getMin(), (int)entry.rep.getMax(), entry.rep.isSliderShowValue(), entry.size); + + if (entry.rep.getSliderTickSpacing() > 0) { // default is zero // display divisions on the slider - slider.setMajorTickSpacing(entry.rep.getSliderTickSpacing()); - slider.setLabelTable(slider.createStandardLabels(entry.rep.getSliderTickSpacing())); - slider.setPaintTicks(true); - slider.setPaintLabels(true); + sliderView.slider.setMajorTickSpacing(entry.rep.getSliderTickSpacing()); + sliderView.slider.setLabelTable(sliderView.slider.createStandardLabels(entry.rep.getSliderTickSpacing())); + sliderView.slider.setPaintTicks(true); + sliderView.slider.setPaintLabels(true); } // (optionally) listen for changes and immediately write if (entry.rep.isSliderImmediate()) { - slider.addChangeListener(new javax.swing.event.ChangeListener(){ + sliderView.slider.addChangeListener(new javax.swing.event.ChangeListener(){ public void stateChanged(javax.swing.event.ChangeEvent e) { - if (!slider.getValueIsAdjusting()) { + if (!sliderView.slider.getValueIsAdjusting()) { if (!suppressInternal && !suppressExternal) { writeDisplayTextToNode(); } @@ -2446,15 +2698,23 @@ public void stateChanged(javax.swing.event.ChangeEvent e) { }); } - textComponent = slider; + + // add the listener that handles changed color + sliderView.slider.addChangeListener(new javax.swing.event.ChangeListener(){ + public void stateChanged(javax.swing.event.ChangeEvent e) { + updateColor(); + } + }); + + textComponent = sliderView; // set the tooltip to min and max values if (entry.rep.getMin() < 0) { - slider.setToolTipText("Signed integer from " + sliderView.setToolTipText("Signed integer from " +entry.rep.getMin()+" to "+entry.rep.getMax() +" ("+entry.size+" bytes)"); } else { - slider.setToolTipText("Unsigned integer from " + sliderView.setToolTipText("Unsigned integer from " +entry.rep.getMin()+" to "+entry.rep.getMax() +" ("+entry.size+" bytes)"); } @@ -2480,17 +2740,21 @@ public java.awt.Dimension getMaximumSize() { } init(); + if (e.isFlaggedReadOnly()) textComponent.setEnabled(false); } + // Takes the current GUI content and writes it to the node @Override protected void writeDisplayTextToNode() { long value; if (textField != null) { value = Long.parseLong(textField.getText()); - } else if (slider != null) { + } else if (sliderView != null) { // get value from current slider position suppressInternal = true; // will be set false once change works through - value = slider.getValue(); + value = sliderView.slider.getValue(); + } else if (radiobuttons != null) { + value = radiobuttons.getCurrentValue(); } else { // have to get key from stored map value String entry = (String) box.getSelectedItem(); @@ -2504,11 +2768,14 @@ protected void writeDisplayTextToNode() { } @Override + // Sets a specific value into the GUI protected void updateDisplayText(@NonNull String value) { if (textField != null) textField.setText(value); - if (slider != null) { + if (sliderView != null) { suppressInternal = true; - slider.setValue(Integer.parseInt(value)); + sliderView.slider.setValue(Integer.parseInt(value)); + } else if (radiobuttons != null) { + radiobuttons.setCurrentValue(value); } if (box != null) { // check to see if item exists @@ -2537,8 +2804,16 @@ protected void updateDisplayText(@NonNull String value) { @NonNull @Override + /* + * Returns the current content of the GUI as a String + * This may be a number, but for a map (or buttons) it's the current label + */ protected String getDisplayText() { - if (slider != null) return ""+slider.getValue(); + if (sliderView != null) { + return ""+sliderView.slider.getValue(); + } else if (radiobuttons != null) { + return radiobuttons.getDisplayText(); + } String s = (box == null) ? (String) textField.getText() : (String) box.getSelectedItem(); return s == null ? "" : s; @@ -2553,7 +2828,11 @@ protected String getDisplayText() { */ @NonNull protected String getCurrentValue() { - if (slider != null) return ""+slider.getValue(); + if (sliderView != null) { + return ""+sliderView.slider.getValue(); + } else if (radiobuttons != null) { + return radiobuttons.getCurrentValueString(); + } String s; if (box==null) { @@ -2616,6 +2895,7 @@ public java.awt.Dimension getMaximumSize() { +" ("+entry.size+" bytes)"); init(); + if (e.isFlaggedReadOnly()) textComponent.setEnabled(false); } @Override @@ -2722,7 +3002,10 @@ public Dimension getMaximumSize() { textField = jtf; } else { // Long string. Show multi-line editor - JTextArea jta = new JTextArea(doc, "", Math.min(40, (int)(entry.size / 40)), 80);// line count is heuristic + // For character count handling, see EntryPane#init() below + JTextArea jta = new JTextArea(doc, "", Math.min(40, (int)(entry.size / 32)), 80); + // Line count estimate is heuristic + // Limited to 40 lines to keep GUI under control jta.setEditable(true); jta.setLineWrap(true); jta.setWrapStyleWord(true); @@ -2731,8 +3014,9 @@ public Dimension getMaximumSize() { textField = jta; } textComponent = textField; - textComponent.setToolTipText("String of up to "+entry.size+" characters"); + textComponent.setToolTipText("String of up to "+(entry.size-1)+" characters"); // -1 for terminating zero in field init(); + if (e.isFlaggedReadOnly()) textComponent.setEnabled(false); } @Override @@ -2936,4 +3220,35 @@ public JTextArea handleEditorValue(JTextArea value) { return value; } } + + /** + * Implements the "link" element by providing a line of + * text that serves as an active hyperlink. + * Neither argument can be null. + */ + class HtmlLabel extends javax.swing.JTextPane { + public HtmlLabel(String text, String ref) { + super(); + setContentType("text/html"); + String content = ""+text+""; + setText(content); + + setAlignmentX(Component.LEFT_ALIGNMENT); + setFont(UIManager.getFont("TextArea.font")); + setEditable(false); + setOpaque(false); + + this.addHyperlinkListener(new javax.swing.event.HyperlinkListener() { + public void hyperlinkUpdate(javax.swing.event.HyperlinkEvent e) { + try { + if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) { + if(java.awt.Desktop.isDesktopSupported()) { + java.awt.Desktop.getDesktop().browse(e.getURL().toURI()); + } + } + } catch (Exception ex) {} + } + }); + } + } } diff --git a/src/org/openlcb/swing/EventIdTextField.java b/src/org/openlcb/swing/EventIdTextField.java index 52a61345..8c360e84 100644 --- a/src/org/openlcb/swing/EventIdTextField.java +++ b/src/org/openlcb/swing/EventIdTextField.java @@ -64,6 +64,7 @@ public static JFormattedTextField getEventIdTextField() { // Let's size the event ID fields for the longest event ID in pixels. retval.setValue("DD.DD.DD.DD.DD.DD.DD.DD"); retval.setPreferredSize(retval.getPreferredSize()); + retval.setMinimumSize(retval.getPreferredSize()); retval.setValue("00.00.00.00.00.00.00.00"); retval.setToolTipText("EventID as eight-byte dotted-hex string, " diff --git a/src/org/openlcb/swing/NodeSelector.java b/src/org/openlcb/swing/NodeSelector.java index dd9a7dab..7fbe9f26 100644 --- a/src/org/openlcb/swing/NodeSelector.java +++ b/src/org/openlcb/swing/NodeSelector.java @@ -272,5 +272,14 @@ public NodeID getSelectedNodeID() { return me.getNodeID(); } + public void setSelectedNodeID(NodeID nodeID) { + for (int i = 0; i < model.getSize(); ++i) { + if (model.getElementAt(i).getNodeID().equals(nodeID)) { + super.setSelectedItem(model.getElementAt(i)); + break; + } + } + } + private static final Logger log = Logger.getLogger(NodeSelector.class.getName()); } diff --git a/test/org/openlcb/ProducerConsumerEventReportMessageTest.java b/test/org/openlcb/ProducerConsumerEventReportMessageTest.java index 2d50e0c1..0f138dcb 100644 --- a/test/org/openlcb/ProducerConsumerEventReportMessageTest.java +++ b/test/org/openlcb/ProducerConsumerEventReportMessageTest.java @@ -84,6 +84,18 @@ public void testPayloadArray() { Assert.assertEquals(12, m2.getPayloadArray()[0]); } + @Test + public void testPayloadToString() { + ProducerConsumerEventReportMessage m1 = new ProducerConsumerEventReportMessage( + nodeID1, eventID1 ); + + byte[] payload1 = new byte[]{0x12, 0x34}; + ProducerConsumerEventReportMessage m2 = new ProducerConsumerEventReportMessage( + nodeID1, eventID1, payload1 ); + + Assert.assertEquals(" Producer/Consumer Event Report EventID:01.00.00.00.00.00.01.00 payload of 2 : 12.34", m2.toString()); + } + @Test public void testPayloadHashAndEquals() { ProducerConsumerEventReportMessage mNone = new ProducerConsumerEventReportMessage(