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