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