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