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