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