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.ThrottleFrameManager; 027import jmri.jmrit.throttle.interfaces.ThrottleControllerUI; 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 ThrottleControllerUI tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame(); 241 tf.toFront(); 242 tf.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 // use the roster parameter, not the re field: displayState() can clear 460 // re from another thread between the assignment above and here 461 jmri.InstanceManager.throttleManagerInstance().attachListener(roster.getDccLocoAddress(), this); 462 Object isForward = jmri.InstanceManager.throttleManagerInstance().getThrottleInfo(roster.getDccLocoAddress(), jmri.Throttle.ISFORWARD); 463 if (isForward != null) { 464 if (!(Boolean) isForward) { 465 flipIcon(NamedIcon.HORIZONTALFLIP); 466 } 467 } 468 return null; 469 } 470 } 471 472 protected jmri.jmrit.roster.RosterEntry re = null; 473 474 /*As the size of a memory label can change we want to adjust the position of the x,y 475 if the width is fixed*/ 476 @SuppressWarnings("hiding") // Overriding value from SwingConstants 477 static final int LEFT = 0x00; 478 @SuppressWarnings("hiding") // Overriding value from SwingConstants 479 static final int RIGHT = 0x02; 480 static final int CENTRE = 0x04; 481 482 @Override 483 public void updateSize() { 484 if (_popupUtil.getFixedWidth() == 0) { 485 //setSize(maxWidth(), maxHeight()); 486 switch (_popupUtil.getJustification()) { 487 case LEFT: 488 super.setLocation(getOriginalX(), getOriginalY()); 489 break; 490 case RIGHT: 491 super.setLocation(getOriginalX() - maxWidth(), getOriginalY()); 492 break; 493 case CENTRE: 494 super.setLocation(getOriginalX() - (maxWidth() / 2), getOriginalY()); 495 break; 496 default: 497 log.warn("Unhandled justification code: {}", _popupUtil.getJustification()); 498 break; 499 } 500 setSize(maxWidth(), maxHeight()); 501 setPreferredSize(new Dimension(maxWidth(), maxHeight())); 502 } else { 503 super.updateSize(); 504 if (_icon && _namedIcon != null) { 505 _namedIcon.reduceTo(maxWidthTrue(), maxHeightTrue(), 0.2); 506 } 507 } 508 } 509 510 /*Stores the original location of the memory, this is then used to calculate 511 the position of the text dependant upon the justification*/ 512 private int originalX = 0; 513 private int originalY = 0; 514 515 public void setOriginalLocation(int x, int y) { 516 originalX = x; 517 originalY = y; 518 updateSize(); 519 } 520 521 @Override 522 public int getOriginalX() { 523 return originalX; 524 } 525 526 @Override 527 public int getOriginalY() { 528 return originalY; 529 } 530 531 @Override 532 public void setLocation(int x, int y) { 533 if (_popupUtil.getFixedWidth() == 0) { 534 setOriginalLocation(x, y); 535 } else { 536 super.setLocation(x, y); 537 } 538 } 539 540 @Override 541 public boolean setEditIconMenu(JPopupMenu popup) { 542 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameMemory")); 543 popup.add(new AbstractAction(txt) { 544 @Override 545 public void actionPerformed(ActionEvent e) { 546 edit(); 547 } 548 }); 549 return true; 550 } 551 552 @Override 553 protected void edit() { 554 makeIconEditorFrame(this, "Memory", true, null); 555 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.memoryPickModelInstance()); 556 ActionListener addIconAction = (ActionEvent a) -> editMemory(); 557 _iconEditor.complete(addIconAction, false, true, true); 558 _iconEditor.setSelection(getMemory()); 559 } 560 561 void editMemory() { 562 setMemory(_iconEditor.getTableSelection().getDisplayName()); 563 updateSize(); 564 _iconEditorFrame.dispose(); 565 _iconEditorFrame = null; 566 _iconEditor = null; 567 invalidate(); 568 } 569 570 @Override 571 public void dispose() { 572 if (getMemory() != null) { 573 getMemory().removePropertyChangeListener(this); 574 } 575 namedMemory = null; 576 if (re != null) { 577 jmri.InstanceManager.throttleManagerInstance().removeListener(re.getDccLocoAddress(), this); 578 re = null; 579 } 580 super.dispose(); 581 } 582 583 @Override 584 public void doMouseClicked(JmriMouseEvent e) { 585 if (e.getClickCount() == 2) { // double click? 586 if (!getEditor().isEditable() && isValueEditDisabled()) { 587 log.debug("Double click memory value edit is disabled"); 588 return; 589 } 590 editMemoryValue(); 591 } 592 } 593 594 protected void editMemoryValue() { 595 596 String reval = (String)JmriJOptionPane.showInputDialog(this, 597 Bundle.getMessage("EditCurrentMemoryValue", namedMemory.getName()), 598 getMemory().getValue()); 599 600 setValue(reval); 601 updateSize(); 602 } 603 604 //This is used by the LayoutEditor 605 protected boolean updateBlockValue = false; 606 607 public void updateBlockValueOnChange(boolean boo) { 608 updateBlockValue = boo; 609 } 610 611 public boolean updateBlockValueOnChange() { 612 return updateBlockValue; 613 } 614 615 protected boolean flipRosterIcon = false; 616 617 protected void addRosterToIcon(RosterEntry roster) { 618 Object[] options = {"Facing West", 619 "Facing East", 620 "Do Not Add"}; 621 int n = JmriJOptionPane.showOptionDialog(this, // TODO I18N 622 "Would you like to assign loco " 623 + roster.titleString() + " to this location", 624 "Assign Loco", 625 JmriJOptionPane.DEFAULT_OPTION, 626 JmriJOptionPane.QUESTION_MESSAGE, 627 null, 628 options, 629 options[2]); 630 if ( n == 2 || n==JmriJOptionPane.CLOSED_OPTION ) { // option array 2 Do Not Add, or Dialog closed 631 return; 632 } 633 flipRosterIcon = (n == 0); // true if option array position 0, Facing West 634 if (getValue() == roster) { 635 //No change in the loco but a change in direction facing might have occurred 636 updateIconFromRosterVal(roster); 637 } else { 638 setValue(roster); 639 } 640 } 641 642 protected Object getValue() { 643 if (getMemory() == null) { 644 return null; 645 } 646 return getMemory().getValue(); 647 } 648 649 protected void setValue(Object val) { 650 getMemory().setValue(val); 651 } 652 653 class TransferHandler extends javax.swing.TransferHandler { 654 @Override 655 public boolean canImport(JComponent c, DataFlavor[] transferFlavors) { 656 for (DataFlavor flavor : transferFlavors) { 657 if (RosterEntrySelection.rosterEntryFlavor.equals(flavor)) { 658 return true; 659 } 660 } 661 return false; 662 } 663 664 @Override 665 public boolean importData(JComponent c, Transferable t) { 666 try { 667 ArrayList<RosterEntry> REs = RosterEntrySelection.getRosterEntries(t); 668 for (RosterEntry roster : REs) { 669 addRosterToIcon(roster); 670 } 671 } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException e) { 672 log.error("Could not add a RosterEntry to Icon.", e); 673 } 674 return true; 675 } 676 677 } 678 679 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MemoryIcon.class); 680 681}