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