001package jmri.jmrit.display.switchboardEditor;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.*;
006import java.awt.event.*;
007import java.util.*;
008import java.util.List;
009
010import javax.annotation.Nonnull;
011import javax.swing.*;
012import javax.swing.border.TitledBorder;
013
014import jmri.*;
015import jmri.jmrit.display.CoordinateEdit;
016import jmri.jmrit.display.Editor;
017import jmri.jmrit.display.Positionable;
018import jmri.jmrit.display.PositionableJComponent;
019import jmri.jmrix.SystemConnectionMemoManager;
020import jmri.swing.ManagerComboBox;
021import jmri.util.ColorUtil;
022import jmri.util.JmriJFrame;
023import jmri.util.ThreadingUtil;
024import jmri.util.swing.JmriColorChooser;
025import jmri.util.swing.JmriJOptionPane;
026import jmri.util.swing.JmriMouseEvent;
027import jmri.util.swing.JmriMouseAdapter;
028import jmri.util.swing.JmriMouseListener;
029import jmri.util.swing.JmriMouseMotionListener;
030
031import static jmri.util.ColorUtil.contrast;
032
033/**
034 * Provides a simple editor for adding jmri.jmrit.display.switchBoard items to a
035 * JLayeredPane inside a captive JFrame. Primary use is for new users.
036 * <p>
037 * GUI is structured as a separate setup panel to set the visible range and type
038 * plus menus.
039 * <p>
040 * All created objects are placed in a GridLayout grid. No special use of the
041 * LayeredPane layers. Inspired by Oracle JLayeredPane demo.
042 * <p>
043 * The "switchesOnBoard" LinkedHashMap keeps track of all the objects added to the target
044 * frame for later manipulation. May be used in an update to store mixed
045 * switchboards with more than 1 connection and more than 1 bean type/range.<br>
046 * The 'ready' flag protects the map during regeneration.
047 * <p>
048 * No DnD as panels will be automatically populated in order of the DCC address.
049 * New beans may be created from the Switchboard by right clicking an
050 * unconnected switch.
051 * TODO allow user entry of connection specific starting name, validated in manager
052 * using hardwareAddressValidator
053 *
054 * @author Pete Cressman Copyright (c) 2009, 2010, 2011
055 * @author Egbert Broerse Copyright (c) 2017, 2018, 2021
056 */
057public class SwitchboardEditor extends Editor {
058
059    protected JMenuBar _menuBar;
060    private JMenu _editorMenu;
061    //protected JMenu _editMenu;
062    protected JMenu _fileMenu;
063    protected JMenu _optionMenu;
064    private transient boolean panelChanged = false;
065
066    // Switchboard items
067    ImageIcon iconPrev = new ImageIcon("resources/icons/misc/gui3/LafLeftArrow_m.gif");
068    private final JLabel prev = new JLabel(iconPrev);
069    ImageIcon iconNext = new ImageIcon("resources/icons/misc/gui3/LafRightArrow_m.gif");
070    private final JLabel next = new JLabel(iconNext);
071    private final int rangeBottom = 1;
072    private final int rangeTop = 100000; // for MERG etc where thousands = node number, total number on board limited to unconnectedRangeLimit anyway
073    private final static int unconnectedRangeLimit = 400;
074    private final static int rangeSizeWarning = 250;
075    private final static int initialMax = 24;
076    private final JSpinner minSpinner = new JSpinner(new SpinnerNumberModel(rangeBottom, rangeBottom, rangeTop - 1, 1));
077    private final JSpinner maxSpinner = new JSpinner(new SpinnerNumberModel(initialMax, rangeBottom + 1, rangeTop, 1));
078    private final JCheckBox hideUnconnected = new JCheckBox(Bundle.getMessage("CheckBoxHideUnconnected"));
079    private final JCheckBox autoItemRange = new JCheckBox(Bundle.getMessage("CheckBoxAutoItemRange"));
080    private JButton allOffButton;
081    private JButton allOnButton;
082    private TargetPane switchboardLayeredPane; // is a JLayeredPane
083    static final String TURNOUT = Bundle.getMessage("Turnouts");
084    static final String SENSOR = Bundle.getMessage("Sensors");
085    static final String LIGHT = Bundle.getMessage("Lights");
086    private final String[] beanTypeStrings = {TURNOUT, SENSOR, LIGHT};
087    private JComboBox<String> beanTypeList;
088    private String _type = TURNOUT;
089    private final String[] switchShapeStrings = {
090        Bundle.getMessage("Buttons"),
091        Bundle.getMessage("Sliders"),
092        Bundle.getMessage("Keys"),
093        Bundle.getMessage("Symbols")
094    };
095    private JComboBox<String> shapeList;
096    final static int BUTTON = 0;
097    final static int SLIDER = 1;
098    final static int KEY = 2;
099    final static int SYMBOL = 3;
100    //final static int ICON = 4;
101    private final ManagerComboBox<Turnout> turnoutManComboBox = new ManagerComboBox<>();
102    private final ManagerComboBox<Sensor> sensorManComboBox = new ManagerComboBox<>();
103    private final ManagerComboBox<Light> lightManComboBox = new ManagerComboBox<>();
104    protected TurnoutManager turnoutManager = InstanceManager.getDefault(TurnoutManager.class);
105    protected SensorManager sensorManager = InstanceManager.getDefault(SensorManager.class);
106    protected LightManager lightManager = InstanceManager.getDefault(LightManager.class);
107    private SystemConnectionMemo memo;
108    private int shape = BUTTON; // for: button
109    //SystemNameValidator hardwareAddressValidator;
110    JTextField addressTextField = new JTextField(10);
111    private TitledBorder border;
112    private final String interact = Bundle.getMessage("SwitchboardInteractHint");
113    private final String noInteract = Bundle.getMessage("SwitchboardNoInteractHint");
114
115    // editor items (adapted from LayoutEditor toolbar)
116    private Color defaultTextColor = Color.BLACK;
117    private Color defaultActiveColor = Color.RED; // user configurable since 4.21.3
118    protected final static Color darkActiveColor = new Color(180, 50, 50);
119    private Color defaultInactiveColor = Color.GREEN; // user configurable since 4.21.3
120    protected final static Color darkInactiveColor = new Color(40, 150, 30);
121    private boolean _hideUnconnected = false;
122    private boolean _autoItemRange = true;
123    private int rows = 4; // matches initial autoRows pref for default pane size
124    private final float cellProportion = 1.0f; // TODO analyse actual W:H per switch type/shape: worthwhile?
125    private int _tileSize = 100;
126    private int _iconSquare = 75;
127    private SwitchBoardLabelDisplays _showUserName = SwitchBoardLabelDisplays.BOTH_NAMES;
128    // tmp @GuardedBy("this")
129    private final JSpinner rowsSpinner = new JSpinner(new SpinnerNumberModel(rows, 1, 25, 1));
130    private final JButton updateButton = new JButton(Bundle.getMessage("ButtonUpdate"));
131    // number of rows displayed on switchboard, disabled when autoRows is on
132    private final JTextArea help2 = new JTextArea(Bundle.getMessage("Help2"));
133    private final JTextArea help3 = new JTextArea(Bundle.getMessage("Help3", Bundle.getMessage("CheckBoxHideUnconnected")));
134    // saved state of options when panel was loaded or created
135    private transient boolean savedEditMode = true;
136    private transient boolean savedControlLayout = true; // menu option to turn this off
137    private final int height = 455;
138    private final int width = 544;
139    private int verticalMargin = 55; // for Nimbus and CDE/Motif
140
141    private final JCheckBoxMenuItem controllingBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxControlling"));
142    private final JCheckBoxMenuItem hideUnconnectedBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxHideUnconnected"));
143    private final JCheckBoxMenuItem autoItemRangeBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxAutoItemRange"));
144    private final JCheckBoxMenuItem showToolTipBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxShowTooltips"));
145    //tmp @GuardedBy("this")
146    private final JCheckBoxMenuItem autoRowsBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxAutoRows"));
147    private final JMenu labelNamesMenu = new JMenu(Bundle.getMessage("SwitchNameDisplayMenu"));
148    private final JCheckBoxMenuItem systemNameBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxSystemName"));
149    private final JCheckBoxMenuItem bothNamesBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxBothNames"));
150    private final JCheckBoxMenuItem displayNameBox = new JCheckBoxMenuItem(Bundle.getMessage("CheckBoxDisplayName"));
151    private final JRadioButtonMenuItem scrollBoth = new JRadioButtonMenuItem(Bundle.getMessage("ScrollBoth"));
152    private final JRadioButtonMenuItem scrollNone = new JRadioButtonMenuItem(Bundle.getMessage("ScrollNone"));
153    private final JRadioButtonMenuItem scrollHorizontal = new JRadioButtonMenuItem(Bundle.getMessage("ScrollHorizontal"));
154    private final JRadioButtonMenuItem scrollVertical = new JRadioButtonMenuItem(Bundle.getMessage("ScrollVertical"));
155    private final JRadioButtonMenuItem sizeSmall = new JRadioButtonMenuItem(Bundle.getMessage("optionSmaller"));
156    private final JRadioButtonMenuItem sizeDefault = new JRadioButtonMenuItem(Bundle.getMessage("optionDefault"));
157    private final JRadioButtonMenuItem sizeLarge = new JRadioButtonMenuItem(Bundle.getMessage("optionLarger"));
158    final static int SIZE_MIN = 50;
159    final static int SIZE_INIT = 100;
160    final static int SIZE_MAX = 150;
161
162    /**
163     * To count number of displayed beanswitches, this array holds all beanswitches to be displayed
164     * until the GridLayout is configured, used to determine the total number of items to be placed.
165     * Accounts for "hide unconnected" setting, so it can be empty. Not synchronized for risk of locking up.
166     */
167    private final LinkedHashMap<String, BeanSwitch> switchesOnBoard = new LinkedHashMap<>();
168    private volatile boolean ready = true;
169
170    /**
171     * Ctor
172     */
173    public SwitchboardEditor() {
174    }
175
176    /**
177     * Ctor by a given name.
178     *
179     * @param name title to assign to the new SwitchBoard
180     */
181    public SwitchboardEditor(String name) {
182        super(name, false, true);
183        init(name);
184    }
185
186    /**
187     * Initialize the newly created Switchboard.
188     *
189     * @param name the title of the switchboard content frame
190     */
191    @Override
192    protected final void init(String name) {
193        //memo = SystemConnectionMemoManager.getDefault().getSystemConnectionMemoForUserName("Internal");
194        // always available (?) and supports all types, not required now, will be set by listener
195
196        Container contentPane = getContentPane(); // the actual Editor configuration pane
197        ThreadingUtil.runOnGUI( () -> setVisible(false)); // start with Editor window hidden
198        setUseGlobalFlag(true); // always true for a Switchboard
199        // handle Editor close box clicked without deleting the Switchboard panel
200        super.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
201        super.addWindowListener(new java.awt.event.WindowAdapter() {
202            @Override
203            public void windowClosing(java.awt.event.WindowEvent e) {
204                log.debug("switchboardEditor close box selected");
205                setAllEditable(false);
206                ThreadingUtil.runOnGUI( () -> setVisible(false)); // hide Editor window
207            }
208        });
209        // make menus
210        _menuBar = new JMenuBar();
211        makeOptionMenu();
212        makeFileMenu();
213
214        setJMenuBar(_menuBar);
215        addHelpMenu("package.jmri.jmrit.display.SwitchboardEditor", true);
216        // set GUI dependant margin if not Nimbus, CDE/Motif (or undefined)
217        if (UIManager.getLookAndFeel() != null) {
218            if (UIManager.getLookAndFeel().getName().equals("Metal")) {
219                verticalMargin = 47;
220            } else if (UIManager.getLookAndFeel().getName().equals("Mac OS X")) {
221                verticalMargin = 25;
222            }
223        }
224        switchboardLayeredPane = new TargetPane(); // extends JLayeredPane();
225        switchboardLayeredPane.setPreferredSize(new Dimension(width, height));
226        border = BorderFactory.createTitledBorder(
227                BorderFactory.createLineBorder(defaultTextColor),
228                "temp",
229                TitledBorder.LEADING,
230                TitledBorder.ABOVE_BOTTOM,
231                getFont(),
232                defaultTextColor);
233        switchboardLayeredPane.setBorder(border);
234        // create contrast with background, should also specify border style
235        // specify title for turnout, sensor, light, mixed? (wait for the Editor to be created)
236        switchboardLayeredPane.addMouseMotionListener(JmriMouseMotionListener.adapt(this));
237
238        // add control pane and layered pane to this JPanel
239        JPanel beanSetupPane = new JPanel();
240        beanSetupPane.setLayout(new FlowLayout(FlowLayout.TRAILING));
241        JLabel beanTypeTitle = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanTypeLabel")));
242        beanSetupPane.add(beanTypeTitle);
243        beanTypeList = new JComboBox<>(beanTypeStrings);
244        beanTypeList.setSelectedIndex(0); // select bean type T in comboBox
245        beanTypeList.addActionListener((ActionEvent event) -> {
246            String typeChoice = (String) beanTypeList.getSelectedItem();
247            if (typeChoice != null) {
248                displayManagerComboBoxes(typeChoice); // so these boxes should already be instantiated by now
249            }
250            updatePressed();
251            setDirty();
252        });
253        beanSetupPane.add(beanTypeList);
254
255        // add connection selection comboBox
256        char beanTypeChar = getSwitchType().charAt(0); // translate from selectedIndex to char
257        log.debug("beanTypeChar set to [{}]", beanTypeChar);
258        JLabel beanManuTitle = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("ConnectionLabel")));
259        beanSetupPane.add(beanManuTitle);
260
261        beanSetupPane.add(turnoutManComboBox);
262        beanSetupPane.add(sensorManComboBox);
263        beanSetupPane.add(lightManComboBox);
264
265        turnoutManComboBox.setToolTipText(Bundle.getMessage("ManComboBoxTip", Bundle.getMessage("BeanNameTurnout")));
266        sensorManComboBox.setToolTipText(Bundle.getMessage("ManComboBoxTip", Bundle.getMessage("BeanNameSensor")));
267        lightManComboBox.setToolTipText(Bundle.getMessage("ManComboBoxTip", Bundle.getMessage("BeanNameLight")));
268
269        configureManagerComboBoxes(); // fill the combos
270        displayManagerComboBoxes(TURNOUT); // show TurnoutManagerBox (matches the beanType combo
271
272//        hardwareAddressValidator = new SystemNameValidator(addressTextField,
273//                turnoutManComboBox.getItemAt(0),
274//                false); // initial system (for type Turnout)
275//        addressTextField.setInputVerifier(hardwareAddressValidator);
276
277//        hardwareAddressValidator.addPropertyChangeListener("validation", (evt) -> { // NOI18N
278//            Validation validation = hardwareAddressValidator.getValidation();
279//            Validation.Type valid = validation.getType();
280//            updateButton.setEnabled(valid != Validation.Type.WARNING && valid != Validation.Type.DANGER);
281//            help2.setText(validation.getMessage());
282//        });
283//        hardwareAddressValidator.setManager(turnoutManComboBox.getItemAt(0)); // initial system (for type Turnout)
284//        hardwareAddressValidator.verify(addressTextField);
285
286        add(beanSetupPane);
287
288        // add shape combobox
289        JPanel switchShapePane = new JPanel();
290        switchShapePane.setLayout(new FlowLayout(FlowLayout.TRAILING));
291        JLabel switchShapeTitle = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("SwitchShape")));
292        switchShapePane.add(switchShapeTitle);
293        shapeList = new JComboBox<>(switchShapeStrings);
294        shapeList.setSelectedIndex(0); // select Button choice in comboBox
295        shapeList.addActionListener((ActionEvent event) -> {
296            shape = (Math.max(shapeList.getSelectedIndex(), 0)); // picks 1st item when no selection
297            updatePressed();
298            setDirty();
299        });
300        switchShapePane.add(shapeList);
301        // add column spinner
302        JLabel rowsLabel = new JLabel(Bundle.getMessage("NumberOfRows"));
303        switchShapePane.add(rowsLabel);
304        rowsSpinner.setToolTipText(Bundle.getMessage("RowsSpinnerOnTooltip"));
305        rowsSpinner.addChangeListener(e -> {
306            //tmp synchronized (this) {
307                if (!autoRowsBox.isSelected()) { // spinner is disabled when autoRows is on, but just in case
308                    rows = (Integer) rowsSpinner.getValue();
309                    updatePressed();
310                    setDirty();
311                }
312            //tmp }
313        });
314        switchShapePane.add(rowsSpinner);
315        rowsSpinner.setEnabled(false);
316        add(switchShapePane);
317
318        JPanel checkboxPane = new JPanel();
319        checkboxPane.setLayout(new FlowLayout(FlowLayout.TRAILING));
320        // autoItemRange checkbox on panel
321        autoItemRange.setSelected(autoItemRange());
322        log.debug("autoItemRangeBox set to {}", autoItemRange.isSelected());
323        autoItemRange.addActionListener((ActionEvent event) -> {
324            setAutoItemRange(autoItemRange.isSelected());
325            autoItemRangeBox.setSelected(autoItemRange()); // also (un)check the box on the menu
326            // if set to checked, store the current range from the spinners
327        });
328        checkboxPane.add(autoItemRange);
329        autoItemRange.setToolTipText(Bundle.getMessage("AutoItemRangeTooltip"));
330        // hideUnconnected checkbox on panel
331        hideUnconnected.setSelected(_hideUnconnected);
332        log.debug("hideUnconnectedBox set to {}", hideUnconnected.isSelected());
333        hideUnconnected.addActionListener((ActionEvent event) -> {
334            setHideUnconnected(hideUnconnected.isSelected());
335            hideUnconnectedBox.setSelected(_hideUnconnected); // also (un)check the box on the menu
336            help2.setVisible(!_hideUnconnected && (!switchesOnBoard.isEmpty())); // and show/hide instruction line unless no items on board
337            updatePressed();
338            setDirty();
339        });
340        checkboxPane.add(hideUnconnected);
341        add(checkboxPane);
342
343        /* Construct special JFrame to hold the actual switchboard */
344        switchboardLayeredPane.setLayout(new GridLayout(3, 8)); // initial layout params
345        // Add at least 1 switch to pane to create switchList: done later, would be deleted soon if added now
346        // see updatePressed()
347
348        // provide a JLayeredPane to place the switches on
349        super.setTargetPanel(switchboardLayeredPane, makeFrame(name));
350        super.getTargetFrame().setSize(550, 430); // width x height //+ was 550, 330
351        //super.getTargetFrame().setSize(width + 6, height + 25); // width x height
352
353        // set scrollbar initial state
354        setScroll(SCROLL_NONE);
355        scrollNone.setSelected(true);
356        // set icon size initial state
357        _iconSquare = SIZE_INIT;
358        sizeDefault.setSelected(true);
359        // register the resulting panel for later configuration
360        ConfigureManager cm = InstanceManager.getNullableDefault(ConfigureManager.class);
361        if (cm != null) {
362            cm.registerUser(this);
363        }
364
365        //add(addressTextField);
366        add(createControlPanel());
367
368        updateButton.addActionListener((ActionEvent event) -> {
369            updatePressed();
370            setDirty();
371        });
372        allOnButton = new JButton(Bundle.getMessage("AllOn"));
373        allOnButton.addActionListener((ActionEvent event) -> switchAllLights(Light.ON));
374        allOffButton = new JButton(Bundle.getMessage("AllOff"));
375        allOffButton.addActionListener((ActionEvent event) -> switchAllLights(Light.OFF));
376        JPanel allPane = new JPanel();
377        allPane.setLayout(new BoxLayout(allPane, BoxLayout.PAGE_AXIS));
378        allPane.add(allOnButton);
379        allPane.add(allOffButton);
380
381        JPanel updatePanel = new JPanel();
382        updatePanel.add(updateButton);
383        updatePanel.add(allPane);
384
385        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS));
386        contentPane.add(updatePanel);
387        setupEditorPane(); // re-layout all the toolbar items
388
389        lightManComboBox.addActionListener((ActionEvent event) -> {
390            Manager<Light> manager = lightManComboBox.getSelectedItem();
391            if (manager != null) {
392                memo = manager.getMemo();
393                addressTextField.setText("");     // Reset input before switching managers
394                //hardwareAddressValidator.setManager(manager);
395                log.debug("Lbox set to {}. Updating", memo.getUserName());
396                updatePressed();
397                setDirty();
398            }
399        });
400        sensorManComboBox.addActionListener((ActionEvent event) -> {
401            Manager<Sensor> manager = sensorManComboBox.getSelectedItem();
402            if (manager != null) {
403                memo = manager.getMemo();
404                addressTextField.setText("");     // Reset input before switching managers
405                //hardwareAddressValidator.setManager(manager);
406                log.debug("Sbox set to {}. Updating", memo.getUserName());
407                updatePressed();
408                setDirty();
409            }
410        });
411        turnoutManComboBox.addActionListener((ActionEvent event) -> {
412            Manager<Turnout> manager = turnoutManComboBox.getSelectedItem();
413            if (manager != null) {
414                memo = manager.getMemo();
415                addressTextField.setText("");     // Reset input before switching managers
416                //hardwareAddressValidator.setManager(manager);
417                log.debug("Tbox set to {}. Updating", memo.getUserName());
418                updatePressed();
419                setDirty();
420            }
421        });
422        turnoutManComboBox.setSelectedItem("Internal"); // order of items in combo may vary on init() wait till now for init completed
423        lightManComboBox.setSelectedItem("Internal"); // NOI18N
424        sensorManComboBox.setSelectedItem("Internal"); // NOI18N
425        log.debug("boxes are set to Internal, attaching listeners");
426
427        updatePressed(); // refresh default Switchboard, rebuilds and resizes all switches, required for tests
428
429        // component listener handles frame resizing event
430        super.getTargetFrame().addComponentListener(new ComponentAdapter() {
431            @Override
432            public void componentResized(ComponentEvent e) {
433                //log.debug("PANEL RESIZED");
434                resizeInFrame();
435            }
436        });
437    }
438
439    /**
440     * Just repaint the Switchboard target panel.
441     * Fired on componentResized(e) event.
442     */
443    private void resizeInFrame() {
444        Dimension frSize = super.getTargetFrame().getSize(); // 5 px for border, var px for footer, autoRows(int)
445        // some GUIs include (wide) menu bar inside frame
446        switchboardLayeredPane.setSize(new Dimension((int) frSize.getWidth() - 6, (int) frSize.getHeight() - verticalMargin));
447        switchboardLayeredPane.repaint();
448        //tmp synchronized (this) {
449            if (autoRowsBox.isSelected()) { // check if autoRows is active
450                int oldRows = rows;
451                rows = autoRows(cellProportion); // if it suggests a different value for rows, call updatePressed()
452                if (rows != oldRows) {
453                    rowsSpinner.setValue(rows); // updatePressed will update rows spinner in display, but spinner will not propagate when disabled
454                    updatePressed(); // redraw if rows value changed
455                }
456            }
457        //tmp }
458    }
459
460    /**
461     * Create a new set of switches after removing the current array.
462     * <p>
463     * Called by Update button click, and automatically after loading a panel
464     * from XML (with all saved options set).
465     * Switchboard JPanel WindowResize() event is handled by resizeInFrame()
466     */
467    public void updatePressed() {
468        ThreadingUtil.runOnGUI(this::updatePressedOnGui);
469    }
470
471    private void updatePressedOnGui() {
472        log.debug("updatePressed START _tileSize = {}", _tileSize);
473
474        if (_autoItemRange && !autoItemRange.isSelected()) {
475            autoItemRange.setSelected(true);
476        }
477        setVisible(_editable); // show/hide editor
478
479        // update selected address range
480        int range = (Integer) maxSpinner.getValue() - (Integer) minSpinner.getValue() + 1;
481        if (range > unconnectedRangeLimit && !_hideUnconnected) {
482            // fixed maximum number of items on a Switchboard to prevent memory overflow
483            range = unconnectedRangeLimit;
484            maxSpinner.setValue((Integer) minSpinner.getValue() + range - 1);
485        }
486        // check for extreme number of items
487        log.debug("address range = {}", range);
488        if (range > rangeSizeWarning) {
489            // ask user if range is indeed desired
490            log.debug("Warning for big range");
491            int retval = JmriJOptionPane.showOptionDialog(this,
492                    Bundle.getMessage("LargeRangeWarning", range, Bundle.getMessage("CheckBoxHideUnconnected")),
493                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
494                    new Object[]{Bundle.getMessage("ButtonYes"), Bundle.getMessage("ButtonCancel")}, null);
495            log.debug("Retval: {}", retval);
496            if (retval != 0) { // NOT array position 0 ButtonYes
497                return;
498            }
499        }
500        ready = false; // set flag for updating
501        // if range is confirmed, go ahead with switchboard update
502        for (int i = switchesOnBoard.size() - 1; i >= 0; i--) {
503            //            if (i >= switchboardLayeredPane.getComponentCount()) { // turn off this check for now
504            //                continue;
505            //            }
506            // remove listeners before removing switches from JLayeredPane
507            ((BeanSwitch) switchboardLayeredPane.getComponent(i)).cleanup();
508            // deleting items starting from 0 will result in skipping the even numbered items
509            switchboardLayeredPane.remove(i);
510        }
511        switchesOnBoard.clear(); // reset beanswitches LinkedHashMap
512        log.debug("switchesOnBoard cleared, size is now: 0"); // always 0 at this point
513        switchboardLayeredPane.setSize(width, height);
514
515        String memoName = (memo != null ? memo.getUserName() : "UNKNOWN");
516        log.debug("creating range for manu index {}", memoName);
517
518//        Validation.Type valid = hardwareAddressValidator.getValidation().getType();
519        String startAddress = "";
520//        if (addressTextField.getText() != null && valid != Validation.Type.WARNING && valid != Validation.Type.DANGER) {
521//            startAddress = addressTextField.getText();
522//        }
523        // fill switchesOnBoard LinkedHashMap, uses memo/manager already set
524        createSwitchRange((Integer) minSpinner.getValue(),
525                (Integer) maxSpinner.getValue(),
526                beanTypeList.getSelectedIndex(),
527                shapeList.getSelectedIndex(),
528                startAddress);
529
530        if (autoRowsBox.isSelected()) {
531            rows = autoRows(cellProportion); // TODO: use specific proportion value per Type/Shape choice?
532            log.debug("autoRows() called in updatePressed(). Rows = {}", rows);
533            rowsSpinner.setValue(rows);
534        }
535        // disable the Rows spinner & Update button on the Switchboard Editor pane
536        // param: GridLayout(vertical, horizontal), at least 1x1
537        switchboardLayeredPane.setLayout(new GridLayout(Math.max(rows, 1), 1));
538
539        // add switches to LayeredPane
540        for (BeanSwitch bs : switchesOnBoard.values()) {
541            switchboardLayeredPane.add(bs);
542        }
543        ready = true; // reset flag
544        help3.setVisible(switchesOnBoard.isEmpty()); // show/hide help3 warning
545        help2.setVisible(!switchesOnBoard.isEmpty()); // hide help2 when help3 is shown vice versa (as no items are dimmed or not)
546        // update the title at the bottom of the switchboard to match (no) layout control
547        if (beanTypeList.getSelectedIndex() >= 0) {
548            border.setTitle(memoName + " " +
549                    beanTypeList.getSelectedItem() + " - " + (allControlling() ? interact : noInteract));
550        }
551        // hide AllOn/Off buttons unless type is Light and control is allowed
552        allOnButton.setVisible((beanTypeList.getSelectedIndex() == 2) && allControlling());
553        allOffButton.setVisible((beanTypeList.getSelectedIndex() == 2) && allControlling());
554        pack();
555        // must repaint again to fit inside frame
556        Dimension frSize = super.getTargetFrame().getSize(); // 2x3 px for border, var px for footer + optional UI menubar, autoRows(int)
557        switchboardLayeredPane.setSize(new Dimension((int) frSize.getWidth() - 6, (int) frSize.getHeight() - verticalMargin));
558        switchboardLayeredPane.repaint();
559
560        log.debug("updatePressed END _tileSize = {}", _tileSize);
561    }
562
563    /**
564     * From default or user entry in Editor, create a LinkedHashMap of Switches.
565     * <p>
566     * Items in range that can connect to existing beans in JMRI are active. The
567     * others are greyed out. Option to later connect (new) beans to switches.
568     *
569     * @param min         starting ordinal of Switch address range
570     * @param max         highest ordinal of Switch address range
571     * @param beanType    index of selected item in Type comboBox, either T, S
572     *                    or L
573     * @param shapeChoice index of selected visual presentation of Switch shape
574     *                    selected in Type comboBox, choose either a JButton
575     *                    showing the name or (to do) a graphic image
576     */
577    private void createSwitchRange(int min, int max, int beanType, int shapeChoice, @Nonnull String startAddress) {
578        log.debug("createSwitchRange - _hideUnconnected = {}", _hideUnconnected);
579        String name;
580        BeanSwitch _switch;
581        NamedBean nb;
582        if (memo == null) {
583            log.error("createSwitchRange - null memo, can't create range");
584            return;
585        }
586        String prefix = memo.getSystemPrefix();
587        // TODO handling of non-numeric system names such as MERG, C/MRI using validator textField
588        // if (!startAddress.equals("")) { // use as start address, spinners are only for the number of items
589        log.debug("createSwitchRange - _manuprefix={} beanType={}", prefix, beanType);
590        // use validated bean names
591        for (int i = min; i <= max; i++) {
592            switch (beanType) {
593                case 0:
594                    try {
595                        name = memo.get(TurnoutManager.class).createSystemName(i + "", prefix);
596                    } catch (JmriException ex) {
597                        log.error("Error creating range at turnout {}", i);
598                        return;
599                    }
600                    nb = InstanceManager.getDefault(TurnoutManager.class).getTurnout(name);
601                    break;
602                case 1:
603                    try { // was: InstanceManager.getDefault(SensorManager.class)
604                        name = memo.get(SensorManager.class).createSystemName(i + "", prefix);
605                    } catch (JmriException | NullPointerException ex) {
606                        log.trace("Error creating range at sensor {}. Connection {}", i, memo.getUserName(), ex);
607                        return;
608                    }
609                    nb = InstanceManager.getDefault(SensorManager.class).getSensor(name);
610                    break;
611                case 2:
612                    try {
613                        name = memo.get(LightManager.class).createSystemName(i + "", prefix);
614                    } catch (JmriException ex) {
615                        log.error("Error creating range at light {}", i);
616                        return;
617                    }
618                    nb = InstanceManager.lightManagerInstance().getLight(name);
619                    break;
620                default:
621                    log.error("addSwitchRange: cannot parse bean name. Prefix = {}; i = {}; type={}", prefix, i, beanType);
622                    return;
623            }
624            if (nb == null && _hideUnconnected) {
625                continue; // skip bean i
626            }
627            log.debug("Creating Switch for {}", name);
628            _switch = new BeanSwitch(i, nb, name, shapeChoice, this); // add button instance i
629            if (nb == null) {
630                _switch.setEnabled(false); // not connected
631            } else {
632                // set switch to display current bean state
633                _switch.displayState(nb.getState());
634            }
635            switchesOnBoard.put(name, _switch); // add to LinkedHashMap of switches for later placement on JLayeredPane
636            log.debug("Added switch {}", name);
637            // keep total number of switches below practical total of 400 (20 x 20 items)
638            if (switchesOnBoard.size() >= unconnectedRangeLimit) {
639                log.warn("switchboards are limited to {} items", unconnectedRangeLimit);
640                break;
641            }
642            // was already checked in first counting loop in init()
643        }
644    }
645
646    /**
647     * Create the setup pane for the top of the frame. From layeredpane demo.
648     */
649    private JPanel createControlPanel() {
650        JPanel controls = new JPanel();
651
652        // navigation top row and range to set
653        JPanel navBarPanel = new JPanel();
654        navBarPanel.setLayout(new BoxLayout(navBarPanel, BoxLayout.X_AXIS));
655
656        navBarPanel.add(prev);
657        prev.addMouseListener(JmriMouseListener.adapt(new JmriMouseAdapter() {
658            @Override
659            public void mouseClicked(JmriMouseEvent e) {
660                int oldMin = getMinSpinner();
661                int oldMax = getMaxSpinner();
662                int range = Math.max(oldMax - oldMin + 1, 1); // make sure range > 0
663                log.debug("prev range was {}, oldMin ={}, oldMax ={}", range, oldMin, oldMax);
664                setMinSpinner(Math.max((oldMin - range), rangeBottom)); // first set new min
665                if (_autoItemRange) {
666                    setMaxSpinner(Math.max((oldMax - range), range));   // set new max (only if auto)
667                }
668                updatePressed();
669                setDirty();
670                log.debug("new prev range = {}, newMin ={}, newMax ={}", range, getMinSpinner(), getMaxSpinner());
671            }
672        }));
673        prev.setToolTipText(Bundle.getMessage("PreviousToolTip", Bundle.getMessage("CheckBoxAutoItemRange")));
674        navBarPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("From"))));
675        JComponent minEditor = minSpinner.getEditor();
676        // enlarge minSpinner editor text field width
677        JFormattedTextField minTf = ((JSpinner.DefaultEditor) minEditor).getTextField();
678        minTf.setColumns(5);
679        minSpinner.addChangeListener(e -> {
680            JSpinner spinner = (JSpinner) e.getSource();
681            int value = (int)spinner.getValue();
682            // stop if value >= maxSpinner -1 (range <= 0)
683            if (value >= (Integer) maxSpinner.getValue() - 1) {
684                maxSpinner.setValue(value + 1);
685            }
686            updatePressed();
687            setDirty();
688        });
689        navBarPanel.add(minSpinner);
690        navBarPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("UpTo"))));
691        // enlarge maxSpinner editor text field width
692        JComponent maxEditor = maxSpinner.getEditor();
693        JFormattedTextField maxTf = ((JSpinner.DefaultEditor) maxEditor).getTextField();
694        maxTf.setColumns(5);
695        maxSpinner.addChangeListener(e -> {
696            JSpinner spinner = (JSpinner) e.getSource();
697            int value = (int)spinner.getValue();
698            // stop if value <= minSpinner + 1 (range <= 0)
699            if (value <= (Integer) minSpinner.getValue() + 1) {
700                minSpinner.setValue(value - 1);
701            }
702            updatePressed();
703            setDirty();
704        });
705        navBarPanel.add(maxSpinner);
706
707        navBarPanel.add(next);
708        next.addMouseListener(JmriMouseListener.adapt(new JmriMouseAdapter() {
709            @Override
710            public void mouseClicked(JmriMouseEvent e) {
711                int oldMin = getMinSpinner();
712                int oldMax = getMaxSpinner();
713                int range = Math.max(oldMax - oldMin + 1, 1); // make sure range > 0
714                log.debug("next range was {}, oldMin ={}, oldMax ={}", range, oldMin, oldMax);
715                setMaxSpinner(Math.min((oldMax + range), rangeTop));               // first set new max
716                if (_autoItemRange) {
717                    setMinSpinner(Math.min(oldMin + range, rangeTop - range + 1)); // set new min (only if auto)
718                }
719                updatePressed();
720                setDirty();
721                log.debug("new next range = {}, newMin ={}, newMax ={}", range, getMinSpinner(), getMaxSpinner());
722            }
723        }));
724        next.setToolTipText(Bundle.getMessage("NextToolTip", Bundle.getMessage("CheckBoxAutoItemRange")));
725        navBarPanel.add(Box.createHorizontalGlue());
726
727        controls.add(navBarPanel); // put items on 2nd Editor Panel
728        controls.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("SelectRangeTitle")));
729        return controls;
730    }
731
732    private int getMinSpinner() { //tmp
733        return (Integer) minSpinner.getValue();
734    }
735
736    private int getMaxSpinner() { //tmp synchronized
737        return (Integer) maxSpinner.getValue();
738    }
739
740    protected void setMinSpinner(int value) { //tmp synchronized
741        if (value >= rangeBottom && value < rangeTop) { // allows to set above MaxSpinner temporarily
742            minSpinner.setValue(value);
743        }
744    }
745
746    protected void setMaxSpinner(int value) { //tmp synchronized
747        if (value > rangeBottom && value <= rangeTop) { // allows to set above MinSpinner temporarily
748            maxSpinner.setValue(value);
749        }
750    }
751
752    private void setupEditorPane() {
753        // Initial setup
754        Container contentPane = getContentPane(); // Editor (configuration) pane
755
756        JPanel innerBorderPanel = new JPanel();
757        innerBorderPanel.setLayout(new BoxLayout(innerBorderPanel, BoxLayout.PAGE_AXIS));
758        TitledBorder TitleBorder = BorderFactory.createTitledBorder(Bundle.getMessage("SwitchboardHelpTitle"));
759        innerBorderPanel.setBorder(TitleBorder);
760        innerBorderPanel.add(new JTextArea(Bundle.getMessage("Help1")));
761        // help2 explains: dimmed icons = unconnected
762        innerBorderPanel.add(help2);
763        if (!_hideUnconnected) {
764            help2.setVisible(false); // hide this text when _hideUnconnected is set to true from menu or checkbox
765        }
766        // help3 warns: no icons to show on switchboard
767        help3.setForeground(Color.red);
768        innerBorderPanel.add(help3);
769        help3.setVisible(false); // initially hide help3 warning text
770        contentPane.add(innerBorderPanel);
771    }
772
773    //@Override
774    protected void makeOptionMenu() {
775        _optionMenu = new JMenu(Bundle.getMessage("MenuOptions"));
776        _menuBar.add(_optionMenu, 0);
777        // controllable item
778        _optionMenu.add(controllingBox);
779        controllingBox.addActionListener((ActionEvent event) -> {
780            setAllControlling(controllingBox.isSelected());
781            // update the title on the switchboard to match (no) layout control
782            if (beanTypeList.getSelectedItem() != null) {
783                border.setTitle(memo.getUserName() + " " +
784                        beanTypeList.getSelectedItem().toString() + " - " + (allControlling() ? interact : noInteract));
785            }
786            allOnButton.setVisible((beanTypeList.getSelectedIndex() == 2) && allControlling());
787            allOffButton.setVisible((beanTypeList.getSelectedIndex() == 2) && allControlling());
788            switchboardLayeredPane.repaint();
789            log.debug("border title updated");
790        });
791        controllingBox.setSelected(allControlling());
792
793        // autoItemRange item
794        _optionMenu.add(autoItemRangeBox);
795        autoItemRangeBox.addActionListener((ActionEvent event) -> {
796            setAutoItemRange(autoItemRangeBox.isSelected());
797            autoItemRange.setSelected(autoItemRange()); // also (un)check the box on the editor
798        });
799        autoItemRangeBox.setSelected(autoItemRange());
800
801        _optionMenu.addSeparator();
802
803        // auto rows item
804        _optionMenu.add(autoRowsBox);
805        //tmp synchronized (this) {
806            autoRowsBox.setSelected(true); // default on
807        //tmp }
808        autoRowsBox.addActionListener((ActionEvent event) -> {
809            //tmp synchronized (this) {
810                if (autoRowsBox.isSelected()) {
811                    log.debug("autoRows was turned ON");
812                    int oldRows = rows;
813                    rows = autoRows(cellProportion); // recalculates rows x columns and redraws pane
814                    // sets _tileSize TODO: specific proportion value per Type/Shape choice?
815                    rowsSpinner.setEnabled(false);
816                    rowsSpinner.setToolTipText(Bundle.getMessage("RowsSpinnerOffTooltip"));
817                    // hide rowsSpinner + rowsLabel?
818                    if (rows != oldRows) {
819                        // rowsSpinner will be recalculated by auto so we don't copy the old value
820                        updatePressed(); // redraw if rows value changed
821                    }
822                } else {
823                    log.debug("autoRows was turned OFF");
824                    rowsSpinner.setValue(rows); // autoRows turned off, copy current auto value to spinner
825                    rowsSpinner.setEnabled(true); // show rowsSpinner + rowsLabel?
826                    rowsSpinner.setToolTipText(Bundle.getMessage("RowsSpinnerOnTooltip"));
827                    // calculate tile size
828                    int colNum = (((getTotal() > 0) ? (getTotal()) : 1) + rows - 1) / Math.max(rows, 1);
829                    int maxW = (super.getTargetFrame().getWidth() - 10) / colNum; // int division, subtract 2x3px for border
830                    int maxH = (super.getTargetFrame().getHeight() - verticalMargin) / Math.max(rows, 1); // for footer
831                    _tileSize = Math.min(maxW, maxH); // store for tile graphics
832                    log.debug("_tileSize {} from {}, {}", _tileSize, maxW, maxH);
833                }
834            //tmp }
835        });
836        // show tooltip item
837        _optionMenu.add(showToolTipBox);
838        showToolTipBox.addActionListener((ActionEvent e) -> setAllShowToolTip(showToolTipBox.isSelected()));
839        showToolTipBox.setSelected(showToolTip());
840        // hideUnconnected item
841        _optionMenu.add(hideUnconnectedBox);
842        hideUnconnectedBox.setSelected(_hideUnconnected);
843        hideUnconnectedBox.addActionListener((ActionEvent event) -> {
844            setHideUnconnected(hideUnconnectedBox.isSelected());
845            hideUnconnected.setSelected(_hideUnconnected); // also (un)check the box on the editor
846            help2.setVisible(!_hideUnconnected && (!switchesOnBoard.isEmpty())); // and show/hide instruction line unless no items on board
847            updatePressed();
848            setDirty();
849        });
850        // switch label options
851        _optionMenu.add(labelNamesMenu);
852        // only system name
853        labelNamesMenu.add(systemNameBox);
854        systemNameBox.setSelected(false); // default off
855        systemNameBox.addActionListener((ActionEvent e) -> setLabel(SwitchBoardLabelDisplays.SYSTEM_NAME));
856        // both names (when set)
857        labelNamesMenu.add(bothNamesBox);
858        bothNamesBox.setSelected(true); // default on
859        bothNamesBox.addActionListener((ActionEvent e) -> setLabel(SwitchBoardLabelDisplays.BOTH_NAMES));
860        // only user name (when set), aka display name
861        labelNamesMenu.add(displayNameBox);
862        displayNameBox.setSelected(false); // default off
863        displayNameBox.addActionListener((ActionEvent e) -> setLabel(SwitchBoardLabelDisplays.USER_NAME));
864
865        // Show/Hide Scroll Bars
866        JMenu scrollMenu = new JMenu(Bundle.getMessage("ComboBoxScrollable"));
867        _optionMenu.add(scrollMenu);
868        ButtonGroup scrollGroup = new ButtonGroup();
869        scrollGroup.add(scrollBoth);
870        scrollMenu.add(scrollBoth);
871        scrollBoth.addActionListener((ActionEvent event) -> setScroll(SCROLL_BOTH));
872        scrollGroup.add(scrollNone);
873        scrollMenu.add(scrollNone);
874        scrollNone.addActionListener((ActionEvent event) -> setScroll(SCROLL_NONE));
875        scrollGroup.add(scrollHorizontal);
876        scrollMenu.add(scrollHorizontal);
877        scrollHorizontal.addActionListener((ActionEvent event) -> setScroll(SCROLL_HORIZONTAL));
878        scrollGroup.add(scrollVertical);
879        scrollMenu.add(scrollVertical);
880        scrollVertical.addActionListener((ActionEvent event) -> setScroll(SCROLL_VERTICAL));
881
882        // add beanswitch size menu item
883        JMenu iconSizeMenu = new JMenu(Bundle.getMessage("MenuIconSize"));
884        _optionMenu.add(iconSizeMenu);
885        ButtonGroup sizeGroup = new ButtonGroup();
886        sizeGroup.add(sizeSmall);
887        iconSizeMenu.add(sizeSmall);
888        sizeSmall.addActionListener((ActionEvent event) -> setIconScale(SIZE_MIN));
889        sizeGroup.add(sizeDefault);
890        iconSizeMenu.add(sizeDefault);
891        sizeDefault.addActionListener((ActionEvent event) -> setIconScale(SIZE_INIT));
892        sizeGroup.add(sizeLarge);
893        iconSizeMenu.add(sizeLarge);
894        sizeLarge.addActionListener((ActionEvent event) -> setIconScale(SIZE_MAX));
895
896        JMenu colorMenu = new JMenu(Bundle.getMessage("Colors"));
897        _optionMenu.add(colorMenu);
898        // add text color menu item
899        JMenuItem textColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultTextColor", "..."));
900        colorMenu.add(textColorMenuItem);
901        textColorMenuItem.addActionListener((ActionEvent event) -> {
902            Color desiredColor = JmriColorChooser.showDialog(this,
903                                 Bundle.getMessage("DefaultTextColor", ""),
904                                 defaultTextColor);
905            if (desiredColor != null && !defaultTextColor.equals(desiredColor)) {
906                // if new defaultTextColor matches bgColor, ask user as labels will become unreadable
907                if (desiredColor.equals(defaultBackgroundColor)) {
908                    int retval = JmriJOptionPane.showOptionDialog(this,
909                    Bundle.getMessage("ColorIdenticalWarningF"), Bundle.getMessage("WarningTitle"),
910                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
911                    new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("ButtonInvert"),
912                    Bundle.getMessage("ButtonCancel")}, Bundle.getMessage("ButtonCancel"));
913                    if (retval == 1) { // array position 1, invert the other color
914                        setDefaultBackgroundColor(contrast(defaultBackgroundColor));
915                    } else if (retval != 0) { // NOT ButtonOK, ie cancel or Dialog closed
916                        return;
917                    }
918                }
919                defaultTextColor = desiredColor;
920                border.setTitleColor(desiredColor);
921                setDirty(true);
922                JmriColorChooser.addRecentColor(desiredColor);
923                updatePressed();
924            }
925        });
926        // add background color menu item
927        JMenuItem backgroundColorMenuItem = new JMenuItem(Bundle.getMessage("SetBackgroundColor", "..."));
928        colorMenu.add(backgroundColorMenuItem);
929        backgroundColorMenuItem.addActionListener((ActionEvent event) -> {
930            Color desiredColor = JmriColorChooser.showDialog(this,
931                    Bundle.getMessage("SetBackgroundColor", ""),
932                    defaultBackgroundColor);
933            if (desiredColor != null && !defaultBackgroundColor.equals(desiredColor)) {
934                // if new bgColor matches the defaultTextColor, ask user as labels will become unreadable
935                if (desiredColor.equals(defaultTextColor)) {
936                    int retval = JmriJOptionPane.showOptionDialog(this,
937                        Bundle.getMessage("ColorIdenticalWarningR"), Bundle.getMessage("WarningTitle"),
938                        JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
939                        new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("ButtonInvert"), Bundle.getMessage("ButtonCancel")},
940                        Bundle.getMessage("ButtonCancel"));
941                    if (retval == 1) { // invert the other color
942                        defaultTextColor = contrast(defaultTextColor);
943                        border.setTitleColor(defaultTextColor);
944                    } else if (retval != 0) {  // cancel or close Dialog
945                        return;
946                    }
947                }
948                defaultBackgroundColor = desiredColor;
949                setBackgroundColor(desiredColor);
950                setDirty(true);
951                JmriColorChooser.addRecentColor(desiredColor);
952                updatePressed();
953            }
954        });
955        // add ActiveColor menu item
956        JMenuItem activeColorMenuItem = new JMenuItem(Bundle.getMessage("SetActiveColor", "..."));
957        colorMenu.add(activeColorMenuItem);
958        activeColorMenuItem.addActionListener((ActionEvent event) -> {
959            Color desiredColor = JmriColorChooser.showDialog(this,
960                    Bundle.getMessage("SetActiveColor", ""),
961                    defaultActiveColor);
962            if (desiredColor != null && !defaultActiveColor.equals(desiredColor)) {
963                // if new ActiveColor matches InactiveColor, ask user as state will become unreadable
964                if (desiredColor.equals(defaultInactiveColor)) {
965                    int retval = JmriJOptionPane.showOptionDialog(this,
966                        Bundle.getMessage("ColorIdenticalWarningF"), Bundle.getMessage("WarningTitle"),
967                        JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
968                        new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("ButtonInvert"), Bundle.getMessage("ButtonCancel")}, Bundle.getMessage("ButtonCancel"));
969                    if (retval == 1) { // array position 1 invert the other color
970                        setDefaultInactiveColor(contrast(defaultInactiveColor));
971                    } else if (retval != 0) { // Cancel or Dialog closed
972                        return; // cancel
973                    }
974                }
975                defaultActiveColor = desiredColor;
976                setDirty(true);
977                JmriColorChooser.addRecentColor(desiredColor);
978                updatePressed();
979            }
980        });
981        // add InctiveColor menu item
982        JMenuItem inactiveColorMenuItem = new JMenuItem(Bundle.getMessage("SetInactiveColor", "..."));
983        colorMenu.add(inactiveColorMenuItem);
984        inactiveColorMenuItem.addActionListener((ActionEvent event) -> {
985            Color desiredColor = JmriColorChooser.showDialog(this,
986                    Bundle.getMessage("SetInactiveColor", ""),
987                    defaultInactiveColor);
988            if (desiredColor != null && !defaultInactiveColor.equals(desiredColor)) {
989                // if new InactiveColor matches ActiveColor, ask user as state will become unreadable
990                if (desiredColor.equals(defaultInactiveColor)) {
991                    int retval = JmriJOptionPane.showOptionDialog(this,
992                        Bundle.getMessage("ColorIdenticalWarningF"), Bundle.getMessage("WarningTitle"),
993                        JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
994                        new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("ButtonInvert"), Bundle.getMessage("ButtonCancel")}, Bundle.getMessage("ButtonCancel"));
995                    if (retval == 1) { // array position 1 invert the other color
996                        setDefaultActiveColor(contrast(defaultActiveColor));
997                    } else if (retval != 0) { // cancel or Dialog closed
998                        return; // cancel
999                    }
1000                }
1001                defaultInactiveColor = desiredColor;
1002                setDirty(true);
1003                JmriColorChooser.addRecentColor(desiredColor);
1004                updatePressed();
1005            }
1006        });
1007    }
1008
1009    private void makeFileMenu() {
1010        _fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
1011        _menuBar.add(_fileMenu, 0);
1012        _fileMenu.add(new jmri.jmrit.display.NewPanelAction(Bundle.getMessage("MenuItemNew")));
1013
1014        _fileMenu.add(new jmri.configurexml.StoreXmlUserAction(Bundle.getMessage("FileMenuItemStore")));
1015
1016        JMenuItem editItem = new JMenuItem(Bundle.getMessage("renamePanelMenu", "..."));
1017        PositionableJComponent z = new PositionableJComponent(this);
1018        z.setScale(getPaintScale());
1019        editItem.addActionListener(CoordinateEdit.getNameEditAction(z));
1020        _fileMenu.add(editItem);
1021
1022        _fileMenu.addSeparator();
1023
1024        JMenuItem deleteItem = new JMenuItem(Bundle.getMessage("DeletePanel"));
1025        _fileMenu.add(deleteItem);
1026        deleteItem.addActionListener((ActionEvent event) -> {
1027            if (deletePanel()) {
1028                getTargetFrame().dispose();
1029                dispose();
1030            }
1031        });
1032        _fileMenu.addSeparator();
1033        editItem = new JMenuItem(Bundle.getMessage("CloseEditor"));
1034        _fileMenu.add(editItem);
1035        editItem.addActionListener((ActionEvent event) -> {
1036            log.debug("switchboardeditor edit menu CloseEditor selected");
1037            setAllEditable(false);
1038            setVisible(false); // hide Editor pane
1039        });
1040    }
1041
1042    public void setDefaultTextColor(Color color) {
1043        defaultTextColor = color;
1044        border.setTitleColor(color);
1045    }
1046
1047    public String getDefaultTextColor() {
1048        return ColorUtil.colorToColorName(defaultTextColor);
1049    }
1050
1051    public Color getDefaultTextColorAsColor() {
1052        return defaultTextColor;
1053    }
1054
1055    public String getActiveSwitchColor() {
1056        return ColorUtil.colorToColorName(defaultActiveColor);
1057    }
1058    public Color getActiveColorAsColor() {
1059        return defaultActiveColor;
1060    }
1061    public void setDefaultActiveColor(Color color) {
1062        defaultActiveColor = color;
1063    }
1064
1065    public String getInactiveSwitchColor() {
1066        return ColorUtil.colorToColorName(defaultInactiveColor);
1067    }
1068    public Color getInactiveColorAsColor() {
1069        return defaultInactiveColor;
1070    }
1071    public void setDefaultInactiveColor(Color color) {
1072        defaultInactiveColor = color;
1073    }
1074
1075    /**
1076     * Load from xml and set bg color of _targetpanel as well as variable.
1077     *
1078     * @param color RGB Color for switchboard background and beanSwitches
1079     */
1080    public void setDefaultBackgroundColor(Color color) {
1081        setBackgroundColor(color); // via Editor to update bg color of JPanel
1082        defaultBackgroundColor = color;
1083    }
1084
1085    /**
1086     * Get current default background color.
1087     *
1088     * @return background color of this Switchboard
1089     */
1090    public Color getDefaultBackgroundColor() {
1091        return defaultBackgroundColor;
1092    }
1093
1094    public void setLabel(SwitchBoardLabelDisplays label) {
1095        _showUserName = label;
1096        switch (label) {
1097            case SYSTEM_NAME :
1098                //deselect box 2 and 3
1099                bothNamesBox.setSelected(false);
1100                displayNameBox.setSelected(false);
1101                break;
1102            case USER_NAME :
1103                //deselect box 1 and 2
1104                systemNameBox.setSelected(false);
1105                bothNamesBox.setSelected(false);
1106                break;
1107            case BOTH_NAMES :
1108            default:
1109                //deselect box 1 and 3
1110                systemNameBox.setSelected(false);
1111                displayNameBox.setSelected(false);
1112                break;
1113        }
1114        updatePressed();
1115    }
1116
1117    // *********************** end Menus ************************
1118
1119    @Override
1120    public void setAllEditable(boolean edit) {
1121        log.debug("_editable set to {} in super", edit);
1122        if (edit) {
1123            if (_editorMenu != null) {
1124                _menuBar.remove(_editorMenu);
1125            }
1126            if (_optionMenu == null) {
1127                makeOptionMenu();
1128            } else {
1129                _menuBar.add(_optionMenu, 0);
1130            }
1131            if (_fileMenu == null) {
1132                makeFileMenu();
1133            } else {
1134                _menuBar.add(_fileMenu, 0);
1135            }
1136            log.debug("added File and Options menubar");
1137            //contentPane.SetUpdateButtonEnabled(false);
1138        } else {
1139            if (_fileMenu != null) {
1140                _menuBar.remove(_fileMenu);
1141            }
1142            if (_optionMenu != null) {
1143                _menuBar.remove(_optionMenu);
1144            }
1145            if (_editorMenu == null) {
1146                _editorMenu = new JMenu(Bundle.getMessage("MenuEdit"));
1147                _editorMenu.add(new AbstractAction(Bundle.getMessage("OpenEditor")) {
1148                    @Override
1149                    public void actionPerformed(ActionEvent e) {
1150                        setAllEditable(true);
1151                        log.debug("Switchboard Editor Open Editor menu called");
1152                    }
1153                });
1154                _menuBar.add(_editorMenu, 0);
1155            }
1156            //contentPane.SetUpdateButtonEnabled(true);
1157        }
1158        super.setAllEditable(edit);
1159        super.setTitle();
1160        _menuBar.revalidate();
1161    }
1162
1163    @Override
1164    public void setUseGlobalFlag(boolean set) {
1165        controllingBox.setEnabled(set);
1166        super.setUseGlobalFlag(set);
1167    }
1168
1169    @Override
1170    public void setTitle() {
1171        String name = getName(); // get name of JFrame
1172        log.debug("JFrame name = {}", name);
1173        if (name == null || name.length() == 0) {
1174            name = Bundle.getMessage("SwitchboardDefaultName", "");
1175        }
1176        super.setTitle(name + " " + Bundle.getMessage("LabelEditor"));
1177        super.getTargetFrame().setTitle(name);
1178    }
1179
1180    /**
1181     * Control whether target panel items without a connection to the layout are
1182     * displayed.
1183     *
1184     * @param state true to hide all in range
1185     */
1186    public void setHideUnconnected(boolean state) {
1187        _hideUnconnected = state;
1188    }
1189
1190    public boolean hideUnconnected() {
1191        return _hideUnconnected;
1192    }
1193
1194    /**
1195     * Control whether range of items is automatically preserved.
1196     *
1197     * @param state true to calculate upper limit from lowest value range value set (default)
1198     */
1199    public void setAutoItemRange(boolean state) {
1200        _autoItemRange = state;
1201    }
1202
1203    public boolean autoItemRange() {
1204        return _autoItemRange;
1205    }
1206
1207    /**
1208     * Determine optimal cols/rows inside JPanel using switch range, icon proportions of beanswitch icons +
1209     * web canvas W:H proportions range from 1.5 (3:2) to 0.7 (1:1.5), assume squares for now.
1210     *
1211     * @param cellProp the W:H proportion of image, currently 1.0f for all shapes
1212     * @return number of rows on current target pane size/proportions displaying biggest tiles
1213     */
1214    private int autoRows(float cellProp) {
1215        // find cell matrix that allows largest size icons
1216        double paneEffectiveWidth = Math.ceil((super.getTargetFrame().getWidth() - 6)/ Math.max(cellProp, 0.1f)); // -2x3px for border
1217        //log.debug("paneEffectiveWidth: {}", paneEffectiveWidth); // compare to resizeInFrame()
1218        double paneHeight = super.getTargetFrame().getHeight() - verticalMargin; // for footer
1219        int columnsNum = 1;
1220        int rowsNum = 1;
1221        float tileSize = 0.1f; // start value
1222        float tileSizeOld = 0.0f;
1223        int totalDisplayed = ((getTotal() > 0) ? (getTotal()) : 1);
1224        // if all items unconnected and set to be hidden, use 1
1225        if (totalDisplayed >= unconnectedRangeLimit) {
1226            log.warn("switchboards are limited to {} items", unconnectedRangeLimit);
1227        }
1228
1229        while (tileSize > tileSizeOld) {
1230            rowsNum = (totalDisplayed + columnsNum - 1) / Math.max(columnsNum, 1); // int roundup
1231            tileSizeOld = tileSize; // store for comparison
1232            tileSize = (float) Math.min(paneEffectiveWidth / Math.max(columnsNum, 1), paneHeight / Math.max(rowsNum, 1));
1233            //log.debug("C>R Cols {} x Rows {}, tileSize {} was {}", columnsNum, rowsNum, String.format("%.2f", tileSize),
1234            //        String.format("%.2f", tileSizeOld));
1235            if (tileSize < tileSizeOld) {
1236                rowsNum = (totalDisplayed + columnsNum - 2) / Math.max((columnsNum - 1), 1);
1237                break;
1238            }
1239            columnsNum++;
1240        }
1241
1242        // start over stepping columns instead of rows
1243        int columnsNumC;
1244        int rowsNumC = 1;
1245        float tileSizeC = 0.1f;
1246        float tileSizeCOld = 0.0f;
1247        while (tileSizeC > tileSizeCOld) {
1248            columnsNumC = (totalDisplayed + rowsNumC - 1) / Math.max(rowsNumC, 1); // int roundup
1249            tileSizeCOld = tileSizeC; // store for comparison
1250            tileSizeC = (float) Math.min(paneEffectiveWidth / Math.max(columnsNumC, 1), paneHeight / Math.max(rowsNumC, 1));
1251            //log.debug("R>C Cols {} x Rows {}, tileSizeC {} was {}", columnsNumC, rowsNumC, String.format("%.2f", tileSizeC),
1252            //        String.format("%.2f", tileSizeCOld));
1253            if (tileSizeC < tileSizeCOld) {
1254                rowsNumC--;
1255                break;
1256            }
1257            rowsNumC++;
1258        }
1259
1260        if (tileSizeC > tileSize) { // we must choose the largest solution
1261            rowsNum = rowsNumC;
1262        }
1263        // Math.min(1,... to prevent >100% width calc (when hide unconnected selected)
1264        // rows = (total + columns - 1) / columns (int roundup) to account for unused tiles in grid:
1265        // for 23 switches we need at least 24 tiles (4x6, 3x8, 2x12 etc)
1266        // similar calculations repeated in panel.js for web display
1267        log.debug("CELL SIZE optimum found: CxR = {}x{} for {} x {} pixels", ((totalDisplayed + rowsNum - 1) / rowsNum), rowsNum, super.getTargetFrame().getWidth(), super.getTargetFrame().getHeight());
1268
1269        _tileSize = Math.round((float) paneHeight / Math.max(rowsNum, 1)); // recalculate tileSize from rowNum, store for tile graphics
1270        log.debug("_tileSize {} from {} / {}", _tileSize, paneHeight, rowsNum);
1271        return rowsNum;
1272    }
1273
1274    /**
1275     * Allow external reset of dirty bit.
1276     */
1277    public void resetDirty() {
1278        setDirty(false);
1279        savedEditMode = isEditable();
1280        savedControlLayout = allControlling();
1281    }
1282
1283    /**
1284     * Allow external set of dirty bit.
1285     * @param val new dirty flag value, true dirty, false clean.
1286     */
1287    public void setDirty(boolean val) {
1288        panelChanged = val;
1289    }
1290
1291    public void setDirty() {
1292        setDirty(true);
1293    }
1294
1295    /**
1296     * Check the dirty state.
1297     * @return true if panel changed, else false.
1298     */
1299    public boolean isDirty() {
1300        return panelChanged;
1301    }
1302
1303    // ********************** load/store board *******************
1304    /**
1305     * Load Range minimum.
1306     *
1307     * @param rangemin lowest address to show
1308     */
1309    public void setPanelMenuRangeMin(int rangemin) {
1310        minSpinner.setValue(rangemin);
1311    }
1312
1313    /**
1314     * Load Range maximum.
1315     *
1316     * @param rangemax highest address to show
1317     */
1318    public void setPanelMenuRangeMax(int rangemax) {
1319        maxSpinner.setValue(rangemax);
1320    }
1321
1322    /**
1323     * Store Range minimum.
1324     *
1325     * @return lowest address shown
1326     */
1327    public int getPanelMenuRangeMin() {
1328        return (int) minSpinner.getValue();
1329    }
1330
1331    /**
1332     * Store Range maximum.
1333     *
1334     * @return highest address shown
1335     */
1336    public int getPanelMenuRangeMax() {
1337        return (int) maxSpinner.getValue();
1338    }
1339
1340    // ***************** Store & Load xml ********************
1341    /**
1342     * Store bean type.
1343     *
1344     * @return bean type prefix as set for Switchboard
1345     */
1346    public String getSwitchType() {
1347        String typePref;
1348        String switchType = "";
1349        if (beanTypeList.getSelectedItem() != null) {
1350            switchType = beanTypeList.getSelectedItem().toString();
1351        }
1352        if (switchType.equals(LIGHT)) { // switch-case doesn't work here
1353            typePref = "L";
1354        } else if (switchType.equals(SENSOR)) {
1355            typePref = "S";
1356        } else { // Turnout
1357            typePref = "T";
1358        }
1359        return typePref;
1360    }
1361
1362    /**
1363     * Get bean type name.
1364     *
1365     * @return bean type name
1366     */
1367    public String getSwitchTypeName() {
1368        return _type;
1369    }
1370
1371    /**
1372     * Load bean type from xml.
1373     *
1374     * @param prefix the bean type prefix
1375     */
1376    public void setSwitchType(String prefix) {
1377        switch (prefix) {
1378            case "L":
1379                _type = LIGHT;
1380                break;
1381            case "S":
1382                _type = SENSOR;
1383                break;
1384            case "T":
1385            default:
1386                _type = TURNOUT;
1387        }
1388        try {
1389            beanTypeList.setSelectedItem(_type);
1390        } catch (IllegalArgumentException e) {
1391            log.error("invalid bean type [{}] in Switchboard", prefix);
1392        }
1393    }
1394
1395    /**
1396     * Store connection type.
1397     *
1398     * @return active bean connection prefix
1399     */
1400    public String getSwitchManu() {
1401        return memo.getSystemPrefix();
1402    }
1403
1404    /**
1405     * Load connection type.
1406     *
1407     * @param manuPrefix connection prefix
1408     */
1409    public void setSwitchManu(String manuPrefix) {
1410        try {
1411            memo = SystemConnectionMemoManager.getDefault().getSystemConnectionMemoForSystemPrefix(manuPrefix);
1412            if (memo == null) {
1413                log.error("No default SystemConnectionMemo defined for prefix {}", manuPrefix);
1414                return;
1415            }
1416            if (memo.get(TurnoutManager.class) != null) { // just for initial view
1417                turnoutManComboBox.setSelectedItem(memo.get(TurnoutManager.class));
1418                log.debug("turnoutManComboBox set to {} for {}", memo.getUserName(), manuPrefix);
1419            }
1420            if (memo.get(SensorManager.class) != null) { // we expect the user has same preference for the other types
1421                sensorManComboBox.setSelectedItem(memo.get(SensorManager.class));
1422                // TODO LocoNet does not provide a sensormanager via the memo
1423                log.debug("sensorManComboBox set to {} for {}", memo.getUserName(), manuPrefix);
1424            }
1425            if (memo.get(LightManager.class) != null) { // so we set them the same (only 1 value stored as set on store)
1426                lightManComboBox.setSelectedItem(memo.get(LightManager.class));
1427                log.debug("lightManComboBox set to {} for {}", memo.getUserName(), manuPrefix);
1428            }
1429        } catch (IllegalArgumentException e) {
1430            log.error("invalid connection [{}] in Switchboard, {}", manuPrefix, e.getMessage());
1431        } catch (NullPointerException e) {
1432            log.error("NPE setting prefix to [{}] in Switchboard", manuPrefix);
1433        }
1434    }
1435
1436    /**
1437     * Store switch shape.
1438     *
1439     * @return bean shape prefix
1440     */
1441    public String getSwitchShape() {
1442        String shapeAsString;
1443        switch (shape) {
1444            case SLIDER:
1445                shapeAsString = "icon";
1446                break;
1447            case KEY:
1448                shapeAsString = "drawing";
1449                break;
1450            case SYMBOL:
1451                shapeAsString = "symbol";
1452                break;
1453            case (BUTTON):
1454            default: // 0 = basic labelled button
1455                shapeAsString = "button";
1456        }
1457        return shapeAsString;
1458    }
1459
1460    /**
1461     * Load switch shape.
1462     *
1463     * @param switchShape name of switch shape
1464     */
1465    public void setSwitchShape(String switchShape) {
1466        switch (switchShape) {
1467            case "icon":
1468                shape = SLIDER;
1469                break;
1470            case "drawing":
1471                shape = KEY;
1472                break;
1473            case "symbol":
1474                shape = SYMBOL;
1475                break;
1476            default: // button
1477                shape = BUTTON;
1478        }
1479        try {
1480            shapeList.setSelectedIndex(shape);
1481        } catch (IllegalArgumentException e) {
1482            log.error("invalid switch shape [{}] in Switchboard", shape);
1483        }
1484    }
1485
1486    /**
1487     * Store Switchboard rowsNum JSpinner or turn on autoRows option.
1488     *
1489     * @return the number of switches to display per row or 0 if autoRowsBox (menu-setting) is selected
1490     */
1491    public int getRows() { //tmp synchronized
1492        if (autoRowsBox.isSelected()) {
1493            return 0;
1494        } else {
1495            return rows;
1496        }
1497    }
1498
1499    /**
1500     * Load Switchboard rowsNum JSpinner.
1501     *
1502     * @param rws the number of switches displayed per row (as text) or 0 te activate autoRowsBox setting
1503     */
1504    public void setRows(int rws) { //tmp synchronized
1505        autoRowsBox.setSelected(rws == 0);
1506        if (rws > 0) {
1507            rowsSpinner.setValue(rws); // rows is set via rowsSpinner
1508            rowsSpinner.setEnabled(true);
1509        } else {
1510            rowsSpinner.setEnabled(false);
1511            rowsSpinner.setToolTipText(Bundle.getMessage("RowsSpinnerOffTooltip"));
1512            rows = autoRows(cellProportion); // recalculate, TODO: specific proportion value for Type/Shape choice?
1513            rowsSpinner.setValue(rows);
1514        }
1515    }
1516
1517    /**
1518     * Store total number of switches displayed (unconnected/hidden excluded).
1519     *
1520     * @return the total number of switches displayed
1521     */
1522    public int getTotal() {
1523        return switchesOnBoard.size();
1524    }
1525
1526    // all content loaded from file.
1527    public void loadComplete() {
1528        log.debug("loadComplete");
1529    }
1530
1531    // used for xml persistent storage and web display
1532    public String showUserName() {
1533        switch (_showUserName) {
1534            case SYSTEM_NAME :
1535                return "no";
1536            case USER_NAME :
1537                return "displayname";
1538            case BOTH_NAMES :
1539            default :
1540                return "yes";
1541        }
1542        // xml type="labelType", see xml/schema/types/switchboardeditor.xsd and panel.js
1543    }
1544
1545    /**
1546     * Get the label type.
1547     * @return current setting of display type (e.g. system name, both, user name)
1548     */
1549    public SwitchBoardLabelDisplays nameDisplay() {
1550        return _showUserName;
1551    }
1552
1553    public void setShowUserName(SwitchBoardLabelDisplays label) {
1554        _showUserName = label;
1555        switch (label) {
1556            case BOTH_NAMES:
1557                systemNameBox.setSelected(false);
1558                bothNamesBox.setSelected(true);
1559                displayNameBox.setSelected(false);
1560                break;
1561            case USER_NAME:
1562                systemNameBox.setSelected(false);
1563                bothNamesBox.setSelected(false);
1564                displayNameBox.setSelected(true);
1565                break;
1566            case SYSTEM_NAME:
1567            default:
1568                systemNameBox.setSelected(true);
1569                bothNamesBox.setSelected(false);
1570                displayNameBox.setSelected(false);
1571                break;
1572        }
1573    }
1574
1575    /**
1576     * After construction, initialize all the widgets to their saved config
1577     * settings.
1578     */
1579    @Override
1580    public void initView() {
1581        controllingBox.setSelected(allControlling());
1582        showToolTipBox.setSelected(showToolTip());
1583        switch (_scrollState) {
1584            case SCROLL_NONE:
1585                scrollNone.setSelected(true);
1586                break;
1587            case SCROLL_BOTH:
1588                scrollBoth.setSelected(true);
1589                break;
1590            case SCROLL_HORIZONTAL:
1591                scrollHorizontal.setSelected(true);
1592                break;
1593            default:
1594                scrollVertical.setSelected(true);
1595        }
1596        log.debug("InitView done");
1597    }
1598
1599    protected Manager<?> getManager(char typeChar) {
1600        switch (typeChar) {
1601            case 'T': // Turnout
1602                return InstanceManager.getNullableDefault(TurnoutManager.class);
1603            case 'S': // Sensor
1604                return InstanceManager.getNullableDefault(SensorManager.class);
1605            case 'L': // Light
1606                return InstanceManager.getNullableDefault(LightManager.class);
1607            default:
1608                log.error("Unsupported bean type character \"{}\" found.", typeChar);
1609                return null;
1610        }
1611    }
1612
1613    /**
1614     * Get the currently active manager.
1615     *
1616     * @return manager in use for the currently selected bean type and connection
1617     */
1618    protected Manager<?> getManager() {
1619        if (_type.equals(TURNOUT)) {
1620            return turnoutManComboBox.getSelectedItem();
1621        } else if (_type.equals(SENSOR)) {
1622                return sensorManComboBox.getSelectedItem();
1623        } else if (_type.equals(LIGHT)) {
1624            return lightManComboBox.getSelectedItem();
1625        } else {
1626            log.error("Unsupported bean type character \"{}\" found.", _type);
1627            return null;
1628        }
1629    }
1630
1631    /**
1632     * KeyListener of Editor.
1633     *
1634     * @param e the key event heard
1635     */
1636    @Override
1637    public void keyPressed(KeyEvent e) {
1638        repaint();
1639        // TODO select another switch using keypad? accessibility
1640    }
1641
1642    @Override
1643    public void mousePressed(JmriMouseEvent event) {
1644    }
1645
1646    @Override
1647    public void mouseReleased(JmriMouseEvent event) {
1648    }
1649
1650    @Override
1651    public void mouseClicked(JmriMouseEvent event) {
1652    }
1653
1654    @Override
1655    public void mouseDragged(JmriMouseEvent event) {
1656    }
1657
1658    @Override
1659    public void mouseMoved(JmriMouseEvent event) {
1660    }
1661
1662    @Override
1663    public void mouseEntered(JmriMouseEvent event) {
1664        _targetPanel.repaint();
1665    }
1666
1667    @Override
1668    public void mouseExited(JmriMouseEvent event) {
1669        setToolTip(null);
1670        _targetPanel.repaint(); // needed for ToolTip on targetPane
1671    }
1672
1673    /**
1674     * Handle close of Editor window.
1675     * <p>
1676     * Overload/override method in JmriJFrame parent, which by default is
1677     * permanently closing the window. Here, we just want to make it invisible,
1678     * so we don't dispose it (yet).
1679     */
1680    @Override
1681    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
1682            justification = "Just hiding the window, not actually closing it")
1683    public void windowClosing(java.awt.event.WindowEvent e) {
1684        setVisible(false);
1685        setAllEditable(false);
1686        log.debug("windowClosing");
1687    }
1688
1689    /**
1690     * Handle opening of Editor window.
1691     * <p>
1692     * Overload/override method in JmriJFrame parent to reset _menuBar.
1693     */
1694    @Override
1695    public void windowOpened(java.awt.event.WindowEvent e) {
1696        _menuBar.revalidate();
1697    }
1698
1699    // ************* implementation of Abstract Editor methods **********
1700
1701    /**
1702     * The target window has been requested to close. Don't delete it at this
1703     * time. Deletion must be accomplished via the "Delete this Panel" menu item.
1704     */
1705    @Override
1706    protected void targetWindowClosingEvent(java.awt.event.WindowEvent e) {
1707        boolean save = (isDirty() || (savedEditMode != isEditable())
1708                || (savedControlLayout != allControlling()));
1709        log.trace("Temp fix to disable CI errors: save = {}", save);
1710        targetWindowClosing();
1711    }
1712
1713    /**
1714     * changeView is not supported by SwitchBoards.
1715     * {@inheritDoc}
1716     */
1717    @Override
1718    protected Editor changeView(String className) {
1719        return null;
1720    }
1721
1722    /**
1723     * Create sequence of panels, etc. for switches: JFrame contains its
1724     * ContentPane which contains a JPanel with BoxLayout (p1) which contains a
1725     * JScrollPane (js) which contains the targetPane.
1726     * Note this is a private menuBar, looking identical to the Editor's _menuBar
1727     *
1728     * @param name title for the Switchboard.
1729     * @return frame containing the switchboard editor.
1730     */
1731    public JmriJFrame makeFrame(String name) {
1732        JmriJFrame targetFrame = new JmriJFrame(name);
1733        ThreadingUtil.runOnGUI( () -> targetFrame.setVisible(true) );
1734
1735        JMenuBar menuBar = new JMenuBar();
1736        JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit"));
1737        menuBar.add(editMenu);
1738        editMenu.add(new AbstractAction(Bundle.getMessage("OpenEditor")) {
1739            @Override
1740            public void actionPerformed(ActionEvent e) {
1741                setVisible(true);
1742                setAllEditable(true);
1743                log.debug("Switchboard Open Editor menu called");
1744            }
1745        });
1746        targetFrame.setJMenuBar(menuBar);
1747
1748        targetFrame.addHelpMenu("package.jmri.jmrit.display.SwitchboardEditor", true);
1749        return targetFrame;
1750    }
1751
1752    @Override
1753    protected void paintTargetPanel(Graphics g) {
1754        // Switch shapes not directly available from switchboardEditor
1755    }
1756
1757    /**
1758     * Get a beanSwitch object from this SwitchBoard panel by a given name.
1759     *
1760     * @param sName name of switch label/connected bean
1761     * @return BeanSwitch switch object with the given name
1762     */
1763    protected BeanSwitch getSwitch(String sName) {
1764        if (ready && switchesOnBoard.containsKey(sName)) {
1765            return switchesOnBoard.get(sName);
1766        }
1767        log.warn("Switch {} not found on panel. Number of switches displayed: {}", sName, switchesOnBoard.size());
1768        return null;
1769    }
1770
1771    /**
1772     * Get a list with copies of BeanSwitch objects currently displayed to transfer to
1773     * Web Server for display.
1774     *
1775     * @return list of all BeanSwitch switch object
1776     */
1777    public List<BeanSwitch> getSwitches() {
1778        ArrayList<BeanSwitch> _switches = new ArrayList<>();
1779        log.debug("N = {}", switchesOnBoard.size());
1780        if (ready) {
1781            for (Map.Entry<String, BeanSwitch> bs : switchesOnBoard.entrySet()) {
1782                _switches.add(bs.getValue());
1783            }
1784        }
1785        return _switches;
1786    }
1787
1788    /**
1789     * Set up item(s) to be copied by paste.
1790     * <p>
1791     * Not used on switchboards but has to override Editor.
1792     */
1793    @Override
1794    protected void copyItem(Positionable p) {
1795    }
1796
1797    /**
1798     * Set an object's location when it is created.
1799     * <p>
1800     * Not used on switchboards but has to override Editor.
1801     *
1802     * @param obj object to position
1803     */
1804    @Override
1805    public void setNextLocation(Positionable obj) {
1806    }
1807
1808    protected ArrayList<Positionable> getSelectionGroup() {
1809        return null;
1810    }
1811
1812    @Override
1813    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1814        List<NamedBeanUsageReport> report = new ArrayList<>();
1815        if (bean != null) {
1816            getSwitches().forEach((beanSwitch) -> {
1817                if (bean.equals(beanSwitch.getNamedBean())) {
1818                    report.add(new NamedBeanUsageReport("SwitchBoard", getName()));
1819                }
1820            });
1821        }
1822        return report;
1823    }
1824
1825    public int getTileSize() { //tmp synchronized
1826        return _tileSize; // initially 100
1827    }
1828
1829    /**
1830     * Set connected Lights (only).
1831     *
1832     * @param on state to set Light.ON or Light.OFF
1833     */
1834    public void switchAllLights(int on) {
1835        if (ready) {
1836            for (BeanSwitch bs : switchesOnBoard.values()) {
1837                bs.switchLight(on);
1838            }
1839        }
1840    }
1841
1842    /**
1843     * Configure the combo box listing managers.
1844     * Adapted from AbstractTableAction.
1845     */
1846    protected void configureManagerComboBoxes() {
1847        LightManager defaultManagerL = InstanceManager.getDefault(LightManager.class);
1848        if (defaultManagerL instanceof ProxyManager) {
1849            lightManComboBox.setManagers(defaultManagerL);
1850        } else {
1851            lightManComboBox.setManagers(lightManager);
1852        }
1853
1854        SensorManager defaultManagerS = InstanceManager.getDefault(SensorManager.class);
1855        if (defaultManagerS instanceof ProxyManager) {
1856            sensorManComboBox.setManagers(defaultManagerS);
1857            log.debug("using PROXYmanager for Sensors");
1858        } else {
1859            sensorManComboBox.setManagers(sensorManager);
1860        }
1861
1862        TurnoutManager defaultManagerT = InstanceManager.getDefault(TurnoutManager.class);
1863        if (defaultManagerT instanceof ProxyManager) {
1864            turnoutManComboBox.setManagers(defaultManagerT);
1865            log.debug("using PROXYmanager for Turnouts");
1866        } else {
1867            turnoutManComboBox.setManagers(turnoutManager);
1868        }
1869    }
1870        // TODO store current selection in prefman
1871
1872    /**
1873     * Show only one of the manuf (manager) combo boxes.
1874     *
1875     * @param type one of the three NamedBean types as String
1876     */
1877    protected void displayManagerComboBoxes(String type) {
1878        _type = type;
1879        if (type.equals(LIGHT)) {
1880            Manager<Light> manager = lightManComboBox.getSelectedItem();
1881            if (manager != null) {
1882                memo = manager.getMemo();
1883            }
1884            turnoutManComboBox.setVisible(false);
1885            sensorManComboBox.setVisible(false);
1886            lightManComboBox.setVisible(true);
1887            log.debug("BOX for LightManager set. LightManComboVisible={}", lightManComboBox.isVisible());
1888        } else if (type.equals(SENSOR)) {
1889            Manager<Sensor> manager = sensorManComboBox.getSelectedItem();
1890            if (manager != null) {
1891                memo = manager.getMemo();
1892            }
1893            turnoutManComboBox.setVisible(false);
1894            sensorManComboBox.setVisible(true);
1895            lightManComboBox.setVisible(false);
1896            log.debug("BOX for SensorManager set. SensorManComboVisible={}", sensorManComboBox.isVisible());
1897        } else { // TURNOUT
1898            Manager<Turnout> manager = turnoutManComboBox.getSelectedItem();
1899            if (manager != null) {
1900                memo = manager.getMemo();
1901            }
1902            turnoutManComboBox.setVisible(true);
1903            sensorManComboBox.setVisible(false);
1904            lightManComboBox.setVisible(false);
1905            log.debug("BOX for TurnoutManager set. TurnoutManComboVisible={}", turnoutManComboBox.isVisible());
1906        }
1907    }
1908
1909    public void setIconScale(int size) {
1910        _iconSquare = size;
1911        // also set the scale radio menu items, all 3 are in sizeGroup so will auto deselect
1912        if (size < 100) {
1913            sizeSmall.setSelected(true);
1914        } else if (size > 100) {
1915            sizeLarge.setSelected(true);
1916        } else {
1917            sizeDefault.setSelected(true);
1918        }
1919        updatePressed();
1920    }
1921
1922    public int getIconScale() {
1923        return _iconSquare;
1924    }
1925
1926    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SwitchboardEditor.class);
1927
1928}