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            final java.util.Vector<String> aspects = getSignalMast().getValidAspects();
304            for (int i = 0; i < aspects.size(); i++) {
305                final int index = i;
306                popup.add(new AbstractAction(aspects.elementAt(index)) {
307                    @Override
308                    public void actionPerformed(ActionEvent e) {
309                        getSignalMast().setAspect(aspects.elementAt(index));
310                    }
311                });
312            }
313        }
314        return true;
315    }
316
317    private void addTransitPopup(JPopupMenu popup) {
318        if ((InstanceManager.getDefault(jmri.SectionManager.class).getNamedBeanSet().size()) > 0
319                && jmri.InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).isAdvancedRoutingEnabled()) {
320
321            if (tct == null) {
322                tct = new jmri.jmrit.display.layoutEditor.TransitCreationTool();
323            }
324            popup.addSeparator();
325            String addString = Bundle.getMessage("MenuTransitCreate");
326            if (tct.isToolInUse()) {
327                addString = Bundle.getMessage("MenuTransitAddTo");
328            }
329            popup.add(new AbstractAction(addString) {
330                @Override
331                public void actionPerformed(ActionEvent e) {
332                    try {
333                        tct.addNamedBean(getSignalMast());
334                    } catch (jmri.JmriException ex) {
335                        JmriJOptionPane.showMessageDialog(null, ex.getMessage(),
336                            Bundle.getMessage("TransitErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
337                    }
338                }
339            });
340            if (tct.isToolInUse()) {
341                popup.add(new AbstractAction(Bundle.getMessage("MenuTransitAddComplete")) {
342                    @Override
343                    public void actionPerformed(ActionEvent e) {
344                        Transit created;
345                        try {
346                            tct.addNamedBean(getSignalMast());
347                            created = tct.createTransit();
348                            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("TransitCreatedMessage", created.getDisplayName()),
349                                Bundle.getMessage("TransitCreatedTitle"), JmriJOptionPane.INFORMATION_MESSAGE);
350                        } catch (jmri.JmriException ex) {
351                            JmriJOptionPane.showMessageDialog(null, ex.getMessage(),
352                                Bundle.getMessage("TransitErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
353                        }
354                    }
355                });
356                popup.add(new AbstractAction(Bundle.getMessage("MenuTransitCancel")) {
357                    @Override
358                    public void actionPerformed(ActionEvent e) {
359                        tct.cancelTransitCreate();
360                    }
361                });
362            }
363            popup.addSeparator();
364        }
365    }
366
367    static volatile jmri.jmrit.display.layoutEditor.TransitCreationTool tct;
368
369    private void setImageTypeList(ButtonGroup iconTypeGroup, JMenu iconSetMenu, final String item) {
370        JRadioButtonMenuItem im;
371        im = new JRadioButtonMenuItem(item);
372        im.addActionListener(e -> useIconSet(item));
373        iconTypeGroup.add(im);
374        if (useIconSet.equals(item)) {
375            im.setSelected(true);
376        } else {
377            im.setSelected(false);
378        }
379        iconSetMenu.add(im);
380
381    }
382
383    @Override
384    public boolean setRotateOrthogonalMenu(JPopupMenu popup) {
385        return false;
386    }
387
388    SignalMastItemPanel _itemPanel;
389
390    @Override
391    public boolean setEditItemMenu(JPopupMenu popup) {
392        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSignalMast"));
393        popup.add(new AbstractAction(txt) {
394            @Override
395            public void actionPerformed(ActionEvent e) {
396                editItem();
397            }
398        });
399        return true;
400    }
401
402    protected void editItem() {
403        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
404                Bundle.getMessage("BeanNameSignalMast")));
405        _itemPanel = new SignalMastItemPanel(_paletteFrame, "SignalMast", getFamily(),
406                PickListModel.signalMastPickModelInstance());
407        ActionListener updateAction = a -> updateItem();
408        // _iconMap keys with local names - Let SignalHeadItemPanel figure this out
409        _itemPanel.init(updateAction, _iconMap);
410        _itemPanel.setSelection(getSignalMast());
411        initPaletteFrame(_paletteFrame, _itemPanel);
412    }
413
414    void updateItem() {
415        setSignalMast(_itemPanel.getTableSelection().getSystemName());
416        setFamily(_itemPanel.getFamilyName());
417        finishItemUpdate(_paletteFrame, _itemPanel);
418    }
419
420    /**
421     * Change the SignalMast aspect when the icon is clicked.
422     *
423     */
424    @Override
425    public void doMouseClicked(JmriMouseEvent e) {
426        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
427            return;
428        }
429        performMouseClicked(e);
430    }
431
432    /**
433     * Handle mouse clicks when no modifier keys are pressed. Mouse clicks with
434     * modifier keys pressed can be processed by the containing component.
435     *
436     * @param e the mouse click event
437     */
438    public void performMouseClicked(JmriMouseEvent e) {
439        if (e.isMetaDown() || e.isAltDown()) {
440            return;
441        }
442        if (getSignalMast() == null) {
443            log.error("No turnout connection, can't process click");
444            return;
445        }
446        switch (clickMode) {
447            case 0:
448                java.util.Vector<String> aspects = getSignalMast().getValidAspects();
449                int idx = aspects.indexOf(getSignalMast().getAspect()) + 1;
450                if (idx >= aspects.size()) {
451                    idx = 0;
452                }
453                getSignalMast().setAspect(aspects.elementAt(idx));
454                return;
455            case 1:
456                getSignalMast().setLit(!getSignalMast().getLit());
457                return;
458            case 2:
459                getSignalMast().setHeld(!getSignalMast().getHeld());
460                return;
461            default:
462                log.error("Click in mode {}", clickMode);
463        }
464    }
465
466    String useIconSet = "default";
467
468    public void useIconSet(String icon) {
469        if (icon == null) {
470            icon = "default";
471        }
472        if (useIconSet.equals(icon)) {
473            return;
474        }
475        //clear the old icon map out.
476        _iconMap = null;
477        useIconSet = icon;
478        getIcons();
479        displayState(mastState());
480        _editor.getTargetPanel().repaint();
481    }
482
483    public String useIconSet() {
484        return useIconSet;
485    }
486
487    /**
488     * Set display of ClipBoard copied or duplicated mast
489     */
490    @Override
491    public void displayState(int s) {
492        displayState(mastState());
493    }
494
495    /**
496     * Drive the current state of the display from the state of the underlying
497     * SignalMast object.
498     *
499     * @param state the state to display
500     */
501    public void displayState(String state) {
502        updateSize();
503        if (log.isDebugEnabled()) { // Avoid signal lookup unless needed
504            if (getSignalMast() == null) {
505                log.debug("Display state {}, disconnected", state);
506            } else {
507                log.debug("Display state {} for {}", state, getSignalMast().getSystemName());
508            }
509        }
510        if (isText()) {
511            if (getSignalMast().getHeld()) {
512                if (isText()) {
513                    super.setText(Bundle.getMessage("Held"));
514                }
515                return;
516            } else if (getLitMode() && !getSignalMast().getLit()) {
517                super.setText(Bundle.getMessage("Dark"));
518                return;
519            }
520            super.setText(state);
521        }
522        if (isIcon()) {
523            if ((state != null) && (getSignalMast() != null)) {
524                String s = getSignalMast().getAppearanceMap().getImageLink(state, useIconSet);
525                if ((getSignalMast().getHeld()) && (getSignalMast().getAppearanceMap().getSpecificAppearance(jmri.SignalAppearanceMap.HELD) != null)) {
526                    s = getSignalMast().getAppearanceMap().getImageLink("$held", useIconSet);
527                } else if (getLitMode() && !getSignalMast().getLit() && (getSignalMast().getAppearanceMap().getImageLink("$dark", useIconSet) != null)) {
528                    s = getSignalMast().getAppearanceMap().getImageLink("$dark", useIconSet);
529                }
530                if (s.equals("")) {
531                    /*We have no appearance to set, therefore we will exit at this point.
532                     This can be considered normal if we are requesting an appearance
533                     that is not support or configured, such as dark or held */
534                    return;
535                }
536                if (!s.contains("preference:")) {
537                    s = s.substring(s.indexOf("resources"));
538                }
539
540                // tiny global cache, due to number of icons
541                if (_iconMap == null) {
542                    getIcons();
543                }
544                NamedIcon n = _iconMap.get(s);
545                super.setIcon(n);
546                updateSize();
547                setSize(n.getIconWidth(), n.getIconHeight());
548            }
549        } else {
550            super.setIcon(null);
551        }
552        return;
553    }
554
555    @Override
556    public boolean setEditIconMenu(JPopupMenu popup) {
557        return false;
558    }
559
560    @Override
561    protected void rotateOrthogonal() {
562        super.rotateOrthogonal();
563        // bug fix, must repaint icons that have same width and height
564        displayState(mastState());
565        repaint();
566    }
567
568    @Override
569    public void rotate(int deg) {
570        super.rotate(deg);
571        if (getSignalMast() != null) {
572            displayState(mastState());
573        }
574    }
575
576    @Override
577    public void setScale(double s) {
578        super.setScale(s);
579        if (getSignalMast() != null) {
580            displayState(mastState());
581        }
582    }
583
584    /**
585     * What to do on click? 0 means sequence through aspects; 1 means alternate
586     * the "lit" aspect; 2 means alternate the
587     * {@link jmri.SignalAppearanceMap#HELD} aspect.
588     */
589    protected int clickMode = 0;
590
591    public void setClickMode(int mode) {
592        clickMode = mode;
593    }
594
595    public int getClickMode() {
596        return clickMode;
597    }
598
599    /**
600     * How to handle lit vs not lit?
601     * <p>
602     * False means ignore (always show R/Y/G/etc appearance on screen); True
603     * means show {@link jmri.SignalAppearanceMap#DARK} if lit is set false.
604     */
605    protected boolean litMode = false;
606
607    public void setLitMode(boolean mode) {
608        litMode = mode;
609    }
610
611    public boolean getLitMode() {
612        return litMode;
613    }
614
615    @Override
616    public void dispose() {
617        if (namedMast != null) {
618            getSignalMast().removePropertyChangeListener(this);
619        }
620        super.dispose();
621    }
622
623    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalMastIcon.class);
624
625}