001package jmri.jmrit.symbolicprog; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.awt.Component; 005import java.awt.event.ActionEvent; 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.HashSet; 009import java.util.Hashtable; 010import java.util.Iterator; 011import javax.swing.JComboBox; 012import javax.swing.JLabel; 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016/** 017 * Extends EnumVariableValue to represent a composition of variable values. 018 * <p> 019 * Internally, each "choice" is stored as a list of "setting" items. Numerical 020 * values for this type of variable (itself) are strictly sequential, because 021 * they are arbitrary. 022 * <p> 023 * This version of the class has certain limitations: 024 * <ol> 025 * <li>Variables referenced in the definition of one of these must have already 026 * been declared earlier in the decoder file. This prevents circular references, 027 * and makes it much easier to find the target variables. 028 * <li> 029 * This version of the variable never changes "State" (color), though it does 030 * track its value from changes to other variables. 031 * <li>There should be a final choice (entry) that doesn't define any settings. 032 * This will then form the default value when the target variables change. 033 * <li>Programming operations on a variable of this type doesn't do anything, 034 * because there doesn't seem to be a consistent model of what "read changes" 035 * and "write changes" should do. This has two implications: 036 * <ul> 037 * <li>Variables referenced as targets must appear on some programming pane, or 038 * they won't be updated by programming operations. 039 * <li>If this variable references variables that are not on this pane, the user 040 * needs to do a read/write all panes operation to record the changes made to 041 * this variable. 042 * </ul> 043 * It's therefore recommended that a CompositeVariableValue just make changes to 044 * target variables on the same programming page. 045 * <li>To apply a mask when setting a value, use an intermediary variable set 046 * from here, which in turn references the goal variable with a mask. 047 * </ol> 048 * 049 * @author Bob Jacobsen Copyright (C) 2001, 2005, 2013 050 */ 051public class CompositeVariableValue extends EnumVariableValue { 052 053 public CompositeVariableValue(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 super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname); 058 _maxVal = maxVal; 059 _minVal = minVal; 060 061 _value = new JComboBox<String>(); 062 _value.getAccessibleContext().setAccessibleName(label()); 063 064 log.debug("New Composite named {}", name); 065 } 066 067 /** 068 * Create a null object. Normally only used for tests and to pre-load 069 * classes. 070 */ 071 public CompositeVariableValue() { 072 _value = new JComboBox<String>(); 073 _value.getAccessibleContext().setAccessibleName(label()); 074 } 075 076 @Override 077 public CvValue[] usesCVs() { 078 HashSet<CvValue> cvSet = new HashSet<CvValue>(20); // 20 is arbitrary 079 for (VariableValue v : variables) { 080 CvValue[] cvs = v.usesCVs(); 081 for (int k = 0; k < cvs.length; k++) { 082 cvSet.add(cvs[k]); 083 } 084 } 085 CvValue[] retval = new CvValue[cvSet.size()]; 086 Iterator<CvValue> j = cvSet.iterator(); 087 int index = 0; 088 while (j.hasNext()) { 089 retval[index++] = j.next(); 090 } 091 return retval; 092 } 093 094 /** 095 * Define objects to save and manipulate a particular setting. 096 */ 097 static class Setting { 098 099 String varName; 100 VariableValue variable; 101 int value; 102 103 Setting(String varName, VariableValue variable, String value) { 104 this.varName = varName; 105 this.variable = variable; 106 try { 107 this.value = Integer.parseInt(value); 108 } catch (NullPointerException | NumberFormatException e) { 109 log.error("Illegal value received for CompositeVariable {}. Should be int but was {}", varName, value); 110 return; 111 } 112 log.debug(" cTor Setting {} = {}", varName, value); 113 } 114 115 void setValue() { 116 log.debug(" Setting.setValue of {} to {}", varName, value); 117 if (variable == null) { 118 log.error("Variable {} not (yet) created. Verify correct compositeSetting", varName); 119 return; 120 } 121 variable.setIntValue(value); 122 } 123 124 boolean match() { 125 if (log.isDebugEnabled()) { 126 log.debug(" Match checks {} == {}", variable.getIntValue(), value); 127 } 128 if (variable == null) { 129 log.error("Variable {} not (yet) created. Verify correct compositeSetting", varName); 130 return false; 131 } 132 return (variable.getIntValue() == value); 133 } 134 } 135 136 /** 137 * Defines a list of Setting objects. 138 * <p> 139 * Serves as a home for various service methods 140 */ 141 static class SettingList extends ArrayList<Setting> { 142 143 public SettingList() { 144 super(); 145 log.debug("New setting list"); 146 } 147 148 void addSetting(String varName, VariableValue variable, String value) { 149 Setting s = new Setting(varName, variable, value); 150 add(s); 151 } 152 153 void setValues() { 154 if (log.isDebugEnabled()) { 155 log.debug(" setValues in length {}", size()); 156 } 157 for (int i = 0; i < this.size(); i++) { 158 Setting s = this.get(i); 159 s.setValue(); 160 } 161 } 162 163 boolean match() { 164 for (int i = 0; i < size(); i++) { 165 if (!this.get(i).match()) { 166 if (log.isDebugEnabled()) { 167 log.debug(" No match in setting list of length {} at position {}", size(), i); 168 } 169 return false; 170 } 171 } 172 if (log.isDebugEnabled()) { 173 log.debug(" Match in setting list of length {}", size()); 174 } 175 return true; 176 } 177 } 178 179 Hashtable<String, SettingList> choiceHash = new Hashtable<String, SettingList>(); 180 HashSet<VariableValue> variables = new HashSet<VariableValue>(20); // VariableValue; 20 is an arbitrary guess 181 182 /** 183 * Create a new possible selection. 184 * 185 * @param name Name of the choice being added 186 */ 187 public void addChoice(String name) { 188 SettingList l = new SettingList(); 189 choiceHash.put(name, l); 190 _value.addItem(name); 191 } 192 193 /** 194 * Add a setting to an existing choice. 195 * @param choice existing choice. 196 * @param varName variable name. 197 * @param variable variable value. 198 * @param value setting value. 199 */ 200 public void addSetting(String choice, String varName, VariableValue variable, String value) { 201 SettingList s = choiceHash.get(choice); 202 s.addSetting(varName, variable, value); 203 204 if (variable != null) { 205 variables.add(variable); 206 if (!variable.label().equals(varName)) { 207 log.warn("Unexpected label /{}/ for varName /{}/ during addSetting", variable.label(), varName); 208 } 209 } else { 210 log.error("Variable pointer null when varName={} in choice {}; ignored", varName, choice); 211 } 212 } 213 214 /** 215 * Do end of initialization processing. 216 */ 217 @SuppressWarnings("null") 218 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH", 219 justification = "we want to force an exception") 220 @Override 221 public void lastItem() { 222 // configure the representation object 223 _defaultColor = _value.getBackground(); 224 super.setState(ValueState.READ); 225 226 // note that we don't set this to COLOR_UNKNOWN! Rather, 227 // we check the current value 228 findValue(); 229 230 // connect to all variables to hear changes 231 Iterator<VariableValue> i = variables.iterator(); 232 while (i.hasNext()) { 233 VariableValue v = i.next(); 234 if (v == null) { 235 log.error("Variable found as null in last item"); 236 } 237 // connect, force an exception if v == null 238 v.addPropertyChangeListener(this); 239 } 240 241 // connect to the JComboBox model so we'll see changes. 242 _value.setActionCommand(""); // so we can tell where change comes from 243 _value.addActionListener(this); 244 } 245 246 @Override 247 public void setToolTipText(String t) { 248 super.setToolTipText(t); // do default stuff 249 _value.setToolTipText(t); // set our value 250 } 251 252 @Override 253 public Object rangeVal() { 254 return "composite: " + _minVal + " - " + _maxVal; 255 } 256 257 @Override 258 public void actionPerformed(ActionEvent e) { 259 // see if this is from _value itself, or from an alternate rep. 260 // if from an alternate rep, it will contain the value to select 261 if (!(e.getActionCommand().equals(""))) { 262 // is from alternate rep 263 _value.setSelectedItem(e.getActionCommand()); 264 } 265 log.debug("action event: {}", e); 266 267 // notify 268 prop.firePropertyChange("Value", null, getIntValue()); 269 // Here for new values; set as needed 270 selectValue(getIntValue()); 271 } 272 273 /** 274 * This variable doesn't change state, hence doesn't change color. 275 */ 276 @Override 277 public void setState(ValueState state) { 278 log.debug("Ignore setState({})", state); 279 } 280 281 /** 282 * Set to a specific value. 283 * <p> 284 * Does this by delegating to the SettingList 285 */ 286 @Override 287 protected void selectValue(int value) { 288 log.debug("selectValue({})", value); 289 if (value > _value.getItemCount() - 1) { 290 log.error("Saw unreasonable internal value for pane combo box: {}", value); 291 return; 292 } 293 294 // locate SettingList for that number 295 String choice = _value.getItemAt(value); 296 SettingList sl = choiceHash.get(choice); 297 sl.setValues(); 298 299 } 300 301 @Override 302 public int getIntValue() { 303 return _value.getSelectedIndex(); 304 } 305 306 @Override 307 public Component getCommonRep() { 308 return _value; 309 } 310 311 @Override 312 public void setValue(int value) { 313 int oldVal = getIntValue(); 314 selectValue(value); 315 316 if (oldVal != value || getState() == ValueState.UNKNOWN) { 317 prop.firePropertyChange("Value", null, value); 318 } 319 } 320 321 /** 322 * Notify the connected CVs of a state change from above by way of the 323 * variables (e.g. not direct to CVs). 324 */ 325 @Override 326 public void setCvState(ValueState state) { 327 Iterator<VariableValue> i = variables.iterator(); 328 while (i.hasNext()) { 329 VariableValue v = i.next(); 330 v.setCvState(state); 331 } 332 } 333 334 @Override 335 public boolean isChanged() { 336 Iterator<VariableValue> i = variables.iterator(); 337 while (i.hasNext()) { 338 VariableValue v = i.next(); 339 if (v.isChanged()) { 340 return true; 341 } 342 } 343 return false; 344 } 345 346 @Override 347 public void setToRead(boolean state) { 348 349 Iterator<VariableValue> i = variables.iterator(); 350 while (i.hasNext()) { 351 VariableValue v = i.next(); 352 v.setToRead(state); 353 } 354 } 355 356 /** 357 * This variable needs to be read if any of its subsidiary variables needs 358 * to be read. 359 */ 360 @Override 361 public boolean isToRead() { 362 Iterator<VariableValue> i = variables.iterator(); 363 while (i.hasNext()) { 364 VariableValue v = i.next(); 365 if (v.isToRead()) { 366 return true; 367 } 368 } 369 return false; 370 } 371 372 @Override 373 public void setToWrite(boolean state) { 374 log.debug("Start setToWrite with {}", state); 375 376 Iterator<VariableValue> i = variables.iterator(); 377 while (i.hasNext()) { 378 VariableValue v = i.next(); 379 v.setToWrite(state); 380 } 381 log.debug("End setToWrite"); 382 } 383 384 /** 385 * This variable needs to be written if any of its subsidiary variables 386 * needs to be written. 387 */ 388 @Override 389 public boolean isToWrite() { 390 Iterator<VariableValue> i = variables.iterator(); 391 while (i.hasNext()) { 392 VariableValue v = i.next(); 393 if (v.isToWrite()) { 394 return true; 395 } 396 } 397 return false; 398 } 399 400 @Override 401 public void readChanges() { 402 if (isChanged()) { 403 readingChanges = true; 404 amReading = true; 405 continueRead(); 406 } 407 } 408 409 @Override 410 public void writeChanges() { 411 if (isChanged()) { 412 writingChanges = true; 413 amWriting = true; 414 continueWrite(); 415 } 416 } 417 418 @Override 419 public void readAll() { 420 readingChanges = false; 421 amReading = true; 422 continueRead(); 423 } 424 boolean amReading = false; 425 boolean readingChanges = false; 426 427 /** 428 * See if there's anything to read, and if so do it. 429 */ 430 protected void continueRead() { 431 // search for something to do 432 log.debug("Start continueRead"); 433 434 Iterator<VariableValue> i = variables.iterator(); 435 while (i.hasNext()) { 436 VariableValue v = i.next(); 437 if (v.isToRead() && (!readingChanges || v.isChanged())) { 438 // something to do! 439 amReading = true; // should be set already 440 setBusy(true); 441 if (readingChanges) { 442 v.readChanges(); 443 } else { 444 v.readAll(); 445 } 446 return; // wait for busy change event to continue 447 } 448 } 449 // found nothing, ensure cleaned up 450 amReading = false; 451 super.setState(ValueState.READ); 452 setBusy(false); 453 log.debug("End continueRead, nothing to do"); 454 } 455 456 @Override 457 public void writeAll() { 458 if (getReadOnly()) { 459 log.error("unexpected write operation when readOnly is set"); 460 } 461 writingChanges = false; 462 amWriting = true; 463 continueWrite(); 464 } 465 boolean amWriting = false; 466 boolean writingChanges = false; 467 468 /** 469 * See if there's anything to write, and if so do it. 470 */ 471 protected void continueWrite() { 472 // search for something to do 473 log.debug("Start continueWrite"); 474 475 Iterator<VariableValue> i = variables.iterator(); 476 while (i.hasNext()) { 477 VariableValue v = i.next(); 478 if (v.isToWrite() && (!writingChanges || v.isChanged())) { 479 // something to do! 480 amWriting = true; // should be set already 481 setBusy(true); 482 log.debug("request write of {} writing changes {}", v.label(), writingChanges); 483 if (writingChanges) { 484 v.writeChanges(); 485 } else { 486 v.writeAll(); 487 } 488 log.debug("return from starting write request"); 489 return; // wait for busy change event to continue 490 } 491 } 492 // found nothing, ensure cleaned up 493 amWriting = false; 494 super.setState(ValueState.STORED); 495 setBusy(false); 496 log.debug("End continueWrite, nothing to do"); 497 } 498 499 // handle incoming parameter notification 500 @Override 501 public void propertyChange(java.beans.PropertyChangeEvent e) { 502 // notification from CV; check for Value being changed 503 if (log.isDebugEnabled()) { 504 log.debug("propertyChange in {} type {} new value {}", label(), e.getPropertyName(), e.getNewValue()); 505 } 506 if (e.getPropertyName().equals("Busy")) { 507 if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) { 508 log.debug("busy change continues programming"); 509 // some programming operation just finished 510 if (amReading) { 511 continueRead(); 512 } else if (amWriting) { 513 continueWrite(); 514 } 515 // if we're not reading or writing, no problem, that's just something else happening 516 } 517 } else if (e.getPropertyName().equals("Value")) { 518 findValue(); 519 } 520 } 521 522 /** 523 * Suspect underlying variables have changed value; check. First match will 524 * succeed, so there should not be multiple matches possible. ("First match" 525 * is defined in choice-sequence). 526 */ 527 void findValue() { 528 if (log.isDebugEnabled()) { 529 log.debug("findValue invoked on {}", label()); 530 } 531 for (int i = 0; i < _value.getItemCount(); i++) { 532 String choice = _value.getItemAt(i); 533 SettingList sl = choiceHash.get(choice); 534 if (sl.match()) { 535 log.debug(" match in {}", i); 536 _value.setSelectedItem(choice); 537 return; 538 } 539 } 540 log.debug(" no match"); 541 } 542 543 // clean up connections when done 544 @Override 545 public void dispose() { 546 log.debug("dispose"); 547 548 Iterator<VariableValue> i = variables.iterator(); 549 while (i.hasNext()) { 550 VariableValue v = i.next(); 551 v.removePropertyChangeListener(this); 552 } 553 554 // remove the graphical representation 555 disposeReps(); 556 } 557 558 // initialize logging 559 private final static Logger log = LoggerFactory.getLogger(CompositeVariableValue.class); 560 561}