001package jmri.jmrit.symbolicprog.tabbedframe;
002
003import java.awt.Color;
004import java.awt.Component;
005import java.awt.Font;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.ItemEvent;
010import java.awt.event.ItemListener;
011import java.io.IOException;
012import java.lang.reflect.Field;
013import java.util.*;
014import javax.swing.AbstractButton;
015import javax.swing.BorderFactory;
016import javax.swing.Box;
017import javax.swing.BoxLayout;
018import javax.swing.JButton;
019import javax.swing.JComponent;
020import javax.swing.JLabel;
021import javax.swing.JPanel;
022import javax.swing.JProgressBar;
023import javax.swing.JScrollPane;
024import javax.swing.JSeparator;
025import javax.swing.JTable;
026import javax.swing.JTextField;
027import javax.swing.JToggleButton;
028import javax.swing.JWindow;
029import javax.swing.RowSorter;
030import javax.swing.SortOrder;
031import javax.swing.SwingConstants;
032import javax.swing.table.TableModel;
033import javax.swing.table.TableRowSorter;
034import jmri.jmrit.roster.RosterEntry;
035import jmri.jmrit.symbolicprog.AbstractValue;
036import jmri.jmrit.symbolicprog.CvTableModel;
037import jmri.jmrit.symbolicprog.CvValue;
038import jmri.jmrit.symbolicprog.DccAddressPanel;
039import jmri.jmrit.symbolicprog.FnMapPanel;
040import jmri.jmrit.symbolicprog.FnMapPanelESU;
041import jmri.jmrit.symbolicprog.PrintCvAction;
042import jmri.jmrit.symbolicprog.Qualifier;
043import jmri.jmrit.symbolicprog.QualifierAdder;
044import jmri.jmrit.symbolicprog.SymbolicProgBundle;
045import jmri.jmrit.symbolicprog.ValueEditor;
046import jmri.jmrit.symbolicprog.CvValueRenderer;
047import jmri.jmrit.symbolicprog.VariableTableModel;
048import jmri.jmrit.symbolicprog.VariableValue;
049import jmri.util.CvUtil;
050import jmri.util.StringUtil;
051import jmri.util.ThreadingUtil;
052import jmri.util.davidflanagan.HardcopyWriter;
053import jmri.util.jdom.LocaleSelector;
054import org.jdom2.Attribute;
055import org.jdom2.Element;
056
057/**
058 * Provide the individual panes for the TabbedPaneProgrammer.
059 * <p>
060 * Note that this is not only the panes carrying variables, but also the special
061 * purpose panes for the CV table, etc.
062 * <p>
063 * This class implements PropertyChangeListener so that it can be notified when
064 * a variable changes its busy status at the end of a programming read/write
065 * operation.
066 *
067 * There are four read and write operation types, all of which have to be
068 * handled carefully:
069 * <DL>
070 * <DT>Write Changes<DD>This must write changes that occur after the operation
071 * starts, because the act of writing a variable/CV may change another. For
072 * example, writing CV 1 will mark CV 29 as changed.
073 * <p>
074 * The definition of "changed" is operationally in the
075 * {@link jmri.jmrit.symbolicprog.VariableValue#isChanged} member function.
076 *
077 * <DT>Write All<DD>Like write changes, this might have to go back and re-write
078 * a variable depending on what has previously happened. It should write every
079 * variable (at least) once.
080 * <DT>Read All<DD>This should read every variable once.
081 * <img src="doc-files/PaneProgPane-ReadAllSequenceDiagram.png" alt="UML Sequence diagram">
082 * <DT>Read Changes<DD>This should read every variable that's marked as changed.
083 * Currently, we use a common definition of changed with the write operations,
084 * and that someday might have to change.
085 *
086 * </DL>
087 *
088 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2004, 2005, 2006
089 * @author D Miller Copyright 2003
090 * @author Howard G. Penny Copyright (C) 2005
091 * @author Dave Heap Copyright (C) 2014, 2019
092 * @see jmri.jmrit.symbolicprog.VariableValue#isChanged
093 */
094/*
095 * @startuml jmri/jmrit/symbolicprog/tabbedframe/doc-files/PaneProgPane-ReadAllSequenceDiagram.png
096 * actor User
097 * box "PaneProgPane"
098 * participant readPaneAll
099 * participant prepReadPane
100 * participant nextRead
101 * participant executeRead
102 * participant propertyChange
103 * participant replyWhileProgrammingVar
104 * participant restartProgramming
105 * end box
106 * box "VariableValue"
107 * participant readAll
108 * participant readChanges
109 * end box
110 *
111 * control Programmer
112 * User -> readPaneAll: Read All Sheets
113 * activate readPaneAll
114 * readPaneAll -> prepReadPane
115 * activate prepReadPane
116 * prepReadPane --> readPaneAll
117 * deactivate prepReadPane
118 * deactivate prepReadPane
119 * readPaneAll -> nextRead
120 * activate nextRead
121 * nextRead -> executeRead
122 * activate executeRead
123 * executeRead -> readAll
124 * activate readAll
125 * readAll -> Programmer
126 * activate Programmer
127 * readAll --> executeRead
128 * deactivate readAll
129 * executeRead --> nextRead
130 * deactivate executeRead
131 * nextRead --> readPaneAll
132 * deactivate nextRead
133 * deactivate readPaneAll
134 * == Callback after read completes ==
135 * Programmer -> propertyChange
136 * activate propertyChange
137 * note over propertyChange
138 * if the first read failed,
139 * setup a second read of
140 * the same value.
141 * otherwise, setup a read of
142 * the next value.
143 * end note
144 * deactivate Programmer
145 * propertyChange -> User: CV value or error
146 * propertyChange -> replyWhileProgrammingVar
147 * activate replyWhileProgrammingVar
148 * replyWhileProgrammingVar -> restartProgramming
149 * activate restartProgramming
150 * restartProgramming -> nextRead
151 * activate nextRead
152 * nextRead -> executeRead
153 * activate executeRead
154 * executeRead -> readAll
155 * activate readAll
156 * readAll -> Programmer
157 * activate Programmer
158 * readAll --> executeRead
159 * deactivate readAll
160 * executeRead -> nextRead
161 * deactivate executeRead
162 * nextRead --> restartProgramming
163 * deactivate nextRead
164 * restartProgramming --> replyWhileProgrammingVar
165 * deactivate restartProgramming
166 * replyWhileProgrammingVar --> propertyChange
167 * deactivate replyWhileProgrammingVar
168 * deactivate propertyChange
169 * deactivate Programmer
170 * == Callback triggered repeat occurs until no more values ==
171 * @enduml
172 */
173public class PaneProgPane extends javax.swing.JPanel
174        implements java.beans.PropertyChangeListener {
175
176    static final String LAST_GRIDX = "last_gridx";
177    static final String LAST_GRIDY = "last_gridy";
178
179    protected CvTableModel _cvModel;
180    protected VariableTableModel _varModel;
181    protected PaneContainer container;
182    protected RosterEntry rosterEntry;
183
184    boolean _cvTable;
185
186    protected JPanel bottom;
187
188    transient ItemListener l1;
189    protected transient ItemListener l2;
190    transient ItemListener l3;
191    protected transient ItemListener l4;
192    transient ItemListener l5;
193    transient ItemListener l6;
194
195    boolean isCvTablePane = false;
196
197    /**
198     * Store name of this programmer Tab (pane)
199     */
200    String mName = "";
201
202    /**
203     * Construct a null object.
204     * <p>
205     * Normally only used for tests and to pre-load classes.
206     */
207    public PaneProgPane() {
208    }
209
210    public PaneProgPane(PaneContainer parent, String name, Element pane, CvTableModel cvModel, VariableTableModel varModel, Element modelElem, RosterEntry pRosterEntry) {
211        this(parent, name, pane, cvModel, varModel, modelElem, pRosterEntry, false);
212    }
213
214    /**
215     * Construct the Pane from the XML definition element.
216     *
217     * In case this is invoked from off the Swing/AWT thread, 
218     * it defers to that thread in a granular manner.
219     *
220     * @param parent       The parent pane
221     * @param name         Name to appear on tab of pane
222     * @param pane         The JDOM Element for the pane definition
223     * @param cvModel      Already existing TableModel containing the CV
224     *                     definitions
225     * @param varModel     Already existing TableModel containing the variable
226     *                     definitions
227     * @param modelElem    "model" element from the Decoder Index, used to check
228     *                     what decoder options are present.
229     * @param pRosterEntry The current roster entry, used to get sound labels.
230     * @param isProgPane   True if the pane is a default programmer pane
231     */
232    public PaneProgPane(PaneContainer parent, String name, Element pane, CvTableModel cvModel, VariableTableModel varModel, Element modelElem, RosterEntry pRosterEntry, boolean isProgPane) {
233
234        container = parent;
235        mName = name;
236        _cvModel = cvModel;
237        _varModel = varModel;
238        rosterEntry = pRosterEntry;
239
240        // when true a cv table with compare was loaded into pane
241        _cvTable = false;
242
243        // This is a JPanel containing a JScrollPane, containing a
244        // laid-out JPanel
245        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
246
247        // Add tooltip (if available)
248        setToolTipText(jmri.util.jdom.LocaleSelector.getAttribute(pane, "tooltip"));
249
250        // find out whether to display "label" (false) or "item" (true)
251        Attribute nameFmt = pane.getAttribute("nameFmt");
252        final boolean showItem = (nameFmt != null && nameFmt.getValue().equals("item"));
253        if (showItem) {
254            log.debug("Pane {} will show items, not labels, from decoder file", name);
255        }
256        // put the columns left to right in a panel
257        JPanel p = new JPanel();
258        panelList.add(p);
259        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
260
261        // handle the xml definition
262        // for all "column" elements ...
263        List<Element> colList = pane.getChildren("column");
264        for (Element element : colList) {
265            ThreadingUtil.runOnGUI(() -> {
266                // load each column
267                p.add(newColumn(element, showItem, modelElem));
268            });
269        }
270        // for all "row" elements ...
271        List<Element> rowList = pane.getChildren("row");
272        for (Element element : rowList) {
273            ThreadingUtil.runOnGUI(() -> {
274                // load each row
275                p.add(newRow(element, showItem, modelElem));
276            });
277        }
278        // for all "grid" elements ...
279        List<Element> gridList = pane.getChildren("grid");
280        for (Element element : gridList) {
281            ThreadingUtil.runOnGUI(() -> {
282                // load each grid
283                p.add(newGrid(element, showItem, modelElem));
284            });
285        }
286        // for all "group" elements ...
287        List<Element> groupList = pane.getChildren("group");
288        for (Element element : groupList) {
289            ThreadingUtil.runOnGUI(() -> {
290                // load each group
291                p.add(newGroup(element, showItem, modelElem));
292            });
293        }
294
295        // explain why pane is empty
296        if (cvList.isEmpty() && varList.isEmpty() && isProgPane) {
297            JPanel pe = new JPanel();
298            pe.setLayout(new BoxLayout(pe, BoxLayout.Y_AXIS));
299            int line = 1;
300            while (line >= 0) {
301                try {
302                    String msg = SymbolicProgBundle.getMessage("TextTabEmptyExplain" + line);
303                    if (msg.isEmpty()) {
304                        msg = " ";
305                    }
306                    JLabel l = new JLabel(msg);
307                    l.setAlignmentX(Component.CENTER_ALIGNMENT);
308                    pe.add(l);
309                    line++;
310                } catch (java.util.MissingResourceException e) {  // deliberately runs until exception
311                    line = -1;
312                }
313            }
314            add(pe);
315            panelList.add(pe);
316            return;
317        }
318
319        // add glue to the right to allow resize - but this isn't working as expected? Alignment?
320        add(Box.createHorizontalGlue());
321
322        add(new JScrollPane(p));
323
324        // add buttons in a new panel
325        bottom = new JPanel();
326        panelList.add(p);
327        bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));
328
329        // enable read buttons, if possible, and
330        // set their tool tips
331        enableReadButtons();
332
333        // add read button listeners
334        readChangesButton.addItemListener(l1 = (ItemEvent e) -> {
335            if (e.getStateChange() == ItemEvent.SELECTED) {
336                readChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopReadChangesSheet"));
337                if (!container.isBusy()) {
338                    prepReadPane(true);
339                    prepGlassPane(readChangesButton);
340                    container.getBusyGlassPane().setVisible(true);
341                    readPaneChanges();
342                }
343            } else {
344                stopProgramming();
345                readChangesButton.setText(SymbolicProgBundle.getMessage("ButtonReadChangesSheet"));
346                if (container.isBusy()) {
347                    readChangesButton.setEnabled(false);
348                }
349            }
350        });
351        readAllButton.addItemListener(l2 = (ItemEvent e) -> {
352            if (e.getStateChange() == ItemEvent.SELECTED) {
353                readAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopReadSheet"));
354                if (!container.isBusy()) {
355                    prepReadPane(false);
356                    prepGlassPane(readAllButton);
357                    container.getBusyGlassPane().setVisible(true);
358                    readPaneAll();
359                }
360            } else {
361                stopProgramming();
362                readAllButton.setText(SymbolicProgBundle.getMessage("ButtonReadFullSheet"));
363                if (container.isBusy()) {
364                    readAllButton.setEnabled(false);
365                }
366            }
367        });
368
369        writeChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipWriteHighlightedSheet"));
370        writeChangesButton.addItemListener(l3 = (ItemEvent e) -> {
371            if (e.getStateChange() == ItemEvent.SELECTED) {
372                writeChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopWriteChangesSheet"));
373                if (!container.isBusy()) {
374                    prepWritePane(true);
375                    prepGlassPane(writeChangesButton);
376                    container.getBusyGlassPane().setVisible(true);
377                    writePaneChanges();
378                }
379            } else {
380                stopProgramming();
381                writeChangesButton.setText(SymbolicProgBundle.getMessage("ButtonWriteChangesSheet"));
382                if (container.isBusy()) {
383                    writeChangesButton.setEnabled(false);
384                }
385            }
386        });
387
388        writeAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipWriteAllSheet"));
389        writeAllButton.addItemListener(l4 = (ItemEvent e) -> {
390            if (e.getStateChange() == ItemEvent.SELECTED) {
391                writeAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopWriteSheet"));
392                if (!container.isBusy()) {
393                    prepWritePane(false);
394                    prepGlassPane(writeAllButton);
395                    container.getBusyGlassPane().setVisible(true);
396                    writePaneAll();
397                }
398            } else {
399                stopProgramming();
400                writeAllButton.setText(SymbolicProgBundle.getMessage("ButtonWriteFullSheet"));
401                if (container.isBusy()) {
402                    writeAllButton.setEnabled(false);
403                }
404            }
405        });
406
407        // enable confirm buttons, if possible, and
408        // set their tool tips
409        enableConfirmButtons();
410
411        // add confirm button listeners
412        confirmChangesButton.addItemListener(l5 = (ItemEvent e) -> {
413            if (e.getStateChange() == ItemEvent.SELECTED) {
414                confirmChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopConfirmChangesSheet"));
415                if (!container.isBusy()) {
416                    prepConfirmPane(true);
417                    prepGlassPane(confirmChangesButton);
418                    container.getBusyGlassPane().setVisible(true);
419                    confirmPaneChanges();
420                }
421            } else {
422                stopProgramming();
423                confirmChangesButton.setText(SymbolicProgBundle.getMessage("ButtonConfirmChangesSheet"));
424                if (container.isBusy()) {
425                    confirmChangesButton.setEnabled(false);
426                }
427            }
428        });
429        confirmAllButton.addItemListener(l6 = (ItemEvent e) -> {
430            if (e.getStateChange() == ItemEvent.SELECTED) {
431                confirmAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopConfirmSheet"));
432                if (!container.isBusy()) {
433                    prepConfirmPane(false);
434                    prepGlassPane(confirmAllButton);
435                    container.getBusyGlassPane().setVisible(true);
436                    confirmPaneAll();
437                }
438            } else {
439                stopProgramming();
440                confirmAllButton.setText(SymbolicProgBundle.getMessage("ButtonConfirmFullSheet"));
441                if (container.isBusy()) {
442                    confirmAllButton.setEnabled(false);
443                }
444            }
445        });
446
447//      Only add change buttons to CV tables
448        bottom.add(readChangesButton);
449        bottom.add(writeChangesButton);
450        if (_cvTable) {
451            bottom.add(confirmChangesButton);
452        }
453        bottom.add(readAllButton);
454        bottom.add(writeAllButton);
455        if (_cvTable) {
456            bottom.add(confirmAllButton);
457        }
458
459        // don't show buttons if no programmer at all
460        if (_cvModel.getProgrammer() != null) {
461            add(bottom);
462        }
463    }
464
465    public void setNoDecoder() {
466        readChangesButton.setEnabled(false);
467        readAllButton.setEnabled(false);
468        writeChangesButton.setEnabled(false);
469        writeAllButton.setEnabled(false);
470        confirmChangesButton.setEnabled(false);
471        confirmAllButton.setEnabled(false);
472    }
473
474    @Override
475    public String getName() {
476        return mName;
477    }
478
479    @Override
480    public String toString() {
481        return getName();
482    }
483
484    /**
485     * Enable the read all and read changes button if possible. This checks to
486     * make sure this is appropriate, given the attached programmer's
487     * capability.
488     */
489    void enableReadButtons() {
490        readChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipReadChangesSheet"));
491        readAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipReadAllSheet"));
492        if (_cvModel.getProgrammer() != null
493                && !_cvModel.getProgrammer().getCanRead()) {
494            // can't read, disable the buttons
495            readChangesButton.setEnabled(false);
496            readAllButton.setEnabled(false);
497            // set tooltip to explain why
498            readChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
499            readAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
500        } else {
501            readChangesButton.setEnabled(true);
502            readAllButton.setEnabled(true);
503        }
504    }
505
506    /**
507     * Enable the compare all and compare changes button if possible. This
508     * checks to make sure this is appropriate, given the attached programmer's
509     * capability.
510     */
511    void enableConfirmButtons() {
512        confirmChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipConfirmChangesSheet"));
513        confirmAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipConfirmAllSheet"));
514        if (_cvModel.getProgrammer() != null
515                && !_cvModel.getProgrammer().getCanRead()) {
516            // can't read, disable the buttons
517            confirmChangesButton.setEnabled(false);
518            confirmAllButton.setEnabled(false);
519            // set tooltip to explain why
520            confirmChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
521            confirmAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
522        } else {
523            confirmChangesButton.setEnabled(true);
524            confirmAllButton.setEnabled(true);
525        }
526    }
527
528    /**
529     * This remembers the variables on this pane for the Read/Write sheet
530     * operation. They are stored as a list of Integer objects, each of which is
531     * the index of the Variable in the VariableTable.
532     */
533    List<Integer> varList = new ArrayList<>();
534    int varListIndex;
535    /**
536     * This remembers the CVs on this pane for the Read/Write sheet operation.
537     * They are stored as a set of Integer objects, each of which is the index
538     * of the CV in the CVTable. Note that variables are handled separately, and
539     * the CVs that are represented by variables are not entered here. So far
540     * (sic), the only use of this is for the cvtable rep.
541     */
542    protected TreeSet<Integer> cvList = new TreeSet<>(); //  TreeSet is iterated in order
543    protected Iterator<Integer> cvListIterator;
544
545    protected JToggleButton readChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonReadChangesSheet"));
546    protected JToggleButton readAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonReadFullSheet"));
547    protected JToggleButton writeChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonWriteChangesSheet"));
548    protected JToggleButton writeAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonWriteFullSheet"));
549    JToggleButton confirmChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonConfirmChangesSheet"));
550    JToggleButton confirmAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonConfirmFullSheet"));
551
552    /**
553     * Estimate the number of CVs that will be accessed when reading or writing
554     * the contents of this pane.
555     *
556     * @param read    true if counting for read, false for write
557     * @param changes true if counting for a *Changes operation; false, if
558     *                counting for a *All operation
559     * @return the total number of CV reads/writes needed for this pane
560     */
561    public int countOpsNeeded(boolean read, boolean changes) {
562        Set<Integer> set = new HashSet<>(cvList.size() + varList.size() + 50);
563        return makeOpsNeededSet(read, changes, set).size();
564    }
565
566    /**
567     * Produce a set of CVs that will be accessed when reading or writing the
568     * contents of this pane.
569     *
570     * @param read    true if counting for read, false for write
571     * @param changes true if counting for a *Changes operation; false, if
572     *                counting for a *All operation
573     * @param set     The set to fill. Any CVs already in here will not be
574     *                duplicated, which provides a way to aggregate a set of CVs
575     *                across multiple panes.
576     * @return the same set as the parameter, for convenient chaining of
577     *         operations.
578     */
579    public Set<Integer> makeOpsNeededSet(boolean read, boolean changes, Set<Integer> set) {
580
581        // scan the variable list
582        for (int varNum : varList) {
583
584            VariableValue var = _varModel.getVariable(varNum);
585
586            // must decide whether this one should be counted
587            if (!changes || var.isChanged()) {
588
589                CvValue[] cvs = var.usesCVs();
590                for (CvValue cv : cvs) {
591                    // always of interest
592                    if (!changes || VariableValue.considerChanged(cv)) {
593                        set.add(Integer.valueOf(cv.number()));
594                    }
595                }
596            }
597        }
598
599        return set;
600    }
601
602    private void prepGlassPane(AbstractButton activeButton) {
603        container.prepGlassPane(activeButton);
604    }
605
606    void enableButtons(boolean stat) {
607        if (stat) {
608            enableReadButtons();
609            enableConfirmButtons();
610        } else {
611            readChangesButton.setEnabled(stat);
612            readAllButton.setEnabled(stat);
613            confirmChangesButton.setEnabled(stat);
614            confirmAllButton.setEnabled(stat);
615        }
616        writeChangesButton.setEnabled(stat);
617        writeAllButton.setEnabled(stat);
618    }
619
620    boolean justChanges;
621
622    /**
623     * Invoked by "Read changes on sheet" button, this sets in motion a
624     * continuing sequence of "read" operations on the variables and
625     * CVs in the Pane. Only variables in states marked as "changed" will be
626     * read.
627     *
628     * @return true is a read has been started, false if the pane is complete.
629     */
630    public boolean readPaneChanges() {
631        if (log.isDebugEnabled()) {
632            log.debug("readPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
633        }
634        prepReadPane(true);
635        return nextRead();
636    }
637
638    /**
639     * Prepare this pane for a read operation.
640     * <p>
641     * The read mechanism only reads variables in certain states (and needs to
642     * do that to handle error processing right now), so this is implemented by
643     * first setting all variables and CVs on this pane to TOREAD via this
644     * method
645     *
646     * @param onlyChanges true if only reading changes; false if reading all
647     */
648    public void prepReadPane(boolean onlyChanges) {
649        log.debug("start prepReadPane with onlyChanges={}", onlyChanges);
650        justChanges = onlyChanges;
651
652        if (isCvTablePane) {
653            setCvListFromTable();  // make sure list of CVs up to date if table
654        }
655        enableButtons(false);
656        if (justChanges) {
657            readChangesButton.setEnabled(true);
658            readChangesButton.setSelected(true);
659        } else {
660            readAllButton.setSelected(true);
661            readAllButton.setEnabled(true);
662        }
663        if (!container.isBusy()) {
664            container.enableButtons(false);
665        }
666        setToRead(justChanges, true);
667        varListIndex = 0;
668        cvListIterator = cvList.iterator();
669        cvReadSoFar = 0 ;
670        cvReadStartTime = System.currentTimeMillis();
671    }
672
673    /**
674     * Invoked by "Read Full Sheet" button, this sets in motion a continuing
675     * sequence of "read" operations on the variables and CVs in the
676     * Pane. The read mechanism only reads variables in certain states (and
677     * needs to do that to handle error processing right now), so this is
678     * implemented by first setting all variables and CVs on this pane to TOREAD
679     * in prepReadPaneAll, then starting the execution.
680     *
681     * @return true is a read has been started, false if the pane is complete
682     */
683    public boolean readPaneAll() {
684        if (log.isDebugEnabled()) {
685            log.debug("readAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
686        }
687        prepReadPane(false);
688        // start operation
689        return nextRead();
690    }
691
692    /**
693     * Set the "ToRead" parameter in all variables and CVs on this pane.
694     *
695     * @param justChanges  true if this is read changes, false if read all
696     * @param startProcess true if this is the start of processing, false if
697     *                     cleaning up at end
698     */
699    void setToRead(boolean justChanges, boolean startProcess) {
700        if (!container.isBusy()
701                || // the frame has already setToRead
702                (!startProcess)) {  // we want to setToRead false if the pane's process is being stopped
703            for (int varNum : varList) {
704                VariableValue var = _varModel.getVariable(varNum);
705                if (justChanges) {
706                    if (var.isChanged()) {
707                        var.setToRead(startProcess);
708                    } else {
709                        var.setToRead(false);
710                    }
711                } else {
712                    var.setToRead(startProcess);
713                }
714            }
715
716            if (isCvTablePane) {
717                setCvListFromTable();  // make sure list of CVs up to date if table
718            }
719            for (int cvNum : cvList) {
720                CvValue cv = _cvModel.getCvByRow(cvNum);
721                if (justChanges) {
722                    if (VariableValue.considerChanged(cv)) {
723                        cv.setToRead(startProcess);
724                    } else {
725                        cv.setToRead(false);
726                    }
727                } else {
728                    cv.setToRead(startProcess);
729                }
730            }
731        }
732    }
733
734    /**
735     * Set the "ToWrite" parameter in all variables and CVs on this pane
736     *
737     * @param justChanges  true if this is read changes, false if read all
738     * @param startProcess true if this is the start of processing, false if
739     *                     cleaning up at end
740     */
741    void setToWrite(boolean justChanges, boolean startProcess) {
742        log.debug("start setToWrite method with {},{}", justChanges, startProcess);
743        if (!container.isBusy()
744                || // the frame has already setToWrite
745                (!startProcess)) {  // we want to setToWrite false if the pane's process is being stopped
746            log.debug("about to start setToWrite of varList");
747            for (int varNum : varList) {
748                VariableValue var = _varModel.getVariable(varNum);
749                if (justChanges) {
750                    if (var.isChanged()) {
751                        var.setToWrite(startProcess);
752                    } else {
753                        var.setToWrite(false);
754                    }
755                } else {
756                    var.setToWrite(startProcess);
757                }
758            }
759
760            log.debug("about to start setToWrite of cvList");
761            if (isCvTablePane) {
762                setCvListFromTable();  // make sure list of CVs up to date if table
763            }
764            for (int cvNum : cvList) {
765                CvValue cv = _cvModel.getCvByRow(cvNum);
766                if (justChanges) {
767                    if (VariableValue.considerChanged(cv)) {
768                        cv.setToWrite(startProcess);
769                    } else {
770                        cv.setToWrite(false);
771                    }
772                } else {
773                    cv.setToWrite(startProcess);
774                }
775            }
776        }
777        log.debug("end setToWrite method");
778    }
779
780    void executeRead(VariableValue var) {
781        setBusy(true);
782        // var.setToRead(false);  // variables set this themselves
783        if (_programmingVar != null) {
784            log.error("listener already set at read start");
785        }
786        _programmingVar = var;
787        _read = true;
788        // get notified when that state changes so can repeat
789        _programmingVar.addPropertyChangeListener(this);
790        // and make the read request
791        if (justChanges) {
792            _programmingVar.readChanges();
793        } else {
794            _programmingVar.readAll();
795        }
796    }
797
798    void executeWrite(VariableValue var) {
799        setBusy(true);
800        // var.setToWrite(false);   // variables reset themselves when done
801        if (_programmingVar != null) {
802            log.error("listener already set at write start");
803        }
804        _programmingVar = var;
805        _read = false;
806        // get notified when that state changes so can repeat
807        _programmingVar.addPropertyChangeListener(this);
808        // and make the write request
809        if (justChanges) {
810            _programmingVar.writeChanges();
811        } else {
812            _programmingVar.writeAll();
813        }
814    }
815
816    // keep track of multi reads.
817    long  cvReadSoFar;
818    long  cvReadStartTime;
819
820    /**
821     * If there are any more read operations to be done on this pane, do the
822     * next one.
823     * <p>
824     * Each invocation of this method reads one variable or CV; completion of
825     * that request will cause it to happen again, reading the next one, until
826     * there's nothing left to read.
827     * @return true is a read has been started, false if the pane is complete.
828     */
829    boolean nextRead() {
830        // look for possible variables
831        if (log.isDebugEnabled()) {
832            log.debug("nextRead scans {} variables", varList.size());
833        }
834        while ((varList.size() > 0) && (varListIndex < varList.size())) {
835            int varNum = varList.get(varListIndex);
836            AbstractValue.ValueState vState = _varModel.getState(varNum);
837            VariableValue var = _varModel.getVariable(varNum);
838            if (log.isDebugEnabled()) {
839                log.debug("nextRead var index {} state {} isToRead: {} label: {}", varNum, vState.getName(), var.isToRead(), var.label());
840            }
841            varListIndex++;
842            if (var.isToRead()) {
843                if (log.isDebugEnabled()) {
844                    log.debug("start read of variable {}", _varModel.getLabel(varNum));
845                }
846                executeRead(var);
847
848                log.debug("return from starting var read");
849                // the request may have instantaneously been satisfied...
850                return true;  // only make one request at a time!
851            }
852        }
853        // found no variables needing read, try CVs
854        if (log.isDebugEnabled()) {
855            log.debug("nextRead scans {} CVs", cvList.size());
856        }
857
858        while (cvListIterator != null && cvListIterator.hasNext()) {
859            int cvNum = cvListIterator.next();
860            cvReadSoFar++;
861            CvValue cv = _cvModel.getCvByRow(cvNum);
862            if (log.isDebugEnabled()) {
863                log.debug("nextRead cv index {} state {}", cvNum, cv.getState());
864            }
865
866            if (cv.isToRead()) {  // always read UNKNOWN state
867                log.debug("start read of cv {}", cvNum);
868                setBusy(true);
869                if (_programmingCV != null) {
870                    log.error("listener already set at read start");
871                }
872                _programmingCV = _cvModel.getCvByRow(cvNum);
873                _read = true;
874                // get notified when that state changes so can repeat
875                _programmingCV.addPropertyChangeListener(this);
876                // and make the read request
877                // _programmingCV.setToRead(false);  // CVs set this themselves
878                _programmingCV.read(_cvModel.getStatusLabel(), cvReadSoFar, cvList.size(), cvReadStartTime);
879                log.debug("return from starting CV read");
880                // the request may have instantateously been satisfied...
881                return true;  // only make one request at a time!
882            }
883        }
884        // nothing to program, end politely
885        log.debug("nextRead found nothing to do");
886        readChangesButton.setSelected(false);
887        readAllButton.setSelected(false);  // reset both, as that's final state we want
888        setBusy(false);
889        container.paneFinished();
890        return false;
891    }
892
893    /**
894     * If there are any more compare operations to be done on this pane, do the
895     * next one.
896     * <p>
897     * Each invocation of this method compares one CV; completion of that request
898     * will cause it to happen again, reading the next one, until there's
899     * nothing left to read.
900     *
901     * @return true is a compare has been started, false if the pane is
902     *         complete.
903     */
904    boolean nextConfirm() {
905        // look for possible CVs
906        while (cvListIterator != null && cvListIterator.hasNext()) {
907            int cvNum = cvListIterator.next();
908            CvValue cv = _cvModel.getCvByRow(cvNum);
909            if (log.isDebugEnabled()) {
910                log.debug("nextConfirm cv index {} state {}", cvNum, cv.getState());
911            }
912
913            if (cv.isToRead()) {
914                log.debug("start confirm of cv {}", cvNum);
915                setBusy(true);
916                if (_programmingCV != null) {
917                    log.error("listener already set at confirm start");
918                }
919                _programmingCV = _cvModel.getCvByRow(cvNum);
920                _read = true;
921                // get notified when that state changes so can repeat
922                _programmingCV.addPropertyChangeListener(this);
923                // and make the compare request
924                _programmingCV.confirm(_cvModel.getStatusLabel());
925                log.debug("return from starting CV confirm");
926                // the request may have instantateously been satisfied...
927                return true;  // only make one request at a time!
928            }
929        }
930        // nothing to program, end politely
931        log.debug("nextConfirm found nothing to do");
932        confirmChangesButton.setSelected(false);
933        confirmAllButton.setSelected(false);  // reset both, as that's final state we want
934        setBusy(false);
935        container.paneFinished();
936        return false;
937    }
938
939    /**
940     * Invoked by "Write changes on sheet" button, this sets in motion a
941     * continuing sequence of "write" operations on the variables in the Pane.
942     * Only variables in isChanged states are written; other states don't need
943     * to be.
944     *
945     * @return true if a write has been started, false if the pane is complete
946     */
947    public boolean writePaneChanges() {
948        log.debug("writePaneChanges starts");
949        prepWritePane(true);
950        boolean val = nextWrite();
951        log.debug("writePaneChanges returns {}", val);
952        return val;
953    }
954
955    /**
956     * Invoked by "Write full sheet" button to write all CVs.
957     *
958     * @return true if a write has been started, false if the pane is complete
959     */
960    public boolean writePaneAll() {
961        prepWritePane(false);
962        return nextWrite();
963    }
964
965    /**
966     * Prepare a "write full sheet" operation.
967     *
968     * @param onlyChanges true if only writing changes; false if writing all
969     */
970    public void prepWritePane(boolean onlyChanges) {
971        log.debug("start prepWritePane with {}", onlyChanges);
972        justChanges = onlyChanges;
973        enableButtons(false);
974
975        if (isCvTablePane) {
976            setCvListFromTable();  // make sure list of CVs up to date if table
977        }
978        if (justChanges) {
979            writeChangesButton.setEnabled(true);
980            writeChangesButton.setSelected(true);
981        } else {
982            writeAllButton.setSelected(true);
983            writeAllButton.setEnabled(true);
984        }
985        if (!container.isBusy()) {
986            container.enableButtons(false);
987        }
988        setToWrite(justChanges, true);
989        varListIndex = 0;
990
991        cvListIterator = cvList.iterator();
992        log.debug("end prepWritePane");
993    }
994
995    boolean nextWrite() {
996        log.debug("start nextWrite");
997        // look for possible variables
998        while ((varList.size() > 0) && (varListIndex < varList.size())) {
999            int varNum = varList.get(varListIndex);
1000            AbstractValue.ValueState vState = _varModel.getState(varNum);
1001            VariableValue var = _varModel.getVariable(varNum);
1002            if (log.isDebugEnabled()) {
1003                log.debug("nextWrite var index {} state {} isToWrite: {} label:{}", varNum, vState.getName(), var.isToWrite(), var.label());
1004            }
1005            varListIndex++;
1006            if (var.isToWrite()) {
1007                log.debug("start write of variable {}", _varModel.getLabel(varNum));
1008
1009                executeWrite(var);
1010
1011                log.debug("return from starting var write");
1012                return true;  // only make one request at a time!
1013            }
1014        }
1015        // check for CVs to handle (e.g. for CV table)
1016        while (cvListIterator != null && cvListIterator.hasNext()) {
1017            int cvNum = cvListIterator.next();
1018            CvValue cv = _cvModel.getCvByRow(cvNum);
1019            if (log.isDebugEnabled()) {
1020                log.debug("nextWrite cv index {} state {}", cvNum, cv.getState());
1021            }
1022
1023            if (cv.isToWrite()) {
1024                log.debug("start write of cv index {}", cvNum);
1025                setBusy(true);
1026                if (_programmingCV != null) {
1027                    log.error("listener already set at write start");
1028                }
1029                _programmingCV = _cvModel.getCvByRow(cvNum);
1030                _read = false;
1031                // get notified when that state changes so can repeat
1032                _programmingCV.addPropertyChangeListener(this);
1033                // and make the write request
1034                // _programmingCV.setToWrite(false);  // CVs set this themselves
1035                _programmingCV.write(_cvModel.getStatusLabel());
1036                log.debug("return from starting cv write");
1037                return true;  // only make one request at a time!
1038            }
1039        }
1040        // nothing to program, end politely
1041        log.debug("nextWrite found nothing to do");
1042        writeChangesButton.setSelected(false);
1043        writeAllButton.setSelected(false);
1044        setBusy(false);
1045        container.paneFinished();
1046        log.debug("return from nextWrite with nothing to do");
1047        return false;
1048    }
1049
1050    /**
1051     * Prepare this pane for a compare operation.
1052     * <p>
1053     * The read mechanism only reads variables in certain states (and needs to
1054     * do that to handle error processing right now), so this is implemented by
1055     * first setting all variables and CVs on this pane to TOREAD via this
1056     * method
1057     *
1058     * @param onlyChanges true if only confirming changes; false if confirming
1059     *                    all
1060     */
1061    public void prepConfirmPane(boolean onlyChanges) {
1062        log.debug("start prepReadPane with onlyChanges={}", onlyChanges);
1063        justChanges = onlyChanges;
1064        enableButtons(false);
1065
1066        if (isCvTablePane) {
1067            setCvListFromTable();  // make sure list of CVs up to date if table
1068        }
1069        if (justChanges) {
1070            confirmChangesButton.setEnabled(true);
1071            confirmChangesButton.setSelected(true);
1072        } else {
1073            confirmAllButton.setSelected(true);
1074            confirmAllButton.setEnabled(true);
1075        }
1076        if (!container.isBusy()) {
1077            container.enableButtons(false);
1078        }
1079        // we can use the read prep since confirm has to read first
1080        setToRead(justChanges, true);
1081        varListIndex = 0;
1082
1083        cvListIterator = cvList.iterator();
1084    }
1085
1086    /**
1087     * Invoked by "Compare changes on sheet" button, this sets in motion a
1088     * continuing sequence of "confirm" operations on the variables and
1089     * CVs in the Pane. Only variables in states marked as "changed" will be
1090     * checked.
1091     *
1092     * @return true is a confirm has been started, false if the pane is
1093     *         complete.
1094     */
1095    public boolean confirmPaneChanges() {
1096        if (log.isDebugEnabled()) {
1097            log.debug("confirmPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
1098        }
1099        prepConfirmPane(true);
1100        return nextConfirm();
1101    }
1102
1103    /**
1104     * Invoked by "Compare Full Sheet" button, this sets in motion a continuing
1105     * sequence of "confirm" operations on the variables and CVs in the
1106     * Pane. The read mechanism only reads variables in certain states (and
1107     * needs to do that to handle error processing right now), so this is
1108     * implemented by first setting all variables and CVs on this pane to TOREAD
1109     * in prepReadPaneAll, then starting the execution.
1110     *
1111     * @return true is a confirm has been started, false if the pane is
1112     *         complete.
1113     */
1114    public boolean confirmPaneAll() {
1115        if (log.isDebugEnabled()) {
1116            log.debug("confirmAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
1117        }
1118        prepConfirmPane(false);
1119        // start operation
1120        return nextConfirm();
1121    }
1122
1123    // reference to variable being programmed (or null if none)
1124    VariableValue _programmingVar = null;
1125    CvValue _programmingCV = null;
1126    boolean _read = true;
1127
1128    // busy during read, write operations
1129    private boolean _busy = false;
1130
1131    public boolean isBusy() {
1132        return _busy;
1133    }
1134
1135    protected void setBusy(boolean busy) {
1136        boolean oldBusy = _busy;
1137        _busy = busy;
1138        if (!busy && !container.isBusy()) {
1139            enableButtons(true);
1140        }
1141        if (oldBusy != busy) {
1142            firePropertyChange("Busy", Boolean.valueOf(oldBusy), Boolean.valueOf(busy));
1143        }
1144    }
1145
1146    private int retry = 0;
1147
1148    /**
1149     * Get notification of a variable property change, specifically "busy" going
1150     * to false at the end of a programming operation. If we're in a programming
1151     * operation, we then continue it by reinvoking the nextRead/writePane
1152     * operation.
1153     *
1154     * @param e the event to respond to
1155     */
1156    @Override
1157    public void propertyChange(java.beans.PropertyChangeEvent e) {
1158        // check for the right event & condition
1159        if (_programmingVar == null && _programmingCV == null ) {
1160            log.warn("unexpected propertChange: {}", e);
1161            return;
1162        } else if (log.isDebugEnabled()) {
1163            log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue());
1164        }
1165
1166        // find the right way to handle this
1167        if (e.getSource() == _programmingVar
1168                && e.getPropertyName().equals("Busy")
1169                && e.getNewValue().equals(Boolean.FALSE)) {
1170            if (_programmingVar.getState() == AbstractValue.ValueState.UNKNOWN) {
1171                if (retry == 0) {
1172                    varListIndex--;
1173                    retry++;
1174                    if (_read) {
1175                        _programmingVar.setToRead(true); // set the variable
1176                        // to read again.
1177                    } else {
1178                        _programmingVar.setToWrite(true); // set the variable
1179                        // to attempt another
1180                        // write.
1181                    }
1182                } else {
1183                    retry = 0;
1184                }
1185            }
1186            replyWhileProgrammingVar();
1187        } else if (e.getSource() == _programmingCV
1188                && e.getPropertyName().equals("Busy")
1189                && e.getNewValue().equals(Boolean.FALSE)) {
1190
1191            // there's no -- operator on the HashSet Iterator we're
1192            // using for the CV list, so we don't do individual retries
1193            // now.
1194            //if (_programmingCV.getState() == CvValue.UNKNOWN) {
1195            //    if (retry == 0) {
1196            //        cvListIndex--;
1197            //        retry++;
1198            //    } else {
1199            //        retry = 0;
1200            //    }
1201            //}
1202            replyWhileProgrammingCV();
1203        } else {
1204            if (log.isDebugEnabled() && e.getPropertyName().equals("Busy")) {
1205                log.debug("ignoring change of Busy {} {}", e.getNewValue(), e.getNewValue().equals(Boolean.FALSE));
1206            }
1207        }
1208    }
1209
1210    public void replyWhileProgrammingVar() {
1211        log.debug("correct event for programming variable, restart operation");
1212        // remove existing listener
1213        _programmingVar.removePropertyChangeListener(this);
1214        _programmingVar = null;
1215        // restart the operation
1216        restartProgramming();
1217    }
1218
1219    public void replyWhileProgrammingCV() {
1220        log.debug("correct event for programming CV, restart operation");
1221        // remove existing listener
1222        _programmingCV.removePropertyChangeListener(this);
1223        _programmingCV = null;
1224        // restart the operation
1225        restartProgramming();
1226    }
1227
1228    void restartProgramming() {
1229        log.debug("start restartProgramming");
1230        if (_read && readChangesButton.isSelected()) {
1231            nextRead();
1232        } else if (_read && readAllButton.isSelected()) {
1233            nextRead();
1234        } else if (_read && confirmChangesButton.isSelected()) {
1235            nextConfirm();
1236        } else if (_read && confirmAllButton.isSelected()) {
1237            nextConfirm();
1238        } else if (writeChangesButton.isSelected()) {
1239            nextWrite();   // was writePaneChanges
1240        } else if (writeAllButton.isSelected()) {
1241            nextWrite();
1242        } else {
1243            log.debug("No operation to restart");
1244            if (isBusy()) {
1245                container.paneFinished();
1246                setBusy(false);
1247            }
1248        }
1249        log.debug("end restartProgramming");
1250    }
1251
1252    protected void stopProgramming() {
1253        log.debug("start stopProgramming");
1254        setToRead(false, false);
1255        setToWrite(false, false);
1256        varListIndex = varList.size();
1257
1258        cvListIterator = null;
1259        log.debug("end stopProgramming");
1260    }
1261
1262    /**
1263     * Create a new group from the JDOM group Element
1264     *
1265     * @param element     element containing group contents
1266     * @param showStdName show the name following the rules for the
1267     * <em>nameFmt</em> element
1268     * @param modelElem   element containing the decoder model
1269     * @return a panel containing the group
1270     */
1271    protected JPanel newGroup(Element element, boolean showStdName, Element modelElem) {
1272
1273        // create a panel to add as a new column or row
1274        final JPanel c = new JPanel();
1275        panelList.add(c);
1276        GridBagLayout g = new GridBagLayout();
1277        GridBagConstraints cs = new GridBagConstraints();
1278        c.setLayout(g);
1279
1280        // handle include/exclude
1281        if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
1282            return c;
1283        }
1284
1285        // handle the xml definition
1286        // for all elements in the column or row
1287        List<Element> elemList = element.getChildren();
1288        log.trace("newColumn starting with {} elements", elemList.size());
1289        for (Element e : elemList) {
1290
1291            String name = e.getName();
1292            log.trace("newGroup processing {} element", name);
1293            // decode the type
1294            if (name.equals("display")) { // its a variable
1295                // load the variable
1296                newVariable(e, c, g, cs, showStdName);
1297            } else if (name.equals("separator")) { // its a separator
1298                JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
1299                cs.fill = GridBagConstraints.BOTH;
1300                cs.gridwidth = GridBagConstraints.REMAINDER;
1301                g.setConstraints(j, cs);
1302                c.add(j);
1303                cs.gridwidth = 1;
1304            } else if (name.equals("label")) {
1305                cs.gridwidth = GridBagConstraints.REMAINDER;
1306                makeLabel(e, c, g, cs);
1307            } else if (name.equals("soundlabel")) {
1308                cs.gridwidth = GridBagConstraints.REMAINDER;
1309                makeSoundLabel(e, c, g, cs);
1310            } else if (name.equals("cvtable")) {
1311                makeCvTable(cs, g, c);
1312            } else if (name.equals("fnmapping")) {
1313                pickFnMapPanel(c, g, cs, modelElem);
1314            } else if (name.equals("dccaddress")) {
1315                JPanel l = addDccAddressPanel(e);
1316                if (l.getComponentCount() > 0) {
1317                    cs.gridwidth = GridBagConstraints.REMAINDER;
1318                    g.setConstraints(l, cs);
1319                    c.add(l);
1320                    cs.gridwidth = 1;
1321                }
1322            } else if (name.equals("column")) {
1323                // nested "column" elements ...
1324                cs.gridheight = GridBagConstraints.REMAINDER;
1325                JPanel l = newColumn(e, showStdName, modelElem);
1326                if (l.getComponentCount() > 0) {
1327                    panelList.add(l);
1328                    g.setConstraints(l, cs);
1329                    c.add(l);
1330                    cs.gridheight = 1;
1331                }
1332            } else if (name.equals("row")) {
1333                // nested "row" elements ...
1334                cs.gridwidth = GridBagConstraints.REMAINDER;
1335                JPanel l = newRow(e, showStdName, modelElem);
1336                if (l.getComponentCount() > 0) {
1337                    panelList.add(l);
1338                    g.setConstraints(l, cs);
1339                    c.add(l);
1340                    cs.gridwidth = 1;
1341                }
1342            } else if (name.equals("grid")) {
1343                // nested "grid" elements ...
1344                cs.gridwidth = GridBagConstraints.REMAINDER;
1345                JPanel l = newGrid(e, showStdName, modelElem);
1346                if (l.getComponentCount() > 0) {
1347                    panelList.add(l);
1348                    g.setConstraints(l, cs);
1349                    c.add(l);
1350                    cs.gridwidth = 1;
1351                }
1352            } else if (name.equals("group")) {
1353                // nested "group" elements ...
1354                JPanel l = newGroup(e, showStdName, modelElem);
1355                if (l.getComponentCount() > 0) {
1356                    panelList.add(l);
1357                    g.setConstraints(l, cs);
1358                    c.add(l);
1359                }
1360            } else if (!name.equals("qualifier")) { // its a mistake
1361                log.error("No code to handle element of type {} in newColumn", e.getName());
1362            }
1363        }
1364        // add glue to the bottom to allow resize
1365        if (c.getComponentCount() > 0) {
1366            c.add(Box.createVerticalGlue());
1367        }
1368
1369        // handle qualification if any
1370        QualifierAdder qa = new QualifierAdder() {
1371            @Override
1372            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1373                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1374            }
1375
1376            @Override
1377            protected void addListener(java.beans.PropertyChangeListener qc) {
1378                c.addPropertyChangeListener(qc);
1379            }
1380        };
1381
1382        qa.processModifierElements(element, _varModel);
1383        return c;
1384    }
1385
1386    /**
1387     * Create a new grid group from the JDOM group Element.
1388     *
1389     * @param element     element containing group contents
1390     * @param c           the panel to create the grid in
1391     * @param g           the layout manager for the panel
1392     * @param globs       properties to configure g
1393     * @param showStdName show the name following the rules for the
1394     * <em>nameFmt</em> element
1395     * @param modelElem   element containing the decoder model
1396     */
1397    protected void newGridGroup(Element element, final JPanel c, GridBagLayout g, GridGlobals globs, boolean showStdName, Element modelElem) {
1398
1399        // handle include/exclude
1400        if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
1401            return;
1402        }
1403
1404        // handle the xml definition
1405        // for all elements in the column or row
1406        List<Element> elemList = element.getChildren();
1407        log.trace("newColumn starting with {} elements", elemList.size());
1408        for (Element e : elemList) {
1409
1410            String name = e.getName();
1411            log.trace("newGroup processing {} element", name);
1412            // decode the type
1413            if (name.equals("griditem")) {
1414                final JPanel l = newGridItem(e, showStdName, modelElem, globs);
1415                if (l.getComponentCount() > 0) {
1416                    panelList.add(l);
1417                    g.setConstraints(l, globs.gridConstraints);
1418                    c.add(l);
1419                    //                     globs.gridConstraints.gridwidth = 1;
1420                    // handle qualification if any
1421                    QualifierAdder qa = new QualifierAdder() {
1422                        @Override
1423                        protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1424                            return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
1425                        }
1426
1427                        @Override
1428                        protected void addListener(java.beans.PropertyChangeListener qc) {
1429                            l.addPropertyChangeListener(qc);
1430                        }
1431                    };
1432
1433                    qa.processModifierElements(e, _varModel);
1434                }
1435            } else if (name.equals("group")) {
1436                // nested "group" elements ...
1437                newGridGroup(e, c, g, globs, showStdName, modelElem);
1438            } else if (!name.equals("qualifier")) { // its a mistake
1439                log.error("No code to handle element of type {} in newColumn", e.getName());
1440            }
1441        }
1442        // add glue to the bottom to allow resize
1443//         if (c.getComponentCount() > 0) {
1444//             c.add(Box.createVerticalGlue());
1445//         }
1446
1447    }
1448
1449    /**
1450     * Create a single column from the JDOM column Element.
1451     *
1452     * @param element     element containing column contents
1453     * @param showStdName show the name following the rules for the
1454     * <em>nameFmt</em> element
1455     * @param modelElem   element containing the decoder model
1456     * @return a panel containing the group
1457     */
1458    public JPanel newColumn(Element element, boolean showStdName, Element modelElem) {
1459
1460        // create a panel to add as a new column or row
1461        final JPanel c = new JPanel();
1462        panelList.add(c);
1463        GridBagLayout g = new GridBagLayout();
1464        GridBagConstraints cs = new GridBagConstraints();
1465        c.setLayout(g);
1466
1467        // handle the xml definition
1468        // for all elements in the column or row
1469        List<Element> elemList = element.getChildren();
1470        log.trace("newColumn starting with {} elements", elemList.size());
1471        for (Element value : elemList) {
1472
1473            // update the grid position
1474            cs.gridy++;
1475            cs.gridx = 0;
1476
1477            String name = value.getName();
1478            log.trace("newColumn processing {} element", name);
1479            // decode the type
1480            if (name.equals("display")) { // it's a variable
1481                // load the variable
1482                newVariable(value, c, g, cs, showStdName);
1483            } else if (name.equals("separator")) { // its a separator
1484                JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
1485                cs.fill = GridBagConstraints.BOTH;
1486                cs.gridwidth = GridBagConstraints.REMAINDER;
1487                g.setConstraints(j, cs);
1488                c.add(j);
1489                cs.gridwidth = 1;
1490            } else if (name.equals("label")) {
1491                cs.gridwidth = GridBagConstraints.REMAINDER;
1492                makeLabel(value, c, g, cs);
1493            } else if (name.equals("soundlabel")) {
1494                cs.gridwidth = GridBagConstraints.REMAINDER;
1495                makeSoundLabel(value, c, g, cs);
1496            } else if (name.equals("cvtable")) {
1497                makeCvTable(cs, g, c);
1498            } else if (name.equals("fnmapping")) {
1499                pickFnMapPanel(c, g, cs, modelElem);
1500            } else if (name.equals("dccaddress")) {
1501                JPanel l = addDccAddressPanel(value);
1502                if (l.getComponentCount() > 0) {
1503                    cs.gridwidth = GridBagConstraints.REMAINDER;
1504                    g.setConstraints(l, cs);
1505                    c.add(l);
1506                    cs.gridwidth = 1;
1507                }
1508            } else if (name.equals("column")) {
1509                // nested "column" elements ...
1510                cs.gridheight = GridBagConstraints.REMAINDER;
1511                JPanel l = newColumn(value, showStdName, modelElem);
1512                if (l.getComponentCount() > 0) {
1513                    panelList.add(l);
1514                    g.setConstraints(l, cs);
1515                    c.add(l);
1516                    cs.gridheight = 1;
1517                }
1518            } else if (name.equals("row")) {
1519                // nested "row" elements ...
1520                cs.gridwidth = GridBagConstraints.REMAINDER;
1521                JPanel l = newRow(value, showStdName, modelElem);
1522                if (l.getComponentCount() > 0) {
1523                    panelList.add(l);
1524                    g.setConstraints(l, cs);
1525                    c.add(l);
1526                    cs.gridwidth = 1;
1527                }
1528            } else if (name.equals("grid")) {
1529                // nested "grid" elements ...
1530                cs.gridwidth = GridBagConstraints.REMAINDER;
1531                JPanel l = newGrid(value, showStdName, modelElem);
1532                if (l.getComponentCount() > 0) {
1533                    panelList.add(l);
1534                    g.setConstraints(l, cs);
1535                    c.add(l);
1536                    cs.gridwidth = 1;
1537                }
1538            } else if (name.equals("group")) {
1539                // nested "group" elements ...
1540                JPanel l = newGroup(value, showStdName, modelElem);
1541                if (l.getComponentCount() > 0) {
1542                    panelList.add(l);
1543                    g.setConstraints(l, cs);
1544                    c.add(l);
1545                }
1546            } else if (!name.equals("qualifier")) { // its a mistake
1547                log.error("No code to handle element of type {} in newColumn", value.getName());
1548            }
1549        }
1550        // add glue to the bottom to allow resize
1551        if (c.getComponentCount() > 0) {
1552            c.add(Box.createVerticalGlue());
1553        }
1554
1555        // handle qualification if any
1556        QualifierAdder qa = new QualifierAdder() {
1557            @Override
1558            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1559                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1560            }
1561
1562            @Override
1563            protected void addListener(java.beans.PropertyChangeListener qc) {
1564                c.addPropertyChangeListener(qc);
1565            }
1566        };
1567
1568        qa.processModifierElements(element, _varModel);
1569        return c;
1570    }
1571
1572    /**
1573     * Create a single row from the JDOM column Element
1574     *
1575     * @param element     element containing row contents
1576     * @param showStdName show the name following the rules for the
1577     * <em>nameFmt</em> element
1578     * @param modelElem   element containing the decoder model
1579     * @return a panel containing the group
1580     */
1581    public JPanel newRow(Element element, boolean showStdName, Element modelElem) {
1582
1583        // create a panel to add as a new column or row
1584        final JPanel c = new JPanel();
1585        panelList.add(c);
1586        GridBagLayout g = new GridBagLayout();
1587        GridBagConstraints cs = new GridBagConstraints();
1588        c.setLayout(g);
1589
1590        // handle the xml definition
1591        // for all elements in the column or row
1592        List<Element> elemList = element.getChildren();
1593        log.trace("newRow starting with {} elements", elemList.size());
1594        for (Element value : elemList) {
1595
1596            // update the grid position
1597            cs.gridy = 0;
1598            cs.gridx++;
1599
1600            String name = value.getName();
1601            log.trace("newRow processing {} element", name);
1602            // decode the type
1603            if (name.equals("display")) { // its a variable
1604                // load the variable
1605                newVariable(value, c, g, cs, showStdName);
1606            } else if (name.equals("separator")) { // its a separator
1607                JSeparator j = new JSeparator(SwingConstants.VERTICAL);
1608                cs.fill = GridBagConstraints.BOTH;
1609                cs.gridheight = GridBagConstraints.REMAINDER;
1610                g.setConstraints(j, cs);
1611                c.add(j);
1612                cs.fill = GridBagConstraints.NONE;
1613                cs.gridheight = 1;
1614            } else if (name.equals("label")) {
1615                cs.gridheight = GridBagConstraints.REMAINDER;
1616                makeLabel(value, c, g, cs);
1617            } else if (name.equals("soundlabel")) {
1618                cs.gridheight = GridBagConstraints.REMAINDER;
1619                makeSoundLabel(value, c, g, cs);
1620            } else if (name.equals("cvtable")) {
1621                makeCvTable(cs, g, c);
1622            } else if (name.equals("fnmapping")) {
1623                pickFnMapPanel(c, g, cs, modelElem);
1624            } else if (name.equals("dccaddress")) {
1625                JPanel l = addDccAddressPanel(value);
1626                if (l.getComponentCount() > 0) {
1627                    cs.gridheight = GridBagConstraints.REMAINDER;
1628                    g.setConstraints(l, cs);
1629                    c.add(l);
1630                    cs.gridheight = 1;
1631                }
1632            } else if (name.equals("column")) {
1633                // nested "column" elements ...
1634                cs.gridheight = GridBagConstraints.REMAINDER;
1635                JPanel l = newColumn(value, showStdName, modelElem);
1636                if (l.getComponentCount() > 0) {
1637                    panelList.add(l);
1638                    g.setConstraints(l, cs);
1639                    c.add(l);
1640                    cs.gridheight = 1;
1641                }
1642            } else if (name.equals("row")) {
1643                // nested "row" elements ...
1644                cs.gridwidth = GridBagConstraints.REMAINDER;
1645                JPanel l = newRow(value, showStdName, modelElem);
1646                if (l.getComponentCount() > 0) {
1647                    panelList.add(l);
1648                    g.setConstraints(l, cs);
1649                    c.add(l);
1650                    cs.gridwidth = 1;
1651                }
1652            } else if (name.equals("grid")) {
1653                // nested "grid" elements ...
1654                cs.gridwidth = GridBagConstraints.REMAINDER;
1655                JPanel l = newGrid(value, showStdName, modelElem);
1656                if (l.getComponentCount() > 0) {
1657                    panelList.add(l);
1658                    g.setConstraints(l, cs);
1659                    c.add(l);
1660                    cs.gridwidth = 1;
1661                }
1662            } else if (name.equals("group")) {
1663                // nested "group" elements ...
1664                JPanel l = newGroup(value, showStdName, modelElem);
1665                if (l.getComponentCount() > 0) {
1666                    panelList.add(l);
1667                    g.setConstraints(l, cs);
1668                    c.add(l);
1669                }
1670            } else if (!name.equals("qualifier")) { // its a mistake
1671                log.error("No code to handle element of type {} in newRow", value.getName());
1672            }
1673        }
1674        // add glue to the bottom to allow resize
1675        if (c.getComponentCount() > 0) {
1676            c.add(Box.createVerticalGlue());
1677        }
1678
1679        // handle qualification if any
1680        QualifierAdder qa = new QualifierAdder() {
1681            @Override
1682            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1683                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1684            }
1685
1686            @Override
1687            protected void addListener(java.beans.PropertyChangeListener qc) {
1688                c.addPropertyChangeListener(qc);
1689            }
1690        };
1691
1692        qa.processModifierElements(element, _varModel);
1693        return c;
1694    }
1695
1696    /**
1697     * Create a grid from the JDOM Element.
1698     *
1699     * @param element     element containing group contents
1700     * @param showStdName show the name following the rules for the
1701     * <em>nameFmt</em> element
1702     * @param modelElem   element containing the decoder model
1703     * @return a panel containing the group
1704     */
1705    public JPanel newGrid(Element element, boolean showStdName, Element modelElem) {
1706
1707        // create a panel to add as a new grid
1708        final JPanel c = new JPanel();
1709        panelList.add(c);
1710        GridBagLayout g = new GridBagLayout();
1711        c.setLayout(g);
1712
1713        GridGlobals globs = new GridGlobals();
1714
1715        // handle the xml definition
1716        // for all elements in the grid
1717        List<Element> elemList = element.getChildren();
1718        globs.gridAttList = element.getAttributes(); // get grid-level attributes
1719        log.trace("newGrid starting with {} elements", elemList.size());
1720        for (Element value : elemList) {
1721            globs.gridConstraints = new GridBagConstraints();
1722            String name = value.getName();
1723            log.trace("newGrid processing {} element", name);
1724            // decode the type
1725            if (name.equals("griditem")) {
1726                JPanel l = newGridItem(value, showStdName, modelElem, globs);
1727                if (l.getComponentCount() > 0) {
1728                    panelList.add(l);
1729                    g.setConstraints(l, globs.gridConstraints);
1730                    c.add(l);
1731                    //                     globs.gridConstraints.gridwidth = 1;
1732                }
1733            } else if (name.equals("group")) {
1734                // nested "group" elements ...
1735                newGridGroup(value, c, g, globs, showStdName, modelElem);
1736            } else if (!name.equals("qualifier")) { // its a mistake
1737                log.error("No code to handle element of type {} in newGrid", value.getName());
1738            }
1739        }
1740
1741        // add glue to the bottom to allow resize
1742        if (c.getComponentCount() > 0) {
1743            c.add(Box.createVerticalGlue());
1744        }
1745
1746        // handle qualification if any
1747        QualifierAdder qa = new QualifierAdder() {
1748            @Override
1749            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1750                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1751            }
1752
1753            @Override
1754            protected void addListener(java.beans.PropertyChangeListener qc) {
1755                c.addPropertyChangeListener(qc);
1756            }
1757        };
1758
1759        qa.processModifierElements(element, _varModel);
1760        return c;
1761    }
1762
1763    protected static class GridGlobals {
1764
1765        public int gridxCurrent = -1;
1766        public int gridyCurrent = -1;
1767        public List<Attribute> gridAttList;
1768        public GridBagConstraints gridConstraints;
1769    }
1770
1771    /**
1772     * Create a grid item from the JDOM Element
1773     *
1774     * @param element     element containing grid item contents
1775     * @param showStdName show the name following the rules for the
1776     * <em>nameFmt</em> element
1777     * @param modelElem   element containing the decoder model
1778     * @param globs       properties to configure the layout
1779     * @return a panel containing the group
1780     */
1781    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED") // setAccessible()
1782    public JPanel newGridItem(Element element, boolean showStdName, Element modelElem, GridGlobals globs) {
1783
1784        List<Attribute> itemAttList = element.getAttributes(); // get item-level attributes
1785        List<Attribute> attList = new ArrayList<>(globs.gridAttList);
1786        attList.addAll(itemAttList); // merge grid and item-level attributes
1787//                log.info("New gridtiem -----------------------------------------------");
1788//                log.info("Attribute list:"+attList);
1789        attList.add(new Attribute(LAST_GRIDX, ""));
1790        attList.add(new Attribute(LAST_GRIDY, ""));
1791//                log.info("Updated Attribute list:"+attList);
1792//                 Attribute ax = attList.get(attList.size()-2);
1793//                 Attribute ay = attList.get(attList.size()-1);
1794//                log.info("ax="+ax+";ay="+ay);
1795//                log.info("Previous gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
1796        for (int j = 0; j < attList.size(); j++) {
1797            Attribute attrib = attList.get(j);
1798            String attribName = attrib.getName();
1799            String attribRawValue = attrib.getValue();
1800            Field constraint;
1801            String constraintType;
1802            // make sure we only process the last gridx or gridy attribute in the list
1803            if (attribName.equals("gridx")) {
1804                Attribute a = new Attribute(LAST_GRIDX, attribRawValue);
1805                attList.set(attList.size() - 2, a);
1806//                        log.info("Moved & Updated Attribute list:"+attList);
1807                continue; //. don't process now
1808            }
1809            if (attribName.equals("gridy")) {
1810                Attribute a = new Attribute(LAST_GRIDY, attribRawValue);
1811                attList.set(attList.size() - 1, a);
1812//                        log.info("Moved & Updated Attribute list:"+attList);
1813                continue; //. don't process now
1814            }
1815            if (attribName.equals(LAST_GRIDX)) { // we must be at end of original list, restore last gridx
1816                attribName = "gridx";
1817                if (attribRawValue.equals("")) { // don't process blank (unused)
1818                    continue;
1819                }
1820            }
1821            if (attribName.equals(LAST_GRIDY)) { // we must be at end of original list, restore last gridy
1822                attribName = "gridy";
1823                if (attribRawValue.equals("")) { // don't process blank (unused)
1824                    continue;
1825                }
1826            }
1827            if ((attribName.equals("gridx") || attribName.equals("gridy")) && attribRawValue.equals("RELATIVE")) {
1828                attribRawValue = "NEXT"; // NEXT is a synonym for RELATIVE
1829            }
1830            if (attribName.equals("gridx") && attribRawValue.equals("CURRENT")) {
1831                attribRawValue = String.valueOf(Math.max(0, globs.gridxCurrent));
1832            }
1833            if (attribName.equals("gridy") && attribRawValue.equals("CURRENT")) {
1834                attribRawValue = String.valueOf(Math.max(0, globs.gridyCurrent));
1835            }
1836            if (attribName.equals("gridx") && attribRawValue.equals("NEXT")) {
1837                attribRawValue = String.valueOf(++globs.gridxCurrent);
1838            }
1839            if (attribName.equals("gridy") && attribRawValue.equals("NEXT")) {
1840                attribRawValue = String.valueOf(++globs.gridyCurrent);
1841            }
1842//                    log.info("attribName="+attribName+";attribRawValue="+attribRawValue);
1843            try {
1844                constraint = globs.gridConstraints.getClass().getDeclaredField(attribName);
1845                constraintType = constraint.getType().toString();
1846                constraint.setAccessible(true);
1847            } catch (NoSuchFieldException ex) {
1848                log.error("Unrecognised attribute \"{}\", skipping", attribName);
1849                continue;
1850            }
1851            switch (constraintType) {
1852                case "int": {
1853                    int attribValue;
1854                    try {
1855                        attribValue = Integer.parseInt(attribRawValue);
1856                        constraint.set(globs.gridConstraints, attribValue);
1857                    } catch (IllegalAccessException ey) {
1858                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1859                    } catch (NumberFormatException ex) {
1860                        try {
1861                            Field constant = globs.gridConstraints.getClass().getDeclaredField(attribRawValue);
1862                            constant.setAccessible(true);
1863                            attribValue = (Integer) GridBagConstraints.class.getField(attribRawValue).get(constant);
1864                            constraint.set(globs.gridConstraints, attribValue);
1865                        } catch (NoSuchFieldException ey) {
1866                            log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1867                        } catch (IllegalAccessException ey) {
1868                            log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1869                        }
1870                    }
1871                    break;
1872                }
1873                case "double": {
1874                    double attribValue;
1875                    try {
1876                        attribValue = Double.parseDouble(attribRawValue);
1877                        constraint.set(globs.gridConstraints, attribValue);
1878                    } catch (IllegalAccessException ey) {
1879                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1880                    } catch (NumberFormatException ex) {
1881                        log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1882                    }
1883                    break;
1884                }
1885                case "class java.awt.Insets":
1886                    try {
1887                        String[] insetStrings = attribRawValue.split(",");
1888                        if (insetStrings.length == 4) {
1889                            Insets attribValue = new Insets(Integer.parseInt(insetStrings[0]), Integer.parseInt(insetStrings[1]), Integer.parseInt(insetStrings[2]), Integer.parseInt(insetStrings[3]));
1890                            constraint.set(globs.gridConstraints, attribValue);
1891                        } else {
1892                            log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1893                            log.error("Value should be four integers of the form \"top,left,bottom,right\"");
1894                        }
1895                    } catch (IllegalAccessException ey) {
1896                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1897                    } catch (NumberFormatException ex) {
1898                        log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1899                        log.error("Value should be four integers of the form \"top,left,bottom,right\"");
1900                    }
1901                    break;
1902                default:
1903                    log.error("Required \"{}\" handler for attribute \"{}\" not defined in JMRI code", constraintType, attribName);
1904                    log.error("Please file a JMRI bug report at https://sourceforge.net/p/jmri/bugs/new/");
1905                    break;
1906            }
1907        }
1908//                log.info("Updated globs.GridBagConstraints.gridx="+globs.gridConstraints.gridx+";globs.GridBagConstraints.gridy="+globs.gridConstraints.gridy);
1909
1910        // create a panel to add as a new grid item
1911        final JPanel c = new JPanel();
1912        panelList.add(c);
1913        GridBagLayout g = new GridBagLayout();
1914        GridBagConstraints cs = new GridBagConstraints();
1915        c.setLayout(g);
1916
1917        // handle the xml definition
1918        // for all elements in the grid item
1919        List<Element> elemList = element.getChildren();
1920        log.trace("newGridItem starting with {} elements", elemList.size());
1921        for (Element value : elemList) {
1922
1923            // update the grid position
1924            cs.gridy = 0;
1925            cs.gridx++;
1926
1927            String name = value.getName();
1928            log.trace("newGridItem processing {} element", name);
1929            // decode the type
1930            if (name.equals("display")) { // its a variable
1931                // load the variable
1932                newVariable(value, c, g, cs, showStdName);
1933            } else if (name.equals("separator")) { // its a separator
1934                JSeparator j = new JSeparator(SwingConstants.VERTICAL);
1935                cs.fill = GridBagConstraints.BOTH;
1936                cs.gridheight = GridBagConstraints.REMAINDER;
1937                g.setConstraints(j, cs);
1938                c.add(j);
1939                cs.fill = GridBagConstraints.NONE;
1940                cs.gridheight = 1;
1941            } else if (name.equals("label")) {
1942                cs.gridheight = GridBagConstraints.REMAINDER;
1943                makeLabel(value, c, g, cs);
1944            } else if (name.equals("soundlabel")) {
1945                cs.gridheight = GridBagConstraints.REMAINDER;
1946                makeSoundLabel(value, c, g, cs);
1947            } else if (name.equals("cvtable")) {
1948                makeCvTable(cs, g, c);
1949            } else if (name.equals("fnmapping")) {
1950                pickFnMapPanel(c, g, cs, modelElem);
1951            } else if (name.equals("dccaddress")) {
1952                JPanel l = addDccAddressPanel(value);
1953                if (l.getComponentCount() > 0) {
1954                    cs.gridheight = GridBagConstraints.REMAINDER;
1955                    g.setConstraints(l, cs);
1956                    c.add(l);
1957                    cs.gridheight = 1;
1958                }
1959            } else if (name.equals("column")) {
1960                // nested "column" elements ...
1961                cs.gridheight = GridBagConstraints.REMAINDER;
1962                JPanel l = newColumn(value, showStdName, modelElem);
1963                if (l.getComponentCount() > 0) {
1964                    panelList.add(l);
1965                    g.setConstraints(l, cs);
1966                    c.add(l);
1967                    cs.gridheight = 1;
1968                }
1969            } else if (name.equals("row")) {
1970                // nested "row" elements ...
1971                cs.gridwidth = GridBagConstraints.REMAINDER;
1972                JPanel l = newRow(value, showStdName, modelElem);
1973                if (l.getComponentCount() > 0) {
1974                    panelList.add(l);
1975                    g.setConstraints(l, cs);
1976                    c.add(l);
1977                    cs.gridwidth = 1;
1978                }
1979            } else if (name.equals("grid")) {
1980                // nested "grid" elements ...
1981                cs.gridwidth = GridBagConstraints.REMAINDER;
1982                JPanel l = newGrid(value, showStdName, modelElem);
1983                if (l.getComponentCount() > 0) {
1984                    panelList.add(l);
1985                    g.setConstraints(l, cs);
1986                    c.add(l);
1987                    cs.gridwidth = 1;
1988                }
1989            } else if (name.equals("group")) {
1990                // nested "group" elements ...
1991                JPanel l = newGroup(value, showStdName, modelElem);
1992                if (l.getComponentCount() > 0) {
1993                    panelList.add(l);
1994                    g.setConstraints(l, cs);
1995                    c.add(l);
1996                }
1997            } else if (!name.equals("qualifier")) { // its a mistake
1998                log.error("No code to handle element of type {} in newGridItem", value.getName());
1999            }
2000        }
2001
2002        globs.gridxCurrent = globs.gridConstraints.gridx;
2003        globs.gridyCurrent = globs.gridConstraints.gridy;
2004//                log.info("Updated gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
2005
2006        // add glue to the bottom to allow resize
2007        if (c.getComponentCount() > 0) {
2008            c.add(Box.createVerticalGlue());
2009        }
2010
2011        // handle qualification if any
2012        QualifierAdder qa = new QualifierAdder() {
2013            @Override
2014            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
2015                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
2016            }
2017
2018            @Override
2019            protected void addListener(java.beans.PropertyChangeListener qc) {
2020                c.addPropertyChangeListener(qc);
2021            }
2022        };
2023
2024        qa.processModifierElements(element, _varModel);
2025        return c;
2026    }
2027
2028    /**
2029     * Create label from Element.
2030     *
2031     * @param e  element containing label contents
2032     * @param c  panel to insert label into
2033     * @param g  panel layout manager
2034     * @param cs constraints on layout manager
2035     */
2036    protected void makeLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
2037        String text = LocaleSelector.getAttribute(e, "text");
2038        if (text == null || text.equals("")) {
2039            text = LocaleSelector.getAttribute(e, "label"); // label subelement not since 3.7.5
2040        }
2041        final JLabel l = new JLabel(text);
2042        l.setAlignmentX(1.0f);
2043        cs.fill = GridBagConstraints.BOTH;
2044        log.trace("Add label: {} cs: {} fill: {} x: {} y: {}",
2045                l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
2046        g.setConstraints(l, cs);
2047        c.add(l);
2048        cs.fill = GridBagConstraints.NONE;
2049        cs.gridwidth = 1;
2050        cs.gridheight = 1;
2051
2052        // handle qualification if any
2053        QualifierAdder qa = new QualifierAdder() {
2054            @Override
2055            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
2056                return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
2057            }
2058
2059            @Override
2060            protected void addListener(java.beans.PropertyChangeListener qc) {
2061                l.addPropertyChangeListener(qc);
2062            }
2063        };
2064
2065        qa.processModifierElements(e, _varModel);
2066    }
2067
2068    /**
2069     * Create sound label from Element.
2070     *
2071     * @param e  element containing label contents
2072     * @param c  panel to insert label into
2073     * @param g  panel layout manager
2074     * @param cs constraints on layout manager
2075     */
2076    protected void makeSoundLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
2077        String labelText = rosterEntry.getSoundLabel(Integer.parseInt(Objects.requireNonNull(LocaleSelector.getAttribute(e, "num"))));
2078        final JLabel l = new JLabel(labelText);
2079        l.setAlignmentX(1.0f);
2080        cs.fill = GridBagConstraints.BOTH;
2081        if (log.isDebugEnabled()) {
2082            log.debug("Add soundlabel: {} cs: {} {} {} {}", l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
2083        }
2084        g.setConstraints(l, cs);
2085        c.add(l);
2086        cs.fill = GridBagConstraints.NONE;
2087        cs.gridwidth = 1;
2088        cs.gridheight = 1;
2089
2090        // handle qualification if any
2091        QualifierAdder qa = new QualifierAdder() {
2092            @Override
2093            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
2094                return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
2095            }
2096
2097            @Override
2098            protected void addListener(java.beans.PropertyChangeListener qc) {
2099                l.addPropertyChangeListener(qc);
2100            }
2101        };
2102
2103        qa.processModifierElements(e, _varModel);
2104    }
2105
2106    void makeCvTable(GridBagConstraints cs, GridBagLayout g, JPanel c) {
2107        log.debug("starting to build CvTable pane");
2108
2109        TableRowSorter<TableModel> sorter = new TableRowSorter<>(_cvModel);
2110
2111        JTable cvTable = new JTable(_cvModel);
2112
2113        sorter.setComparator(CvTableModel.NUMCOLUMN, new jmri.jmrit.symbolicprog.CVNameComparator());
2114
2115        List<RowSorter.SortKey> sortKeys = new ArrayList<>();
2116        sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
2117        sorter.setSortKeys(sortKeys);
2118
2119        cvTable.setRowSorter(sorter);
2120
2121        cvTable.setDefaultRenderer(JTextField.class, new CvValueRenderer());
2122        cvTable.setDefaultRenderer(JButton.class, new CvValueRenderer());
2123        cvTable.setDefaultRenderer(String.class, new CvValueRenderer());
2124        cvTable.setDefaultRenderer(Integer.class, new CvValueRenderer());
2125        cvTable.setDefaultEditor(JTextField.class, new ValueEditor());
2126        cvTable.setDefaultEditor(JButton.class, new ValueEditor());
2127        cvTable.setRowHeight(new JButton("X").getPreferredSize().height);
2128        // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541)
2129        // instead of forcing the columns to fill the frame (and only fill)
2130        //cvTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
2131        JScrollPane cvScroll = new JScrollPane(cvTable);
2132        cvScroll.setColumnHeaderView(cvTable.getTableHeader());
2133
2134        cs.fill = GridBagConstraints.BOTH;
2135        cs.weighty = 2.0;
2136        cs.weightx = 0.75;
2137        g.setConstraints(cvScroll, cs);
2138        c.add(cvScroll);
2139
2140        // remember which CVs to read/write
2141        isCvTablePane = true;
2142        setCvListFromTable();
2143
2144        _cvTable = true;
2145        log.debug("end of building CvTable pane");
2146    }
2147
2148    void setCvListFromTable() {
2149        // remember which CVs to read/write
2150        for (int j = 0; j < _cvModel.getRowCount(); j++) {
2151            cvList.add(j);
2152        }
2153        _varModel.setButtonModeFromProgrammer();
2154    }
2155
2156    /**
2157     * Pick an appropriate function map panel depending on model attribute.
2158     * <dl>
2159     * <dt>If attribute extFnsESU="yes":</dt>
2160     * <dd>Invoke
2161     * {@code FnMapPanelESU(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
2162     * <dt>Otherwise:</dt>
2163     * <dd>Invoke
2164     * {@code FnMapPanel(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
2165     * </dl>
2166     *
2167     * @param modelElem element containing model attributes
2168     * @param c         panel to add function map panel to
2169     * @param g         panel layout manager
2170     * @param cs        constraints on layout manager
2171     */
2172    // why does this use a different parameter order than all similar methods?
2173    void pickFnMapPanel(JPanel c, GridBagLayout g, GridBagConstraints cs, Element modelElem) {
2174        boolean extFnsESU = false;
2175        Attribute a = modelElem.getAttribute("extFnsESU");
2176        try {
2177            if (a != null) {
2178                extFnsESU = !(a.getValue()).equalsIgnoreCase("no");
2179            }
2180        } catch (Exception ex) {
2181            log.error("error handling decoder's extFnsESU value");
2182        }
2183        if (extFnsESU) {
2184            FnMapPanelESU l = new FnMapPanelESU(_varModel, varList, modelElem, rosterEntry, _cvModel);
2185            fnMapListESU.add(l); // remember for deletion
2186            cs.gridwidth = GridBagConstraints.REMAINDER;
2187            g.setConstraints(l, cs);
2188            c.add(l);
2189        } else {
2190            FnMapPanel l = new FnMapPanel(_varModel, varList, modelElem);
2191            fnMapList.add(l); // remember for deletion
2192            cs.gridwidth = GridBagConstraints.REMAINDER;
2193            g.setConstraints(l, cs);
2194            c.add(l);
2195        }
2196        cs.gridwidth = 1;
2197    }
2198
2199    /**
2200     * Add the representation of a single variable. The variable is defined by a
2201     * JDOM variable Element from the XML file.
2202     *
2203     * @param var         element containing variable
2204     * @param col         column to insert label into
2205     * @param g           panel layout manager
2206     * @param cs          constraints on layout manager
2207     * @param showStdName show the name following the rules for the
2208     * <em>nameFmt</em> element
2209     */
2210    public void newVariable(Element var, JComponent col,
2211            GridBagLayout g, GridBagConstraints cs, boolean showStdName) {
2212
2213        // get the name
2214        String name = var.getAttribute("item").getValue();
2215
2216        // if it doesn't exist, do nothing
2217        int i = _varModel.findVarIndex(name);
2218        if (i < 0) {
2219            log.trace("Variable \"{}\" not found, omitted", name);
2220            return;
2221        }
2222//        Leave here for now. Need to track pre-existing corner-case issue
2223//        log.info("Entry item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
2224
2225        // check label orientation
2226        Attribute attr;
2227        String layout = "left";  // this default is also set in the DTD
2228        if ((attr = var.getAttribute("layout")) != null && attr.getValue() != null) {
2229            layout = attr.getValue();
2230        }
2231
2232        // load label if specified, else use name
2233        String label = name;
2234        if (!showStdName) {
2235            // get name attribute from variable, as that's the mfg name
2236            label = _varModel.getLabel(i);
2237        }
2238        String temp = LocaleSelector.getAttribute(var, "label");
2239        if (temp != null) {
2240            label = temp;
2241        }
2242
2243        // get representation; store into the list to be programmed
2244        JComponent rep = getRepresentation(name, var);
2245        varList.add(i);
2246
2247        // create the paired label
2248        JLabel l = new WatchingLabel(label, rep);
2249
2250        int spaceWidth = getFontMetrics(l.getFont()).stringWidth(" ");
2251
2252        // now handle the four orientations
2253        // assemble v from label, rep
2254        switch (layout) {
2255            case "left":
2256                cs.anchor = GridBagConstraints.EAST;
2257                cs.ipadx = spaceWidth;
2258                g.setConstraints(l, cs);
2259                col.add(l);
2260                cs.ipadx = 0;
2261                cs.gridx++;
2262                cs.anchor = GridBagConstraints.WEST;
2263                g.setConstraints(rep, cs);
2264                col.add(rep);
2265                break;
2266//        log.info("Exit item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
2267            case "right":
2268                cs.anchor = GridBagConstraints.EAST;
2269                g.setConstraints(rep, cs);
2270                col.add(rep);
2271                cs.gridx++;
2272                cs.anchor = GridBagConstraints.WEST;
2273                cs.ipadx = spaceWidth;
2274                g.setConstraints(l, cs);
2275                col.add(l);
2276                cs.ipadx = 0;
2277                break;
2278            case "below":
2279                // variable in center of upper line
2280                cs.anchor = GridBagConstraints.CENTER;
2281                g.setConstraints(rep, cs);
2282                col.add(rep);
2283                // label aligned like others
2284                cs.gridy++;
2285                cs.anchor = GridBagConstraints.WEST;
2286                cs.ipadx = spaceWidth;
2287                g.setConstraints(l, cs);
2288                col.add(l);
2289                cs.ipadx = 0;
2290                break;
2291            case "above":
2292                // label aligned like others
2293                cs.anchor = GridBagConstraints.WEST;
2294                cs.ipadx = spaceWidth;
2295                g.setConstraints(l, cs);
2296                col.add(l);
2297                cs.ipadx = 0;
2298                // variable in center of lower line
2299                cs.gridy++;
2300                cs.anchor = GridBagConstraints.CENTER;
2301                g.setConstraints(rep, cs);
2302                col.add(rep);
2303                break;
2304            default:
2305                log.error("layout internally inconsistent: {}", layout);
2306        }
2307    }
2308
2309    /**
2310     * Get a GUI representation of a particular variable for display.
2311     *
2312     * @param name Name used to look up the Variable object
2313     * @param var  XML Element which might contain a "format" attribute to be
2314     *             used in the {@link VariableValue#getNewRep} call from the
2315     *             Variable object; "tooltip" elements are also processed here.
2316     * @return JComponent representing this variable
2317     */
2318    public JComponent getRepresentation(String name, Element var) {
2319        int i = _varModel.findVarIndex(name);
2320        VariableValue variable = _varModel.getVariable(i);
2321        JComponent rep = null;
2322        String format = "default";
2323        Attribute attr;
2324        if ((attr = var.getAttribute("format")) != null && attr.getValue() != null) {
2325            format = attr.getValue();
2326        }
2327
2328        boolean viewOnly = (var.getAttribute("viewOnly") != null &&
2329                var.getAttribute("viewOnly").getValue().equals("yes"));
2330
2331        if (i >= 0) {
2332            rep = getRep(i, format);
2333            rep.setMaximumSize(rep.getPreferredSize());
2334            // set tooltip if specified here & not overridden by defn in Variable
2335            String tip = LocaleSelector.getAttribute(var, "tooltip");
2336            if (rep.getToolTipText() != null) {
2337                tip = rep.getToolTipText();
2338            }
2339            rep.setToolTipText(modifyToolTipText(tip, variable));
2340            if (viewOnly) {
2341            rep.setEnabled(false);
2342            }
2343        }
2344        return rep;
2345    }
2346
2347    /**
2348     * Takes default tool tip text, e.g. from the decoder element, and modifies
2349     * it as needed.
2350     * <p>
2351     * Intended to handle e.g. adding CV numbers to variables.
2352     *
2353     * @param start    existing tool tip text
2354     * @param variable the CV
2355     * @return new tool tip text
2356     */
2357    String modifyToolTipText(String start, VariableValue variable) {
2358        log.trace("modifyToolTipText: {}", variable.label());
2359        // this is the place to invoke VariableValue methods to (conditionally)
2360        // add information about CVs, etc in the ToolTip text
2361
2362        // Optionally add CV numbers based on Roster Preferences setting
2363        start = CvUtil.addCvDescription(start, variable.getCvDescription(), variable.getMask());
2364
2365        // Indicate what the command station can do
2366        // need to update this with e.g. the specific CV numbers
2367        if (_cvModel.getProgrammer() != null
2368                && !_cvModel.getProgrammer().getCanRead()) {
2369            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotRead"));
2370        }
2371        if (_cvModel.getProgrammer() != null
2372                && !_cvModel.getProgrammer().getCanWrite()) {
2373            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotWrite"));
2374        }
2375
2376        // indicate other reasons for read/write constraints
2377        if (variable.getReadOnly()) {
2378            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeReadOnly"));
2379        }
2380        if (variable.getWriteOnly()) {
2381            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeWriteOnly"));
2382        }
2383
2384        return start;
2385    }
2386
2387    JComponent getRep(int i, String format) {
2388        return (JComponent) (_varModel.getRep(i, format));
2389    }
2390
2391    /**
2392     * list of fnMapping objects to dispose
2393     */
2394    ArrayList<FnMapPanel> fnMapList = new ArrayList<>();
2395    ArrayList<FnMapPanelESU> fnMapListESU = new ArrayList<>();
2396    /**
2397     * list of JPanel objects to removeAll
2398     */
2399    ArrayList<JPanel> panelList = new ArrayList<>();
2400
2401    public void dispose() {
2402        log.debug("dispose");
2403
2404        // remove components
2405        removeAll();
2406
2407        readChangesButton.removeItemListener(l1);
2408        readAllButton.removeItemListener(l2);
2409        writeChangesButton.removeItemListener(l3);
2410        writeAllButton.removeItemListener(l4);
2411        confirmChangesButton.removeItemListener(l5);
2412        confirmAllButton.removeItemListener(l6);
2413        l1 = l2 = l3 = l4 = l5 = l6 = null;
2414
2415        if (_programmingVar != null) {
2416            _programmingVar.removePropertyChangeListener(this);
2417        }
2418        if (_programmingCV != null) {
2419            _programmingCV.removePropertyChangeListener(this);
2420        }
2421
2422        _programmingVar = null;
2423        _programmingCV = null;
2424
2425        varList.clear();
2426        varList = null;
2427        cvList.clear();
2428        cvList = null;
2429
2430        // dispose of any panels
2431        for (JPanel jPanel : panelList) {
2432            jPanel.removeAll();
2433        }
2434        panelList.clear();
2435        panelList = null;
2436
2437        // dispose of any fnMaps
2438        for (FnMapPanel fnMapPanel : fnMapList) {
2439            fnMapPanel.dispose();
2440        }
2441        fnMapList.clear();
2442        fnMapList = null;
2443
2444        // dispose of any fnMaps
2445        for (FnMapPanelESU fnMapPanelESU : fnMapListESU) {
2446            fnMapPanelESU.dispose();
2447        }
2448        fnMapListESU.clear();
2449        fnMapListESU = null;
2450
2451        readChangesButton = null;
2452        writeChangesButton = null;
2453
2454        // these are disposed elsewhere
2455        _cvModel = null;
2456        _varModel = null;
2457    }
2458
2459    /**
2460     * Check if varList and cvList, and thus the tab, is empty.
2461     *
2462     * @return true if empty
2463     */
2464    public boolean isEmpty() {
2465        return (varList.isEmpty() && cvList.isEmpty());
2466    }
2467
2468    public boolean includeInPrint() {
2469        return print;
2470    }
2471
2472    public void includeInPrint(boolean inc) {
2473        print = inc;
2474    }
2475    boolean print = false;
2476
2477    public void printPane(HardcopyWriter w) {
2478        // if pane is empty, don't print anything
2479        if (isEmpty()) {
2480            return;
2481        }
2482
2483        // Define column widths for name and value output.
2484        // Make col 2 slightly larger than col 1 and reduce both to allow for
2485        // extra spaces that will be added during concatenation
2486        int col1Width = w.getCharactersPerLine() / 2 - 3 - 5;
2487        int col2Width = w.getCharactersPerLine() / 2 - 3 + 5;
2488
2489        try {
2490            //Create a string of spaces the width of the first column
2491            StringBuilder spaces = new StringBuilder();
2492            spaces.append(" ".repeat(Math.max(0, col1Width)));
2493            // start with pane name in bold
2494            String heading1 = SymbolicProgBundle.getMessage("PrintHeadingField");
2495            String heading2 = SymbolicProgBundle.getMessage("PrintHeadingSetting");
2496            String s;
2497            int interval = spaces.length() - heading1.length();
2498            w.setFontStyle(Font.BOLD);
2499            // write the section name and dividing line
2500            s = mName;
2501            w.write(s, 0, s.length());
2502            w.writeBorders();
2503            //Draw horizontal dividing line for each Pane section
2504            w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
2505                    w.getCharactersPerLine() + 1);
2506            s = "\n";
2507            w.write(s, 0, s.length());
2508            // if this isn't the raw CV section, write the column headings
2509            if (cvList.isEmpty()) {
2510                w.setFontStyle(Font.BOLD + Font.ITALIC);
2511                s = "   " + heading1 + spaces.substring(0, interval) + "   " + heading2;
2512                w.write(s, 0, s.length());
2513                w.writeBorders();
2514                s = "\n";
2515                w.write(s, 0, s.length());
2516            }
2517            w.setFontStyle(Font.PLAIN);
2518            // Define a vector to store the names of variables that have been printed
2519            // already.  If they have been printed, they will be skipped.
2520            // Using a vector here since we don't know how many variables will
2521            // be printed and it allows expansion as necessary
2522            ArrayList<String> printedVariables = new ArrayList<>(10);
2523            // index over variables
2524            for (int varNum : varList) {
2525                VariableValue var = _varModel.getVariable(varNum);
2526                String name = var.label();
2527                if (name == null) {
2528                    name = var.item();
2529                }
2530                // Check if variable has been printed.  If not store it and print
2531                boolean alreadyPrinted = false;
2532                for (String printedVariable : printedVariables) {
2533                    if (name.equals(printedVariable)) {
2534                        alreadyPrinted = true;
2535                        break;
2536                    }
2537                }
2538                // If already printed, skip it.  If not, store it and print
2539                if (alreadyPrinted) {
2540                    continue;
2541                }
2542                printedVariables.add(name);
2543
2544                String value = var.getTextValue();
2545                String originalName = name;
2546                String originalValue = value;
2547                name = name + " (CV" + var.getCvNum() + ")"; // NO I18N
2548
2549                // define index values for name and value substrings
2550                int nameLeftIndex = 0;
2551                int nameRightIndex = name.length();
2552                int valueLeftIndex = 0;
2553                int valueRightIndex = value.length();
2554                String trimmedName;
2555                String trimmedValue;
2556
2557                // Check the name length to see if it is wider than the column.
2558                // If so, split it and do the same checks for the Value
2559                // Then concatenate the name and value (or the split versions thereof)
2560                // before writing - if split, repeat until all pieces have been output
2561                while ((valueLeftIndex < value.length()) || (nameLeftIndex < name.length())) {
2562                    // name split code
2563                    if (name.substring(nameLeftIndex).length() > col1Width) {
2564                        for (int j = 0; j < col1Width; j++) {
2565                            String delimiter = name.substring(nameLeftIndex + col1Width - j - 1, nameLeftIndex + col1Width - j);
2566                            if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
2567                                nameRightIndex = nameLeftIndex + col1Width - j;
2568                                break;
2569                            }
2570                        }
2571                        trimmedName = name.substring(nameLeftIndex, nameRightIndex);
2572                        nameLeftIndex = nameRightIndex;
2573                        int space = spaces.length() - trimmedName.length();
2574                        s = "   " + trimmedName + spaces.substring(0, space);
2575                    } else {
2576                        trimmedName = name.substring(nameLeftIndex);
2577                        int space = spaces.length() - trimmedName.length();
2578                        s = "   " + trimmedName + spaces.substring(0, space);
2579                        name = "";
2580                        nameLeftIndex = 0;
2581                    }
2582                    // value split code
2583                    if (value.substring(valueLeftIndex).length() > col2Width) {
2584                        for (int j = 0; j < col2Width; j++) {
2585                            String delimiter = value.substring(valueLeftIndex + col2Width - j - 1, valueLeftIndex + col2Width - j);
2586                            if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
2587                                valueRightIndex = valueLeftIndex + col2Width - j;
2588                                break;
2589                            }
2590                        }
2591                        trimmedValue = value.substring(valueLeftIndex, valueRightIndex);
2592                        valueLeftIndex = valueRightIndex;
2593                        s = s + "   " + trimmedValue;
2594                    } else {
2595                        trimmedValue = value.substring(valueLeftIndex);
2596                        s = s + "   " + trimmedValue;
2597                        valueLeftIndex = 0;
2598                        value = "";
2599                    }
2600                    w.write(s, 0, s.length());
2601                    w.writeBorders();
2602                    s = "\n";
2603                    w.write(s, 0, s.length());
2604                }
2605                // Check for a Speed Table output and create a graphic display.
2606                // Java 1.5 has a known bug, #6328248, that prevents printing of progress
2607                //  bars using old style printing classes.  It results in blank bars on Windows,
2608                //  but hangs Macs. The version check is a workaround.
2609                float v = Float.parseFloat(System.getProperty("java.version").substring(0, 3));
2610                if (originalName.equals("Speed Table") && v < 1.5) {
2611                    // set the height of the speed table graph in lines
2612                    int speedFrameLineHeight = 11;
2613                    s = "\n";
2614
2615                    // check that there is enough room on the page; if not,
2616                    // space down the rest of the page.
2617                    // don't use page break because we want the table borders to be written
2618                    // to the bottom of the page
2619                    int pageSize = w.getLinesPerPage();
2620                    int here = w.getCurrentLineNumber();
2621                    if (pageSize - here < speedFrameLineHeight) {
2622                        for (int j = 0; j < (pageSize - here); j++) {
2623                            w.writeBorders();
2624                            w.write(s, 0, s.length());
2625                        }
2626                    }
2627
2628                    // Now that there is page space, create the window to hold the graphic speed table
2629                    JWindow speedWindow = new JWindow();
2630                    // Window size as wide as possible to allow for largest type size
2631                    speedWindow.setSize(512, 165);
2632                    speedWindow.getContentPane().setBackground(Color.white);
2633                    speedWindow.getContentPane().setLayout(null);
2634                    // in preparation for display, extract the speed table values into an array
2635                    StringTokenizer valueTokens = new StringTokenizer(originalValue, ",", false);
2636                    int[] speedVals = new int[28];
2637                    int k = 0;
2638                    while (valueTokens.hasMoreTokens()) {
2639                        speedVals[k] = Integer.parseInt(valueTokens.nextToken());
2640                        k++;
2641                    }
2642
2643                    // Now create a set of vertical progress bar whose length is based
2644                    // on the speed table value (half height) and add them to the window
2645                    for (int j = 0; j < 28; j++) {
2646                        JProgressBar printerBar = new JProgressBar(JProgressBar.VERTICAL, 0, 127);
2647                        printerBar.setBounds(52 + j * 15, 19, 10, 127);
2648                        printerBar.setValue(speedVals[j] / 2);
2649                        printerBar.setBackground(Color.white);
2650                        printerBar.setForeground(Color.darkGray);
2651                        printerBar.setBorder(BorderFactory.createLineBorder(Color.black));
2652                        speedWindow.getContentPane().add(printerBar);
2653                        // create a set of value labels at the top containing the speed table values
2654                        JLabel barValLabel = new JLabel(Integer.toString(speedVals[j]), SwingConstants.CENTER);
2655                        barValLabel.setBounds(50 + j * 15, 4, 15, 15);
2656                        barValLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2657                        speedWindow.getContentPane().add(barValLabel);
2658                        //Create a set of labels at the bottom with the CV numbers in them
2659                        JLabel barCvLabel = new JLabel(Integer.toString(67 + j), SwingConstants.CENTER);
2660                        barCvLabel.setBounds(50 + j * 15, 150, 15, 15);
2661                        barCvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2662                        speedWindow.getContentPane().add(barCvLabel);
2663                    }
2664                    JLabel cvLabel = new JLabel(Bundle.getMessage("Value"));
2665                    cvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2666                    cvLabel.setBounds(25, 4, 26, 15);
2667                    speedWindow.getContentPane().add(cvLabel);
2668                    JLabel valueLabel = new JLabel("CV"); // I18N seems undesirable for support
2669                    valueLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2670                    valueLabel.setBounds(37, 150, 13, 15);
2671                    speedWindow.getContentPane().add(valueLabel);
2672                    // pass the complete window to the printing class
2673                    w.write(speedWindow);
2674                    // Now need to write the borders on sides of table
2675                    for (int j = 0; j < speedFrameLineHeight; j++) {
2676                        w.writeBorders();
2677                        w.write(s, 0, s.length());
2678                    }
2679                }
2680            }
2681
2682            final int TABLE_COLS = 3;
2683
2684            // index over CVs
2685            if (!cvList.isEmpty()) {
2686//            Check how many Cvs there are to print
2687                int cvCount = cvList.size();
2688                w.setFontStyle(Font.BOLD); //set font to Bold
2689                // print a simple heading with I18N
2690                s = String.format("%1$21s", Bundle.getMessage("Value"))
2691                    + String.format("%1$28s", Bundle.getMessage("Value")) +
2692                        String.format("%1$28s", Bundle.getMessage("Value"));
2693                w.write(s, 0, s.length());
2694                w.writeBorders();
2695                s = "\n";
2696                w.write(s, 0, s.length());
2697                // NO I18N
2698                s = "            CV  Dec Hex                 CV  Dec Hex                 CV  Dec Hex";
2699                w.write(s, 0, s.length());
2700                w.writeBorders();
2701                s = "\n";
2702                w.write(s, 0, s.length());
2703                w.setFontStyle(0); //set font back to Normal
2704                //           }
2705                /*create an array to hold CV/Value strings to allow reformatting and sorting
2706                 Same size as the table drawn above (TABLE_COLS columns*tableHeight; heading rows
2707                 not included). Use the count of how many CVs there are to determine the number
2708                 of table rows required.  Add one more row if the divison into TABLE_COLS columns
2709                 isn't even.
2710                 */
2711                int tableHeight = cvCount / TABLE_COLS;
2712                if (cvCount % TABLE_COLS > 0) {
2713                    tableHeight++;
2714                }
2715                String[] cvStrings = new String[TABLE_COLS * tableHeight];
2716
2717                //blank the array
2718                Arrays.fill(cvStrings, "");
2719
2720                // get each CV and value
2721                int i = 0;
2722                for (int cvNum : cvList) {
2723                    CvValue cv = _cvModel.getCvByRow(cvNum);
2724
2725                    int value = cv.getValue();
2726
2727                    //convert and pad numbers as needed
2728                    String numString = String.format("%12s", cv.number());
2729                    StringBuilder valueString = new StringBuilder(Integer.toString(value));
2730                    String valueStringHex = Integer.toHexString(value).toUpperCase(Locale.ENGLISH);
2731                    if (value < 16) {
2732                        valueStringHex = "0" + valueStringHex;
2733                    }
2734                    for (int j = 1; j < 3; j++) {
2735                        if (valueString.length() < 3) {
2736                            valueString.insert(0, " ");
2737                        }
2738                    }
2739                    //Create composite string of CV and its decimal and hex values
2740                    s = "  " + numString + "  " + valueString + "  " + valueStringHex
2741                            + " ";
2742
2743                    //populate printing array - still treated as a single column
2744                    cvStrings[i] = s;
2745                    i++;
2746                }
2747
2748                //sort the array in CV order (just the members with values)
2749                String temp;
2750                boolean swap;
2751                do {
2752                    swap = false;
2753                    for (i = 0; i < _cvModel.getRowCount() - 1; i++) {
2754                        if (PrintCvAction.cvSortOrderVal(cvStrings[i + 1].substring(0, 15).trim()) < PrintCvAction.cvSortOrderVal(cvStrings[i].substring(0, 15).trim())) {
2755                            temp = cvStrings[i + 1];
2756                            cvStrings[i + 1] = cvStrings[i];
2757                            cvStrings[i] = temp;
2758                            swap = true;
2759                        }
2760                    }
2761                } while (swap);
2762
2763                //Print the array in four columns
2764                for (i = 0; i < tableHeight; i++) {
2765                    s = cvStrings[i] + "    " + cvStrings[i + tableHeight] + "    " + cvStrings[i
2766                            + tableHeight * 2];
2767                    w.write(s, 0, s.length());
2768                    w.writeBorders();
2769                    s = "\n";
2770                    w.write(s, 0, s.length());
2771                }
2772            }
2773            s = "\n";
2774            w.writeBorders();
2775            w.write(s, 0, s.length());
2776            w.writeBorders();
2777            w.write(s, 0, s.length());
2778
2779            // handle special cases
2780        } catch (IOException e) {
2781            log.warn("error during printing", e);
2782        }
2783
2784    }
2785
2786    private JPanel addDccAddressPanel(Element e) {
2787        JPanel l = new DccAddressPanel(_varModel);
2788        panelList.add(l);
2789        // make sure this will get read/written, even if real vars not on pane
2790        int iVar;
2791
2792        // note we want Short Address first, as it might change others
2793        iVar = _varModel.findVarIndex("Short Address");
2794        if (iVar >= 0) {
2795            varList.add(iVar);
2796        } else {
2797            log.debug("addDccAddressPanel did not find Short Address");
2798        }
2799
2800        iVar = _varModel.findVarIndex("Address Format");
2801        if (iVar >= 0) {
2802            varList.add(iVar);
2803        } else {
2804            log.debug("addDccAddressPanel did not find Address Format");
2805        }
2806
2807        iVar = _varModel.findVarIndex("Long Address");
2808        if (iVar >= 0) {
2809            varList.add(iVar);
2810        } else {
2811            log.debug("addDccAddressPanel did not find Long Address");
2812        }
2813
2814        // included here because CV1 can modify it, even if it doesn't show on pane;
2815        iVar = _varModel.findVarIndex("Consist Address");
2816        if (iVar >= 0) {
2817            varList.add(iVar);
2818        } else {
2819            log.debug("addDccAddressPanel did not find CV19 Consist Address");
2820        }
2821
2822        return l;
2823    }
2824
2825    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PaneProgPane.class);
2826
2827}