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 @Deprecated 417 public String getSecondCvNum() { 418 String retString = ""; 419 if (cvCount > 1) { 420 retString = cvList.get(1).cvName; 421 } 422 return retString; 423 } 424 425 long _minVal; 426 long _maxVal; 427 428 @Override 429 public Object rangeVal() { 430 return "Split value"; 431 } 432 433 String oldContents = "0"; 434 435 long getValueFromText(String s) { 436 return (Long.parseUnsignedLong(s)); 437 } 438 439 String getTextFromValue(long v) { 440 return (Long.toUnsignedString(v)); 441 } 442 443 /** 444 * Contains numeric-value specific code. 445 * <br><br> 446 * Calculates new value for _enumField and invokes 447 * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the 448 * change 449 * 450 * @param intVals array of new CV values 451 */ 452 void updateVariableValue(int[] intVals) { 453 if (intVals.length > 0){ 454 long newVal = 0; 455 for (int i = 0; i < intVals.length; i++) { 456 newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset); 457 log.debug("Variable={}; i={}; intVals={}; startOffset={}; newVal={}", 458 _name, i, intVals[i], cvList.get(i).startOffset, getTextFromValue(newVal)); 459 } 460 log.debug("Variable={}; set value to {}", _name, newVal); 461 setLongValue(newVal); // check for duplicate is done inside setLongValue 462 log.debug("Variable={}; in property change after setValue call", _name); 463 } 464 } 465 466 /** 467 * Saves selected item from _value (enumField) to oldContents. 468 */ 469 void enterField() { 470 oldContents = String.valueOf(_value.getSelectedItem()); 471 log.debug("enterField sets oldContents to {}", oldContents); 472 } 473 474 /** 475 * Contains numeric-value specific code. 476 * <br> 477 * firePropertyChange for "Value" with new and old contents of _enumField 478 */ 479 void exitField(){ 480 // there may be a lost focus event left in the queue when disposed so protect 481 log.trace("exitField starts"); 482 if (_value != null && !oldContents.equals(_value.getSelectedItem())) { 483 long newFieldVal = 0; 484 try { 485 newFieldVal = Long.parseLong((String) Objects.requireNonNull(_value.getSelectedItem())); 486 } catch (NumberFormatException e) { 487 //_value.setText(oldContents); 488 } 489 log.debug("_minVal={};_maxVal={};newFieldVal={}", 490 Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal)); 491 if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) { 492 493 } else { 494 long newVal = (newFieldVal - mOffset) / mFactor; 495 long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor; 496 prop.firePropertyChange("Value", oldVal, newVal); 497 } 498 } 499 log.trace("exitField ends"); 500 } 501 502 boolean _fieldShrink = false; 503 504 void updatedDropDown() { 505 log.debug("Variable='{}'; enter updatedDropDown in {} with DropDownValue='{}'", _name, (this.getClass().getSimpleName()), _value.getSelectedIndex()); 506 // called for new values in text field - set the CVs as needed 507 508 int[] retVals = getCvValsFromSingleInt(getIntValue()); 509 510 // combine with existing values via mask 511 for (int j = 0; j < cvCount; j++) { 512 int i = j; 513 log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask)); 514 int cvMask = maskValAsInt(cvList.get(i).cvMask); 515 CvValue thisCV = cvList.get(i).thisCV; 516 int oldCvVal = thisCV.getValue(); 517 int newCvVal = (oldCvVal & ~cvMask) 518 | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask); 519 log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal); 520 521 // cv updates here trigger updated property changes, which means 522 // we're going to get notified sooner or later. 523 if (newCvVal != oldCvVal) { 524 thisCV.setValue(newCvVal); 525 } 526 } 527 log.debug("Variable={}; exit updatedDropDown", _name); 528 } 529 530 int[] getCvValsFromSingleInt(long newEntry) { 531 // calculate resulting number 532 long newVal = (newEntry - mOffset) / mFactor; 533 log.debug("getCvValsFromSingleInt Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor); 534 535 int[] retVals = new int[cvCount]; 536 537 // extract individual values via masks 538 for (int i = 0; i < cvCount; i++) { 539 log.trace(" Starting with newVal={} startOffset={} mask={} offsetVal={}", 540 newVal, cvList.get(i).startOffset, maskValAsInt(cvList.get(i).cvMask), offsetVal(cvList.get(i).cvMask)); 541 retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset)) 542 & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask))); 543 log.trace(" Calculated {} entry is {}", i, retVals[i]); 544 } 545 return retVals; 546 } 547 548 /** 549 * ActionListener implementation. Called by new selection in the JComboBox representation. 550 * <p> 551 * Invokes {@link #exitField exitField()} 552 * 553 * @param e the action event 554 */ 555 @Override 556 public void actionPerformed(ActionEvent e) { 557 // see if this is from _value itself, or from an alternate rep. 558 // if from an alternate rep, it will contain the value to select 559 if (e != null){ 560 if (log.isDebugEnabled()) { 561 log.debug("Variable = {} start action event cmd={}", label(), e.getActionCommand()); 562 } 563 if (!(e.getActionCommand().equals(""))) { 564 // is from alternate rep 565 log.debug("{} action event {} was from alternate rep", label(), e.getActionCommand()); 566 _value.setSelectedItem(e.getActionCommand()); 567 568 // match and select in tree 569 if (_nstored > 0) { 570 for (int i = 0; i < _nstored; i++) { 571 if (e.getActionCommand().equals(_itemArray[i])) { 572 // now select in the tree 573 TreePath path = _pathArray[i]; 574 for (JTree tree : trees) { 575 tree.setSelectionPath(path); 576 // ensure selection is in visible portion of JScrollPane 577 tree.scrollPathToVisible(path); 578 } 579 break; // first one is enough 580 } 581 } 582 } 583 } 584 585 // called for new values - set the CV as needed 586 CvValue cv = _cvMap.get(getCvNum()); 587 if (cv == null) { 588 log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName())); 589 return; 590 } 591 592 updatedDropDown(); 593 594 } 595 exitField(); 596 } 597 598 /** 599 * FocusListener implementations. 600 */ 601 @Override 602 public void focusGained(FocusEvent e) { 603 log.debug("Variable={}; focusGained", _name); 604 enterField(); 605 } 606 607 @Override 608 public void focusLost(FocusEvent e) { 609 log.debug("Variable={}; focusLost", _name); 610 exitField(); 611 } 612 613 // to complete this class, fill in the routines to handle "Value" parameter 614 // and to read/write/hear parameter changes. 615 @Override 616 public String getValueString() { 617 return Integer.toString(getIntValue()); 618 } 619 620 /** 621 * Set value from a String value. 622 * 623 * @param value a string representing the Long value to be set 624 */ 625 public void setValue(int value) { 626 if(value > 0){ 627 try { 628 long longVal = value; 629 long val = longVal; 630 setLongValue(val); 631 } catch (NumberFormatException e) { 632 log.warn("skipping set of non-long value \"{}\"", value); 633 } 634 selectValue(value); 635 } 636 } 637 638 @Override 639 public void setIntValue(int i) { 640 setLongValue(i); 641 } 642 643 @Override 644 public int getIntValue() { 645 if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) { 646 log.error("trying to get value {} too large for array length {} in var {}", _value.getSelectedIndex(), _valueArray.length, label()); 647 } 648 log.debug("SelectedIndex={} value={}", _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]); 649 return _valueArray[_value.getSelectedIndex()]; 650 } 651 652 /** 653 * Get the value as an unsigned long. 654 * 655 * @return the value as a long 656 */ 657 @Override 658 public long getLongValue() { 659 return _valueArray[_value.getSelectedIndex()]; 660 } 661 662 @Override 663 public String getTextValue() { 664 if (_value.getSelectedItem() != null) { 665 return _value.getSelectedItem().toString(); 666 } else { 667 return ""; 668 } 669 } 670 671 @Override 672 public Object getValueObject() { 673 return getLongValue(); 674 } 675 676 @Override 677 public Component getCommonRep() { 678 if (getReadOnly()) { 679 JLabel r = new JLabel((String)_value.getSelectedItem()); 680 updateRepresentation(r); 681 return r; 682 } else { 683 return _value; 684 } 685 } 686 687 private void addReservedEntry(long value) { 688 log.warn("Variable \"{}\" had to add reserved entry for {}", _name, value); 689 // We can be commanded to a number that hasn't been defined. 690 // But that's OK for certain applications. 691 // When this happens, we add enum values as needed 692 log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label()); 693 694 // lengthen arrays 695 _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1); 696 697 _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1); 698 699 _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1); 700 701 addItem("Reserved value " + value, (int)value); 702 703 // update the JComboBox 704 _value.addItem(_itemArray[_nstored - 1]); 705 _value.setSelectedItem(_itemArray[_nstored - 1]); 706 707 // tell trees to redisplay & select 708 for (JTree tree : trees) { 709 ((DefaultTreeModel) tree.getModel()).reload(); 710 tree.setSelectionPath(_pathArray[_nstored - 1]); 711 // ensure selection is in visible portion of JScrollPane 712 tree.scrollPathToVisible(_pathArray[_nstored - 1]); 713 } 714 } 715 716 public void setLongValue(long value) { 717 log.debug("Variable={}; enter setLongValue {}", _name, value); 718 long oldVal; 719 try { 720 oldVal = (Long.parseLong((String)_value.getSelectedItem()) - mOffset) / mFactor; 721 } catch (java.lang.NumberFormatException ex) { 722 oldVal = -999; 723 } 724 log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal); 725 726 int lengthOfArray = this._valueArray.length; 727 728 boolean foundIt = false; // did we find entry? If not, have to add one 729 for (int i = 0; i < lengthOfArray; i++) { 730 if (this._valueArray[i] == value){ 731 log.trace("{} setLongValue setSelectedIndex to {}", _name, i); 732 _value.setSelectedIndex(i); 733 foundIt = true; 734 } 735 } 736 if (!foundIt) { 737 addReservedEntry(value); 738 } 739 740 if (oldVal != value || getState() == ValueState.UNKNOWN) { 741 actionPerformed(null); 742 } 743 // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation; 744 // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old) 745 // using different scales. Probably a bug, but it has been there from well before 746 // the extended splitVal. Because of the risk of breaking existing 747 // behaviour somewhere, deferring correction until at least the next test release. 748 prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset); 749 log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value); 750 } 751 752 Color _defaultColor; 753 754 // implement an abstract member to set colors 755 @Override 756 void setColor(Color c) { 757 if (c != null && _value != null) { 758 _value.setBackground(c); 759 log.debug("Variable={}; Set Color to {}", _name, c.toString()); 760 } else if (_value != null) { 761 log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString()); 762 _value.setBackground(_defaultColor); 763 } 764 765 // prop.firePropertyChange("Value", null, null); 766 } 767 768 int _columns = 1; 769 770 771 @Override 772 public Component getNewRep(String format) { 773 // sort on format type 774 switch (format) { 775 case "tree": 776 DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst()); 777 JTree dTree = new JTree(dModel); 778 trees.add(dTree); 779 JScrollPane dScroll = new JScrollPane(dTree); 780 dTree.setRootVisible(false); 781 dTree.setShowsRootHandles(true); 782 dTree.setScrollsOnExpand(true); 783 dTree.setExpandsSelectedPaths(true); 784 dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION); 785 // arrange for only leaf nodes can be selected 786 dTree.addTreeSelectionListener(new TreeSelectionListener() { 787 @Override 788 public void valueChanged(TreeSelectionEvent e) { 789 TreePath[] paths = e.getPaths(); 790 for (TreePath path : paths) { 791 DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent(); 792 if (o.getChildCount() > 0) { 793 ((JTree) e.getSource()).removeSelectionPath(path); 794 } 795 } 796 // now record selection 797 if (paths.length >= 1) { 798 if (paths[0].getLastPathComponent() instanceof SplitEnumVariableValue.TreeLeafNode) { 799 // update value of Variable 800 setValue(_valueArray[((SplitEnumVariableValue.TreeLeafNode) paths[0].getLastPathComponent()).index]); 801 } 802 } 803 } 804 }); 805 // select initial value 806 TreePath path = _pathArray[_value.getSelectedIndex()]; 807 dTree.setSelectionPath(path); 808 // ensure selection is in visible portion of JScrollPane 809 dTree.scrollPathToVisible(path); 810 811 if (getReadOnly() || getInfoOnly()) { 812 log.error("read only variables cannot use tree format: {}", item()); 813 } 814 updateRepresentation(dScroll); 815 return dScroll; 816 default: { 817 // return a new JComboBox representing the same model 818 SplitEnumVariableValue.VarComboBox b = new SplitEnumVariableValue.VarComboBox(_value.getModel(), this); 819 comboVars.add(b); 820 if (getReadOnly() || getInfoOnly()) { 821 b.setEnabled(false); 822 } 823 updateRepresentation(b); 824 return b; 825 } 826 } 827 } 828 829 /** 830 * Select a specific value in the JComboBox display 831 * or, if need be, create another one 832 * @param value The new numerical value for the complete enum variable. 833 */ 834 protected void selectValue(int value) { 835 if (_nstored > 0 && value != 0) { 836 for (int i = 0; i < _nstored; i++) { 837 if (_valueArray[i] == value) { 838 //found it, select it 839 log.debug("{}: selectValue sets to {}", _name, i); 840 _value.setSelectedIndex(i); 841 842 // now select in the tree 843 TreePath path = _pathArray[i]; 844 for (JTree tree : trees) { 845 tree.setSelectionPath(path); 846 // ensure selection is in visible portion of JScrollPane 847 tree.scrollPathToVisible(path); 848 } 849 return; 850 } 851 } 852 } 853 854 // if we got to here, we need to add a new reserved value entry 855 addReservedEntry(value); 856 } 857 858 java.util.List<Component> reps = new java.util.ArrayList<>(); 859 860 public int retry = 0; // counts retrys of a single CV 861 862 int _progState = 0; // coded by the following 863 static final int IDLE = 0; 864 static final int READING_FIRST = 1; // positive values are reading, i.e. 2 is read 2nd CV 865 static final int WRITING_FIRST = -1; // negative values are writing, i.e. -2 is write 2nd CV 866 867 static final int bitCount = Long.bitCount(~0); 868 static final long intMask = Integer.toUnsignedLong(~0); 869 870 /** 871 * Notify the connected CVs of a state change from above 872 * 873 * @param state The new state 874 */ 875 @Override 876 public void setCvState(ValueState state) { 877 for (int i = 0; i < cvCount; i++) { 878 cvList.get(i).thisCV.setState(state); 879 } 880 } 881 882 @Override 883 public boolean isChanged() { 884 boolean changed = false; 885 for (int i = 0; i < cvCount; i++) { 886 changed = (changed || considerChanged(cvList.get(i).thisCV)); 887 } 888 return changed; 889 } 890 891 @Override 892 public boolean isToRead() { 893 boolean toRead = false; 894 for (int i = 0; i < cvCount; i++) { 895 toRead = (toRead || (cvList.get(i).thisCV).isToRead()); 896 } 897 return toRead; 898 } 899 900 @Override 901 public boolean isToWrite() { 902 boolean toWrite = false; 903 for (int i = 0; i < cvCount; i++) { 904 toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite()); 905 } 906 return toWrite; 907 } 908 909 @Override 910 public void readChanges() { 911 if (isToRead() && !isChanged()) { 912 log.debug("!!!!!!! unacceptable combination in readChanges: {}", label()); 913 } 914 if (isChanged() || isToRead()) { 915 readAll(); 916 } 917 } 918 919 @Override 920 public void writeChanges() { 921 if (isToWrite() && !isChanged()) { 922 log.debug("!!!!!! unacceptable combination in writeChanges: {}", label()); 923 } 924 if (isChanged() || isToWrite()) { 925 writeAll(); 926 } 927 } 928 929 @Override 930 public void readAll() { 931 log.debug("Variable={}; splitVal read() invoked", _name); 932 setToRead(false); 933 setBusy(true); // will be reset when value changes 934 //super.setState(READ); 935 //_value.setSelectedIndex(0); // start with a clean slate 936 for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be read 937 cvList.get(i).thisCV.setState(ValueState.READ); 938 } 939 //super.setState(READING_FIRST); 940 _progState = READING_FIRST; 941 retry = 0; 942 log.debug("Variable={}; Start CV read", _name); 943 log.debug(" Reading CV={}", cvList.get(0).cvName); 944 (cvList.get(0).thisCV).read(_status); // kick off the read sequence 945 } 946 947 @Override 948 public void writeAll() { 949 log.debug("Variable={}; write() invoked", _name); 950 if (getReadOnly()) { 951 log.error("Variable={}; unexpected write operation when readOnly is set", _name); 952 } 953 setToWrite(false); 954 setBusy(true); // will be reset when value changes 955 if (_progState != IDLE) { 956 log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState); 957 } 958 959 for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be written 960 cvList.get(i).thisCV.setState(ValueState.STORED); 961 } 962 963 _progState = WRITING_FIRST; 964 log.debug("Variable={}; Start CV write", _name); 965 log.debug(" Writing CV={}", cvList.get(0).cvName); 966 (cvList.get(0).thisCV).write(_status); // kick off the write sequence 967 } 968 969 /** 970 * Assigns a priority value to a given state. 971 * 972 * @param state State to be converted to a priority value 973 * @return Priority value from state, with UNKNOWN numerically highest 974 */ 975 @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value") 976 int priorityValue(ValueState state) { 977 int value = 0; 978 switch (state) { 979 case UNKNOWN: 980 value++; 981 //$FALL-THROUGH$ 982 case DIFFERENT: 983 value++; 984 //$FALL-THROUGH$ 985 case EDITED: 986 value++; 987 //$FALL-THROUGH$ 988 case FROMFILE: 989 value++; 990 //$FALL-THROUGH$ 991 default: 992 //$FALL-THROUGH$ 993 return value; 994 } 995 } 996 997 // handle incoming parameter notification 998 @Override 999 public void propertyChange(java.beans.PropertyChangeEvent e) { 1000 // notification from CV; check for Value being changed 1001 log.trace("propertyChange for {} {} _progState = {} from {}", e.getPropertyName(), e.getNewValue(), _progState, e.getSource()); 1002 switch (e.getPropertyName()) { 1003 case "Busy": 1004 1005 if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) { 1006 1007 // check for expected cv 1008 if ( (_progState >= READING_FIRST || _progState <= WRITING_FIRST ) && e.getSource() != cvList.get(Math.abs(_progState) - 1).thisCV ) { 1009 log.trace("From \"{}\" but expected \"{}\", ignoring", 1010 e.getSource(), cvList.get(Math.abs(_progState) - 1).thisCV ); 1011 break; 1012 } 1013 1014 if (_progState >= READING_FIRST){ 1015 ValueState curState = (cvList.get(Math.abs(_progState) - 1).thisCV).getState(); 1016 log.trace("propertyChange Busy _progState={} curState={}", _progState, curState); 1017 if (curState == ValueState.READ) { // was the last read successful? 1018 retry = 0; 1019 log.debug(" Variable={}; Busy finds ValueState.READ cvCount={}", _name, cvCount); 1020 if (Math.abs(_progState) < cvCount) { // read next CV 1021 _progState++; 1022 log.debug("Increment _progState to {}, reading CV={}", _progState, cvList.get(Math.abs(_progState) - 1).cvName); 1023 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 1024 } else { // finally done, set not busy 1025 log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState); 1026 _progState = IDLE; 1027 setToRead(false); 1028 setBusy(false); 1029 } 1030 } else { // read failed 1031 log.debug(" Variable={}; Busy finds other than ValueState.READ _progState {}", _name, _progState); 1032 if (retry < RETRY_COUNT) { //have we exhausted retry count? 1033 retry++; 1034 // stay on same sequence number for retry, don't update _progState 1035 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 1036 } else { 1037 log.warn("Retry failed for CV{}" ,(cvList.get(Math.abs(_progState) - 1).thisCV).toString()); 1038 _progState = IDLE; 1039 setToRead(false); 1040 setBusy(false); 1041 if (RETRY_COUNT > 0) { 1042 for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur 1043 cvList.get(i).thisCV.setState(ValueState.UNKNOWN); 1044 } 1045 } 1046 } 1047 } 1048 } else if (_progState <= WRITING_FIRST) { // writing CVs 1049 if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) { // was the last read successful? 1050 if (Math.abs(_progState) < cvCount) { // write next CV 1051 _progState--; 1052 log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName); 1053 (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status); 1054 } else { // finally done, set not busy 1055 log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState); 1056 _progState = IDLE; 1057 setBusy(false); 1058 setToWrite(false); 1059 } 1060 } else { // write failed we're done! 1061 log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState); 1062 _progState = IDLE; 1063 setToWrite(false); 1064 setBusy(false); 1065 } 1066 } 1067 } 1068 break; 1069 case "State": { 1070 log.debug("Possible {} variable state change due to CV state change, so propagate that", _name); 1071 ValueState varState = getState(); // AbstractValue.SAME; 1072 log.debug("{} variable state was {}", _name, varState.getName()); 1073 for (int i = 0; i < cvCount; i++) { 1074 ValueState state = cvList.get(i).thisCV.getState(); 1075 if (i == 0) { 1076 varState = state; 1077 } else if (priorityValue(state) > priorityValue(varState)) { 1078 varState = ValueState.UNKNOWN; // or should it be = state ? 1079// varState = state; // or should it be = state ? 1080 } 1081 } 1082 setState(varState); 1083 for (JTree tree : trees) { 1084 tree.setBackground(_value.getBackground()); 1085 //tree.setOpaque(true); 1086 } 1087 log.debug("{} variable state set to {}", _name, varState.getName()); 1088 break; 1089 } 1090 case "Value": { 1091 // update value of Variable 1092 1093 //setLongValue(Long.parseLong((String)_value.getSelectedItem())); // check for duplicate done inside setValue 1094 log.debug("update value of Variable {} cvCount={}", _name, cvCount); 1095 1096 int[] intVals = new int[cvCount]; 1097 1098 for (int i = 0; i < cvCount; i++) { 1099 intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask); 1100 log.trace(" with intVal[{}] = {}", i, intVals[i]); 1101 } 1102 1103 updateVariableValue(intVals); 1104 1105 log.debug("state change due to CV value change, so propagate that"); 1106 ValueState varState = ValueState.SAME; 1107 for (int i = 0; i < cvCount; i++) { 1108 ValueState state = cvList.get(i).thisCV.getState(); 1109 if (priorityValue(state) > priorityValue(varState)) { 1110 varState = state; 1111 } 1112 } 1113 setState(varState); 1114 1115 updatedDropDown(); 1116 1117 break; 1118 } 1119 default: 1120 break; 1121 } 1122 } 1123 1124 /* Internal class extends a JComboBox so that its color is consistent with 1125 * an underlying variable 1126 * 1127 * @author Bob Jacobsen Copyright (C) 2001 1128 * @author tweaked by Jordan McBride Copyright (C) 2021 1129 * 1130 */ 1131 public static class VarComboBox extends JComboBox<String> { 1132 1133 VarComboBox(ComboBoxModel<String> m, SplitEnumVariableValue var) { 1134 super(m); 1135 _var = var; 1136 _l = new java.beans.PropertyChangeListener() { 1137 @Override 1138 public void propertyChange(java.beans.PropertyChangeEvent e) { 1139 log.debug("VarComboBox saw property change: {}", e); 1140 originalPropertyChanged(e); 1141 } 1142 }; 1143 // get the original color right 1144 setBackground(_var._value.getBackground()); 1145 setOpaque(true); 1146 // listen for changes to original state 1147 _var.addPropertyChangeListener(_l); 1148 } 1149 1150 SplitEnumVariableValue _var; 1151 transient java.beans.PropertyChangeListener _l = null; 1152 1153 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 1154 // update this color from original state 1155 if (e.getPropertyName().equals("State")) { 1156 setBackground(_var._value.getBackground()); 1157 setOpaque(true); 1158 } 1159 } 1160 1161 public void dispose() { 1162 if (_var != null && _l != null) { 1163 _var.removePropertyChangeListener(_l); 1164 } 1165 _l = null; 1166 _var = null; 1167 } 1168 } 1169 1170 /** 1171 * Class to hold CV parameters for CVs used. 1172 */ 1173 static class CvItem { 1174 1175 // class fields 1176 String cvName; 1177 String cvMask; 1178 int startOffset; 1179 CvValue thisCV; 1180 1181 CvItem(String cvNameVal, String cvMaskVal) { 1182 cvName = cvNameVal; 1183 cvMask = cvMaskVal; 1184 } 1185 } 1186 1187// clean up connections when done 1188 @Override 1189 public void dispose() { 1190 log.debug("dispose"); 1191 1192 // remove connection to CV 1193 if (_cvMap.get(getCvNum()) == null) { 1194 log.error("no CV defined for variable {}, no listeners to remove", getCvNum()); 1195 } else { 1196 _cvMap.get(getCvNum()).removePropertyChangeListener(this); 1197 } 1198 // remove connection to graphical representation 1199 disposeReps(); 1200 } 1201 1202 void disposeReps() { 1203 if (_value != null) { 1204 _value.removeActionListener(this); 1205 } 1206 for (int i = 0; i < comboCBs.size(); i++) { 1207 comboCBs.get(i).dispose(); 1208 } 1209 for (int i = 0; i < comboVars.size(); i++) { 1210 comboVars.get(i).dispose(); 1211 } 1212 for (int i = 0; i < comboRBs.size(); i++) { 1213 comboRBs.get(i).dispose(); 1214 } 1215 } 1216 1217 static class TreeLeafNode extends DefaultMutableTreeNode { 1218 1219 TreeLeafNode(String name, int index) { 1220 super(name); 1221 this.index = index; 1222 } 1223 1224 int index; 1225 } 1226 1227 1228 1229 // initialize logging 1230 private final static Logger log = LoggerFactory.getLogger(SplitEnumVariableValue.class 1231 .getName()); 1232 1233}