001package jmri.jmrit.symbolicprog; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.Color; 006import java.awt.Component; 007import java.awt.event.ActionEvent; 008import java.awt.event.ActionListener; 009import java.awt.event.FocusEvent; 010import java.awt.event.FocusListener; 011import java.util.*; 012 013import javax.swing.tree.DefaultMutableTreeNode; 014import javax.swing.tree.TreePath; 015 016import javax.swing.ComboBoxModel; 017import javax.swing.JComboBox; 018import javax.swing.JLabel; 019import javax.swing.JScrollPane; 020import javax.swing.JTree; 021import javax.swing.event.TreeSelectionEvent; 022import javax.swing.event.TreeSelectionListener; 023import javax.swing.tree.DefaultTreeModel; 024import javax.swing.tree.DefaultTreeSelectionModel; 025 026import jmri.util.CvUtil; 027 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031/** 032 * Extends VariableValue to represent a variable split across multiple CVs with 033 * values from a pre-selected range each of which is associated with a text name 034 * (aka, a drop down) 035 * <br> 036 * The {@code mask} attribute represents the part of the value that's present in 037 * each CV; higher-order bits are loaded to subsequent CVs.<br> 038 * It is possible to assign a specific mask for each CV by providing a space 039 * separated list of masks, starting with the lowest, and matching the order of 040 * CVs 041 * <br><br> 042 * The original use was for addresses of stationary (accessory) decoders. 043 * <br> 044 * The original version only allowed two CVs, with the second CV specified by 045 * the attributes {@code highCV} and {@code upperMask}. 046 * <br><br> 047 * The preferred technique is now to specify all CVs in the {@code CV} attribute 048 * alone, as documented at {@link CvUtil#expandCvList expandCvList(String)}. 049 * <br><br> 050 * Optional attributes {@code factor} and {@code offset} are applied when going 051 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa: 052 * <pre> 053 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor} 054 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset} 055 * </pre> 056 * 057 * @author Bob Jacobsen Copyright (C) 2002, 2003, 2004, 2013 058 * @author Dave Heap Copyright (C) 2016, 2019 059 * @author Egbert Broerse Copyright (C) 2020 060 * @author Jordan McBride Copyright (C) 2021 061 */ 062public class SplitEnumVariableValue extends VariableValue 063 implements ActionListener, FocusListener { 064 065 private static final int RETRY_COUNT = 2; 066 067 int atest = 1; 068 private final List<JTree> trees = new ArrayList<>(); 069 070 private final List<ComboCheckBox> comboCBs = new ArrayList<>(); 071 private final List<SplitEnumVariableValue.VarComboBox> comboVars = new ArrayList<>(); 072 private final List<ComboRadioButtons> comboRBs = new ArrayList<>(); 073 074 075 public SplitEnumVariableValue(String name, String comment, String cvName, 076 boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, 077 String cvNum, String mask, int minVal, int maxVal, 078 HashMap<String, CvValue> v, JLabel status, String stdname, 079 String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) { 080 super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname); 081 _minVal = 0; 082 _maxVal = ~0; 083 stepOneActions(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname, pSecondCV, pFactor, pOffset, uppermask, extra1, extra2, extra3, extra4); 084 _name = name; 085 _mask = mask; // will be converted to MaskArray to apply separate mask for each CV 086 if (mask != null && mask.contains(" ")) { 087 _maskArray = mask.split(" "); // type accepts multiple masks for SplitVariableValue 088 } else { 089 _maskArray = new String[1]; 090 _maskArray[0] = mask; 091 } 092 _cvNum = cvNum; 093 mFactor = pFactor; 094 mOffset = pOffset; 095 // legacy format variables 096 mSecondCV = pSecondCV; 097 _uppermask = uppermask; 098 099 100 log.debug("Variable={};comment={};cvName={};cvNum={};stdname={}", _name, comment, cvName, _cvNum, stdname); 101 102 // upper bit offset includes lower bit offset, and MSB bits missing from upper part 103 log.debug("Variable={}; upper mask {} had offsetVal={} so upperbitoffset={}", _name, _uppermask, offsetVal(_uppermask), offsetVal(_uppermask)); 104 105 // set up array of used CVs 106 cvList = new ArrayList<>(); 107 108 List<String> nameList = CvUtil.expandCvList(_cvNum); // see if cvName needs expanding 109 if (nameList.isEmpty()) { 110 // primary CV 111 String tMask; 112 if (_maskArray != null && _maskArray.length == 1) { 113 log.debug("PrimaryCV mask={}", _maskArray[0]); 114 tMask = _maskArray[0]; 115 } else { 116 tMask = _mask; // mask supplied could be an empty string 117 } 118 cvList.add(new CvItem(_cvNum, tMask)); 119 120 if (pSecondCV != null && !pSecondCV.equals("")) { 121 cvList.add(new CvItem(pSecondCV, _uppermask)); 122 } 123 } else { 124 for (int i = 0; i < nameList.size(); i++) { 125 cvList.add(new CvItem(nameList.get(i), _maskArray[Math.min(i, _maskArray.length - 1)])); 126 // use last mask for all following CVs if fewer masks than the number of CVs listed were provided 127 log.debug("Added mask #{}: {}", i, _maskArray[Math.min(i, _maskArray.length - 1)]); 128 } 129 } 130 131 cvCount = cvList.size(); 132 133 for (int i = 0; i < cvCount; i++) { 134 cvList.get(i).startOffset = currentOffset; 135 String t = cvList.get(i).cvMask; 136 if (t.contains("V")) { 137 currentOffset = currentOffset + t.lastIndexOf("V") - t.indexOf("V") + 1; 138 } else { 139 log.error("Variable={};cvName={};cvMask={} is an invalid bitmask", _name, cvList.get(i).cvName, cvList.get(i).cvMask); 140 } 141 log.debug("Variable={};cvName={};cvMask={};startOffset={};currentOffset={}", _name, cvList.get(i).cvName, cvList.get(i).cvMask, cvList.get(i).startOffset, currentOffset); 142 143 // connect CV for notification 144 CvValue cv = _cvMap.get(cvList.get(i).cvName); 145 cvList.get(i).thisCV = cv; 146 } 147 148 stepTwoActions(); 149 150 151 // have to do when list is complete 152 for (int i = 0; i < cvCount; i++) { 153 cvList.get(i).thisCV.addPropertyChangeListener(this); 154 cvList.get(i).thisCV.setState(ValueState.FROMFILE); 155 } 156 treeNodes.addLast(new DefaultMutableTreeNode("")); 157 } 158 159 /** 160 * Subclasses can override this to pick up constructor-specific attributes 161 * and perform other actions before cvList has been built. 162 * 163 * @param name name. 164 * @param comment comment. 165 * @param cvName cv name. 166 * @param readOnly true for read only, else false. 167 * @param infoOnly true for info only, else false. 168 * @param writeOnly true for write only, else false. 169 * @param opsOnly true for ops only, else false. 170 * @param cvNum cv number. 171 * @param mask cv mask. 172 * @param minVal minimum value. 173 * @param maxVal maximum value. 174 * @param v hashmap of string and cv value. 175 * @param status status. 176 * @param stdname std name. 177 * @param pSecondCV second cv (no longer preferred, specify in cv) 178 * @param pFactor factor. 179 * @param pOffset offset. 180 * @param uppermask upper mask (no longer preferred, specify in mask) 181 * @param extra1 extra 1. 182 * @param extra2 extra 2. 183 * @param extra3 extra 3. 184 * @param extra4 extra 4. 185 */ 186 public void stepOneActions(String name, String comment, String cvName, 187 boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, 188 String cvNum, String mask, int minVal, int maxVal, 189 HashMap<String, CvValue> v, JLabel status, String stdname, 190 String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) { 191 if (extra3 != null) { 192 _minVal = getValueFromText(extra3); 193 } 194 if (extra4 != null) { 195 _maxVal = getValueFromText(extra4); 196 } 197 } 198 199 public void nItems(int n) { 200 _itemArray = new String[n]; 201 _pathArray = new TreePath[n]; 202 _valueArray = new int[n]; 203 _nstored = 0; 204 log.debug("enumeration arrays size={}", n); 205 } 206 207 /** 208 * Create a new item in the enumeration, with an associated value one more 209 * than the last item (or zero if this is the first one added) 210 * 211 * @param s Name of the enumeration item 212 */ 213 public void addItem(String s) { 214 if (_nstored == 0) { 215 addItem(s, 0); 216 } else { 217 addItem(s, _valueArray[_nstored - 1] + 1); 218 } 219 } 220 221 public void addItem(String s, int value) { 222 _valueArray[_nstored] = value; 223 SplitEnumVariableValue.TreeLeafNode node = new SplitEnumVariableValue.TreeLeafNode(s, _nstored); 224 treeNodes.getLast().add(node); 225 _pathArray[_nstored] = new TreePath(node.getPath()); 226 _itemArray[_nstored++] = s; 227 log.debug("_itemArray.length={},_nstored={},s='{}',value={}", _itemArray.length, _nstored, s, value); 228 } 229 230 public void startGroup(String name) { 231 DefaultMutableTreeNode next = new DefaultMutableTreeNode(name); 232 treeNodes.getLast().add(next); 233 treeNodes.addLast(next); 234 } 235 236 public void endGroup() { 237 treeNodes.removeLast(); 238 } 239 240 public void lastItem() { 241 _value = new JComboBox<>(java.util.Arrays.copyOf(_itemArray, _nstored)); 242 _value.getAccessibleContext().setAccessibleName(label()); 243 244 // finish initialization 245 _value.setActionCommand(""); 246 _defaultColor = _value.getBackground(); 247 _value.setBackground(ValueState.UNKNOWN.getColor()); 248 _value.setOpaque(true); 249 // connect to the JComboBox model and the CV so we'll see changes. 250 _value.addActionListener(this); 251 CvValue cv1 = cvList.get(0).thisCV; 252 CvValue cv2 = cvList.get(1).thisCV; 253 if (cv1 == null || cv2 == null) { 254 log.error("no CV defined in enumVal {}, skipping setState", getCvName()); 255 return; 256 } 257 cv1.addPropertyChangeListener(this); 258 cv1.setState(ValueState.FROMFILE); 259 cv2.addPropertyChangeListener(this); 260 cv2.setState(ValueState.FROMFILE); 261 } 262 263 264 265 @Override 266 public void setToolTipText(String t) { 267 super.setToolTipText(t); // do default stuff 268 _value.setToolTipText(t); // set our value 269 } 270 // stored value 271 JComboBox<String> _value = null; 272 273 // place to keep the items & associated numbers 274 private String[] _itemArray = null; 275 private TreePath[] _pathArray = null; 276 private int[] _valueArray = null; 277 private int _nstored; 278 279 Deque<DefaultMutableTreeNode> treeNodes = new ArrayDeque<>(); 280 281 /** 282 * Subclasses can override this to invoke further actions after cvList has 283 * been built. 284 */ 285 public void stepTwoActions() { 286 if (currentOffset > bitCount) { 287 String eol = System.getProperty("line.separator"); 288 throw new Error( 289 "Decoder File parsing error:" 290 + eol + "The Decoder Definition File specified \"" + _cvNum 291 + "\" for variable \"" + _name + "\". This expands to:" 292 + eol + "\"" + getCvDescription() + "\"" 293 + eol + "This requires " + currentOffset + " bits, which exceeds the " + bitCount 294 + " bit capacity of the long integer used to store the variable." 295 + eol + "The Decoder Definition File needs correction."); 296 } 297 _columns = cvCount * 2; //update column width now we have a better idea 298 } 299 300 @Override 301 public void setAvailable(boolean a) { 302 _value.setVisible(a); 303 for (ComboCheckBox c : comboCBs) { 304 c.setVisible(a); 305 } 306 for (SplitEnumVariableValue.VarComboBox c : comboVars) { 307 c.setVisible(a); 308 } 309 for (ComboRadioButtons c : comboRBs) { 310 c.setVisible(a); 311 } 312 super.setAvailable(a); 313 } 314 315 /** 316 * Simple request getter for the CVs composing this variable 317 * <br> 318 * @return Array of CvValue for all of associated CVs 319 */ 320 @Override 321 public CvValue[] usesCVs() { 322 CvValue[] theseCvs = new CvValue[cvCount]; 323 for (int i = 0; i < cvCount; i++) { 324 theseCvs[i] = cvList.get(i).thisCV; 325 } 326 return theseCvs; 327 } 328 329 /** 330 * Multiple masks can be defined for the CVs accessed by this variable. 331 * <br> 332 * Actual individual masks are returned in 333 * {@link #getCvDescription getCvDescription()}. 334 * 335 * @return The legacy two-CV mask if {@code highCV} is specified. 336 * <br> 337 * The {@code mask} if {@code highCV} is not specified. 338 */ 339 @Override 340 public String getMask() { 341 if (mSecondCV != null && !mSecondCV.equals("")) { 342 return _uppermask + _mask; 343 } else { 344 return _mask; // a list of 1-n masks, separated by spaces 345 } 346 } 347 348 /** 349 * Access a specific mask, used in tests 350 * 351 * @param i index of CV in variable 352 * @return a single mask as string in the form XXXXVVVV, or empty string if 353 * index out of bounds 354 */ 355 protected String getMask(int i) { 356 if (i < cvCount) { 357 return cvList.get(i).cvMask; 358 } 359 return ""; 360 } 361 362 /** 363 * Provide a user-readable description of the CVs accessed by this variable. 364 * <br> 365 * Actual individual masks are added to CVs if more are present. 366 * 367 * @return A user-friendly CV(s) and bitmask(s) description 368 */ 369 @Override 370 public String getCvDescription() { 371 StringBuilder buf = new StringBuilder(); 372 for (int i = 0; i < cvCount; i++) { 373 if (buf.length() > 0) { 374 buf.append(" & "); 375 } 376 buf.append("CV"); 377 buf.append(cvList.get(i).cvName); 378 String temp = CvUtil.getMaskDescription(cvList.get(i).cvMask); 379 if (temp.length() > 0) { 380 buf.append(" "); 381 buf.append(temp); 382 } 383 } 384 buf.append("."); // mark that mask descriptions are already inserted for CvUtil.addCvDescription 385 return buf.toString(); 386 } 387 388 String mSecondCV; 389 String _uppermask; 390 int mFactor; 391 int mOffset; 392 String _name; 393 String _mask; // full string as provided, use _maskArray to access one of multiple masks 394 String[] _maskArray = new String[0]; 395 String _cvNum; 396 397 List<CvItem> cvList; 398 399 int cvCount = 0; 400 int currentOffset = 0; 401 402 /** 403 * Get the first CV from the set used to define this variable 404 * <br> 405 * @return The legacy two-CV mask if {@code highCV} is specified. 406 */ 407 @Override 408 public String getCvNum() { 409 String retString = ""; 410 if (cvCount > 0) { 411 retString = cvList.get(0).cvName; 412 } 413 return retString; 414 } 415 416 long _minVal; 417 long _maxVal; 418 419 @Override 420 public Object rangeVal() { 421 return "Split value"; 422 } 423 424 String oldContents = "0"; 425 426 long getValueFromText(String s) { 427 return (Long.parseUnsignedLong(s)); 428 } 429 430 String getTextFromValue(long v) { 431 return (Long.toUnsignedString(v)); 432 } 433 434 /** 435 * Contains numeric-value specific code. 436 * <br><br> 437 * Calculates new value for _enumField and invokes 438 * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the 439 * change 440 * 441 * @param intVals array of new CV values 442 */ 443 void updateVariableValue(int[] intVals) { 444 if (intVals.length > 0){ 445 long newVal = 0; 446 for (int i = 0; i < intVals.length; i++) { 447 newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset); 448 log.debug("Variable={}; i={}; intVals={}; startOffset={}; newVal={}", 449 _name, i, intVals[i], cvList.get(i).startOffset, getTextFromValue(newVal)); 450 } 451 log.debug("Variable={}; set value to {}", _name, newVal); 452 setLongValue(newVal); // check for duplicate is done inside setLongValue 453 log.debug("Variable={}; in property change after setValue call", _name); 454 } 455 } 456 457 /** 458 * Saves selected item from _value (enumField) to oldContents. 459 */ 460 void enterField() { 461 oldContents = String.valueOf(_value.getSelectedItem()); 462 log.debug("enterField sets oldContents to {}", oldContents); 463 } 464 465 /** 466 * Contains numeric-value specific code. 467 * <br> 468 * firePropertyChange for "Value" with new and old contents of _enumField 469 */ 470 void exitField(){ 471 // there may be a lost focus event left in the queue when disposed so protect 472 log.trace("exitField starts"); 473 if (_value != null && !oldContents.equals(_value.getSelectedItem())) { 474 long newFieldVal = 0; 475 try { 476 newFieldVal = Long.parseLong((String) Objects.requireNonNull(_value.getSelectedItem())); 477 } catch (NumberFormatException e) { 478 //_value.setText(oldContents); 479 } 480 log.debug("_minVal={};_maxVal={};newFieldVal={}", 481 Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal)); 482 if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) { 483 484 } else { 485 long newVal = (newFieldVal - mOffset) / mFactor; 486 long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor; 487 prop.firePropertyChange("Value", oldVal, newVal); 488 } 489 } 490 log.trace("exitField ends"); 491 } 492 493 boolean _fieldShrink = false; 494 495 void updatedDropDown() { 496 log.debug("Variable='{}'; enter updatedDropDown in {} with DropDownValue='{}'", _name, (this.getClass().getSimpleName()), _value.getSelectedIndex()); 497 // called for new values in text field - set the CVs as needed 498 499 int[] retVals = getCvValsFromSingleInt(getIntValue()); 500 501 // combine with existing values via mask 502 for (int j = 0; j < cvCount; j++) { 503 int i = j; 504 log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask)); 505 int cvMask = maskValAsInt(cvList.get(i).cvMask); 506 CvValue thisCV = cvList.get(i).thisCV; 507 int oldCvVal = thisCV.getValue(); 508 int newCvVal = (oldCvVal & ~cvMask) 509 | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask); 510 log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal); 511 512 // cv updates here trigger updated property changes, which means 513 // we're going to get notified sooner or later. 514 if (newCvVal != oldCvVal) { 515 thisCV.setValue(newCvVal); 516 } 517 } 518 log.debug("Variable={}; exit updatedDropDown", _name); 519 } 520 521 int[] getCvValsFromSingleInt(long newEntry) { 522 // calculate resulting number 523 long newVal = (newEntry - mOffset) / mFactor; 524 log.debug("getCvValsFromSingleInt Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor); 525 526 int[] retVals = new int[cvCount]; 527 528 // extract individual values via masks 529 for (int i = 0; i < cvCount; i++) { 530 log.trace(" Starting with newVal={} startOffset={} mask={} offsetVal={}", 531 newVal, cvList.get(i).startOffset, maskValAsInt(cvList.get(i).cvMask), offsetVal(cvList.get(i).cvMask)); 532 retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset)) 533 & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask))); 534 log.trace(" Calculated {} entry is {}", i, retVals[i]); 535 } 536 return retVals; 537 } 538 539 /** 540 * ActionListener implementation. Called by new selection in the JComboBox representation. 541 * <p> 542 * Invokes {@link #exitField exitField()} 543 * 544 * @param e the action event 545 */ 546 @Override 547 public void actionPerformed(ActionEvent e) { 548 // see if this is from _value itself, or from an alternate rep. 549 // if from an alternate rep, it will contain the value to select 550 if (e != null){ 551 if (log.isDebugEnabled()) { 552 log.debug("Variable = {} start action event cmd={}", label(), e.getActionCommand()); 553 } 554 if (!(e.getActionCommand().equals(""))) { 555 // is from alternate rep 556 log.debug("{} action event {} was from alternate rep", label(), e.getActionCommand()); 557 _value.setSelectedItem(e.getActionCommand()); 558 559 // match and select in tree 560 if (_nstored > 0) { 561 for (int i = 0; i < _nstored; i++) { 562 if (e.getActionCommand().equals(_itemArray[i])) { 563 // now select in the tree 564 TreePath path = _pathArray[i]; 565 for (JTree tree : trees) { 566 tree.setSelectionPath(path); 567 // ensure selection is in visible portion of JScrollPane 568 tree.scrollPathToVisible(path); 569 } 570 break; // first one is enough 571 } 572 } 573 } 574 } 575 576 // called for new values - set the CV as needed 577 CvValue cv = _cvMap.get(getCvNum()); 578 if (cv == null) { 579 log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName())); 580 return; 581 } 582 583 updatedDropDown(); 584 585 } 586 exitField(); 587 } 588 589 /** 590 * FocusListener implementations. 591 */ 592 @Override 593 public void focusGained(FocusEvent e) { 594 log.debug("Variable={}; focusGained", _name); 595 enterField(); 596 } 597 598 @Override 599 public void focusLost(FocusEvent e) { 600 log.debug("Variable={}; focusLost", _name); 601 exitField(); 602 } 603 604 // to complete this class, fill in the routines to handle "Value" parameter 605 // and to read/write/hear parameter changes. 606 @Override 607 public String getValueString() { 608 return Integer.toString(getIntValue()); 609 } 610 611 /** 612 * Set value from a String value. 613 * 614 * @param value a string representing the Long value to be set 615 */ 616 public void setValue(int value) { 617 if(value > 0){ 618 try { 619 long longVal = value; 620 long val = longVal; 621 setLongValue(val); 622 } catch (NumberFormatException e) { 623 log.warn("skipping set of non-long value \"{}\"", value); 624 } 625 selectValue(value); 626 } 627 } 628 629 @Override 630 public void setIntValue(int i) { 631 setLongValue(i); 632 } 633 634 @Override 635 public int getIntValue() { 636 if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) { 637 log.error("trying to get value {} too large for array length {} in var {}", _value.getSelectedIndex(), _valueArray.length, label()); 638 } 639 log.debug("SelectedIndex={} value={}", _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]); 640 return _valueArray[_value.getSelectedIndex()]; 641 } 642 643 /** 644 * Get the value as an unsigned long. 645 * 646 * @return the value as a long 647 */ 648 @Override 649 public long getLongValue() { 650 return _valueArray[_value.getSelectedIndex()]; 651 } 652 653 @Override 654 public String getTextValue() { 655 if (_value.getSelectedItem() != null) { 656 return _value.getSelectedItem().toString(); 657 } else { 658 return ""; 659 } 660 } 661 662 @Override 663 public Object getValueObject() { 664 return getLongValue(); 665 } 666 667 @Override 668 public Component getCommonRep() { 669 if (getReadOnly()) { 670 JLabel r = new JLabel((String)_value.getSelectedItem()); 671 updateRepresentation(r); 672 return r; 673 } else { 674 return _value; 675 } 676 } 677 678 private void addReservedEntry(long value) { 679 log.warn("Variable \"{}\" had to add reserved entry for {}", _name, value); 680 // We can be commanded to a number that hasn't been defined. 681 // But that's OK for certain applications. 682 // When this happens, we add enum values as needed 683 log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label()); 684 685 // lengthen arrays 686 _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1); 687 688 _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1); 689 690 _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1); 691 692 addItem("Reserved value " + value, (int)value); 693 694 // update the JComboBox 695 _value.addItem(_itemArray[_nstored - 1]); 696 _value.setSelectedItem(_itemArray[_nstored - 1]); 697 698 // tell trees to redisplay & select 699 for (JTree tree : trees) { 700 ((DefaultTreeModel) tree.getModel()).reload(); 701 tree.setSelectionPath(_pathArray[_nstored - 1]); 702 // ensure selection is in visible portion of JScrollPane 703 tree.scrollPathToVisible(_pathArray[_nstored - 1]); 704 } 705 } 706 707 public void setLongValue(long value) { 708 log.debug("Variable={}; enter setLongValue {}", _name, value); 709 long oldVal; 710 try { 711 oldVal = (Long.parseLong((String)_value.getSelectedItem()) - mOffset) / mFactor; 712 } catch (java.lang.NumberFormatException ex) { 713 oldVal = -999; 714 } 715 log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal); 716 717 int lengthOfArray = this._valueArray.length; 718 719 boolean foundIt = false; // did we find entry? If not, have to add one 720 for (int i = 0; i < lengthOfArray; i++) { 721 if (this._valueArray[i] == value){ 722 log.trace("{} setLongValue setSelectedIndex to {}", _name, i); 723 _value.setSelectedIndex(i); 724 foundIt = true; 725 } 726 } 727 if (!foundIt) { 728 addReservedEntry(value); 729 } 730 731 if (oldVal != value || getState() == ValueState.UNKNOWN) { 732 actionPerformed(null); 733 } 734 // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation; 735 // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old) 736 // using different scales. Probably a bug, but it has been there from well before 737 // the extended splitVal. Because of the risk of breaking existing 738 // behaviour somewhere, deferring correction until at least the next test release. 739 prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset); 740 log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value); 741 } 742 743 Color _defaultColor; 744 745 // implement an abstract member to set colors 746 @Override 747 void setColor(Color c) { 748 if (c != null && _value != null) { 749 _value.setBackground(c); 750 log.debug("Variable={}; Set Color to {}", _name, c.toString()); 751 } else if (_value != null) { 752 log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString()); 753 _value.setBackground(_defaultColor); 754 } 755 756 // prop.firePropertyChange("Value", null, null); 757 } 758 759 int _columns = 1; 760 761 762 @Override 763 public Component getNewRep(String format) { 764 // sort on format type 765 switch (format) { 766 case "tree": 767 DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst()); 768 JTree dTree = new JTree(dModel); 769 trees.add(dTree); 770 JScrollPane dScroll = new JScrollPane(dTree); 771 dTree.setRootVisible(false); 772 dTree.setShowsRootHandles(true); 773 dTree.setScrollsOnExpand(true); 774 dTree.setExpandsSelectedPaths(true); 775 dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION); 776 // arrange for only leaf nodes can be selected 777 dTree.addTreeSelectionListener(new TreeSelectionListener() { 778 @Override 779 public void valueChanged(TreeSelectionEvent e) { 780 TreePath[] paths = e.getPaths(); 781 for (TreePath path : paths) { 782 DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent(); 783 if (o.getChildCount() > 0) { 784 ((JTree) e.getSource()).removeSelectionPath(path); 785 } 786 } 787 // now record selection 788 if (paths.length >= 1) { 789 if (paths[0].getLastPathComponent() instanceof SplitEnumVariableValue.TreeLeafNode) { 790 // update value of Variable 791 setValue(_valueArray[((SplitEnumVariableValue.TreeLeafNode) paths[0].getLastPathComponent()).index]); 792 } 793 } 794 } 795 }); 796 // select initial value 797 TreePath path = _pathArray[_value.getSelectedIndex()]; 798 dTree.setSelectionPath(path); 799 // ensure selection is in visible portion of JScrollPane 800 dTree.scrollPathToVisible(path); 801 802 if (getReadOnly() || getInfoOnly()) { 803 log.error("read only variables cannot use tree format: {}", item()); 804 } 805 updateRepresentation(dScroll); 806 return dScroll; 807 default: { 808 // return a new JComboBox representing the same model 809 SplitEnumVariableValue.VarComboBox b = new SplitEnumVariableValue.VarComboBox(_value.getModel(), this); 810 comboVars.add(b); 811 if (getReadOnly() || getInfoOnly()) { 812 b.setEnabled(false); 813 } 814 updateRepresentation(b); 815 return b; 816 } 817 } 818 } 819 820 /** 821 * Select a specific value in the JComboBox display 822 * or, if need be, create another one 823 * @param value The new numerical value for the complete enum variable. 824 */ 825 protected void selectValue(int value) { 826 if (_nstored > 0 && value != 0) { 827 for (int i = 0; i < _nstored; i++) { 828 if (_valueArray[i] == value) { 829 //found it, select it 830 log.debug("{}: selectValue sets to {}", _name, i); 831 _value.setSelectedIndex(i); 832 833 // now select in the tree 834 TreePath path = _pathArray[i]; 835 for (JTree tree : trees) { 836 tree.setSelectionPath(path); 837 // ensure selection is in visible portion of JScrollPane 838 tree.scrollPathToVisible(path); 839 } 840 return; 841 } 842 } 843 } 844 845 // if we got to here, we need to add a new reserved value entry 846 addReservedEntry(value); 847 } 848 849 java.util.List<Component> reps = new java.util.ArrayList<>(); 850 851 public int retry = 0; // counts retrys of a single CV 852 853 int _progState = 0; // coded by the following 854 static final int IDLE = 0; 855 static final int READING_FIRST = 1; // positive values are reading, i.e. 2 is read 2nd CV 856 static final int WRITING_FIRST = -1; // negative values are writing, i.e. -2 is write 2nd CV 857 858 static final int bitCount = Long.bitCount(~0); 859 static final long intMask = Integer.toUnsignedLong(~0); 860 861 /** 862 * Notify the connected CVs of a state change from above 863 * 864 * @param state The new state 865 */ 866 @Override 867 public void setCvState(ValueState state) { 868 for (int i = 0; i < cvCount; i++) { 869 cvList.get(i).thisCV.setState(state); 870 } 871 } 872 873 @Override 874 public boolean isChanged() { 875 boolean changed = false; 876 for (int i = 0; i < cvCount; i++) { 877 changed = (changed || considerChanged(cvList.get(i).thisCV)); 878 } 879 return changed; 880 } 881 882 @Override 883 public boolean isToRead() { 884 boolean toRead = false; 885 for (int i = 0; i < cvCount; i++) { 886 toRead = (toRead || (cvList.get(i).thisCV).isToRead()); 887 } 888 return toRead; 889 } 890 891 @Override 892 public boolean isToWrite() { 893 boolean toWrite = false; 894 for (int i = 0; i < cvCount; i++) { 895 toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite()); 896 } 897 return toWrite; 898 } 899 900 @Override 901 public void readChanges() { 902 if (isToRead() && !isChanged()) { 903 log.debug("!!!!!!! unacceptable combination in readChanges: {}", label()); 904 } 905 if (isChanged() || isToRead()) { 906 readAll(); 907 } 908 } 909 910 @Override 911 public void writeChanges() { 912 if (isToWrite() && !isChanged()) { 913 log.debug("!!!!!! unacceptable combination in writeChanges: {}", label()); 914 } 915 if (isChanged() || isToWrite()) { 916 writeAll(); 917 } 918 } 919 920 @Override 921 public void readAll() { 922 log.debug("Variable={}; splitVal read() invoked", _name); 923 setToRead(false); 924 setBusy(true); // will be reset when value changes 925 //super.setState(READ); 926 //_value.setSelectedIndex(0); // start with a clean slate 927 for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be read 928 cvList.get(i).thisCV.setState(ValueState.READ); 929 } 930 //super.setState(READING_FIRST); 931 _progState = READING_FIRST; 932 retry = 0; 933 log.debug("Variable={}; Start CV read", _name); 934 log.debug(" Reading CV={}", cvList.get(0).cvName); 935 (cvList.get(0).thisCV).read(_status); // kick off the read sequence 936 } 937 938 @Override 939 public void writeAll() { 940 log.debug("Variable={}; write() invoked", _name); 941 if (getReadOnly()) { 942 log.error("Variable={}; unexpected write operation when readOnly is set", _name); 943 } 944 setToWrite(false); 945 setBusy(true); // will be reset when value changes 946 if (_progState != IDLE) { 947 log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState); 948 } 949 950 for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be written 951 cvList.get(i).thisCV.setState(ValueState.STORED); 952 } 953 954 _progState = WRITING_FIRST; 955 log.debug("Variable={}; Start CV write", _name); 956 log.debug(" Writing CV={}", cvList.get(0).cvName); 957 (cvList.get(0).thisCV).write(_status); // kick off the write sequence 958 } 959 960 /** 961 * Assigns a priority value to a given state. 962 * 963 * @param state State to be converted to a priority value 964 * @return Priority value from state, with UNKNOWN numerically highest 965 */ 966 @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value") 967 int priorityValue(ValueState state) { 968 int value = 0; 969 switch (state) { 970 case UNKNOWN: 971 value++; 972 //$FALL-THROUGH$ 973 case DIFFERENT: 974 value++; 975 //$FALL-THROUGH$ 976 case EDITED: 977 value++; 978 //$FALL-THROUGH$ 979 case FROMFILE: 980 value++; 981 //$FALL-THROUGH$ 982 default: 983 //$FALL-THROUGH$ 984 return value; 985 } 986 } 987 988 // handle incoming parameter notification 989 @Override 990 public void propertyChange(java.beans.PropertyChangeEvent e) { 991 // notification from CV; check for Value being changed 992 log.trace("propertyChange for {} {} _progState = {} from {}", e.getPropertyName(), e.getNewValue(), _progState, e.getSource()); 993 switch (e.getPropertyName()) { 994 case "Busy": 995 996 if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) { 997 998 // check for expected cv 999 if ( (_progState >= READING_FIRST || _progState <= WRITING_FIRST ) && e.getSource() != cvList.get(Math.abs(_progState) - 1).thisCV ) { 1000 log.trace("From \"{}\" but expected \"{}\", ignoring", 1001 e.getSource(), cvList.get(Math.abs(_progState) - 1).thisCV ); 1002 break; 1003 } 1004 1005 if (_progState >= READING_FIRST){ 1006 ValueState curState = (cvList.get(Math.abs(_progState) - 1).thisCV).getState(); 1007 log.trace("propertyChange Busy _progState={} curState={}", _progState, curState); 1008 if (curState == ValueState.READ) { // was the last read successful? 1009 retry = 0; 1010 log.debug(" Variable={}; Busy finds ValueState.READ cvCount={}", _name, cvCount); 1011 if (Math.abs(_progState) < cvCount) { // read next CV 1012 _progState++; 1013 log.debug("Increment _progState to {}, reading CV={}", _progState, cvList.get(Math.abs(_progState) - 1).cvName); 1014 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 1015 } else { // finally done, set not busy 1016 log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState); 1017 _progState = IDLE; 1018 setToRead(false); 1019 setBusy(false); 1020 } 1021 } else { // read failed 1022 log.debug(" Variable={}; Busy finds other than ValueState.READ _progState {}", _name, _progState); 1023 if (retry < RETRY_COUNT) { //have we exhausted retry count? 1024 retry++; 1025 // stay on same sequence number for retry, don't update _progState 1026 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 1027 } else { 1028 log.warn("Retry failed for CV{}" ,(cvList.get(Math.abs(_progState) - 1).thisCV).toString()); 1029 _progState = IDLE; 1030 setToRead(false); 1031 setBusy(false); 1032 if (RETRY_COUNT > 0) { 1033 for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur 1034 cvList.get(i).thisCV.setState(ValueState.UNKNOWN); 1035 } 1036 } 1037 } 1038 } 1039 } else if (_progState <= WRITING_FIRST) { // writing CVs 1040 if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) { // was the last read successful? 1041 if (Math.abs(_progState) < cvCount) { // write next CV 1042 _progState--; 1043 log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName); 1044 (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status); 1045 } else { // finally done, set not busy 1046 log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState); 1047 _progState = IDLE; 1048 setBusy(false); 1049 setToWrite(false); 1050 } 1051 } else { // write failed we're done! 1052 log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState); 1053 _progState = IDLE; 1054 setToWrite(false); 1055 setBusy(false); 1056 } 1057 } 1058 } 1059 break; 1060 case "State": { 1061 log.debug("Possible {} variable state change due to CV state change, so propagate that", _name); 1062 ValueState varState = getState(); // AbstractValue.SAME; 1063 log.debug("{} variable state was {}", _name, varState.getName()); 1064 for (int i = 0; i < cvCount; i++) { 1065 ValueState state = cvList.get(i).thisCV.getState(); 1066 if (i == 0) { 1067 varState = state; 1068 } else if (priorityValue(state) > priorityValue(varState)) { 1069 varState = ValueState.UNKNOWN; // or should it be = state ? 1070// varState = state; // or should it be = state ? 1071 } 1072 } 1073 setState(varState); 1074 for (JTree tree : trees) { 1075 tree.setBackground(_value.getBackground()); 1076 //tree.setOpaque(true); 1077 } 1078 log.debug("{} variable state set to {}", _name, varState.getName()); 1079 break; 1080 } 1081 case "Value": { 1082 // update value of Variable 1083 1084 //setLongValue(Long.parseLong((String)_value.getSelectedItem())); // check for duplicate done inside setValue 1085 log.debug("update value of Variable {} cvCount={}", _name, cvCount); 1086 1087 int[] intVals = new int[cvCount]; 1088 1089 for (int i = 0; i < cvCount; i++) { 1090 intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask); 1091 log.trace(" with intVal[{}] = {}", i, intVals[i]); 1092 } 1093 1094 updateVariableValue(intVals); 1095 1096 log.debug("state change due to CV value change, so propagate that"); 1097 ValueState varState = ValueState.SAME; 1098 for (int i = 0; i < cvCount; i++) { 1099 ValueState state = cvList.get(i).thisCV.getState(); 1100 if (priorityValue(state) > priorityValue(varState)) { 1101 varState = state; 1102 } 1103 } 1104 setState(varState); 1105 1106 updatedDropDown(); 1107 1108 break; 1109 } 1110 default: 1111 break; 1112 } 1113 } 1114 1115 /* Internal class extends a JComboBox so that its color is consistent with 1116 * an underlying variable 1117 * 1118 * @author Bob Jacobsen Copyright (C) 2001 1119 * @author tweaked by Jordan McBride Copyright (C) 2021 1120 * 1121 */ 1122 public static class VarComboBox extends JComboBox<String> { 1123 1124 VarComboBox(ComboBoxModel<String> m, SplitEnumVariableValue var) { 1125 super(m); 1126 _var = var; 1127 _l = new java.beans.PropertyChangeListener() { 1128 @Override 1129 public void propertyChange(java.beans.PropertyChangeEvent e) { 1130 log.debug("VarComboBox saw property change: {}", e); 1131 originalPropertyChanged(e); 1132 } 1133 }; 1134 // get the original color right 1135 setBackground(_var._value.getBackground()); 1136 setOpaque(true); 1137 // listen for changes to original state 1138 _var.addPropertyChangeListener(_l); 1139 } 1140 1141 SplitEnumVariableValue _var; 1142 transient java.beans.PropertyChangeListener _l = null; 1143 1144 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 1145 // update this color from original state 1146 if (e.getPropertyName().equals("State")) { 1147 setBackground(_var._value.getBackground()); 1148 setOpaque(true); 1149 } 1150 } 1151 1152 public void dispose() { 1153 if (_var != null && _l != null) { 1154 _var.removePropertyChangeListener(_l); 1155 } 1156 _l = null; 1157 _var = null; 1158 } 1159 } 1160 1161 /** 1162 * Class to hold CV parameters for CVs used. 1163 */ 1164 static class CvItem { 1165 1166 // class fields 1167 String cvName; 1168 String cvMask; 1169 int startOffset; 1170 CvValue thisCV; 1171 1172 CvItem(String cvNameVal, String cvMaskVal) { 1173 cvName = cvNameVal; 1174 cvMask = cvMaskVal; 1175 } 1176 } 1177 1178// clean up connections when done 1179 @Override 1180 public void dispose() { 1181 log.debug("dispose"); 1182 1183 // remove connection to CV 1184 if (_cvMap.get(getCvNum()) == null) { 1185 log.error("no CV defined for variable {}, no listeners to remove", getCvNum()); 1186 } else { 1187 _cvMap.get(getCvNum()).removePropertyChangeListener(this); 1188 } 1189 // remove connection to graphical representation 1190 disposeReps(); 1191 } 1192 1193 void disposeReps() { 1194 if (_value != null) { 1195 _value.removeActionListener(this); 1196 } 1197 for (int i = 0; i < comboCBs.size(); i++) { 1198 comboCBs.get(i).dispose(); 1199 } 1200 for (int i = 0; i < comboVars.size(); i++) { 1201 comboVars.get(i).dispose(); 1202 } 1203 for (int i = 0; i < comboRBs.size(); i++) { 1204 comboRBs.get(i).dispose(); 1205 } 1206 } 1207 1208 static class TreeLeafNode extends DefaultMutableTreeNode { 1209 1210 TreeLeafNode(String name, int index) { 1211 super(name); 1212 this.index = index; 1213 } 1214 1215 int index; 1216 } 1217 1218 1219 1220 // initialize logging 1221 private final static Logger log = LoggerFactory.getLogger(SplitEnumVariableValue.class 1222 .getName()); 1223 1224}