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