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