001package jmri.jmrit.display; 002 003import java.awt.Color; 004import java.awt.datatransfer.DataFlavor; 005import java.awt.datatransfer.Transferable; 006import java.awt.event.ActionEvent; 007import java.awt.event.ActionListener; 008import java.util.ArrayList; 009import java.util.Map; 010 011import javax.annotation.Nonnull; 012import javax.swing.AbstractAction; 013import javax.swing.JComponent; 014import javax.swing.JPopupMenu; 015import javax.swing.JSeparator; 016 017import jmri.InstanceManager; 018import jmri.Memory; 019import jmri.NamedBeanHandle; 020import jmri.Reportable; 021import jmri.NamedBean.DisplayOptions; 022import jmri.jmrit.catalog.NamedIcon; 023import jmri.jmrit.roster.RosterEntry; 024import jmri.jmrit.roster.RosterIconFactory; 025import jmri.jmrit.throttle.ThrottleFrame; 026import jmri.jmrit.throttle.ThrottleFrameManager; 027import jmri.util.datatransfer.RosterEntrySelection; 028import jmri.util.swing.JmriJOptionPane; 029import jmri.util.swing.JmriMouseEvent; 030 031/** 032 * An icon to display a status of a Memory. 033 * <p> 034 * The value of the memory can't be changed with this icon. 035 * 036 * @author Bob Jacobsen Copyright (c) 2004 037 */ 038public class MemoryIcon extends MemoryOrGVIcon implements java.beans.PropertyChangeListener/*, DropTargetListener*/ { 039 040 NamedIcon defaultIcon = null; 041 // the map of icons 042 java.util.HashMap<String, NamedIcon> map = null; 043 private NamedBeanHandle<Memory> namedMemory; 044 045 public MemoryIcon(String s, Editor editor) { 046 super(s, editor); 047 resetDefaultIcon(); 048 _namedIcon = defaultIcon; 049 //By default all memory is left justified 050 _popupUtil.setJustification(LEFT); 051 this.setTransferHandler(new TransferHandler()); 052 } 053 054 public MemoryIcon(NamedIcon s, Editor editor) { 055 super(s, editor); 056 setDisplayLevel(Editor.LABELS); 057 defaultIcon = s; 058 _popupUtil.setJustification(LEFT); 059 log.debug("MemoryIcon ctor= {}", MemoryIcon.class.getName()); 060 this.setTransferHandler(new TransferHandler()); 061 } 062 063 @Override 064 public Positionable deepClone() { 065 MemoryIcon pos = new MemoryIcon("", _editor); 066 return finishClone(pos); 067 } 068 069 protected Positionable finishClone(MemoryIcon pos) { 070 pos.setMemory(namedMemory.getName()); 071 pos.setOriginalLocation(getOriginalX(), getOriginalY()); 072 if (map != null) { 073 for (Map.Entry<String, NamedIcon> entry : map.entrySet()) { 074 String url = entry.getValue().getName(); 075 pos.addKeyAndIcon(NamedIcon.getIconByName(url), entry.getKey()); 076 } 077 } 078 return super.finishClone(pos); 079 } 080 081 public void resetDefaultIcon() { 082 defaultIcon = new NamedIcon("resources/icons/misc/X-red.gif", 083 "resources/icons/misc/X-red.gif"); 084 } 085 086 public void setDefaultIcon(NamedIcon n) { 087 defaultIcon = n; 088 } 089 090 public NamedIcon getDefaultIcon() { 091 return defaultIcon; 092 } 093 094 private void setMap() { 095 if (map == null) { 096 map = new java.util.HashMap<>(); 097 } 098 } 099 100 /** 101 * Attach a named Memory to this display item. 102 * 103 * @param pName Used as a system/user name to lookup the Memory object 104 */ 105 public void setMemory(String pName) { 106 if (InstanceManager.getNullableDefault(jmri.MemoryManager.class) != null) { 107 try { 108 Memory memory = InstanceManager.memoryManagerInstance().provideMemory(pName); 109 setMemory(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, memory)); 110 } catch (IllegalArgumentException e) { 111 log.error("Memory '{}' not available, icon won't see changes", pName); 112 } 113 } else { 114 log.error("No MemoryManager for this protocol, icon won't see changes"); 115 } 116 updateSize(); 117 } 118 119 /** 120 * Attach a named Memory to this display item. 121 * 122 * @param m The Memory object 123 */ 124 public void setMemory(NamedBeanHandle<Memory> m) { 125 if (namedMemory != null) { 126 getMemory().removePropertyChangeListener(this); 127 } 128 namedMemory = m; 129 if (namedMemory != null) { 130 getMemory().addPropertyChangeListener(this, namedMemory.getName(), "Memory Icon"); 131 displayState(); 132 setName(namedMemory.getName()); 133 } 134 } 135 136 public NamedBeanHandle<Memory> getNamedMemory() { 137 return namedMemory; 138 } 139 140 public Memory getMemory() { 141 if (namedMemory == null) { 142 return null; 143 } 144 return namedMemory.getBean(); 145 } 146 147 @Override 148 public jmri.NamedBean getNamedBean() { 149 return getMemory(); 150 } 151 152 public java.util.HashMap<String, NamedIcon> getMap() { 153 return map; 154 } 155 156 // display icons 157 public void addKeyAndIcon(NamedIcon icon, String keyValue) { 158 if (map == null) { 159 setMap(); // initialize if needed 160 } 161 map.put(keyValue, icon); 162 // drop size cache 163 //height = -1; 164 //width = -1; 165 displayState(); // in case changed 166 } 167 168 // update icon as state of Memory changes 169 @Override 170 public void propertyChange(java.beans.PropertyChangeEvent e) { 171 if (log.isDebugEnabled()) { 172 log.debug("property change: {} is now {}", 173 e.getPropertyName(), e.getNewValue()); 174 } 175 if (e.getPropertyName().equals("value")) { 176 displayState(); 177 } 178 if (e.getSource() instanceof jmri.Throttle) { 179 if (e.getPropertyName().equals(jmri.Throttle.ISFORWARD)) { 180 Boolean boo = (Boolean) e.getNewValue(); 181 if (boo) { 182 flipIcon(NamedIcon.NOFLIP); 183 } else { 184 flipIcon(NamedIcon.HORIZONTALFLIP); 185 } 186 } 187 } 188 } 189 190 @Override 191 @Nonnull 192 public String getTypeString() { 193 return Bundle.getMessage("PositionableType_MemoryIcon"); 194 } 195 196 @Override 197 public String getNameString() { 198 String name; 199 if (namedMemory == null) { 200 name = Bundle.getMessage("NotConnected"); 201 } else { 202 name = getMemory().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME); 203 } 204 return name; 205 } 206 207 public void setSelectable(boolean b) { 208 selectable = b; 209 } 210 211 public boolean isSelectable() { 212 return selectable; 213 } 214 boolean selectable = false; 215 216 @Override 217 public boolean showPopUp(JPopupMenu popup) { 218 if (isEditable() && selectable) { 219 popup.add(new JSeparator()); 220 221 for (String key : map.keySet()) { 222 //String value = ((NamedIcon)map.get(key)).getName(); 223 popup.add(new AbstractAction(key) { 224 225 @Override 226 public void actionPerformed(ActionEvent e) { 227 String key = e.getActionCommand(); 228 setValue(key); 229 } 230 }); 231 } 232 return true; 233 } // end of selectable 234 if (re != null) { 235 popup.add(new AbstractAction(Bundle.getMessage("OpenThrottle")) { 236 237 @Override 238 public void actionPerformed(ActionEvent e) { 239 ThrottleFrame tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame(); 240 tf.toFront(); 241 tf.getAddressPanel().setRosterEntry(re); 242 } 243 }); 244 //don't like the idea of refering specifically to the layout block manager for this, but it has to be done if we are to allow the panel editor to also assign trains to block, when used with a layouteditor 245 if ((InstanceManager.getDefault(jmri.SectionManager.class).getNamedBeanSet().size()) > 0 && jmri.InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).getBlockWithMemoryAssigned(getMemory()) != null) { 246 final jmri.jmrit.dispatcher.DispatcherFrame df = jmri.InstanceManager.getNullableDefault(jmri.jmrit.dispatcher.DispatcherFrame.class); 247 if (df != null) { 248 final jmri.jmrit.dispatcher.ActiveTrain at = df.getActiveTrainForRoster(re); 249 if (at != null) { 250 popup.add(new AbstractAction(Bundle.getMessage("MenuTerminateTrain")) { 251 252 @Override 253 public void actionPerformed(ActionEvent e) { 254 df.terminateActiveTrain(at,true,false); 255 } 256 }); 257 popup.add(new AbstractAction(Bundle.getMessage("MenuAllocateExtra")) { 258 259 @Override 260 public void actionPerformed(ActionEvent e) { 261 //Just brings up the standard allocate extra frame, this could be expanded in the future 262 //As a point and click operation. 263 df.allocateExtraSection(e, at); 264 } 265 }); 266 if (at.getStatus() == jmri.jmrit.dispatcher.ActiveTrain.DONE) { 267 popup.add(new AbstractAction(Bundle.getMessage("MenuRestartTrain")) { 268 269 @Override 270 public void actionPerformed(ActionEvent e) { 271 at.allocateAFresh(); 272 } 273 }); 274 } 275 } else { 276 popup.add(new AbstractAction(Bundle.getMessage("MenuNewTrain")) { 277 278 @Override 279 public void actionPerformed(ActionEvent e) { 280 jmri.jmrit.display.layoutEditor.LayoutBlock lBlock = jmri.InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).getBlockWithMemoryAssigned(getMemory()); 281 if (!df.getNewTrainActive() && lBlock!=null) { 282 df.getActiveTrainFrame().initiateTrain(e, re, lBlock.getBlock()); 283 df.setNewTrainActive(true); 284 } else { 285 df.getActiveTrainFrame().showActivateFrame(re); 286 } 287 } 288 289 }); 290 } 291 } 292 } 293 return true; 294 } 295 return false; 296 } 297 298 /** 299 * Text edits cannot be done to Memory text - override 300 */ 301 @Override 302 public boolean setTextEditMenu(JPopupMenu popup) { 303 popup.add(new AbstractAction(Bundle.getMessage("EditMemoryValue")) { 304 305 @Override 306 public void actionPerformed(ActionEvent e) { 307 editMemoryValue(); 308 } 309 }); 310 return true; 311 } 312 313 protected void flipIcon(int flip) { 314 if (_namedIcon != null) { 315 _namedIcon.flip(flip, this); 316 } 317 updateSize(); 318 repaint(); 319 } 320 Color _saveColor; 321 322 /** 323 * Drive the current state of the display from the state of the Memory. 324 */ 325 @Override 326 public void displayState() { 327 log.debug("displayState()"); 328 329 if (namedMemory == null) { // use default if not connected yet 330 setIcon(defaultIcon); 331 updateSize(); 332 return; 333 } 334 if (re != null) { 335 jmri.InstanceManager.throttleManagerInstance().removeListener(re.getDccLocoAddress(), this); 336 re = null; 337 } 338 Object key = getMemory().getValue(); 339 displayState(key); 340 } 341 342 /** 343 * Special method to transfer a setAttributes call from the LE version of 344 * MemoryIcon. This eliminates the need to change references to public. 345 * 346 * @since 4.11.6 347 * @param util The LE popup util object. 348 * @param that The current positional object (this). 349 */ 350 public void setAttributes(PositionablePopupUtil util, Positionable that) { 351 _editor.setAttributes(util, that); 352 } 353 354 protected void displayState(Object key) { 355 log.debug("displayState({})", key); 356 if (key != null) { 357 if (map == null) { 358 Object val = key; 359 // no map, attempt to show object directly 360 if (val instanceof jmri.jmrit.roster.RosterEntry) { 361 jmri.jmrit.roster.RosterEntry roster = (jmri.jmrit.roster.RosterEntry) val; 362 val = updateIconFromRosterVal(roster); 363 flipRosterIcon = false; 364 if (val == null) { 365 return; 366 } 367 } 368 if (val instanceof String) { 369 String str = (String) val; 370 _icon = false; 371 _text = true; 372 setText(str); 373 updateIcon(null); 374 if (log.isDebugEnabled()) { 375 log.debug("String str= \"{}\" str.trim().length()= {}", str, str.trim().length()); 376 log.debug(" maxWidth()= {}, maxHeight()= {}", maxWidth(), maxHeight()); 377 log.debug(" getBackground(): {}", getBackground()); 378 log.debug(" _editor.getTargetPanel().getBackground(): {}", _editor.getTargetPanel().getBackground()); 379 log.debug(" setAttributes to getPopupUtility({}) with", getPopupUtility()); 380 log.debug(" hasBackground() {}", getPopupUtility().hasBackground()); 381 log.debug(" getBackground() {}", getPopupUtility().getBackground()); 382 log.debug(" on editor {}", _editor); 383 } 384 _editor.setAttributes(getPopupUtility(), this); 385 } else if (val instanceof javax.swing.ImageIcon) { 386 _icon = true; 387 _text = false; 388 setIcon((javax.swing.ImageIcon) val); 389 setText(null); 390 } else if (val instanceof Number) { 391 _icon = false; 392 _text = true; 393 setText(val.toString()); 394 setIcon(null); 395 } else if (val instanceof jmri.IdTag){ 396 // most IdTags are Reportable objects, so 397 // this needs to be before Reportable 398 _icon = false; 399 _text = true; 400 setIcon(null); 401 setText(((jmri.IdTag)val).getDisplayName()); 402 } else if (val instanceof Reportable) { 403 _icon = false; 404 _text = true; 405 setText(((Reportable)val).toReportString()); 406 setIcon(null); 407 } else { 408 // don't recognize the type, do our best with toString 409 log.debug("display current value of {} as String, val= {} of Class {}", 410 getNameString(), val, val.getClass().getName()); 411 _icon = false; 412 _text = true; 413 setText(val.toString()); 414 setIcon(null); 415 } 416 } else { 417 // map exists, use it 418 NamedIcon newicon = map.get(key.toString()); 419 if (newicon != null) { 420 421 setText(null); 422 super.setIcon(newicon); 423 } else { 424 // no match, use default 425 _icon = true; 426 _text = false; 427 setIcon(defaultIcon); 428 setText(null); 429 } 430 } 431 } else { 432 log.debug("object null"); 433 _icon = true; 434 _text = false; 435 setIcon(defaultIcon); 436 setText(null); 437 } 438 updateSize(); 439 } 440 441 protected Object updateIconFromRosterVal(RosterEntry roster) { 442 re = roster; 443 javax.swing.ImageIcon icon = jmri.InstanceManager.getDefault(RosterIconFactory.class).getIcon(roster); 444 if (icon == null || icon.getIconWidth() == -1 || icon.getIconHeight() == -1) { 445 //the IconPath is still at default so no icon set 446 return roster.titleString(); 447 } else { 448 NamedIcon rosterIcon = new NamedIcon(roster.getIconPath(), roster.getIconPath()); 449 _text = false; 450 _icon = true; 451 updateIcon(rosterIcon); 452 453 if (flipRosterIcon) { 454 flipIcon(NamedIcon.HORIZONTALFLIP); 455 } 456 jmri.InstanceManager.throttleManagerInstance().attachListener(re.getDccLocoAddress(), this); 457 Object isForward = jmri.InstanceManager.throttleManagerInstance().getThrottleInfo(re.getDccLocoAddress(), jmri.Throttle.ISFORWARD); 458 if (isForward != null) { 459 if (!(Boolean) isForward) { 460 flipIcon(NamedIcon.HORIZONTALFLIP); 461 } 462 } 463 return null; 464 } 465 } 466 467 protected jmri.jmrit.roster.RosterEntry re = null; 468 469 /*As the size of a memory label can change we want to adjust the position of the x,y 470 if the width is fixed*/ 471 @SuppressWarnings("hiding") // Overriding value from SwingConstants 472 static final int LEFT = 0x00; 473 @SuppressWarnings("hiding") // Overriding value from SwingConstants 474 static final int RIGHT = 0x02; 475 static final int CENTRE = 0x04; 476 477 @Override 478 public void updateSize() { 479 if (_popupUtil.getFixedWidth() == 0) { 480 //setSize(maxWidth(), maxHeight()); 481 switch (_popupUtil.getJustification()) { 482 case LEFT: 483 super.setLocation(getOriginalX(), getOriginalY()); 484 break; 485 case RIGHT: 486 super.setLocation(getOriginalX() - maxWidth(), getOriginalY()); 487 break; 488 case CENTRE: 489 super.setLocation(getOriginalX() - (maxWidth() / 2), getOriginalY()); 490 break; 491 default: 492 log.warn("Unhandled justification code: {}", _popupUtil.getJustification()); 493 break; 494 } 495 setSize(maxWidth(), maxHeight()); 496 } else { 497 super.updateSize(); 498 if (_icon && _namedIcon != null) { 499 _namedIcon.reduceTo(maxWidthTrue(), maxHeightTrue(), 0.2); 500 } 501 } 502 } 503 504 /*Stores the original location of the memory, this is then used to calculate 505 the position of the text dependant upon the justification*/ 506 private int originalX = 0; 507 private int originalY = 0; 508 509 public void setOriginalLocation(int x, int y) { 510 originalX = x; 511 originalY = y; 512 updateSize(); 513 } 514 515 @Override 516 public int getOriginalX() { 517 return originalX; 518 } 519 520 @Override 521 public int getOriginalY() { 522 return originalY; 523 } 524 525 @Override 526 public void setLocation(int x, int y) { 527 if (_popupUtil.getFixedWidth() == 0) { 528 setOriginalLocation(x, y); 529 } else { 530 super.setLocation(x, y); 531 } 532 } 533 534 @Override 535 public boolean setEditIconMenu(JPopupMenu popup) { 536 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameMemory")); 537 popup.add(new AbstractAction(txt) { 538 @Override 539 public void actionPerformed(ActionEvent e) { 540 edit(); 541 } 542 }); 543 return true; 544 } 545 546 @Override 547 protected void edit() { 548 makeIconEditorFrame(this, "Memory", true, null); 549 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.memoryPickModelInstance()); 550 ActionListener addIconAction = (ActionEvent a) -> editMemory(); 551 _iconEditor.complete(addIconAction, false, true, true); 552 _iconEditor.setSelection(getMemory()); 553 } 554 555 void editMemory() { 556 setMemory(_iconEditor.getTableSelection().getDisplayName()); 557 updateSize(); 558 _iconEditorFrame.dispose(); 559 _iconEditorFrame = null; 560 _iconEditor = null; 561 invalidate(); 562 } 563 564 @Override 565 public void dispose() { 566 if (getMemory() != null) { 567 getMemory().removePropertyChangeListener(this); 568 } 569 namedMemory = null; 570 if (re != null) { 571 jmri.InstanceManager.throttleManagerInstance().removeListener(re.getDccLocoAddress(), this); 572 re = null; 573 } 574 super.dispose(); 575 } 576 577 @Override 578 public void doMouseClicked(JmriMouseEvent e) { 579 if (e.getClickCount() == 2) { // double click? 580 if (!getEditor().isEditable() && isValueEditDisabled()) { 581 log.debug("Double click memory value edit is disabled"); 582 return; 583 } 584 editMemoryValue(); 585 } 586 } 587 588 protected void editMemoryValue() { 589 590 String reval = (String)JmriJOptionPane.showInputDialog(this, 591 Bundle.getMessage("EditCurrentMemoryValue", namedMemory.getName()), 592 getMemory().getValue()); 593 594 setValue(reval); 595 updateSize(); 596 } 597 598 //This is used by the LayoutEditor 599 protected boolean updateBlockValue = false; 600 601 public void updateBlockValueOnChange(boolean boo) { 602 updateBlockValue = boo; 603 } 604 605 public boolean updateBlockValueOnChange() { 606 return updateBlockValue; 607 } 608 609 protected boolean flipRosterIcon = false; 610 611 protected void addRosterToIcon(RosterEntry roster) { 612 Object[] options = {"Facing West", 613 "Facing East", 614 "Do Not Add"}; 615 int n = JmriJOptionPane.showOptionDialog(this, // TODO I18N 616 "Would you like to assign loco " 617 + roster.titleString() + " to this location", 618 "Assign Loco", 619 JmriJOptionPane.DEFAULT_OPTION, 620 JmriJOptionPane.QUESTION_MESSAGE, 621 null, 622 options, 623 options[2]); 624 if ( n == 2 || n==JmriJOptionPane.CLOSED_OPTION ) { // option array 2 Do Not Add, or Dialog closed 625 return; 626 } 627 flipRosterIcon = (n == 0); // true if option array position 0, Facing West 628 if (getValue() == roster) { 629 //No change in the loco but a change in direction facing might have occurred 630 updateIconFromRosterVal(roster); 631 } else { 632 setValue(roster); 633 } 634 } 635 636 protected Object getValue() { 637 if (getMemory() == null) { 638 return null; 639 } 640 return getMemory().getValue(); 641 } 642 643 protected void setValue(Object val) { 644 getMemory().setValue(val); 645 } 646 647 class TransferHandler extends javax.swing.TransferHandler { 648 @Override 649 public boolean canImport(JComponent c, DataFlavor[] transferFlavors) { 650 for (DataFlavor flavor : transferFlavors) { 651 if (RosterEntrySelection.rosterEntryFlavor.equals(flavor)) { 652 return true; 653 } 654 } 655 return false; 656 } 657 658 @Override 659 public boolean importData(JComponent c, Transferable t) { 660 try { 661 ArrayList<RosterEntry> REs = RosterEntrySelection.getRosterEntries(t); 662 for (RosterEntry roster : REs) { 663 addRosterToIcon(roster); 664 } 665 } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException e) { 666 log.error("Could not add a RosterEntry to Icon.", e); 667 } 668 return true; 669 } 670 671 } 672 673 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MemoryIcon.class); 674 675}