diff --git a/src/org/openlcb/Utilities.java b/src/org/openlcb/Utilities.java index 09e9740e..a0937c2e 100644 --- a/src/org/openlcb/Utilities.java +++ b/src/org/openlcb/Utilities.java @@ -9,8 +9,9 @@ *
* NodeID objects are immutable once created.
*
- * @author Bob Jacobsen Copyright 2009, 2010, 2011 2012
- * @version $Revision$
+ * @see org.openlcb.cdi.cmd.Util
+ *
+ * @author Bob Jacobsen Copyright 2009, 2010, 2011, 2012, 2023
*/
@Immutable
@ThreadSafe
@@ -218,4 +219,26 @@ static public void HostToNetworkUint48(byte[] arr, int offset, long value) {
arr[offset+5] = (byte) ((value) & 0xff);
}
+ /**
+ * Find the longest starting substring of a List of Strings.
+ * This is useful for finding e.g. the common prefix of a replication dump
+ */
+ @CheckReturnValue
+ @NonNull
+ static public String longestLeadingSubstring(@NonNull java.util.List
* You have to manually keep this synchronized with the manifest file.
- *
+ *
* @author Bob Jacobsen Copyright 2011 - 2012
- * @version $Revision: 17977 $
*/
public class Version {
diff --git a/src/org/openlcb/cdi/CdiRep.java b/src/org/openlcb/cdi/CdiRep.java
index cc45775c..5508ca59 100644
--- a/src/org/openlcb/cdi/CdiRep.java
+++ b/src/org/openlcb/cdi/CdiRep.java
@@ -7,7 +7,6 @@
* object implementing this interface.
*
* @author Bob Jacobsen Copyright 2011
- * @version $Revision: -1 $
*/
public interface CdiRep {
diff --git a/src/org/openlcb/cdi/cmd/BackupConfig.java b/src/org/openlcb/cdi/cmd/BackupConfig.java
index 8968d96e..c5690195 100644
--- a/src/org/openlcb/cdi/cmd/BackupConfig.java
+++ b/src/org/openlcb/cdi/cmd/BackupConfig.java
@@ -7,6 +7,7 @@
import java.io.BufferedWriter;
import java.io.IOException;
+import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -17,12 +18,12 @@
public class BackupConfig {
- static public void writeEntry(BufferedWriter outFile, String key, String value) {
+ static public void writeEntry(BufferedWriter writer, String key, String value) {
try {
- outFile.write(Util.escapeString(key));
- outFile.write('=');
- outFile.write(Util.escapeString(value));
- outFile.write('\n');
+ writer.write(Util.escapeString(key));
+ writer.write('=');
+ writer.write(Util.escapeString(value));
+ writer.write('\n');
} catch (IOException e1) {
e1.printStackTrace();
System.exit(1);
@@ -31,29 +32,42 @@ static public void writeEntry(BufferedWriter outFile, String key, String value)
public static void writeConfigToFile(String fileName, ConfigRepresentation repr) throws
IOException {
- BufferedWriter outFile = null;
+
+ BufferedWriter outFile = Files.newBufferedWriter(Paths.get(fileName), Charset.forName("UTF-8"));
+
+ writeConfigToWriter(outFile, repr);
+
+ outFile.close();
+ }
- outFile = Files.newBufferedWriter(Paths.get(fileName), Charset.forName("UTF-8"));
- final BufferedWriter finalOutFile = outFile;
+ /**
+ * @param writer Receives output. Flushed at end, but not closed.
+ * @param repr Representation containing contents to be written.
+ */
+ public static void writeConfigToWriter(BufferedWriter writer, ConfigRepresentation repr) throws
+ IOException {
+
+ final BufferedWriter finalWriter = writer;
repr.visit(new ConfigRepresentation.Visitor() {
@Override
public void visitString(ConfigRepresentation.StringEntry e) {
- writeEntry(finalOutFile, e.key, e.getValue());
+ writeEntry(finalWriter, e.key, e.getValue());
}
@Override
public void visitInt(ConfigRepresentation.IntegerEntry e) {
- writeEntry(finalOutFile, e.key, Long.toString(e.getValue()));
+ writeEntry(finalWriter, e.key, Long.toString(e.getValue()));
}
@Override
public void visitEvent(ConfigRepresentation.EventEntry e) {
- writeEntry(finalOutFile, e.key, Utilities.toHexDotsString(e.getValue
+ writeEntry(finalWriter, e.key, Utilities.toHexDotsString(e.getValue
().getContents()));
}
}
);
- outFile.close();
+
+ finalWriter.flush();
}
diff --git a/src/org/openlcb/cdi/cmd/RestoreConfig.java b/src/org/openlcb/cdi/cmd/RestoreConfig.java
index cc579de3..49cd91b3 100644
--- a/src/org/openlcb/cdi/cmd/RestoreConfig.java
+++ b/src/org/openlcb/cdi/cmd/RestoreConfig.java
@@ -32,9 +32,13 @@ public static void parseConfigFromFile(@NonNull String filePath, @NonNull Config
callback.onError("Failed to open input file: " + e.toString());
return;
}
+ parseConfigFromReader(inFile, callback);
+ }
+
+ public static void parseConfigFromReader(@NonNull BufferedReader reader, @NonNull ConfigCallback callback) {
String line = null;
try {
- while ((line = inFile.readLine()) != null) {
+ while ((line = reader.readLine()) != null) {
if (line.charAt(0) == '#') continue;
int pos = line.indexOf('=');
if (pos < 0) {
@@ -46,9 +50,9 @@ public static void parseConfigFromFile(@NonNull String filePath, @NonNull Config
callback.onConfigEntry(key, value);
}
- inFile.close();
+ reader.close();
} catch (IOException x) {
- callback.onError("Error reading input file: " + x.toString());
+ callback.onError("Error reading for restore: " + x.toString());
return;
}
}
diff --git a/src/org/openlcb/cdi/cmd/Util.java b/src/org/openlcb/cdi/cmd/Util.java
index e79ce016..150d70a3 100644
--- a/src/org/openlcb/cdi/cmd/Util.java
+++ b/src/org/openlcb/cdi/cmd/Util.java
@@ -11,6 +11,8 @@
/**
* Created by bracz on 4/9/16.
+ *
+ * @see org.openlcb.Utilities
*/
public class Util {
private final static Logger logger = Logger.getLogger(Util.class.getName());
diff --git a/src/org/openlcb/cdi/jdom/CdiMemConfigReader.java b/src/org/openlcb/cdi/jdom/CdiMemConfigReader.java
index 20a9a179..8b5f6ef1 100644
--- a/src/org/openlcb/cdi/jdom/CdiMemConfigReader.java
+++ b/src/org/openlcb/cdi/jdom/CdiMemConfigReader.java
@@ -15,7 +15,6 @@
* by call back.
*
* @author Bob Jacobsen Copyright (C) 2012
- * @version $Revision$
*/
public class CdiMemConfigReader {
private final static Logger logger = getLogger(CdiMemConfigReader.class.getName());
diff --git a/src/org/openlcb/cdi/jdom/JdomCdiReader.java b/src/org/openlcb/cdi/jdom/JdomCdiReader.java
index 66a810ab..e2978b7f 100644
--- a/src/org/openlcb/cdi/jdom/JdomCdiReader.java
+++ b/src/org/openlcb/cdi/jdom/JdomCdiReader.java
@@ -11,7 +11,6 @@
* JDOM-based OpenLCB loader
*
* @author Bob Jacobsen Copyright 2011
- * @version $Revision: -1 $
*/
public class JdomCdiReader {
diff --git a/src/org/openlcb/cdi/jdom/JdomCdiRep.java b/src/org/openlcb/cdi/jdom/JdomCdiRep.java
index 2a888faf..47ec80cd 100644
--- a/src/org/openlcb/cdi/jdom/JdomCdiRep.java
+++ b/src/org/openlcb/cdi/jdom/JdomCdiRep.java
@@ -9,7 +9,6 @@
* JDOM for reading the underlying XML.
*
* @author Bob Jacobsen Copyright 2011
- * @version $Revision: -1 $
*/
public class JdomCdiRep implements CdiRep {
diff --git a/src/org/openlcb/cdi/swing/CdiPanel.java b/src/org/openlcb/cdi/swing/CdiPanel.java
index 3b9f18fd..64c1e29f 100644
--- a/src/org/openlcb/cdi/swing/CdiPanel.java
+++ b/src/org/openlcb/cdi/swing/CdiPanel.java
@@ -3,6 +3,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import org.openlcb.EventID;
import org.openlcb.NodeID;
+import org.openlcb.Utilities;
import org.openlcb.cdi.CdiRep;
import org.openlcb.cdi.cmd.BackupConfig;
import org.openlcb.cdi.cmd.RestoreConfig;
@@ -30,14 +31,21 @@
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
+import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -63,6 +71,7 @@
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
+import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
@@ -114,7 +123,7 @@ public class CdiPanel extends JPanel {
*/
static JFileChooser fci = new JFileChooser();
{
- fci.setSelectedFile(new File(".txt"));
+ fci.setSelectedFile(new File(".txt"));
}
private ConfigRepresentation rep;
@@ -314,6 +323,8 @@ public java.awt.Dimension getMaximumSize() {
}
/**
+ * Load from a CDI representation with a default {@link GuiItemFactory}.
+ *
* @param rep Representation of the config to be loaded
*/
public void initComponents(ConfigRepresentation rep) {
@@ -385,7 +396,9 @@ private void checkForSave() {
}
/**
- * Triggers a warning at the close of this dialog that the panel file needs to be saved in JMRI.
+ * Triggers a warning at the close of this dialog that a Sensor has been made.
+ * This triggers a message the panel file needs to be saved in JMRI.
+ *
* @param uName unused.
*/
public void madeSensor(String uName) {
@@ -393,7 +406,9 @@ public void madeSensor(String uName) {
}
/**
- * Triggers a warning at the close of this dialog that the panel file needs to be saved in JMRI.
+ * Triggers a warning at the close of this dialog that a Turnout has been made.
+ * This triggers a message the panel file needs to be saved in JMRI.
+ *
* @param uName unused.
*/
public void madeTurnout(String uName) {
@@ -547,6 +562,11 @@ public void run() {
}, 500);
}
+ /**
+ * Remove the listener to the CDI representation that gets a notification that
+ * loading of the CDI is complete.
+ * Paired with {@link #addLoadingListener}.
+ */
private void removeLoadingListener() {
synchronized (rep) {
if (loadingListener != null) rep.removePropertyChangeListener(loadingListener);
@@ -554,6 +574,11 @@ private void removeLoadingListener() {
}
}
+ /**
+ * Add a listener to the CDI representation to get a notification that
+ * loading of the CDI is complete.
+ * Paired with {@link #removeLoadingListener}.
+ */
private void addLoadingListener() {
synchronized(rep) {
if (loadingListener != null) return;
@@ -576,6 +601,9 @@ public void propertyChange(PropertyChangeEvent event) {
}
}
+ /**
+ * CDI loading is done, update the UI and listeners
+ */
private void hideLoadingProgress() {
if (loadingPanel == null) return;
removeLoadingListener();
@@ -591,6 +619,10 @@ private void displayLoadingProgress() {
loadingPanel.setVisible(true);
}
+ /**
+ * Create a separate thread to render the CDI to the UI.
+ * When done, invokes {@link displayComplete} on the Swing/AWT thread
+ */
private void displayCdi() {
displayLoadingProgress();
loadingText.setText("Creating display...");
@@ -608,6 +640,10 @@ public void run() {
}, "openlcb-cdi-render").start();
}
+ /**
+ * Rendering thread is complete, show the CDI display in the UI.
+ * Must be invoked on the Swing/AWT thread
+ */
private void displayComplete() {
synchronized (startupTasks) {
renderingInProgress = false;
@@ -713,6 +749,10 @@ private void setSaveClean() {
});
}
+ /**
+ * Visitor class to find a full name for changed elements
+ * that have not been saved.
+ */
private class GetEntryNameVisitor extends ConfigRepresentation.Visitor {
CdiRep.Item item;
int segNum = 1;
@@ -966,13 +1006,16 @@ public void visitGroupRep(final ConfigRepresentation.GroupRep e) {
final String name = (item.getRepName() != null ? (item.getRepName()) : "Group") + " "
+ (e.index);
//currentPane.setBorder(BorderFactory.createTitledBorder(name));
+
+ // set the name of this pane, which names the tab
currentPane.setName(name);
-
+
// Finds a string field that could be used as a caption.
FindDescriptorVisitor vv = new FindDescriptorVisitor();
vv.visitContainer(e);
if (vv.foundEntry != null) {
+ // here a unique descriptor has been found
final JPanel tabPanel = currentPane;
final ConfigRepresentation.StringEntry source = vv.foundEntry;
final JTabbedPane parentTabs = currentTabbedPane;
@@ -985,15 +1028,20 @@ public void propertyChange(PropertyChangeEvent event) {
String downstreamName = "";
if (source.lastVisibleValue != null && !source.lastVisibleValue
.isEmpty()) {
+ // we have a new name from a description, use it
String newName = (name + " (" + source.lastVisibleValue + ")");
tabPanel.setName(newName);
if (parentTabs.getTabCount() >= e.index) {
- parentTabs.setTitleAt(e.index - 1, newName);
+ JComponent tabLabel = getTabLabel(parentTabs, e.index-1, newName, e);
+ parentTabs.setTabComponentAt(e.index - 1, tabLabel);
}
downstreamName = source.lastVisibleValue;
} else {
+ // use the name created above from the repName
if (parentTabs.getTabCount() >= e.index) {
- parentTabs.setTitleAt(e.index - 1, name);
+ // update the name listed in the tab
+ JComponent tabLabel = getTabLabel(parentTabs, e.index-1, name, e);
+ parentTabs.setTabComponentAt(e.index - 1, tabLabel);
}
}
new UpdateGroupNameVisitor(e.key, downstreamName).visitContainer(e);
@@ -1010,10 +1058,209 @@ public void propertyChange(PropertyChangeEvent event) {
factory.handleGroupPaneEnd(currentPane);
currentPane.add(Box.createVerticalGlue());
+ // add this new pane to the combined tab pane
currentTabbedPane.add(currentPane);
tabsByKey.put(e.key, currentTabbedPane);
+
+ // set the tab to a label with copy/pasteValue
+ int index = currentTabbedPane.indexOfComponent(currentPane);
+ JComponent tabLabel = getTabLabel(currentTabbedPane, index, name, e);
+ currentTabbedPane.setTabComponentAt(index, tabLabel);
+
+ }
+
+ /**
+ * Generate the tab label for a group item.
+ * Including any needed navigation, tooltip, popup menu, etc.
+ * @param parentTabbedPane The tabbed pane which it to be navigated
+ * @param name The name to display
+ * @param rep the configuration data representation
+ * @return Tab label component
+ */
+ protected JComponent getTabLabel(JTabbedPane parentTabbedPane, int index, String name, ConfigRepresentation.GroupRep rep) {
+ JLabel tabLabel = new JLabel(name);
+
+ tabLabel.addMouseListener(new MouseAdapter() {
+
+ // for click logic: https://stackoverflow.com/questions/46840814/right-click-jpopupmenu-on-jtabbedpane
+ @Override
+ public void mouseClicked(MouseEvent event) {
+
+ // isPopupTrigger doesn't work on all platforms, all versions?
+ boolean isPopup = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0 || event.isPopupTrigger();
+ if ( !isPopup ) {
+ // user selected a tab
+ parentTabbedPane.setSelectedIndex(index);
+ } else {
+ // user requested the popup menu
+ // move to tab, in case non-active one clicked
+ parentTabbedPane.setSelectedIndex(index);
+
+ JPopupMenu popupMenu = new JPopupMenu();
+ JMenuItem menuItem = new JMenuItem("Copy");
+ popupMenu.add(menuItem);
+ menuItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ performGroupReplCopy(index, rep);
+ }
+ });
+ menuItem = new JMenuItem("Paste");
+ popupMenu.add(menuItem);
+ menuItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ performGroupReplPaste(index, rep);
+ }
+ });
+
+ popupMenu.show(tabLabel, event.getX(), event.getY());
+ }
+ }
+
+ });
+
+ return tabLabel;
}
+
+ /**
+ * Perform a "copy" operation on a selected group tab
+ */
+ protected void performGroupReplCopy(int index, ConfigRepresentation.GroupRep rep) {
+ String result = groupReplToString(rep);
+
+ // store to clipboard
+ StringSelection selection = new StringSelection(result);
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ clipboard.setContents(selection, selection);
+
+ }
+
+ /**
+ * Copy an entire group replication to a String
+ */
+ protected String groupReplToString(ConfigRepresentation.GroupRep rep) {
+ StringBuilder result = new StringBuilder();
+
+ ConfigRepresentation.Visitor visitor = new ConfigRepresentation.Visitor() {
+
+ @Override
+ public void visitString(ConfigRepresentation.StringEntry e) {
+ writeEntry(e.key, e.getValue());
+ }
+
+ @Override
+ public void visitInt(ConfigRepresentation.IntegerEntry e) {
+ writeEntry(e.key, Long.toString(e.getValue()));
+ }
+
+ @Override
+ public void visitEvent(ConfigRepresentation.EventEntry e) {
+ writeEntry(e.key, org.openlcb.Utilities.toHexDotsString(e.getValue
+ ().getContents()));
+ }
+
+ protected void writeEntry(String key, String entry) {
+ result.append(key);
+ result.append("=");
+ // result.append(entry); // use the value currently in CD
+ result.append(entriesByKey.get(key).getCurrentValue()); // use value currently in UI
+ result.append("\n");
+ }
+ };
+
+ visitor.visitGroupRep(rep);
+ return new String(result);
+ }
+
+
+ /**
+ * Perform a "paste" operation into a selected group tab
+ */
+ protected void performGroupReplPaste(int index, ConfigRepresentation.GroupRep rep) {
+ // retrieve from clipboard
+ Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
+ Transferable t = c.getContents( null );
+ String newContentString = "";
+ if ( t.isDataFlavorSupported(DataFlavor.stringFlavor) ) {
+ try {
+ newContentString = (String)t.getTransferData( DataFlavor.stringFlavor );
+ } catch (UnsupportedFlavorException | IOException e) {
+ // this can never happen as we checked before
+ return;
+ }
+ } // this should always have succeeded, but if it doesn't the match below will fail
+
+ // store the values that are going to be replaced
+ String previousContentString = groupReplToString(rep);
+
+ String[] newContentLines = newContentString.split("\n");
+ String[] previousContentLines = previousContentString.split("\n");
+
+ // compare keys to see if the variables match up
+ // this version just checks the line count, more could be added here
+ if (previousContentLines.length != newContentLines.length) {
+ logger.log(Level.WARNING, "Cannot paste into a mis-matching entry type");
+ // provide a system alert to notify user
+ Toolkit.getDefaultToolkit().beep();
+ // end of attempt to paste
+ return;
+ }
+
+ // change the repl number in the newContentLines to this index
+ // First, find the prefix we're going to change
+ List
*
* @author Bob Jacobsen Copyright 2012
- * @version $Revision$
*/
public class DatagramMeteringBuffer extends MessageDecoder {
diff --git a/src/org/openlcb/implementations/DatagramReceiver.java b/src/org/openlcb/implementations/DatagramReceiver.java
index 382edad8..c4528e45 100644
--- a/src/org/openlcb/implementations/DatagramReceiver.java
+++ b/src/org/openlcb/implementations/DatagramReceiver.java
@@ -4,10 +4,8 @@
/**
* Example of receiving a OpenLCB datagram.
- *
*
* @author Bob Jacobsen Copyright 2009
- * @version $Revision$
*/
public class DatagramReceiver extends MessageDecoder {
public DatagramReceiver(NodeID here, NodeID far, Connection c) {
diff --git a/src/org/openlcb/implementations/DatagramTransmitter.java b/src/org/openlcb/implementations/DatagramTransmitter.java
index 5c1b3ea7..11e6fb2c 100644
--- a/src/org/openlcb/implementations/DatagramTransmitter.java
+++ b/src/org/openlcb/implementations/DatagramTransmitter.java
@@ -4,10 +4,8 @@
/**
* Example of sending a OpenLCB datagram.
- *
*
* @author Bob Jacobsen Copyright 2009
- * @version $Revision$
*/
public class DatagramTransmitter extends MessageDecoder {
diff --git a/src/org/openlcb/implementations/StreamReceiver.java b/src/org/openlcb/implementations/StreamReceiver.java
index e428195a..66491b93 100644
--- a/src/org/openlcb/implementations/StreamReceiver.java
+++ b/src/org/openlcb/implementations/StreamReceiver.java
@@ -4,10 +4,8 @@
/**
* Example of receiving a OpenLCB stream.
- *
*
* @author Bob Jacobsen Copyright 2009
- * @version $Revision$
*/
public class StreamReceiver extends MessageDecoder {
public StreamReceiver(NodeID here, NodeID far, Connection c) {
diff --git a/src/org/openlcb/swing/networktree/NodeTreeRep.java b/src/org/openlcb/swing/networktree/NodeTreeRep.java
index 22ed0575..ba991cdd 100644
--- a/src/org/openlcb/swing/networktree/NodeTreeRep.java
+++ b/src/org/openlcb/swing/networktree/NodeTreeRep.java
@@ -15,10 +15,8 @@
/**
* Represent a single node for the tree display
- *
*
* @author Bob Jacobsen Copyright (C) 2010, 2012
- * @version $Revision$
*/
public class NodeTreeRep extends DefaultMutableTreeNode {
/** Comment for
*
* @author Bob Jacobsen Copyright (C) 2010, 2012
- * @version $Revision$
*/
public class TreePane extends JPanel {
/** Comment for serialVersionUID
. */
diff --git a/src/org/openlcb/swing/networktree/TreePane.java b/src/org/openlcb/swing/networktree/TreePane.java
index 3aadcad8..75acdbe0 100644
--- a/src/org/openlcb/swing/networktree/TreePane.java
+++ b/src/org/openlcb/swing/networktree/TreePane.java
@@ -45,10 +45,8 @@
/**
* Pane for monitoring an entire OpenLCB network as a logical tree
- *serialVersionUID
. */
diff --git a/test/org/openlcb/UtilitiesTest.java b/test/org/openlcb/UtilitiesTest.java
index 5d899fd2..752fdea7 100644
--- a/test/org/openlcb/UtilitiesTest.java
+++ b/test/org/openlcb/UtilitiesTest.java
@@ -1,5 +1,7 @@
package org.openlcb;
+import java.util.ArrayList;
+
import org.junit.Assert;
import org.junit.Test;
@@ -114,4 +116,17 @@ boolean compareArrays(byte[] a, byte[]b) {
}
return false;
}
+
+ @Test
+ public void testLongestLeadingSubstring() {
+ ArrayList