001package jmri.jmrit.symbolicprog; 002 003import java.awt.Color; 004import java.awt.Component; 005import java.awt.event.ActionEvent; 006import java.awt.event.ActionListener; 007import java.awt.event.FocusEvent; 008import java.awt.event.FocusListener; 009import java.util.ArrayList; 010import java.util.HashMap; 011import java.util.Hashtable; 012import javax.swing.JLabel; 013import javax.swing.JSlider; 014import javax.swing.JTextField; 015import javax.swing.text.Document; 016 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020/** 021 * Decimal representation of a value. 022 * <br> 023 * The {@code mask} attribute represents the part of the value that's present in 024 * the CV. 025 * <br> 026 * Optional attributes {@code factor} and {@code offset} are applied when going 027 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa: 028 * <pre> 029 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor} 030 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset} 031 * </pre> * 032 * 033 * @author Bob Jacobsen Copyright (C) 2001, 2022 034 */ 035public class DecVariableValue extends VariableValue 036 implements ActionListener, FocusListener { 037 038 public DecVariableValue(String name, String comment, String cvName, boolean readOnly, boolean infoOnly, 039 boolean writeOnly, boolean opsOnly, String cvNum, String mask, int minVal, int maxVal, 040 HashMap<String, CvValue> v, JLabel status, String stdname) { 041 this(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, 042 v, status, stdname, 0, 1); 043 } 044 045 public DecVariableValue(String name, String comment, String cvName, boolean readOnly, boolean infoOnly, 046 boolean writeOnly, boolean opsOnly, String cvNum, String mask, int minVal, int maxVal, 047 HashMap<String, CvValue> v, JLabel status, String stdname, int offset, int factor) { 048 super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname); 049 _maxVal = maxVal; 050 _minVal = minVal; 051 _offset = offset; 052 _factor = factor; 053 _value = new JTextField("0", fieldLength()); 054 _value.getAccessibleContext().setAccessibleName(label()); 055 _defaultColor = _value.getBackground(); 056 _value.setBackground(ValueState.UNKNOWN.getColor()); 057 // connect to the JTextField value, cv 058 _value.addActionListener(this); 059 _value.addFocusListener(this); 060 CvValue cv = _cvMap.get(getCvNum()); 061 cv.addPropertyChangeListener(this); 062 cv.setState(ValueState.FROMFILE); 063 simplifyMask(); 064 } 065 066 @Override 067 public void setToolTipText(String t) { 068 super.setToolTipText(t); // do default stuff 069 _value.setToolTipText(t); // set our value 070 } 071 072 int _maxVal; 073 int _minVal; 074 int _offset; 075 int _factor; 076 077 int fieldLength() { 078 if (_maxVal <= 255) { 079 return 3; 080 } 081 return (int) Math.ceil(Math.log10(_maxVal)) + 1; 082 } 083 084 @Override 085 public CvValue[] usesCVs() { 086 return new CvValue[]{_cvMap.get(getCvNum())}; 087 } 088 089 @Override 090 public Object rangeVal() { 091 return "Decimal: " + _minVal + " - " + _maxVal; 092 } 093 094 String oldContents = ""; 095 096 void enterField() { 097 oldContents = _value.getText(); 098 } 099 100 int textToValue(String s) { 101 return (Integer.parseInt(s)); 102 } 103 104 String valueToText(int v) { 105 return (Integer.toString(v)); 106 } 107 108 void exitField() { 109 if (_value == null) { 110 // There's no value Object yet, so just ignore & exit 111 return; 112 } 113 // what to do for the case where _value != null? 114 if (!_value.getText().equals("")) { 115 // there may be a lost focus event left in the queue when disposed, so protect 116 if (!oldContents.equals(_value.getText())) { 117 try { 118 int newVal = textToValue(_value.getText()); 119 int oldVal = textToValue(oldContents); 120 if (newVal < _minVal || newVal > _maxVal) { 121 _value.setText(oldContents); 122 } else { 123 updatedTextField(); 124 prop.firePropertyChange("Value", oldVal, newVal); 125 } 126 } catch (java.lang.NumberFormatException ex) { 127 _value.setText(oldContents); 128 } 129 } 130 } else { 131 // As the user has left the contents blank, we shall re-instate the old value as, 132 // when a write operation to decoder is performed, the cv remains the same value. 133 _value.setText(oldContents); 134 } 135 } 136 137 /** 138 * Invoked when a permanent change to the JTextField has been made. Note 139 * that this does _not_ notify property listeners; that should be done by 140 * the invoker, who may or may not know what the old value was. Can be 141 * overridden in subclasses that want to display the value differently. 142 */ 143 @Override 144 void updatedTextField() { 145 log.debug("updatedTextField"); 146 // called for new values - set the CV as needed 147 CvValue cv = _cvMap.get(getCvNum()); 148 // compute new cv value by combining old and request 149 int oldCvVal = cv.getValue(); 150 int newVal; 151 try { 152 newVal = textToValue(_value.getText()); 153 } catch (java.lang.NumberFormatException ex) { 154 newVal = 0; 155 } 156 157 newVal = newVal - _offset; 158 if (_factor != 0) { 159 newVal = newVal / _factor; 160 } else { 161 // ignore division 162 log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction"); 163 } 164 165 int newCvVal = setValueInCV(oldCvVal, newVal, getMask(), _maxVal); 166 log.debug("newVal={} newCvVal ={}", newVal, newCvVal); 167 if (oldCvVal != newCvVal) { 168 cv.setValue(newCvVal); 169 } 170 } 171 172 /** 173 * ActionListener implementations 174 */ 175 @Override 176 public void actionPerformed(ActionEvent e) { 177 log.debug("actionPerformed"); 178 try { 179 int newVal = textToValue(_value.getText()); 180 if (newVal < _minVal || newVal > _maxVal) { 181 _value.setText(oldContents); 182 } else { 183 updatedTextField(); 184 prop.firePropertyChange("Value", null, newVal); 185 } 186 } catch (java.lang.NumberFormatException ex) { 187 _value.setText(oldContents); 188 } 189 } 190 191 /** 192 * FocusListener implementations 193 */ 194 @Override 195 public void focusGained(FocusEvent e) { 196 log.debug("focusGained"); 197 enterField(); 198 } 199 200 @Override 201 public void focusLost(FocusEvent e) { 202 log.debug("focusLost"); 203 exitField(); 204 } 205 206 // to complete this class, fill in the routines to handle "Value" parameter 207 // and to read/write/hear parameter changes. 208 @Override 209 public String getValueString() { 210 return _value.getText(); 211 } 212 213 @Override 214 public void setIntValue(int i) { 215 setValue(i); 216 } 217 218 @Override 219 public int getIntValue() { 220 return textToValue(_value.getText()); 221 } 222 223 @Override 224 public Object getValueObject() { 225 return Integer.valueOf(_value.getText()); 226 } 227 228 @Override 229 public Component getCommonRep() { 230 if (getReadOnly()) { 231 JLabel r = new JLabel(_value.getText()); 232 reps.add(r); 233 updateRepresentation(r); 234 return r; 235 } else { 236 return _value; 237 } 238 } 239 240 @Override 241 public void setAvailable(boolean a) { 242 _value.setVisible(a); 243 for (Component c : reps) { 244 c.setVisible(a); 245 } 246 super.setAvailable(a); 247 } 248 249 java.util.List<Component> reps = new java.util.ArrayList<>(); 250 251 @Override 252 public Component getNewRep(String format) { 253 switch (format) { 254 case "vslider": { 255 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 256 b.setOrientation(JSlider.VERTICAL); 257 sliders.add(b); 258 reps.add(b); 259 updateRepresentation(b); 260 return b; 261 } 262 case "hslider": { 263 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 264 b.setOrientation(JSlider.HORIZONTAL); 265 sliders.add(b); 266 reps.add(b); 267 updateRepresentation(b); 268 return b; 269 } 270 case "hslider-percent": { 271 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 272 b.setOrientation(JSlider.HORIZONTAL); 273 if (_maxVal > 20) { 274 b.setMajorTickSpacing(_maxVal / 2); 275 b.setMinorTickSpacing((_maxVal + 1) / 8); 276 } else { 277 b.setMajorTickSpacing(5); 278 b.setMinorTickSpacing(1); // because JSlider does not SnapToValue 279 b.setSnapToTicks(true); // like it should, we fake it here 280 } 281 b.setSize(b.getWidth(), 28); 282 Hashtable<Integer, JLabel> labelTable = new Hashtable<>(); 283 labelTable.put(0, new JLabel("0%")); 284 if (_maxVal == 63) { // this if for the QSI mute level, not very universal, needs work 285 labelTable.put(_maxVal / 2, new JLabel("25%")); 286 labelTable.put(_maxVal, new JLabel("50%")); 287 } else { 288 labelTable.put(_maxVal / 2, new JLabel("50%")); 289 labelTable.put(_maxVal, new JLabel("100%")); 290 } 291 b.setLabelTable(labelTable); 292 b.setPaintTicks(true); 293 b.setPaintLabels(true); 294 sliders.add(b); 295 updateRepresentation(b); 296 if (!getAvailable()) { 297 b.setVisible(false); 298 } 299 return b; 300 } 301 default: 302 JTextField value = new VarTextField(_value.getDocument(), _value.getText(), fieldLength(), this); 303 if (getReadOnly() || getInfoOnly()) { 304 value.setEditable(false); 305 } 306 reps.add(value); 307 updateRepresentation(value); 308 return value; 309 } 310 } 311 312 ArrayList<DecVarSlider> sliders = new ArrayList<>(); 313 314 /** 315 * Set a new value in the variable (text box), including notification as needed. 316 * <p> 317 * This does the conversion from string to int, so it's the place where 318 * formatting needs to be applied. 319 * @param value new value. 320 */ 321 public void setValue(int value) { 322 int oldVal; 323 try { 324 oldVal = textToValue(_value.getText()); 325 } catch (java.lang.NumberFormatException ex) { 326 oldVal = -999; 327 } 328 329 if (value < _minVal) value = _minVal; 330 if (value > _maxVal) value = _maxVal; 331 log.debug("setValue with new value {} old value {}", value, oldVal); 332 if (oldVal != value) { 333 _value.setText(valueToText(value)); 334 updatedTextField(); 335 prop.firePropertyChange("Value", Integer.valueOf(oldVal), Integer.valueOf(value)); 336 } 337 } 338 339 Color _defaultColor; 340 341 // implement an abstract member to set colors 342 Color getDefaultColor() { 343 return _defaultColor; 344 } 345 346 Color getColor() { 347 return _value.getBackground(); 348 } 349 350 @Override 351 void setColor(Color c) { 352 if (c != null) { 353 _value.setBackground(c); 354 } else { 355 _value.setBackground(_defaultColor); 356 } 357 // prop.firePropertyChange("Value", null, null); 358 } 359 360 /** 361 * Notify the connected CVs of a state change from above 362 * 363 */ 364 @Override 365 public void setCvState(ValueState state) { 366 _cvMap.get(getCvNum()).setState(state); 367 } 368 369 @Override 370 public boolean isChanged() { 371 CvValue cv = _cvMap.get(getCvNum()); 372 log.debug("isChanged for {} state {}", getCvNum(), cv.getState()); 373 return considerChanged(cv); 374 } 375 376 @Override 377 public void readChanges() { 378 if (isChanged()) { 379 readAll(); 380 } 381 } 382 383 @Override 384 public void writeChanges() { 385 if (isChanged()) { 386 writeAll(); 387 } 388 } 389 390 @Override 391 public void readAll() { 392 setToRead(false); 393 setBusy(true); // will be reset when value changes 394 //super.setState(READ); 395 _cvMap.get(getCvNum()).read(_status); 396 } 397 398 @Override 399 public void writeAll() { 400 setToWrite(false); 401 if (getReadOnly()) { 402 log.error("unexpected write operation when readOnly is set"); 403 } 404 setBusy(true); // will be reset when value changes 405 _cvMap.get(getCvNum()).write(_status); 406 } 407 408 // handle incoming parameter notification 409 @Override 410 public void propertyChange(java.beans.PropertyChangeEvent e) { 411 // notification from CV; check for Value being changed 412 if (log.isDebugEnabled()) { 413 log.debug("Property changed: {}", e.getPropertyName()); 414 } 415 if (e.getPropertyName().equals("Busy")) { 416 if (e.getNewValue().equals(Boolean.FALSE)) { 417 setToRead(false); 418 setToWrite(false); // some programming operation just finished 419 setBusy(false); 420 } 421 } else if (e.getPropertyName().equals("State")) { 422 CvValue cv = _cvMap.get(getCvNum()); 423 if (cv.getState() == ValueState.STORED) { 424 setToWrite(false); 425 } 426 if (cv.getState() == ValueState.READ) { 427 setToRead(false); 428 } 429 setState(cv.getState()); 430 } else if (e.getPropertyName().equals("Value")) { 431 // update value of Variable 432 CvValue cv = _cvMap.get(getCvNum()); 433 int transfer = getValueInCV(cv.getValue(), getMask(), _maxVal); 434 435 int newVal = (transfer * _factor) + _offset; 436 437 // handle possible negative value 438 if (_minVal < 0 && newVal > _maxVal) { 439 // here a 2's-complement variable, find the sign bit in the value 440 int signBit = signBit(getMask()); 441 // sign extend the value 442 newVal = (newVal ^ signBit) - signBit; 443 } 444 445 setValue(newVal); // check for duplicate done inside setValue 446 } 447 } 448 449 // find the sign bit for a masked field 450 // e.g. sign bit of XXXXVVVV is 0b00001000 451 // and sign bit of XXVVVXXX is 0b00000100 452 int signBit(String mask) { 453 int shift = offsetVal(mask); 454 int oneBits = maskValAsInt(mask)>>shift; 455 int firstBit = 31 - Integer.numberOfLeadingZeros(oneBits); 456 return 1<<firstBit; 457 458 } 459 460 461 // stored value, read-only Value 462 JTextField _value; 463 464 /* Internal class extends a JTextField so that its color is consistent with 465 * an underlying variable 466 * 467 * @author Bob Jacobsen Copyright (C) 2001 468 */ 469 public class VarTextField extends JTextField { 470 471 VarTextField(Document doc, String text, int col, DecVariableValue var) { 472 super(doc, text, col); 473 _var = var; 474 // get the original color right 475 setBackground(_var._value.getBackground()); 476 // listen for changes to ourself 477 addActionListener(this::thisActionPerformed); 478 addFocusListener(new java.awt.event.FocusListener() { 479 @Override 480 public void focusGained(FocusEvent e) { 481 log.debug("focusGained"); 482 enterField(); 483 } 484 485 @Override 486 public void focusLost(FocusEvent e) { 487 log.debug("focusLost"); 488 exitField(); 489 } 490 }); 491 // listen for changes to original state 492 _var.addPropertyChangeListener(this::originalPropertyChanged); 493 } 494 495 DecVariableValue _var; 496 497 void thisActionPerformed(java.awt.event.ActionEvent e) { 498 // tell original 499 _var.actionPerformed(e); 500 } 501 502 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 503 // update this color from original state 504 if (e.getPropertyName().equals("State")) { 505 setBackground(_var._value.getBackground()); 506 } 507 } 508 509 } 510 511 // clean up connections when done 512 @Override 513 public void dispose() { 514 log.debug("dispose"); 515 if (_value != null) { 516 _value.removeActionListener(this); 517 } 518 _cvMap.get(getCvNum()).removePropertyChangeListener(this); 519 520 _value = null; 521 // do something about the VarTextField 522 } 523 524 // initialize logging 525 private final static Logger log = LoggerFactory.getLogger(DecVariableValue.class); 526 527}