001package jmri.jmrit.symbolicprog; 002 003import java.awt.Color; 004import java.awt.Component; 005import java.awt.event.ActionEvent; 006import java.awt.event.ActionListener; 007import java.util.ArrayDeque; 008import java.util.ArrayList; 009import java.util.Deque; 010import java.util.HashMap; 011import java.util.List; 012import javax.swing.ComboBoxModel; 013import javax.swing.JComboBox; 014import javax.swing.JLabel; 015import javax.swing.JScrollPane; 016import javax.swing.JTree; 017import javax.swing.tree.DefaultMutableTreeNode; 018import javax.swing.tree.DefaultTreeModel; 019import javax.swing.tree.DefaultTreeSelectionModel; 020import javax.swing.tree.TreePath; 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024/** 025 * Extends VariableValue to represent an enumerated variable. 026 * @see VariableValue 027 * 028 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2003, 2013, 2014, 2022 029 */ 030public class EnumVariableValue extends VariableValue implements ActionListener { 031 032 public EnumVariableValue(String name, String comment, String cvName, 033 boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, 034 String cvNum, String mask, int minVal, int maxVal, 035 HashMap<String, CvValue> v, JLabel status, String stdname) { 036 super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname); 037 _maxVal = maxVal; // count of possibles in the masked part, i.e. radix mask. Can be higher that the enums count 038 _minVal = minVal; 039 040 treeNodes.addLast(new DefaultMutableTreeNode("")); // root 041 simplifyMask(); 042 } 043 044 /** 045 * Create a null object. Normally only used for tests and to pre-load 046 * classes. 047 */ 048 public EnumVariableValue() { 049 } 050 051 @Override 052 public CvValue[] usesCVs() { 053 return new CvValue[] {_cvMap.get(getCvNum())}; 054 } 055 056 public void nItems(int n) { 057 _itemArray = new String[n]; 058 _pathArray = new TreePath[n]; 059 _valueArray = new int[n]; 060 _nstored = 0; 061 log.debug("enumeration arrays size={}", n); 062 } 063 064 /** 065 * Create a new item in the enumeration, with an associated value one more 066 * than the last item (or zero if this is the first one added) 067 * 068 * @param s Name of the enumeration item 069 */ 070 public void addItem(String s) { 071 if (_nstored == 0) { 072 addItem(s, 0); 073 } else { 074 addItem(s, _valueArray[_nstored - 1] + 1); 075 } 076 } 077 078 /** 079 * Create a new item in the enumeration, with a specified associated value. 080 * 081 * @param s Name of the enumeration item 082 * @param value item value. 083 */ 084 public void addItem(String s, int value) { 085 _valueArray[_nstored] = value; 086 TreeLeafNode node = new TreeLeafNode(s, _nstored); 087 treeNodes.getLast().add(node); 088 _pathArray[_nstored] = new TreePath(node.getPath()); 089 _itemArray[_nstored++] = s; 090 log.debug("_itemArray.length={},_nstored={},s='{}',value={}", _itemArray.length, _nstored, s, value); 091 } 092 093 public void startGroup(String name) { 094 DefaultMutableTreeNode next = new DefaultMutableTreeNode(name); 095 treeNodes.getLast().add(next); 096 treeNodes.addLast(next); 097 } 098 099 public void endGroup() { 100 treeNodes.removeLast(); 101 } 102 103 public void lastItem() { 104 _value = new JComboBox<>(java.util.Arrays.copyOf(_itemArray, _nstored)); 105 _value.getAccessibleContext().setAccessibleName(label()); 106 107 // finish initialization 108 _value.setActionCommand(""); 109 _defaultColor = _value.getBackground(); 110 _value.setBackground(ValueState.UNKNOWN.getColor()); 111 _value.setOpaque(true); 112 // connect to the JComboBox model and the CV so we'll see changes. 113 _value.addActionListener(this); 114 CvValue cv = _cvMap.get(getCvNum()); 115 if (cv == null) { 116 log.error("no CV defined in enumVal {}, skipping setState", getCvName()); 117 return; 118 } 119 cv.addPropertyChangeListener(this); 120 cv.setState(ValueState.FROMFILE); 121 } 122 123 @Override 124 public void setToolTipText(String t) { 125 super.setToolTipText(t); // do default stuff 126 _value.setToolTipText(t); // set our value 127 } 128 129 // stored value 130 JComboBox<String> _value = null; 131 132 // place to keep the items & associated numbers 133 private String[] _itemArray = null; 134 private TreePath[] _pathArray = null; 135 private int[] _valueArray = null; 136 private int _nstored; 137 138 Deque<DefaultMutableTreeNode> treeNodes = new ArrayDeque<>(); 139 140 int _maxVal; 141 int _minVal; 142 Color _defaultColor; 143 144 @Override 145 public void setAvailable(boolean a) { 146 _value.setVisible(a); 147 for (ComboCheckBox c : comboCBs) { 148 c.setVisible(a); 149 } 150 for (VarComboBox c : comboVars) { 151 c.setVisible(a); 152 } 153 for (ComboRadioButtons c : comboRBs) { 154 c.setVisible(a); 155 } 156 super.setAvailable(a); 157 } 158 159 @Override 160 public Object rangeVal() { 161 return "enum: " + _minVal + " - " + _maxVal; 162 } 163 164 @Override 165 public void actionPerformed(ActionEvent e) { 166 // see if this is from _value itself, or from an alternate rep. 167 // if from an alternate rep, it will contain the value to select 168 if (log.isDebugEnabled()) { 169 log.debug("{} start action event: {}", label(), e); 170 } 171 if (!(e.getActionCommand().equals(""))) { 172 // is from alternate rep 173 _value.setSelectedItem(e.getActionCommand()); 174 if (log.isDebugEnabled()) { 175 log.debug("{} action event was from alternate rep", label()); 176 } 177 // match and select in tree 178 if (_nstored > 0) { 179 for (int i = 0; i < _nstored; i++) { 180 if (e.getActionCommand().equals(_itemArray[i])) { 181 // now select in the tree 182 TreePath path = _pathArray[i]; 183 for (JTree tree : trees) { 184 tree.setSelectionPath(path); 185 // ensure selection is in visible portion of JScrollPane 186 tree.scrollPathToVisible(path); 187 } 188 break; // first one is enough 189 } 190 } 191 } 192 } 193 194 int oldVal = getIntValue(); 195 196 // called for new values - set the CV as needed 197 CvValue cv = _cvMap.get(getCvNum()); 198 if (cv == null) { 199 log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName())); 200 return; 201 } 202 int oldCv = cv.getValue(); 203 int newVal = getIntValue(); 204 int newCv = setValueInCV(oldCv, newVal, getMask(), _maxVal - 1); 205 if (newCv != oldCv) { 206 cv.setValue(newCv); // to prevent CV going EDITED during loading of decoder file 207 208 // notify (this used to be before setting the values) 209 log.debug("{} about to firePropertyChange", label()); 210 prop.firePropertyChange("Value", null, oldVal); 211 log.debug("{} returned to from firePropertyChange", label()); 212 } 213 log.debug("{} end action event saw oldCv={} newVal={} newCv={}", label(), oldCv, newVal, newCv); 214 } 215 216 // to complete this class, fill in the routines to handle "Value" parameter 217 // and to read/write/hear parameter changes. 218 @Override 219 public String getValueString() { 220 return Integer.toString(getIntValue()); 221 } 222 223 @Override 224 public void setIntValue(int i) { 225 // needs to fire Value property as well, as per suggestion by Svata Dedic. 226 setValue(i); 227 } 228 229 @Override 230 public String getTextValue() { 231 if (_value.getSelectedItem() != null) { 232 return _value.getSelectedItem().toString(); 233 } else { 234 return ""; 235 } 236 } 237 238 @Override 239 public Object getValueObject() { 240 return _value.getSelectedIndex(); 241 } 242 243 /** 244 * Set to a specific value. 245 * <p> 246 * This searches for the displayed value, and sets the enum to that 247 * particular one. It used to work off an index, but now it looks for the 248 * value. 249 * <p> 250 * If the value is larger than any defined, a new one is created. 251 * @param value What to set to. 252 */ 253 protected void selectValue(int value) { 254 if (_nstored > 0) { 255 for (int i = 0; i < _nstored; i++) { 256 if (_valueArray[i] == value) { 257 // found it, select it 258 _value.setSelectedIndex(i); 259 260 // now select in the tree 261 TreePath path = _pathArray[i]; 262 for (JTree tree : trees) { 263 tree.setSelectionPath(path); 264 // ensure selection is in visible portion of JScrollPane 265 tree.scrollPathToVisible(path); 266 } 267 return; 268 } 269 } 270 } 271 272 // We can be commanded to a number that hasn't been defined. 273 // But that's OK for certain applications. Instead, we add them as needed 274 log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label()); 275 // lengthen arrays 276 _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1); 277 278 _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1); 279 280 _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1); 281 282 addItem("Reserved value " + value, value); 283 284 // update the JComboBox 285 _value.addItem(_itemArray[_nstored - 1]); 286 _value.setSelectedItem(_itemArray[_nstored - 1]); 287 288 // tell trees to redisplay & select 289 for (JTree tree : trees) { 290 ((DefaultTreeModel) tree.getModel()).reload(); 291 tree.setSelectionPath(_pathArray[_nstored - 1]); 292 // ensure selection is in visible portion of JScrollPane 293 tree.scrollPathToVisible(_pathArray[_nstored - 1]); 294 } 295 } 296 297 @Override 298 public int getIntValue() { 299 if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) { 300 log.error("trying to get value {} too large for array length {} in var {}", 301 _value.getSelectedIndex(), _valueArray.length, label()); 302 } 303 if (log.isDebugEnabled()) { 304 log.debug("SelectedIndex={}, Value={}", 305 _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]); 306 } 307 return _valueArray[_value.getSelectedIndex()]; 308 } 309 310 @Override 311 public Component getCommonRep() { 312 return _value; 313 } 314 315 public void setValue(int value) { 316 int oldVal = getIntValue(); 317 log.debug("setValue in EnumVariableValue to {}", value); 318 selectValue(value); 319 320 if (oldVal != value || getState() == ValueState.UNKNOWN) { 321 prop.firePropertyChange("Value", null, value); 322 } 323 } 324 325 @Override 326 public Component getNewRep(String format) { 327 // sort on format type 328 switch (format) { 329 case "checkbox": { 330 // this only makes sense if there are exactly two options 331 ComboCheckBox b = new ComboCheckBox(_value, this); 332 b.getAccessibleContext().setAccessibleName(label()); 333 comboCBs.add(b); 334 if (getReadOnly() || getInfoOnly()) { 335 b.setEnabled(false); 336 } 337 updateRepresentation(b); 338 return b; 339 } 340 case "radiobuttons": { 341 ComboRadioButtons b = new ComboRadioButtons(_value, this); 342 b.getAccessibleContext().setAccessibleName(label()); 343 comboRBs.add(b); 344 if (getReadOnly() || getInfoOnly()) { 345 b.setEnabled(false); 346 } 347 updateRepresentation(b); 348 return b; 349 } 350 case "onradiobutton": { 351 ComboRadioButtons b = new ComboOnRadioButton(_value, this); 352 b.getAccessibleContext().setAccessibleName(label()); 353 comboRBs.add(b); 354 if (getReadOnly() || getInfoOnly()) { 355 b.setEnabled(false); 356 } 357 updateRepresentation(b); 358 return b; 359 } 360 case "offradiobutton": { 361 ComboRadioButtons b = new ComboOffRadioButton(_value, this); 362 b.getAccessibleContext().setAccessibleName(label()); 363 comboRBs.add(b); 364 if (getReadOnly() || getInfoOnly()) { 365 b.setEnabled(false); 366 } 367 updateRepresentation(b); 368 return b; 369 } 370 case "tree": 371 DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst()); 372 JTree dTree = new JTree(dModel); 373 trees.add(dTree); 374 JScrollPane dScroll = new JScrollPane(dTree); 375 dTree.setRootVisible(false); 376 dTree.setShowsRootHandles(true); 377 dTree.setScrollsOnExpand(true); 378 dTree.setExpandsSelectedPaths(true); 379 dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION); 380 // arrange for only leaf nodes can be selected 381 dTree.addTreeSelectionListener(e -> { 382 TreePath[] paths = e.getPaths(); 383 for (TreePath path : paths) { 384 DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent(); 385 if (o.getChildCount() > 0) { 386 ((JTree) e.getSource()).removeSelectionPath(path); 387 } 388 } 389 // now record selection 390 if (paths.length >= 1) { 391 if (paths[0].getLastPathComponent() instanceof TreeLeafNode) { 392 // update value of Variable 393 setValue(_valueArray[((TreeLeafNode) paths[0].getLastPathComponent()).index]); 394 } 395 } 396 }); 397 // select initial value 398 TreePath path = _pathArray[_value.getSelectedIndex()]; 399 dTree.setSelectionPath(path); 400 // ensure selection is in visible portion of JScrollPane 401 dTree.scrollPathToVisible(path); 402 403 if (getReadOnly() || getInfoOnly()) { 404 log.error("read only variables cannot use tree format: {}", item()); 405 } 406 updateRepresentation(dScroll); 407 dScroll.getAccessibleContext().setAccessibleName(label()); 408 return dScroll; 409 default: { 410 // return a new JComboBox representing the same model 411 VarComboBox b = new VarComboBox(_value.getModel(), this); 412 b.getAccessibleContext().setAccessibleName(label()); 413 comboVars.add(b); 414 if (getReadOnly() || getInfoOnly()) { 415 b.setEnabled(false); 416 } 417 updateRepresentation(b); 418 return b; 419 } 420 } 421 } 422 423 private final List<ComboCheckBox> comboCBs = new ArrayList<>(); 424 private final List<VarComboBox> comboVars = new ArrayList<>(); 425 private final List<ComboRadioButtons> comboRBs = new ArrayList<>(); 426 private final List<JTree> trees = new ArrayList<>(); 427 428 // implement an abstract member to set colors 429 @Override 430 void setColor(Color c) { 431 if (c != null) { 432 _value.setBackground(c); 433 } else { 434 _value.setBackground(_defaultColor); 435 } 436 _value.setOpaque(true); 437 } 438 439 /** 440 * Notify the connected CVs of a state change from above 441 */ 442 @Override 443 public void setCvState(ValueState state) { 444 _cvMap.get(getCvNum()).setState(state); 445 } 446 447 @Override 448 public boolean isChanged() { 449 CvValue cv = _cvMap.get(getCvNum()); 450 return considerChanged(cv); 451 } 452 453 @Override 454 public void readChanges() { 455 if (isToRead() && !isChanged()) { 456 log.debug("!!!!!!! unacceptable combination in readChanges: {}", label()); 457 } 458 if (isChanged() || isToRead()) { 459 readAll(); 460 } 461 } 462 463 @Override 464 public void writeChanges() { 465 if (isToWrite() && !isChanged()) { 466 log.debug("!!!!!! unacceptable combination in writeChanges: {}", label()); 467 } 468 if (isChanged() || isToWrite()) { 469 writeAll(); 470 } 471 } 472 473 @Override 474 public void readAll() { 475 setToRead(false); 476 setBusy(true); // will be reset when value changes 477 _cvMap.get(getCvNum()).read(_status); 478 } 479 480 @Override 481 public void writeAll() { 482 setToWrite(false); 483 if (getReadOnly() || getInfoOnly()) { 484 log.error("unexpected write operation when readOnly is set"); 485 } 486 setBusy(true); // will be reset when value changes 487 _cvMap.get(getCvNum()).write(_status); 488 } 489 490 // handle incoming parameter notification 491 @Override 492 public void propertyChange(java.beans.PropertyChangeEvent e) { 493 // notification from CV; check for Value being changed 494 switch (e.getPropertyName()) { 495 case "Busy": 496 if (e.getNewValue().equals(Boolean.FALSE)) { 497 setToRead(false); 498 setToWrite(false); // some programming operation just finished 499 setBusy(false); 500 } 501 break; 502 case "State": { 503 CvValue cv = _cvMap.get(getCvNum()); 504 if (cv.getState() == ValueState.STORED) { 505 setToWrite(false); 506 } 507 if (cv.getState() == ValueState.READ) { 508 setToRead(false); 509 } 510 setState(cv.getState()); 511 for (JTree tree : trees) { 512 tree.setBackground(_value.getBackground()); 513 //tree.setOpaque(true); 514 } 515 break; 516 } 517 case "Value": { 518 // update value of Variable 519 CvValue cv = _cvMap.get(getCvNum()); 520 int newVal = getValueInCV(cv.getValue(), getMask(), _maxVal - 1); // _maxVal value is count of possibles, i.e. radix 521 setValue(newVal); // check for duplicate done inside setValue 522 break; 523 } 524 default: 525 break; 526 } 527 } 528 529 /* Internal class extends a JComboBox so that its color is consistent with 530 * an underlying variable; we return one of these in getNewRep. 531 * <p> 532 * Unlike similar cases elsewhere, this doesn't have to listen to 533 * value changes. Those are handled automagically since we're sharing the same 534 * model between this object and the real JComboBox value. 535 * 536 * @author Bob Jacobsen Copyright (C) 2001 537 */ 538 public static class VarComboBox extends JComboBox<String> { 539 540 VarComboBox(ComboBoxModel<String> m, EnumVariableValue var) { 541 super(m); 542 _var = var; 543 _l = e -> { 544 if (log.isDebugEnabled()) { 545 log.debug("VarComboBox saw property change: {}", e); 546 } 547 originalPropertyChanged(e); 548 }; 549 // get the original color right 550 setBackground(_var._value.getBackground()); 551 setOpaque(true); 552 // listen for changes to original state 553 _var.addPropertyChangeListener(_l); 554 } 555 556 EnumVariableValue _var; 557 transient java.beans.PropertyChangeListener _l; 558 559 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 560 // update this color from original state 561 if (e.getPropertyName().equals("State")) { 562 setBackground(_var._value.getBackground()); 563 setOpaque(true); 564 } 565 } 566 567 public void dispose() { 568 if (_var != null && _l != null) { 569 _var.removePropertyChangeListener(_l); 570 } 571 _l = null; 572 _var = null; 573 } 574 } 575 576 // clean up connections when done 577 @Override 578 public void dispose() { 579 log.debug("dispose"); 580 581 // remove connection to CV 582 if (_cvMap.get(getCvNum()) == null) { 583 log.error("no CV defined for variable {}, no listeners to remove", getCvNum()); 584 } else { 585 _cvMap.get(getCvNum()).removePropertyChangeListener(this); 586 } 587 // remove connection to graphical representation 588 disposeReps(); 589 } 590 591 void disposeReps() { 592 if (_value != null) { 593 _value.removeActionListener(this); 594 } 595 for (ComboCheckBox comboCB : comboCBs) { 596 comboCB.dispose(); 597 } 598 for (VarComboBox comboVar : comboVars) { 599 comboVar.dispose(); 600 } 601 for (ComboRadioButtons comboRB : comboRBs) { 602 comboRB.dispose(); 603 } 604 } 605 606 static class TreeLeafNode extends DefaultMutableTreeNode { 607 608 TreeLeafNode(String name, int index) { 609 super(name); 610 this.index = index; 611 } 612 613 int index; 614 } 615 616 // initialize logging 617 private final static Logger log = LoggerFactory.getLogger(EnumVariableValue.class); 618 619}