001package jmri.jmrix.loconet.duplexgroup.swing;
002
003import java.awt.Cursor;
004import java.awt.Dimension;
005import java.time.LocalDateTime;
006import java.time.format.DateTimeFormatter;
007
008import javax.swing.BoxLayout;
009import javax.swing.JButton;
010import javax.swing.JLabel;
011import javax.swing.JPanel;
012import javax.swing.JScrollPane;
013import javax.swing.JSeparator;
014import javax.swing.JTable;
015import javax.swing.table.DefaultTableModel;
016
017import jmri.jmrix.loconet.*;
018import jmri.jmrix.loconet.duplexgroup.LnDplxGrpInfoImplConstants;
019import jmri.util.swing.ValidatedTextField;
020
021/**
022 * Provides a JPanel for querying and configuring Digitrax Duplex and WiFi
023 * network identification. Provides useful function if one or more devices
024 * are connected to LocoNet.
025 * <p>
026 * This tool makes use of LocoNet messages which have not been publicly
027 * documented by Digitrax. This tool is made possible by the reverse-
028 * engineering efforts of B. Milhaupt. Because these messages have been
029 * reverse-engineered, it is possible that the tool may not function as desired
030 * for some Digitrax hardware, and that future Digitrax hardware may not be
031 * compatible with this tool.
032 *
033 * @author B. Milhaupt Copyright 2010, 2011
034 */
035public class DuplexGroupInfoPanel extends jmri.jmrix.loconet.swing.LnPanel
036        implements java.beans.PropertyChangeListener {
037
038    // member declarations
039    JButton swingReadButton;
040    JButton swingSetButton;
041    JButton swingUpdateGrpName;
042    JButton swingUpdateChannel;
043    JButton swingUpdatePassword;
044    JButton swingUpdateGroupId;
045    ValidatedTextField swingNameValueField = new ValidatedTextField(1, false, "a", "b");
046    ValidatedTextField swingChannelValueField = new ValidatedTextField(1, false, "a", "b");
047    ValidatedTextField swingPasswordValueField = new ValidatedTextField(1, false, "a", "b");
048    ValidatedTextField swingIdValueField = new ValidatedTextField(1, false, "a", "b");
049    JTable swingDeviceTable=new JTable();
050    JTable swingDeviceResponseTable=new JTable();
051    ResponsesTableModel dtModel;
052    ResponsesTableModel dtrtModel;
053    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss.S");
054    JLabel swingNumUr92Label;
055    JLabel swingStatusValueLabel;
056    private int numDuplexTypeDevices;
057
058    private LnDplxGrpInfoImpl duplexGroupImplementation;
059
060    private int minWindowWidth = 0;
061
062    public DuplexGroupInfoPanel() {
063        swingNameValueField = new ValidatedTextField(9, false, "^.{1,8}$",  // NOI18N
064                "ErrorBadGroupName");
065
066        swingChannelValueField = new ValidatedTextField(3, false, 11, 26, "ErrorBadGroupChannel");
067        swingPasswordValueField = new ValidatedTextField(5, true, "^[0-9A-C]{4}$",  // NOI18N
068                "ErrorBadGroupPassword");
069        swingIdValueField = new ValidatedTextField(3, false, 0, 127, "ErrorBadGroupId");
070        swingNameValueField.addPropertyChangeListener(ValidatedTextField.VTF_PC_STAT_LN_UPDATE, this);
071        swingChannelValueField.addPropertyChangeListener(ValidatedTextField.VTF_PC_STAT_LN_UPDATE, this);
072        swingPasswordValueField.addPropertyChangeListener(ValidatedTextField.VTF_PC_STAT_LN_UPDATE, this);
073        swingIdValueField.addPropertyChangeListener(ValidatedTextField.VTF_PC_STAT_LN_UPDATE, this);
074
075        duplexGroupImplementation = null;
076
077    }
078
079    @Override
080    public void initComponents() {
081        // uses swing operations
082        JLabel swingTempLabel;
083
084        try {
085            minWindowWidth = Integer.parseInt(Bundle.getMessage("MinimumWidthForWindow"), 10);
086        } catch (RuntimeException e) {
087            minWindowWidth = DEFAULT_WINDOW_WIDTH;
088        }
089
090        numDuplexTypeDevices = 0;        // assume 0 UR92 devices available
091        swingStatusValueLabel = new JLabel();
092        swingStatusValueLabel.setName("ProcessingInitialStatusMessage");  //this string is used as a reference to a .properties file entry; internationalization is handled there.
093        swingStatusValueLabel.setText(convertToHtml(swingStatusValueLabel.getName(), minWindowWidth));
094
095        swingNameValueField.setText(Bundle.getMessage("ValueUnknownGroupName"));
096        swingNameValueField.setToolTipText(Bundle.getMessage("ToolTipGroupName"));
097        swingNameValueField.setLastQueriedValue(Bundle.getMessage("ValueUnknownGroupName"));
098        swingUpdateGrpName = new JButton(Bundle.getMessage("ButtonUpdate"));
099        swingUpdateGrpName.setToolTipText(Bundle.getMessage("ButtonUpdateDuplexNameHint"));
100
101        swingChannelValueField.setText(Bundle.getMessage("ValueUnknownGroupChannel"));
102        swingChannelValueField.setToolTipText(Bundle.getMessage("ToolTipGroupChannel"));
103        swingChannelValueField.setLastQueriedValue(Bundle.getMessage("ValueUnknownGroupChannel"));
104        swingUpdateChannel = new JButton(Bundle.getMessage("ButtonUpdate"));
105        swingUpdateChannel.setToolTipText(Bundle.getMessage("ButtonUpdateChannelHint"));
106
107        swingPasswordValueField.setText(Bundle.getMessage("ValueUnknownGroupPassword"));
108        swingPasswordValueField.setToolTipText(Bundle.getMessage("ToolTipGroupPassword"));
109        swingPasswordValueField.setLastQueriedValue(Bundle.getMessage("ValueUnknownGroupPassword"));
110        swingUpdatePassword = new JButton(Bundle.getMessage("ButtonUpdate"));
111        swingUpdatePassword.setToolTipText(Bundle.getMessage("ButtonDuplexPasswordHint"));
112
113        swingIdValueField.setText(Bundle.getMessage("ValueUnknownGroupID"));
114        swingIdValueField.setToolTipText(Bundle.getMessage("ToolTipGroupID"));
115        swingIdValueField.setLastQueriedValue(Bundle.getMessage("ValueUnknownGroupID"));
116        swingUpdateGroupId = new JButton(Bundle.getMessage("ButtonUpdate"));
117        swingUpdateGroupId.setToolTipText(Bundle.getMessage("ButtonDuplexGroupIDHint"));
118
119        // Want to force space in the GUI for N lines of status, where N comes
120        // from a value in the DuplexGroup.properties file;
121        // assume 2 if not able to parse the variable from the .properties file.
122        int numLinesForStatus = 2;
123        try {
124            numLinesForStatus = Integer.parseInt(Bundle.getMessage("FixedLinesForStatus"));
125        } catch (RuntimeException e) {
126            numLinesForStatus = 2;
127        }
128
129        swingStatusValueLabel.setPreferredSize(new java.awt.Dimension(minWindowWidth,
130                numLinesForStatus * (int) swingStatusValueLabel.getMaximumSize().getHeight()));
131
132        swingReadButton = new JButton(Bundle.getMessage("ButtonRead"));
133        swingSetButton = new JButton(Bundle.getMessage("ButtonSet"));
134
135        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));  // need to override the default layout of FlowLayout (horizontal layout)
136        swingReadButton.hasFocus();
137
138        JPanel swingTempPanel = new JPanel();
139        swingTempPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 3));
140        swingTempPanel.add(swingTempLabel = new JLabel(Bundle.getMessage("LabelDuplexName")));
141        swingTempPanel.add(swingNameValueField);
142        swingTempLabel.setLabelFor(swingNameValueField); // for "assistive technology" per JLabel on-line documentation
143        swingTempPanel.add(swingUpdateGrpName);
144        add(swingTempPanel);
145
146        swingTempPanel = new JPanel();
147        swingTempPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 3));
148        swingTempPanel.add(swingTempLabel = new JLabel(Bundle.getMessage("LabelDuplexChannel")));
149        swingTempPanel.add(swingChannelValueField);
150        swingTempLabel.setLabelFor(swingChannelValueField); // for "assistive technology" per JLabel on-line documentation
151        swingTempPanel.add(swingUpdateChannel);
152        add(swingTempPanel);
153
154        swingTempPanel = new JPanel();
155        swingTempPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 3));
156        swingTempPanel.add(swingTempLabel = new JLabel(Bundle.getMessage("LabelDuplexPassword")));
157        swingTempPanel.add(swingPasswordValueField);
158        swingTempLabel.setLabelFor(swingPasswordValueField); // for "assistive technology" per JLabel on-line documentation
159        swingTempPanel.add(swingUpdatePassword);
160        add(swingTempPanel);
161
162        swingTempPanel = new JPanel();
163        swingTempPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 3));
164        swingTempPanel.add(swingTempLabel = new JLabel(Bundle.getMessage("LabelDuplexGroupID")));
165        swingTempPanel.add(swingIdValueField);
166        swingTempLabel.setLabelFor(swingIdValueField); // for "assistive technology" per JLabel on-line documentation
167        swingTempPanel.add(swingUpdateGroupId);
168        add(swingTempPanel);
169
170        swingTempPanel = new JPanel();
171        swingTempPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 3));
172        swingTempPanel.add(swingNumUr92Label = new JLabel(" "));
173        updateDisplayOfUr92Count();
174        add(swingTempPanel);
175
176        swingTempPanel = new JPanel();
177        swingTempPanel.setLayout(new java.awt.BorderLayout());
178        dtModel = new ResponsesTableModel(
179                new String[]{ Bundle.getMessage("IPLDeviceInfoHeaderType"),
180                        Bundle.getMessage("IPLDeviceInfoHeaderSerial"),
181                        Bundle.getMessage("IPLDeviceInfoHeaderFwv") },0);
182        swingDeviceTable.setModel(dtModel);
183        JScrollPane sp=new JScrollPane(swingDeviceTable);
184        sp.setPreferredSize(new Dimension(0,swingDeviceTable.getRowHeight()*5)); //five rows high
185        swingTempPanel.add(sp);
186        add(swingTempPanel);
187
188        swingTempPanel = new JPanel();
189        swingTempPanel.setLayout(new java.awt.BorderLayout());
190        dtrtModel = new ResponsesTableModel(new String[]{ Bundle.getMessage("IPLDeviceRespHeaderTime"),
191                Bundle.getMessage("IPLDeviceRespHeaderName"),
192                Bundle.getMessage("IPLDeviceRespHeaderChannel"),
193                Bundle.getMessage("IPLDeviceRespHeaderPassword"),
194                Bundle.getMessage("IPLDeviceRespHeaderGroupID") },0);
195        swingDeviceResponseTable.setModel(dtrtModel);
196        JScrollPane spdtrt=new JScrollPane(swingDeviceResponseTable);
197        spdtrt.setPreferredSize(new Dimension(0,swingDeviceResponseTable.getRowHeight()*5)); //five rows high
198        swingTempPanel.add(spdtrt);
199        add(swingTempPanel);
200
201        swingTempPanel = new JPanel();
202        swingTempPanel.setLayout(new java.awt.FlowLayout());
203        swingTempPanel.add(swingReadButton);
204        swingTempPanel.add(swingSetButton);
205        add(swingTempPanel);
206
207        add(new JSeparator());
208
209        swingTempPanel = new JPanel();
210        swingTempPanel.setLayout(new java.awt.FlowLayout());
211        swingTempPanel.add(swingStatusValueLabel);
212        add(swingTempPanel);
213
214        swingUpdateGrpName.addActionListener(e -> {
215            swingNameValueField.setForeground(COLOR_OK);
216            updateStatusLineMessage(" ", COLOR_STATUS_OK);
217            if (validateGroupNameField() == false) {
218                swingNameValueField.setForeground(COLOR_ERROR_VAL);
219                updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
220                swingNameValueField.requestFocusInWindow();
221                return;
222            }
223            writeGroupName();
224            readButtonActionPerformed();
225        });
226        swingUpdateChannel.addActionListener(e -> {
227            swingChannelValueField.setForeground(COLOR_OK);
228            updateStatusLineMessage(" ", COLOR_STATUS_OK);
229            if (validateGroupNameField() == false) {
230                swingChannelValueField.setForeground(COLOR_ERROR_VAL);
231                updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
232                swingChannelValueField.requestFocusInWindow();
233                return;
234            }
235            writeChannel();
236            readButtonActionPerformed();
237        });
238        swingUpdatePassword.addActionListener(e -> {
239            swingPasswordValueField.setForeground(COLOR_OK);
240            updateStatusLineMessage(" ", COLOR_STATUS_OK);
241            if (validateGroupNameField() == false) {
242                swingPasswordValueField.setForeground(COLOR_ERROR_VAL);
243                updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
244                swingPasswordValueField.requestFocusInWindow();
245                return;
246            }
247            writePassword();
248            readButtonActionPerformed();
249        });
250        swingUpdateGroupId.addActionListener(e -> {
251            swingIdValueField.setForeground(COLOR_OK);
252            updateStatusLineMessage(" ", COLOR_STATUS_OK);
253            if (validateGroupNameField() == false) {
254                swingIdValueField.setForeground(COLOR_ERROR_VAL);
255                updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
256                swingIdValueField.requestFocusInWindow();
257                return;
258            }
259            writeDuplexId();
260            readButtonActionPerformed();
261        });
262
263        swingSetButton.addActionListener(new java.awt.event.ActionListener() {
264            @Override
265            public void actionPerformed(java.awt.event.ActionEvent e) {
266                setButtonActionPerformed();
267            }
268        });
269        swingReadButton.addActionListener(new java.awt.event.ActionListener() {
270            @Override
271            public void actionPerformed(java.awt.event.ActionEvent e) {
272                scanButtonActionPerformed();
273            }
274
275        });
276    }
277
278    @Override
279    public String getHelpTarget() {
280        return "package.jmri.jmrix.loconet.duplexgroup.DuplexGroupTabbedPanel"; // NOI18N
281    }
282
283    @Override
284    public String getTitle() {
285        return Bundle.getMessage("Title");
286    }
287
288    @Override
289    public void initComponents(LocoNetSystemConnectionMemo memo) {
290        super.initComponents(memo);
291        duplexGroupImplementation = new LnDplxGrpInfoImpl(memo);
292        duplexGroupImplementation.addPropertyChangeListener(this);
293
294        scanButtonActionPerformed();    // begin a query for UR92s
295    }
296
297    /**
298     * Update GUI status then generates LocoNet message used to count the
299     * available UR92 devices.
300     */
301    private void scanButtonActionPerformed() {
302        numDuplexTypeDevices = 0;
303        updateStatusLineMessage("ProcessingFindingUR92s", COLOR_STATUS_OK);
304        duplexGroupImplementation.countUr92sAndQueryDuplexIdentityInfo();
305    }
306
307    /**
308     * Modify GUI to show that displayed Duplex network information is not
309     * currently valid. Creates and sends LocoNet traffic to query any available
310     * UR92(s) for Duplex network identity information.
311     */
312    private void readButtonActionPerformed() {
313        if (numDuplexTypeDevices == 0) {
314            scanButtonActionPerformed();
315            return;
316        }
317
318        swingNameValueField.setForeground(COLOR_OK);     // set foreground to default color
319        swingChannelValueField.setForeground(COLOR_OK);  // set foreground to default color
320        swingPasswordValueField.setForeground(COLOR_OK); // set foreground to default color
321        swingIdValueField.setForeground(COLOR_OK);       // set foreground to default color
322        swingNameValueField.setText(Bundle.getMessage("ValueUnknownGroupName"));
323        swingChannelValueField.setText(Bundle.getMessage("ValueUnknownGroupChannel"));
324        swingPasswordValueField.setText(Bundle.getMessage("ValueUnknownGroupPassword"));
325        swingIdValueField.setText(Bundle.getMessage("ValueUnknownGroupID"));
326        updateStatusLineMessage("ProcessingReadingInfo", COLOR_STATUS_OK);
327        duplexGroupImplementation.queryDuplexGroupIdentity();
328        updateStatusLineMessage("ProcessingWaitingForReport", COLOR_STATUS_OK);
329    }
330
331    /**
332     * Validate the Duplex group name currently specified in the GUI. If the
333     * group name is invalid, the GUI status line is updated with an appropriate
334     * message.
335     *
336     * @return true if current swingNameValueField is a valid Duplex group name
337     */
338    private boolean validateGroupNameField() {
339        return swingNameValueField.isValid();
340    }
341
342    /**
343     * Validates the Duplex group channel number currently specified in the GUI.
344     * If the group channel number is invalid, the GUI status line is updated
345     * with an appropriate message.
346     *
347     * @return true if current swingChannelValueField is a valid Duplex group
348     *         channel
349     */
350    private boolean validateGroupChannelField() {
351        return swingChannelValueField.isValid();
352    }
353
354    /**
355     * Validate the Duplex group ID number currently specified in the GUI. If
356     * the group ID number is invalid, the GUI status line is updated with an
357     * appropriate message.
358     *
359     * @return true if current swingIdValueField is a valid Duplex group ID
360     *         number
361     */
362    private boolean validateGroupIDField() {
363        return swingIdValueField.isValid();
364    }
365
366    /**
367     * Validate the Duplex group password currently specified in the GUI.
368     *
369     * @return true if current swingNameValueField is a valid Duplex group
370     *         password
371     */
372    private boolean validateGroupPasswordField() {
373        return swingPasswordValueField.isValid();
374    }
375
376    /**
377     * Perform actions required when the Set Group Information button is
378     * clicked.
379     * <p>
380     * First validates the Duplex group name, channel, password, and group ID.
381     * If any is invalid, the GUI status line is updated and the process is
382     * aborted. If all are valid, the appropriate LocoNet messages are created,
383     * and sent in sequence, to update the attached UR92(s) to the specified
384     * Duplex group identity information, then initiates a read of the UR92(s)
385     * to update the GUI.
386     */
387    private void setButtonActionPerformed() {
388        boolean result = true;
389
390        // assume all values are valid, so put fields to valid color and clear
391        // status line
392        swingNameValueField.setForeground(COLOR_OK);
393        swingChannelValueField.setForeground(COLOR_OK);
394        swingPasswordValueField.setForeground(COLOR_OK);
395        swingIdValueField.setForeground(COLOR_OK);
396        updateStatusLineMessage(" ", COLOR_STATUS_OK);
397        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
398
399        if (validateGroupNameField() == false) {
400            swingNameValueField.setForeground(COLOR_ERROR_VAL);
401            result = false;
402            updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
403            swingNameValueField.requestFocusInWindow();
404        } else if (validateGroupChannelField() == false) {
405            swingChannelValueField.setForeground(COLOR_ERROR_VAL);
406            result = false;
407            updateStatusLineMessage("ErrorBadGroupChannel", COLOR_STATUS_ERROR);
408            swingChannelValueField.requestFocusInWindow();
409        } else if (validateGroupPasswordField() == false) {
410            swingPasswordValueField.setForeground(COLOR_ERROR_VAL);
411            result = false;
412            updateStatusLineMessage("ErrorBadGroupPassword", COLOR_STATUS_ERROR);
413            swingPasswordValueField.requestFocusInWindow();
414        } else if (validateGroupIDField() == false) {
415            swingIdValueField.setForeground(COLOR_ERROR_VAL);
416            result = false;
417            updateStatusLineMessage("ErrorBadGroupId", COLOR_STATUS_ERROR);
418            swingIdValueField.requestFocusInWindow();
419        }
420
421        if (result == true) {
422            if (!writeGroupName() ||
423                    !writeChannel() ||
424                    !writePassword() ||
425                    !writeDuplexId()) {
426                // do nothing
427            } else {
428                readButtonActionPerformed();
429            }
430        }
431        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
432    }
433
434    // delay need between messages slower Duplex/LNWI devices
435    // eg LNWI, UR93
436    private static final int WRITE_DELAY = 250;
437    private static final int WRITE_DELAY_PASSWORD = 2500; // LNWI need a lot of time on password updates
438
439    // four routines for performing the writes.
440    private boolean writeGroupName() {
441        updateStatusLineMessage("ProcessingGroupUpdate", COLOR_STATUS_OK);
442        StringBuilder writeGroupName = new StringBuilder();
443        writeGroupName.append(swingNameValueField.getText());
444        writeGroupName.append("         "); // ensure length at least 8 characters
445        writeGroupName.setLength(LnDplxGrpInfoImplConstants.DPLX_NAME_LEN); // trim to required length
446        try {
447            duplexGroupImplementation.setDuplexGroupName(writeGroupName.toString());
448            Thread.sleep(WRITE_DELAY);
449        } catch (LocoNetException | InterruptedException e) {
450            // illegal Duplex Group Name
451            updateStatusLineMessage("ErrorBadGroupName", COLOR_STATUS_ERROR);
452            swingNameValueField.requestFocusInWindow();
453            return false;
454        }
455        return true;
456    }
457
458    private boolean writeChannel() {
459        updateStatusLineMessage("ProcessingGroupUpdate", COLOR_STATUS_OK);
460        try {
461            duplexGroupImplementation.setDuplexGroupChannel(Integer.parseInt(swingChannelValueField.getText(), 10));
462            Thread.sleep(WRITE_DELAY);
463        } catch (LocoNetException | InterruptedException e) {
464            // illegal Duplex Group Channel
465            updateStatusLineMessage("ErrorBadGroupChannel", COLOR_STATUS_ERROR);
466            swingChannelValueField.requestFocusInWindow();
467            return false;
468        }
469        return true;
470    }
471
472    private boolean writePassword() {
473        try {
474            duplexGroupImplementation.setDuplexGroupPassword(swingPasswordValueField.getText());
475            Thread.sleep(WRITE_DELAY_PASSWORD);
476        } catch (LocoNetException | InterruptedException e) {
477            // illegal Duplex Group Password
478            updateStatusLineMessage("ErrorBadGroupPassword", COLOR_STATUS_ERROR);
479            swingPasswordValueField.requestFocusInWindow();
480            return false;
481        }
482        return true;
483    }
484
485    private boolean writeDuplexId() {
486        try {
487            duplexGroupImplementation.setDuplexGroupId(swingIdValueField.getText());
488            Thread.sleep(WRITE_DELAY);
489        } catch (LocoNetException | InterruptedException e) {
490            // illegal Duplex Group Id
491            updateStatusLineMessage("ErrorBadGroupId", COLOR_STATUS_ERROR);
492            swingIdValueField.requestFocusInWindow();
493            return false;
494        }
495        return true;
496    }
497
498    /**
499     *
500     * @param s     Name of tag in .properties file for string to be converted
501     *              to HTML
502     * @param width Width of resulting HTML, in Swing dimensional units
503     * @return String containing HTML for input string s
504     */
505    private String convertToHtml(String s, int width) {
506        String result = "<html><body><div align=center style='width: "; // NOI18N
507
508        if (s.length() == 1) {
509            result = " ";
510        } else {
511            result = result + width + "'>" +  // NOI18N
512                    Bundle.getMessage(s);
513        }
514        return result;
515    }
516
517    /**
518     * Update the GUI label showing the number of UR92 devices.
519     */
520    private void updateDisplayOfUr92Count() {
521        Object[] messageArguments = {
522            numDuplexTypeDevices,
523            numDuplexTypeDevices
524        };
525        java.text.MessageFormat formatter = new java.text.MessageFormat("");
526
527        try {
528            formatter.applyPattern(Bundle.getMessage("LabelDeviceCountUR92"));
529            double[] pluralLimits = {0, 1, 2};
530            String[] devicePlurals = {
531                    Bundle.getMessage("LabelDeviceCountUR92Plural0"),
532                    Bundle.getMessage("LabelDeviceCountUR92Plural1"),
533                    Bundle.getMessage("LabelDeviceCountUR92Plural2")
534            };
535            java.text.ChoiceFormat pluralForm = new java.text.ChoiceFormat(pluralLimits, devicePlurals);
536            java.text.Format[] messageFormats = {
537                java.text.NumberFormat.getInstance(),
538                pluralForm
539            };
540            formatter.setFormats(messageFormats);
541            String ur92CountString = formatter.format(messageArguments);
542            swingNumUr92Label.setText(ur92CountString);
543        } catch (RuntimeException e) {
544            swingNumUr92Label.setText(Bundle.getMessage("LabelDeviceCountUR92Except", numDuplexTypeDevices));
545            // eat the exception and show a simple, gramatically ambiguous message
546        }
547        swingNumUr92Label.repaint();
548    }
549
550    private void updateStatusLineMessage(String statusMessage, java.awt.Color fgColor) {
551        if (statusMessage == null) {
552            swingStatusValueLabel.setForeground(fgColor);
553            swingStatusValueLabel.setName(" ");  //this string is used as a reference to a .properties file entry; internationalization is handled there.
554            swingStatusValueLabel.setText(convertToHtml(swingStatusValueLabel.getName(), minWindowWidth));
555        } else {
556            swingStatusValueLabel.setForeground(fgColor);
557            swingStatusValueLabel.setName(statusMessage);  //this string is used as a reference to a .properties file entry; internationalization is handled there.
558            swingStatusValueLabel.setText(convertToHtml(swingStatusValueLabel.getName(), minWindowWidth));
559        }
560    }
561
562    /**
563     * Process the "property change" events from LnDplxGrpInfoImpl and
564     * ValidatedTextField object. Includes processing of:
565     * <ul>
566     *     <li>ValidatedTextField - ValidatedTextField.VTF_PC_STAT_LN_UPDATE
567     *     <li>LnDplxGrpInfoImpl - StatusDontBlastError
568     *     <li>StatusLineUpdate
569     *     <li>NumberOfUr92sUpdate
570     * </ul>
571     */
572    @Override
573    public void propertyChange(java.beans.PropertyChangeEvent evt) {
574        // these messages can arrive without a complete
575        // GUI, in which case we just ignore them
576        String eventName = evt.getPropertyName();
577
578        if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_STAT_LN_UPDATE_IF_NOT_CURRENTLY_ERROR)) {
579            if (swingStatusValueLabel == null) {
580                return;
581            }
582
583            if (swingStatusValueLabel.getForeground().equals(COLOR_STATUS_ERROR)) {
584                return; // don't overwrite an existing error message for this case
585            }
586            String statusMessage = (String) evt.getNewValue();
587            java.awt.Color fgColor = COLOR_STATUS_OK;
588            if (statusMessage == null) {
589                updateStatusLineMessage(" ", COLOR_STATUS_OK);
590                return;
591            } // if current status message begins with Error, then don't replace it.
592            else if ((statusMessage.startsWith("Error"))
593                    || (swingStatusValueLabel.getForeground().equals(COLOR_STATUS_ERROR))) {
594                return;
595            }
596            // is not an error message, so replace it.
597            updateStatusLineMessage(statusMessage, fgColor);
598        } else if ( eventName.equals(LnDplxGrpInfoImpl.DPLX_IPL_DEVICE_DETAILS) ) {
599            LnDplxGrpInfoImpl.BasicIPLDeviceInfo tmpItem = (LnDplxGrpInfoImpl.BasicIPLDeviceInfo)evt.getNewValue();
600            if (tmpItem.getType().isEmpty()) {
601                for (int ix = dtModel.getRowCount()-1  ; ix > -1 ; ix --) {
602                    dtModel.removeRow(ix);
603                }
604            } else {
605                dtModel.addRow(new String[] {tmpItem.getType(), tmpItem.getSerialNumber(), tmpItem.getSwVersion()});
606            }
607            return;
608        } else if ( eventName.equals(LnDplxGrpInfoImpl.DPLX_IPL_DEVICE_RESPONSE_DETAILS) ) {
609            LnDplxGrpInfoImpl.BasicIPLDeviceResponseInfo tmpItem = (LnDplxGrpInfoImpl.BasicIPLDeviceResponseInfo)evt.getNewValue();
610            if (tmpItem.getGroupName().isEmpty()) {
611                for (int ix = dtrtModel.getRowCount()-1  ; ix > -1 ; ix --) {
612                    dtrtModel.removeRow(ix);
613                }
614            } else {
615                dtrtModel.addRow(new String[] {dtf.format(LocalDateTime.now()),tmpItem.getGroupName(), tmpItem.getChannel(), tmpItem.getPassword(), tmpItem.getGroupId()});
616            }
617            return;
618        } else if ((eventName.equals(ValidatedTextField.VTF_PC_STAT_LN_UPDATE))
619                || (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_STAT_LN_UPDATE))) {
620            if (swingStatusValueLabel == null) {
621                return;
622            }
623            String statusMessage = (String) evt.getNewValue();
624            if (statusMessage == null) {
625                updateStatusLineMessage(" ", COLOR_STATUS_OK);
626                return;
627            } else {
628                java.awt.Color fgColor = COLOR_STATUS_OK;
629                if (statusMessage.startsWith("ERROR:")) { // NOI18N
630                    fgColor = COLOR_STATUS_ERROR;
631                    statusMessage = statusMessage.substring(6);
632                } else if (statusMessage.startsWith("Error")) { // NOI18N
633                    fgColor = COLOR_STATUS_ERROR;
634                }
635                updateStatusLineMessage(statusMessage, fgColor);
636            }
637        } else if (eventName.equals("NumberOfUr92sUpdate")) { // NOI18N
638            numDuplexTypeDevices = (Integer) evt.getNewValue();
639            updateDisplayOfUr92Count();
640        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_NAME_VALIDITY)) {
641            swingNameValueField.setForeground(COLOR_OK);
642            swingNameValueField.setEnabled(evt.getNewValue().equals(true));
643            if (swingNameValueField.isEnabled()
644                    && swingChannelValueField.isEnabled()
645                    && swingPasswordValueField.isEnabled()
646                    && swingIdValueField.isEnabled()) {
647                swingSetButton.setEnabled(true);
648            } else {
649                swingSetButton.setEnabled(false);
650            }
651        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_CHANNEL_VALIDITY)) {
652            swingChannelValueField.setForeground(COLOR_OK);
653            swingChannelValueField.setEnabled(evt.getNewValue().equals(true));
654            if (swingNameValueField.isEnabled()
655                    && swingChannelValueField.isEnabled()
656                    && swingPasswordValueField.isEnabled()
657                    && swingIdValueField.isEnabled()) {
658                swingSetButton.setEnabled(true);
659            } else {
660                swingSetButton.setEnabled(false);
661            }
662        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_PASSWORD_VALIDITY)) {
663            swingPasswordValueField.setForeground(COLOR_OK);
664            swingPasswordValueField.setEnabled(evt.getNewValue().equals(true));
665            if (swingNameValueField.isEnabled()
666                    && swingChannelValueField.isEnabled()
667                    && swingPasswordValueField.isEnabled()
668                    && swingIdValueField.isEnabled()) {
669                swingSetButton.setEnabled(true);
670            } else {
671                swingSetButton.setEnabled(false);
672            }
673        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_ID_VALIDITY)) {
674            swingIdValueField.setForeground(COLOR_OK);
675            swingIdValueField.setEnabled(evt.getNewValue().equals(true));
676            if (swingNameValueField.isEnabled()
677                    && swingChannelValueField.isEnabled()
678                    && swingPasswordValueField.isEnabled()
679                    && swingIdValueField.isEnabled()) {
680                swingSetButton.setEnabled(true);
681            } else {
682                swingSetButton.setEnabled(false);
683            }
684        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_NAME_UPDATE)) {
685            if (evt.getNewValue().equals(true)) {
686                String s = duplexGroupImplementation.getFetchedDuplexGroupName();
687                showValidGroupName(s);
688                swingNameValueField.setLastQueriedValue(s);
689            } else {
690                disableGroupName();
691            }
692        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_CHANNEL_UPDATE)) {
693            if (evt.getNewValue().equals(true)) {
694                String s = duplexGroupImplementation.getFetchedDuplexGroupChannel();
695                showValidGroupChannel(s);
696                swingChannelValueField.setLastQueriedValue(s);
697            } else {
698                disableGroupChannel();
699            }
700        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_PASSWORD_UPDATE)) {
701            if (evt.getNewValue().equals(true)) {
702                String s = duplexGroupImplementation.getFetchedDuplexGroupPassword();
703                showValidGroupPassword(s);
704                swingPasswordValueField.setLastQueriedValue(s);
705            } else {
706                disableGroupPassword();
707            }
708        } else if (eventName.equals(LnDplxGrpInfoImpl.DPLX_PC_ID_UPDATE)) {
709            if (evt.getNewValue().equals(true)) {
710                String s = duplexGroupImplementation.getFetchedDuplexGroupId();
711                showValidGroupId(s);
712                swingIdValueField.setLastQueriedValue(s);
713            } else {
714                disableGroupId();
715            }
716        }
717    }
718
719    private void showValidGroupName(String gn) {
720        swingNameValueField.setForeground(COLOR_OK);
721        swingNameValueField.setBackground(COLOR_BG_UNEDITED);
722        swingNameValueField.setEnabled(true);
723        swingNameValueField.setText(gn);
724    }
725
726    private void disableGroupName() {
727        swingNameValueField.setForeground(COLOR_OK);
728        swingNameValueField.setBackground(COLOR_BG_UNEDITED);
729        swingNameValueField.setEnabled(false);
730        swingNameValueField.setText("????????");
731    }
732
733    private void showValidGroupChannel(String gc) {
734        swingChannelValueField.setForeground(COLOR_OK);
735        swingChannelValueField.setBackground(COLOR_BG_UNEDITED);
736        swingChannelValueField.setEnabled(true);
737        swingChannelValueField.setText(gc);
738    }
739
740    private void disableGroupChannel() {
741        swingChannelValueField.setForeground(COLOR_OK);
742        swingChannelValueField.setBackground(COLOR_BG_UNEDITED);
743        swingChannelValueField.setEnabled(false);
744        swingChannelValueField.setText("??");
745    }
746
747    private void showValidGroupPassword(String gp) {
748        swingPasswordValueField.setForeground(COLOR_OK);
749        swingPasswordValueField.setBackground(COLOR_BG_UNEDITED);
750        swingPasswordValueField.setEnabled(true);
751        swingPasswordValueField.setText(gp);
752    }
753
754    private void disableGroupPassword() {
755        swingPasswordValueField.setForeground(COLOR_OK);
756        swingPasswordValueField.setBackground(COLOR_BG_UNEDITED);
757        swingPasswordValueField.setEnabled(false);
758        swingPasswordValueField.setText("????");
759    }
760
761    private void showValidGroupId(String gi) {
762        swingIdValueField.setForeground(COLOR_OK);
763        swingIdValueField.setBackground(COLOR_BG_UNEDITED);
764        swingIdValueField.setEnabled(true);
765        swingIdValueField.setText(gi);
766    }
767
768    private void disableGroupId() {
769        swingIdValueField.setForeground(COLOR_OK);
770        swingIdValueField.setBackground(COLOR_BG_UNEDITED);
771        swingIdValueField.setEnabled(false);
772        swingIdValueField.setText("???");
773    }
774
775    // defines for colorizing the user input GUI elements and status line
776    public final static java.awt.Color COLOR_MISMATCH_VAL = java.awt.Color.red.darker();
777    public final static java.awt.Color COLOR_UNKN_VAL = java.awt.Color.yellow.brighter();
778    public final static java.awt.Color COLOR_READ = null; // use default color for the component
779    public final static java.awt.Color COLOR_BG_EDITED = java.awt.Color.orange; // use default color for the component
780    public final static java.awt.Color COLOR_ERROR_VAL = java.awt.Color.black;
781    public final static java.awt.Color COLOR_OK = java.awt.Color.black;
782    public final static java.awt.Color COLOR_BG_OK = java.awt.Color.white;
783    public final static java.awt.Color COLOR_BG_UNEDITED = java.awt.Color.white;
784    public final static java.awt.Color COLOR_STATUS_OK = java.awt.Color.black;
785    public final static java.awt.Color COLOR_STATUS_ERROR = java.awt.Color.red;
786    public final static java.awt.Color COLOR_BG_ERROR = java.awt.Color.red;
787
788    // helper for laying out the GUI
789    public final static int DEFAULT_WINDOW_WIDTH = 200;
790
791    /**
792     * Nested class to create a DuplexGroupInfoPanel using old-style defaults.
793     * This is most useful when adding DuplexGroupInfoPanel as a JMRI Start-up
794     * action.
795     */
796    static public class Default extends jmri.jmrix.loconet.swing.LnNamedPaneAction {
797
798        public Default() {
799            super(Bundle.getMessage("MenuItemDuplexInfo"),
800                    new jmri.util.swing.sdi.JmriJFrameInterface(),
801                    DuplexGroupInfoPanel.class.getName(),
802                    jmri.InstanceManager.getDefault(LocoNetSystemConnectionMemo.class));
803        }
804    }
805
806    // make the table model read only
807    static public class ResponsesTableModel extends DefaultTableModel {
808         public ResponsesTableModel(String[] columns, int rows) {
809             super(columns, rows);
810         }
811        @Override
812        public boolean isCellEditable(int row, int column){
813            return false;
814        }
815   }
816
817   //    private final static Logger log = LoggerFactory.getLogger(DuplexGroupInfoPanel.class);
818
819}