001package jmri.jmrit.display;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.util.HashMap;
006import java.util.Hashtable;
007import java.util.Map.Entry;
008
009import javax.annotation.Nonnull;
010import javax.swing.AbstractAction;
011import javax.swing.ButtonGroup;
012import javax.swing.JMenu;
013import javax.swing.JPopupMenu;
014import javax.swing.JRadioButtonMenuItem;
015
016import jmri.InstanceManager;
017import jmri.NamedBeanHandle;
018import jmri.SignalHead;
019import jmri.jmrit.catalog.NamedIcon;
020import jmri.jmrit.display.palette.SignalHeadItemPanel;
021import jmri.jmrit.picker.PickListModel;
022import jmri.util.swing.JmriMouseEvent;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * An icon to display a status of a SignalHead.
029 * <p>
030 * SignalHeads are located via the SignalHeadManager, which in turn is located
031 * via the InstanceManager.
032 *
033 * @see jmri.SignalHeadManager
034 * @see jmri.InstanceManager
035 * @author Bob Jacobsen Copyright (C) 2001, 2002
036 */
037public class SignalHeadIcon extends PositionableIcon implements java.beans.PropertyChangeListener {
038
039    private String[] _validKeys;
040
041    public SignalHeadIcon(Editor editor) {
042        super(editor);
043        _control = true;
044    }
045
046    @Override
047    public Positionable deepClone() {
048        SignalHeadIcon pos = new SignalHeadIcon(_editor);
049        return finishClone(pos);
050    }
051
052    protected Positionable finishClone(SignalHeadIcon pos) {
053        pos.setSignalHead(getNamedSignalHead().getName());
054        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
055            pos.setIcon(entry.getKey(), entry.getValue());
056        }
057        pos.setClickMode(getClickMode());
058        pos.setLitMode(getLitMode());
059        return super.finishClone(pos);
060    }
061
062    // private SignalHead mHead;
063    private NamedBeanHandle<SignalHead> namedHead;
064
065    private HashMap<String, NamedIcon> _saveMap;
066
067    /**
068     * Attach a SignalHead element to this display item by bean.
069     *
070     * @param sh the specific SignalHead object to attach
071     */
072    public void setSignalHead(NamedBeanHandle<SignalHead> sh) {
073        if (namedHead != null) {
074            getSignalHead().removePropertyChangeListener(this);
075        }
076        namedHead = sh;
077        if (namedHead != null) {
078            _iconMap = new HashMap<>();
079            _validKeys = getSignalHead().getValidStateKeys();
080            displayState(headState());
081            getSignalHead().addPropertyChangeListener(this, namedHead.getName(), "SignalHead Icon");
082        }
083    }
084
085    /**
086     * Attach a SignalHead element to this display item by name. Taken from the
087     * Layout Editor.
088     *
089     * @param pName Used as a system/user name to lookup the SignalHead object
090     */
091    public void setSignalHead(String pName) {
092        SignalHead mHead = InstanceManager.getDefault(jmri.SignalHeadManager.class).getNamedBean(pName);
093        if (mHead == null) {
094            log.warn("did not find a SignalHead named {}", pName);
095        } else {
096            setSignalHead(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, mHead));
097        }
098    }
099
100    public NamedBeanHandle<SignalHead> getNamedSignalHead() {
101        return namedHead;
102    }
103
104    public SignalHead getSignalHead() {
105        if (namedHead == null) {
106            return null;
107        }
108        return namedHead.getBean();
109    }
110
111    @Override
112    public jmri.NamedBean getNamedBean() {
113        return getSignalHead();
114    }
115
116    /**
117     * Place icon by its non-localized bean state name.
118     *
119     * @param state the non-localized state
120     * @param icon  the icon to place
121     */
122    public void setIcon(String state, NamedIcon icon) {
123        log.debug("setIcon for {}", state);
124        if (isValidState(state)) {
125            _iconMap.put(state, icon);
126            displayState(headState());
127        }
128    }
129
130    /**
131     * Check that device supports the state. Valid state names returned by the
132     * bean are (non-localized) property key names.
133     */
134    private boolean isValidState(String key) {
135        if (key == null) {
136            return false;
137        }
138        if (key.equals("SignalHeadStateDark") || key.equals("SignalHeadStateHeld")) {
139            log.debug("{} is a valid state.", key);
140            return true;
141        }
142        for (String valid : _validKeys) {
143            if (key.equals(valid)) {
144                log.debug("{} is a valid state.", key);
145                return true;
146            }
147        }
148        log.debug("{} is NOT a valid state.", key);
149        return false;
150    }
151
152    /**
153     * Get current appearance of the head.
154     *
155     * @return an appearance variable from a SignalHead, e.g. SignalHead.RED
156     */
157    public int headState() {
158        if (getSignalHead() == null) {
159            return 0;
160        } else {
161            return getSignalHead().getAppearance();
162        }
163    }
164
165    // update icon as state of turnout changes
166    @Override
167    public void propertyChange(java.beans.PropertyChangeEvent e) {
168        log.debug("property change: {} current state: {}", e.getPropertyName(), headState());
169        displayState(headState());
170        _editor.getTargetPanel().repaint();
171    }
172
173    @Override
174    @Nonnull
175    public String getTypeString() {
176        return Bundle.getMessage("PositionableType_SignalHead");
177    }
178
179    @Override
180    public @Nonnull
181    String getNameString() {
182        if (namedHead == null) {
183            return Bundle.getMessage("NotConnected");
184        }
185        return namedHead.getName(); // short NamedIcon name
186    }
187
188    private ButtonGroup litButtonGroup = null;
189
190    /**
191     * Pop-up just displays the name
192     */
193    @Override
194    public boolean showPopUp(JPopupMenu popup) {
195        if (isEditable()) {
196            // add menu to select action on click
197            JMenu clickMenu = new JMenu(Bundle.getMessage("WhenClicked"));
198            ButtonGroup clickButtonGroup = new ButtonGroup();
199            JRadioButtonMenuItem r;
200            r = new JRadioButtonMenuItem(Bundle.getMessage("ChangeAspect"));
201            r.addActionListener(e -> setClickMode(3));
202            clickButtonGroup.add(r);
203            if (clickMode == 3) {
204                r.setSelected(true);
205            } else {
206                r.setSelected(false);
207            }
208            clickMenu.add(r);
209            r = new JRadioButtonMenuItem(Bundle.getMessage("Cycle3Aspects"));
210            r.addActionListener(e -> setClickMode(0));
211            clickButtonGroup.add(r);
212            if (clickMode == 0) {
213                r.setSelected(true);
214            } else {
215                r.setSelected(false);
216            }
217            clickMenu.add(r);
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 -> setLitMode(false));
244            litButtonGroup.add(r);
245            if (!litMode) {
246                r.setSelected(true);
247            } else {
248                r.setSelected(false);
249            }
250            litMenu.add(r);
251            r = new JRadioButtonMenuItem(Bundle.getMessage("ShowDarkIcon"));
252            r.setIconTextGap(10);
253            r.addActionListener(e -> setLitMode(true));
254            litButtonGroup.add(r);
255            if (litMode) {
256                r.setSelected(true);
257            } else {
258                r.setSelected(false);
259            }
260            litMenu.add(r);
261            popup.add(litMenu);
262
263            popup.add(new AbstractAction(Bundle.getMessage("EditLogic")) {
264                @Override
265                public void actionPerformed(ActionEvent e) {
266                    jmri.jmrit.blockboss.BlockBossFrame f = new jmri.jmrit.blockboss.BlockBossFrame();
267                    String name = getNameString();
268                    f.setTitle(java.text.MessageFormat.format(Bundle.getMessage("SignalLogic"), name));
269                    f.setSignal(getSignalHead());
270                    f.setVisible(true);
271                }
272            });
273            return true;
274        }
275        return false;
276    }
277
278    /**
279     * ************* popup AbstractAction.actionPerformed method overrides
280     * ***********
281     */
282    @Override
283    protected void rotateOrthogonal() {
284        super.rotateOrthogonal();
285        displayState(headState());
286    }
287
288    @Override
289    public void setScale(double s) {
290        super.setScale(s);
291        displayState(headState());
292    }
293
294    @Override
295    public void rotate(int deg) {
296        super.rotate(deg);
297        displayState(headState());
298    }
299
300    /**
301     * Drive the current state of the display from the state of the underlying
302     * SignalHead object.
303     * <ul>
304     * <li>If the signal is held, display that.
305     * <li>If set to monitor the status of the lit parameter and lit is false,
306     * show the dark icon ("dark", when set as an explicit appearance, is
307     * displayed anyway)
308     * <li>Show the icon corresponding to one of the (max seven) appearances.
309     * </ul>
310     */
311    @Override
312    public void displayState(int state) {
313        updateSize();
314        if (getSignalHead() == null) {
315            log.debug("Display state {}, disconnected", state);
316            return;
317        }
318        log.debug("Display state {} for {}", state, getNameString());
319        if (getSignalHead().getHeld()) {
320            if (isText()) {
321                super.setText(Bundle.getMessage("Held"));
322            }
323            if (isIcon()) {
324                super.setIcon(_iconMap.get("SignalHeadStateHeld"));
325            }
326        } else if (getLitMode() && !getSignalHead().getLit()) {
327            if (isText()) {
328                super.setText(Bundle.getMessage("Dark"));
329            }
330            if (isIcon()) {
331                super.setIcon(_iconMap.get("SignalHeadStateDark"));
332            }
333        } else {
334            if (isText()) {
335                super.setText(Bundle.getMessage(getSignalHead().getAppearanceKey(state)));
336            }
337            if (isIcon()) {
338                NamedIcon icon = _iconMap.get(getSignalHead().getAppearanceKey(state));
339                if (icon != null) {
340                    super.setIcon(icon);
341                }
342            }
343        }
344    }
345
346    private SignalHeadItemPanel _itemPanel;
347
348    @Override
349    public boolean setEditItemMenu(JPopupMenu popup) {
350        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
351                Bundle.getMessage("BeanNameSignalHead"));
352        popup.add(new AbstractAction(txt) {
353            @Override
354            public void actionPerformed(ActionEvent e) {
355                editItem();
356            }
357        });
358        return true;
359    }
360
361    protected void editItem() {
362        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
363                Bundle.getMessage("BeanNameSignalHead")));
364        _itemPanel = new SignalHeadItemPanel(_paletteFrame, "SignalHead", getFamily(),
365                PickListModel.signalHeadPickModelInstance()); // NOI18N
366        ActionListener updateAction = a -> updateItem();
367        // _iconMap keys with non-localized keys
368        // duplicate _iconMap map with unscaled and unrotated icons
369        HashMap<String, NamedIcon> map = new HashMap<>();
370        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
371            NamedIcon oldIcon = entry.getValue();
372            NamedIcon newIcon = cloneIcon(oldIcon, this);
373            newIcon.rotate(0, this);
374            newIcon.scale(1.0, this);
375            newIcon.setRotation(4, this);
376            map.put(entry.getKey(), newIcon);
377        }
378        _itemPanel.init(updateAction, map);
379        _itemPanel.setSelection(getSignalHead());
380        initPaletteFrame(_paletteFrame, _itemPanel);
381    }
382
383    void updateItem() {
384        _saveMap = _iconMap;  // setSignalHead() clears _iconMap. We need a copy for setIcons()
385        setSignalHead(_itemPanel.getTableSelection().getSystemName());
386        setFamily(_itemPanel.getFamilyName());
387        HashMap<String, NamedIcon> map1 = _itemPanel.getIconMap();
388        if (map1 != null) {
389            // map1 may be keyed with NamedBean names. Convert to local name keys.
390            Hashtable<String, NamedIcon> map2 = new Hashtable<>();
391            for (Entry<String, NamedIcon> entry : map1.entrySet()) {
392                map2.put(entry.getKey(), entry.getValue());
393            }
394            setIcons(map2);
395        }   // otherwise retain current map
396        displayState(getSignalHead().getAppearance());
397        finishItemUpdate(_paletteFrame, _itemPanel);
398    }
399
400    @Override
401    public boolean setEditIconMenu(JPopupMenu popup) {
402        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSignalHead"));
403        popup.add(new AbstractAction(txt) {
404            @Override
405            public void actionPerformed(ActionEvent e) {
406                edit();
407            }
408        });
409        return true;
410    }
411
412    @Override
413    protected void edit() {
414        makeIconEditorFrame(this, "SignalHead", true, null);
415        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.signalHeadPickModelInstance());
416        int i = 0;
417        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
418            _iconEditor.setIcon(i++, entry.getKey(), new NamedIcon(entry.getValue()));
419        }
420        _iconEditor.makeIconPanel(false);
421
422        ActionListener addIconAction = a -> updateSignal();
423        _iconEditor.complete(addIconAction, true, false, true);
424        _iconEditor.setSelection(getSignalHead());
425    }
426
427    /**
428     * Replace the icons in _iconMap with those from map, but preserve the scale
429     * and rotation.
430     */
431    private void setIcons(Hashtable<String, NamedIcon> map) {
432        HashMap<String, NamedIcon> tempMap = new HashMap<>();
433        for (Entry<String, NamedIcon> entry : map.entrySet()) {
434            String name = entry.getKey();
435            NamedIcon icon = entry.getValue();
436            NamedIcon oldIcon = _saveMap.get(name); // setSignalHead() has cleared _iconMap
437            log.debug("key= {}, localKey= {}, newIcon= {}, oldIcon= {}", entry.getKey(), name, icon, oldIcon);
438            if (oldIcon != null) {
439                icon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
440                icon.setRotation(oldIcon.getRotation(), this);
441            }
442            tempMap.put(name, icon);
443        }
444        _iconMap = tempMap;
445    }
446
447    void updateSignal() {
448        _saveMap = _iconMap;  // setSignalHead() clears _iconMap. We need a copy for setIcons()
449        if (_iconEditor != null) {
450            setSignalHead(_iconEditor.getTableSelection().getDisplayName());
451            setIcons(_iconEditor.getIconMap());
452            _iconEditorFrame.dispose();
453            _iconEditorFrame = null;
454            _iconEditor = null;
455            invalidate();
456        }
457        displayState(headState());
458    }
459
460    /**
461     * What to do on click? 0 means sequence through aspects; 1 means alternate
462     * the "lit" aspect; 2 means alternate the "held" aspect.
463     */
464    protected int clickMode = 3;
465
466    public void setClickMode(int mode) {
467        clickMode = mode;
468    }
469
470    public int getClickMode() {
471        return clickMode;
472    }
473
474    /**
475     * How to handle lit vs not lit?
476     * <p>
477     * False means ignore (always show R/Y/G/etc appearance on screen); True
478     * means show "dark" if lit is set false.
479     * <p>
480     * Note that setting the appearance "DARK" explicitly will show the dark
481     * icon regardless of how this is set.
482     */
483    protected boolean litMode = false;
484
485    public void setLitMode(boolean mode) {
486        litMode = mode;
487    }
488
489    public boolean getLitMode() {
490        return litMode;
491    }
492
493    /**
494     * Change the SignalHead state when the icon is clicked. Note that this
495     * change may not be permanent if there is logic controlling the signal
496     * head.
497     */
498    @Override
499    public void doMouseClicked(JmriMouseEvent e) {
500        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
501            return;
502        }
503        performMouseClicked(e);
504    }
505
506    /**
507     * Handle mouse clicks when no modifier keys are pressed. Mouse clicks with
508     * modifier keys pressed can be processed by the containing component.
509     *
510     * @param e the mouse click event
511     */
512    public void performMouseClicked(JmriMouseEvent e) {
513        if (e.isMetaDown() || e.isAltDown()) {
514            return;
515        }
516        if (getSignalHead() == null) {
517            log.error("No turnout connection, can't process click");
518            return;
519        }
520        switch (clickMode) {
521            case 0:
522                switch (getSignalHead().getAppearance()) {
523                    case jmri.SignalHead.RED:
524                    case jmri.SignalHead.FLASHRED:
525                        getSignalHead().setAppearance(jmri.SignalHead.YELLOW);
526                        break;
527                    case jmri.SignalHead.YELLOW:
528                    case jmri.SignalHead.FLASHYELLOW:
529                        getSignalHead().setAppearance(jmri.SignalHead.GREEN);
530                        break;
531                    case jmri.SignalHead.GREEN:
532                    case jmri.SignalHead.FLASHGREEN:
533                    default:
534                        getSignalHead().setAppearance(jmri.SignalHead.RED);
535                        break;
536                }
537                return;
538            case 1:
539                getSignalHead().setLit(!getSignalHead().getLit());
540                return;
541            case 2:
542                getSignalHead().setHeld(!getSignalHead().getHeld());
543                return;
544            case 3:
545                SignalHead sh = getSignalHead();
546                int[] states = sh.getValidStates();
547                int state = sh.getAppearance();
548                for (int i = 0; i < states.length; i++) {
549                    if (state == states[i]) {
550                        i++;
551                        if (i >= states.length) {
552                            i = 0;
553                        }
554                        state = states[i];
555                        break;
556                    }
557                }
558                sh.setAppearance(state);
559                log.debug("Set state= {}", state);
560                return;
561            default:
562                log.error("Click in mode {}", clickMode);
563        }
564    }
565
566    //private static boolean warned = false;
567    @Override
568    public void dispose() {
569        if (getSignalHead() != null) {
570            getSignalHead().removePropertyChangeListener(this);
571        }
572        namedHead = null;
573        _iconMap = null;
574        super.dispose();
575    }
576
577    private final static Logger log = LoggerFactory.getLogger(SignalHeadIcon.class);
578
579}