001package jmri.jmrit.display; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.util.HashMap; 006import java.util.Hashtable; 007import java.util.Map.Entry; 008 009import javax.annotation.Nonnull; 010import javax.swing.AbstractAction; 011import javax.swing.ButtonGroup; 012import javax.swing.JMenu; 013import javax.swing.JPopupMenu; 014import javax.swing.JRadioButtonMenuItem; 015 016import jmri.InstanceManager; 017import jmri.NamedBeanHandle; 018import jmri.SignalHead; 019import jmri.jmrit.catalog.NamedIcon; 020import jmri.jmrit.display.palette.SignalHeadItemPanel; 021import jmri.jmrit.picker.PickListModel; 022import jmri.util.swing.JmriMouseEvent; 023 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027/** 028 * An icon to display a status of a SignalHead. 029 * <p> 030 * SignalHeads are located via the SignalHeadManager, which in turn is located 031 * via the InstanceManager. 032 * 033 * @see jmri.SignalHeadManager 034 * @see jmri.InstanceManager 035 * @author Bob Jacobsen Copyright (C) 2001, 2002 036 */ 037public class SignalHeadIcon extends PositionableIcon implements java.beans.PropertyChangeListener { 038 039 private String[] _validKeys; 040 041 public SignalHeadIcon(Editor editor) { 042 super(editor); 043 _control = true; 044 } 045 046 @Override 047 public Positionable deepClone() { 048 SignalHeadIcon pos = new SignalHeadIcon(_editor); 049 return finishClone(pos); 050 } 051 052 protected Positionable finishClone(SignalHeadIcon pos) { 053 pos.setSignalHead(getNamedSignalHead().getName()); 054 for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) { 055 pos.setIcon(entry.getKey(), entry.getValue()); 056 } 057 pos.setClickMode(getClickMode()); 058 pos.setLitMode(getLitMode()); 059 return super.finishClone(pos); 060 } 061 062 // private SignalHead mHead; 063 private NamedBeanHandle<SignalHead> namedHead; 064 065 private HashMap<String, NamedIcon> _saveMap; 066 067 /** 068 * Attach a SignalHead element to this display item by bean. 069 * 070 * @param sh the specific SignalHead object to attach 071 */ 072 public void setSignalHead(NamedBeanHandle<SignalHead> sh) { 073 if (namedHead != null) { 074 getSignalHead().removePropertyChangeListener(this); 075 } 076 namedHead = sh; 077 if (namedHead != null) { 078 _iconMap = new HashMap<>(); 079 _validKeys = getSignalHead().getValidStateKeys(); 080 displayState(headState()); 081 getSignalHead().addPropertyChangeListener(this, namedHead.getName(), "SignalHead Icon"); 082 } 083 } 084 085 /** 086 * Attach a SignalHead element to this display item by name. Taken from the 087 * Layout Editor. 088 * 089 * @param pName Used as a system/user name to lookup the SignalHead object 090 */ 091 public void setSignalHead(String pName) { 092 SignalHead mHead = InstanceManager.getDefault(jmri.SignalHeadManager.class).getNamedBean(pName); 093 if (mHead == null) { 094 log.warn("did not find a SignalHead named {}", pName); 095 } else { 096 setSignalHead(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, mHead)); 097 } 098 } 099 100 public NamedBeanHandle<SignalHead> getNamedSignalHead() { 101 return namedHead; 102 } 103 104 public SignalHead getSignalHead() { 105 if (namedHead == null) { 106 return null; 107 } 108 return namedHead.getBean(); 109 } 110 111 @Override 112 public jmri.NamedBean getNamedBean() { 113 return getSignalHead(); 114 } 115 116 /** 117 * Place icon by its non-localized bean state name. 118 * 119 * @param state the non-localized state 120 * @param icon the icon to place 121 */ 122 public void setIcon(String state, NamedIcon icon) { 123 log.debug("setIcon for {}", state); 124 if (isValidState(state)) { 125 _iconMap.put(state, icon); 126 displayState(headState()); 127 } 128 } 129 130 /** 131 * Check that device supports the state. Valid state names returned by the 132 * bean are (non-localized) property key names. 133 */ 134 private boolean isValidState(String key) { 135 if (key == null) { 136 return false; 137 } 138 if (key.equals("SignalHeadStateDark") || key.equals("SignalHeadStateHeld")) { 139 log.debug("{} is a valid state.", key); 140 return true; 141 } 142 for (String valid : _validKeys) { 143 if (key.equals(valid)) { 144 log.debug("{} is a valid state.", key); 145 return true; 146 } 147 } 148 log.debug("{} is NOT a valid state.", key); 149 return false; 150 } 151 152 /** 153 * Get current appearance of the head. 154 * 155 * @return an appearance variable from a SignalHead, e.g. SignalHead.RED 156 */ 157 public int headState() { 158 if (getSignalHead() == null) { 159 return 0; 160 } else { 161 return getSignalHead().getAppearance(); 162 } 163 } 164 165 // update icon as state of turnout changes 166 @Override 167 public void propertyChange(java.beans.PropertyChangeEvent e) { 168 log.debug("property change: {} current state: {}", e.getPropertyName(), headState()); 169 displayState(headState()); 170 _editor.getTargetPanel().repaint(); 171 } 172 173 @Override 174 @Nonnull 175 public String getTypeString() { 176 return Bundle.getMessage("PositionableType_SignalHead"); 177 } 178 179 @Override 180 public @Nonnull 181 String getNameString() { 182 if (namedHead == null) { 183 return Bundle.getMessage("NotConnected"); 184 } 185 return namedHead.getName(); // short NamedIcon name 186 } 187 188 private ButtonGroup litButtonGroup = null; 189 190 /** 191 * Pop-up just displays the name 192 */ 193 @Override 194 public boolean showPopUp(JPopupMenu popup) { 195 if (isEditable()) { 196 // add menu to select action on click 197 JMenu clickMenu = new JMenu(Bundle.getMessage("WhenClicked")); 198 ButtonGroup clickButtonGroup = new ButtonGroup(); 199 JRadioButtonMenuItem r; 200 r = new JRadioButtonMenuItem(Bundle.getMessage("ChangeAspect")); 201 r.addActionListener(e -> setClickMode(3)); 202 clickButtonGroup.add(r); 203 if (clickMode == 3) { 204 r.setSelected(true); 205 } else { 206 r.setSelected(false); 207 } 208 clickMenu.add(r); 209 r = new JRadioButtonMenuItem(Bundle.getMessage("Cycle3Aspects")); 210 r.addActionListener(e -> setClickMode(0)); 211 clickButtonGroup.add(r); 212 if (clickMode == 0) { 213 r.setSelected(true); 214 } else { 215 r.setSelected(false); 216 } 217 clickMenu.add(r); 218 r = new JRadioButtonMenuItem(Bundle.getMessage("AlternateLit")); 219 r.addActionListener(e -> setClickMode(1)); 220 clickButtonGroup.add(r); 221 if (clickMode == 1) { 222 r.setSelected(true); 223 } else { 224 r.setSelected(false); 225 } 226 clickMenu.add(r); 227 r = new JRadioButtonMenuItem(Bundle.getMessage("AlternateHeld")); 228 r.addActionListener(e -> setClickMode(2)); 229 clickButtonGroup.add(r); 230 if (clickMode == 2) { 231 r.setSelected(true); 232 } else { 233 r.setSelected(false); 234 } 235 clickMenu.add(r); 236 popup.add(clickMenu); 237 238 // add menu to select handling of lit parameter 239 JMenu litMenu = new JMenu(Bundle.getMessage("WhenNotLit")); 240 litButtonGroup = new ButtonGroup(); 241 r = new JRadioButtonMenuItem(Bundle.getMessage("ShowAppearance")); 242 r.setIconTextGap(10); 243 r.addActionListener(e -> setLitMode(false)); 244 litButtonGroup.add(r); 245 if (!litMode) { 246 r.setSelected(true); 247 } else { 248 r.setSelected(false); 249 } 250 litMenu.add(r); 251 r = new JRadioButtonMenuItem(Bundle.getMessage("ShowDarkIcon")); 252 r.setIconTextGap(10); 253 r.addActionListener(e -> setLitMode(true)); 254 litButtonGroup.add(r); 255 if (litMode) { 256 r.setSelected(true); 257 } else { 258 r.setSelected(false); 259 } 260 litMenu.add(r); 261 popup.add(litMenu); 262 263 popup.add(new AbstractAction(Bundle.getMessage("EditLogic")) { 264 @Override 265 public void actionPerformed(ActionEvent e) { 266 jmri.jmrit.blockboss.BlockBossFrame f = new jmri.jmrit.blockboss.BlockBossFrame(); 267 String name = getNameString(); 268 f.setTitle(java.text.MessageFormat.format(Bundle.getMessage("SignalLogic"), name)); 269 f.setSignal(getSignalHead()); 270 f.setVisible(true); 271 } 272 }); 273 return true; 274 } 275 return false; 276 } 277 278 /** 279 * ************* popup AbstractAction.actionPerformed method overrides 280 * *********** 281 */ 282 @Override 283 protected void rotateOrthogonal() { 284 super.rotateOrthogonal(); 285 displayState(headState()); 286 } 287 288 @Override 289 public void setScale(double s) { 290 super.setScale(s); 291 displayState(headState()); 292 } 293 294 @Override 295 public void rotate(int deg) { 296 super.rotate(deg); 297 displayState(headState()); 298 } 299 300 /** 301 * Drive the current state of the display from the state of the underlying 302 * SignalHead object. 303 * <ul> 304 * <li>If the signal is held, display that. 305 * <li>If set to monitor the status of the lit parameter and lit is false, 306 * show the dark icon ("dark", when set as an explicit appearance, is 307 * displayed anyway) 308 * <li>Show the icon corresponding to one of the (max seven) appearances. 309 * </ul> 310 */ 311 @Override 312 public void displayState(int state) { 313 updateSize(); 314 if (getSignalHead() == null) { 315 log.debug("Display state {}, disconnected", state); 316 return; 317 } 318 log.debug("Display state {} for {}", state, getNameString()); 319 if (getSignalHead().getHeld()) { 320 if (isText()) { 321 super.setText(Bundle.getMessage("Held")); 322 } 323 if (isIcon()) { 324 super.setIcon(_iconMap.get("SignalHeadStateHeld")); 325 } 326 } else if (getLitMode() && !getSignalHead().getLit()) { 327 if (isText()) { 328 super.setText(Bundle.getMessage("Dark")); 329 } 330 if (isIcon()) { 331 super.setIcon(_iconMap.get("SignalHeadStateDark")); 332 } 333 } else { 334 if (isText()) { 335 super.setText(Bundle.getMessage(getSignalHead().getAppearanceKey(state))); 336 } 337 if (isIcon()) { 338 NamedIcon icon = _iconMap.get(getSignalHead().getAppearanceKey(state)); 339 if (icon != null) { 340 super.setIcon(icon); 341 } 342 } 343 } 344 } 345 346 private SignalHeadItemPanel _itemPanel; 347 348 @Override 349 public boolean setEditItemMenu(JPopupMenu popup) { 350 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), 351 Bundle.getMessage("BeanNameSignalHead")); 352 popup.add(new AbstractAction(txt) { 353 @Override 354 public void actionPerformed(ActionEvent e) { 355 editItem(); 356 } 357 }); 358 return true; 359 } 360 361 protected void editItem() { 362 _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"), 363 Bundle.getMessage("BeanNameSignalHead"))); 364 _itemPanel = new SignalHeadItemPanel(_paletteFrame, "SignalHead", getFamily(), 365 PickListModel.signalHeadPickModelInstance()); // NOI18N 366 ActionListener updateAction = a -> updateItem(); 367 // _iconMap keys with non-localized keys 368 // duplicate _iconMap map with unscaled and unrotated icons 369 HashMap<String, NamedIcon> map = new HashMap<>(); 370 for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) { 371 NamedIcon oldIcon = entry.getValue(); 372 NamedIcon newIcon = cloneIcon(oldIcon, this); 373 newIcon.rotate(0, this); 374 newIcon.scale(1.0, this); 375 newIcon.setRotation(4, this); 376 map.put(entry.getKey(), newIcon); 377 } 378 _itemPanel.init(updateAction, map); 379 _itemPanel.setSelection(getSignalHead()); 380 initPaletteFrame(_paletteFrame, _itemPanel); 381 } 382 383 void updateItem() { 384 _saveMap = _iconMap; // setSignalHead() clears _iconMap. We need a copy for setIcons() 385 setSignalHead(_itemPanel.getTableSelection().getSystemName()); 386 setFamily(_itemPanel.getFamilyName()); 387 HashMap<String, NamedIcon> map1 = _itemPanel.getIconMap(); 388 if (map1 != null) { 389 // map1 may be keyed with NamedBean names. Convert to local name keys. 390 Hashtable<String, NamedIcon> map2 = new Hashtable<>(); 391 for (Entry<String, NamedIcon> entry : map1.entrySet()) { 392 map2.put(entry.getKey(), entry.getValue()); 393 } 394 setIcons(map2); 395 } // otherwise retain current map 396 displayState(getSignalHead().getAppearance()); 397 finishItemUpdate(_paletteFrame, _itemPanel); 398 } 399 400 @Override 401 public boolean setEditIconMenu(JPopupMenu popup) { 402 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSignalHead")); 403 popup.add(new AbstractAction(txt) { 404 @Override 405 public void actionPerformed(ActionEvent e) { 406 edit(); 407 } 408 }); 409 return true; 410 } 411 412 @Override 413 protected void edit() { 414 makeIconEditorFrame(this, "SignalHead", true, null); 415 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.signalHeadPickModelInstance()); 416 int i = 0; 417 for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) { 418 _iconEditor.setIcon(i++, entry.getKey(), new NamedIcon(entry.getValue())); 419 } 420 _iconEditor.makeIconPanel(false); 421 422 ActionListener addIconAction = a -> updateSignal(); 423 _iconEditor.complete(addIconAction, true, false, true); 424 _iconEditor.setSelection(getSignalHead()); 425 } 426 427 /** 428 * Replace the icons in _iconMap with those from map, but preserve the scale 429 * and rotation. 430 */ 431 private void setIcons(Hashtable<String, NamedIcon> map) { 432 HashMap<String, NamedIcon> tempMap = new HashMap<>(); 433 for (Entry<String, NamedIcon> entry : map.entrySet()) { 434 String name = entry.getKey(); 435 NamedIcon icon = entry.getValue(); 436 NamedIcon oldIcon = _saveMap.get(name); // setSignalHead() has cleared _iconMap 437 log.debug("key= {}, localKey= {}, newIcon= {}, oldIcon= {}", entry.getKey(), name, icon, oldIcon); 438 if (oldIcon != null) { 439 icon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this); 440 icon.setRotation(oldIcon.getRotation(), this); 441 } 442 tempMap.put(name, icon); 443 } 444 _iconMap = tempMap; 445 } 446 447 void updateSignal() { 448 _saveMap = _iconMap; // setSignalHead() clears _iconMap. We need a copy for setIcons() 449 if (_iconEditor != null) { 450 setSignalHead(_iconEditor.getTableSelection().getDisplayName()); 451 setIcons(_iconEditor.getIconMap()); 452 _iconEditorFrame.dispose(); 453 _iconEditorFrame = null; 454 _iconEditor = null; 455 invalidate(); 456 } 457 displayState(headState()); 458 } 459 460 /** 461 * What to do on click? 0 means sequence through aspects; 1 means alternate 462 * the "lit" aspect; 2 means alternate the "held" aspect. 463 */ 464 protected int clickMode = 3; 465 466 public void setClickMode(int mode) { 467 clickMode = mode; 468 } 469 470 public int getClickMode() { 471 return clickMode; 472 } 473 474 /** 475 * How to handle lit vs not lit? 476 * <p> 477 * False means ignore (always show R/Y/G/etc appearance on screen); True 478 * means show "dark" if lit is set false. 479 * <p> 480 * Note that setting the appearance "DARK" explicitly will show the dark 481 * icon regardless of how this is set. 482 */ 483 protected boolean litMode = false; 484 485 public void setLitMode(boolean mode) { 486 litMode = mode; 487 } 488 489 public boolean getLitMode() { 490 return litMode; 491 } 492 493 /** 494 * Change the SignalHead state when the icon is clicked. Note that this 495 * change may not be permanent if there is logic controlling the signal 496 * head. 497 */ 498 @Override 499 public void doMouseClicked(JmriMouseEvent e) { 500 if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) { 501 return; 502 } 503 performMouseClicked(e); 504 } 505 506 /** 507 * Handle mouse clicks when no modifier keys are pressed. Mouse clicks with 508 * modifier keys pressed can be processed by the containing component. 509 * 510 * @param e the mouse click event 511 */ 512 public void performMouseClicked(JmriMouseEvent e) { 513 if (e.isMetaDown() || e.isAltDown()) { 514 return; 515 } 516 if (getSignalHead() == null) { 517 log.error("No turnout connection, can't process click"); 518 return; 519 } 520 switch (clickMode) { 521 case 0: 522 switch (getSignalHead().getAppearance()) { 523 case jmri.SignalHead.RED: 524 case jmri.SignalHead.FLASHRED: 525 getSignalHead().setAppearance(jmri.SignalHead.YELLOW); 526 break; 527 case jmri.SignalHead.YELLOW: 528 case jmri.SignalHead.FLASHYELLOW: 529 getSignalHead().setAppearance(jmri.SignalHead.GREEN); 530 break; 531 case jmri.SignalHead.GREEN: 532 case jmri.SignalHead.FLASHGREEN: 533 default: 534 getSignalHead().setAppearance(jmri.SignalHead.RED); 535 break; 536 } 537 return; 538 case 1: 539 getSignalHead().setLit(!getSignalHead().getLit()); 540 return; 541 case 2: 542 getSignalHead().setHeld(!getSignalHead().getHeld()); 543 return; 544 case 3: 545 SignalHead sh = getSignalHead(); 546 int[] states = sh.getValidStates(); 547 int state = sh.getAppearance(); 548 for (int i = 0; i < states.length; i++) { 549 if (state == states[i]) { 550 i++; 551 if (i >= states.length) { 552 i = 0; 553 } 554 state = states[i]; 555 break; 556 } 557 } 558 sh.setAppearance(state); 559 log.debug("Set state= {}", state); 560 return; 561 default: 562 log.error("Click in mode {}", clickMode); 563 } 564 } 565 566 //private static boolean warned = false; 567 @Override 568 public void dispose() { 569 if (getSignalHead() != null) { 570 getSignalHead().removePropertyChangeListener(this); 571 } 572 namedHead = null; 573 _iconMap = null; 574 super.dispose(); 575 } 576 577 private final static Logger log = LoggerFactory.getLogger(SignalHeadIcon.class); 578 579}