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