001package jmri.jmrit.display;
002
003import java.awt.Color;
004import java.awt.Rectangle;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.Hashtable;
010import java.util.Map.Entry;
011
012import javax.annotation.Nonnull;
013import javax.swing.AbstractAction;
014import javax.swing.JCheckBoxMenuItem;
015import javax.swing.JComponent;
016import javax.swing.JMenu;
017import javax.swing.JMenuItem;
018import javax.swing.JPopupMenu;
019import javax.swing.Timer;
020
021import jmri.InstanceManager;
022import jmri.NamedBeanHandle;
023import jmri.Sensor;
024import jmri.NamedBean.DisplayOptions;
025import jmri.jmrit.catalog.NamedIcon;
026import jmri.jmrit.display.palette.TableItemPanel;
027import jmri.jmrit.picker.PickListModel;
028import jmri.util.swing.JmriColorChooser;
029import jmri.util.swing.JmriMouseEvent;
030
031/**
032 * An icon to display a status of a Sensor.
033 *
034 * @author Bob Jacobsen Copyright (C) 2001
035 * @author Pete Cressman Copyright (C) 2010, 2011
036 */
037public class SensorIcon extends PositionableIcon implements java.beans.PropertyChangeListener {
038
039    public static final int UNKOWN_FONT_COLOR = 0x03;
040    public static final int UNKOWN_BACKGROUND_COLOR = 0x04;
041    public static final int ACTIVE_FONT_COLOR = 0x05;
042    public static final int ACTIVE_BACKGROUND_COLOR = 0x06;
043    public static final int INACTIVE_FONT_COLOR = 0x07;
044    public static final int INACTIVE_BACKGROUND_COLOR = 0x08;
045    public static final int INCONSISTENT_FONT_COLOR = 0x0A;
046    public static final int INCONSISTENT_BACKGROUND_COLOR = 0x0B;
047
048    protected HashMap<String, Integer> _name2stateMap;       // name to state
049    protected HashMap<Integer, String> _state2nameMap;       // state to name
050
051    public SensorIcon(Editor editor) {
052        // super ctor call to make sure this is an icon label
053        this(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
054                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), editor);
055    }
056
057    public SensorIcon(NamedIcon s, Editor editor) {
058        // super ctor call to make sure this is an icon label
059        super(s, editor);
060        setOpaque(false);
061        _control = true;
062        setPopupUtility(new SensorPopupUtil(this, this));
063    }
064
065    public SensorIcon(String s, Editor editor) {
066        super(s, editor);
067        _control = true;
068        originalText = s;
069        setPopupUtility(new SensorPopupUtil(this, this));
070        displayState(sensorState());
071    }
072
073    @Override
074    public Positionable deepClone() {
075        SensorIcon pos = new SensorIcon(_editor);
076        return finishClone(pos);
077    }
078
079    protected Positionable finishClone(SensorIcon pos) {
080        pos.setSensor(getNamedSensor().getName());
081        pos.makeIconMap();
082        pos._iconMap = cloneMap(_iconMap, pos);
083        pos.setMomentary(getMomentary());
084        pos.originalText = originalText;
085        pos.setText(getText());
086        pos.setIcon(null);
087        pos._namedIcon = null;
088        pos.activeText = activeText;
089        pos.inactiveText = inactiveText;
090        pos.inconsistentText = inconsistentText;
091        pos.unknownText = unknownText;
092        pos.textColorInconsistent = textColorInconsistent;
093        pos.textColorUnknown = textColorUnknown;
094        pos.textColorInActive = textColorInActive;
095        pos.textColorActive = textColorActive;
096        pos.backgroundColorInActive = backgroundColorInActive;
097        pos.backgroundColorActive = backgroundColorActive;
098        pos.backgroundColorUnknown = backgroundColorUnknown;
099        pos.backgroundColorInconsistent = backgroundColorInconsistent;
100        return super.finishClone(pos);
101    }
102
103    // the associated Sensor object
104    private NamedBeanHandle<Sensor> namedSensor;
105
106    /**
107     * Attached a named sensor to this display item
108     *
109     * @param pName System/user name to lookup the sensor object
110     */
111    public void setSensor(String pName) {
112        if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) {
113            try {
114                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
115                setSensor(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
116            } catch (IllegalArgumentException ex) {
117                log.error("Sensor '{}' not available, icon won't see changes", pName);
118            }
119        } else {
120            log.error("No SensorManager for this protocol, icon won't see changes");
121        }
122    }
123
124    /**
125     * Attached a named sensor to this display item
126     *
127     * @param s the Sensor
128     */
129    public void setSensor(NamedBeanHandle<Sensor> s) {
130        if (namedSensor != null) {
131            getSensor().removePropertyChangeListener(this);
132        }
133
134        namedSensor = s;
135        if (namedSensor != null) {
136            if (_iconMap == null) {
137                makeIconMap();
138            }
139            getSensor().addPropertyChangeListener(this, s.getName(), "SensorIcon on Panel " + _editor.getName());
140            setName(namedSensor.getName());  // Swing name for e.g. tests
141        }
142        setAttributes();
143    }
144
145    private void setAttributes() {
146        if (isText()) {
147            if (namedSensor != null) {
148                if (getSensor().getUserName() != null) {
149                    String userName = getSensor().getUserName();
150                    if (activeText == null) {
151                        activeText = userName;
152                        //textColorActive=Color.red;
153                    }
154                    if (inactiveText == null) {
155                        inactiveText = userName;
156                        //textColorInActive=Color.yellow;
157                    }
158                    if (inconsistentText == null) {
159                        inconsistentText = userName;
160                        //textColorUnknown=Color.black;
161                    }
162                    if (unknownText == null) {
163                        unknownText = userName;
164                        //textColorInconsistent=Color.blue;
165                    }
166                }
167            }
168            if (activeText == null) {
169                activeText = "<" + Bundle.getMessage("SensorStateActive").toLowerCase() + ">"; // initiate state label string
170                // created from Bundle as lower case, enclosed in < >
171            }
172            if (inactiveText == null) {
173                inactiveText = "<" + Bundle.getMessage("SensorStateInactive").toLowerCase() + ">";
174            }
175            if (inconsistentText == null) {
176                inconsistentText = "<" + Bundle.getMessage("BeanStateInconsistent").toLowerCase() + ">";
177            }
178            if (unknownText == null) {
179                unknownText = "<" + Bundle.getMessage("BeanStateUnknown").toLowerCase() + ">";
180            }
181            if (textColorActive == null) {
182                textColorActive = Color.red;
183            }
184            if (textColorInActive == null) {
185                textColorInActive = Color.yellow;
186            }
187            if (textColorUnknown == null) {
188                textColorUnknown = Color.blue;
189            }
190            if (textColorInconsistent == null) {
191                textColorInconsistent = Color.black;
192            }
193        } else {
194            setOpaque(false);
195        }
196        displayState(sensorState());
197        if (log.isDebugEnabled()) { // avoid String building unless needed
198            log.debug("setSensor: namedSensor= {} isIcon= {}, isText= {}, activeText= {}",
199                    ((namedSensor == null) ? "null" : getNameString()), isIcon(), isText(), activeText);
200        }
201        repaint();
202    }
203
204    public Sensor getSensor() {
205        if (namedSensor == null) {
206            return null;
207        }
208        return namedSensor.getBean();
209    }
210
211    @Override
212    public jmri.NamedBean getNamedBean() {
213        return getSensor();
214    }
215
216    public NamedBeanHandle<Sensor> getNamedSensor() {
217        return namedSensor;
218    }
219
220    void makeIconMap() {
221        _iconMap = new HashMap<>();
222        _name2stateMap = new HashMap<>();
223        _name2stateMap.put("BeanStateUnknown", Sensor.UNKNOWN);
224        _name2stateMap.put("BeanStateInconsistent", Sensor.INCONSISTENT);
225        _name2stateMap.put("SensorStateActive", Sensor.ACTIVE);
226        _name2stateMap.put("SensorStateInactive", Sensor.INACTIVE);
227        _state2nameMap = new HashMap<>();
228        _state2nameMap.put(Sensor.UNKNOWN, "BeanStateUnknown");
229        _state2nameMap.put(Sensor.INCONSISTENT, "BeanStateInconsistent");
230        _state2nameMap.put(Sensor.ACTIVE, "SensorStateActive");
231        _state2nameMap.put(Sensor.INACTIVE, "SensorStateInactive");
232    }
233
234    @Override
235    public Collection<String> getStateNameCollection() {
236        return _state2nameMap.values();
237    }
238
239    /**
240     * Place icon by its bean state name key found in the properties file
241     * jmri.NamedBeanBundle.properties by its localized bean state name.
242     *
243     * @param name the icon state name
244     * @param icon the icon to place
245     */
246    public void setIcon(String name, NamedIcon icon) {
247        log.debug("setIcon for name \"{}\"", name);
248        if (_iconMap == null) {
249            makeIconMap();
250        }
251        _iconMap.put(name, icon);
252        displayState(sensorState());
253    }
254
255    /**
256     * Get icon by its localized bean state name.
257     *
258     * @param state the state to get the icon for
259     * @return the icon or null if state not found
260     */
261    @Override
262    public NamedIcon getIcon(String state) {
263        return _iconMap.get(state);
264    }
265
266    public NamedIcon getIcon(int state) {
267        return _iconMap.get(_state2nameMap.get(state));
268    }
269
270    @Override
271    public String getFamily() {
272        return _iconFamily;
273    }
274
275    @Override
276    public void setFamily(String family) {
277        _iconFamily = family;
278    }
279
280    /**
281     * Get current state of attached sensor
282     *
283     * @return A state variable from a Sensor, e.g. Sensor.ACTIVE
284     */
285    int sensorState() {
286        if (namedSensor != null) {
287            return getSensor().getKnownState();
288        } else {
289            return Sensor.UNKNOWN;
290        }
291    }
292
293    // update icon as state of turnout changes
294    @Override
295    public void propertyChange(java.beans.PropertyChangeEvent e) {
296        log.debug("property change: {}", e);
297        if (e.getPropertyName().equals("KnownState")) {
298            int now = ((Integer) e.getNewValue());
299            Rectangle dirty = getBounds();
300            displayState(now);
301            dirty.add(getBounds()); // union with the new bounds, in case displayState resized this icon
302            _editor.repaintTargetPanel(dirty);
303        }
304    }
305
306    @Override
307    @Nonnull
308    public String getTypeString() {
309        return Bundle.getMessage("PositionableType_SensorIcon");
310    }
311
312    @Override
313    @Nonnull
314    public String getNameString() {
315        String name;
316        if (namedSensor == null) {
317            name = Bundle.getMessage("NotConnected");
318        } else {
319            name = getSensor().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
320        }
321        return name;
322    }
323
324    JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary"));
325
326    /**
327     * Pop-up just displays the sensor name.
328     *
329     * @param popup the menu to display
330     * @return always true
331     */
332    @Override
333    public boolean showPopUp(JPopupMenu popup) {
334        if (isEditable()) {
335            if (isIcon()) {
336                popup.add(new AbstractAction(Bundle.getMessage("ChangeToText")) {
337                    @Override
338                    public void actionPerformed(ActionEvent e) {
339                        changeLayoutSensorType();
340                    }
341                });
342            } else {
343                popup.add(new AbstractAction(Bundle.getMessage("ChangeToIcon")) {
344                    @Override
345                    public void actionPerformed(ActionEvent e) {
346                        changeLayoutSensorType();
347                    }
348                });
349            }
350
351            popup.add(momentaryItem);
352            momentaryItem.setSelected(getMomentary());
353            momentaryItem.addActionListener((java.awt.event.ActionEvent e) -> setMomentary(momentaryItem.isSelected()));
354        } else if (getPopupUtility() != null) {
355            getPopupUtility().setAdditionalViewPopUpMenu(popup);
356        }
357        return true;
358    }
359
360    //////// popup AbstractAction.actionPerformed method overrides ////////
361
362    @Override
363    public boolean setTextEditMenu(JPopupMenu popup) {
364        log.debug("setTextEditMenu isIcon={}, isText={}", isIcon(), isText());
365        if (isIcon()) {
366            popup.add(CoordinateEdit.getTextEditAction(this, "OverlayText"));
367        } else {
368            popup.add(new AbstractAction(Bundle.getMessage("SetSensorText")) {
369                @Override
370                public void actionPerformed(ActionEvent e) {
371                    String name = getNameString();
372                    sensorTextEdit(name);
373                }
374            });
375            if (isText() && !isIcon()) {
376                JMenu stateColor = new JMenu(Bundle.getMessage("StateColors"));
377                stateColor.add(stateMenu(Bundle.getMessage("BeanStateUnknown"), UNKOWN_FONT_COLOR)); //Unknown
378                stateColor.add(stateMenu(Bundle.getMessage("SensorStateActive"), ACTIVE_FONT_COLOR)); //Active
379                stateColor.add(stateMenu(Bundle.getMessage("SensorStateInactive"), INACTIVE_FONT_COLOR)); //Inactive
380                stateColor.add(stateMenu(Bundle.getMessage("BeanStateInconsistent"), INCONSISTENT_FONT_COLOR)); //Inconsistent
381                popup.add(stateColor);
382            }
383        }
384        return true;
385    }
386
387    public void sensorTextEdit(String name) {
388        log.debug("make text edit menu");
389
390        SensorTextEdit f = new SensorTextEdit();
391        f.addHelpMenu("package.jmri.jmrit.display.SensorTextEdit", true);
392        try {
393            f.initComponents(this, name);
394        } catch (Exception ex) {
395            log.error("Exception: {}", ex.toString());
396        }
397        f.setVisible(true);
398    }
399
400    /**
401     * Drive the current state of the display from the state of the sensor.
402     *
403     * @param state the sensor state
404     */
405    @Override
406    public void displayState(int state) {
407        if (getNamedSensor() == null) {
408            log.debug("Display state {}, disconnected", state);
409        } else if (isIcon()) {
410            NamedIcon icon = getIcon(state);
411            if (icon != null) {
412                super.setIcon(icon);
413            }
414        } else if (isText()) {
415            switch (state) {
416                case Sensor.UNKNOWN:
417                    super.setText(unknownText);
418                    getPopupUtility().setBackgroundColor(backgroundColorUnknown);
419                    getPopupUtility().setForeground(textColorUnknown);
420                    break;
421                case Sensor.ACTIVE:
422                    super.setText(activeText);
423                    getPopupUtility().setBackgroundColor(backgroundColorActive);
424                    getPopupUtility().setForeground(textColorActive);
425                    break;
426                case Sensor.INACTIVE:
427                    super.setText(inactiveText);
428                    getPopupUtility().setBackgroundColor(backgroundColorInActive);
429                    getPopupUtility().setForeground(textColorInActive);
430                    break;
431                default:
432                    super.setText(inconsistentText);
433                    getPopupUtility().setBackgroundColor(backgroundColorInconsistent);
434                    getPopupUtility().setForeground(textColorInconsistent);
435                    break;
436            }
437        }
438        int deg = getDegrees();
439        rotate(deg);
440        if (deg == 0) {
441            setOpaque(getPopupUtility().hasBackground());
442        }
443
444        updateSize();
445    }
446
447    TableItemPanel<Sensor> _itemPanel;
448
449    @Override
450    public boolean setEditItemMenu(JPopupMenu popup) {
451        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSensor"));
452        popup.add(new AbstractAction(txt) {
453            @Override
454            public void actionPerformed(ActionEvent e) {
455                editItem();
456            }
457        });
458        return true;
459    }
460
461    protected void editItem() {
462        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSensor")));
463        _itemPanel = new TableItemPanel<>(_paletteFrame, "Sensor", _iconFamily,
464                PickListModel.sensorPickModelInstance()); // NOI18N
465        ActionListener updateAction = (ActionEvent a) -> updateItem();
466        // duplicate _iconMap map with unscaled and unrotated icons
467        HashMap<String, NamedIcon> map = new HashMap<>();
468        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
469            NamedIcon oldIcon = entry.getValue();
470            NamedIcon newIcon = cloneIcon(oldIcon, this);
471            newIcon.rotate(0, this);
472            newIcon.scale(1.0, this);
473            newIcon.setRotation(4, this);
474            map.put(entry.getKey(), newIcon);
475        }
476        _itemPanel.init(updateAction, map);
477        _itemPanel.setSelection(getSensor());
478        initPaletteFrame(_paletteFrame, _itemPanel);
479    }
480
481    void updateItem() {
482        HashMap<String, NamedIcon> oldMap = cloneMap(_iconMap, this);
483        setSensor(_itemPanel.getTableSelection().getSystemName());
484        _iconFamily = _itemPanel.getFamilyName();
485        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
486        if (iconMap != null) {
487            for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
488                if (log.isDebugEnabled()) {
489                    log.debug("key= {}", entry.getKey());
490                }
491                NamedIcon newIcon = entry.getValue();
492                NamedIcon oldIcon = oldMap.get(entry.getKey());
493                newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
494                newIcon.setRotation(oldIcon.getRotation(), this);
495                setIcon(entry.getKey(), newIcon);
496            }
497        }   // otherwise retain current map
498        finishItemUpdate(_paletteFrame, _itemPanel);
499    }
500
501    @Override
502    public boolean setEditIconMenu(JPopupMenu popup) {
503        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameSensor"));
504        popup.add(new AbstractAction(txt) {
505            @Override
506            public void actionPerformed(ActionEvent e) {
507                edit();
508            }
509        });
510        return true;
511    }
512
513    @Override
514    protected void edit() {
515        makeIconEditorFrame(this, "Sensor", true, null);
516        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.sensorPickModelInstance());
517        int i = 0;
518        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
519            _iconEditor.setIcon(i++, entry.getKey(), entry.getValue());
520        }
521        _iconEditor.makeIconPanel(false);
522
523        // set default icons, then override with this turnout's icons
524        ActionListener addIconAction = (ActionEvent a) -> updateSensor();
525        _iconEditor.complete(addIconAction, true, true, true);
526        _iconEditor.setSelection(getSensor());
527    }
528
529    void updateSensor() {
530        HashMap<String, NamedIcon> oldMap = cloneMap(_iconMap, this);
531        setSensor(_iconEditor.getTableSelection().getDisplayName());
532        Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap();
533
534        for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
535            log.debug("key= {}", entry.getKey());
536            NamedIcon newIcon = entry.getValue();
537            NamedIcon oldIcon = oldMap.get(entry.getKey());
538            newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
539            newIcon.setRotation(oldIcon.getRotation(), this);
540            setIcon(entry.getKey(), newIcon);
541        }
542        _iconEditorFrame.dispose();
543        _iconEditorFrame = null;
544        _iconEditor = null;
545        invalidate();
546    }
547
548    // Original text is used when changing between icon and text, this allows for a undo when reverting back.
549    String originalText;
550
551    public void setOriginalText(String s) {
552        originalText = s;
553        displayState(sensorState());
554    }
555
556    public String getOriginalText() {
557        return originalText;
558    }
559
560    @Override
561    public void setText(String s) {
562        setOpaque(false);
563        if (super._rotateText && !_icon) {
564            return;
565        }
566        _text = (s != null && s.length() > 0);
567        super.setText(s);
568        updateSize();
569    }
570
571    boolean momentary = false;
572
573    public boolean getMomentary() {
574        return momentary;
575    }
576
577    public void setMomentary(boolean m) {
578        momentary = m;
579    }
580
581    public boolean buttonLive() {
582        if (namedSensor == null) {  // no sensor connected for this protocol
583            log.error("No sensor connection, can't process click");
584            return false;
585        }
586        return _editor.getFlag(Editor.OPTION_CONTROLS, isControlling());
587    }
588
589    @Override
590    public void doMousePressed(JmriMouseEvent e) {
591        log.debug("doMousePressed {},{} clicks={}, buttonLive={}, getMomentary={}", 
592            e.getX(), e.getY(), e.getClickCount(),
593            buttonLive(), getMomentary());
594            
595        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
596            // this is a momentary button press
597            try {
598                getSensor().setKnownState(jmri.Sensor.ACTIVE);
599            } catch (jmri.JmriException reason) {
600                log.warn("Exception setting momentary sensor", reason);
601            }
602        }
603        super.doMousePressed(e);
604    }
605
606    @Override
607    public void doMouseReleased(JmriMouseEvent e) {
608        log.debug("doMouseReleased {},{} clicks={}, buttonLive={}, getMomentary={}", 
609            e.getX(), e.getY(), e.getClickCount(),
610            buttonLive(), getMomentary());
611        
612        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
613            // this is a momentary button release
614            try {
615                getSensor().setKnownState(jmri.Sensor.INACTIVE);
616            } catch (jmri.JmriException reason) {
617                log.warn("Exception setting momentary sensor", reason);
618            }
619        }
620        super.doMouseReleased(e);
621    }
622
623    @Override
624    public void doMouseClicked(JmriMouseEvent e) {
625        log.debug("doMouseClicked {},{} clicks={}, buttonLive={}, getMomentary={}", 
626            e.getX(), e.getY(), e.getClickCount(),
627            buttonLive(), getMomentary());
628
629        if (buttonLive() && !getMomentary()) {
630            // this button responds to clicks
631            if (!e.isMetaDown() && !e.isAltDown()) {
632                try {
633                    if (getSensor().getKnownState() == jmri.Sensor.INACTIVE) {
634                        getSensor().setKnownState(jmri.Sensor.ACTIVE);
635                    } else {
636                        getSensor().setKnownState(jmri.Sensor.INACTIVE);
637                    }
638                } catch (jmri.JmriException reason) {
639                    log.warn("Exception flipping sensor", reason);
640                }
641            }
642        }
643        super.doMouseClicked(e);
644    }
645
646    @Override
647    public void dispose() {
648        if (namedSensor != null) {
649            getSensor().removePropertyChangeListener(this);
650        }
651        namedSensor = null;
652        _iconMap = null;
653        _name2stateMap = null;
654        _state2nameMap = null;
655
656        super.dispose();
657    }
658
659    protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map,
660            SensorIcon pos) {
661        HashMap<Integer, NamedIcon> clone = new HashMap<>();
662        if (map != null) {
663            for (Entry<Integer, NamedIcon> entry : map.entrySet()) {
664                clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos));
665                if (pos != null) {
666                    pos.setIcon(pos._state2nameMap.get(entry.getKey()), _iconMap.get(entry.getKey().toString()));
667                }
668            }
669        }
670        return clone;
671    }
672    // The code below here is from the layoutsensoricon.
673
674    Color textColorActive = Color.red;
675
676    public void setTextActive(Color color) {
677        textColorActive = color;
678        displayState(sensorState());
679        JmriColorChooser.addRecentColor(color);
680    }
681
682    public Color getTextActive() {
683        return textColorActive;
684    }
685
686    Color textColorInActive = Color.yellow;
687
688    public void setTextInActive(Color color) {
689        textColorInActive = color;
690        displayState(sensorState());
691        JmriColorChooser.addRecentColor(color);
692    }
693
694    public Color getTextInActive() {
695        return textColorInActive;
696    }
697
698    Color textColorUnknown = Color.blue;
699
700    public void setTextUnknown(Color color) {
701        textColorUnknown = color;
702        displayState(sensorState());
703        JmriColorChooser.addRecentColor(color);
704    }
705
706    public Color getTextUnknown() {
707        return textColorUnknown;
708    }
709
710    Color textColorInconsistent = Color.black;
711
712    public void setTextInconsistent(Color color) {
713        textColorInconsistent = color;
714        displayState(sensorState());
715        JmriColorChooser.addRecentColor(color);
716    }
717
718    public Color getTextInconsistent() {
719        return textColorInconsistent;
720    }
721
722    Color backgroundColorActive = null;
723
724    public void setBackgroundActive(Color color) {
725        backgroundColorActive = color;
726        displayState(sensorState());
727        JmriColorChooser.addRecentColor(color);
728    }
729
730    public Color getBackgroundActive() {
731        return backgroundColorActive;
732    }
733
734    Color backgroundColorInActive = null;
735
736    public void setBackgroundInActive(Color color) {
737        backgroundColorInActive = color;
738        displayState(sensorState());
739        JmriColorChooser.addRecentColor(color);
740    }
741
742    public Color getBackgroundInActive() {
743        return backgroundColorInActive;
744    }
745
746    Color backgroundColorUnknown = null;
747
748    public void setBackgroundUnknown(Color color) {
749        backgroundColorUnknown = color;
750        displayState(sensorState());
751        JmriColorChooser.addRecentColor(color);
752    }
753
754    public Color getBackgroundUnknown() {
755        return backgroundColorUnknown;
756    }
757
758    Color backgroundColorInconsistent = null;
759
760    public void setBackgroundInconsistent(Color color) {
761        backgroundColorInconsistent = color;
762        displayState(sensorState());
763        JmriColorChooser.addRecentColor(color);
764    }
765
766    public Color getBackgroundInconsistent() {
767        return backgroundColorInconsistent;
768    }
769
770    private String activeText;
771    private String inactiveText;
772    private String inconsistentText;
773    private String unknownText;
774
775    public String getActiveText() {
776        return activeText;
777    }
778
779    public void setActiveText(String i) {
780        activeText = i;
781        displayState(sensorState());
782    }
783
784    public String getInactiveText() {
785        return inactiveText;
786    }
787
788    public void setInactiveText(String i) {
789        inactiveText = i;
790        displayState(sensorState());
791    }
792
793    public String getInconsistentText() {
794        return inconsistentText;
795    }
796
797    public void setInconsistentText(String i) {
798        inconsistentText = i;
799        displayState(sensorState());
800    }
801
802    public String getUnknownText() {
803        return unknownText;
804    }
805
806    public void setUnknownText(String i) {
807        unknownText = i;
808        displayState(sensorState());
809    }
810
811    JMenu stateMenu(final String name, int state) {
812        JMenu menu = new JMenu(name);
813        JMenuItem colorMenu = new JMenuItem(Bundle.getMessage("FontColor"));
814        colorMenu.addActionListener((ActionEvent event) -> {
815            Color desiredColor = JmriColorChooser.showDialog(this,
816                                 Bundle.getMessage("FontColor"),
817                                 getColor(state));
818            if (desiredColor!=null ) {
819                 setColor(desiredColor,state);
820            }
821        });
822        menu.add(colorMenu);
823        colorMenu = new JMenuItem(Bundle.getMessage("FontBackgroundColor"));
824        colorMenu.addActionListener((ActionEvent event) -> {
825            Color desiredColor = JmriColorChooser.showDialog(this,
826                                 Bundle.getMessage("FontBackgroundColor"),
827                                 getColor(state+1));
828            if (desiredColor!=null ) {
829                 setColor(desiredColor,state+1);
830            }
831        });
832        menu.add(colorMenu);
833        return menu;
834    }
835
836    private void setColor(Color desiredColor, int state) {
837        PositionablePopupUtil pop = getPopupUtility();
838        if (pop instanceof SensorPopupUtil) {
839            SensorPopupUtil util = (SensorPopupUtil) pop;
840            switch (state) {
841                case PositionablePopupUtil.FONT_COLOR:
842                    util.setForeground(desiredColor);
843                    break;
844                case PositionablePopupUtil.BACKGROUND_COLOR:
845                    util.setBackgroundColor(desiredColor);
846                    break;
847                case PositionablePopupUtil.BORDER_COLOR:
848                    util.setBorderColor(desiredColor);
849                    break;
850                case UNKOWN_FONT_COLOR:
851                    setTextUnknown(desiredColor);
852                    break;
853                case UNKOWN_BACKGROUND_COLOR:
854                    util.setHasBackground(desiredColor != null);
855                    setBackgroundUnknown(desiredColor);
856                    break;
857                case ACTIVE_FONT_COLOR:
858                    setTextActive(desiredColor);
859                    break;
860                case ACTIVE_BACKGROUND_COLOR:
861                    util.setHasBackground(desiredColor != null);
862                    setBackgroundActive(desiredColor);
863                    break;
864                case INACTIVE_FONT_COLOR:
865                    setTextInActive(desiredColor);
866                    break;
867                case INACTIVE_BACKGROUND_COLOR:
868                    util.setHasBackground(desiredColor != null);
869                    setBackgroundInActive(desiredColor);
870                    break;
871                case INCONSISTENT_FONT_COLOR:
872                    setTextInconsistent(desiredColor);
873                    break;
874                case INCONSISTENT_BACKGROUND_COLOR:
875                    util.setHasBackground(desiredColor != null);
876                    setBackgroundInconsistent(desiredColor);
877                    break;
878                default:
879                    break;
880            }
881        }
882    }
883
884    private Color getColor(int state){
885        PositionablePopupUtil pop = getPopupUtility();
886        if (pop instanceof SensorPopupUtil) {
887            SensorPopupUtil util = (SensorPopupUtil) pop;
888            switch (state) {
889                case PositionablePopupUtil.FONT_COLOR:
890                    return util.getForeground();
891                case PositionablePopupUtil.BACKGROUND_COLOR:
892                    return util.getBackground();
893                case PositionablePopupUtil.BORDER_COLOR:
894                    return util.getBorderColor();
895                case UNKOWN_FONT_COLOR:
896                    return getTextUnknown();
897                case UNKOWN_BACKGROUND_COLOR:
898                    return getBackgroundUnknown();
899                case ACTIVE_FONT_COLOR:
900                    return getTextActive();
901                case ACTIVE_BACKGROUND_COLOR:
902                    return getBackgroundActive();
903                case INACTIVE_FONT_COLOR:
904                    return getTextInActive();
905                case INACTIVE_BACKGROUND_COLOR:
906                    return getBackgroundInActive();
907                case INCONSISTENT_FONT_COLOR:
908                    return getTextInconsistent();
909                case INCONSISTENT_BACKGROUND_COLOR:
910                    return getBackgroundInconsistent();
911                default:
912                    return null;
913            }
914        }
915        return null;
916    }
917
918    void changeLayoutSensorType() {
919//        NamedBeanHandle <Sensor> handle = getNamedSensor();
920        if (isIcon()) {
921            _icon = false;
922            _text = true;
923            setIcon(null);
924//            setOriginalText(getUnRotatedText());
925            setSuperText(null);
926            setOpaque(true);
927        } else if (isText()) {
928            _icon = true;
929            _text = (originalText != null && originalText.length() > 0);
930            setUnRotatedText(getOriginalText());
931            setOpaque(false);
932        }
933        _namedIcon = null;
934        setAttributes();
935        displayState(sensorState());
936//        setSensor(handle);
937        int deg = getDegrees();
938        rotate(deg);
939        if (deg != 0 && _text && !_icon) {
940            setSuperText(null);
941        }
942    }
943
944    private int flashStateOn = -1;
945    private int flashStateOff = -1;
946    private boolean flashon = false;
947    private ActionListener taskPerformer;
948    private Timer flashTimer;
949
950    synchronized public void flashSensor(int tps, int state1, int state2) {
951        if ((flashTimer != null) && flashTimer.isRunning()) {
952            return;
953        }
954        //Set the maximum number of state changes to 10 per second
955        if (tps > 10) {
956            tps = 10;
957        } else if (tps <= 0) {
958            return;
959        }
960        if ((_state2nameMap.get(state1) == null) || _state2nameMap.get(state2) == null) {
961            log.error("one or other of the states passed for flash is null");
962            return;
963        } else if (state1 == state2) {
964            log.debug("Both states to flash between are the same, therefore no flashing will occur");
965            return;
966        }
967        int interval = (1000 / tps) / 2;
968        flashStateOn = state1;
969        flashStateOff = state2;
970        if (taskPerformer == null) {
971            taskPerformer = (ActionEvent evt) -> {
972                if (flashon) {
973                    flashon = false;
974                    displayState(flashStateOn);
975                } else {
976                    flashon = true;
977                    displayState(flashStateOff);
978                }
979            };
980        }
981        flashTimer = new Timer(interval, taskPerformer);
982        flashTimer.start();
983    }
984
985    synchronized public void stopFlash() {
986        if (flashTimer != null) {
987            flashTimer.stop();
988        }
989        displayState(sensorState());
990    }
991
992    class SensorPopupUtil extends PositionablePopupUtil {
993
994        SensorPopupUtil(Positionable parent, javax.swing.JComponent textComp) {
995            super(parent, textComp);
996        }
997
998        @Override
999        public SensorPopupUtil clone(Positionable parent, JComponent textComp) {
1000            SensorPopupUtil util = new SensorPopupUtil(parent, textComp);
1001            util.setJustification(getJustification());
1002            util.setHorizontalAlignment(getJustification());
1003            util.setFixedWidth(getFixedWidth());
1004            util.setFixedHeight(getFixedHeight());
1005            util.setMargin(getMargin());
1006            util.setBorderSize(getBorderSize());
1007            util.setBorderColor(getBorderColor());
1008            util.setFont(util.getFont().deriveFont(getFontStyle()));
1009            util.setFontSize(getFontSize());
1010            util.setOrientation(getOrientation());
1011            util.setBackgroundColor(getBackground());
1012            util.setForeground(getForeground());
1013            util.setHasBackground(hasBackground());     // must do this AFTER setBackgroundColor
1014            return util;
1015        }
1016
1017        @Override
1018        public void setTextJustificationMenu(JPopupMenu popup) {
1019            if (isText()) {
1020                super.setTextJustificationMenu(popup);
1021            }
1022        }
1023
1024        @Override
1025        public void setTextOrientationMenu(JPopupMenu popup) {
1026            if (isText()) {
1027                super.setTextOrientationMenu(popup);
1028            }
1029        }
1030
1031        @Override
1032        public void setFixedTextMenu(JPopupMenu popup) {
1033            if (isText()) {
1034                super.setFixedTextMenu(popup);
1035            }
1036        }
1037
1038        @Override
1039        public void setTextMarginMenu(JPopupMenu popup) {
1040            if (isText()) {
1041                super.setTextMarginMenu(popup);
1042            }
1043        }
1044
1045        @Override
1046        public void setTextBorderMenu(JPopupMenu popup) {
1047            if (isText()) {
1048                super.setTextBorderMenu(popup);
1049            }
1050        }
1051
1052        @Override
1053        public void setTextFontMenu(JPopupMenu popup) {
1054            if (isText()) {
1055                super.setTextFontMenu(popup);
1056            }
1057        }
1058
1059        @Override
1060        public void setBackgroundMenu(JPopupMenu popup) {
1061            if (isIcon()) {
1062                super.setBackgroundMenu(popup);
1063            }
1064        }
1065    }
1066
1067    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SensorIcon.class);
1068
1069}