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