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