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