java/src/jmri/jmrit/symbolicprog/tabbedframe/PaneProgPane.java
package jmri.jmrit.symbolicprog.tabbedframe;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JWindow;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.SwingConstants;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import jmri.jmrit.roster.RosterEntry;
import jmri.jmrit.symbolicprog.AbstractValue;
import jmri.jmrit.symbolicprog.CvTableModel;
import jmri.jmrit.symbolicprog.CvValue;
import jmri.jmrit.symbolicprog.DccAddressPanel;
import jmri.jmrit.symbolicprog.FnMapPanel;
import jmri.jmrit.symbolicprog.FnMapPanelESU;
import jmri.jmrit.symbolicprog.PrintCvAction;
import jmri.jmrit.symbolicprog.Qualifier;
import jmri.jmrit.symbolicprog.QualifierAdder;
import jmri.jmrit.symbolicprog.SymbolicProgBundle;
import jmri.jmrit.symbolicprog.ValueEditor;
import jmri.jmrit.symbolicprog.CvValueRenderer;
import jmri.jmrit.symbolicprog.VariableTableModel;
import jmri.jmrit.symbolicprog.VariableValue;
import jmri.util.CvUtil;
import jmri.util.StringUtil;
import jmri.util.davidflanagan.HardcopyWriter;
import jmri.util.jdom.LocaleSelector;
import org.jdom2.Attribute;
import org.jdom2.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provide the individual panes for the TabbedPaneProgrammer.
* <p>
* Note that this is not only the panes carrying variables, but also the special
* purpose panes for the CV table, etc.
* <p>
* This class implements PropertyChangeListener so that it can be notified when
* a variable changes its busy status at the end of a programming read/write
* operation.
*
* There are four read and write operation types, all of which have to be
* handled carefully:
* <DL>
* <DT>Write Changes<DD>This must write changes that occur after the operation
* starts, because the act of writing a variable/CV may change another. For
* example, writing CV 1 will mark CV 29 as changed.
* <p>
* The definition of "changed" is operationally in the
* {@link jmri.jmrit.symbolicprog.VariableValue#isChanged} member function.
*
* <DT>Write All<DD>Like write changes, this might have to go back and re-write
* a variable depending on what has previously happened. It should write every
* variable (at least) once.
* <DT>Read All<DD>This should read every variable once.
* <img src="doc-files/PaneProgPane-ReadAllSequenceDiagram.png" alt="UML Sequence diagram">
* <DT>Read Changes<DD>This should read every variable that's marked as changed.
* Currently, we use a common definition of changed with the write operations,
* and that someday might have to change.
*
* </DL>
*
* @author Bob Jacobsen Copyright (C) 2001, 2003, 2004, 2005, 2006
* @author D Miller Copyright 2003
* @author Howard G. Penny Copyright (C) 2005
* @author Dave Heap Copyright (C) 2014, 2019
* @see jmri.jmrit.symbolicprog.VariableValue#isChanged
*/
/*
* @startuml jmri/jmrit/symbolicprog/tabbedframe/doc-files/PaneProgPane-ReadAllSequenceDiagram.png
* actor User
* box "PaneProgPane"
* participant readPaneAll
* participant prepReadPane
* participant nextRead
* participant executeRead
* participant propertyChange
* participant replyWhileProgrammingVar
* participant restartProgramming
* end box
* box "VariableValue"
* participant readAll
* participant readChanges
* end box
*
* control Programmer
* User -> readPaneAll: Read All Sheets
* activate readPaneAll
* readPaneAll -> prepReadPane
* activate prepReadPane
* prepReadPane --> readPaneAll
* deactivate prepReadPane
* deactivate prepReadPane
* readPaneAll -> nextRead
* activate nextRead
* nextRead -> executeRead
* activate executeRead
* executeRead -> readAll
* activate readAll
* readAll -> Programmer
* activate Programmer
* readAll --> executeRead
* deactivate readAll
* executeRead --> nextRead
* deactivate executeRead
* nextRead --> readPaneAll
* deactivate nextRead
* deactivate readPaneAll
* == Callback after read completes ==
* Programmer -> propertyChange
* activate propertyChange
* note over propertyChange
* if the first read failed,
* setup a second read of
* the same value.
* otherwise, setup a read of
* the next value.
* end note
* deactivate Programmer
* propertyChange -> User: CV value or error
* propertyChange -> replyWhileProgrammingVar
* activate replyWhileProgrammingVar
* replyWhileProgrammingVar -> restartProgramming
* activate restartProgramming
* restartProgramming -> nextRead
* activate nextRead
* nextRead -> executeRead
* activate executeRead
* executeRead -> readAll
* activate readAll
* readAll -> Programmer
* activate Programmer
* readAll --> executeRead
* deactivate readAll
* executeRead -> nextRead
* deactivate executeRead
* nextRead --> restartProgramming
* deactivate nextRead
* restartProgramming --> replyWhileProgrammingVar
* deactivate restartProgramming
* replyWhileProgrammingVar --> propertyChange
* deactivate replyWhileProgrammingVar
* deactivate propertyChange
* deactivate Programmer
* == Callback triggered repeat occurs until no more values ==
* @enduml
*/
public class PaneProgPane extends javax.swing.JPanel
implements java.beans.PropertyChangeListener {
static final String LAST_GRIDX = "last_gridx";
static final String LAST_GRIDY = "last_gridy";
protected CvTableModel _cvModel;
protected VariableTableModel _varModel;
protected PaneContainer container;
protected RosterEntry rosterEntry;
boolean _cvTable;
protected JPanel bottom;
transient ItemListener l1;
protected transient ItemListener l2;
transient ItemListener l3;
protected transient ItemListener l4;
transient ItemListener l5;
transient ItemListener l6;
boolean isCvTablePane = false;
/**
* Store name of this programmer Tab (pane)
*/
String mName = "";
/**
* Construct a null object.
* <p>
* Normally only used for tests and to pre-load classes.
*/
public PaneProgPane() {
}
public PaneProgPane(PaneContainer parent, String name, Element pane, CvTableModel cvModel, VariableTableModel varModel, Element modelElem, RosterEntry pRosterEntry) {
this(parent, name, pane, cvModel, varModel, modelElem, pRosterEntry, false);
}
/**
* Construct the Pane from the XML definition element.
*
* @param parent The parent pane
* @param name Name to appear on tab of pane
* @param pane The JDOM Element for the pane definition
* @param cvModel Already existing TableModel containing the CV
* definitions
* @param varModel Already existing TableModel containing the variable
* definitions
* @param modelElem "model" element from the Decoder Index, used to check
* what decoder options are present.
* @param pRosterEntry The current roster entry, used to get sound labels.
* @param isProgPane True if the pane is a default programmer pane
*/
public PaneProgPane(PaneContainer parent, String name, Element pane, CvTableModel cvModel, VariableTableModel varModel, Element modelElem, RosterEntry pRosterEntry, boolean isProgPane) {
container = parent;
mName = name;
_cvModel = cvModel;
_varModel = varModel;
rosterEntry = pRosterEntry;
// when true a cv table with compare was loaded into pane
_cvTable = false;
// This is a JPanel containing a JScrollPane, containing a
// laid-out JPanel
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
// Add tooltip (if available)
setToolTipText(jmri.util.jdom.LocaleSelector.getAttribute(pane, "tooltip"));
// find out whether to display "label" (false) or "item" (true)
boolean showItem = false;
Attribute nameFmt = pane.getAttribute("nameFmt");
if (nameFmt != null && nameFmt.getValue().equals("item")) {
log.debug("Pane {} will show items, not labels, from decoder file", name);
showItem = true;
}
// put the columns left to right in a panel
JPanel p = new JPanel();
panelList.add(p);
p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
// handle the xml definition
// for all "column" elements ...
List<Element> colList = pane.getChildren("column");
for (Element element : colList) {
// load each column
p.add(newColumn(element, showItem, modelElem));
}
// for all "row" elements ...
List<Element> rowList = pane.getChildren("row");
for (Element element : rowList) {
// load each row
p.add(newRow(element, showItem, modelElem));
}
// for all "grid" elements ...
List<Element> gridList = pane.getChildren("grid");
for (Element element : gridList) {
// load each grid
p.add(newGrid(element, showItem, modelElem));
}
// for all "group" elements ...
List<Element> groupList = pane.getChildren("group");
for (Element element : groupList) {
// load each group
p.add(newGroup(element, showItem, modelElem));
}
// explain why pane is empty
if (cvList.isEmpty() && varList.isEmpty() && isProgPane) {
JPanel pe = new JPanel();
pe.setLayout(new BoxLayout(pe, BoxLayout.Y_AXIS));
int line = 1;
while (line >= 0) {
try {
String msg = SymbolicProgBundle.getMessage("TextTabEmptyExplain" + line);
if (msg.isEmpty()) {
msg = " ";
}
JLabel l = new JLabel(msg);
l.setAlignmentX(Component.CENTER_ALIGNMENT);
pe.add(l);
line++;
} catch (java.util.MissingResourceException e) { // deliberately runs until exception
line = -1;
}
}
add(pe);
panelList.add(pe);
return;
}
// add glue to the right to allow resize - but this isn't working as expected? Alignment?
add(Box.createHorizontalGlue());
add(new JScrollPane(p));
// add buttons in a new panel
bottom = new JPanel();
panelList.add(p);
bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));
// enable read buttons, if possible, and
// set their tool tips
enableReadButtons();
// add read button listeners
readChangesButton.addItemListener(l1 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
readChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopReadChangesSheet"));
if (!container.isBusy()) {
prepReadPane(true);
prepGlassPane(readChangesButton);
container.getBusyGlassPane().setVisible(true);
readPaneChanges();
}
} else {
stopProgramming();
readChangesButton.setText(SymbolicProgBundle.getMessage("ButtonReadChangesSheet"));
if (container.isBusy()) {
readChangesButton.setEnabled(false);
}
}
});
readAllButton.addItemListener(l2 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
readAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopReadSheet"));
if (!container.isBusy()) {
prepReadPane(false);
prepGlassPane(readAllButton);
container.getBusyGlassPane().setVisible(true);
readPaneAll();
}
} else {
stopProgramming();
readAllButton.setText(SymbolicProgBundle.getMessage("ButtonReadFullSheet"));
if (container.isBusy()) {
readAllButton.setEnabled(false);
}
}
});
writeChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipWriteHighlightedSheet"));
writeChangesButton.addItemListener(l3 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
writeChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopWriteChangesSheet"));
if (!container.isBusy()) {
prepWritePane(true);
prepGlassPane(writeChangesButton);
container.getBusyGlassPane().setVisible(true);
writePaneChanges();
}
} else {
stopProgramming();
writeChangesButton.setText(SymbolicProgBundle.getMessage("ButtonWriteChangesSheet"));
if (container.isBusy()) {
writeChangesButton.setEnabled(false);
}
}
});
writeAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipWriteAllSheet"));
writeAllButton.addItemListener(l4 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
writeAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopWriteSheet"));
if (!container.isBusy()) {
prepWritePane(false);
prepGlassPane(writeAllButton);
container.getBusyGlassPane().setVisible(true);
writePaneAll();
}
} else {
stopProgramming();
writeAllButton.setText(SymbolicProgBundle.getMessage("ButtonWriteFullSheet"));
if (container.isBusy()) {
writeAllButton.setEnabled(false);
}
}
});
// enable confirm buttons, if possible, and
// set their tool tips
enableConfirmButtons();
// add confirm button listeners
confirmChangesButton.addItemListener(l5 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
confirmChangesButton.setText(SymbolicProgBundle.getMessage("ButtonStopConfirmChangesSheet"));
if (!container.isBusy()) {
prepConfirmPane(true);
prepGlassPane(confirmChangesButton);
container.getBusyGlassPane().setVisible(true);
confirmPaneChanges();
}
} else {
stopProgramming();
confirmChangesButton.setText(SymbolicProgBundle.getMessage("ButtonConfirmChangesSheet"));
if (container.isBusy()) {
confirmChangesButton.setEnabled(false);
}
}
});
confirmAllButton.addItemListener(l6 = (ItemEvent e) -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
confirmAllButton.setText(SymbolicProgBundle.getMessage("ButtonStopConfirmSheet"));
if (!container.isBusy()) {
prepConfirmPane(false);
prepGlassPane(confirmAllButton);
container.getBusyGlassPane().setVisible(true);
confirmPaneAll();
}
} else {
stopProgramming();
confirmAllButton.setText(SymbolicProgBundle.getMessage("ButtonConfirmFullSheet"));
if (container.isBusy()) {
confirmAllButton.setEnabled(false);
}
}
});
// Only add change buttons to CV tables
bottom.add(readChangesButton);
bottom.add(writeChangesButton);
if (_cvTable) {
bottom.add(confirmChangesButton);
}
bottom.add(readAllButton);
bottom.add(writeAllButton);
if (_cvTable) {
bottom.add(confirmAllButton);
}
// don't show buttons if no programmer at all
if (_cvModel.getProgrammer() != null) {
add(bottom);
}
}
@Override
public String getName() {
return mName;
}
@Override
public String toString() {
return getName();
}
/**
* Enable the read all and read changes button if possible. This checks to
* make sure this is appropriate, given the attached programmer's
* capability.
*/
void enableReadButtons() {
readChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipReadChangesSheet"));
readAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipReadAllSheet"));
if (_cvModel.getProgrammer() != null
&& !_cvModel.getProgrammer().getCanRead()) {
// can't read, disable the buttons
readChangesButton.setEnabled(false);
readAllButton.setEnabled(false);
// set tooltip to explain why
readChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
readAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
} else {
readChangesButton.setEnabled(true);
readAllButton.setEnabled(true);
}
}
/**
* Enable the compare all and compare changes button if possible. This
* checks to make sure this is appropriate, given the attached programmer's
* capability.
*/
void enableConfirmButtons() {
confirmChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipConfirmChangesSheet"));
confirmAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipConfirmAllSheet"));
if (_cvModel.getProgrammer() != null
&& !_cvModel.getProgrammer().getCanRead()) {
// can't read, disable the buttons
confirmChangesButton.setEnabled(false);
confirmAllButton.setEnabled(false);
// set tooltip to explain why
confirmChangesButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
confirmAllButton.setToolTipText(SymbolicProgBundle.getMessage("TipNoRead"));
} else {
confirmChangesButton.setEnabled(true);
confirmAllButton.setEnabled(true);
}
}
/**
* This remembers the variables on this pane for the Read/Write sheet
* operation. They are stored as a list of Integer objects, each of which is
* the index of the Variable in the VariableTable.
*/
List<Integer> varList = new ArrayList<>();
int varListIndex;
/**
* This remembers the CVs on this pane for the Read/Write sheet operation.
* They are stored as a set of Integer objects, each of which is the index
* of the CV in the CVTable. Note that variables are handled separately, and
* the CVs that are represented by variables are not entered here. So far
* (sic), the only use of this is for the cvtable rep.
*/
protected TreeSet<Integer> cvList = new TreeSet<>(); // TreeSet is iterated in order
protected Iterator<Integer> cvListIterator;
protected JToggleButton readChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonReadChangesSheet"));
protected JToggleButton readAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonReadFullSheet"));
protected JToggleButton writeChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonWriteChangesSheet"));
protected JToggleButton writeAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonWriteFullSheet"));
JToggleButton confirmChangesButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonConfirmChangesSheet"));
JToggleButton confirmAllButton = new JToggleButton(SymbolicProgBundle.getMessage("ButtonConfirmFullSheet"));
/**
* Estimate the number of CVs that will be accessed when reading or writing
* the contents of this pane.
*
* @param read true if counting for read, false for write
* @param changes true if counting for a *Changes operation; false, if
* counting for a *All operation
* @return the total number of CV reads/writes needed for this pane
*/
public int countOpsNeeded(boolean read, boolean changes) {
Set<Integer> set = new HashSet<>(cvList.size() + varList.size() + 50);
return makeOpsNeededSet(read, changes, set).size();
}
/**
* Produce a set of CVs that will be accessed when reading or writing the
* contents of this pane.
*
* @param read true if counting for read, false for write
* @param changes true if counting for a *Changes operation; false, if
* counting for a *All operation
* @param set The set to fill. Any CVs already in here will not be
* duplicated, which provides a way to aggregate a set of CVs
* across multiple panes.
* @return the same set as the parameter, for convenient chaining of
* operations.
*/
public Set<Integer> makeOpsNeededSet(boolean read, boolean changes, Set<Integer> set) {
// scan the variable list
for (int varNum : varList) {
VariableValue var = _varModel.getVariable(varNum);
// must decide whether this one should be counted
if (!changes || var.isChanged()) {
CvValue[] cvs = var.usesCVs();
for (CvValue cv : cvs) {
// always of interest
if (!changes || VariableValue.considerChanged(cv)) {
set.add(Integer.valueOf(cv.number()));
}
}
}
}
return set;
}
private void prepGlassPane(AbstractButton activeButton) {
container.prepGlassPane(activeButton);
}
void enableButtons(boolean stat) {
if (stat) {
enableReadButtons();
enableConfirmButtons();
} else {
readChangesButton.setEnabled(stat);
readAllButton.setEnabled(stat);
confirmChangesButton.setEnabled(stat);
confirmAllButton.setEnabled(stat);
}
writeChangesButton.setEnabled(stat);
writeAllButton.setEnabled(stat);
}
boolean justChanges;
/**
* Invoked by "Read changes on sheet" button, this sets in motion a
* continuing sequence of "read" operations on the variables and
* CVs in the Pane. Only variables in states marked as "changed" will be
* read.
*
* @return true is a read has been started, false if the pane is complete.
*/
public boolean readPaneChanges() {
if (log.isDebugEnabled()) {
log.debug("readPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
}
prepReadPane(true);
return nextRead();
}
/**
* Prepare this pane for a read operation.
* <p>
* The read mechanism only reads variables in certain states (and needs to
* do that to handle error processing right now), so this is implemented by
* first setting all variables and CVs on this pane to TOREAD via this
* method
*
* @param onlyChanges true if only reading changes; false if reading all
*/
public void prepReadPane(boolean onlyChanges) {
log.debug("start prepReadPane with onlyChanges={}", onlyChanges);
justChanges = onlyChanges;
if (isCvTablePane) {
setCvListFromTable(); // make sure list of CVs up to date if table
}
enableButtons(false);
if (justChanges) {
readChangesButton.setEnabled(true);
readChangesButton.setSelected(true);
} else {
readAllButton.setSelected(true);
readAllButton.setEnabled(true);
}
if (!container.isBusy()) {
container.enableButtons(false);
}
setToRead(justChanges, true);
varListIndex = 0;
cvListIterator = cvList.iterator();
}
/**
* Invoked by "Read Full Sheet" button, this sets in motion a continuing
* sequence of "read" operations on the variables and CVs in the
* Pane. The read mechanism only reads variables in certain states (and
* needs to do that to handle error processing right now), so this is
* implemented by first setting all variables and CVs on this pane to TOREAD
* in prepReadPaneAll, then starting the execution.
*
* @return true is a read has been started, false if the pane is complete
*/
public boolean readPaneAll() {
if (log.isDebugEnabled()) {
log.debug("readAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
}
prepReadPane(false);
// start operation
return nextRead();
}
/**
* Set the "ToRead" parameter in all variables and CVs on this pane.
*
* @param justChanges true if this is read changes, false if read all
* @param startProcess true if this is the start of processing, false if
* cleaning up at end
*/
void setToRead(boolean justChanges, boolean startProcess) {
if (!container.isBusy()
|| // the frame has already setToRead
(!startProcess)) { // we want to setToRead false if the pane's process is being stopped
for (int varNum : varList) {
VariableValue var = _varModel.getVariable(varNum);
if (justChanges) {
if (var.isChanged()) {
var.setToRead(startProcess);
} else {
var.setToRead(false);
}
} else {
var.setToRead(startProcess);
}
}
if (isCvTablePane) {
setCvListFromTable(); // make sure list of CVs up to date if table
}
for (int cvNum : cvList) {
CvValue cv = _cvModel.getCvByRow(cvNum);
if (justChanges) {
if (VariableValue.considerChanged(cv)) {
cv.setToRead(startProcess);
} else {
cv.setToRead(false);
}
} else {
cv.setToRead(startProcess);
}
}
}
}
/**
* Set the "ToWrite" parameter in all variables and CVs on this pane
*
* @param justChanges true if this is read changes, false if read all
* @param startProcess true if this is the start of processing, false if
* cleaning up at end
*/
void setToWrite(boolean justChanges, boolean startProcess) {
log.debug("start setToWrite method with {},{}", justChanges, startProcess);
if (!container.isBusy()
|| // the frame has already setToWrite
(!startProcess)) { // we want to setToWrite false if the pane's process is being stopped
log.debug("about to start setToWrite of varList");
for (int varNum : varList) {
VariableValue var = _varModel.getVariable(varNum);
if (justChanges) {
if (var.isChanged()) {
var.setToWrite(startProcess);
} else {
var.setToWrite(false);
}
} else {
var.setToWrite(startProcess);
}
}
log.debug("about to start setToWrite of cvList");
if (isCvTablePane) {
setCvListFromTable(); // make sure list of CVs up to date if table
}
for (int cvNum : cvList) {
CvValue cv = _cvModel.getCvByRow(cvNum);
if (justChanges) {
if (VariableValue.considerChanged(cv)) {
cv.setToWrite(startProcess);
} else {
cv.setToWrite(false);
}
} else {
cv.setToWrite(startProcess);
}
}
}
log.debug("end setToWrite method");
}
void executeRead(VariableValue var) {
setBusy(true);
// var.setToRead(false); // variables set this themselves
if (_programmingVar != null) {
log.error("listener already set at read start");
}
_programmingVar = var;
_read = true;
// get notified when that state changes so can repeat
_programmingVar.addPropertyChangeListener(this);
// and make the read request
if (justChanges) {
_programmingVar.readChanges();
} else {
_programmingVar.readAll();
}
}
void executeWrite(VariableValue var) {
setBusy(true);
// var.setToWrite(false); // variables reset themselves when done
if (_programmingVar != null) {
log.error("listener already set at write start");
}
_programmingVar = var;
_read = false;
// get notified when that state changes so can repeat
_programmingVar.addPropertyChangeListener(this);
// and make the write request
if (justChanges) {
_programmingVar.writeChanges();
} else {
_programmingVar.writeAll();
}
}
/**
* If there are any more read operations to be done on this pane, do the
* next one.
* <p>
* Each invocation of this method reads one variable or CV; completion of
* that request will cause it to happen again, reading the next one, until
* there's nothing left to read.
* @return true is a read has been started, false if the pane is complete.
*/
boolean nextRead() {
// look for possible variables
if (log.isDebugEnabled()) {
log.debug("nextRead scans {} variables", varList.size());
}
while ((varList.size() > 0) && (varListIndex < varList.size())) {
int varNum = varList.get(varListIndex);
AbstractValue.ValueState vState = _varModel.getState(varNum);
VariableValue var = _varModel.getVariable(varNum);
if (log.isDebugEnabled()) {
log.debug("nextRead var index {} state {} isToRead: {} label: {}", varNum, vState.getName(), var.isToRead(), var.label());
}
varListIndex++;
if (var.isToRead()) {
if (log.isDebugEnabled()) {
log.debug("start read of variable {}", _varModel.getLabel(varNum));
}
executeRead(var);
log.debug("return from starting var read");
// the request may have instantaneously been satisfied...
return true; // only make one request at a time!
}
}
// found no variables needing read, try CVs
if (log.isDebugEnabled()) {
log.debug("nextRead scans {} CVs", cvList.size());
}
while (cvListIterator != null && cvListIterator.hasNext()) {
int cvNum = cvListIterator.next();
CvValue cv = _cvModel.getCvByRow(cvNum);
if (log.isDebugEnabled()) {
log.debug("nextRead cv index {} state {}", cvNum, cv.getState());
}
if (cv.isToRead()) { // always read UNKNOWN state
log.debug("start read of cv {}", cvNum);
setBusy(true);
if (_programmingCV != null) {
log.error("listener already set at read start");
}
_programmingCV = _cvModel.getCvByRow(cvNum);
_read = true;
// get notified when that state changes so can repeat
_programmingCV.addPropertyChangeListener(this);
// and make the read request
// _programmingCV.setToRead(false); // CVs set this themselves
_programmingCV.read(_cvModel.getStatusLabel());
log.debug("return from starting CV read");
// the request may have instantateously been satisfied...
return true; // only make one request at a time!
}
}
// nothing to program, end politely
log.debug("nextRead found nothing to do");
readChangesButton.setSelected(false);
readAllButton.setSelected(false); // reset both, as that's final state we want
setBusy(false);
container.paneFinished();
return false;
}
/**
* If there are any more compare operations to be done on this pane, do the
* next one.
* <p>
* Each invocation of this method compares one CV; completion of that request
* will cause it to happen again, reading the next one, until there's
* nothing left to read.
*
* @return true is a compare has been started, false if the pane is
* complete.
*/
boolean nextConfirm() {
// look for possible CVs
while (cvListIterator != null && cvListIterator.hasNext()) {
int cvNum = cvListIterator.next();
CvValue cv = _cvModel.getCvByRow(cvNum);
if (log.isDebugEnabled()) {
log.debug("nextConfirm cv index {} state {}", cvNum, cv.getState());
}
if (cv.isToRead()) {
log.debug("start confirm of cv {}", cvNum);
setBusy(true);
if (_programmingCV != null) {
log.error("listener already set at confirm start");
}
_programmingCV = _cvModel.getCvByRow(cvNum);
_read = true;
// get notified when that state changes so can repeat
_programmingCV.addPropertyChangeListener(this);
// and make the compare request
_programmingCV.confirm(_cvModel.getStatusLabel());
log.debug("return from starting CV confirm");
// the request may have instantateously been satisfied...
return true; // only make one request at a time!
}
}
// nothing to program, end politely
log.debug("nextConfirm found nothing to do");
confirmChangesButton.setSelected(false);
confirmAllButton.setSelected(false); // reset both, as that's final state we want
setBusy(false);
container.paneFinished();
return false;
}
/**
* Invoked by "Write changes on sheet" button, this sets in motion a
* continuing sequence of "write" operations on the variables in the Pane.
* Only variables in isChanged states are written; other states don't need
* to be.
*
* @return true if a write has been started, false if the pane is complete
*/
public boolean writePaneChanges() {
log.debug("writePaneChanges starts");
prepWritePane(true);
boolean val = nextWrite();
log.debug("writePaneChanges returns {}", val);
return val;
}
/**
* Invoked by "Write full sheet" button to write all CVs.
*
* @return true if a write has been started, false if the pane is complete
*/
public boolean writePaneAll() {
prepWritePane(false);
return nextWrite();
}
/**
* Prepare a "write full sheet" operation.
*
* @param onlyChanges true if only writing changes; false if writing all
*/
public void prepWritePane(boolean onlyChanges) {
log.debug("start prepWritePane with {}", onlyChanges);
justChanges = onlyChanges;
enableButtons(false);
if (isCvTablePane) {
setCvListFromTable(); // make sure list of CVs up to date if table
}
if (justChanges) {
writeChangesButton.setEnabled(true);
writeChangesButton.setSelected(true);
} else {
writeAllButton.setSelected(true);
writeAllButton.setEnabled(true);
}
if (!container.isBusy()) {
container.enableButtons(false);
}
setToWrite(justChanges, true);
varListIndex = 0;
cvListIterator = cvList.iterator();
log.debug("end prepWritePane");
}
boolean nextWrite() {
log.debug("start nextWrite");
// look for possible variables
while ((varList.size() > 0) && (varListIndex < varList.size())) {
int varNum = varList.get(varListIndex);
AbstractValue.ValueState vState = _varModel.getState(varNum);
VariableValue var = _varModel.getVariable(varNum);
if (log.isDebugEnabled()) {
log.debug("nextWrite var index {} state {} isToWrite: {} label:{}", varNum, vState.getName(), var.isToWrite(), var.label());
}
varListIndex++;
if (var.isToWrite()) {
log.debug("start write of variable {}", _varModel.getLabel(varNum));
executeWrite(var);
log.debug("return from starting var write");
return true; // only make one request at a time!
}
}
// check for CVs to handle (e.g. for CV table)
while (cvListIterator != null && cvListIterator.hasNext()) {
int cvNum = cvListIterator.next();
CvValue cv = _cvModel.getCvByRow(cvNum);
if (log.isDebugEnabled()) {
log.debug("nextWrite cv index {} state {}", cvNum, cv.getState());
}
if (cv.isToWrite()) {
log.debug("start write of cv index {}", cvNum);
setBusy(true);
if (_programmingCV != null) {
log.error("listener already set at write start");
}
_programmingCV = _cvModel.getCvByRow(cvNum);
_read = false;
// get notified when that state changes so can repeat
_programmingCV.addPropertyChangeListener(this);
// and make the write request
// _programmingCV.setToWrite(false); // CVs set this themselves
_programmingCV.write(_cvModel.getStatusLabel());
log.debug("return from starting cv write");
return true; // only make one request at a time!
}
}
// nothing to program, end politely
log.debug("nextWrite found nothing to do");
writeChangesButton.setSelected(false);
writeAllButton.setSelected(false);
setBusy(false);
container.paneFinished();
log.debug("return from nextWrite with nothing to do");
return false;
}
/**
* Prepare this pane for a compare operation.
* <p>
* The read mechanism only reads variables in certain states (and needs to
* do that to handle error processing right now), so this is implemented by
* first setting all variables and CVs on this pane to TOREAD via this
* method
*
* @param onlyChanges true if only confirming changes; false if confirming
* all
*/
public void prepConfirmPane(boolean onlyChanges) {
log.debug("start prepReadPane with onlyChanges={}", onlyChanges);
justChanges = onlyChanges;
enableButtons(false);
if (isCvTablePane) {
setCvListFromTable(); // make sure list of CVs up to date if table
}
if (justChanges) {
confirmChangesButton.setEnabled(true);
confirmChangesButton.setSelected(true);
} else {
confirmAllButton.setSelected(true);
confirmAllButton.setEnabled(true);
}
if (!container.isBusy()) {
container.enableButtons(false);
}
// we can use the read prep since confirm has to read first
setToRead(justChanges, true);
varListIndex = 0;
cvListIterator = cvList.iterator();
}
/**
* Invoked by "Compare changes on sheet" button, this sets in motion a
* continuing sequence of "confirm" operations on the variables and
* CVs in the Pane. Only variables in states marked as "changed" will be
* checked.
*
* @return true is a confirm has been started, false if the pane is
* complete.
*/
public boolean confirmPaneChanges() {
if (log.isDebugEnabled()) {
log.debug("confirmPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
}
prepConfirmPane(true);
return nextConfirm();
}
/**
* Invoked by "Compare Full Sheet" button, this sets in motion a continuing
* sequence of "confirm" operations on the variables and CVs in the
* Pane. The read mechanism only reads variables in certain states (and
* needs to do that to handle error processing right now), so this is
* implemented by first setting all variables and CVs on this pane to TOREAD
* in prepReadPaneAll, then starting the execution.
*
* @return true is a confirm has been started, false if the pane is
* complete.
*/
public boolean confirmPaneAll() {
if (log.isDebugEnabled()) {
log.debug("confirmAllPane starts with {} vars, {} cvs ", varList.size(), cvList.size());
}
prepConfirmPane(false);
// start operation
return nextConfirm();
}
// reference to variable being programmed (or null if none)
VariableValue _programmingVar = null;
CvValue _programmingCV = null;
boolean _read = true;
// busy during read, write operations
private boolean _busy = false;
public boolean isBusy() {
return _busy;
}
protected void setBusy(boolean busy) {
boolean oldBusy = _busy;
_busy = busy;
if (!busy && !container.isBusy()) {
enableButtons(true);
}
if (oldBusy != busy) {
firePropertyChange("Busy", Boolean.valueOf(oldBusy), Boolean.valueOf(busy));
}
}
private int retry = 0;
/**
* Get notification of a variable property change, specifically "busy" going
* to false at the end of a programming operation. If we're in a programming
* operation, we then continue it by reinvoking the nextRead/writePane
* operation.
*
* @param e the event to respond to
*/
@Override
public void propertyChange(java.beans.PropertyChangeEvent e) {
// check for the right event & condition
if (_programmingVar == null && _programmingCV == null ) {
log.warn("unexpected propertChange: {}", e);
return;
} else if (log.isDebugEnabled()) {
log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue());
}
// find the right way to handle this
if (e.getSource() == _programmingVar
&& e.getPropertyName().equals("Busy")
&& e.getNewValue().equals(Boolean.FALSE)) {
if (_programmingVar.getState() == AbstractValue.ValueState.UNKNOWN) {
if (retry == 0) {
varListIndex--;
retry++;
if (_read) {
_programmingVar.setToRead(true); // set the variable
// to read again.
} else {
_programmingVar.setToWrite(true); // set the variable
// to attempt another
// write.
}
} else {
retry = 0;
}
}
replyWhileProgrammingVar();
} else if (e.getSource() == _programmingCV
&& e.getPropertyName().equals("Busy")
&& e.getNewValue().equals(Boolean.FALSE)) {
// there's no -- operator on the HashSet Iterator we're
// using for the CV list, so we don't do individual retries
// now.
//if (_programmingCV.getState() == CvValue.UNKNOWN) {
// if (retry == 0) {
// cvListIndex--;
// retry++;
// } else {
// retry = 0;
// }
//}
replyWhileProgrammingCV();
} else {
if (log.isDebugEnabled() && e.getPropertyName().equals("Busy")) {
log.debug("ignoring change of Busy {} {}", e.getNewValue(), e.getNewValue().equals(Boolean.FALSE));
}
}
}
public void replyWhileProgrammingVar() {
log.debug("correct event for programming variable, restart operation");
// remove existing listener
_programmingVar.removePropertyChangeListener(this);
_programmingVar = null;
// restart the operation
restartProgramming();
}
public void replyWhileProgrammingCV() {
log.debug("correct event for programming CV, restart operation");
// remove existing listener
_programmingCV.removePropertyChangeListener(this);
_programmingCV = null;
// restart the operation
restartProgramming();
}
void restartProgramming() {
log.debug("start restartProgramming");
if (_read && readChangesButton.isSelected()) {
nextRead();
} else if (_read && readAllButton.isSelected()) {
nextRead();
} else if (_read && confirmChangesButton.isSelected()) {
nextConfirm();
} else if (_read && confirmAllButton.isSelected()) {
nextConfirm();
} else if (writeChangesButton.isSelected()) {
nextWrite(); // was writePaneChanges
} else if (writeAllButton.isSelected()) {
nextWrite();
} else {
log.debug("No operation to restart");
if (isBusy()) {
container.paneFinished();
setBusy(false);
}
}
log.debug("end restartProgramming");
}
protected void stopProgramming() {
log.debug("start stopProgramming");
setToRead(false, false);
setToWrite(false, false);
varListIndex = varList.size();
cvListIterator = null;
log.debug("end stopProgramming");
}
/**
* Create a new group from the JDOM group Element
*
* @param element element containing group contents
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
* @return a panel containing the group
*/
protected JPanel newGroup(Element element, boolean showStdName, Element modelElem) {
// create a panel to add as a new column or row
final JPanel c = new JPanel();
panelList.add(c);
GridBagLayout g = new GridBagLayout();
GridBagConstraints cs = new GridBagConstraints();
c.setLayout(g);
// handle include/exclude
if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
return c;
}
// handle the xml definition
// for all elements in the column or row
List<Element> elemList = element.getChildren();
log.trace("newColumn starting with {} elements", elemList.size());
for (Element e : elemList) {
String name = e.getName();
log.trace("newGroup processing {} element", name);
// decode the type
if (name.equals("display")) { // its a variable
// load the variable
newVariable(e, c, g, cs, showStdName);
} else if (name.equals("separator")) { // its a separator
JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
cs.fill = GridBagConstraints.BOTH;
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(j, cs);
c.add(j);
cs.gridwidth = 1;
} else if (name.equals("label")) {
cs.gridwidth = GridBagConstraints.REMAINDER;
makeLabel(e, c, g, cs);
} else if (name.equals("soundlabel")) {
cs.gridwidth = GridBagConstraints.REMAINDER;
makeSoundLabel(e, c, g, cs);
} else if (name.equals("cvtable")) {
makeCvTable(cs, g, c);
} else if (name.equals("fnmapping")) {
pickFnMapPanel(c, g, cs, modelElem);
} else if (name.equals("dccaddress")) {
JPanel l = addDccAddressPanel(e);
if (l.getComponentCount() > 0) {
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("column")) {
// nested "column" elements ...
cs.gridheight = GridBagConstraints.REMAINDER;
JPanel l = newColumn(e, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("row")) {
// nested "row" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newRow(e, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("grid")) {
// nested "grid" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newGrid(e, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("group")) {
// nested "group" elements ...
JPanel l = newGroup(e, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
}
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newColumn", e.getName());
}
}
// add glue to the bottom to allow resize
if (c.getComponentCount() > 0) {
c.add(Box.createVerticalGlue());
}
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
c.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(element, _varModel);
return c;
}
/**
* Create a new grid group from the JDOM group Element.
*
* @param element element containing group contents
* @param c the panel to create the grid in
* @param g the layout manager for the panel
* @param globs properties to configure g
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
*/
protected void newGridGroup(Element element, final JPanel c, GridBagLayout g, GridGlobals globs, boolean showStdName, Element modelElem) {
// handle include/exclude
if (!PaneProgFrame.isIncludedFE(element, modelElem, rosterEntry, "", "")) {
return;
}
// handle the xml definition
// for all elements in the column or row
List<Element> elemList = element.getChildren();
log.trace("newColumn starting with {} elements", elemList.size());
for (Element e : elemList) {
String name = e.getName();
log.trace("newGroup processing {} element", name);
// decode the type
if (name.equals("griditem")) {
final JPanel l = newGridItem(e, showStdName, modelElem, globs);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, globs.gridConstraints);
c.add(l);
// globs.gridConstraints.gridwidth = 1;
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
l.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(e, _varModel);
}
} else if (name.equals("group")) {
// nested "group" elements ...
newGridGroup(e, c, g, globs, showStdName, modelElem);
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newColumn", e.getName());
}
}
// add glue to the bottom to allow resize
// if (c.getComponentCount() > 0) {
// c.add(Box.createVerticalGlue());
// }
}
/**
* Create a single column from the JDOM column Element.
*
* @param element element containing column contents
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
* @return a panel containing the group
*/
public JPanel newColumn(Element element, boolean showStdName, Element modelElem) {
// create a panel to add as a new column or row
final JPanel c = new JPanel();
panelList.add(c);
GridBagLayout g = new GridBagLayout();
GridBagConstraints cs = new GridBagConstraints();
c.setLayout(g);
// handle the xml definition
// for all elements in the column or row
List<Element> elemList = element.getChildren();
log.trace("newColumn starting with {} elements", elemList.size());
for (Element value : elemList) {
// update the grid position
cs.gridy++;
cs.gridx = 0;
String name = value.getName();
log.trace("newColumn processing {} element", name);
// decode the type
if (name.equals("display")) { // it's a variable
// load the variable
newVariable(value, c, g, cs, showStdName);
} else if (name.equals("separator")) { // its a separator
JSeparator j = new JSeparator(SwingConstants.HORIZONTAL);
cs.fill = GridBagConstraints.BOTH;
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(j, cs);
c.add(j);
cs.gridwidth = 1;
} else if (name.equals("label")) {
cs.gridwidth = GridBagConstraints.REMAINDER;
makeLabel(value, c, g, cs);
} else if (name.equals("soundlabel")) {
cs.gridwidth = GridBagConstraints.REMAINDER;
makeSoundLabel(value, c, g, cs);
} else if (name.equals("cvtable")) {
makeCvTable(cs, g, c);
} else if (name.equals("fnmapping")) {
pickFnMapPanel(c, g, cs, modelElem);
} else if (name.equals("dccaddress")) {
JPanel l = addDccAddressPanel(value);
if (l.getComponentCount() > 0) {
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("column")) {
// nested "column" elements ...
cs.gridheight = GridBagConstraints.REMAINDER;
JPanel l = newColumn(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("row")) {
// nested "row" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newRow(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("grid")) {
// nested "grid" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newGrid(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("group")) {
// nested "group" elements ...
JPanel l = newGroup(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
}
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newColumn", value.getName());
}
}
// add glue to the bottom to allow resize
if (c.getComponentCount() > 0) {
c.add(Box.createVerticalGlue());
}
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
c.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(element, _varModel);
return c;
}
/**
* Create a single row from the JDOM column Element
*
* @param element element containing row contents
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
* @return a panel containing the group
*/
public JPanel newRow(Element element, boolean showStdName, Element modelElem) {
// create a panel to add as a new column or row
final JPanel c = new JPanel();
panelList.add(c);
GridBagLayout g = new GridBagLayout();
GridBagConstraints cs = new GridBagConstraints();
c.setLayout(g);
// handle the xml definition
// for all elements in the column or row
List<Element> elemList = element.getChildren();
log.trace("newRow starting with {} elements", elemList.size());
for (Element value : elemList) {
// update the grid position
cs.gridy = 0;
cs.gridx++;
String name = value.getName();
log.trace("newRow processing {} element", name);
// decode the type
if (name.equals("display")) { // its a variable
// load the variable
newVariable(value, c, g, cs, showStdName);
} else if (name.equals("separator")) { // its a separator
JSeparator j = new JSeparator(SwingConstants.VERTICAL);
cs.fill = GridBagConstraints.BOTH;
cs.gridheight = GridBagConstraints.REMAINDER;
g.setConstraints(j, cs);
c.add(j);
cs.fill = GridBagConstraints.NONE;
cs.gridheight = 1;
} else if (name.equals("label")) {
cs.gridheight = GridBagConstraints.REMAINDER;
makeLabel(value, c, g, cs);
} else if (name.equals("soundlabel")) {
cs.gridheight = GridBagConstraints.REMAINDER;
makeSoundLabel(value, c, g, cs);
} else if (name.equals("cvtable")) {
makeCvTable(cs, g, c);
} else if (name.equals("fnmapping")) {
pickFnMapPanel(c, g, cs, modelElem);
} else if (name.equals("dccaddress")) {
JPanel l = addDccAddressPanel(value);
if (l.getComponentCount() > 0) {
cs.gridheight = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("column")) {
// nested "column" elements ...
cs.gridheight = GridBagConstraints.REMAINDER;
JPanel l = newColumn(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("row")) {
// nested "row" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newRow(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("grid")) {
// nested "grid" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newGrid(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("group")) {
// nested "group" elements ...
JPanel l = newGroup(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
}
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newRow", value.getName());
}
}
// add glue to the bottom to allow resize
if (c.getComponentCount() > 0) {
c.add(Box.createVerticalGlue());
}
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
c.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(element, _varModel);
return c;
}
/**
* Create a grid from the JDOM Element.
*
* @param element element containing group contents
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
* @return a panel containing the group
*/
public JPanel newGrid(Element element, boolean showStdName, Element modelElem) {
// create a panel to add as a new grid
final JPanel c = new JPanel();
panelList.add(c);
GridBagLayout g = new GridBagLayout();
c.setLayout(g);
GridGlobals globs = new GridGlobals();
// handle the xml definition
// for all elements in the grid
List<Element> elemList = element.getChildren();
globs.gridAttList = element.getAttributes(); // get grid-level attributes
log.trace("newGrid starting with {} elements", elemList.size());
for (Element value : elemList) {
globs.gridConstraints = new GridBagConstraints();
String name = value.getName();
log.trace("newGrid processing {} element", name);
// decode the type
if (name.equals("griditem")) {
JPanel l = newGridItem(value, showStdName, modelElem, globs);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, globs.gridConstraints);
c.add(l);
// globs.gridConstraints.gridwidth = 1;
}
} else if (name.equals("group")) {
// nested "group" elements ...
newGridGroup(value, c, g, globs, showStdName, modelElem);
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newGrid", value.getName());
}
}
// add glue to the bottom to allow resize
if (c.getComponentCount() > 0) {
c.add(Box.createVerticalGlue());
}
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
c.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(element, _varModel);
return c;
}
protected static class GridGlobals {
public int gridxCurrent = -1;
public int gridyCurrent = -1;
public List<Attribute> gridAttList;
public GridBagConstraints gridConstraints;
}
/**
* Create a grid item from the JDOM Element
*
* @param element element containing grid item contents
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
* @param modelElem element containing the decoder model
* @param globs properties to configure the layout
* @return a panel containing the group
*/
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED") // setAccessible()
public JPanel newGridItem(Element element, boolean showStdName, Element modelElem, GridGlobals globs) {
List<Attribute> itemAttList = element.getAttributes(); // get item-level attributes
List<Attribute> attList = new ArrayList<>(globs.gridAttList);
attList.addAll(itemAttList); // merge grid and item-level attributes
// log.info("New gridtiem -----------------------------------------------");
// log.info("Attribute list:"+attList);
attList.add(new Attribute(LAST_GRIDX, ""));
attList.add(new Attribute(LAST_GRIDY, ""));
// log.info("Updated Attribute list:"+attList);
// Attribute ax = attList.get(attList.size()-2);
// Attribute ay = attList.get(attList.size()-1);
// log.info("ax="+ax+";ay="+ay);
// log.info("Previous gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
for (int j = 0; j < attList.size(); j++) {
Attribute attrib = attList.get(j);
String attribName = attrib.getName();
String attribRawValue = attrib.getValue();
Field constraint;
String constraintType;
// make sure we only process the last gridx or gridy attribute in the list
if (attribName.equals("gridx")) {
Attribute a = new Attribute(LAST_GRIDX, attribRawValue);
attList.set(attList.size() - 2, a);
// log.info("Moved & Updated Attribute list:"+attList);
continue; //. don't process now
}
if (attribName.equals("gridy")) {
Attribute a = new Attribute(LAST_GRIDY, attribRawValue);
attList.set(attList.size() - 1, a);
// log.info("Moved & Updated Attribute list:"+attList);
continue; //. don't process now
}
if (attribName.equals(LAST_GRIDX)) { // we must be at end of original list, restore last gridx
attribName = "gridx";
if (attribRawValue.equals("")) { // don't process blank (unused)
continue;
}
}
if (attribName.equals(LAST_GRIDY)) { // we must be at end of original list, restore last gridy
attribName = "gridy";
if (attribRawValue.equals("")) { // don't process blank (unused)
continue;
}
}
if ((attribName.equals("gridx") || attribName.equals("gridy")) && attribRawValue.equals("RELATIVE")) {
attribRawValue = "NEXT"; // NEXT is a synonym for RELATIVE
}
if (attribName.equals("gridx") && attribRawValue.equals("CURRENT")) {
attribRawValue = String.valueOf(Math.max(0, globs.gridxCurrent));
}
if (attribName.equals("gridy") && attribRawValue.equals("CURRENT")) {
attribRawValue = String.valueOf(Math.max(0, globs.gridyCurrent));
}
if (attribName.equals("gridx") && attribRawValue.equals("NEXT")) {
attribRawValue = String.valueOf(++globs.gridxCurrent);
}
if (attribName.equals("gridy") && attribRawValue.equals("NEXT")) {
attribRawValue = String.valueOf(++globs.gridyCurrent);
}
// log.info("attribName="+attribName+";attribRawValue="+attribRawValue);
try {
constraint = globs.gridConstraints.getClass().getDeclaredField(attribName);
constraintType = constraint.getType().toString();
constraint.setAccessible(true);
} catch (NoSuchFieldException ex) {
log.error("Unrecognised attribute \"{}\", skipping", attribName);
continue;
}
switch (constraintType) {
case "int": {
int attribValue;
try {
attribValue = Integer.parseInt(attribRawValue);
constraint.set(globs.gridConstraints, attribValue);
} catch (IllegalAccessException ey) {
log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
} catch (NumberFormatException ex) {
try {
Field constant = globs.gridConstraints.getClass().getDeclaredField(attribRawValue);
constant.setAccessible(true);
attribValue = (Integer) GridBagConstraints.class.getField(attribRawValue).get(constant);
constraint.set(globs.gridConstraints, attribValue);
} catch (NoSuchFieldException ey) {
log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
} catch (IllegalAccessException ey) {
log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
}
}
break;
}
case "double": {
double attribValue;
try {
attribValue = Double.parseDouble(attribRawValue);
constraint.set(globs.gridConstraints, attribValue);
} catch (IllegalAccessException ey) {
log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
} catch (NumberFormatException ex) {
log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
}
break;
}
case "class java.awt.Insets":
try {
String[] insetStrings = attribRawValue.split(",");
if (insetStrings.length == 4) {
Insets attribValue = new Insets(Integer.parseInt(insetStrings[0]), Integer.parseInt(insetStrings[1]), Integer.parseInt(insetStrings[2]), Integer.parseInt(insetStrings[3]));
constraint.set(globs.gridConstraints, attribValue);
} else {
log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
log.error("Value should be four integers of the form \"top,left,bottom,right\"");
}
} catch (IllegalAccessException ey) {
log.error("Unable to set constraint \"{}. IllegalAccessException error thrown.", attribName);
} catch (NumberFormatException ex) {
log.error("Invalid value \"{}\" for attribute \"{}\"", attribRawValue, attribName);
log.error("Value should be four integers of the form \"top,left,bottom,right\"");
}
break;
default:
log.error("Required \"{}\" handler for attribute \"{}\" not defined in JMRI code", constraintType, attribName);
log.error("Please file a JMRI bug report at https://sourceforge.net/p/jmri/bugs/new/");
break;
}
}
// log.info("Updated globs.GridBagConstraints.gridx="+globs.gridConstraints.gridx+";globs.GridBagConstraints.gridy="+globs.gridConstraints.gridy);
// create a panel to add as a new grid item
final JPanel c = new JPanel();
panelList.add(c);
GridBagLayout g = new GridBagLayout();
GridBagConstraints cs = new GridBagConstraints();
c.setLayout(g);
// handle the xml definition
// for all elements in the grid item
List<Element> elemList = element.getChildren();
log.trace("newGridItem starting with {} elements", elemList.size());
for (Element value : elemList) {
// update the grid position
cs.gridy = 0;
cs.gridx++;
String name = value.getName();
log.trace("newGridItem processing {} element", name);
// decode the type
if (name.equals("display")) { // its a variable
// load the variable
newVariable(value, c, g, cs, showStdName);
} else if (name.equals("separator")) { // its a separator
JSeparator j = new JSeparator(SwingConstants.VERTICAL);
cs.fill = GridBagConstraints.BOTH;
cs.gridheight = GridBagConstraints.REMAINDER;
g.setConstraints(j, cs);
c.add(j);
cs.fill = GridBagConstraints.NONE;
cs.gridheight = 1;
} else if (name.equals("label")) {
cs.gridheight = GridBagConstraints.REMAINDER;
makeLabel(value, c, g, cs);
} else if (name.equals("soundlabel")) {
cs.gridheight = GridBagConstraints.REMAINDER;
makeSoundLabel(value, c, g, cs);
} else if (name.equals("cvtable")) {
makeCvTable(cs, g, c);
} else if (name.equals("fnmapping")) {
pickFnMapPanel(c, g, cs, modelElem);
} else if (name.equals("dccaddress")) {
JPanel l = addDccAddressPanel(value);
if (l.getComponentCount() > 0) {
cs.gridheight = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("column")) {
// nested "column" elements ...
cs.gridheight = GridBagConstraints.REMAINDER;
JPanel l = newColumn(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridheight = 1;
}
} else if (name.equals("row")) {
// nested "row" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newRow(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("grid")) {
// nested "grid" elements ...
cs.gridwidth = GridBagConstraints.REMAINDER;
JPanel l = newGrid(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
cs.gridwidth = 1;
}
} else if (name.equals("group")) {
// nested "group" elements ...
JPanel l = newGroup(value, showStdName, modelElem);
if (l.getComponentCount() > 0) {
panelList.add(l);
g.setConstraints(l, cs);
c.add(l);
}
} else if (!name.equals("qualifier")) { // its a mistake
log.error("No code to handle element of type {} in newGridItem", value.getName());
}
}
globs.gridxCurrent = globs.gridConstraints.gridx;
globs.gridyCurrent = globs.gridConstraints.gridy;
// log.info("Updated gridxCurrent="+globs.gridxCurrent+";gridyCurrent="+globs.gridyCurrent);
// add glue to the bottom to allow resize
if (c.getComponentCount() > 0) {
c.add(Box.createVerticalGlue());
}
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(c, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
c.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(element, _varModel);
return c;
}
/**
* Create label from Element.
*
* @param e element containing label contents
* @param c panel to insert label into
* @param g panel layout manager
* @param cs constraints on layout manager
*/
protected void makeLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
String text = LocaleSelector.getAttribute(e, "text");
if (text == null || text.equals("")) {
text = LocaleSelector.getAttribute(e, "label"); // label subelement not since 3.7.5
}
final JLabel l = new JLabel(text);
l.setAlignmentX(1.0f);
cs.fill = GridBagConstraints.BOTH;
log.trace("Add label: {} cs: {} fill: {} x: {} y: {}",
l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
g.setConstraints(l, cs);
c.add(l);
cs.fill = GridBagConstraints.NONE;
cs.gridwidth = 1;
cs.gridheight = 1;
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
l.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(e, _varModel);
}
/**
* Create sound label from Element.
*
* @param e element containing label contents
* @param c panel to insert label into
* @param g panel layout manager
* @param cs constraints on layout manager
*/
protected void makeSoundLabel(Element e, JPanel c, GridBagLayout g, GridBagConstraints cs) {
String labelText = rosterEntry.getSoundLabel(Integer.parseInt(Objects.requireNonNull(LocaleSelector.getAttribute(e, "num"))));
final JLabel l = new JLabel(labelText);
l.setAlignmentX(1.0f);
cs.fill = GridBagConstraints.BOTH;
if (log.isDebugEnabled()) {
log.debug("Add soundlabel: {} cs: {} {} {} {}", l.getText(), cs.gridwidth, cs.fill, cs.gridx, cs.gridy);
}
g.setConstraints(l, cs);
c.add(l);
cs.fill = GridBagConstraints.NONE;
cs.gridwidth = 1;
cs.gridheight = 1;
// handle qualification if any
QualifierAdder qa = new QualifierAdder() {
@Override
protected Qualifier createQualifier(VariableValue var, String relation, String value) {
return new JComponentQualifier(l, var, Integer.parseInt(value), relation);
}
@Override
protected void addListener(java.beans.PropertyChangeListener qc) {
l.addPropertyChangeListener(qc);
}
};
qa.processModifierElements(e, _varModel);
}
void makeCvTable(GridBagConstraints cs, GridBagLayout g, JPanel c) {
log.debug("starting to build CvTable pane");
TableRowSorter<TableModel> sorter = new TableRowSorter<>(_cvModel);
JTable cvTable = new JTable(_cvModel);
sorter.setComparator(CvTableModel.NUMCOLUMN, new jmri.jmrit.symbolicprog.CVNameComparator());
List<RowSorter.SortKey> sortKeys = new ArrayList<>();
sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
sorter.setSortKeys(sortKeys);
cvTable.setRowSorter(sorter);
cvTable.setDefaultRenderer(JTextField.class, new CvValueRenderer());
cvTable.setDefaultRenderer(JButton.class, new CvValueRenderer());
cvTable.setDefaultRenderer(String.class, new CvValueRenderer());
cvTable.setDefaultRenderer(Integer.class, new CvValueRenderer());
cvTable.setDefaultEditor(JTextField.class, new ValueEditor());
cvTable.setDefaultEditor(JButton.class, new ValueEditor());
cvTable.setRowHeight(new JButton("X").getPreferredSize().height);
// have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541)
// instead of forcing the columns to fill the frame (and only fill)
//cvTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane cvScroll = new JScrollPane(cvTable);
cvScroll.setColumnHeaderView(cvTable.getTableHeader());
cs.fill = GridBagConstraints.BOTH;
cs.weighty = 2.0;
cs.weightx = 0.75;
g.setConstraints(cvScroll, cs);
c.add(cvScroll);
// remember which CVs to read/write
isCvTablePane = true;
setCvListFromTable();
_cvTable = true;
log.debug("end of building CvTable pane");
}
void setCvListFromTable() {
// remember which CVs to read/write
for (int j = 0; j < _cvModel.getRowCount(); j++) {
cvList.add(j);
}
_varModel.setButtonModeFromProgrammer();
}
/**
* Pick an appropriate function map panel depending on model attribute.
* <dl>
* <dt>If attribute extFnsESU="yes":</dt>
* <dd>Invoke
* {@code FnMapPanelESU(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
* <dt>Otherwise:</dt>
* <dd>Invoke
* {@code FnMapPanel(VariableTableModel v, List<Integer> varsUsed, Element model)}</dd>
* </dl>
*
* @param modelElem element containing model attributes
* @param c panel to add function map panel to
* @param g panel layout manager
* @param cs constraints on layout manager
*/
// why does this use a different parameter order than all similar methods?
void pickFnMapPanel(JPanel c, GridBagLayout g, GridBagConstraints cs, Element modelElem) {
boolean extFnsESU = false;
Attribute a = modelElem.getAttribute("extFnsESU");
try {
if (a != null) {
extFnsESU = !(a.getValue()).equalsIgnoreCase("no");
}
} catch (Exception ex) {
log.error("error handling decoder's extFnsESU value");
}
if (extFnsESU) {
FnMapPanelESU l = new FnMapPanelESU(_varModel, varList, modelElem, rosterEntry, _cvModel);
fnMapListESU.add(l); // remember for deletion
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
} else {
FnMapPanel l = new FnMapPanel(_varModel, varList, modelElem);
fnMapList.add(l); // remember for deletion
cs.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(l, cs);
c.add(l);
}
cs.gridwidth = 1;
}
/**
* Add the representation of a single variable. The variable is defined by a
* JDOM variable Element from the XML file.
*
* @param var element containing variable
* @param col column to insert label into
* @param g panel layout manager
* @param cs constraints on layout manager
* @param showStdName show the name following the rules for the
* <em>nameFmt</em> element
*/
public void newVariable(Element var, JComponent col,
GridBagLayout g, GridBagConstraints cs, boolean showStdName) {
// get the name
String name = var.getAttribute("item").getValue();
// if it doesn't exist, do nothing
int i = _varModel.findVarIndex(name);
if (i < 0) {
log.trace("Variable \"{}\" not found, omitted", name);
return;
}
// Leave here for now. Need to track pre-existing corner-case issue
// log.info("Entry item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
// check label orientation
Attribute attr;
String layout = "left"; // this default is also set in the DTD
if ((attr = var.getAttribute("layout")) != null && attr.getValue() != null) {
layout = attr.getValue();
}
// load label if specified, else use name
String label = name;
if (!showStdName) {
// get name attribute from variable, as that's the mfg name
label = _varModel.getLabel(i);
}
String temp = LocaleSelector.getAttribute(var, "label");
if (temp != null) {
label = temp;
}
// get representation; store into the list to be programmed
JComponent rep = getRepresentation(name, var);
varList.add(i);
// create the paired label
JLabel l = new WatchingLabel(label, rep);
int spaceWidth = getFontMetrics(l.getFont()).stringWidth(" ");
// now handle the four orientations
// assemble v from label, rep
switch (layout) {
case "left":
cs.anchor = GridBagConstraints.EAST;
cs.ipadx = spaceWidth;
g.setConstraints(l, cs);
col.add(l);
cs.ipadx = 0;
cs.gridx++;
cs.anchor = GridBagConstraints.WEST;
g.setConstraints(rep, cs);
col.add(rep);
break;
// log.info("Exit item="+name+";cs.gridx="+cs.gridx+";cs.gridy="+cs.gridy+";cs.anchor="+cs.anchor+";cs.ipadx="+cs.ipadx);
case "right":
cs.anchor = GridBagConstraints.EAST;
g.setConstraints(rep, cs);
col.add(rep);
cs.gridx++;
cs.anchor = GridBagConstraints.WEST;
cs.ipadx = spaceWidth;
g.setConstraints(l, cs);
col.add(l);
cs.ipadx = 0;
break;
case "below":
// variable in center of upper line
cs.anchor = GridBagConstraints.CENTER;
g.setConstraints(rep, cs);
col.add(rep);
// label aligned like others
cs.gridy++;
cs.anchor = GridBagConstraints.WEST;
cs.ipadx = spaceWidth;
g.setConstraints(l, cs);
col.add(l);
cs.ipadx = 0;
break;
case "above":
// label aligned like others
cs.anchor = GridBagConstraints.WEST;
cs.ipadx = spaceWidth;
g.setConstraints(l, cs);
col.add(l);
cs.ipadx = 0;
// variable in center of lower line
cs.gridy++;
cs.anchor = GridBagConstraints.CENTER;
g.setConstraints(rep, cs);
col.add(rep);
break;
default:
log.error("layout internally inconsistent: {}", layout);
}
}
/**
* Get a GUI representation of a particular variable for display.
*
* @param name Name used to look up the Variable object
* @param var XML Element which might contain a "format" attribute to be
* used in the {@link VariableValue#getNewRep} call from the
* Variable object; "tooltip" elements are also processed here.
* @return JComponent representing this variable
*/
public JComponent getRepresentation(String name, Element var) {
int i = _varModel.findVarIndex(name);
VariableValue variable = _varModel.getVariable(i);
JComponent rep = null;
String format = "default";
Attribute attr;
if ((attr = var.getAttribute("format")) != null && attr.getValue() != null) {
format = attr.getValue();
}
boolean viewOnly = (var.getAttribute("viewOnly") != null &&
var.getAttribute("viewOnly").getValue().equals("yes"));
if (i >= 0) {
rep = getRep(i, format);
rep.setMaximumSize(rep.getPreferredSize());
// set tooltip if specified here & not overridden by defn in Variable
String tip = LocaleSelector.getAttribute(var, "tooltip");
if (rep.getToolTipText() != null) {
tip = rep.getToolTipText();
}
rep.setToolTipText(modifyToolTipText(tip, variable));
if (viewOnly) {
rep.setEnabled(false);
}
}
return rep;
}
/**
* Takes default tool tip text, e.g. from the decoder element, and modifies
* it as needed.
* <p>
* Intended to handle e.g. adding CV numbers to variables.
*
* @param start existing tool tip text
* @param variable the CV
* @return new tool tip text
*/
String modifyToolTipText(String start, VariableValue variable) {
log.trace("modifyToolTipText: {}", variable.label());
// this is the place to invoke VariableValue methods to (conditionally)
// add information about CVs, etc in the ToolTip text
// Optionally add CV numbers based on Roster Preferences setting
start = CvUtil.addCvDescription(start, variable.getCvDescription(), variable.getMask());
// Indicate what the command station can do
// need to update this with e.g. the specific CV numbers
if (_cvModel.getProgrammer() != null
&& !_cvModel.getProgrammer().getCanRead()) {
start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotRead"));
}
if (_cvModel.getProgrammer() != null
&& !_cvModel.getProgrammer().getCanWrite()) {
start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipHardwareCannotWrite"));
}
// indicate other reasons for read/write constraints
if (variable.getReadOnly()) {
start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeReadOnly"));
}
if (variable.getWriteOnly()) {
start = StringUtil.concatTextHtmlAware(start, SymbolicProgBundle.getMessage("TipDefinedToBeWriteOnly"));
}
return start;
}
JComponent getRep(int i, String format) {
return (JComponent) (_varModel.getRep(i, format));
}
/**
* list of fnMapping objects to dispose
*/
ArrayList<FnMapPanel> fnMapList = new ArrayList<>();
ArrayList<FnMapPanelESU> fnMapListESU = new ArrayList<>();
/**
* list of JPanel objects to removeAll
*/
ArrayList<JPanel> panelList = new ArrayList<>();
public void dispose() {
log.debug("dispose");
// remove components
removeAll();
readChangesButton.removeItemListener(l1);
readAllButton.removeItemListener(l2);
writeChangesButton.removeItemListener(l3);
writeAllButton.removeItemListener(l4);
confirmChangesButton.removeItemListener(l5);
confirmAllButton.removeItemListener(l6);
l1 = l2 = l3 = l4 = l5 = l6 = null;
if (_programmingVar != null) {
_programmingVar.removePropertyChangeListener(this);
}
if (_programmingCV != null) {
_programmingCV.removePropertyChangeListener(this);
}
_programmingVar = null;
_programmingCV = null;
varList.clear();
varList = null;
cvList.clear();
cvList = null;
// dispose of any panels
for (JPanel jPanel : panelList) {
jPanel.removeAll();
}
panelList.clear();
panelList = null;
// dispose of any fnMaps
for (FnMapPanel fnMapPanel : fnMapList) {
fnMapPanel.dispose();
}
fnMapList.clear();
fnMapList = null;
// dispose of any fnMaps
for (FnMapPanelESU fnMapPanelESU : fnMapListESU) {
fnMapPanelESU.dispose();
}
fnMapListESU.clear();
fnMapListESU = null;
readChangesButton = null;
writeChangesButton = null;
// these are disposed elsewhere
_cvModel = null;
_varModel = null;
}
/**
* Check if varList and cvList, and thus the tab, is empty.
*
* @return true if empty
*/
public boolean isEmpty() {
return (varList.isEmpty() && cvList.isEmpty());
}
public boolean includeInPrint() {
return print;
}
public void includeInPrint(boolean inc) {
print = inc;
}
boolean print = false;
public void printPane(HardcopyWriter w) {
// if pane is empty, don't print anything
if (isEmpty()) {
return;
}
// Define column widths for name and value output.
// Make col 2 slightly larger than col 1 and reduce both to allow for
// extra spaces that will be added during concatenation
int col1Width = w.getCharactersPerLine() / 2 - 3 - 5;
int col2Width = w.getCharactersPerLine() / 2 - 3 + 5;
try {
//Create a string of spaces the width of the first column
StringBuilder spaces = new StringBuilder();
spaces.append(" ".repeat(Math.max(0, col1Width)));
// start with pane name in bold
String heading1 = SymbolicProgBundle.getMessage("PrintHeadingField");
String heading2 = SymbolicProgBundle.getMessage("PrintHeadingSetting");
String s;
int interval = spaces.length() - heading1.length();
w.setFontStyle(Font.BOLD);
// write the section name and dividing line
s = mName;
w.write(s, 0, s.length());
w.writeBorders();
//Draw horizontal dividing line for each Pane section
w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
w.getCharactersPerLine() + 1);
s = "\n";
w.write(s, 0, s.length());
// if this isn't the raw CV section, write the column headings
if (cvList.isEmpty()) {
w.setFontStyle(Font.BOLD + Font.ITALIC);
s = " " + heading1 + spaces.substring(0, interval) + " " + heading2;
w.write(s, 0, s.length());
w.writeBorders();
s = "\n";
w.write(s, 0, s.length());
}
w.setFontStyle(Font.PLAIN);
// Define a vector to store the names of variables that have been printed
// already. If they have been printed, they will be skipped.
// Using a vector here since we don't know how many variables will
// be printed and it allows expansion as necessary
ArrayList<String> printedVariables = new ArrayList<>(10);
// index over variables
for (int varNum : varList) {
VariableValue var = _varModel.getVariable(varNum);
String name = var.label();
if (name == null) {
name = var.item();
}
// Check if variable has been printed. If not store it and print
boolean alreadyPrinted = false;
for (String printedVariable : printedVariables) {
if (name.equals(printedVariable)) {
alreadyPrinted = true;
break;
}
}
// If already printed, skip it. If not, store it and print
if (alreadyPrinted) {
continue;
}
printedVariables.add(name);
String value = var.getTextValue();
String originalName = name;
String originalValue = value;
name = name + " (CV" + var.getCvNum() + ")"; // NO I18N
// define index values for name and value substrings
int nameLeftIndex = 0;
int nameRightIndex = name.length();
int valueLeftIndex = 0;
int valueRightIndex = value.length();
String trimmedName;
String trimmedValue;
// Check the name length to see if it is wider than the column.
// If so, split it and do the same checks for the Value
// Then concatenate the name and value (or the split versions thereof)
// before writing - if split, repeat until all pieces have been output
while ((valueLeftIndex < value.length()) || (nameLeftIndex < name.length())) {
// name split code
if (name.substring(nameLeftIndex).length() > col1Width) {
for (int j = 0; j < col1Width; j++) {
String delimiter = name.substring(nameLeftIndex + col1Width - j - 1, nameLeftIndex + col1Width - j);
if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
nameRightIndex = nameLeftIndex + col1Width - j;
break;
}
}
trimmedName = name.substring(nameLeftIndex, nameRightIndex);
nameLeftIndex = nameRightIndex;
int space = spaces.length() - trimmedName.length();
s = " " + trimmedName + spaces.substring(0, space);
} else {
trimmedName = name.substring(nameLeftIndex);
int space = spaces.length() - trimmedName.length();
s = " " + trimmedName + spaces.substring(0, space);
name = "";
nameLeftIndex = 0;
}
// value split code
if (value.substring(valueLeftIndex).length() > col2Width) {
for (int j = 0; j < col2Width; j++) {
String delimiter = value.substring(valueLeftIndex + col2Width - j - 1, valueLeftIndex + col2Width - j);
if (delimiter.equals(" ") || delimiter.equals(";") || delimiter.equals(",")) {
valueRightIndex = valueLeftIndex + col2Width - j;
break;
}
}
trimmedValue = value.substring(valueLeftIndex, valueRightIndex);
valueLeftIndex = valueRightIndex;
s = s + " " + trimmedValue;
} else {
trimmedValue = value.substring(valueLeftIndex);
s = s + " " + trimmedValue;
valueLeftIndex = 0;
value = "";
}
w.write(s, 0, s.length());
w.writeBorders();
s = "\n";
w.write(s, 0, s.length());
}
// Check for a Speed Table output and create a graphic display.
// Java 1.5 has a known bug, #6328248, that prevents printing of progress
// bars using old style printing classes. It results in blank bars on Windows,
// but hangs Macs. The version check is a workaround.
float v = Float.parseFloat(System.getProperty("java.version").substring(0, 3));
if (originalName.equals("Speed Table") && v < 1.5) {
// set the height of the speed table graph in lines
int speedFrameLineHeight = 11;
s = "\n";
// check that there is enough room on the page; if not,
// space down the rest of the page.
// don't use page break because we want the table borders to be written
// to the bottom of the page
int pageSize = w.getLinesPerPage();
int here = w.getCurrentLineNumber();
if (pageSize - here < speedFrameLineHeight) {
for (int j = 0; j < (pageSize - here); j++) {
w.writeBorders();
w.write(s, 0, s.length());
}
}
// Now that there is page space, create the window to hold the graphic speed table
JWindow speedWindow = new JWindow();
// Window size as wide as possible to allow for largest type size
speedWindow.setSize(512, 165);
speedWindow.getContentPane().setBackground(Color.white);
speedWindow.getContentPane().setLayout(null);
// in preparation for display, extract the speed table values into an array
StringTokenizer valueTokens = new StringTokenizer(originalValue, ",", false);
int[] speedVals = new int[28];
int k = 0;
while (valueTokens.hasMoreTokens()) {
speedVals[k] = Integer.parseInt(valueTokens.nextToken());
k++;
}
// Now create a set of vertical progress bar whose length is based
// on the speed table value (half height) and add them to the window
for (int j = 0; j < 28; j++) {
JProgressBar printerBar = new JProgressBar(JProgressBar.VERTICAL, 0, 127);
printerBar.setBounds(52 + j * 15, 19, 10, 127);
printerBar.setValue(speedVals[j] / 2);
printerBar.setBackground(Color.white);
printerBar.setForeground(Color.darkGray);
printerBar.setBorder(BorderFactory.createLineBorder(Color.black));
speedWindow.getContentPane().add(printerBar);
// create a set of value labels at the top containing the speed table values
JLabel barValLabel = new JLabel(Integer.toString(speedVals[j]), SwingConstants.CENTER);
barValLabel.setBounds(50 + j * 15, 4, 15, 15);
barValLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
speedWindow.getContentPane().add(barValLabel);
//Create a set of labels at the bottom with the CV numbers in them
JLabel barCvLabel = new JLabel(Integer.toString(67 + j), SwingConstants.CENTER);
barCvLabel.setBounds(50 + j * 15, 150, 15, 15);
barCvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
speedWindow.getContentPane().add(barCvLabel);
}
JLabel cvLabel = new JLabel(Bundle.getMessage("Value"));
cvLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
cvLabel.setBounds(25, 4, 26, 15);
speedWindow.getContentPane().add(cvLabel);
JLabel valueLabel = new JLabel("CV"); // I18N seems undesirable for support
valueLabel.setFont(new Font("Monospaced", Font.PLAIN, 7));
valueLabel.setBounds(37, 150, 13, 15);
speedWindow.getContentPane().add(valueLabel);
// pass the complete window to the printing class
w.write(speedWindow);
// Now need to write the borders on sides of table
for (int j = 0; j < speedFrameLineHeight; j++) {
w.writeBorders();
w.write(s, 0, s.length());
}
}
}
final int TABLE_COLS = 3;
// index over CVs
if (cvList.size() > 0) {
// Check how many Cvs there are to print
int cvCount = cvList.size();
w.setFontStyle(Font.BOLD); //set font to Bold
// print a simple heading with I18N
s = String.format("%1$21s", Bundle.getMessage("Value")) + String.format("%1$28s", Bundle.getMessage("Value")) +
String.format("%1$28s", Bundle.getMessage("Value"));
w.write(s, 0, s.length());
w.writeBorders();
s = "\n";
w.write(s, 0, s.length());
// NO I18N
s = " CV Dec Hex CV Dec Hex CV Dec Hex";
w.write(s, 0, s.length());
w.writeBorders();
s = "\n";
w.write(s, 0, s.length());
w.setFontStyle(0); //set font back to Normal
// }
/*create an array to hold CV/Value strings to allow reformatting and sorting
Same size as the table drawn above (TABLE_COLS columns*tableHeight; heading rows
not included). Use the count of how many CVs there are to determine the number
of table rows required. Add one more row if the divison into TABLE_COLS columns
isn't even.
*/
int tableHeight = cvCount / TABLE_COLS;
if (cvCount % TABLE_COLS > 0) {
tableHeight++;
}
String[] cvStrings = new String[TABLE_COLS * tableHeight];
//blank the array
Arrays.fill(cvStrings, "");
// get each CV and value
int i = 0;
for (int cvNum : cvList) {
CvValue cv = _cvModel.getCvByRow(cvNum);
int value = cv.getValue();
//convert and pad numbers as needed
String numString = String.format("%12s", cv.number());
StringBuilder valueString = new StringBuilder(Integer.toString(value));
String valueStringHex = Integer.toHexString(value).toUpperCase();
if (value < 16) {
valueStringHex = "0" + valueStringHex;
}
for (int j = 1; j < 3; j++) {
if (valueString.length() < 3) {
valueString.insert(0, " ");
}
}
//Create composite string of CV and its decimal and hex values
s = " " + numString + " " + valueString + " " + valueStringHex
+ " ";
//populate printing array - still treated as a single column
cvStrings[i] = s;
i++;
}
//sort the array in CV order (just the members with values)
String temp;
boolean swap;
do {
swap = false;
for (i = 0; i < _cvModel.getRowCount() - 1; i++) {
if (PrintCvAction.cvSortOrderVal(cvStrings[i + 1].substring(0, 15).trim()) < PrintCvAction.cvSortOrderVal(cvStrings[i].substring(0, 15).trim())) {
temp = cvStrings[i + 1];
cvStrings[i + 1] = cvStrings[i];
cvStrings[i] = temp;
swap = true;
}
}
} while (swap);
//Print the array in four columns
for (i = 0; i < tableHeight; i++) {
s = cvStrings[i] + " " + cvStrings[i + tableHeight] + " " + cvStrings[i
+ tableHeight * 2];
w.write(s, 0, s.length());
w.writeBorders();
s = "\n";
w.write(s, 0, s.length());
}
}
s = "\n";
w.writeBorders();
w.write(s, 0, s.length());
w.writeBorders();
w.write(s, 0, s.length());
// handle special cases
} catch (IOException e) {
log.warn("error during printing", e);
}
}
private JPanel addDccAddressPanel(Element e) {
JPanel l = new DccAddressPanel(_varModel);
panelList.add(l);
// make sure this will get read/written, even if real vars not on pane
int iVar;
// note we want Short Address first, as it might change others
iVar = _varModel.findVarIndex("Short Address");
if (iVar >= 0) {
varList.add(iVar);
} else {
log.debug("addDccAddressPanel did not find Short Address");
}
iVar = _varModel.findVarIndex("Address Format");
if (iVar >= 0) {
varList.add(iVar);
} else {
log.debug("addDccAddressPanel did not find Address Format");
}
iVar = _varModel.findVarIndex("Long Address");
if (iVar >= 0) {
varList.add(iVar);
} else {
log.debug("addDccAddressPanel did not find Long Address");
}
// included here because CV1 can modify it, even if it doesn't show on pane;
iVar = _varModel.findVarIndex("Consist Address");
if (iVar >= 0) {
varList.add(iVar);
} else {
log.debug("addDccAddressPanel did not find CV19 Consist Address");
}
return l;
}
private final static Logger log = LoggerFactory.getLogger(PaneProgPane.class);
}