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}