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    }
661
662    /**
663     * Invoked by "Read Full Sheet" button, this sets in motion a continuing
664     * sequence of "read" operations on the variables and CVs in the
665     * Pane. The read mechanism only reads variables in certain states (and
666     * needs to do that to handle error processing right now), so this is
667     * implemented by first setting all variables and CVs on this pane to TOREAD
668     * in prepReadPaneAll, then starting the execution.
669     *
670     * @return true is a read has been started, false if the pane is complete
671     */
672    public boolean readPaneAll() {
673        if (log.isDebugEnabled()) {
674            log.debug("readAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
675        }
676        prepReadPane(false);
677        // start operation
678        return nextRead();
679    }
680
681    /**
682     * Set the "ToRead" parameter in all variables and CVs on this pane.
683     *
684     * @param justChanges  true if this is read changes, false if read all
685     * @param startProcess true if this is the start of processing, false if
686     *                     cleaning up at end
687     */
688    void setToRead(boolean justChanges, boolean startProcess) {
689        if (!container.isBusy()
690                || // the frame has already setToRead
691                (!startProcess)) {  // we want to setToRead false if the pane's process is being stopped
692            for (int varNum : varList) {
693                VariableValue var = _varModel.getVariable(varNum);
694                if (justChanges) {
695                    if (var.isChanged()) {
696                        var.setToRead(startProcess);
697                    } else {
698                        var.setToRead(false);
699                    }
700                } else {
701                    var.setToRead(startProcess);
702                }
703            }
704
705            if (isCvTablePane) {
706                setCvListFromTable();  // make sure list of CVs up to date if table
707            }
708            for (int cvNum : cvList) {
709                CvValue cv = _cvModel.getCvByRow(cvNum);
710                if (justChanges) {
711                    if (VariableValue.considerChanged(cv)) {
712                        cv.setToRead(startProcess);
713                    } else {
714                        cv.setToRead(false);
715                    }
716                } else {
717                    cv.setToRead(startProcess);
718                }
719            }
720        }
721    }
722
723    /**
724     * Set the "ToWrite" parameter in all variables and CVs on this pane
725     *
726     * @param justChanges  true if this is read changes, false if read all
727     * @param startProcess true if this is the start of processing, false if
728     *                     cleaning up at end
729     */
730    void setToWrite(boolean justChanges, boolean startProcess) {
731        log.debug("start setToWrite method with {},{}", justChanges, startProcess);
732        if (!container.isBusy()
733                || // the frame has already setToWrite
734                (!startProcess)) {  // we want to setToWrite false if the pane's process is being stopped
735            log.debug("about to start setToWrite of varList");
736            for (int varNum : varList) {
737                VariableValue var = _varModel.getVariable(varNum);
738                if (justChanges) {
739                    if (var.isChanged()) {
740                        var.setToWrite(startProcess);
741                    } else {
742                        var.setToWrite(false);
743                    }
744                } else {
745                    var.setToWrite(startProcess);
746                }
747            }
748
749            log.debug("about to start setToWrite of cvList");
750            if (isCvTablePane) {
751                setCvListFromTable();  // make sure list of CVs up to date if table
752            }
753            for (int cvNum : cvList) {
754                CvValue cv = _cvModel.getCvByRow(cvNum);
755                if (justChanges) {
756                    if (VariableValue.considerChanged(cv)) {
757                        cv.setToWrite(startProcess);
758                    } else {
759                        cv.setToWrite(false);
760                    }
761                } else {
762                    cv.setToWrite(startProcess);
763                }
764            }
765        }
766        log.debug("end setToWrite method");
767    }
768
769    void executeRead(VariableValue var) {
770        setBusy(true);
771        // var.setToRead(false);  // variables set this themselves
772        if (_programmingVar != null) {
773            log.error("listener already set at read start");
774        }
775        _programmingVar = var;
776        _read = true;
777        // get notified when that state changes so can repeat
778        _programmingVar.addPropertyChangeListener(this);
779        // and make the read request
780        if (justChanges) {
781            _programmingVar.readChanges();
782        } else {
783            _programmingVar.readAll();
784        }
785    }
786
787    void executeWrite(VariableValue var) {
788        setBusy(true);
789        // var.setToWrite(false);   // variables reset themselves when done
790        if (_programmingVar != null) {
791            log.error("listener already set at write start");
792        }
793        _programmingVar = var;
794        _read = false;
795        // get notified when that state changes so can repeat
796        _programmingVar.addPropertyChangeListener(this);
797        // and make the write request
798        if (justChanges) {
799            _programmingVar.writeChanges();
800        } else {
801            _programmingVar.writeAll();
802        }
803    }
804
805    /**
806     * If there are any more read operations to be done on this pane, do the
807     * next one.
808     * <p>
809     * Each invocation of this method reads one variable or CV; completion of
810     * that request will cause it to happen again, reading the next one, until
811     * there's nothing left to read.
812     * @return true is a read has been started, false if the pane is complete.
813     */
814    boolean nextRead() {
815        // look for possible variables
816        if (log.isDebugEnabled()) {
817            log.debug("nextRead scans {} variables", varList.size());
818        }
819        while ((varList.size() > 0) && (varListIndex < varList.size())) {
820            int varNum = varList.get(varListIndex);
821            AbstractValue.ValueState vState = _varModel.getState(varNum);
822            VariableValue var = _varModel.getVariable(varNum);
823            if (log.isDebugEnabled()) {
824                log.debug("nextRead var index {} state {} isToRead: {} label: {}", varNum, vState.getName(), var.isToRead(), var.label());
825            }
826            varListIndex++;
827            if (var.isToRead()) {
828                if (log.isDebugEnabled()) {
829                    log.debug("start read of variable {}", _varModel.getLabel(varNum));
830                }
831                executeRead(var);
832
833                log.debug("return from starting var read");
834                // the request may have instantaneously been satisfied...
835                return true;  // only make one request at a time!
836            }
837        }
838        // found no variables needing read, try CVs
839        if (log.isDebugEnabled()) {
840            log.debug("nextRead scans {} CVs", cvList.size());
841        }
842        while (cvListIterator != null && cvListIterator.hasNext()) {
843            int cvNum = cvListIterator.next();
844            CvValue cv = _cvModel.getCvByRow(cvNum);
845            if (log.isDebugEnabled()) {
846                log.debug("nextRead cv index {} state {}", cvNum, cv.getState());
847            }
848
849            if (cv.isToRead()) {  // always read UNKNOWN state
850                log.debug("start read of cv {}", cvNum);
851                setBusy(true);
852                if (_programmingCV != null) {
853                    log.error("listener already set at read start");
854                }
855                _programmingCV = _cvModel.getCvByRow(cvNum);
856                _read = true;
857                // get notified when that state changes so can repeat
858                _programmingCV.addPropertyChangeListener(this);
859                // and make the read request
860                // _programmingCV.setToRead(false);  // CVs set this themselves
861                _programmingCV.read(_cvModel.getStatusLabel());
862                log.debug("return from starting CV read");
863                // the request may have instantateously been satisfied...
864                return true;  // only make one request at a time!
865            }
866        }
867        // nothing to program, end politely
868        log.debug("nextRead found nothing to do");
869        readChangesButton.setSelected(false);
870        readAllButton.setSelected(false);  // reset both, as that's final state we want
871        setBusy(false);
872        container.paneFinished();
873        return false;
874    }
875
876    /**
877     * If there are any more compare operations to be done on this pane, do the
878     * next one.
879     * <p>
880     * Each invocation of this method compares one CV; completion of that request
881     * will cause it to happen again, reading the next one, until there's
882     * nothing left to read.
883     *
884     * @return true is a compare has been started, false if the pane is
885     *         complete.
886     */
887    boolean nextConfirm() {
888        // look for possible CVs
889        while (cvListIterator != null && cvListIterator.hasNext()) {
890            int cvNum = cvListIterator.next();
891            CvValue cv = _cvModel.getCvByRow(cvNum);
892            if (log.isDebugEnabled()) {
893                log.debug("nextConfirm cv index {} state {}", cvNum, cv.getState());
894            }
895
896            if (cv.isToRead()) {
897                log.debug("start confirm of cv {}", cvNum);
898                setBusy(true);
899                if (_programmingCV != null) {
900                    log.error("listener already set at confirm start");
901                }
902                _programmingCV = _cvModel.getCvByRow(cvNum);
903                _read = true;
904                // get notified when that state changes so can repeat
905                _programmingCV.addPropertyChangeListener(this);
906                // and make the compare request
907                _programmingCV.confirm(_cvModel.getStatusLabel());
908                log.debug("return from starting CV confirm");
909                // the request may have instantateously been satisfied...
910                return true;  // only make one request at a time!
911            }
912        }
913        // nothing to program, end politely
914        log.debug("nextConfirm found nothing to do");
915        confirmChangesButton.setSelected(false);
916        confirmAllButton.setSelected(false);  // reset both, as that's final state we want
917        setBusy(false);
918        container.paneFinished();
919        return false;
920    }
921
922    /**
923     * Invoked by "Write changes on sheet" button, this sets in motion a
924     * continuing sequence of "write" operations on the variables in the Pane.
925     * Only variables in isChanged states are written; other states don't need
926     * to be.
927     *
928     * @return true if a write has been started, false if the pane is complete
929     */
930    public boolean writePaneChanges() {
931        log.debug("writePaneChanges starts");
932        prepWritePane(true);
933        boolean val = nextWrite();
934        log.debug("writePaneChanges returns {}", val);
935        return val;
936    }
937
938    /**
939     * Invoked by "Write full sheet" button to write all CVs.
940     *
941     * @return true if a write has been started, false if the pane is complete
942     */
943    public boolean writePaneAll() {
944        prepWritePane(false);
945        return nextWrite();
946    }
947
948    /**
949     * Prepare a "write full sheet" operation.
950     *
951     * @param onlyChanges true if only writing changes; false if writing all
952     */
953    public void prepWritePane(boolean onlyChanges) {
954        log.debug("start prepWritePane with {}", onlyChanges);
955        justChanges = onlyChanges;
956        enableButtons(false);
957
958        if (isCvTablePane) {
959            setCvListFromTable();  // make sure list of CVs up to date if table
960        }
961        if (justChanges) {
962            writeChangesButton.setEnabled(true);
963            writeChangesButton.setSelected(true);
964        } else {
965            writeAllButton.setSelected(true);
966            writeAllButton.setEnabled(true);
967        }
968        if (!container.isBusy()) {
969            container.enableButtons(false);
970        }
971        setToWrite(justChanges, true);
972        varListIndex = 0;
973
974        cvListIterator = cvList.iterator();
975        log.debug("end prepWritePane");
976    }
977
978    boolean nextWrite() {
979        log.debug("start nextWrite");
980        // look for possible variables
981        while ((varList.size() > 0) && (varListIndex < varList.size())) {
982            int varNum = varList.get(varListIndex);
983            AbstractValue.ValueState vState = _varModel.getState(varNum);
984            VariableValue var = _varModel.getVariable(varNum);
985            if (log.isDebugEnabled()) {
986                log.debug("nextWrite var index {} state {} isToWrite: {} label:{}", varNum, vState.getName(), var.isToWrite(), var.label());
987            }
988            varListIndex++;
989            if (var.isToWrite()) {
990                log.debug("start write of variable {}", _varModel.getLabel(varNum));
991
992                executeWrite(var);
993
994                log.debug("return from starting var write");
995                return true;  // only make one request at a time!
996            }
997        }
998        // check for CVs to handle (e.g. for CV table)
999        while (cvListIterator != null && cvListIterator.hasNext()) {
1000            int cvNum = cvListIterator.next();
1001            CvValue cv = _cvModel.getCvByRow(cvNum);
1002            if (log.isDebugEnabled()) {
1003                log.debug("nextWrite cv index {} state {}", cvNum, cv.getState());
1004            }
1005
1006            if (cv.isToWrite()) {
1007                log.debug("start write of cv index {}", cvNum);
1008                setBusy(true);
1009                if (_programmingCV != null) {
1010                    log.error("listener already set at write start");
1011                }
1012                _programmingCV = _cvModel.getCvByRow(cvNum);
1013                _read = false;
1014                // get notified when that state changes so can repeat
1015                _programmingCV.addPropertyChangeListener(this);
1016                // and make the write request
1017                // _programmingCV.setToWrite(false);  // CVs set this themselves
1018                _programmingCV.write(_cvModel.getStatusLabel());
1019                log.debug("return from starting cv write");
1020                return true;  // only make one request at a time!
1021            }
1022        }
1023        // nothing to program, end politely
1024        log.debug("nextWrite found nothing to do");
1025        writeChangesButton.setSelected(false);
1026        writeAllButton.setSelected(false);
1027        setBusy(false);
1028        container.paneFinished();
1029        log.debug("return from nextWrite with nothing to do");
1030        return false;
1031    }
1032
1033    /**
1034     * Prepare this pane for a compare operation.
1035     * <p>
1036     * The read mechanism only reads variables in certain states (and needs to
1037     * do that to handle error processing right now), so this is implemented by
1038     * first setting all variables and CVs on this pane to TOREAD via this
1039     * method
1040     *
1041     * @param onlyChanges true if only confirming changes; false if confirming
1042     *                    all
1043     */
1044    public void prepConfirmPane(boolean onlyChanges) {
1045        log.debug("start prepReadPane with onlyChanges={}", onlyChanges);
1046        justChanges = onlyChanges;
1047        enableButtons(false);
1048
1049        if (isCvTablePane) {
1050            setCvListFromTable();  // make sure list of CVs up to date if table
1051        }
1052        if (justChanges) {
1053            confirmChangesButton.setEnabled(true);
1054            confirmChangesButton.setSelected(true);
1055        } else {
1056            confirmAllButton.setSelected(true);
1057            confirmAllButton.setEnabled(true);
1058        }
1059        if (!container.isBusy()) {
1060            container.enableButtons(false);
1061        }
1062        // we can use the read prep since confirm has to read first
1063        setToRead(justChanges, true);
1064        varListIndex = 0;
1065
1066        cvListIterator = cvList.iterator();
1067    }
1068
1069    /**
1070     * Invoked by "Compare changes on sheet" button, this sets in motion a
1071     * continuing sequence of "confirm" operations on the variables and
1072     * CVs in the Pane. Only variables in states marked as "changed" will be
1073     * checked.
1074     *
1075     * @return true is a confirm has been started, false if the pane is
1076     *         complete.
1077     */
1078    public boolean confirmPaneChanges() {
1079        if (log.isDebugEnabled()) {
1080            log.debug("confirmPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
1081        }
1082        prepConfirmPane(true);
1083        return nextConfirm();
1084    }
1085
1086    /**
1087     * Invoked by "Compare Full Sheet" button, this sets in motion a continuing
1088     * sequence of "confirm" operations on the variables and CVs in the
1089     * Pane. The read mechanism only reads variables in certain states (and
1090     * needs to do that to handle error processing right now), so this is
1091     * implemented by first setting all variables and CVs on this pane to TOREAD
1092     * in prepReadPaneAll, then starting the execution.
1093     *
1094     * @return true is a confirm has been started, false if the pane is
1095     *         complete.
1096     */
1097    public boolean confirmPaneAll() {
1098        if (log.isDebugEnabled()) {
1099            log.debug("confirmAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
1100        }
1101        prepConfirmPane(false);
1102        // start operation
1103        return nextConfirm();
1104    }
1105
1106    // reference to variable being programmed (or null if none)
1107    VariableValue _programmingVar = null;
1108    CvValue _programmingCV = null;
1109    boolean _read = true;
1110
1111    // busy during read, write operations
1112    private boolean _busy = false;
1113
1114    public boolean isBusy() {
1115        return _busy;
1116    }
1117
1118    protected void setBusy(boolean busy) {
1119        boolean oldBusy = _busy;
1120        _busy = busy;
1121        if (!busy && !container.isBusy()) {
1122            enableButtons(true);
1123        }
1124        if (oldBusy != busy) {
1125            firePropertyChange("Busy", Boolean.valueOf(oldBusy), Boolean.valueOf(busy));
1126        }
1127    }
1128
1129    private int retry = 0;
1130
1131    /**
1132     * Get notification of a variable property change, specifically "busy" going
1133     * to false at the end of a programming operation. If we're in a programming
1134     * operation, we then continue it by reinvoking the nextRead/writePane
1135     * operation.
1136     *
1137     * @param e the event to respond to
1138     */
1139    @Override
1140    public void propertyChange(java.beans.PropertyChangeEvent e) {
1141        // check for the right event & condition
1142        if (_programmingVar == null && _programmingCV == null ) {
1143            log.warn("unexpected propertChange: {}", e);
1144            return;
1145        } else if (log.isDebugEnabled()) {
1146            log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue());
1147        }
1148
1149        // find the right way to handle this
1150        if (e.getSource() == _programmingVar
1151                && e.getPropertyName().equals("Busy")
1152                && e.getNewValue().equals(Boolean.FALSE)) {
1153            if (_programmingVar.getState() == AbstractValue.ValueState.UNKNOWN) {
1154                if (retry == 0) {
1155                    varListIndex--;
1156                    retry++;
1157                    if (_read) {
1158                        _programmingVar.setToRead(true); // set the variable
1159                        // to read again.
1160                    } else {
1161                        _programmingVar.setToWrite(true); // set the variable
1162                        // to attempt another
1163                        // write.
1164                    }
1165                } else {
1166                    retry = 0;
1167                }
1168            }
1169            replyWhileProgrammingVar();
1170        } else if (e.getSource() == _programmingCV
1171                && e.getPropertyName().equals("Busy")
1172                && e.getNewValue().equals(Boolean.FALSE)) {
1173
1174            // there's no -- operator on the HashSet Iterator we're
1175            // using for the CV list, so we don't do individual retries
1176            // now.
1177            //if (_programmingCV.getState() == CvValue.UNKNOWN) {
1178            //    if (retry == 0) {
1179            //        cvListIndex--;
1180            //        retry++;
1181            //    } else {
1182            //        retry = 0;
1183            //    }
1184            //}
1185            replyWhileProgrammingCV();
1186        } else {
1187            if (log.isDebugEnabled() && e.getPropertyName().equals("Busy")) {
1188                log.debug("ignoring change of Busy {} {}", e.getNewValue(), e.getNewValue().equals(Boolean.FALSE));
1189            }
1190        }
1191    }
1192
1193    public void replyWhileProgrammingVar() {
1194        log.debug("correct event for programming variable, restart operation");
1195        // remove existing listener
1196        _programmingVar.removePropertyChangeListener(this);
1197        _programmingVar = null;
1198        // restart the operation
1199        restartProgramming();
1200    }
1201
1202    public void replyWhileProgrammingCV() {
1203        log.debug("correct event for programming CV, restart operation");
1204        // remove existing listener
1205        _programmingCV.removePropertyChangeListener(this);
1206        _programmingCV = null;
1207        // restart the operation
1208        restartProgramming();
1209    }
1210
1211    void restartProgramming() {
1212        log.debug("start restartProgramming");
1213        if (_read && readChangesButton.isSelected()) {
1214            nextRead();
1215        } else if (_read && readAllButton.isSelected()) {
1216            nextRead();
1217        } else if (_read && confirmChangesButton.isSelected()) {
1218            nextConfirm();
1219        } else if (_read && confirmAllButton.isSelected()) {
1220            nextConfirm();
1221        } else if (writeChangesButton.isSelected()) {
1222            nextWrite();   // was writePaneChanges
1223        } else if (writeAllButton.isSelected()) {
1224            nextWrite();
1225        } else {
1226            log.debug("No operation to restart");
1227            if (isBusy()) {
1228                container.paneFinished();
1229                setBusy(false);
1230            }
1231        }
1232        log.debug("end restartProgramming");
1233    }
1234
1235    protected void stopProgramming() {
1236        log.debug("start stopProgramming");
1237        setToRead(false, false);
1238        setToWrite(false, false);
1239        varListIndex = varList.size();
1240
1241        cvListIterator = null;
1242        log.debug("end stopProgramming");
1243    }
1244
1245    /**
1246     * Create a new group from the JDOM group Element
1247     *
1248     * @param element     element containing group contents
1249     * @param showStdName show the name following the rules for the
1250     * <em>nameFmt</em> element
1251     * @param modelElem   element containing the decoder model
1252     * @return a panel containing the group
1253     */
1254    protected JPanel newGroup(Element element, boolean showStdName, Element modelElem) {
1255
1256        // create a panel to add as a new column or row
1257        final JPanel c = new JPanel();
1258        panelList.add(c);
1259        GridBagLayout g = new GridBagLayout();
1260        GridBagConstraints cs = new GridBagConstraints();
1261        c.setLayout(g);
1262
1263        // handle include/exclude
1264        if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
1265            return c;
1266        }
1267
1268        // handle the xml definition
1269        // for all elements in the column or row
1270        List<Element> elemList = element.getChildren();
1271        log.trace("newColumn starting with {} elements", elemList.size());
1272        for (Element e : elemList) {
1273
1274            String name = e.getName();
1275            log.trace("newGroup processing {} element", name);
1276            // decode the type
1277            if (name.equals("display")) { // its a variable
1278                // load the variable
1279                newVariable(e, c, g, cs, showStdName);
1280            } else if (name.equals("separator")) { // its a separator
1281                JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
1282                cs.fill = GridBagConstraints.BOTH;
1283                cs.gridwidth = GridBagConstraints.REMAINDER;
1284                g.setConstraints(j, cs);
1285                c.add(j);
1286                cs.gridwidth = 1;
1287            } else if (name.equals("label")) {
1288                cs.gridwidth = GridBagConstraints.REMAINDER;
1289                makeLabel(e, c, g, cs);
1290            } else if (name.equals("soundlabel")) {
1291                cs.gridwidth = GridBagConstraints.REMAINDER;
1292                makeSoundLabel(e, c, g, cs);
1293            } else if (name.equals("cvtable")) {
1294                makeCvTable(cs, g, c);
1295            } else if (name.equals("fnmapping")) {
1296                pickFnMapPanel(c, g, cs, modelElem);
1297            } else if (name.equals("dccaddress")) {
1298                JPanel l = addDccAddressPanel(e);
1299                if (l.getComponentCount() > 0) {
1300                    cs.gridwidth = GridBagConstraints.REMAINDER;
1301                    g.setConstraints(l, cs);
1302                    c.add(l);
1303                    cs.gridwidth = 1;
1304                }
1305            } else if (name.equals("column")) {
1306                // nested "column" elements ...
1307                cs.gridheight = GridBagConstraints.REMAINDER;
1308                JPanel l = newColumn(e, showStdName, modelElem);
1309                if (l.getComponentCount() > 0) {
1310                    panelList.add(l);
1311                    g.setConstraints(l, cs);
1312                    c.add(l);
1313                    cs.gridheight = 1;
1314                }
1315            } else if (name.equals("row")) {
1316                // nested "row" elements ...
1317                cs.gridwidth = GridBagConstraints.REMAINDER;
1318                JPanel l = newRow(e, showStdName, modelElem);
1319                if (l.getComponentCount() > 0) {
1320                    panelList.add(l);
1321                    g.setConstraints(l, cs);
1322                    c.add(l);
1323                    cs.gridwidth = 1;
1324                }
1325            } else if (name.equals("grid")) {
1326                // nested "grid" elements ...
1327                cs.gridwidth = GridBagConstraints.REMAINDER;
1328                JPanel l = newGrid(e, showStdName, modelElem);
1329                if (l.getComponentCount() > 0) {
1330                    panelList.add(l);
1331                    g.setConstraints(l, cs);
1332                    c.add(l);
1333                    cs.gridwidth = 1;
1334                }
1335            } else if (name.equals("group")) {
1336                // nested "group" elements ...
1337                JPanel l = newGroup(e, showStdName, modelElem);
1338                if (l.getComponentCount() > 0) {
1339                    panelList.add(l);
1340                    g.setConstraints(l, cs);
1341                    c.add(l);
1342                }
1343            } else if (!name.equals("qualifier")) { // its a mistake
1344                log.error("No code to handle element of type {} in newColumn", e.getName());
1345            }
1346        }
1347        // add glue to the bottom to allow resize
1348        if (c.getComponentCount() > 0) {
1349            c.add(Box.createVerticalGlue());
1350        }
1351
1352        // handle qualification if any
1353        QualifierAdder qa = new QualifierAdder() {
1354            @Override
1355            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1356                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1357            }
1358
1359            @Override
1360            protected void addListener(java.beans.PropertyChangeListener qc) {
1361                c.addPropertyChangeListener(qc);
1362            }
1363        };
1364
1365        qa.processModifierElements(element, _varModel);
1366        return c;
1367    }
1368
1369    /**
1370     * Create a new grid group from the JDOM group Element.
1371     *
1372     * @param element     element containing group contents
1373     * @param c           the panel to create the grid in
1374     * @param g           the layout manager for the panel
1375     * @param globs       properties to configure g
1376     * @param showStdName show the name following the rules for the
1377     * <em>nameFmt</em> element
1378     * @param modelElem   element containing the decoder model
1379     */
1380    protected void newGridGroup(Element element, final JPanel c, GridBagLayout g, GridGlobals globs, boolean showStdName, Element modelElem) {
1381
1382        // handle include/exclude
1383        if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
1384            return;
1385        }
1386
1387        // handle the xml definition
1388        // for all elements in the column or row
1389        List<Element> elemList = element.getChildren();
1390        log.trace("newColumn starting with {} elements", elemList.size());
1391        for (Element e : elemList) {
1392
1393            String name = e.getName();
1394            log.trace("newGroup processing {} element", name);
1395            // decode the type
1396            if (name.equals("griditem")) {
1397                final JPanel l = newGridItem(e, showStdName, modelElem, globs);
1398                if (l.getComponentCount() > 0) {
1399                    panelList.add(l);
1400                    g.setConstraints(l, globs.gridConstraints);
1401                    c.add(l);
1402                    //                     globs.gridConstraints.gridwidth = 1;
1403                    // handle qualification if any
1404                    QualifierAdder qa = new QualifierAdder() {
1405                        @Override
1406                        protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1407                            return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
1408                        }
1409
1410                        @Override
1411                        protected void addListener(java.beans.PropertyChangeListener qc) {
1412                            l.addPropertyChangeListener(qc);
1413                        }
1414                    };
1415
1416                    qa.processModifierElements(e, _varModel);
1417                }
1418            } else if (name.equals("group")) {
1419                // nested "group" elements ...
1420                newGridGroup(e, c, g, globs, showStdName, modelElem);
1421            } else if (!name.equals("qualifier")) { // its a mistake
1422                log.error("No code to handle element of type {} in newColumn", e.getName());
1423            }
1424        }
1425        // add glue to the bottom to allow resize
1426//         if (c.getComponentCount() > 0) {
1427//             c.add(Box.createVerticalGlue());
1428//         }
1429
1430    }
1431
1432    /**
1433     * Create a single column from the JDOM column Element.
1434     *
1435     * @param element     element containing column contents
1436     * @param showStdName show the name following the rules for the
1437     * <em>nameFmt</em> element
1438     * @param modelElem   element containing the decoder model
1439     * @return a panel containing the group
1440     */
1441    public JPanel newColumn(Element element, boolean showStdName, Element modelElem) {
1442
1443        // create a panel to add as a new column or row
1444        final JPanel c = new JPanel();
1445        panelList.add(c);
1446        GridBagLayout g = new GridBagLayout();
1447        GridBagConstraints cs = new GridBagConstraints();
1448        c.setLayout(g);
1449
1450        // handle the xml definition
1451        // for all elements in the column or row
1452        List<Element> elemList = element.getChildren();
1453        log.trace("newColumn starting with {} elements", elemList.size());
1454        for (Element value : elemList) {
1455
1456            // update the grid position
1457            cs.gridy++;
1458            cs.gridx = 0;
1459
1460            String name = value.getName();
1461            log.trace("newColumn processing {} element", name);
1462            // decode the type
1463            if (name.equals("display")) { // it's a variable
1464                // load the variable
1465                newVariable(value, c, g, cs, showStdName);
1466            } else if (name.equals("separator")) { // its a separator
1467                JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
1468                cs.fill = GridBagConstraints.BOTH;
1469                cs.gridwidth = GridBagConstraints.REMAINDER;
1470                g.setConstraints(j, cs);
1471                c.add(j);
1472                cs.gridwidth = 1;
1473            } else if (name.equals("label")) {
1474                cs.gridwidth = GridBagConstraints.REMAINDER;
1475                makeLabel(value, c, g, cs);
1476            } else if (name.equals("soundlabel")) {
1477                cs.gridwidth = GridBagConstraints.REMAINDER;
1478                makeSoundLabel(value, c, g, cs);
1479            } else if (name.equals("cvtable")) {
1480                makeCvTable(cs, g, c);
1481            } else if (name.equals("fnmapping")) {
1482                pickFnMapPanel(c, g, cs, modelElem);
1483            } else if (name.equals("dccaddress")) {
1484                JPanel l = addDccAddressPanel(value);
1485                if (l.getComponentCount() > 0) {
1486                    cs.gridwidth = GridBagConstraints.REMAINDER;
1487                    g.setConstraints(l, cs);
1488                    c.add(l);
1489                    cs.gridwidth = 1;
1490                }
1491            } else if (name.equals("column")) {
1492                // nested "column" elements ...
1493                cs.gridheight = GridBagConstraints.REMAINDER;
1494                JPanel l = newColumn(value, showStdName, modelElem);
1495                if (l.getComponentCount() > 0) {
1496                    panelList.add(l);
1497                    g.setConstraints(l, cs);
1498                    c.add(l);
1499                    cs.gridheight = 1;
1500                }
1501            } else if (name.equals("row")) {
1502                // nested "row" elements ...
1503                cs.gridwidth = GridBagConstraints.REMAINDER;
1504                JPanel l = newRow(value, showStdName, modelElem);
1505                if (l.getComponentCount() > 0) {
1506                    panelList.add(l);
1507                    g.setConstraints(l, cs);
1508                    c.add(l);
1509                    cs.gridwidth = 1;
1510                }
1511            } else if (name.equals("grid")) {
1512                // nested "grid" elements ...
1513                cs.gridwidth = GridBagConstraints.REMAINDER;
1514                JPanel l = newGrid(value, showStdName, modelElem);
1515                if (l.getComponentCount() > 0) {
1516                    panelList.add(l);
1517                    g.setConstraints(l, cs);
1518                    c.add(l);
1519                    cs.gridwidth = 1;
1520                }
1521            } else if (name.equals("group")) {
1522                // nested "group" elements ...
1523                JPanel l = newGroup(value, showStdName, modelElem);
1524                if (l.getComponentCount() > 0) {
1525                    panelList.add(l);
1526                    g.setConstraints(l, cs);
1527                    c.add(l);
1528                }
1529            } else if (!name.equals("qualifier")) { // its a mistake
1530                log.error("No code to handle element of type {} in newColumn", value.getName());
1531            }
1532        }
1533        // add glue to the bottom to allow resize
1534        if (c.getComponentCount() > 0) {
1535            c.add(Box.createVerticalGlue());
1536        }
1537
1538        // handle qualification if any
1539        QualifierAdder qa = new QualifierAdder() {
1540            @Override
1541            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1542                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1543            }
1544
1545            @Override
1546            protected void addListener(java.beans.PropertyChangeListener qc) {
1547                c.addPropertyChangeListener(qc);
1548            }
1549        };
1550
1551        qa.processModifierElements(element, _varModel);
1552        return c;
1553    }
1554
1555    /**
1556     * Create a single row from the JDOM column Element
1557     *
1558     * @param element     element containing row contents
1559     * @param showStdName show the name following the rules for the
1560     * <em>nameFmt</em> element
1561     * @param modelElem   element containing the decoder model
1562     * @return a panel containing the group
1563     */
1564    public JPanel newRow(Element element, boolean showStdName, Element modelElem) {
1565
1566        // create a panel to add as a new column or row
1567        final JPanel c = new JPanel();
1568        panelList.add(c);
1569        GridBagLayout g = new GridBagLayout();
1570        GridBagConstraints cs = new GridBagConstraints();
1571        c.setLayout(g);
1572
1573        // handle the xml definition
1574        // for all elements in the column or row
1575        List<Element> elemList = element.getChildren();
1576        log.trace("newRow starting with {} elements", elemList.size());
1577        for (Element value : elemList) {
1578
1579            // update the grid position
1580            cs.gridy = 0;
1581            cs.gridx++;
1582
1583            String name = value.getName();
1584            log.trace("newRow processing {} element", name);
1585            // decode the type
1586            if (name.equals("display")) { // its a variable
1587                // load the variable
1588                newVariable(value, c, g, cs, showStdName);
1589            } else if (name.equals("separator")) { // its a separator
1590                JSeparator j = new JSeparator(SwingConstants.VERTICAL);
1591                cs.fill = GridBagConstraints.BOTH;
1592                cs.gridheight = GridBagConstraints.REMAINDER;
1593                g.setConstraints(j, cs);
1594                c.add(j);
1595                cs.fill = GridBagConstraints.NONE;
1596                cs.gridheight = 1;
1597            } else if (name.equals("label")) {
1598                cs.gridheight = GridBagConstraints.REMAINDER;
1599                makeLabel(value, c, g, cs);
1600            } else if (name.equals("soundlabel")) {
1601                cs.gridheight = GridBagConstraints.REMAINDER;
1602                makeSoundLabel(value, c, g, cs);
1603            } else if (name.equals("cvtable")) {
1604                makeCvTable(cs, g, c);
1605            } else if (name.equals("fnmapping")) {
1606                pickFnMapPanel(c, g, cs, modelElem);
1607            } else if (name.equals("dccaddress")) {
1608                JPanel l = addDccAddressPanel(value);
1609                if (l.getComponentCount() > 0) {
1610                    cs.gridheight = GridBagConstraints.REMAINDER;
1611                    g.setConstraints(l, cs);
1612                    c.add(l);
1613                    cs.gridheight = 1;
1614                }
1615            } else if (name.equals("column")) {
1616                // nested "column" elements ...
1617                cs.gridheight = GridBagConstraints.REMAINDER;
1618                JPanel l = newColumn(value, showStdName, modelElem);
1619                if (l.getComponentCount() > 0) {
1620                    panelList.add(l);
1621                    g.setConstraints(l, cs);
1622                    c.add(l);
1623                    cs.gridheight = 1;
1624                }
1625            } else if (name.equals("row")) {
1626                // nested "row" elements ...
1627                cs.gridwidth = GridBagConstraints.REMAINDER;
1628                JPanel l = newRow(value, showStdName, modelElem);
1629                if (l.getComponentCount() > 0) {
1630                    panelList.add(l);
1631                    g.setConstraints(l, cs);
1632                    c.add(l);
1633                    cs.gridwidth = 1;
1634                }
1635            } else if (name.equals("grid")) {
1636                // nested "grid" elements ...
1637                cs.gridwidth = GridBagConstraints.REMAINDER;
1638                JPanel l = newGrid(value, showStdName, modelElem);
1639                if (l.getComponentCount() > 0) {
1640                    panelList.add(l);
1641                    g.setConstraints(l, cs);
1642                    c.add(l);
1643                    cs.gridwidth = 1;
1644                }
1645            } else if (name.equals("group")) {
1646                // nested "group" elements ...
1647                JPanel l = newGroup(value, showStdName, modelElem);
1648                if (l.getComponentCount() > 0) {
1649                    panelList.add(l);
1650                    g.setConstraints(l, cs);
1651                    c.add(l);
1652                }
1653            } else if (!name.equals("qualifier")) { // its a mistake
1654                log.error("No code to handle element of type {} in newRow", value.getName());
1655            }
1656        }
1657        // add glue to the bottom to allow resize
1658        if (c.getComponentCount() > 0) {
1659            c.add(Box.createVerticalGlue());
1660        }
1661
1662        // handle qualification if any
1663        QualifierAdder qa = new QualifierAdder() {
1664            @Override
1665            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1666                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1667            }
1668
1669            @Override
1670            protected void addListener(java.beans.PropertyChangeListener qc) {
1671                c.addPropertyChangeListener(qc);
1672            }
1673        };
1674
1675        qa.processModifierElements(element, _varModel);
1676        return c;
1677    }
1678
1679    /**
1680     * Create a grid from the JDOM Element.
1681     *
1682     * @param element     element containing group contents
1683     * @param showStdName show the name following the rules for the
1684     * <em>nameFmt</em> element
1685     * @param modelElem   element containing the decoder model
1686     * @return a panel containing the group
1687     */
1688    public JPanel newGrid(Element element, boolean showStdName, Element modelElem) {
1689
1690        // create a panel to add as a new grid
1691        final JPanel c = new JPanel();
1692        panelList.add(c);
1693        GridBagLayout g = new GridBagLayout();
1694        c.setLayout(g);
1695
1696        GridGlobals globs = new GridGlobals();
1697
1698        // handle the xml definition
1699        // for all elements in the grid
1700        List<Element> elemList = element.getChildren();
1701        globs.gridAttList = element.getAttributes(); // get grid-level attributes
1702        log.trace("newGrid starting with {} elements", elemList.size());
1703        for (Element value : elemList) {
1704            globs.gridConstraints = new GridBagConstraints();
1705            String name = value.getName();
1706            log.trace("newGrid processing {} element", name);
1707            // decode the type
1708            if (name.equals("griditem")) {
1709                JPanel l = newGridItem(value, showStdName, modelElem, globs);
1710                if (l.getComponentCount() > 0) {
1711                    panelList.add(l);
1712                    g.setConstraints(l, globs.gridConstraints);
1713                    c.add(l);
1714                    //                     globs.gridConstraints.gridwidth = 1;
1715                }
1716            } else if (name.equals("group")) {
1717                // nested "group" elements ...
1718                newGridGroup(value, c, g, globs, showStdName, modelElem);
1719            } else if (!name.equals("qualifier")) { // its a mistake
1720                log.error("No code to handle element of type {} in newGrid", value.getName());
1721            }
1722        }
1723
1724        // add glue to the bottom to allow resize
1725        if (c.getComponentCount() > 0) {
1726            c.add(Box.createVerticalGlue());
1727        }
1728
1729        // handle qualification if any
1730        QualifierAdder qa = new QualifierAdder() {
1731            @Override
1732            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1733                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1734            }
1735
1736            @Override
1737            protected void addListener(java.beans.PropertyChangeListener qc) {
1738                c.addPropertyChangeListener(qc);
1739            }
1740        };
1741
1742        qa.processModifierElements(element, _varModel);
1743        return c;
1744    }
1745
1746    protected static class GridGlobals {
1747
1748        public int gridxCurrent = -1;
1749        public int gridyCurrent = -1;
1750        public List<Attribute> gridAttList;
1751        public GridBagConstraints gridConstraints;
1752    }
1753
1754    /**
1755     * Create a grid item from the JDOM Element
1756     *
1757     * @param element     element containing grid item contents
1758     * @param showStdName show the name following the rules for the
1759     * <em>nameFmt</em> element
1760     * @param modelElem   element containing the decoder model
1761     * @param globs       properties to configure the layout
1762     * @return a panel containing the group
1763     */
1764    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED") // setAccessible()
1765    public JPanel newGridItem(Element element, boolean showStdName, Element modelElem, GridGlobals globs) {
1766
1767        List<Attribute> itemAttList = element.getAttributes(); // get item-level attributes
1768        List<Attribute> attList = new ArrayList<>(globs.gridAttList);
1769        attList.addAll(itemAttList); // merge grid and item-level attributes
1770//                log.info("New gridtiem -----------------------------------------------");
1771//                log.info("Attribute list:"+attList);
1772        attList.add(new Attribute(LAST_GRIDX, ""));
1773        attList.add(new Attribute(LAST_GRIDY, ""));
1774//                log.info("Updated Attribute list:"+attList);
1775//                 Attribute ax = attList.get(attList.size()-2);
1776//                 Attribute ay = attList.get(attList.size()-1);
1777//                log.info("ax="+ax+";ay="+ay);
1778//                log.info("Previous gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
1779        for (int j = 0; j < attList.size(); j++) {
1780            Attribute attrib = attList.get(j);
1781            String attribName = attrib.getName();
1782            String attribRawValue = attrib.getValue();
1783            Field constraint;
1784            String constraintType;
1785            // make sure we only process the last gridx or gridy attribute in the list
1786            if (attribName.equals("gridx")) {
1787                Attribute a = new Attribute(LAST_GRIDX, attribRawValue);
1788                attList.set(attList.size() - 2, a);
1789//                        log.info("Moved & Updated Attribute list:"+attList);
1790                continue; //. don't process now
1791            }
1792            if (attribName.equals("gridy")) {
1793                Attribute a = new Attribute(LAST_GRIDY, attribRawValue);
1794                attList.set(attList.size() - 1, a);
1795//                        log.info("Moved & Updated Attribute list:"+attList);
1796                continue; //. don't process now
1797            }
1798            if (attribName.equals(LAST_GRIDX)) { // we must be at end of original list, restore last gridx
1799                attribName = "gridx";
1800                if (attribRawValue.equals("")) { // don't process blank (unused)
1801                    continue;
1802                }
1803            }
1804            if (attribName.equals(LAST_GRIDY)) { // we must be at end of original list, restore last gridy
1805                attribName = "gridy";
1806                if (attribRawValue.equals("")) { // don't process blank (unused)
1807                    continue;
1808                }
1809            }
1810            if ((attribName.equals("gridx") || attribName.equals("gridy")) && attribRawValue.equals("RELATIVE")) {
1811                attribRawValue = "NEXT"; // NEXT is a synonym for RELATIVE
1812            }
1813            if (attribName.equals("gridx") && attribRawValue.equals("CURRENT")) {
1814                attribRawValue = String.valueOf(Math.max(0, globs.gridxCurrent));
1815            }
1816            if (attribName.equals("gridy") && attribRawValue.equals("CURRENT")) {
1817                attribRawValue = String.valueOf(Math.max(0, globs.gridyCurrent));
1818            }
1819            if (attribName.equals("gridx") && attribRawValue.equals("NEXT")) {
1820                attribRawValue = String.valueOf(++globs.gridxCurrent);
1821            }
1822            if (attribName.equals("gridy") && attribRawValue.equals("NEXT")) {
1823                attribRawValue = String.valueOf(++globs.gridyCurrent);
1824            }
1825//                    log.info("attribName="+attribName+";attribRawValue="+attribRawValue);
1826            try {
1827                constraint = globs.gridConstraints.getClass().getDeclaredField(attribName);
1828                constraintType = constraint.getType().toString();
1829                constraint.setAccessible(true);
1830            } catch (NoSuchFieldException ex) {
1831                log.error("Unrecognised attribute \"{}\", skipping", attribName);
1832                continue;
1833            }
1834            switch (constraintType) {
1835                case "int": {
1836                    int attribValue;
1837                    try {
1838                        attribValue = Integer.parseInt(attribRawValue);
1839                        constraint.set(globs.gridConstraints, attribValue);
1840                    } catch (IllegalAccessException ey) {
1841                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1842                    } catch (NumberFormatException ex) {
1843                        try {
1844                            Field constant = globs.gridConstraints.getClass().getDeclaredField(attribRawValue);
1845                            constant.setAccessible(true);
1846                            attribValue = (Integer) GridBagConstraints.class.getField(attribRawValue).get(constant);
1847                            constraint.set(globs.gridConstraints, attribValue);
1848                        } catch (NoSuchFieldException ey) {
1849                            log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1850                        } catch (IllegalAccessException ey) {
1851                            log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1852                        }
1853                    }
1854                    break;
1855                }
1856                case "double": {
1857                    double attribValue;
1858                    try {
1859                        attribValue = Double.parseDouble(attribRawValue);
1860                        constraint.set(globs.gridConstraints, attribValue);
1861                    } catch (IllegalAccessException ey) {
1862                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1863                    } catch (NumberFormatException ex) {
1864                        log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1865                    }
1866                    break;
1867                }
1868                case "class java.awt.Insets":
1869                    try {
1870                        String[] insetStrings = attribRawValue.split(",");
1871                        if (insetStrings.length == 4) {
1872                            Insets attribValue = new Insets(Integer.parseInt(insetStrings[0]), Integer.parseInt(insetStrings[1]), Integer.parseInt(insetStrings[2]), Integer.parseInt(insetStrings[3]));
1873                            constraint.set(globs.gridConstraints, attribValue);
1874                        } else {
1875                            log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1876                            log.error("Value should be four integers of the form \"top,left,bottom,right\"");
1877                        }
1878                    } catch (IllegalAccessException ey) {
1879                        log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
1880                    } catch (NumberFormatException ex) {
1881                        log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
1882                        log.error("Value should be four integers of the form \"top,left,bottom,right\"");
1883                    }
1884                    break;
1885                default:
1886                    log.error("Required \"{}\" handler for attribute \"{}\" not defined in JMRI code", constraintType, attribName);
1887                    log.error("Please file a JMRI bug report at https://sourceforge.net/p/jmri/bugs/new/");
1888                    break;
1889            }
1890        }
1891//                log.info("Updated globs.GridBagConstraints.gridx="+globs.gridConstraints.gridx+";globs.GridBagConstraints.gridy="+globs.gridConstraints.gridy);
1892
1893        // create a panel to add as a new grid item
1894        final JPanel c = new JPanel();
1895        panelList.add(c);
1896        GridBagLayout g = new GridBagLayout();
1897        GridBagConstraints cs = new GridBagConstraints();
1898        c.setLayout(g);
1899
1900        // handle the xml definition
1901        // for all elements in the grid item
1902        List<Element> elemList = element.getChildren();
1903        log.trace("newGridItem starting with {} elements", elemList.size());
1904        for (Element value : elemList) {
1905
1906            // update the grid position
1907            cs.gridy = 0;
1908            cs.gridx++;
1909
1910            String name = value.getName();
1911            log.trace("newGridItem processing {} element", name);
1912            // decode the type
1913            if (name.equals("display")) { // its a variable
1914                // load the variable
1915                newVariable(value, c, g, cs, showStdName);
1916            } else if (name.equals("separator")) { // its a separator
1917                JSeparator j = new JSeparator(SwingConstants.VERTICAL);
1918                cs.fill = GridBagConstraints.BOTH;
1919                cs.gridheight = GridBagConstraints.REMAINDER;
1920                g.setConstraints(j, cs);
1921                c.add(j);
1922                cs.fill = GridBagConstraints.NONE;
1923                cs.gridheight = 1;
1924            } else if (name.equals("label")) {
1925                cs.gridheight = GridBagConstraints.REMAINDER;
1926                makeLabel(value, c, g, cs);
1927            } else if (name.equals("soundlabel")) {
1928                cs.gridheight = GridBagConstraints.REMAINDER;
1929                makeSoundLabel(value, c, g, cs);
1930            } else if (name.equals("cvtable")) {
1931                makeCvTable(cs, g, c);
1932            } else if (name.equals("fnmapping")) {
1933                pickFnMapPanel(c, g, cs, modelElem);
1934            } else if (name.equals("dccaddress")) {
1935                JPanel l = addDccAddressPanel(value);
1936                if (l.getComponentCount() > 0) {
1937                    cs.gridheight = GridBagConstraints.REMAINDER;
1938                    g.setConstraints(l, cs);
1939                    c.add(l);
1940                    cs.gridheight = 1;
1941                }
1942            } else if (name.equals("column")) {
1943                // nested "column" elements ...
1944                cs.gridheight = GridBagConstraints.REMAINDER;
1945                JPanel l = newColumn(value, showStdName, modelElem);
1946                if (l.getComponentCount() > 0) {
1947                    panelList.add(l);
1948                    g.setConstraints(l, cs);
1949                    c.add(l);
1950                    cs.gridheight = 1;
1951                }
1952            } else if (name.equals("row")) {
1953                // nested "row" elements ...
1954                cs.gridwidth = GridBagConstraints.REMAINDER;
1955                JPanel l = newRow(value, showStdName, modelElem);
1956                if (l.getComponentCount() > 0) {
1957                    panelList.add(l);
1958                    g.setConstraints(l, cs);
1959                    c.add(l);
1960                    cs.gridwidth = 1;
1961                }
1962            } else if (name.equals("grid")) {
1963                // nested "grid" elements ...
1964                cs.gridwidth = GridBagConstraints.REMAINDER;
1965                JPanel l = newGrid(value, showStdName, modelElem);
1966                if (l.getComponentCount() > 0) {
1967                    panelList.add(l);
1968                    g.setConstraints(l, cs);
1969                    c.add(l);
1970                    cs.gridwidth = 1;
1971                }
1972            } else if (name.equals("group")) {
1973                // nested "group" elements ...
1974                JPanel l = newGroup(value, showStdName, modelElem);
1975                if (l.getComponentCount() > 0) {
1976                    panelList.add(l);
1977                    g.setConstraints(l, cs);
1978                    c.add(l);
1979                }
1980            } else if (!name.equals("qualifier")) { // its a mistake
1981                log.error("No code to handle element of type {} in newGridItem", value.getName());
1982            }
1983        }
1984
1985        globs.gridxCurrent = globs.gridConstraints.gridx;
1986        globs.gridyCurrent = globs.gridConstraints.gridy;
1987//                log.info("Updated gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
1988
1989        // add glue to the bottom to allow resize
1990        if (c.getComponentCount() > 0) {
1991            c.add(Box.createVerticalGlue());
1992        }
1993
1994        // handle qualification if any
1995        QualifierAdder qa = new QualifierAdder() {
1996            @Override
1997            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1998                return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
1999            }
2000
2001            @Override
2002            protected void addListener(java.beans.PropertyChangeListener qc) {
2003                c.addPropertyChangeListener(qc);
2004            }
2005        };
2006
2007        qa.processModifierElements(element, _varModel);
2008        return c;
2009    }
2010
2011    /**
2012     * Create label from Element.
2013     *
2014     * @param e  element containing label contents
2015     * @param c  panel to insert label into
2016     * @param g  panel layout manager
2017     * @param cs constraints on layout manager
2018     */
2019    protected void makeLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
2020        String text = LocaleSelector.getAttribute(e, "text");
2021        if (text == null || text.equals("")) {
2022            text = LocaleSelector.getAttribute(e, "label"); // label subelement not since 3.7.5
2023        }
2024        final JLabel l = new JLabel(text);
2025        l.setAlignmentX(1.0f);
2026        cs.fill = GridBagConstraints.BOTH;
2027        log.trace("Add label: {} cs: {} fill: {} x: {} y: {}",
2028                l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
2029        g.setConstraints(l, cs);
2030        c.add(l);
2031        cs.fill = GridBagConstraints.NONE;
2032        cs.gridwidth = 1;
2033        cs.gridheight = 1;
2034
2035        // handle qualification if any
2036        QualifierAdder qa = new QualifierAdder() {
2037            @Override
2038            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
2039                return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
2040            }
2041
2042            @Override
2043            protected void addListener(java.beans.PropertyChangeListener qc) {
2044                l.addPropertyChangeListener(qc);
2045            }
2046        };
2047
2048        qa.processModifierElements(e, _varModel);
2049    }
2050
2051    /**
2052     * Create sound label from Element.
2053     *
2054     * @param e  element containing label contents
2055     * @param c  panel to insert label into
2056     * @param g  panel layout manager
2057     * @param cs constraints on layout manager
2058     */
2059    protected void makeSoundLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
2060        String labelText = rosterEntry.getSoundLabel(Integer.parseInt(Objects.requireNonNull(LocaleSelector.getAttribute(e, "num"))));
2061        final JLabel l = new JLabel(labelText);
2062        l.setAlignmentX(1.0f);
2063        cs.fill = GridBagConstraints.BOTH;
2064        if (log.isDebugEnabled()) {
2065            log.debug("Add soundlabel: {} cs: {} {} {} {}", l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
2066        }
2067        g.setConstraints(l, cs);
2068        c.add(l);
2069        cs.fill = GridBagConstraints.NONE;
2070        cs.gridwidth = 1;
2071        cs.gridheight = 1;
2072
2073        // handle qualification if any
2074        QualifierAdder qa = new QualifierAdder() {
2075            @Override
2076            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
2077                return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
2078            }
2079
2080            @Override
2081            protected void addListener(java.beans.PropertyChangeListener qc) {
2082                l.addPropertyChangeListener(qc);
2083            }
2084        };
2085
2086        qa.processModifierElements(e, _varModel);
2087    }
2088
2089    void makeCvTable(GridBagConstraints cs, GridBagLayout g, JPanel c) {
2090        log.debug("starting to build CvTable pane");
2091
2092        TableRowSorter<TableModel> sorter = new TableRowSorter<>(_cvModel);
2093
2094        JTable cvTable = new JTable(_cvModel);
2095
2096        sorter.setComparator(CvTableModel.NUMCOLUMN, new jmri.jmrit.symbolicprog.CVNameComparator());
2097
2098        List<RowSorter.SortKey> sortKeys = new ArrayList<>();
2099        sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
2100        sorter.setSortKeys(sortKeys);
2101
2102        cvTable.setRowSorter(sorter);
2103
2104        cvTable.setDefaultRenderer(JTextField.class, new CvValueRenderer());
2105        cvTable.setDefaultRenderer(JButton.class, new CvValueRenderer());
2106        cvTable.setDefaultRenderer(String.class, new CvValueRenderer());
2107        cvTable.setDefaultRenderer(Integer.class, new CvValueRenderer());
2108        cvTable.setDefaultEditor(JTextField.class, new ValueEditor());
2109        cvTable.setDefaultEditor(JButton.class, new ValueEditor());
2110        cvTable.setRowHeight(new JButton("X").getPreferredSize().height);
2111        // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541)
2112        // instead of forcing the columns to fill the frame (and only fill)
2113        //cvTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
2114        JScrollPane cvScroll = new JScrollPane(cvTable);
2115        cvScroll.setColumnHeaderView(cvTable.getTableHeader());
2116
2117        cs.fill = GridBagConstraints.BOTH;
2118        cs.weighty = 2.0;
2119        cs.weightx = 0.75;
2120        g.setConstraints(cvScroll, cs);
2121        c.add(cvScroll);
2122
2123        // remember which CVs to read/write
2124        isCvTablePane = true;
2125        setCvListFromTable();
2126
2127        _cvTable = true;
2128        log.debug("end of building CvTable pane");
2129    }
2130
2131    void setCvListFromTable() {
2132        // remember which CVs to read/write
2133        for (int j = 0; j < _cvModel.getRowCount(); j++) {
2134            cvList.add(j);
2135        }
2136        _varModel.setButtonModeFromProgrammer();
2137    }
2138
2139    /**
2140     * Pick an appropriate function map panel depending on model attribute.
2141     * <dl>
2142     * <dt>If attribute extFnsESU="yes":</dt>
2143     * <dd>Invoke
2144     * {@code FnMapPanelESU(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
2145     * <dt>Otherwise:</dt>
2146     * <dd>Invoke
2147     * {@code FnMapPanel(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
2148     * </dl>
2149     *
2150     * @param modelElem element containing model attributes
2151     * @param c         panel to add function map panel to
2152     * @param g         panel layout manager
2153     * @param cs        constraints on layout manager
2154     */
2155    // why does this use a different parameter order than all similar methods?
2156    void pickFnMapPanel(JPanel c, GridBagLayout g, GridBagConstraints cs, Element modelElem) {
2157        boolean extFnsESU = false;
2158        Attribute a = modelElem.getAttribute("extFnsESU");
2159        try {
2160            if (a != null) {
2161                extFnsESU = !(a.getValue()).equalsIgnoreCase("no");
2162            }
2163        } catch (Exception ex) {
2164            log.error("error handling decoder's extFnsESU value");
2165        }
2166        if (extFnsESU) {
2167            FnMapPanelESU l = new FnMapPanelESU(_varModel, varList, modelElem, rosterEntry, _cvModel);
2168            fnMapListESU.add(l); // remember for deletion
2169            cs.gridwidth = GridBagConstraints.REMAINDER;
2170            g.setConstraints(l, cs);
2171            c.add(l);
2172        } else {
2173            FnMapPanel l = new FnMapPanel(_varModel, varList, modelElem);
2174            fnMapList.add(l); // remember for deletion
2175            cs.gridwidth = GridBagConstraints.REMAINDER;
2176            g.setConstraints(l, cs);
2177            c.add(l);
2178        }
2179        cs.gridwidth = 1;
2180    }
2181
2182    /**
2183     * Add the representation of a single variable. The variable is defined by a
2184     * JDOM variable Element from the XML file.
2185     *
2186     * @param var         element containing variable
2187     * @param col         column to insert label into
2188     * @param g           panel layout manager
2189     * @param cs          constraints on layout manager
2190     * @param showStdName show the name following the rules for the
2191     * <em>nameFmt</em> element
2192     */
2193    public void newVariable(Element var, JComponent col,
2194            GridBagLayout g, GridBagConstraints cs, boolean showStdName) {
2195
2196        // get the name
2197        String name = var.getAttribute("item").getValue();
2198
2199        // if it doesn't exist, do nothing
2200        int i = _varModel.findVarIndex(name);
2201        if (i < 0) {
2202            log.trace("Variable \"{}\" not found, omitted", name);
2203            return;
2204        }
2205//        Leave here for now. Need to track pre-existing corner-case issue
2206//        log.info("Entry item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
2207
2208        // check label orientation
2209        Attribute attr;
2210        String layout = "left";  // this default is also set in the DTD
2211        if ((attr = var.getAttribute("layout")) != null && attr.getValue() != null) {
2212            layout = attr.getValue();
2213        }
2214
2215        // load label if specified, else use name
2216        String label = name;
2217        if (!showStdName) {
2218            // get name attribute from variable, as that's the mfg name
2219            label = _varModel.getLabel(i);
2220        }
2221        String temp = LocaleSelector.getAttribute(var, "label");
2222        if (temp != null) {
2223            label = temp;
2224        }
2225
2226        // get representation; store into the list to be programmed
2227        JComponent rep = getRepresentation(name, var);
2228        varList.add(i);
2229
2230        // create the paired label
2231        JLabel l = new WatchingLabel(label, rep);
2232
2233        int spaceWidth = getFontMetrics(l.getFont()).stringWidth(" ");
2234
2235        // now handle the four orientations
2236        // assemble v from label, rep
2237        switch (layout) {
2238            case "left":
2239                cs.anchor = GridBagConstraints.EAST;
2240                cs.ipadx = spaceWidth;
2241                g.setConstraints(l, cs);
2242                col.add(l);
2243                cs.ipadx = 0;
2244                cs.gridx++;
2245                cs.anchor = GridBagConstraints.WEST;
2246                g.setConstraints(rep, cs);
2247                col.add(rep);
2248                break;
2249//        log.info("Exit item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
2250            case "right":
2251                cs.anchor = GridBagConstraints.EAST;
2252                g.setConstraints(rep, cs);
2253                col.add(rep);
2254                cs.gridx++;
2255                cs.anchor = GridBagConstraints.WEST;
2256                cs.ipadx = spaceWidth;
2257                g.setConstraints(l, cs);
2258                col.add(l);
2259                cs.ipadx = 0;
2260                break;
2261            case "below":
2262                // variable in center of upper line
2263                cs.anchor = GridBagConstraints.CENTER;
2264                g.setConstraints(rep, cs);
2265                col.add(rep);
2266                // label aligned like others
2267                cs.gridy++;
2268                cs.anchor = GridBagConstraints.WEST;
2269                cs.ipadx = spaceWidth;
2270                g.setConstraints(l, cs);
2271                col.add(l);
2272                cs.ipadx = 0;
2273                break;
2274            case "above":
2275                // label aligned like others
2276                cs.anchor = GridBagConstraints.WEST;
2277                cs.ipadx = spaceWidth;
2278                g.setConstraints(l, cs);
2279                col.add(l);
2280                cs.ipadx = 0;
2281                // variable in center of lower line
2282                cs.gridy++;
2283                cs.anchor = GridBagConstraints.CENTER;
2284                g.setConstraints(rep, cs);
2285                col.add(rep);
2286                break;
2287            default:
2288                log.error("layout internally inconsistent: {}", layout);
2289        }
2290    }
2291
2292    /**
2293     * Get a GUI representation of a particular variable for display.
2294     *
2295     * @param name Name used to look up the Variable object
2296     * @param var  XML Element which might contain a "format" attribute to be
2297     *             used in the {@link VariableValue#getNewRep} call from the
2298     *             Variable object; "tooltip" elements are also processed here.
2299     * @return JComponent representing this variable
2300     */
2301    public JComponent getRepresentation(String name, Element var) {
2302        int i = _varModel.findVarIndex(name);
2303        VariableValue variable = _varModel.getVariable(i);
2304        JComponent rep = null;
2305        String format = "default";
2306        Attribute attr;
2307        if ((attr = var.getAttribute("format")) != null && attr.getValue() != null) {
2308            format = attr.getValue();
2309        }
2310
2311        boolean viewOnly = (var.getAttribute("viewOnly") != null &&
2312                var.getAttribute("viewOnly").getValue().equals("yes"));
2313
2314        if (i >= 0) {
2315            rep = getRep(i, format);
2316            rep.setMaximumSize(rep.getPreferredSize());
2317            // set tooltip if specified here & not overridden by defn in Variable
2318            String tip = LocaleSelector.getAttribute(var, "tooltip");
2319            if (rep.getToolTipText() != null) {
2320                tip = rep.getToolTipText();
2321            }
2322            rep.setToolTipText(modifyToolTipText(tip, variable));
2323            if (viewOnly) {
2324            rep.setEnabled(false);
2325            }
2326        }
2327        return rep;
2328    }
2329
2330    /**
2331     * Takes default tool tip text, e.g. from the decoder element, and modifies
2332     * it as needed.
2333     * <p>
2334     * Intended to handle e.g. adding CV numbers to variables.
2335     *
2336     * @param start    existing tool tip text
2337     * @param variable the CV
2338     * @return new tool tip text
2339     */
2340    String modifyToolTipText(String start, VariableValue variable) {
2341        log.trace("modifyToolTipText: {}", variable.label());
2342        // this is the place to invoke VariableValue methods to (conditionally)
2343        // add information about CVs, etc in the ToolTip text
2344
2345        // Optionally add CV numbers based on Roster Preferences setting
2346        start = CvUtil.addCvDescription(start, variable.getCvDescription(), variable.getMask());
2347
2348        // Indicate what the command station can do
2349        // need to update this with e.g. the specific CV numbers
2350        if (_cvModel.getProgrammer() != null
2351                && !_cvModel.getProgrammer().getCanRead()) {
2352            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotRead"));
2353        }
2354        if (_cvModel.getProgrammer() != null
2355                && !_cvModel.getProgrammer().getCanWrite()) {
2356            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotWrite"));
2357        }
2358
2359        // indicate other reasons for read/write constraints
2360        if (variable.getReadOnly()) {
2361            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeReadOnly"));
2362        }
2363        if (variable.getWriteOnly()) {
2364            start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeWriteOnly"));
2365        }
2366
2367        return start;
2368    }
2369
2370    JComponent getRep(int i, String format) {
2371        return (JComponent) (_varModel.getRep(i, format));
2372    }
2373
2374    /**
2375     * list of fnMapping objects to dispose
2376     */
2377    ArrayList<FnMapPanel> fnMapList = new ArrayList<>();
2378    ArrayList<FnMapPanelESU> fnMapListESU = new ArrayList<>();
2379    /**
2380     * list of JPanel objects to removeAll
2381     */
2382    ArrayList<JPanel> panelList = new ArrayList<>();
2383
2384    public void dispose() {
2385        log.debug("dispose");
2386
2387        // remove components
2388        removeAll();
2389
2390        readChangesButton.removeItemListener(l1);
2391        readAllButton.removeItemListener(l2);
2392        writeChangesButton.removeItemListener(l3);
2393        writeAllButton.removeItemListener(l4);
2394        confirmChangesButton.removeItemListener(l5);
2395        confirmAllButton.removeItemListener(l6);
2396        l1 = l2 = l3 = l4 = l5 = l6 = null;
2397
2398        if (_programmingVar != null) {
2399            _programmingVar.removePropertyChangeListener(this);
2400        }
2401        if (_programmingCV != null) {
2402            _programmingCV.removePropertyChangeListener(this);
2403        }
2404
2405        _programmingVar = null;
2406        _programmingCV = null;
2407
2408        varList.clear();
2409        varList = null;
2410        cvList.clear();
2411        cvList = null;
2412
2413        // dispose of any panels
2414        for (JPanel jPanel : panelList) {
2415            jPanel.removeAll();
2416        }
2417        panelList.clear();
2418        panelList = null;
2419
2420        // dispose of any fnMaps
2421        for (FnMapPanel fnMapPanel : fnMapList) {
2422            fnMapPanel.dispose();
2423        }
2424        fnMapList.clear();
2425        fnMapList = null;
2426
2427        // dispose of any fnMaps
2428        for (FnMapPanelESU fnMapPanelESU : fnMapListESU) {
2429            fnMapPanelESU.dispose();
2430        }
2431        fnMapListESU.clear();
2432        fnMapListESU = null;
2433
2434        readChangesButton = null;
2435        writeChangesButton = null;
2436
2437        // these are disposed elsewhere
2438        _cvModel = null;
2439        _varModel = null;
2440    }
2441
2442    /**
2443     * Check if varList and cvList, and thus the tab, is empty.
2444     *
2445     * @return true if empty
2446     */
2447    public boolean isEmpty() {
2448        return (varList.isEmpty() && cvList.isEmpty());
2449    }
2450
2451    public boolean includeInPrint() {
2452        return print;
2453    }
2454
2455    public void includeInPrint(boolean inc) {
2456        print = inc;
2457    }
2458    boolean print = false;
2459
2460    public void printPane(HardcopyWriter w) {
2461        // if pane is empty, don't print anything
2462        if (isEmpty()) {
2463            return;
2464        }
2465
2466        // Define column widths for name and value output.
2467        // Make col 2 slightly larger than col 1 and reduce both to allow for
2468        // extra spaces that will be added during concatenation
2469        int col1Width = w.getCharactersPerLine() / 2 - 3 - 5;
2470        int col2Width = w.getCharactersPerLine() / 2 - 3 + 5;
2471
2472        try {
2473            //Create a string of spaces the width of the first column
2474            StringBuilder spaces = new StringBuilder();
2475            spaces.append(" ".repeat(Math.max(0, col1Width)));
2476            // start with pane name in bold
2477            String heading1 = SymbolicProgBundle.getMessage("PrintHeadingField");
2478            String heading2 = SymbolicProgBundle.getMessage("PrintHeadingSetting");
2479            String s;
2480            int interval = spaces.length() - heading1.length();
2481            w.setFontStyle(Font.BOLD);
2482            // write the section name and dividing line
2483            s = mName;
2484            w.write(s, 0, s.length());
2485            w.writeBorders();
2486            //Draw horizontal dividing line for each Pane section
2487            w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
2488                    w.getCharactersPerLine() + 1);
2489            s = "\n";
2490            w.write(s, 0, s.length());
2491            // if this isn't the raw CV section, write the column headings
2492            if (cvList.isEmpty()) {
2493                w.setFontStyle(Font.BOLD + Font.ITALIC);
2494                s = "   " + heading1 + spaces.substring(0, interval) + "   " + heading2;
2495                w.write(s, 0, s.length());
2496                w.writeBorders();
2497                s = "\n";
2498                w.write(s, 0, s.length());
2499            }
2500            w.setFontStyle(Font.PLAIN);
2501            // Define a vector to store the names of variables that have been printed
2502            // already.  If they have been printed, they will be skipped.
2503            // Using a vector here since we don't know how many variables will
2504            // be printed and it allows expansion as necessary
2505            ArrayList<String> printedVariables = new ArrayList<>(10);
2506            // index over variables
2507            for (int varNum : varList) {
2508                VariableValue var = _varModel.getVariable(varNum);
2509                String name = var.label();
2510                if (name == null) {
2511                    name = var.item();
2512                }
2513                // Check if variable has been printed.  If not store it and print
2514                boolean alreadyPrinted = false;
2515                for (String printedVariable : printedVariables) {
2516                    if (name.equals(printedVariable)) {
2517                        alreadyPrinted = true;
2518                        break;
2519                    }
2520                }
2521                // If already printed, skip it.  If not, store it and print
2522                if (alreadyPrinted) {
2523                    continue;
2524                }
2525                printedVariables.add(name);
2526
2527                String value = var.getTextValue();
2528                String originalName = name;
2529                String originalValue = value;
2530                name = name + " (CV" + var.getCvNum() + ")"; // NO I18N
2531
2532                // define index values for name and value substrings
2533                int nameLeftIndex = 0;
2534                int nameRightIndex = name.length();
2535                int valueLeftIndex = 0;
2536                int valueRightIndex = value.length();
2537                String trimmedName;
2538                String trimmedValue;
2539
2540                // Check the name length to see if it is wider than the column.
2541                // If so, split it and do the same checks for the Value
2542                // Then concatenate the name and value (or the split versions thereof)
2543                // before writing - if split, repeat until all pieces have been output
2544                while ((valueLeftIndex < value.length()) || (nameLeftIndex < name.length())) {
2545                    // name split code
2546                    if (name.substring(nameLeftIndex).length() > col1Width) {
2547                        for (int j = 0; j < col1Width; j++) {
2548                            String delimiter = name.substring(nameLeftIndex + col1Width - j - 1, nameLeftIndex + col1Width - j);
2549                            if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
2550                                nameRightIndex = nameLeftIndex + col1Width - j;
2551                                break;
2552                            }
2553                        }
2554                        trimmedName = name.substring(nameLeftIndex, nameRightIndex);
2555                        nameLeftIndex = nameRightIndex;
2556                        int space = spaces.length() - trimmedName.length();
2557                        s = "   " + trimmedName + spaces.substring(0, space);
2558                    } else {
2559                        trimmedName = name.substring(nameLeftIndex);
2560                        int space = spaces.length() - trimmedName.length();
2561                        s = "   " + trimmedName + spaces.substring(0, space);
2562                        name = "";
2563                        nameLeftIndex = 0;
2564                    }
2565                    // value split code
2566                    if (value.substring(valueLeftIndex).length() > col2Width) {
2567                        for (int j = 0; j < col2Width; j++) {
2568                            String delimiter = value.substring(valueLeftIndex + col2Width - j - 1, valueLeftIndex + col2Width - j);
2569                            if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
2570                                valueRightIndex = valueLeftIndex + col2Width - j;
2571                                break;
2572                            }
2573                        }
2574                        trimmedValue = value.substring(valueLeftIndex, valueRightIndex);
2575                        valueLeftIndex = valueRightIndex;
2576                        s = s + "   " + trimmedValue;
2577                    } else {
2578                        trimmedValue = value.substring(valueLeftIndex);
2579                        s = s + "   " + trimmedValue;
2580                        valueLeftIndex = 0;
2581                        value = "";
2582                    }
2583                    w.write(s, 0, s.length());
2584                    w.writeBorders();
2585                    s = "\n";
2586                    w.write(s, 0, s.length());
2587                }
2588                // Check for a Speed Table output and create a graphic display.
2589                // Java 1.5 has a known bug, #6328248, that prevents printing of progress
2590                //  bars using old style printing classes.  It results in blank bars on Windows,
2591                //  but hangs Macs. The version check is a workaround.
2592                float v = Float.parseFloat(System.getProperty("java.version").substring(0, 3));
2593                if (originalName.equals("Speed Table") && v < 1.5) {
2594                    // set the height of the speed table graph in lines
2595                    int speedFrameLineHeight = 11;
2596                    s = "\n";
2597
2598                    // check that there is enough room on the page; if not,
2599                    // space down the rest of the page.
2600                    // don't use page break because we want the table borders to be written
2601                    // to the bottom of the page
2602                    int pageSize = w.getLinesPerPage();
2603                    int here = w.getCurrentLineNumber();
2604                    if (pageSize - here < speedFrameLineHeight) {
2605                        for (int j = 0; j < (pageSize - here); j++) {
2606                            w.writeBorders();
2607                            w.write(s, 0, s.length());
2608                        }
2609                    }
2610
2611                    // Now that there is page space, create the window to hold the graphic speed table
2612                    JWindow speedWindow = new JWindow();
2613                    // Window size as wide as possible to allow for largest type size
2614                    speedWindow.setSize(512, 165);
2615                    speedWindow.getContentPane().setBackground(Color.white);
2616                    speedWindow.getContentPane().setLayout(null);
2617                    // in preparation for display, extract the speed table values into an array
2618                    StringTokenizer valueTokens = new StringTokenizer(originalValue, ",", false);
2619                    int[] speedVals = new int[28];
2620                    int k = 0;
2621                    while (valueTokens.hasMoreTokens()) {
2622                        speedVals[k] = Integer.parseInt(valueTokens.nextToken());
2623                        k++;
2624                    }
2625
2626                    // Now create a set of vertical progress bar whose length is based
2627                    // on the speed table value (half height) and add them to the window
2628                    for (int j = 0; j < 28; j++) {
2629                        JProgressBar printerBar = new JProgressBar(JProgressBar.VERTICAL, 0, 127);
2630                        printerBar.setBounds(52 + j * 15, 19, 10, 127);
2631                        printerBar.setValue(speedVals[j] / 2);
2632                        printerBar.setBackground(Color.white);
2633                        printerBar.setForeground(Color.darkGray);
2634                        printerBar.setBorder(BorderFactory.createLineBorder(Color.black));
2635                        speedWindow.getContentPane().add(printerBar);
2636                        // create a set of value labels at the top containing the speed table values
2637                        JLabel barValLabel = new JLabel(Integer.toString(speedVals[j]), SwingConstants.CENTER);
2638                        barValLabel.setBounds(50 + j * 15, 4, 15, 15);
2639                        barValLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2640                        speedWindow.getContentPane().add(barValLabel);
2641                        //Create a set of labels at the bottom with the CV numbers in them
2642                        JLabel barCvLabel = new JLabel(Integer.toString(67 + j), SwingConstants.CENTER);
2643                        barCvLabel.setBounds(50 + j * 15, 150, 15, 15);
2644                        barCvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2645                        speedWindow.getContentPane().add(barCvLabel);
2646                    }
2647                    JLabel cvLabel = new JLabel(Bundle.getMessage("Value"));
2648                    cvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2649                    cvLabel.setBounds(25, 4, 26, 15);
2650                    speedWindow.getContentPane().add(cvLabel);
2651                    JLabel valueLabel = new JLabel("CV"); // I18N seems undesirable for support
2652                    valueLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
2653                    valueLabel.setBounds(37, 150, 13, 15);
2654                    speedWindow.getContentPane().add(valueLabel);
2655                    // pass the complete window to the printing class
2656                    w.write(speedWindow);
2657                    // Now need to write the borders on sides of table
2658                    for (int j = 0; j < speedFrameLineHeight; j++) {
2659                        w.writeBorders();
2660                        w.write(s, 0, s.length());
2661                    }
2662                }
2663            }
2664
2665            final int TABLE_COLS = 3;
2666
2667            // index over CVs
2668            if (cvList.size() > 0) {
2669//            Check how many Cvs there are to print
2670                int cvCount = cvList.size();
2671                w.setFontStyle(Font.BOLD); //set font to Bold
2672                // print a simple heading with I18N
2673                s = String.format("%1$21s", Bundle.getMessage("Value")) + String.format("%1$28s", Bundle.getMessage("Value")) +
2674                        String.format("%1$28s", Bundle.getMessage("Value"));
2675                w.write(s, 0, s.length());
2676                w.writeBorders();
2677                s = "\n";
2678                w.write(s, 0, s.length());
2679                // NO I18N
2680                s = "            CV  Dec Hex                 CV  Dec Hex                 CV  Dec Hex";
2681                w.write(s, 0, s.length());
2682                w.writeBorders();
2683                s = "\n";
2684                w.write(s, 0, s.length());
2685                w.setFontStyle(0); //set font back to Normal
2686                //           }
2687                /*create an array to hold CV/Value strings to allow reformatting and sorting
2688                 Same size as the table drawn above (TABLE_COLS columns*tableHeight; heading rows
2689                 not included). Use the count of how many CVs there are to determine the number
2690                 of table rows required.  Add one more row if the divison into TABLE_COLS columns
2691                 isn't even.
2692                 */
2693                int tableHeight = cvCount / TABLE_COLS;
2694                if (cvCount % TABLE_COLS > 0) {
2695                    tableHeight++;
2696                }
2697                String[] cvStrings = new String[TABLE_COLS * tableHeight];
2698
2699                //blank the array
2700                Arrays.fill(cvStrings, "");
2701
2702                // get each CV and value
2703                int i = 0;
2704                for (int cvNum : cvList) {
2705                    CvValue cv = _cvModel.getCvByRow(cvNum);
2706
2707                    int value = cv.getValue();
2708
2709                    //convert and pad numbers as needed
2710                    String numString = String.format("%12s", cv.number());
2711                    StringBuilder valueString = new StringBuilder(Integer.toString(value));
2712                    String valueStringHex = Integer.toHexString(value).toUpperCase();
2713                    if (value < 16) {
2714                        valueStringHex = "0" + valueStringHex;
2715                    }
2716                    for (int j = 1; j < 3; j++) {
2717                        if (valueString.length() < 3) {
2718                            valueString.insert(0, " ");
2719                        }
2720                    }
2721                    //Create composite string of CV and its decimal and hex values
2722                    s = "  " + numString + "  " + valueString + "  " + valueStringHex
2723                            + " ";
2724
2725                    //populate printing array - still treated as a single column
2726                    cvStrings[i] = s;
2727                    i++;
2728                }
2729
2730                //sort the array in CV order (just the members with values)
2731                String temp;
2732                boolean swap;
2733                do {
2734                    swap = false;
2735                    for (i = 0; i < _cvModel.getRowCount() - 1; i++) {
2736                        if (PrintCvAction.cvSortOrderVal(cvStrings[i + 1].substring(0, 15).trim()) < PrintCvAction.cvSortOrderVal(cvStrings[i].substring(0, 15).trim())) {
2737                            temp = cvStrings[i + 1];
2738                            cvStrings[i + 1] = cvStrings[i];
2739                            cvStrings[i] = temp;
2740                            swap = true;
2741                        }
2742                    }
2743                } while (swap);
2744
2745                //Print the array in four columns
2746                for (i = 0; i < tableHeight; i++) {
2747                    s = cvStrings[i] + "    " + cvStrings[i + tableHeight] + "    " + cvStrings[i
2748                            + tableHeight * 2];
2749                    w.write(s, 0, s.length());
2750                    w.writeBorders();
2751                    s = "\n";
2752                    w.write(s, 0, s.length());
2753                }
2754            }
2755            s = "\n";
2756            w.writeBorders();
2757            w.write(s, 0, s.length());
2758            w.writeBorders();
2759            w.write(s, 0, s.length());
2760
2761            // handle special cases
2762        } catch (IOException e) {
2763            log.warn("error during printing", e);
2764        }
2765
2766    }
2767
2768    private JPanel addDccAddressPanel(Element e) {
2769        JPanel l = new DccAddressPanel(_varModel);
2770        panelList.add(l);
2771        // make sure this will get read/written, even if real vars not on pane
2772        int iVar;
2773
2774        // note we want Short Address first, as it might change others
2775        iVar = _varModel.findVarIndex("Short Address");
2776        if (iVar >= 0) {
2777            varList.add(iVar);
2778        } else {
2779            log.debug("addDccAddressPanel did not find Short Address");
2780        }
2781
2782        iVar = _varModel.findVarIndex("Address Format");
2783        if (iVar >= 0) {
2784            varList.add(iVar);
2785        } else {
2786            log.debug("addDccAddressPanel did not find Address Format");
2787        }
2788
2789        iVar = _varModel.findVarIndex("Long Address");
2790        if (iVar >= 0) {
2791            varList.add(iVar);
2792        } else {
2793            log.debug("addDccAddressPanel did not find Long Address");
2794        }
2795
2796        // included here because CV1 can modify it, even if it doesn't show on pane;
2797        iVar = _varModel.findVarIndex("Consist Address");
2798        if (iVar >= 0) {
2799            varList.add(iVar);
2800        } else {
2801            log.debug("addDccAddressPanel did not find CV19 Consist Address");
2802        }
2803
2804        return l;
2805    }
2806
2807    private final static Logger log = LoggerFactory.getLogger(PaneProgPane.class);
2808
2809}