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 int transfer = Math.max(newVal - _offset, 0); // prevent negative values, especially in tests outside UI 157 if (_factor != 0) { 158 transfer = transfer / _factor; 159 } else { 160 // ignore division 161 log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction"); 162 } 163 int newCvVal = setValueInCV(oldCvVal, transfer, getMask(), _maxVal); 164 log.debug("newVal={} transfer={} newCvVal ={}", newVal, transfer, newCvVal); 165 if (oldCvVal != newCvVal) { 166 cv.setValue(newCvVal); 167 } 168 } 169 170 /** 171 * ActionListener implementations 172 */ 173 @Override 174 public void actionPerformed(ActionEvent e) { 175 log.debug("actionPerformed"); 176 try { 177 int newVal = textToValue(_value.getText()); 178 if (newVal < _minVal || newVal > _maxVal) { 179 _value.setText(oldContents); 180 } else { 181 updatedTextField(); 182 prop.firePropertyChange("Value", null, newVal); 183 } 184 } catch (java.lang.NumberFormatException ex) { 185 _value.setText(oldContents); 186 } 187 } 188 189 /** 190 * FocusListener implementations 191 */ 192 @Override 193 public void focusGained(FocusEvent e) { 194 log.debug("focusGained"); 195 enterField(); 196 } 197 198 @Override 199 public void focusLost(FocusEvent e) { 200 log.debug("focusLost"); 201 exitField(); 202 } 203 204 // to complete this class, fill in the routines to handle "Value" parameter 205 // and to read/write/hear parameter changes. 206 @Override 207 public String getValueString() { 208 return _value.getText(); 209 } 210 211 @Override 212 public void setIntValue(int i) { 213 setValue(i); 214 } 215 216 @Override 217 public int getIntValue() { 218 return textToValue(_value.getText()); 219 } 220 221 @Override 222 public Object getValueObject() { 223 return Integer.valueOf(_value.getText()); 224 } 225 226 @Override 227 public Component getCommonRep() { 228 if (getReadOnly()) { 229 JLabel r = new JLabel(_value.getText()); 230 reps.add(r); 231 updateRepresentation(r); 232 return r; 233 } else { 234 return _value; 235 } 236 } 237 238 @Override 239 public void setAvailable(boolean a) { 240 _value.setVisible(a); 241 for (Component c : reps) { 242 c.setVisible(a); 243 } 244 super.setAvailable(a); 245 } 246 247 java.util.List<Component> reps = new java.util.ArrayList<>(); 248 249 @Override 250 public Component getNewRep(String format) { 251 switch (format) { 252 case "vslider": { 253 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 254 b.setOrientation(JSlider.VERTICAL); 255 sliders.add(b); 256 reps.add(b); 257 updateRepresentation(b); 258 return b; 259 } 260 case "hslider": { 261 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 262 b.setOrientation(JSlider.HORIZONTAL); 263 sliders.add(b); 264 reps.add(b); 265 updateRepresentation(b); 266 return b; 267 } 268 case "hslider-percent": { 269 DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal); 270 b.setOrientation(JSlider.HORIZONTAL); 271 if (_maxVal > 20) { 272 b.setMajorTickSpacing(_maxVal / 2); 273 b.setMinorTickSpacing((_maxVal + 1) / 8); 274 } else { 275 b.setMajorTickSpacing(5); 276 b.setMinorTickSpacing(1); // because JSlider does not SnapToValue 277 b.setSnapToTicks(true); // like it should, we fake it here 278 } 279 b.setSize(b.getWidth(), 28); 280 Hashtable<Integer, JLabel> labelTable = new Hashtable<>(); 281 labelTable.put(0, new JLabel("0%")); 282 if (_maxVal == 63) { // this if for the QSI mute level, not very universal, needs work 283 labelTable.put(_maxVal / 2, new JLabel("25%")); 284 labelTable.put(_maxVal, new JLabel("50%")); 285 } else { 286 labelTable.put(_maxVal / 2, new JLabel("50%")); 287 labelTable.put(_maxVal, new JLabel("100%")); 288 } 289 b.setLabelTable(labelTable); 290 b.setPaintTicks(true); 291 b.setPaintLabels(true); 292 sliders.add(b); 293 updateRepresentation(b); 294 if (!getAvailable()) { 295 b.setVisible(false); 296 } 297 return b; 298 } 299 default: 300 JTextField value = new VarTextField(_value.getDocument(), _value.getText(), fieldLength(), this); 301 if (getReadOnly() || getInfoOnly()) { 302 value.setEditable(false); 303 } 304 reps.add(value); 305 updateRepresentation(value); 306 return value; 307 } 308 } 309 310 ArrayList<DecVarSlider> sliders = new ArrayList<>(); 311 312 /** 313 * Set a new value in the variable (text box), including notification as needed. 314 * <p> 315 * This does the conversion from string to int, so it's the place where 316 * formatting needs to be applied. 317 * @param value new value. 318 */ 319 public void setValue(int value) { 320 int oldVal; 321 try { 322 oldVal = textToValue(_value.getText()); 323 } catch (java.lang.NumberFormatException ex) { 324 oldVal = -999; 325 } 326 if (value < _minVal) value = _minVal; 327 if (value > _maxVal) value = _maxVal; 328 log.debug("setValue with new value {} old value {}", value, oldVal); 329 if (oldVal != value) { 330 _value.setText(valueToText(value)); 331 updatedTextField(); 332 prop.firePropertyChange("Value", Integer.valueOf(oldVal), Integer.valueOf(value)); 333 } 334 } 335 336 Color _defaultColor; 337 338 // implement an abstract member to set colors 339 Color getDefaultColor() { 340 return _defaultColor; 341 } 342 343 Color getColor() { 344 return _value.getBackground(); 345 } 346 347 @Override 348 void setColor(Color c) { 349 if (c != null) { 350 _value.setBackground(c); 351 } else { 352 _value.setBackground(_defaultColor); 353 } 354 // prop.firePropertyChange("Value", null, null); 355 } 356 357 /** 358 * Notify the connected CVs of a state change from above 359 * 360 */ 361 @Override 362 public void setCvState(ValueState state) { 363 _cvMap.get(getCvNum()).setState(state); 364 } 365 366 @Override 367 public boolean isChanged() { 368 CvValue cv = _cvMap.get(getCvNum()); 369 log.debug("isChanged for {} state {}", getCvNum(), cv.getState()); 370 return considerChanged(cv); 371 } 372 373 @Override 374 public void readChanges() { 375 if (isChanged()) { 376 readAll(); 377 } 378 } 379 380 @Override 381 public void writeChanges() { 382 if (isChanged()) { 383 writeAll(); 384 } 385 } 386 387 @Override 388 public void readAll() { 389 setToRead(false); 390 setBusy(true); // will be reset when value changes 391 //super.setState(READ); 392 _cvMap.get(getCvNum()).read(_status); 393 } 394 395 @Override 396 public void writeAll() { 397 setToWrite(false); 398 if (getReadOnly()) { 399 log.error("unexpected write operation when readOnly is set"); 400 } 401 setBusy(true); // will be reset when value changes 402 _cvMap.get(getCvNum()).write(_status); 403 } 404 405 // handle incoming parameter notification 406 @Override 407 public void propertyChange(java.beans.PropertyChangeEvent e) { 408 // notification from CV; check for Value being changed 409 if (log.isDebugEnabled()) { 410 log.debug("Property changed: {}", e.getPropertyName()); 411 } 412 if (e.getPropertyName().equals("Busy")) { 413 if (e.getNewValue().equals(Boolean.FALSE)) { 414 setToRead(false); 415 setToWrite(false); // some programming operation just finished 416 setBusy(false); 417 } 418 } else if (e.getPropertyName().equals("State")) { 419 CvValue cv = _cvMap.get(getCvNum()); 420 if (cv.getState() == ValueState.STORED) { 421 setToWrite(false); 422 } 423 if (cv.getState() == ValueState.READ) { 424 setToRead(false); 425 } 426 setState(cv.getState()); 427 } else if (e.getPropertyName().equals("Value")) { 428 // update value of Variable 429 CvValue cv = _cvMap.get(getCvNum()); 430 int transfer = getValueInCV(cv.getValue(), getMask(), _maxVal); 431 int newVal = (transfer * _factor) + _offset; 432 setValue(newVal); // check for duplicate done inside setValue 433 } 434 } 435 436 // stored value, read-only Value 437 JTextField _value; 438 439 /* Internal class extends a JTextField so that its color is consistent with 440 * an underlying variable 441 * 442 * @author Bob Jacobsen Copyright (C) 2001 443 */ 444 public class VarTextField extends JTextField { 445 446 VarTextField(Document doc, String text, int col, DecVariableValue var) { 447 super(doc, text, col); 448 _var = var; 449 // get the original color right 450 setBackground(_var._value.getBackground()); 451 // listen for changes to ourself 452 addActionListener(this::thisActionPerformed); 453 addFocusListener(new java.awt.event.FocusListener() { 454 @Override 455 public void focusGained(FocusEvent e) { 456 log.debug("focusGained"); 457 enterField(); 458 } 459 460 @Override 461 public void focusLost(FocusEvent e) { 462 log.debug("focusLost"); 463 exitField(); 464 } 465 }); 466 // listen for changes to original state 467 _var.addPropertyChangeListener(this::originalPropertyChanged); 468 } 469 470 DecVariableValue _var; 471 472 void thisActionPerformed(java.awt.event.ActionEvent e) { 473 // tell original 474 _var.actionPerformed(e); 475 } 476 477 void originalPropertyChanged(java.beans.PropertyChangeEvent e) { 478 // update this color from original state 479 if (e.getPropertyName().equals("State")) { 480 setBackground(_var._value.getBackground()); 481 } 482 } 483 484 } 485 486 // clean up connections when done 487 @Override 488 public void dispose() { 489 log.debug("dispose"); 490 if (_value != null) { 491 _value.removeActionListener(this); 492 } 493 _cvMap.get(getCvNum()).removePropertyChangeListener(this); 494 495 _value = null; 496 // do something about the VarTextField 497 } 498 499 // initialize logging 500 private final static Logger log = LoggerFactory.getLogger(DecVariableValue.class); 501 502}