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