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