001package jmri.jmrit.display;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005
006import javax.annotation.Nonnull;
007import javax.swing.AbstractAction;
008import javax.swing.ButtonGroup;
009import javax.swing.JMenu;
010import javax.swing.JPopupMenu;
011import javax.swing.JRadioButtonMenuItem;
012
013import jmri.InstanceManager;
014import jmri.NamedBeanHandle;
015import jmri.SignalMast;
016import jmri.Transit;
017import jmri.NamedBean.DisplayOptions;
018import jmri.jmrit.catalog.NamedIcon;
019import jmri.jmrit.display.palette.SignalMastItemPanel;
020import jmri.jmrit.picker.PickListModel;
021import jmri.util.swing.JmriJOptionPane;
022import jmri.util.swing.JmriMouseEvent;
023
024/**
025 * An icon to display a status of a {@link jmri.SignalMast}.
026 * <p>
027 * The icons displayed are loaded from the {@link jmri.SignalAppearanceMap} in
028 * the {@link jmri.SignalMast}.
029 *
030 * @see jmri.SignalMastManager
031 * @see jmri.InstanceManager
032 * @author Bob Jacobsen Copyright (C) 2009, 2014
033 */
034public class SignalMastIcon extends PositionableIcon implements java.beans.PropertyChangeListener {
035
036    public SignalMastIcon(Editor editor) {
037        // super ctor call to make sure this is an icon label
038        super(editor);
039        _control = true;
040    }
041
042    private NamedBeanHandle<SignalMast> namedMast;
043
044    public void setShowAutoText(boolean state) {
045        _text = state;
046        _icon = !_text;
047    }
048
049    @Override
050    public Positionable deepClone() {
051        SignalMastIcon pos = new SignalMastIcon(_editor);
052        return finishClone(pos);
053    }
054
055    protected Positionable finishClone(SignalMastIcon pos) {
056        pos.setSignalMast(getNamedSignalMast());
057        pos._iconMap = cloneMap(_iconMap, pos);
058        pos.setClickMode(getClickMode());
059        pos.setLitMode(getLitMode());
060        pos.useIconSet(useIconSet());
061        return super.finishClone(pos);
062    }
063
064    /**
065     * Attached a signalmast element to this display item
066     *
067     * @param sh Specific SignalMast handle
068     */
069    public void setSignalMast(NamedBeanHandle<SignalMast> sh) {
070        if (namedMast != null) {
071            getSignalMast().removePropertyChangeListener(this);
072        }
073        namedMast = sh;
074        if (namedMast != null) {
075            getIcons();
076            displayState(mastState());
077            getSignalMast().addPropertyChangeListener(this, namedMast.getName(), "SignalMast Icon");
078        }
079    }
080
081    /**
082     * Taken from the layout editor Attached a numbered element to this display
083     * item
084     *
085     * @param pName Used as a system/user name to lookup the SignalMast object
086     */
087    public void setSignalMast(String pName) {
088        SignalMast mMast = InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBean(pName);
089        if (mMast == null) {
090            log.warn("did not find a SignalMast named {}", pName);
091        } else {
092            setSignalMast(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, mMast));
093        }
094    }
095
096    private void getIcons() {
097        _iconMap = new java.util.HashMap<>();
098        java.util.Enumeration<String> e = getSignalMast().getAppearanceMap().getAspects();
099        boolean error = false;
100        while (e.hasMoreElements()) {
101            String aspect = e.nextElement();
102            error = loadIcons(aspect);
103        }
104        if (error) {
105            JmriJOptionPane.showMessageDialog(_editor.getTargetFrame(),
106                    java.text.MessageFormat.format(Bundle.getMessage("SignalMastIconLoadError"),
107                            new Object[]{getSignalMast().getDisplayName()}),
108                    Bundle.getMessage("SignalMastIconLoadErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
109        }
110        //Add in specific appearances for dark and held
111        loadIcons("$dark");
112        loadIcons("$held");
113    }
114
115    private boolean loadIcons(String aspect) {
116        String s = getSignalMast().getAppearanceMap().getImageLink(aspect, useIconSet);
117        if (s.isEmpty()) {
118            if (aspect.startsWith("$")) {
119                log.debug("No icon found for specific appearance {}", aspect);
120            } else {
121                log.error("No icon found for appearance {}", aspect);
122            }
123            return true;
124        } else {
125            if (!s.contains("preference:")) {
126                s = s.substring(s.indexOf("resources"));
127            }
128            NamedIcon n;
129            try {
130                n = new NamedIcon(s, s);
131            } catch (java.lang.NullPointerException e) {
132                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SignalMastIconLoadError2", new Object[]{aspect, s, getNameString()}),
133                    Bundle.getMessage("SignalMastIconLoadErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
134                log.error("{} : Cannot load Icon", Bundle.getMessage("SignalMastIconLoadError2", aspect, s, getNameString()));
135                return true;
136            }
137            _iconMap.put(s, n);
138            if (_rotate != 0) {
139                n.rotate(_rotate, this);
140            }
141            if (_scale != 1.0) {
142                n.scale(_scale, this);
143            }
144        }
145        return false;
146    }
147
148    public NamedBeanHandle<SignalMast> getNamedSignalMast() {
149        return namedMast;
150    }
151
152    public SignalMast getSignalMast() {
153        if (namedMast == null) {
154            return null;
155        }
156        return namedMast.getBean();
157    }
158
159    @Override
160    public jmri.NamedBean getNamedBean() {
161        return getSignalMast();
162    }
163
164    /**
165     * Get current appearance of the mast
166     *
167     * @return An aspect from the SignalMast
168     */
169    public String mastState() {
170        if (getSignalMast() == null) {
171            return "<empty>";
172        } else {
173            return getSignalMast().getAspect();
174        }
175    }
176
177    // update icon as state of turnout changes
178    @Override
179    public void propertyChange(java.beans.PropertyChangeEvent e) {
180        log.debug("property change: {} current state: {}", e.getPropertyName(), mastState());
181        displayState(mastState());
182        _editor.getTargetPanel().repaint();
183    }
184
185    @Override
186    @Nonnull
187    public String getTypeString() {
188        return Bundle.getMessage("PositionableType_SignalMastIcon");
189    }
190
191//    public String getPName() { return namedMast.getName(); }
192    @Override
193    public String getNameString() {
194        String name;
195        if (getSignalMast() == null) {
196            name = Bundle.getMessage("NotConnected");
197        } else {
198            name = getSignalMast().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
199        }
200        return name;
201    }
202
203    ButtonGroup litButtonGroup = null;
204
205    /**
206     * Pop-up just displays the name
207     */
208    @Override
209    public boolean showPopUp(JPopupMenu popup) {
210        if (isEditable()) {
211
212            JMenu clickMenu = new JMenu(Bundle.getMessage("WhenClicked"));
213            ButtonGroup clickButtonGroup = new ButtonGroup();
214            JRadioButtonMenuItem r;
215            r = new JRadioButtonMenuItem(Bundle.getMessage("ChangeAspect"));
216            r.addActionListener(e -> setClickMode(0));
217            clickButtonGroup.add(r);
218            if (clickMode == 0) {
219                r.setSelected(true);
220            } else {
221                r.setSelected(false);
222            }
223            clickMenu.add(r);
224
225            r = new JRadioButtonMenuItem(Bundle.getMessage("AlternateLit"));
226            r.addActionListener(e -> setClickMode(1));
227            clickButtonGroup.add(r);
228            if (clickMode == 1) {
229                r.setSelected(true);
230            } else {
231                r.setSelected(false);
232            }
233            clickMenu.add(r);
234            r = new JRadioButtonMenuItem(Bundle.getMessage("AlternateHeld"));
235            r.addActionListener(e -> setClickMode(2));
236            clickButtonGroup.add(r);
237            if (clickMode == 2) {
238                r.setSelected(true);
239            } else {
240                r.setSelected(false);
241            }
242            clickMenu.add(r);
243            popup.add(clickMenu);
244
245            // add menu to select handling of lit parameter
246            JMenu litMenu = new JMenu(Bundle.getMessage("WhenNotLit"));
247            litButtonGroup = new ButtonGroup();
248            r = new JRadioButtonMenuItem(Bundle.getMessage("ShowAppearance"));
249            r.setIconTextGap(10);
250            r.addActionListener(e -> {
251                setLitMode(false);
252                displayState(mastState());
253            });
254            litButtonGroup.add(r);
255            if (!litMode) {
256                r.setSelected(true);
257            } else {
258                r.setSelected(false);
259            }
260            litMenu.add(r);
261            r = new JRadioButtonMenuItem(Bundle.getMessage("ShowDarkIcon"));
262            r.setIconTextGap(10);
263            r.addActionListener(e -> {
264                setLitMode(true);
265                displayState(mastState());
266            });
267            litButtonGroup.add(r);
268            if (litMode) {
269                r.setSelected(true);
270            } else {
271                r.setSelected(false);
272            }
273            litMenu.add(r);
274            popup.add(litMenu);
275
276            if (namedMast != null) {
277                java.util.Enumeration<String> en = getSignalMast().getSignalSystem().getImageTypeList();
278                if (en.hasMoreElements()) {
279                    JMenu iconSetMenu = new JMenu(Bundle.getMessage("SignalMastIconSet"));
280                    ButtonGroup iconTypeGroup = new ButtonGroup();
281                    setImageTypeList(iconTypeGroup, iconSetMenu, "default");
282                    while (en.hasMoreElements()) {
283                        setImageTypeList(iconTypeGroup, iconSetMenu, en.nextElement());
284                    }
285                    popup.add(iconSetMenu);
286                }
287                popup.add(new jmri.jmrit.signalling.SignallingSourceAction(Bundle.getMessage("SignalMastLogic"), getSignalMast()));
288                JMenu aspect = new JMenu(Bundle.getMessage("ChangeAspect"));
289                final java.util.Vector<String> aspects = getSignalMast().getValidAspects();
290                for (int i = 0; i < aspects.size(); i++) {
291                    final int index = i;
292                    aspect.add(new AbstractAction(aspects.elementAt(index)) {
293                        @Override
294                        public void actionPerformed(ActionEvent e) {
295                            getSignalMast().setAspect(aspects.elementAt(index));
296                        }
297                    });
298                }
299                popup.add(aspect);
300            }
301            addTransitPopup(popup);
302        } else {
303            if (!isControlling()) {
304                log.debug("The signal mast icon is disabled, skip the aspect list popup");
305                return false;
306            }
307            final java.util.Vector<String> aspects = getSignalMast().getValidAspects();
308            for (int i = 0; i < aspects.size(); i++) {
309                final int index = i;
310                popup.add(new AbstractAction(aspects.elementAt(index)) {
311                    @Override
312                    public void actionPerformed(ActionEvent e) {
313                        getSignalMast().setAspect(aspects.elementAt(index));
314                    }
315                });
316            }
317        }
318        return true;
319    }
320
321    private void addTransitPopup(JPopupMenu popup) {
322        if ((InstanceManager.getDefault(jmri.SectionManager.class).getNamedBeanSet().size()) > 0
323                && jmri.InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).isAdvancedRoutingEnabled()) {
324
325            if (tct == null) {
326                tct = new jmri.jmrit.display.layoutEditor.TransitCreationTool();
327            }
328            popup.addSeparator();
329            String addString = Bundle.getMessage("MenuTransitCreate");
330            if (tct.isToolInUse()) {
331                addString = Bundle.getMessage("MenuTransitAddTo");
332            }
333            popup.add(new AbstractAction(addString) {
334                @Override
335                public void actionPerformed(ActionEvent e) {
336                    try {
337                        tct.addNamedBean(getSignalMast());
338                    } catch (jmri.JmriException ex) {
339                        JmriJOptionPane.showMessageDialog(null, ex.getMessage(),
340                            Bundle.getMessage("TransitErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
341                    }
342                }
343            });
344            if (tct.isToolInUse()) {
345                popup.add(new AbstractAction(Bundle.getMessage("MenuTransitAddComplete")) {
346                    @Override
347                    public void actionPerformed(ActionEvent e) {
348                        Transit created;
349                        try {
350                            tct.addNamedBean(getSignalMast());
351                            created = tct.createTransit();
352                            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("TransitCreatedMessage", created.getDisplayName()),
353                                Bundle.getMessage("TransitCreatedTitle"), JmriJOptionPane.INFORMATION_MESSAGE);
354                        } catch (jmri.JmriException ex) {
355                            JmriJOptionPane.showMessageDialog(null, ex.getMessage(),
356                                Bundle.getMessage("TransitErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
357                        }
358                    }
359                });
360                popup.add(new AbstractAction(Bundle.getMessage("MenuTransitCancel")) {
361                    @Override
362                    public void actionPerformed(ActionEvent e) {
363                        tct.cancelTransitCreate();
364                    }
365                });
366            }
367            popup.addSeparator();
368        }
369    }
370
371    static volatile jmri.jmrit.display.layoutEditor.TransitCreationTool tct;
372
373    private void setImageTypeList(ButtonGroup iconTypeGroup, JMenu iconSetMenu, final String item) {
374        JRadioButtonMenuItem im;
375        im = new JRadioButtonMenuItem(item);
376        im.addActionListener(e -> useIconSet(item));
377        iconTypeGroup.add(im);
378        if (useIconSet.equals(item)) {
379            im.setSelected(true);
380        } else {
381            im.setSelected(false);
382        }
383        iconSetMenu.add(im);
384
385    }
386
387    @Override
388    public boolean setRotateOrthogonalMenu(JPopupMenu popup) {
389        return false;
390    }
391
392    SignalMastItemPanel _itemPanel;
393
394    @Override
395    public boolean setEditItemMenu(JPopupMenu popup) {
396        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSignalMast"));
397        popup.add(new AbstractAction(txt) {
398            @Override
399            public void actionPerformed(ActionEvent e) {
400                editItem();
401            }
402        });
403        return true;
404    }
405
406    protected void editItem() {
407        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
408                Bundle.getMessage("BeanNameSignalMast")));
409        _itemPanel = new SignalMastItemPanel(_paletteFrame, "SignalMast", getFamily(),
410                PickListModel.signalMastPickModelInstance());
411        ActionListener updateAction = a -> updateItem();
412        // _iconMap keys with local names - Let SignalHeadItemPanel figure this out
413        _itemPanel.init(updateAction, _iconMap);
414        _itemPanel.setSelection(getSignalMast());
415        initPaletteFrame(_paletteFrame, _itemPanel);
416    }
417
418    void updateItem() {
419        setSignalMast(_itemPanel.getTableSelection().getSystemName());
420        setFamily(_itemPanel.getFamilyName());
421        finishItemUpdate(_paletteFrame, _itemPanel);
422    }
423
424    /**
425     * Change the SignalMast aspect when the icon is clicked.
426     *
427     */
428    @Override
429    public void doMouseClicked(JmriMouseEvent e) {
430        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
431            return;
432        }
433        performMouseClicked(e);
434    }
435
436    /**
437     * Handle mouse clicks when no modifier keys are pressed. Mouse clicks with
438     * modifier keys pressed can be processed by the containing component.
439     *
440     * @param e the mouse click event
441     */
442    public void performMouseClicked(JmriMouseEvent e) {
443        if (e.isMetaDown() || e.isAltDown()) {
444            return;
445        }
446        if (getSignalMast() == null) {
447            log.error("No turnout connection, can't process click");
448            return;
449        }
450        switch (clickMode) {
451            case 0:
452                java.util.Vector<String> aspects = getSignalMast().getValidAspects();
453                int idx = aspects.indexOf(getSignalMast().getAspect()) + 1;
454                if (idx >= aspects.size()) {
455                    idx = 0;
456                }
457                getSignalMast().setAspect(aspects.elementAt(idx));
458                return;
459            case 1:
460                getSignalMast().setLit(!getSignalMast().getLit());
461                return;
462            case 2:
463                getSignalMast().setHeld(!getSignalMast().getHeld());
464                return;
465            default:
466                log.error("Click in mode {}", clickMode);
467        }
468    }
469
470    String useIconSet = "default";
471
472    public void useIconSet(String icon) {
473        if (icon == null) {
474            icon = "default";
475        }
476        if (useIconSet.equals(icon)) {
477            return;
478        }
479        //clear the old icon map out.
480        _iconMap = null;
481        useIconSet = icon;
482        getIcons();
483        displayState(mastState());
484        _editor.getTargetPanel().repaint();
485    }
486
487    public String useIconSet() {
488        return useIconSet;
489    }
490
491    /**
492     * Set display of ClipBoard copied or duplicated mast
493     */
494    @Override
495    public void displayState(int s) {
496        displayState(mastState());
497    }
498
499    /**
500     * Drive the current state of the display from the state of the underlying
501     * SignalMast object.
502     *
503     * @param state the state to display
504     */
505    public void displayState(String state) {
506        updateSize();
507        if (log.isDebugEnabled()) { // Avoid signal lookup unless needed
508            if (getSignalMast() == null) {
509                log.debug("Display state {}, disconnected", state);
510            } else {
511                log.debug("Display state {} for {}", state, getSignalMast().getSystemName());
512            }
513        }
514        if (isText()) {
515            if (getSignalMast().getHeld()) {
516                if (isText()) {
517                    super.setText(Bundle.getMessage("Held"));
518                }
519                return;
520            } else if (getLitMode() && !getSignalMast().getLit()) {
521                super.setText(Bundle.getMessage("Dark"));
522                return;
523            }
524            super.setText(state);
525        }
526        if (isIcon()) {
527            if ((state != null) && (getSignalMast() != null)) {
528                String s = getSignalMast().getAppearanceMap().getImageLink(state, useIconSet);
529                if ((getSignalMast().getHeld()) && (getSignalMast().getAppearanceMap().getSpecificAppearance(jmri.SignalAppearanceMap.HELD) != null)) {
530                    s = getSignalMast().getAppearanceMap().getImageLink("$held", useIconSet);
531                } else if (getLitMode() && !getSignalMast().getLit() && (getSignalMast().getAppearanceMap().getImageLink("$dark", useIconSet) != null)) {
532                    s = getSignalMast().getAppearanceMap().getImageLink("$dark", useIconSet);
533                }
534                if (s.equals("")) {
535                    /*We have no appearance to set, therefore we will exit at this point.
536                     This can be considered normal if we are requesting an appearance
537                     that is not support or configured, such as dark or held */
538                    return;
539                }
540                if (!s.contains("preference:")) {
541                    s = s.substring(s.indexOf("resources"));
542                }
543
544                // tiny global cache, due to number of icons
545                if (_iconMap == null) {
546                    getIcons();
547                }
548                NamedIcon n = _iconMap.get(s);
549                super.setIcon(n);
550                updateSize();
551                setSize(n.getIconWidth(), n.getIconHeight());
552            }
553        } else {
554            super.setIcon(null);
555        }
556        return;
557    }
558
559    @Override
560    public boolean setEditIconMenu(JPopupMenu popup) {
561        return false;
562    }
563
564    @Override
565    protected void rotateOrthogonal() {
566        super.rotateOrthogonal();
567        // bug fix, must repaint icons that have same width and height
568        displayState(mastState());
569        repaint();
570    }
571
572    @Override
573    public void rotate(int deg) {
574        super.rotate(deg);
575        if (getSignalMast() != null) {
576            displayState(mastState());
577        }
578    }
579
580    @Override
581    public void setScale(double s) {
582        super.setScale(s);
583        if (getSignalMast() != null) {
584            displayState(mastState());
585        }
586    }
587
588    /**
589     * What to do on click? 0 means sequence through aspects; 1 means alternate
590     * the "lit" aspect; 2 means alternate the
591     * {@link jmri.SignalAppearanceMap#HELD} aspect.
592     */
593    protected int clickMode = 0;
594
595    public void setClickMode(int mode) {
596        clickMode = mode;
597    }
598
599    public int getClickMode() {
600        return clickMode;
601    }
602
603    /**
604     * How to handle lit vs not lit?
605     * <p>
606     * False means ignore (always show R/Y/G/etc appearance on screen); True
607     * means show {@link jmri.SignalAppearanceMap#DARK} if lit is set false.
608     */
609    protected boolean litMode = false;
610
611    public void setLitMode(boolean mode) {
612        litMode = mode;
613    }
614
615    public boolean getLitMode() {
616        return litMode;
617    }
618
619    @Override
620    public void dispose() {
621        if (namedMast != null) {
622            getSignalMast().removePropertyChangeListener(this);
623        }
624        super.dispose();
625    }
626
627    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalMastIcon.class);
628
629}