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