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