001package jmri.jmrit.symbolicprog; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.awt.Color; 005import java.awt.Component; 006import java.awt.event.ActionEvent; 007import java.awt.event.ActionListener; 008import java.awt.event.FocusEvent; 009import java.awt.event.FocusListener; 010import java.util.ArrayList; 011import java.util.HashMap; 012import java.util.List; 013import javax.swing.JLabel; 014import javax.swing.JTextField; 015import javax.swing.text.Document; 016import jmri.util.CvUtil; 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020/** 021 * Extends VariableValue to represent a variable split across multiple CVs. 022 * <br> 023 * The {@code mask} attribute represents the part of the value that's present in 024 * each CV; higher-order bits are loaded to subsequent CVs.<br> 025 * It is possible to assign a specific mask for each CV by providing a space 026 * separated list of masks, starting with the lowest, and matching the order of 027 * CVs 028 * <br><br> 029 * The original use was for addresses of stationary (accessory) decoders. 030 * <br> 031 * The original version only allowed two CVs, with the second CV specified by 032 * the attributes {@code highCV} and {@code upperMask}. 033 * <br><br> 034 * The preferred technique is now to specify all CVs in the {@code CV} attribute 035 * alone, as documented at {@link CvUtil#expandCvList expandCvList(String)}. 036 * <br><br> 037 * Optional attributes {@code factor} and {@code offset} are applied when going 038 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa: 039 * <pre> 040 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor} 041 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset} 042 * </pre> 043 * 044 * @author Bob Jacobsen Copyright (C) 2002, 2003, 2004, 2013 045 * @author Dave Heap Copyright (C) 2016, 2019 046 * @author Egbert Broerse Copyright (C) 2020 047 */ 048public class SplitVariableValue extends VariableValue 049 implements ActionListener, FocusListener { 050 051 private static final int RETRY_COUNT = 2; 052 053 public SplitVariableValue(String name, String comment, String cvName, 054 boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, 055 String cvNum, String mask, int minVal, int maxVal, 056 HashMap<String, CvValue> v, JLabel status, String stdname, 057 String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) { 058 super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname); 059 _minVal = 0; 060 _maxVal = ~0; 061 stepOneActions(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname, pSecondCV, pFactor, pOffset, uppermask, extra1, extra2, extra3, extra4); 062 _name = name; 063 _mask = mask; // will be converted to MaskArray to apply separate mask for each CV 064 if (mask != null && mask.contains(" ")) { 065 _maskArray = mask.split(" "); // type accepts multiple masks for SplitVariableValue 066 } else { 067 _maskArray = new String[1]; 068 _maskArray[0] = mask; 069 } 070 _cvNum = cvNum; 071 _textField = new JTextField("0"); 072 _defaultColor = _textField.getBackground(); 073 074 _textField.setBackground(ValueState.UNKNOWN.getColor()); 075 _textField.getAccessibleContext().setAccessibleName(label()); 076 077 mFactor = pFactor; 078 mOffset = pOffset; 079 // legacy format variables 080 mSecondCV = pSecondCV; 081 _uppermask = uppermask; 082 083 // connect to the JTextField value 084 _textField.addActionListener(this); 085 _textField.addFocusListener(this); 086 087 log.debug("Variable={};comment={};cvName={};cvNum={};stdname={}", _name, comment, cvName, _cvNum, stdname); 088 089 // upper bit offset includes lower bit offset, and MSB bits missing from upper part 090 log.debug("Variable={}; upper mask {} had offsetVal={} so upperbitoffset={}", _name, _uppermask, offsetVal(_uppermask), offsetVal(_uppermask)); 091 092 // set up array of used CVs 093 cvList = new ArrayList<>(); 094 095 List<String> nameList = CvUtil.expandCvList(_cvNum); // see if cvName needs expanding 096 if (nameList.isEmpty()) { 097 // primary CV 098 String tMask; 099 if (_maskArray != null && _maskArray.length == 1) { 100 log.debug("PrimaryCV mask={}", _maskArray[0]); 101 tMask = _maskArray[0]; 102 } else { 103 tMask = _mask; // mask supplied could be an empty string 104 } 105 cvList.add(new CvItem(_cvNum, tMask)); 106 107 if (pSecondCV != null && !pSecondCV.equals("")) { 108 cvList.add(new CvItem(pSecondCV, _uppermask)); 109 } 110 } else { 111 for (int i = 0; i < nameList.size(); i++) { 112 cvList.add(new CvItem(nameList.get(i), _maskArray[Math.min(i, _maskArray.length - 1)])); 113 // use last mask for all following CVs if fewer masks than the number of CVs listed were provided 114 log.debug("Added mask #{}: {}", i, _maskArray[Math.min(i, _maskArray.length - 1)]); 115 } 116 } 117 118 cvCount = cvList.size(); 119 120 for (int i = 0; i < cvCount; i++) { 121 cvList.get(i).startOffset = currentOffset; 122 String t = cvList.get(i).cvMask; 123 if (t.contains("V")) { 124 currentOffset = currentOffset + t.lastIndexOf("V") - t.indexOf("V") + 1; 125 } else { 126 log.error("Variable={};cvName={};cvMask={} is an invalid bitmask", _name, cvList.get(i).cvName, cvList.get(i).cvMask); 127 } 128 log.debug("Variable={};cvName={};cvMask={};startOffset={};currentOffset={}", _name, cvList.get(i).cvName, cvList.get(i).cvMask, cvList.get(i).startOffset, currentOffset); 129 130 // connect CV for notification 131 CvValue cv = _cvMap.get(cvList.get(i).cvName); 132 cvList.get(i).thisCV = cv; 133 } 134 135 stepTwoActions(); 136 137 _textField.setColumns(_columns); 138 139 // have to do when list is complete 140 for (int i = 0; i < cvCount; i++) { 141 cvList.get(i).thisCV.addPropertyChangeListener(this); 142 cvList.get(i).thisCV.setState(ValueState.FROMFILE); 143 } 144 } 145 146 /** 147 * Subclasses can override this to pick up constructor-specific attributes 148 * and perform other actions before cvList has been built. 149 * 150 * @param name name. 151 * @param comment comment. 152 * @param cvName cv name. 153 * @param readOnly true for read only, else false. 154 * @param infoOnly true for info only, else false. 155 * @param writeOnly true for write only, else false. 156 * @param opsOnly true for ops only, else false. 157 * @param cvNum cv number. 158 * @param mask cv mask. 159 * @param minVal minimum value. 160 * @param maxVal maximum value. 161 * @param v hashmap of string and cv value. 162 * @param status status. 163 * @param stdname std name. 164 * @param pSecondCV second cv (no longer preferred, specify in cv) 165 * @param pFactor factor. 166 * @param pOffset offset. 167 * @param uppermask upper mask (no longer preferred, specify in mask) 168 * @param extra1 extra 1. 169 * @param extra2 extra 2. 170 * @param extra3 extra 3. 171 * @param extra4 extra 4. 172 */ 173 public void stepOneActions(String name, String comment, String cvName, 174 boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, 175 String cvNum, String mask, int minVal, int maxVal, 176 HashMap<String, CvValue> v, JLabel status, String stdname, 177 String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) { 178 if (extra3 != null) { 179 _minVal = getValueFromText(extra3); 180 } 181 if (extra4 != null) { 182 _maxVal = getValueFromText(extra4); 183 } 184 } 185 186 /** 187 * Subclasses can override this to invoke further actions after cvList has 188 * been built. 189 */ 190 public void stepTwoActions() { 191 if (currentOffset > bitCount) { 192 String eol = System.getProperty("line.separator"); 193 throw new Error( 194 "Decoder File parsing error:" 195 + eol + "The Decoder Definition File specified \"" + _cvNum 196 + "\" for variable \"" + _name + "\". This expands to:" 197 + eol + "\"" + getCvDescription() + "\"" 198 + eol + "This requires " + currentOffset + " bits, which exceeds the " + bitCount 199 + " bit capacity of the long integer used to store the variable." 200 + eol + "The Decoder Definition File needs correction."); 201 } 202 _columns = cvCount * 2; //update column width now we have a better idea 203 } 204 205 @Override 206 public CvValue[] usesCVs() { 207 CvValue[] theseCvs = new CvValue[cvCount]; 208 for (int i = 0; i < cvCount; i++) { 209 theseCvs[i] = cvList.get(i).thisCV; 210 } 211 return theseCvs; 212 } 213 214 /** 215 * Multiple masks can be defined for the CVs accessed by this variable. 216 * <br> 217 * Actual individual masks are returned in 218 * {@link #getCvDescription getCvDescription()}. 219 * 220 * @return The legacy two-CV mask if {@code highCV} is specified. 221 * <br> 222 * The {@code mask} if {@code highCV} is not specified. 223 */ 224 @Override 225 public String getMask() { 226 if (mSecondCV != null && !mSecondCV.equals("")) { 227 return _uppermask + _mask; 228 } else { 229 return _mask; // a list of 1-n masks, separated by spaces 230 } 231 } 232 233 /** 234 * Access a specific mask, used in tests 235 * 236 * @param i index of CV in variable 237 * @return a single mask as string in the form XXXXVVVV, or empty string if 238 * index out of bounds 239 */ 240 protected String getMask(int i) { 241 if (i < cvCount) { 242 return cvList.get(i).cvMask; 243 } 244 return ""; 245 } 246 247 /** 248 * Provide a user-readable description of the CVs accessed by this variable. 249 * <br> 250 * Actual individual masks are added to CVs if more are present. 251 * 252 * @return A user-friendly CV(s) and bitmask(s) description 253 */ 254 @Override 255 public String getCvDescription() { 256 StringBuilder buf = new StringBuilder(); 257 for (int i = 0; i < cvCount; i++) { 258 if (buf.length() > 0) { 259 buf.append(" & "); 260 } 261 buf.append("CV"); 262 buf.append(cvList.get(i).cvName); 263 String temp = CvUtil.getMaskDescription(cvList.get(i).cvMask); 264 if (temp.length() > 0) { 265 buf.append(" "); 266 buf.append(temp); 267 } 268 } 269 buf.append("."); // mark that mask descriptions are already inserted for CvUtil.addCvDescription 270 return buf.toString(); 271 } 272 273 String mSecondCV; 274 String _uppermask; 275 int mFactor; 276 int mOffset; 277 String _name; 278 String _mask; // full string as provided, use _maskArray to access one of multiple masks 279 String[] _maskArray; 280 String _cvNum; 281 282 List<CvItem> cvList; 283 284 int cvCount; 285 int currentOffset = 0; 286 287 @Override 288 public String getCvNum() { 289 String retString = ""; 290 if (cvCount > 0) { 291 retString = cvList.get(0).cvName; 292 } 293 return retString; 294 } 295 296 @Override 297 public void setToolTipText(String t) { 298 super.setToolTipText(t); // do default stuff 299 _textField.setToolTipText(t); // set our value 300 } 301 302 // the connection is to cvNum and cvNum+1 303 long _minVal; 304 long _maxVal; 305 306 @Override 307 public Object rangeVal() { 308 return "Split value"; 309 } 310 311 String oldContents = "0"; 312 313 long getValueFromText(String s) { 314 return (Long.parseUnsignedLong(s)); 315 } 316 317 String getTextFromValue(long v) { 318 return (Long.toUnsignedString(v)); 319 } 320 321 int[] getCvValsFromTextField() { 322 long newEntry; // entered value 323 try { 324 newEntry = getValueFromText(_textField.getText()); 325 } catch (java.lang.NumberFormatException ex) { 326 newEntry = 0; 327 } 328 329 // calculate resulting number 330 long newVal = newEntry - mOffset; 331 // long newVal = Math.max(newEntry - mOffset, 0); // prevent negative values, especially in tests outside UI 332 if (mFactor != 0) { 333 newVal = newVal / mFactor; 334 } else { 335 log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction"); 336 } 337 log.debug("Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor); 338 339 int[] retVals = new int[cvCount]; 340 341 // extract individual values via masks 342 for (int i = 0; i < cvCount; i++) { 343 retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset)) 344 & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask))); 345 } 346 return retVals; 347 } 348 349 /** 350 * Contains numeric-value specific code. 351 * <br><br> 352 * Calculates new value for _textField and invokes 353 * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the 354 * change 355 * 356 * @param intVals array of new CV values 357 */ 358 void updateVariableValue(int[] intVals) { 359 360 long newVal = 0; 361 for (int i = 0; i < intVals.length; i++) { 362 newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset); 363 log.debug("Variable={}; i={}; newVal={}", _name, i, getTextFromValue(newVal)); 364 } 365 log.debug("Variable={}; set value to {}", _name, newVal); 366 setLongValue(newVal); // check for duplicate is done inside setLongValue 367 log.debug("Variable={}; in property change after setValue call", _name); 368 } 369 370 /** 371 * Saves contents of _textField to oldContents. 372 */ 373 void enterField() { 374 oldContents = _textField.getText(); 375 } 376 377 /** 378 * Contains numeric-value specific code. 379 * <br> 380 * firePropertyChange for "Value" with new and old contents of _textField 381 */ 382 void exitField() { 383 // there may be a lost focus event left in the queue when disposed so protect 384 if (_textField != null && !oldContents.equals(_textField.getText())) { 385 long newFieldVal = 0; 386 try { 387 newFieldVal = getValueFromText(_textField.getText()); 388 } catch (NumberFormatException e) { 389 _textField.setText(oldContents); 390 } 391 log.debug("_minVal={};_maxVal={};newFieldVal={}", 392 Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal)); 393 if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) { 394 _textField.setText(oldContents); 395 } else { 396 long newVal = (newFieldVal - mOffset) / mFactor; 397 long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor; 398 log.debug("Enter updatedTextField from exitField"); 399 updatedTextField(); 400 prop.firePropertyChange("Value", oldVal, newVal); 401 } 402 } 403 } 404 405 boolean _fieldShrink = false; 406 407 @Override 408 void updatedTextField() { 409 log.debug("Variable='{}'; enter updatedTextField in {} with TextField='{}'", _name, (this.getClass().getSimpleName()), _textField.getText()); 410 // called for new values in text field - set the CVs as needed 411 412 int[] retVals = getCvValsFromTextField(); 413 414 // combine with existing values via mask 415 for (int j = 0; j < cvCount; j++) { 416 int i = j; 417 // special care needed if _textField is shrinking 418 if (_fieldShrink) { 419 i = (cvCount - 1) - j; // reverse CV updating order 420 } 421 log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask)); 422 int cvMask = maskValAsInt(cvList.get(i).cvMask); 423 CvValue thisCV = cvList.get(i).thisCV; 424 int oldCvVal = thisCV.getValue(); 425 int newCvVal = (oldCvVal & ~cvMask) 426 | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask); 427 log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal); 428 429 // cv updates here trigger updated property changes, which means 430 // we're going to get notified sooner or later. 431 if (newCvVal != oldCvVal) { 432 thisCV.setValue(newCvVal); 433 } 434 } 435 log.debug("Variable={}; exit updatedTextField", _name); 436 } 437 438 /** 439 * ActionListener implementation. 440 * <p> 441 * Invokes {@link #exitField exitField()} 442 * 443 * @param e the action event 444 */ 445 @Override 446 public void actionPerformed(ActionEvent e) { 447 log.debug("Variable='{}'; actionPerformed", _name); 448 exitField(); 449 } 450 451 /** 452 * FocusListener implementations. 453 */ 454 @Override 455 public void focusGained(FocusEvent e) { 456 log.debug("Variable={}; focusGained", _name); 457 enterField(); 458 } 459 460 @Override 461 public void focusLost(FocusEvent e) { 462 log.debug("Variable={}; focusLost", _name); 463 exitField(); 464 } 465 466 // to complete this class, fill in the routines to handle "Value" parameter 467 // and to read/write/hear parameter changes. 468 @Override 469 public String getValueString() { 470 log.debug("getValueString {}", _textField.getText()); 471 return _textField.getText(); 472 } 473 474 /** 475 * Set value from a String value. 476 * 477 * @param value a string representing the Long value to be set 478 */ 479 @Override 480 public void setValue(String value) { 481 try { 482 long val = Long.parseUnsignedLong(value); 483 setLongValue(val); 484 } catch (NumberFormatException e) { 485 log.warn("skipping set of non-long value \"{}\"", value); 486 } 487 } 488 489 @Override 490 public void setIntValue(int i) { 491 setLongValue(i); 492 } 493 494 @Override 495 public int getIntValue() { 496 long x = getLongValue(); 497 long y = x & intMask; 498 if ((Long.compareUnsigned(x, y) != 0)) { 499 log.error("Value {} from textField {} cannot be converted to 'int'", x, _name); 500 } 501 return (int) ((getValueFromText(_textField.getText()) - mOffset) / mFactor); 502 } 503 504 /** 505 * Get the value as an unsigned long. 506 * 507 * @return the value as a long 508 */ 509 @Override 510 public long getLongValue() { 511 return ((getValueFromText(_textField.getText()) - mOffset) / mFactor); 512 } 513 514 @Override 515 public Object getValueObject() { 516 return getLongValue(); 517 } 518 519 @Override 520 public Component getCommonRep() { 521 if (getReadOnly()) { 522 JLabel r = new JLabel(_textField.getText()); 523 updateRepresentation(r); 524 return r; 525 } else { 526 return _textField; 527 } 528 } 529 530 public void setLongValue(long value) { 531 log.debug("Variable={}; enter setLongValue {}", _name, value); 532 long oldVal; 533 try { 534 oldVal = (getValueFromText(_textField.getText()) - mOffset) / mFactor; 535 } catch (java.lang.NumberFormatException ex) { 536 oldVal = -999; 537 } 538 log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal); 539 _textField.setText(getTextFromValue(value * mFactor + mOffset)); 540 if (oldVal != value || getState() == ValueState.UNKNOWN) { 541 actionPerformed(null); 542 } 543 // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation; 544 // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old) 545 // using different scales. Probably a bug, but it has been there from well before 546 // the extended splitVal. Because of the risk of breaking existing 547 // behaviour somewhere, deferring correction until at least the next test release. 548 prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset); 549 log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value); 550 } 551 552 Color _defaultColor; 553 554 // implement an abstract member to set colors 555 @Override 556 void setColor(Color c) { 557 if (c != null) { 558 _textField.setBackground(c); 559 log.debug("Variable={}; Set Color to {}", _name, c); 560 } else { 561 log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString()); 562 _textField.setBackground(_defaultColor); 563 } 564 // prop.firePropertyChange("Value", null, null); 565 } 566 567 int _columns = 1; 568 569 @Override 570 public Component getNewRep(String format) { 571 JTextField value = new VarTextField(_textField.getDocument(), _textField.getText(), _columns, this); 572 if (getReadOnly() || getInfoOnly()) { 573 value.setEditable(false); 574 } 575 reps.add(value); 576 return updateRepresentation(value); 577 } 578 579 @Override 580 public void setAvailable(boolean a) { 581 _textField.setVisible(a); 582 for (Component c : reps) { 583 c.setVisible(a); 584 } 585 super.setAvailable(a); 586 } 587 588 java.util.List<Component> reps = new java.util.ArrayList<>(); 589 590 private int retry = 0; 591 private int _progState = 0; 592 private static final int IDLE = 0; 593 private static final int READING_FIRST = 1; 594 private static final int WRITING_FIRST = -1; 595 private static final int bitCount = Long.bitCount(~0); 596 private static final long intMask = Integer.toUnsignedLong(~0); 597 598 /** 599 * Notify the connected CVs of a state change from above 600 * 601 * @param state The new state 602 */ 603 @Override 604 public void setCvState(ValueState state) { 605 for (int i = 0; i < cvCount; i++) { 606 cvList.get(i).thisCV.setState(state); 607 } 608 } 609 610 @Override 611 public boolean isChanged() { 612 boolean changed = false; 613 for (int i = 0; i < cvCount; i++) { 614 changed = (changed || considerChanged(cvList.get(i).thisCV)); 615 } 616 return changed; 617 } 618 619 @Override 620 public boolean isToRead() { 621 boolean toRead = false; 622 for (int i = 0; i < cvCount; i++) { 623 toRead = (toRead || (cvList.get(i).thisCV).isToRead()); 624 } 625 return toRead; 626 } 627 628 @Override 629 public boolean isToWrite() { 630 boolean toWrite = false; 631 for (int i = 0; i < cvCount; i++) { 632 toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite()); 633 } 634 return toWrite; 635 } 636 637 @Override 638 public void readChanges() { 639 if (isToRead() && !isChanged()) { 640 log.debug("!!!!!!! unacceptable combination in readChanges: {}", label()); 641 } 642 if (isChanged() || isToRead()) { 643 readAll(); 644 } 645 } 646 647 @Override 648 public void writeChanges() { 649 if (isToWrite() && !isChanged()) { 650 log.debug("!!!!!! unacceptable combination in writeChanges: {}", label()); 651 } 652 if (isChanged() || isToWrite()) { 653 writeAll(); 654 } 655 } 656 657 @Override 658 public void readAll() { 659 log.debug("Variable={}; splitVal read() invoked", _name); 660 setToRead(false); 661 setBusy(true); // will be reset when value changes 662 //super.setState(READ); 663 if (_progState != IDLE) { 664 log.warn("Variable={}; programming state {}, not IDLE, in read()", _name, _progState); 665 } 666 _textField.setText(""); // start with a clean slate 667 for (int i = 0; i < cvCount; i++) { // mark all Cvs as unknown otherwise problems occur 668 cvList.get(i).thisCV.setState(ValueState.UNKNOWN); 669 } 670 _progState = READING_FIRST; 671 retry = 0; 672 log.debug("Variable={}; Start CV read", _name); 673 log.debug("Reading CV={}", cvList.get(0).cvName); 674 (cvList.get(0).thisCV).read(_status); // kick off the read sequence 675 } 676 677 @Override 678 public void writeAll() { 679 log.debug("Variable={}; write() invoked", _name); 680 if (getReadOnly()) { 681 log.error("Variable={}; unexpected write operation when readOnly is set", _name); 682 } 683 setToWrite(false); 684 setBusy(true); // will be reset when value changes 685 if (_progState != IDLE) { 686 log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState); 687 } 688 _progState = WRITING_FIRST; 689 log.debug("Variable={}; Start CV write", _name); 690 log.debug("Writing CV={}", cvList.get(0).cvName); 691 (cvList.get(0).thisCV).write(_status); // kick off the write-sequence 692 } 693 694 /** 695 * Assigns a priority value to a given state. 696 * 697 * @param state State to be converted to a priority value 698 * @return Priority value from state, with UNKNOWN numerically highest 699 */ 700 @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value") 701 int priorityValue(ValueState state) { 702 int value = 0; 703 switch (state) { 704 case UNKNOWN: 705 value++; 706 //$FALL-THROUGH$ 707 case DIFFERENT: 708 value++; 709 //$FALL-THROUGH$ 710 case EDITED: 711 value++; 712 //$FALL-THROUGH$ 713 case FROMFILE: 714 value++; 715 //$FALL-THROUGH$ 716 default: 717 //$FALL-THROUGH$ 718 return value; 719 } 720 } 721 722 // handle incoming parameter notification 723 @Override 724 public void propertyChange(java.beans.PropertyChangeEvent e) { 725 log.debug("Variable={} source={}; property {} changed from {} to {}", _name, e.getSource().toString(), e.getPropertyName(), e.getOldValue(),e.getNewValue()); 726 // notification from CV; check for Value being changed 727 if (e.getPropertyName().equals("Busy") && e.getNewValue().equals(Boolean.FALSE)) { 728 // busy transitions drive the state 729 if (_progState != IDLE) { 730 log.debug("Variable={} source={}; getState() = {}", _name, e.getSource().toString(), (cvList.get(Math.abs(_progState) - 1).thisCV).getState()); 731 } 732 733 if (_progState == IDLE) { // State machine is idle, so "Busy" transition is the result of a CV update by another source. 734 // The source would be a Read/Write from either the CVs pane or another Variable with one or more overlapping CV(s). 735 // It is definitely not an error condition, but needs to be ignored by this variable's state machine. 736 log.debug("Variable={}; Busy goes false with _progState IDLE, so ignore by state machine", _name); 737 } else if (_progState >= READING_FIRST) { // reading CVs 738 if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.READ) { // was the last read successful? 739 retry = 0; 740 if (Math.abs(_progState) < cvCount) { // read next CV 741 _progState++; 742 log.debug("Reading CV={}", cvList.get(Math.abs(_progState) - 1).cvName); 743 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 744 } else { // finally done, set not busy 745 log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState); 746 _progState = IDLE; 747 setBusy(false); 748 } 749 } else { // read failed 750 log.debug("Variable={}; Busy goes false with failure READING _progState {}", _name, _progState); 751 if (retry < RETRY_COUNT) { //have we exhausted retry count? 752 retry++; 753 (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status); 754 } else { 755 _progState = IDLE; 756 setBusy(false); 757 if (RETRY_COUNT > 0) { 758 for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur 759 cvList.get(i).thisCV.setState(ValueState.UNKNOWN); 760 } 761 } 762 } 763 } 764 } else { // writing CVs 765 if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) { // was the last read successful? 766 if (Math.abs(_progState) < cvCount) { // write next CV 767 _progState--; 768 log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName); 769 (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status); 770 } else { // finally done, set not busy 771 log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState); 772 _progState = IDLE; 773 setBusy(false); 774 } 775 } else { // read failed we're done! 776 log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState); 777 _progState = IDLE; 778 setBusy(false); 779 } 780 } 781 } else if (e.getPropertyName().equals("State")) { 782 log.debug("Possible {} variable state change due to CV state change, so propagate that", _name); 783 ValueState varState = getState(); // AbstractValue.SAME; 784 log.debug("{} variable state was {}", _name, varState.getName()); 785 for (int i = 0; i < cvCount; i++) { 786 var state = cvList.get(i).thisCV.getState(); 787 if (i == 0) { 788 varState = state; 789 } else if (priorityValue(state) > priorityValue(varState)) { 790 //varState = AbstractValue.UNKNOWN; // or should it be = state ? 791 varState = state; // or should it be = state ? 792 } 793 } 794 setState(varState); 795 log.debug("{} variable state set to {}", _name, varState.getName()); 796 } else if (e.getPropertyName().equals("Value")) { 797 // update value of Variable 798 log.debug("update value of Variable {}", _name); 799 800 int[] intVals = new int[cvCount]; 801 802 for (int i = 0; i < cvCount; i++) { 803 intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask); 804 } 805 806 updateVariableValue(intVals); 807 808 log.debug("state change due to CV value change, so propagate that"); 809 ValueState varState = ValueState.SAME; 810 for (int i = 0; i < cvCount; i++) { 811 ValueState state = cvList.get(i).thisCV.getState(); 812 if (priorityValue(state) > priorityValue(varState)) { 813 varState = state; 814 } 815 } 816 setState(varState); 817 } 818 } 819 820 // stored reference to the JTextField 821 JTextField _textField; 822 823 /* Internal class extends a JTextField so that its color is consistent with 824 * an underlying variable 825 * 826 * @author Bob Jacobsen Copyright (C) 2001 827 * 828 */ 829 public class VarTextField extends JTextField { 830 831 VarTextField(Document doc, String text, int col, SplitVariableValue var) { 832 super(doc, text, col); 833 _var = var; 834 // get the original color right 835 setBackground(_var._textField.getBackground()); 836 // listen for changes to ourself 837 addActionListener(this::thisActionPerformed); 838 addFocusListener(new java.awt.event.FocusListener() { 839 @Override 840 public void focusGained(FocusEvent e) { 841 log.debug("Variable={}; focusGained", _name); 842 enterField(); 843 } 844 845 @Override 846 public void focusLost(FocusEvent e) { 847 log.debug("Variable={}; focusLost", _name); 848 exitField(); 849 } 850 }); 851 // listen for changes to original state 852 _var.addPropertyChangeListener(this::originalPropertyChanged); 853 } 854 855 SplitVariableValue _var; 856 857 void thisActionPerformed(java.awt.event.ActionEvent e) { 858 // tell original 859 _var.actionPerformed(e); 860 } 861 862 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 863 // update this color from original state 864 if (e.getPropertyName().equals("State")) { 865 setBackground(_var._textField.getBackground()); 866 } 867 } 868 869 } 870 871 /** 872 * Class to hold CV parameters for CVs used. 873 */ 874 static class CvItem { 875 876 // class fields 877 String cvName; 878 String cvMask; 879 int startOffset; 880 CvValue thisCV; 881 882 CvItem(String cvNameVal, String cvMaskVal) { 883 cvName = cvNameVal; 884 cvMask = cvMaskVal; 885 } 886 } 887 888 // clean up connections when done 889 @Override 890 public void dispose() { 891 log.debug("dispose"); 892 if (_textField != null) { 893 _textField.removeActionListener(this); 894 } 895 for (int i = 0; i < cvCount; i++) { 896 (_cvMap.get(cvList.get(i).cvName)).removePropertyChangeListener(this); 897 } 898 899 _textField = null; 900 _maskArray = null; 901 // do something about the VarTextField 902 } 903 904 // initialize logging 905 private final static Logger log = LoggerFactory.getLogger(SplitVariableValue.class); 906 907}