001package jmri.jmrit.display.switchboardEditor;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.awt.image.BufferedImage;
006import java.awt.image.RescaleOp;
007import java.io.File;
008import java.io.IOException;
009import javax.annotation.Nonnull;
010import javax.imageio.ImageIO;
011import javax.swing.*;
012
013import jmri.InstanceManager;
014import jmri.JmriException;
015import jmri.Light;
016import jmri.NamedBean;
017import jmri.NamedBeanHandle;
018import jmri.Sensor;
019import jmri.Turnout;
020import jmri.jmrit.beantable.AddNewDevicePanel;
021import jmri.jmrit.display.Positionable;
022import jmri.util.JmriJFrame;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026/**
027 * Class for a switchboard interface object.
028 * <p>
029 * Contains a JButton or JPanel to control existing turnouts, sensors and
030 * lights.
031 * Separated from SwitchboardEditor.java in 4.12.3
032 *
033 * @author Egbert Broerse Copyright (c) 2017, 2018, 2020
034 */
035public class BeanSwitch extends JPanel implements java.beans.PropertyChangeListener, ActionListener {
036
037    private final JButton beanButton;
038    private final int _shape;
039    private final String _switchSysName;
040    private String _uName = "unconnected";
041    private String _uLabel = ""; // for display, empty if userName == null
042    private final Boolean showUserName;
043    protected String switchLabel;
044    protected String switchButtonLabel;
045    protected String switchTooltip;
046    protected boolean _text;
047    protected boolean _icon = false;
048    protected boolean _control = false;
049    protected String _state;
050    protected String _color;
051    protected String stateClosed = Bundle.getMessage("StateClosedShort");
052    protected String stateThrown = Bundle.getMessage("StateThrownShort");
053
054    // the associated Bean object
055    private final NamedBean _bname;
056    private NamedBeanHandle<?> namedBean = null; // could be Turnout, Sensor or Light
057    protected jmri.NamedBeanHandleManager nbhm = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class);
058    private IconSwitch beanIcon;
059    private IconSwitch beanKey;
060    private IconSwitch beanSymbol;
061    private final char beanTypeChar;
062    private final Color defaultActiveColor = Color.RED;
063    private final Color defaultInactiveColor = Color.GREEN;
064    //private final Color defaultUnknownColor = Color.WHITE; // often hard to see
065    private final SwitchboardEditor _editor;
066
067    /**
068     * Ctor.
069     *
070     * @param index       DCC address.
071     * @param bean        layout object to connect to.
072     * @param switchName  descriptive name corresponding with system name to
073     *                    display in switch tooltip, i.e. LT1.
074     * @param shapeChoice Button, Icon (static) or Drawing (vector graphics).
075     * @param editor      main switchboard editor.
076     */
077    public BeanSwitch(int index, NamedBean bean, String switchName, int shapeChoice, SwitchboardEditor editor) {
078        log.debug("Name = [{}]", switchName);
079        _switchSysName = switchName;
080        sysNameTextBox.setText(switchName); // setting name here allows test of AddNew()
081        _editor = editor;
082        _bname = bean;
083        showUserName = (_editor.showUserName().equals("yes"));
084        if (bean != null) {
085            _uName = bean.getUserName();
086            log.debug("Switch userName from bean: {}", _uName);
087            if (_uName == null) {
088                _uName = Bundle.getMessage("NoUserName");
089            } else if (showUserName) {
090                _uLabel = _uName;
091            }
092        }
093
094        beanButton = new JButton();
095        beanButton.setText(getSwitchButtonLabel(_switchSysName + ": ?")); // initial text to display
096
097        switchTooltip = switchName + " (" + _uName + ")";
098        this.setLayout(new BorderLayout()); // makes JButtons expand to the whole grid cell
099        _shape = shapeChoice;
100        String beanManuPrefix = _editor.getSwitchManu(); // connection/manufacturer prefix i.e. M for MERG
101        beanTypeChar = _switchSysName.charAt(beanManuPrefix.length()); // bean type, i.e. L, usually at char(1)
102        // check for space char which might be caused by connection name > 2 chars and/or space in name
103        if (beanTypeChar != 'T' && beanTypeChar != 'S' && beanTypeChar != 'L') { // add if more bean types are supported
104            log.error("invalid char in Switchboard Button \"{}\". Check connection name.", _switchSysName);
105            JOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorSwitchAddFailed"),
106                    Bundle.getMessage("WarningTitle"),
107                    JOptionPane.ERROR_MESSAGE);
108            return;
109        }
110
111        int r = _editor.getTileSize()/2; // max WxH of canvas inside cell, used as relative unit to draw
112        log.debug("BeanSwitch graphic tilesize/2  r = {}", r);
113        beanIcon = new IconSwitch(iconOnPath, iconOffPath, r);
114        beanKey = new IconSwitch(keyOnPath, keyOffPath, r);
115        beanSymbol = new IconSwitch(rootPath + beanTypeChar + "-on-s.png", rootPath + beanTypeChar + "-off-s.png", r);
116
117        // look for bean to connect to by name
118        log.debug("beanconnect = {}, beantype = {}", beanManuPrefix, beanTypeChar);
119        try {
120            if (bean != null) {
121                namedBean = nbhm.getNamedBeanHandle(switchName, bean);
122            }
123        } catch (IllegalArgumentException e) {
124            log.error("invalid bean name= \"{}\" in Switchboard Button", switchName);
125        }
126        // attach shape specific code to this beanSwitch
127        switch (_shape) {
128            case 1: // slider shape
129                beanIcon.addMouseListener(new MouseAdapter() { // handled by JPanel
130                    @Override
131                    public void mouseClicked(MouseEvent me) {
132                        operate(me, switchName);
133                    }
134
135                    @Override
136                    public void mouseReleased(MouseEvent me) { // for Windows
137                        if (me.isPopupTrigger()) {
138                            showPopUp(me); // display the popup
139                        }
140                    }
141
142                    @Override
143                    public void mousePressed(MouseEvent me) { // for macOS, Linux
144                        if (me.isPopupTrigger()) {
145                            showPopUp(me); // display the popup
146                        }
147                    }
148                });
149                _text = true; // not actually used
150                _icon = true;
151                beanIcon.setPreferredSize(new Dimension(2*r, 2*r));
152                beanIcon.setLabels(switchLabel, _uLabel);
153                beanIcon.positionLabel(0, 5*r/-8, Component.CENTER_ALIGNMENT, Math.max(12,r/4));
154                beanIcon.positionSubLabel(0, r/3, Component.CENTER_ALIGNMENT, Math.max(9,r/5)); // smaller (system name)
155                if (_editor.showToolTip()) {
156                    beanIcon.setToolTipText(switchTooltip);
157                }
158                beanIcon.setBackground(_editor.getDefaultBackgroundColor());
159                //remove the line around icon switches?
160                this.add(beanIcon);
161                break;
162            case 2: // Maerklin style keyboard
163                beanKey.addMouseListener(new MouseAdapter() { // handled by JPanel
164                    @Override
165                    public void mouseClicked(MouseEvent me) {
166                        operate(me, switchName);
167                    }
168
169                    @Override
170                    public void mouseReleased(MouseEvent me) { // for Windows
171                        if (me.isPopupTrigger()) {
172                            showPopUp(me); // display the popup
173                        }
174                    }
175
176                    @Override
177                    public void mousePressed(MouseEvent me) { // for macOS, Linux
178                        if (me.isPopupTrigger()) {
179                            showPopUp(me); // display the popup
180                        }
181                    }
182                });
183                _text = true; // not actually used for Switchboards
184                _icon = true;
185                beanKey.setPreferredSize(new Dimension(new Dimension(2*r, 2*r)));
186                beanKey.setLabels(switchLabel, _uLabel);
187                beanKey.positionLabel(0, r/16, Component.CENTER_ALIGNMENT, Math.max(12,r/4));
188                beanKey.positionSubLabel(0, r/4, Component.CENTER_ALIGNMENT, Math.max(9,r/5)); // smaller (system name)
189                // provide x, y offset, depending on image size and free space
190                if (_editor.showToolTip()) {
191                    beanKey.setToolTipText(switchTooltip);
192                }
193                beanKey.setBackground(_editor.getDefaultBackgroundColor());
194                //remove the line around icon switches?
195                this.add(beanKey);
196                break;
197            case 3: // turnout/sensor/light Icon (selecting image by letter in switch name/label)
198                beanSymbol.addMouseListener(new MouseAdapter() { // handled by JPanel
199                    @Override
200                    public void mouseClicked(MouseEvent me) {
201                        operate(me, switchName);
202                    }
203
204                    @Override
205                    public void mouseReleased(MouseEvent me) { // for Windows
206                        if (me.isPopupTrigger()) {
207                            showPopUp(me); // display the popup
208                        }
209                    }
210
211                    @Override
212                    public void mousePressed(MouseEvent me) { // for macOS, Linux
213                        if (me.isPopupTrigger()) {
214                            showPopUp(me); // display the popup
215                        }
216                    }
217                });
218                _text = true; // web server supports in-browser drawn switches
219                _icon = true; // panel.js assigns only text OR icon for a single class such as BeanSwitch
220                beanSymbol.setPreferredSize(new Dimension(2*r, 2*r));
221                beanSymbol.setLabels(switchLabel, _uLabel);
222                switch (beanTypeChar) {
223                    case 'T' :
224                        beanSymbol.positionLabel(0, -3*r/5, Component.CENTER_ALIGNMENT, Math.max(12,r/4));
225                        beanSymbol.positionSubLabel(0, r/-5, Component.CENTER_ALIGNMENT, Math.max(9,r/5));
226                        break;
227                    case 'S' :
228                    case 'L' :
229                    default :
230                        beanSymbol.positionLabel(0, r/-3, Component.CENTER_ALIGNMENT, Math.max(12,r/4));
231                        beanSymbol.positionSubLabel(0, 7*r/8, Component.CENTER_ALIGNMENT, Math.max(9,r/5));
232                }
233                if (_editor.showToolTip()) {
234                    beanSymbol.setToolTipText(switchTooltip);
235                }
236                beanSymbol.setBackground(_editor.getDefaultBackgroundColor());
237                // common (in)activecolor etc defined in SwitchboardEditor, retrieved by Servlet
238                // remove the line around icon switches?
239                this.setBorder(BorderFactory.createLineBorder(_editor.getDefaultBackgroundColor(), 3));
240                this.add(beanSymbol);
241                break;
242            case 0: // 0 = "Button" shape
243            default:
244                beanButton.addMouseListener(new MouseAdapter() { // handled by JPanel
245                    @Override
246                    public void mouseClicked(MouseEvent me) {
247                        operate(me, switchName);
248                    }
249
250                    @Override
251                    public void mouseReleased(MouseEvent me) { // for Windows
252                        if (me.isPopupTrigger()) {
253                            showPopUp(me); // display the popup
254                        }
255                    }
256
257                    @Override
258                    public void mousePressed(MouseEvent me) { // for macOS, Linux
259                        if (me.isPopupTrigger()) {
260                            showPopUp(me); // display the popup
261                        }
262                    }
263                });
264                _text = true;
265                _icon = false;
266                beanButton.setForeground(_editor.getDefaultTextColorAsColor());
267                beanButton.setOpaque(true); // to show color from the start
268                this.setBorder(BorderFactory.createLineBorder(_editor.getDefaultBackgroundColor(), 2));
269                if (_editor.showToolTip()) {
270                    beanButton.setToolTipText(switchTooltip);
271                }
272                beanButton.addComponentListener(new ComponentAdapter() {
273                    @Override
274                    public void componentResized(ComponentEvent e) {
275                        if (showUserName && beanButton.getHeight() < 50) {
276                            beanButton.setVerticalAlignment(JLabel.TOP);
277                        } else {
278                            beanButton.setVerticalAlignment(JLabel.CENTER); //default
279                        }
280                    }
281                });
282
283
284                beanButton.setMargin(new Insets(4, 1, 2, 1));
285                this.add(beanButton);
286                break;
287        }
288
289        // connect to object or dim switch
290        if (bean == null) {
291            if (!_editor.hideUnconnected()) {
292                // to dim unconnected symbols TODO make graphics see through, now icons just become bleak
293                //float dim = 100f;
294                switch (_shape) {
295                    case 0:
296                        beanButton.setEnabled(false);
297                        break;
298                    case 1:
299//                        beanIcon.setOpacity(dim);
300//                        break;
301                    case 2:
302//                        beanKey.setOpacity(dim);
303//                        break;
304                    case 3:
305                    default:
306//                        beanSymbol.setOpacity(dim);
307                }
308                displayState(0); // show unconnected as unknown/greyed
309            }
310        } else {
311            _control = true;
312            switch (beanTypeChar) {
313                case 'T':
314                    getTurnout().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Turnout Switch");
315                    if (getTurnout().canInvert()) {
316                        this.setInverted(getTurnout().getInverted()); // only add and set when supported by object/connection
317                    }
318                    break;
319                case 'S':
320                    getSensor().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Sensor Switch");
321                    if (getSensor().canInvert()) {
322                        this.setInverted(getSensor().getInverted()); // only add and set when supported by object/connection
323                    }
324                    break;
325                default: // light
326                    getLight().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Light Switch");
327                // Lights do not support Invert
328            }
329        }
330        // from finishClone
331        setTristate(getTristate());
332        setMomentary(getMomentary());
333        log.debug("Created switch {}", index);
334    }
335
336    public NamedBean getNamedBean() {
337        return _bname;
338    }
339
340    /**
341     * Store an object as NamedBeanHandle, using _label as the display
342     * name.
343     *
344     * @param bean the object (either a Turnout, Sensor or Light) to attach
345     *             to this switch
346     */
347    public void setNamedBean(@Nonnull NamedBean bean) {
348        try {
349            namedBean = nbhm.getNamedBeanHandle(_switchSysName, bean);
350        } catch (IllegalArgumentException e) {
351            log.error("invalid bean name= \"{}\" in Switchboard Button", _switchSysName);
352        }
353        _uName = bean.getUserName();
354        if (_uName == null) {
355            _uName = Bundle.getMessage("NoUserName");
356        } else {
357            if (showUserName) _uLabel = _uName;
358        }
359        _control = true;
360    }
361
362    public Turnout getTurnout() {
363        if (namedBean == null) {
364            return null;
365        }
366        return (Turnout) namedBean.getBean();
367    }
368
369    public Sensor getSensor() {
370        if (namedBean == null) {
371            return null;
372        }
373        return (Sensor) namedBean.getBean();
374    }
375
376    public Light getLight() {
377        if (namedBean == null) {
378            return null;
379        }
380        return (Light) namedBean.getBean();
381    }
382
383    /**
384     * Get the user selected switch shape (e.g. 3 for Slider)
385     *
386     * @return the index of the selected item in Shape comboBox
387     */
388    public int getShape() {
389        return _shape;
390    }
391
392    /**
393     * Get text to display on this switch on Switchboard and in Web Server panel when attached
394     * object is Active.
395     *
396     * @return text to show on active state (differs per type of object)
397     */
398    public String getActiveText() {
399        // fetch bean specific abbreviation
400        switch (beanTypeChar) {
401            case 'T':
402                _state = stateClosed; // +
403                break;
404            default: // Light, Sensor
405                _state = "+";         // 1 char abbreviation for StateOff not clear
406        }
407        return _switchSysName + ": " + _state;
408    }
409
410    /**
411     * Get text to display on this switch on Switchboard and in Web Server panel when attached
412     * object is Inactive.
413     *
414     * @return text to show on inactive state (differs per type of objects)
415     */
416    public String getInactiveText() {
417        // fetch bean specific abbreviation
418        switch (beanTypeChar) {
419            case 'T':
420                _state = stateThrown; // +
421                break;
422            default: // Light, Sensor
423                _state = "-";         // 1 char abbreviation for StateOff not clear
424        }
425        return _switchSysName + ": " + _state;
426    }
427
428    /**
429     * Get text to display on this switch in Web Server panel when attached
430     * object is Unknown (initial state displayed).
431     *
432     * @return text to show on unknown state (used on all types of objects)
433     */
434    public String getUnknownText() {
435        return _switchSysName + ": ?";
436    }
437
438    public String getInconsistentText() {
439        return _switchSysName + ": X";
440    }
441
442    /**
443     * Get text to display as switch tooltip in Web Server panel.
444     * Used in jmri.jmrit.display.switchboardEditor.configureXml.BeanSwitchXml#store(Object)
445     *
446     * @return switch tooltip text
447     */
448    public String getToolTip() {
449        return switchTooltip;
450    }
451
452    // ******************* Display ***************************
453
454    @Override
455    public void actionPerformed(ActionEvent e) {
456        //updateBean();
457    }
458
459    /**
460     * Get the label of this switch.
461     *
462     * @return display name including current state
463     */
464    public String getNameString() {
465        return _switchSysName;
466    }
467
468    public String getUserNameString() {
469        return _uLabel;
470    }
471
472    private String getSwitchButtonLabel(String label) {
473        if (!showUserName || _uLabel.equals("")) {
474            return label;
475        } else {
476            String subLabel = _uLabel.substring(0, (Math.min(_uLabel.length(), 35))); // reasonable max. to display 2 lines on tile
477            return "<html><center>" + label + "</center><br><center><i>" + subLabel + "</i></center></html>"; // 2 lines of text
478        }
479    }
480
481    /**
482     * Drive the current state of the display from the state of the
483     * connected bean.
484     *
485     * @param state integer representing the new state e.g. Turnout.CLOSED
486     */
487    public void displayState(int state) {
488        String switchLabel;
489        Color switchColor;
490        log.debug("Change heard. STATE={}", state);
491        // display abbreviated name of state instead of state index, fine for unconnected switches too
492        switch (state) {
493            case 1:
494                switchLabel = getUnknownText();
495                switchColor = Color.GRAY;
496                break;
497            case 2:
498                switchLabel = getActiveText();
499                switchColor = defaultActiveColor;
500                break;
501            case 4:
502                switchLabel = getInactiveText();
503                switchColor = defaultInactiveColor;
504                break;
505            default:
506                switchLabel = getInconsistentText();
507                switchColor = Color.WHITE;
508                //log.warn("SwitchState INCONSISTENT"); // normal for unconnected switchboard
509        }
510        if (getNamedBean() == null) {
511            switchLabel = _switchSysName; // unconnected, doesn't show state using : and ?
512            log.debug("Switch label {} state {}, disconnected", switchLabel, state);
513        } else {
514            log.debug("Switch label {} state: {}, connected", switchLabel, state);
515        }
516        if (isText() && !isIcon()) { // to allow text buttons on web switchboard. always add graphic switches on web
517            log.debug("Label = {}", getSwitchButtonLabel(switchLabel));
518            beanButton.setText(getSwitchButtonLabel(switchLabel));
519            beanButton.setBackground(switchColor); // only the color is visible TODO get access to bg color of JButton?
520            beanButton.setOpaque(true);
521        }
522        if (isIcon() && beanIcon != null && beanKey != null && beanSymbol != null) {
523            beanIcon.showSwitchIcon(state);
524            beanIcon.setLabels(switchLabel, _uLabel);
525            beanKey.showSwitchIcon(state);
526            beanKey.setLabels(switchLabel, _uLabel);
527            beanSymbol.showSwitchIcon(state);
528            beanSymbol.setLabels(switchLabel, _uLabel);
529        }
530    }
531
532    /**
533     * Switch presentation is graphic image based.
534     *
535     * @see #displayState(int)
536     * @return true when switch shape other than 'Button' is selected
537     */
538    public final boolean isIcon() {
539        return _icon;
540    }
541
542    /**
543     * Switch presentation is text based.
544     *
545     * @see #displayState(int)
546     * @return true when switch shape 'Button' is selected (and also for the
547     *         other, graphic switch types until SwitchboardServlet directly
548     *         supports their graphic icons)
549     */
550    public final boolean isText() {
551        return _text;
552    }
553
554    /**
555     * Get current state of attached turnout.
556     *
557     * @return A state variable from a Turnout, e.g. Turnout.CLOSED
558     */
559    int turnoutState() {
560        if (namedBean != null) {
561            return getTurnout().getKnownState();
562        } else {
563            return Turnout.UNKNOWN;
564        }
565    }
566
567    /**
568     * Update switch as state of turnout changes.
569     *
570     * @param e the PropertyChangeEvent heard
571     */
572    @Override
573    public void propertyChange(java.beans.PropertyChangeEvent e) {
574        if (log.isDebugEnabled()) {
575            log.debug("property change: {} {} is now: {}", _switchSysName, e.getPropertyName(), e.getNewValue());
576        }
577        // when there's feedback, transition through inconsistent icon for better animation
578        if (getTristate()
579                && (getTurnout().getFeedbackMode() != Turnout.DIRECT)
580                && (e.getPropertyName().equals("CommandedState"))) {
581            if (getTurnout().getCommandedState() != getTurnout().getKnownState()) {
582                int now = Turnout.INCONSISTENT;
583                displayState(now);
584            }
585            // this takes care of the quick double click
586            if (getTurnout().getCommandedState() == getTurnout().getKnownState()) {
587                int now = ((Integer) e.getNewValue());
588                displayState(now);
589            }
590        }
591        if (e.getPropertyName().equals("KnownState")) {
592            int now = ((Integer) e.getNewValue());
593            displayState(now);
594            log.debug("Item state changed");
595        }
596        if (e.getPropertyName().equals("UserName")) {
597            // update tooltip
598            String newUserName;
599            if (_editor.showToolTip()) {
600                newUserName = ((String) e.getNewValue());
601                _uLabel = (newUserName == null ? "" : newUserName); // store for display on icon
602                if (newUserName == null || newUserName.equals("")) {
603                    newUserName = Bundle.getMessage("NoUserName"); // longer for tooltip
604                }
605                beanButton.setToolTipText(_switchSysName + " (" + newUserName + ")");
606                beanIcon.setToolTipText(_switchSysName + " (" + newUserName + ")");
607                beanKey.setToolTipText(_switchSysName + " (" + newUserName + ")");
608                beanSymbol.setToolTipText(_switchSysName + " (" + newUserName + ")");
609                log.debug("User Name changed to {}", newUserName);
610            }
611        }
612    }
613
614    void cleanup() {
615        if (namedBean != null) {
616            getTurnout().removePropertyChangeListener(this);
617        }
618        namedBean = null;
619    }
620
621    private boolean tristate = false;
622
623    public void setTristate(boolean set) {
624        tristate = set;
625    }
626
627    public boolean getTristate() {
628        return tristate;
629    }
630
631    boolean momentary = false;
632
633    public boolean getMomentary() {
634        return momentary;
635    }
636
637    public void setMomentary(boolean m) {
638        momentary = m;
639    }
640
641    JPopupMenu switchPopup;
642    JMenuItem connectNewMenu = new JMenuItem(Bundle.getMessage("ConnectNewMenu", "..."));
643
644    /**
645     * Show pop-up on a switch with its unique attributes including the
646     * (un)connected bean. Derived from
647     * {@link jmri.jmrit.display.switchboardEditor.SwitchboardEditor#showPopUp(Positionable, MouseEvent)}
648     *
649     * @param e unused
650     * @return true when pop up displayed
651     */
652    public boolean showPopUp(MouseEvent e) {
653        if (switchPopup != null) {
654            switchPopup.removeAll();
655        } else {
656            switchPopup = new JPopupMenu();
657        }
658
659        switchPopup.add(getNameString());
660
661        if (_editor.isEditable() && _editor.allControlling()) {
662            // add tristate option if turnout has feedback
663            if (namedBean != null) {
664                //addTristateEntry(switchPopup); // beanswitches don't do anything with this property
665                addEditUserName(switchPopup);
666                switch (beanTypeChar) {
667                    case 'T':
668                        if (getTurnout().canInvert()) { // check whether supported by this turnout
669                            addInvert(switchPopup);
670                        }
671                        break;
672                    case 'S':
673                        if (getSensor().canInvert()) { // check whether supported by this sensor
674                            addInvert(switchPopup);
675                        }
676                        break;
677                    default:
678                    // invert is not supported by Lights, so skip
679                }
680            } else {
681                // show option to attach a new bean
682                switchPopup.add(connectNewMenu);
683                connectNewMenu.addActionListener((java.awt.event.ActionEvent e1) -> connectNew(_switchSysName));
684            }
685        }
686        // display the popup
687        switchPopup.show(this, this.getWidth() / 3 + (int) ((_editor.getPaintScale() - 1.0) * this.getX()),
688                this.getHeight() / 3 + (int) ((_editor.getPaintScale() - 1.0) * this.getY()));
689
690        return true;
691    }
692
693    javax.swing.JMenuItem editItem = null;
694
695    void addEditUserName(JPopupMenu popup) {
696        editItem = new javax.swing.JMenuItem(Bundle.getMessage("EditNameTitle", "..."));
697        popup.add(editItem);
698        editItem.addActionListener((java.awt.event.ActionEvent e) -> renameBean());
699    }
700
701    javax.swing.JCheckBoxMenuItem invertItem = null;
702
703    void addInvert(JPopupMenu popup) {
704        invertItem = new javax.swing.JCheckBoxMenuItem(Bundle.getMessage("MenuInvertItem", _switchSysName));
705        invertItem.setSelected(getInverted());
706        popup.add(invertItem);
707        invertItem.addActionListener((java.awt.event.ActionEvent e) -> setBeanInverted(invertItem.isSelected()));
708    }
709
710    /**
711     * Edit user name on a switch.
712     */
713    public void renameBean() {
714        NamedBean nb;
715        String oldName = _uName;
716        // show input dialog
717        String newUserName = (String) JOptionPane.showInputDialog(null,
718                Bundle.getMessage("EnterNewName", _switchSysName),
719                Bundle.getMessage("EditNameTitle", ""), JOptionPane.PLAIN_MESSAGE, null, null, oldName);
720        if (newUserName == null) { // user cancelled
721            log.debug("NewName dialog returned Null, cancelled");
722            return;
723        }
724        log.debug("New name: {}", newUserName);
725        if (newUserName.length() == 0) {
726            log.debug("new user name is empty");
727            JOptionPane.showMessageDialog(null, Bundle.getMessage("WarningEmptyUserName"),
728                    Bundle.getMessage("WarningTitle"),
729                    JOptionPane.ERROR_MESSAGE);
730            return;
731        }
732
733        if (newUserName.equals(oldName)) { // name was not changed by user
734            return;
735        } else { // check if name is already in use
736            switch (beanTypeChar) {
737                case 'T':
738                    nb = jmri.InstanceManager.turnoutManagerInstance().getTurnout(newUserName);
739                    break;
740                case 'S':
741                    nb = jmri.InstanceManager.sensorManagerInstance().getSensor(newUserName);
742                    break;
743                case 'L':
744                    nb = jmri.InstanceManager.lightManagerInstance().getLight(newUserName);
745                    break;
746                default:
747                    log.error("Check userName: cannot parse bean name. userName = {}", newUserName);
748                    return;
749            }
750            if (nb != null) {
751                log.error("User name is not unique {}", newUserName);
752                String msg = Bundle.getMessage("WarningUserName", newUserName);
753                JOptionPane.showMessageDialog(null, msg,
754                        Bundle.getMessage("WarningTitle"),
755                        JOptionPane.ERROR_MESSAGE);
756                return;
757            }
758        }
759        _bname.setUserName(newUserName);
760        if (oldName == null || oldName.equals("")) {
761            if (!nbhm.inUse(_switchSysName, _bname)) {
762                return; // no problem, so stop
763            }
764            String msg = Bundle.getMessage("UpdateToUserName", _editor.getSwitchTypeName(), newUserName, _switchSysName);
765            int optionPane = JOptionPane.showConfirmDialog(null,
766                    msg, Bundle.getMessage("UpdateToUserNameTitle"),
767                    JOptionPane.YES_NO_OPTION);
768            if (optionPane == JOptionPane.YES_OPTION) {
769                //This will update the bean reference from the systemName to the userName
770                try {
771                    nbhm.updateBeanFromSystemToUser(_bname);
772                } catch (JmriException ex) {
773                    // We should never get an exception here as we already check that the username is not valid
774                }
775            }
776
777        } else {
778            nbhm.renameBean(oldName, newUserName, _bname); // will pick up name change in label
779        }
780        _editor.updatePressed(); // but we must redraw whole board
781    }
782
783    private boolean inverted = false;
784
785    public void setInverted(boolean set) {
786        inverted = set;
787    }
788
789    public boolean getInverted() {
790        return inverted;
791    }
792
793    /**
794     * Invert attached object on the layout, if supported by its connection.
795     *
796     * @param set new inverted state, true for inverted, false for normal.
797     */
798    public void setBeanInverted(boolean set) {
799        switch (beanTypeChar) {
800            case 'T':
801                if (getTurnout() != null && getTurnout().canInvert()) { // if supported
802                    this.setInverted(set);
803                    getTurnout().setInverted(set);
804                }
805                break;
806            case 'S':
807                if (getSensor() != null && getSensor().canInvert()) { // if supported
808                    this.setInverted(set);
809                    getSensor().setInverted(set);
810                }
811                break;
812            case 'L':
813                // Lights cannot be inverted, so never called
814                return;
815            default:
816                log.error("Invert item: cannot parse bean name. userName = {}", _switchSysName);
817        }
818    }
819
820    /**
821     * Process mouseClick on this switch, passing in name for debug.
822     *
823     * @param e    the event heard
824     * @param name ID of this button (identical to name of suggested bean
825     *             object)
826     */
827    public void operate(MouseEvent e, String name) {
828        log.debug("Button {} clicked", name);
829        if (namedBean == null || e == null || e.isMetaDown()) {
830            return;
831        }
832        alternateOnClick();
833    }
834
835    /**
836     * Process mouseClick on this switch.
837     * Similar to {@link #operate(MouseEvent, String)}.
838     *
839     * @param e the event heard
840     */
841    public void doMouseClicked(java.awt.event.MouseEvent e) {
842        log.debug("Switch clicked");
843        if (namedBean == null || e == null || e.isMetaDown()) {
844            return;
845        }
846        alternateOnClick();
847    }
848
849    /**
850     * Change the state of attached Turnout, Light or Sensor on the layout
851     * unless menu option Panel Items Control Layout is set to off.
852     */
853    void alternateOnClick() {
854        if (_editor.allControlling()) {
855            switch (beanTypeChar) {
856                case 'T': // Turnout
857                    log.debug("T clicked");
858                    if (getTurnout().getKnownState() == jmri.Turnout.CLOSED) // if clear known state, set to opposite
859                    {
860                        getTurnout().setCommandedState(jmri.Turnout.THROWN);
861                    } else if (getTurnout().getKnownState() == jmri.Turnout.THROWN) {
862                        getTurnout().setCommandedState(jmri.Turnout.CLOSED);
863                    } else if (getTurnout().getCommandedState() == jmri.Turnout.CLOSED) {
864                        getTurnout().setCommandedState(jmri.Turnout.THROWN);  // otherwise, set to opposite of current commanded state if known
865                    } else {
866                        getTurnout().setCommandedState(jmri.Turnout.CLOSED);  // just force Closed
867                    }
868                    break;
869                case 'L': // Light
870                    log.debug("L clicked");
871                    if (getLight().getState() == jmri.Light.OFF) {
872                        getLight().setState(jmri.Light.ON);
873                    } else {
874                        getLight().setState(jmri.Light.OFF);
875                    }
876                    break;
877                case 'S': // Sensor
878                    log.debug("S clicked");
879                    try {
880                        if (getSensor().getKnownState() == jmri.Sensor.INACTIVE) {
881                            getSensor().setKnownState(jmri.Sensor.ACTIVE);
882                        } else {
883                            getSensor().setKnownState(jmri.Sensor.INACTIVE);
884                        }
885                    } catch (jmri.JmriException reason) {
886                        log.warn("Exception flipping sensor: {}", (Object) reason);
887                    }
888                    break;
889                default:
890                    log.error("invalid char in Switchboard Button \"{}\". State not set.", _switchSysName);
891            }
892        }
893    }
894
895    /**
896     * Only for lights. Used for All Off/All On.
897     * Skips unconnected switch icons.
898     *
899     * @param state On = 1, Off = 0
900     */
901    public void switchLight(int state) {
902        if (namedBean != null) {
903            getLight().setState(state);
904        }
905    }
906
907    public void setBackgroundColor(Color bgcolor) {
908        this.setBackground(bgcolor);
909    }
910
911    JmriJFrame addFrame = null;
912    JTextField sysNameTextBox = new JTextField(12);
913    JTextField userName = new JTextField(15);
914
915    /**
916     * Create new bean and connect it to this switch. Use type letter from
917     * switch label (S, T or L).
918     *
919     * @param systemName system name of bean.
920     */
921    protected void connectNew(String systemName) {
922        log.debug("Request new bean");
923        userName.setText(""); // only available on unconnected switches, so no useful content yet
924        // provide etc.
925        if (addFrame == null) {
926            addFrame = new JmriJFrame(Bundle.getMessage("ConnectNewMenu", ""), false, true);
927            addFrame.addHelpMenu("package.jmri.jmrit.display.switchboardEditor.SwitchboardEditor", true);
928            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
929
930            ActionListener okListener = this::okAddPressed;
931            ActionListener cancelListener = this::cancelAddPressed;
932            AddNewDevicePanel switchConnect = new AddNewDevicePanel(sysNameTextBox, userName, "ButtonOK", okListener, cancelListener);
933            switchConnect.setSystemNameFieldIneditable(); // prevent user interference with switch label
934            switchConnect.setOK(); // activate OK button on Add new device pane
935            addFrame.add(switchConnect);
936        }
937        addFrame.pack();
938        addFrame.setVisible(true);
939    }
940
941    protected void cancelAddPressed(ActionEvent e) {
942        if (addFrame != null) {
943            addFrame.setVisible(false);
944            addFrame.dispose();
945            addFrame = null;
946        }
947    }
948
949    protected void okAddPressed(ActionEvent e) {
950        NamedBean nb;
951        String manuPrefix = _editor.getSwitchManu();
952        String user = userName.getText();
953        if (user.trim().equals("")) {
954            user = null;
955        }
956        // systemName can't be changed, fixed
957        if (addFrame != null) {
958            addFrame.setVisible(false);
959            addFrame.dispose();
960            addFrame = null;
961        }
962        switch (_switchSysName.charAt(manuPrefix.length())) {
963            case 'T':
964                Turnout t;
965                try {
966                    // add turnout to JMRI (w/appropriate manager)
967                    t = InstanceManager.turnoutManagerInstance().provideTurnout(_switchSysName);
968                    t.setUserName(user);
969                } catch (IllegalArgumentException ex) {
970                    // user input no good
971                    handleCreateException(_switchSysName);
972                    return; // without creating
973                }
974                nb = jmri.InstanceManager.turnoutManagerInstance().getTurnout(_switchSysName);
975                break;
976            case 'S':
977                Sensor s;
978                try {
979                    // add Sensor to JMRI (w/appropriate manager)
980                    s = InstanceManager.sensorManagerInstance().provideSensor(_switchSysName);
981                    s.setUserName(user);
982                } catch (IllegalArgumentException ex) {
983                    // user input no good
984                    handleCreateException(_switchSysName);
985                    return; // without creating
986                }
987                nb = jmri.InstanceManager.sensorManagerInstance().getSensor(_switchSysName);
988                break;
989            case 'L':
990                Light l;
991                try {
992                    // add Light to JMRI (w/appropriate manager)
993                    l = InstanceManager.lightManagerInstance().provideLight(_switchSysName);
994                    l.setUserName(user);
995                } catch (IllegalArgumentException ex) {
996                    // user input no good
997                    handleCreateException(_switchSysName);
998                    return; // without creating
999                }
1000                nb = jmri.InstanceManager.lightManagerInstance().getLight(_switchSysName);
1001                break;
1002            default:
1003                log.error("connectNew - okAddPressed: cannot parse bean name. sName = {}", _switchSysName);
1004                return;
1005        }
1006        if (nb == null) {
1007            log.warn("failed to connect switch to item {}", _switchSysName);
1008        } else {
1009            // set switch on Switchboard to display current state of just connected bean
1010            log.debug("sName state: {}", nb.getState());
1011            try {
1012                if (_editor.getSwitch(_switchSysName) == null) {
1013                    log.warn("failed to update switch to state of {}", _switchSysName);
1014                } else {
1015                    _editor.updatePressed();
1016                }
1017            } catch (NullPointerException npe) {
1018                handleCreateException(_switchSysName);
1019                // exit without updating
1020            }
1021        }
1022    }
1023
1024    /**
1025     * Chack the switch label currently displayed.
1026     * Used in test.
1027     *
1028     * @return line 1 of the label of this switch
1029     */
1030    protected String getIconLabel() {
1031        switch (_shape) {
1032            case 0 : // button
1033                String lbl = beanButton.getText();
1034                if (!lbl.startsWith("<")) {
1035                    return lbl;
1036                } else { // 2 line label, "<html><center>" + label + "</center>..."
1037                    return lbl.substring(14, lbl.indexOf("</center>"));
1038                }
1039            case 1:
1040                return beanIcon.getIconLabel();
1041            case 2:
1042                return beanKey.getIconLabel();
1043            case 3:
1044                return beanSymbol.getIconLabel();
1045            default:
1046                return "";
1047        }
1048    }
1049
1050    void handleCreateException(String sysName) {
1051        javax.swing.JOptionPane.showMessageDialog(addFrame,
1052                java.text.MessageFormat.format(
1053                        Bundle.getMessage("ErrorSwitchAddFailed"), sysName),
1054                Bundle.getMessage("ErrorTitle"),
1055                javax.swing.JOptionPane.ERROR_MESSAGE);
1056    }
1057
1058    String rootPath = "resources/icons/misc/switchboard/";
1059    String iconOffPath = rootPath + "appslide-off-s.png";
1060    String iconOnPath = rootPath + "appslide-on-s.png";
1061    String keyOffPath = rootPath + "markl-off-s.png";
1062    String keyOnPath = rootPath + "markl-on-s.png";
1063    String symbolOffPath; // = rootPath + "T-off-s.png"; // default for Turnout, replace T by S or L
1064    String symbolOnPath; // = rootPath + "T-on-s.png";
1065
1066    /**
1067     * Class to display individual bean state switches on a JMRI Switchboard
1068     * using 2 image files.
1069     */
1070    public class IconSwitch extends JPanel {
1071
1072        private BufferedImage image;
1073        private BufferedImage image1;
1074        private BufferedImage image2;
1075        private String tag = "tag";
1076        private String subTag = "";
1077        private int labelX = 16;
1078        private int labelY = 53;
1079        private int subLabelX = 16;
1080        private int subLabelY = 53;
1081        private int textSize = 12;
1082        private int subTextSize = 10;
1083        private float textAlign = 0.0f;
1084        private float subTextAlign = 0.0f;
1085        private float ropOffset = 0f;
1086        private int r = 10; // radius of circle fitting inside tile rect in px drawing units
1087        private RescaleOp rop;
1088
1089        /**
1090         * Create an icon from 2 alternating png images.
1091         *
1092         * @param filepath1 the ON image
1093         * @param filepath2 the OFF image
1094         * @param drawingRadius max distance in px from center of switch canvas, unit used for relative scaling
1095         */
1096        public IconSwitch(String filepath1, String filepath2, int drawingRadius) {
1097            // load image files
1098            try {
1099                image1 = ImageIO.read(new File(filepath1));
1100                image2 = ImageIO.read(new File(filepath2));
1101                image = image2; // start off as showing inactive/closed
1102            } catch (IOException ex) {
1103                log.error("error reading image from {}-{}", filepath1, filepath2, ex);
1104            }
1105            if (drawingRadius > 10) r = drawingRadius;
1106            log.debug("radius={} size={}", r, getWidth());
1107        }
1108
1109        public void setOpacity(float offset) {
1110            ropOffset = offset;
1111            float ropScale = 1f;
1112            rop = new RescaleOp(ropScale, ropOffset, null);
1113        }
1114
1115        protected void showSwitchIcon(int stateIndex) {
1116            log.debug("showSwitchIcon {}", stateIndex);
1117            if (image1 != null && image2 != null) {
1118                switch (stateIndex) {
1119                    case 2:
1120                        image = image1; // on/Thrown/Active
1121                        break;
1122                    default:
1123                        image = image2; // off, also for connected & unknown
1124                        break;
1125                }
1126                this.repaint();
1127            }
1128        }
1129
1130        protected void setImage1(String newImagePath) {
1131            try {
1132                image1 = ImageIO.read(new File(newImagePath));
1133            } catch (IOException ex) {
1134                log.error("error reading image from {}", newImagePath, ex);
1135            }
1136        }
1137
1138        /**
1139         * Set or change label text on switch.
1140         *
1141         * @param sName string to display (system name)
1142         * @param uName secondary string to display (user name)
1143         */
1144        protected void setLabels(String sName, String uName) {
1145            tag = sName;
1146            subTag = uName;
1147            this.repaint();
1148        }
1149
1150        private String getIconLabel() {
1151            return tag;
1152        }
1153
1154        /**
1155         * Position (sub)label on switch.
1156         *
1157         * @param x horizontal offset from top left corner, positive to the
1158         *          right
1159         * @param y vertical offset from top left corner, positive down
1160         * @param align one of: JComponent.LEFT_ALIGNMENT (0.0f), CENTER_ALIGNMENT (0.5f),
1161         *              RIGHT_ALIGNMENT (1.0f)
1162         * @param fontsize size in points for label text display
1163         */
1164        protected void positionLabel(int x, int y, float align, int fontsize) {
1165            labelX = x;
1166            labelY = y;
1167            textAlign = align;
1168            textSize = fontsize;
1169        }
1170
1171        protected void positionSubLabel(int x, int y, float align, int fontsize) {
1172            subLabelX = x;
1173            subLabelY = y;
1174            subTextAlign = align;
1175            subTextSize = fontsize;
1176        }
1177
1178        @Override
1179        protected void paintComponent(Graphics g) {
1180            super.paintComponent(g);
1181            Graphics2D g2d = (Graphics2D) g;
1182            g.translate(r, r); // set origin to center
1183            //int imgZoom = Math.min(2*r/image.getWidth(), 2*r/image.getHeight()); // how to enlarge icon on painting?
1184            g2d.drawImage(image, rop, image.getWidth()/-2, image.getHeight()/-2); // center bitmap
1185            //g.drawImage(image, 0, 0, null);
1186            g.setFont(getFont());
1187            if (ropOffset > 0f) {
1188                g.setColor(Color.GRAY); // dimmed
1189            } else {
1190                g.setColor(_editor.getDefaultTextColorAsColor());
1191            }
1192
1193            g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, textSize));
1194            g2d.setRenderingHint( // smoother text display
1195                RenderingHints.KEY_TEXT_ANTIALIASING,
1196                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
1197            if (Math.abs(textAlign - Component.CENTER_ALIGNMENT) < .0001) {
1198                FontMetrics metrics = g.getFontMetrics(); // figure out where the center of the string is
1199                labelX = metrics.stringWidth(tag) / -2;
1200            }
1201            g.drawString(tag, labelX, labelY); // draw name on top of button image (vertical, horizontal offset from top left)
1202
1203            if (showUserName) {
1204                g.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, Math.max(subTextSize, 6)));
1205                if (Math.abs(subTextAlign - Component.CENTER_ALIGNMENT) < .0001) {
1206                    FontMetrics metrics2 = g.getFontMetrics(); // figure out where the center of the string is
1207                    subLabelX = metrics2.stringWidth(subTag) / -2;
1208                }
1209                g.drawString(subTag, subLabelX, subLabelY); // draw user name at bottom
1210            }
1211            this.repaint();
1212        }
1213
1214    }
1215
1216    private final static Logger log = LoggerFactory.getLogger(BeanSwitch.class);
1217
1218}