001package jmri.jmrit.display.layoutEditor;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.*;
006import java.awt.event.*;
007import java.awt.geom.Point2D;
008import java.awt.geom.Rectangle2D;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyVetoException;
011import java.io.File;
012import java.lang.reflect.Field;
013import java.text.MessageFormat;
014import java.text.ParseException;
015import java.util.List;
016import java.util.*;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.stream.Collectors;
019import java.util.stream.Stream;
020
021import javax.annotation.CheckForNull;
022import javax.annotation.Nonnull;
023import javax.swing.*;
024import javax.swing.event.PopupMenuEvent;
025import javax.swing.event.PopupMenuListener;
026import javax.swing.filechooser.FileNameExtensionFilter;
027
028import jmri.*;
029import jmri.configurexml.StoreXmlUserAction;
030import jmri.jmrit.catalog.NamedIcon;
031import jmri.jmrit.dispatcher.DispatcherAction;
032import jmri.jmrit.dispatcher.DispatcherFrame;
033import jmri.jmrit.display.*;
034import jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.*;
035import jmri.jmrit.display.layoutEditor.LayoutEditorToolBarPanel.LocationFormat;
036import jmri.jmrit.display.panelEditor.PanelEditor;
037import jmri.jmrit.entryexit.AddEntryExitPairAction;
038import jmri.jmrit.logixng.GlobalVariable;
039import jmri.swing.NamedBeanComboBox;
040import jmri.util.*;
041import jmri.util.swing.JComboBoxUtil;
042import jmri.util.swing.JmriColorChooser;
043import jmri.util.swing.JmriJOptionPane;
044import jmri.util.swing.JmriMouseEvent;
045
046/**
047 * Provides a scrollable Layout Panel and editor toolbars (that can be hidden)
048 * <p>
049 * This module serves as a manager for the LayoutTurnout, Layout Block,
050 * PositionablePoint, Track Segment, LayoutSlip and LevelXing objects which are
051 * integral subparts of the LayoutEditor class.
052 * <p>
053 * All created objects are put on specific levels depending on their type
054 * (higher levels are in front): Note that higher numbers appear behind lower
055 * numbers.
056 * <p>
057 * The "contents" List keeps track of all text and icon label objects added to
058 * the target frame for later manipulation. Other Lists keep track of drawn
059 * items.
060 * <p>
061 * Based in part on PanelEditor.java (Bob Jacobsen (c) 2002, 2003). In
062 * particular, text and icon label items are copied from Panel editor, as well
063 * as some of the control design.
064 *
065 * @author Dave Duchamp Copyright: (c) 2004-2007
066 * @author George Warner Copyright: (c) 2017-2019
067 */
068public final class LayoutEditor extends PanelEditor implements MouseWheelListener, LayoutModels {
069
070    // Operational instance variables - not saved to disk
071    private JmriJFrame floatingEditToolBoxFrame = null;
072    private JScrollPane floatingEditContentScrollPane = null;
073    private JPanel floatEditHelpPanel = null;
074
075    private JPanel editToolBarContainerPanel = null;
076    private JScrollPane editToolBarScrollPane = null;
077
078    private JPanel helpBarPanel = null;
079    private final JPanel helpBar = new JPanel();
080
081    private final boolean editorUseOldLocSize;
082
083    private LayoutEditorToolBarPanel leToolBarPanel = null;
084
085    @Nonnull
086    public LayoutEditorToolBarPanel getLayoutEditorToolBarPanel() {
087        return leToolBarPanel;
088    }
089
090    // end of main panel controls
091    private boolean delayedPopupTrigger = false;
092    private Point2D currentPoint = new Point2D.Double(100.0, 100.0);
093    private Point2D dLoc = new Point2D.Double(0.0, 0.0);
094
095    private int toolbarHeight = 100;
096    private int toolbarWidth = 100;
097
098    private TrackSegment newTrack = null;
099    private boolean panelChanged = false;
100
101    // size of point boxes
102    public static final double SIZE = 3.0;
103    public static final double SIZE2 = SIZE * 2.; // must be twice SIZE
104
105    public Color turnoutCircleColor = Color.black; // matches earlier versions
106    public Color turnoutCircleThrownColor = Color.black;
107    private boolean turnoutFillControlCircles = false;
108    private int turnoutCircleSize = 4; // matches earlier versions
109
110    // use turnoutCircleSize when you need an int and these when you need a double
111    // note: these only change when setTurnoutCircleSize is called
112    // using these avoids having to call getTurnoutCircleSize() and
113    // the multiply (x2) and the int -> double conversion overhead
114    public double circleRadius = SIZE * getTurnoutCircleSize();
115    public double circleDiameter = 2.0 * circleRadius;
116
117    // selection variables
118    public boolean selectionActive = false;
119    private double selectionX = 0.0;
120    private double selectionY = 0.0;
121    public double selectionWidth = 0.0;
122    public double selectionHeight = 0.0;
123
124    // Option menu items
125    private JCheckBoxMenuItem editModeCheckBoxMenuItem = null;
126
127    private JRadioButtonMenuItem toolBarSideTopButton = null;
128    private JRadioButtonMenuItem toolBarSideLeftButton = null;
129    private JRadioButtonMenuItem toolBarSideBottomButton = null;
130    private JRadioButtonMenuItem toolBarSideRightButton = null;
131    private JRadioButtonMenuItem toolBarSideFloatButton = null;
132
133    private final JCheckBoxMenuItem wideToolBarCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ToolBarWide"));
134
135    private JCheckBoxMenuItem positionableCheckBoxMenuItem = null;
136    private JCheckBoxMenuItem controlCheckBoxMenuItem = null;
137    private JCheckBoxMenuItem animationCheckBoxMenuItem = null;
138    private JCheckBoxMenuItem showHelpCheckBoxMenuItem = null;
139    private JCheckBoxMenuItem showGridCheckBoxMenuItem = null;
140    private JCheckBoxMenuItem autoAssignBlocksCheckBoxMenuItem = null;
141    private JMenu scrollMenu = null;
142    private JRadioButtonMenuItem scrollBothMenuItem = null;
143    private JRadioButtonMenuItem scrollNoneMenuItem = null;
144    private JRadioButtonMenuItem scrollHorizontalMenuItem = null;
145    private JRadioButtonMenuItem scrollVerticalMenuItem = null;
146    private JMenu tooltipMenu = null;
147    private JRadioButtonMenuItem tooltipAlwaysMenuItem = null;
148    private JRadioButtonMenuItem tooltipNoneMenuItem = null;
149    private JRadioButtonMenuItem tooltipInEditMenuItem = null;
150    private JRadioButtonMenuItem tooltipNotInEditMenuItem = null;
151
152    private JCheckBoxMenuItem pixelsCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("Pixels"));
153    private JCheckBoxMenuItem metricCMCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("MetricCM"));
154    private JCheckBoxMenuItem englishFeetInchesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("EnglishFeetInches"));
155
156    private JCheckBoxMenuItem snapToGridOnAddCheckBoxMenuItem = null;
157    private JCheckBoxMenuItem snapToGridOnMoveCheckBoxMenuItem = null;
158    private JCheckBoxMenuItem antialiasingOnCheckBoxMenuItem = null;
159    private JCheckBoxMenuItem drawLayoutTracksLabelCheckBoxMenuItem = null;
160    private JCheckBoxMenuItem turnoutCirclesOnCheckBoxMenuItem = null;
161    private JCheckBoxMenuItem turnoutDrawUnselectedLegCheckBoxMenuItem = null;
162    private JCheckBoxMenuItem turnoutFillControlCirclesCheckBoxMenuItem = null;
163    private JCheckBoxMenuItem hideTrackSegmentConstructionLinesCheckBoxMenuItem = null;
164    private JCheckBoxMenuItem useDirectTurnoutControlCheckBoxMenuItem = null;
165    private JCheckBoxMenuItem highlightCursorCheckBoxMenuItem = null;
166    private ButtonGroup turnoutCircleSizeButtonGroup = null;
167
168    private boolean turnoutDrawUnselectedLeg = true;
169    private boolean autoAssignBlocks = false;
170
171    // Tools menu items
172    private final JMenu zoomMenu = new JMenu(Bundle.getMessage("MenuZoom"));
173    private final JRadioButtonMenuItem zoom025Item = new JRadioButtonMenuItem("x 0.25");
174    private final JRadioButtonMenuItem zoom05Item = new JRadioButtonMenuItem("x 0.5");
175    private final JRadioButtonMenuItem zoom075Item = new JRadioButtonMenuItem("x 0.75");
176    private final JRadioButtonMenuItem noZoomItem = new JRadioButtonMenuItem(Bundle.getMessage("NoZoom"));
177    private final JRadioButtonMenuItem zoom15Item = new JRadioButtonMenuItem("x 1.5");
178    private final JRadioButtonMenuItem zoom20Item = new JRadioButtonMenuItem("x 2.0");
179    private final JRadioButtonMenuItem zoom30Item = new JRadioButtonMenuItem("x 3.0");
180    private final JRadioButtonMenuItem zoom40Item = new JRadioButtonMenuItem("x 4.0");
181    private final JRadioButtonMenuItem zoom50Item = new JRadioButtonMenuItem("x 5.0");
182    private final JRadioButtonMenuItem zoom60Item = new JRadioButtonMenuItem("x 6.0");
183    private final JRadioButtonMenuItem zoom70Item = new JRadioButtonMenuItem("x 7.0");
184    private final JRadioButtonMenuItem zoom80Item = new JRadioButtonMenuItem("x 8.0");
185
186    private final JMenuItem undoTranslateSelectionMenuItem = new JMenuItem(Bundle.getMessage("UndoTranslateSelection"));
187    private final JMenuItem assignBlockToSelectionMenuItem = new JMenuItem(Bundle.getMessage("AssignBlockToSelectionTitle") + "...");
188
189    // Selected point information
190    private Point2D startDelta = new Point2D.Double(0.0, 0.0); // starting delta coordinates
191    public Object selectedObject = null;       // selected object, null if nothing selected
192    public Object prevSelectedObject = null;   // previous selected object, for undo
193    private HitPointType selectedHitPointType = HitPointType.NONE;         // hit point type within the selected object
194
195    public LayoutTrack foundTrack = null;      // found object, null if nothing found
196    public LayoutTrackView foundTrackView = null;                 // found view object, null if nothing found
197    private Point2D foundLocation = new Point2D.Double(0.0, 0.0); // location of found object
198    public HitPointType foundHitPointType = HitPointType.NONE;          // connection type within the found object
199
200    public LayoutTrack beginTrack = null;      // begin track segment connection object, null if none
201    public Point2D beginLocation = new Point2D.Double(0.0, 0.0); // location of begin object
202    private HitPointType beginHitPointType = HitPointType.NONE; // connection type within begin connection object
203
204    public Point2D currentLocation = new Point2D.Double(0.0, 0.0); // current location
205
206    // Lists of items that describe the Layout, and allow it to be drawn
207    // Each of the items must be saved to disk over sessions
208    private List<AnalogClock2Display> clocks = new ArrayList<>();           // fast clocks
209    private List<LocoIcon> markerImage = new ArrayList<>();                 // marker images
210    private List<MultiSensorIcon> multiSensors = new ArrayList<>();         // multi-sensor images
211    private List<PositionableLabel> backgroundImage = new ArrayList<>();    // background images
212    private List<PositionableLabel> labelImage = new ArrayList<>();         // positionable label images
213    private List<SensorIcon> sensorImage = new ArrayList<>();               // sensor images
214    private List<TurnoutIcon> turnoutImage = new ArrayList<>();             // turnout _images_
215    private List<SignalHeadIcon> signalHeadImage = new ArrayList<>();       // signal head images
216
217    // PositionableLabel's
218    private List<BlockContentsIcon> blockContentsLabelList = new ArrayList<>(); // BlockContents Label List
219    private List<BlockContentsInputIcon> blockContentsInputList = new ArrayList<>(); // BlockContents Input List
220    private List<MemoryIcon> memoryLabelList = new ArrayList<>();               // Memory Label List
221    private List<MemoryInputIcon> memoryInputList = new ArrayList<>();          // Memory Input List
222    private List<GlobalVariableIcon> globalVariableLabelList = new ArrayList<>(); // LogixNG Global Variable Label List
223    private List<SensorIcon> sensorList = new ArrayList<>();                    // Sensor Icons
224    private List<TurnoutIcon> turnoutList = new ArrayList<>();                  // Turnout _Icons_
225    private List<SignalHeadIcon> signalList = new ArrayList<>();                // Signal Head Icons
226    private List<SignalMastIcon> signalMastList = new ArrayList<>();            // Signal Mast Icons
227
228    public final LayoutEditorViewContext gContext = new LayoutEditorViewContext(); // public for now, as things work access changes
229
230    @Nonnull
231    public List<SensorIcon> getSensorList() {
232        return sensorList;
233    }
234
235    @Nonnull
236    public List<TurnoutIcon> getTurnoutList() {
237        return turnoutList;
238    }
239
240    @Nonnull
241    public List<PositionableLabel> getLabelImageList()  {
242        return labelImage;
243    }
244
245    @Nonnull
246    public List<BlockContentsIcon> getBlockContentsLabelList() {
247        return blockContentsLabelList;
248    }
249
250    @Nonnull
251    public List<MemoryIcon> getMemoryLabelList() {
252        return memoryLabelList;
253    }
254
255    @Nonnull
256    public List<MemoryInputIcon> getMemoryInputList() {
257        return memoryInputList;
258    }
259
260    @Nonnull
261    public List<BlockContentsInputIcon> getBlockContensInputList() {
262        return blockContentsInputList;
263    }
264
265    @Nonnull
266    public List<GlobalVariableIcon> getGlobalVariableLabelList() {
267        return globalVariableLabelList;
268    }
269
270    @Nonnull
271    public List<SignalHeadIcon> getSignalList() {
272        return signalList;
273    }
274
275    @Nonnull
276    public List<SignalMastIcon> getSignalMastList() {
277        return signalMastList;
278    }
279
280    private final List<LayoutShape> layoutShapes = new ArrayList<>();               // LayoutShap list
281
282    // counts used to determine unique internal names
283    private int numAnchors = 0;
284    private int numEndBumpers = 0;
285    private int numEdgeConnectors = 0;
286    private int numTrackSegments = 0;
287    private int numLevelXings = 0;
288    private int numLayoutSlips = 0;
289    private int numLayoutTurnouts = 0;
290    private int numLayoutTurntables = 0;
291    private int numLayoutTraversers = 0;
292
293    private LayoutEditorFindItems finder = new LayoutEditorFindItems(this);
294
295    @Nonnull
296    public LayoutEditorFindItems getFinder() {
297        return finder;
298    }
299
300    private Color mainlineTrackColor = Color.DARK_GRAY;
301    private Color sidelineTrackColor = Color.DARK_GRAY;
302    public Color defaultTrackColor = Color.DARK_GRAY;
303    private Color defaultOccupiedTrackColor = Color.red;
304    private Color defaultAlternativeTrackColor = Color.white;
305    private Color defaultTextColor = Color.black;
306
307    private String layoutName = "";
308    private boolean animatingLayout = true;
309    private boolean showHelpBar = true;
310    private boolean drawGrid = true;
311
312    private boolean snapToGridOnAdd = false;
313    private boolean snapToGridOnMove = false;
314    private boolean snapToGridInvert = false;
315
316    private boolean antialiasingOn = false;
317    private boolean drawLayoutTracksLabel = false;
318    private boolean highlightSelectedBlockFlag = false;
319
320    private boolean turnoutCirclesWithoutEditMode = false;
321    private boolean tooltipsWithoutEditMode = false;
322    private boolean tooltipsInEditMode = true;
323    private boolean tooltipsAlwaysOrNever = false;     // When true, don't call setAllShowToolTip().
324
325    // turnout size parameters - saved with panel
326    private double turnoutBX = LayoutTurnout.turnoutBXDefault; // RH, LH, WYE
327    private double turnoutCX = LayoutTurnout.turnoutCXDefault;
328    private double turnoutWid = LayoutTurnout.turnoutWidDefault;
329    private double xOverLong = LayoutTurnout.xOverLongDefault; // DOUBLE_XOVER, RH_XOVER, LH_XOVER
330    private double xOverHWid = LayoutTurnout.xOverHWidDefault;
331    private double xOverShort = LayoutTurnout.xOverShortDefault;
332    private boolean useDirectTurnoutControl = false; // Uses Left click for closing points, Right click for throwing.
333    private boolean highlightCursor = false; // Highlight finger/mouse press/drag area, good for touchscreens
334
335    // saved state of options when panel was loaded or created
336    private boolean savedEditMode = true;
337    private boolean savedPositionable = true;
338    private boolean savedControlLayout = true;
339    private boolean savedAnimatingLayout = true;
340    private boolean savedShowHelpBar = true;
341
342    // zoom
343    private double minZoom = 0.25;
344    private final double maxZoom = 8.0;
345
346    // A hash to store string -> KeyEvent constants, used to set keyboard shortcuts per locale
347    private HashMap<String, Integer> stringsToVTCodes = new HashMap<>();
348
349    /*==============*\
350    |* Toolbar side *|
351    \*==============*/
352    private enum ToolBarSide {
353        eTOP("top"),
354        eLEFT("left"),
355        eBOTTOM("bottom"),
356        eRIGHT("right"),
357        eFLOAT("float");
358
359        private final String name;
360        private static final Map<String, ToolBarSide> ENUM_MAP;
361
362        ToolBarSide(String name) {
363            this.name = name;
364        }
365
366        // Build an immutable map of String name to enum pairs.
367        static {
368            Map<String, ToolBarSide> map = new ConcurrentHashMap<>();
369
370            for (ToolBarSide instance : ToolBarSide.values()) {
371                map.put(instance.getName(), instance);
372            }
373            ENUM_MAP = Collections.unmodifiableMap(map);
374        }
375
376        public static ToolBarSide getName(@CheckForNull String name) {
377            return ENUM_MAP.get(name);
378        }
379
380        public String getName() {
381            return name;
382        }
383    }
384
385    private ToolBarSide toolBarSide = ToolBarSide.eTOP;
386
387    public LayoutEditor() {
388        this("My Layout");
389    }
390
391    public LayoutEditor(@Nonnull String name) {
392        super(name);
393        setSaveSize(true);
394        layoutName = name;
395
396        editorUseOldLocSize = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isEditorUseOldLocSize();
397
398        // initialise keycode map
399        initStringsToVTCodes();
400
401        setupToolBar();
402        setupMenuBar();
403
404        super.setDefaultToolTip(new ToolTip(null, 0, 0, new Font("SansSerif", Font.PLAIN, 12),
405                Color.black, new Color(215, 225, 255), Color.black, null));
406
407        // setup help bar
408        helpBar.setLayout(new BoxLayout(helpBar, BoxLayout.PAGE_AXIS));
409        JTextArea helpTextArea1 = new JTextArea(Bundle.getMessage("Help1"));
410        helpBar.add(helpTextArea1);
411        JTextArea helpTextArea2 = new JTextArea(Bundle.getMessage("Help2"));
412        helpBar.add(helpTextArea2);
413
414        String helpText3 = "";
415
416        switch (SystemType.getType()) {
417            case SystemType.MACOSX: {
418                helpText3 = Bundle.getMessage("Help3Mac");
419                break;
420            }
421
422            case SystemType.WINDOWS:
423            case SystemType.LINUX: {
424                helpText3 = Bundle.getMessage("Help3Win");
425                break;
426            }
427
428            default:
429                helpText3 = Bundle.getMessage("Help3");
430        }
431
432        JTextArea helpTextArea3 = new JTextArea(helpText3);
433        helpBar.add(helpTextArea3);
434
435        // set to full screen
436        Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
437        gContext.setWindowWidth(screenDim.width - 20);
438        gContext.setWindowHeight(screenDim.height - 120);
439
440        // Let Editor make target, and use this frame
441        super.setTargetPanel(null, null);
442        super.setTargetPanelSize(gContext.getWindowWidth(), gContext.getWindowHeight());
443        setSize(screenDim.width, screenDim.height);
444
445        // register the resulting panel for later configuration
446        InstanceManager.getOptionalDefault(ConfigureManager.class)
447                .ifPresent(cm -> cm.registerUser(this));
448
449        // confirm that panel hasn't already been loaded
450        if (!this.equals(InstanceManager.getDefault(EditorManager.class).get(name))) {
451            log.warn("File contains a panel with the same name ({}) as an existing panel", name);
452        }
453        setFocusable(true);
454        addKeyListener(this);
455        resetDirty();
456
457        // establish link to LayoutEditor Tools
458        auxTools = getLEAuxTools();
459
460        SwingUtilities.invokeLater(() -> {
461            // initialize preferences
462            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
463                String windowFrameRef = getWindowFrameRef();
464
465                Object prefsProp = prefsMgr.getProperty(windowFrameRef, "toolBarSide");
466                // log.debug("{}.toolBarSide is {}", windowFrameRef, prefsProp);
467                if (prefsProp != null) {
468                    ToolBarSide newToolBarSide = ToolBarSide.getName((String) prefsProp);
469                    setToolBarSide(newToolBarSide);
470                }
471
472                // Note: since prefs default to false and we want wide to be the default
473                // we invert it and save it as thin
474                boolean prefsToolBarIsWide = prefsMgr.getSimplePreferenceState(windowFrameRef + ".toolBarThin");
475
476                log.debug("{}.toolBarThin is {}", windowFrameRef, prefsProp);
477                setToolBarWide(prefsToolBarIsWide);
478
479                boolean prefsShowHelpBar = prefsMgr.getSimplePreferenceState(windowFrameRef + ".showHelpBar");
480                // log.debug("{}.showHelpBar is {}", windowFrameRef, prefsShowHelpBar);
481
482                setShowHelpBar(prefsShowHelpBar);
483
484                boolean prefsAntialiasingOn = prefsMgr.getSimplePreferenceState(windowFrameRef + ".antialiasingOn");
485                // log.debug("{}.antialiasingOn is {}", windowFrameRef, prefsAntialiasingOn);
486
487                setAntialiasingOn(prefsAntialiasingOn);
488
489                boolean prefsDrawLayoutTracksLabel = prefsMgr.getSimplePreferenceState(windowFrameRef + ".drawLayoutTracksLabel");
490                // log.debug("{}.drawLayoutTracksLabel is {}", windowFrameRef, prefsDrawLayoutTracksLabel);
491                setDrawLayoutTracksLabel(prefsDrawLayoutTracksLabel);
492
493                boolean prefsHighlightSelectedBlockFlag
494                        = prefsMgr.getSimplePreferenceState(windowFrameRef + ".highlightSelectedBlock");
495                // log.debug("{}.highlightSelectedBlock is {}", windowFrameRef, prefsHighlightSelectedBlockFlag);
496
497                setHighlightSelectedBlock(prefsHighlightSelectedBlockFlag);
498            }); // InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr)
499
500            // make sure that the layoutEditorComponent is in the _targetPanel components
501            List<Component> componentList = Arrays.asList(_targetPanel.getComponents());
502            if (!componentList.contains(layoutEditorComponent)) {
503                try {
504                    _targetPanel.remove(layoutEditorComponent);
505                    // Note that Integer.valueOf(3) must not be replaced with 3 in the line below.
506                    // add(c, Integer.valueOf(3)) means adding at depth 3 in the JLayeredPane, while add(c, 3) means adding at index 3 in the container.
507                    _targetPanel.add(layoutEditorComponent, Integer.valueOf(3));
508                    _targetPanel.moveToFront(layoutEditorComponent);
509                } catch (Exception e) {
510                    log.warn("paintTargetPanelBefore: ", e);
511                }
512            }
513        });
514    }
515
516    @SuppressWarnings("deprecation")  // getMenuShortcutKeyMask()
517    private void setupMenuBar() {
518        // initialize menu bar
519        JMenuBar menuBar = new JMenuBar();
520
521        // set up File menu
522        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
523        fileMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuFileMnemonic")));
524        menuBar.add(fileMenu);
525        StoreXmlUserAction store = new StoreXmlUserAction(Bundle.getMessage("FileMenuItemStore"));
526        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
527        store.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(
528                stringsToVTCodes.get(Bundle.getMessage("MenuItemStoreAccelerator")), primary_modifier));
529        fileMenu.add(store);
530        fileMenu.addSeparator();
531
532        JMenuItem deleteItem = new JMenuItem(Bundle.getMessage("DeletePanel"));
533        fileMenu.add(deleteItem);
534        deleteItem.addActionListener((ActionEvent event) -> {
535            if (deletePanel()) {
536                dispose();
537            }
538        });
539        setJMenuBar(menuBar);
540
541        // setup Options menu
542        setupOptionMenu(menuBar);
543
544        // setup Tools menu
545        setupToolsMenu(menuBar);
546
547        // setup Zoom menu
548        setupZoomMenu(menuBar);
549
550        // setup marker menu
551        setupMarkerMenu(menuBar);
552
553        // Setup Dispatcher window
554        setupDispatcherMenu(menuBar);
555
556        // setup Help menu
557        addHelpMenu("package.jmri.jmrit.display.LayoutEditor", true);
558    }
559
560    @Override
561    public void newPanelDefaults() {
562        getLayoutTrackDrawingOptions().setMainRailWidth(2);
563        getLayoutTrackDrawingOptions().setSideRailWidth(1);
564        setBackgroundColor(defaultBackgroundColor);
565        JmriColorChooser.addRecentColor(defaultTrackColor);
566        JmriColorChooser.addRecentColor(defaultOccupiedTrackColor);
567        JmriColorChooser.addRecentColor(defaultAlternativeTrackColor);
568        JmriColorChooser.addRecentColor(defaultBackgroundColor);
569        JmriColorChooser.addRecentColor(defaultTextColor);
570    }
571
572    private final LayoutEditorComponent layoutEditorComponent = new LayoutEditorComponent(this);
573
574    private void setupToolBar() {
575        // Initial setup for both horizontal and vertical
576        Container contentPane = getContentPane();
577
578        // remove these (if present) so we can add them back (without duplicates)
579        if (editToolBarContainerPanel != null) {
580            editToolBarContainerPanel.setVisible(false);
581            contentPane.remove(editToolBarContainerPanel);
582        }
583
584        if (helpBarPanel != null) {
585            contentPane.remove(helpBarPanel);
586        }
587
588        deletefloatingEditToolBoxFrame();
589        if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
590            createfloatingEditToolBoxFrame();
591            createFloatingHelpPanel();
592            return;
593        }
594
595        Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
596        boolean toolBarIsVertical = (toolBarSide.equals(ToolBarSide.eRIGHT) || toolBarSide.equals(ToolBarSide.eLEFT));
597        if ( leToolBarPanel != null ) {
598            leToolBarPanel.dispose();
599        }
600        if (toolBarIsVertical) {
601            leToolBarPanel = new LayoutEditorVerticalToolBarPanel(this);
602            editToolBarScrollPane = new JScrollPane(leToolBarPanel);
603            toolbarWidth = editToolBarScrollPane.getPreferredSize().width;
604            toolbarHeight = screenDim.height;
605        } else {
606            leToolBarPanel = new LayoutEditorHorizontalToolBarPanel(this);
607            editToolBarScrollPane = new JScrollPane(leToolBarPanel);
608            toolbarWidth = screenDim.width;
609            toolbarHeight = editToolBarScrollPane.getPreferredSize().height;
610        }
611
612        editToolBarContainerPanel = new JPanel();
613        editToolBarContainerPanel.setLayout(new BoxLayout(editToolBarContainerPanel, BoxLayout.PAGE_AXIS));
614        editToolBarContainerPanel.add(editToolBarScrollPane);
615
616        // setup notification for when horizontal scrollbar changes visibility
617        // editToolBarScroll.getViewport().addChangeListener(e -> {
618        // log.warn("scrollbars visible: " + editToolBarScroll.getHorizontalScrollBar().isVisible());
619        //});
620        editToolBarContainerPanel.setMinimumSize(new Dimension(toolbarWidth, toolbarHeight));
621        editToolBarContainerPanel.setPreferredSize(new Dimension(toolbarWidth, toolbarHeight));
622
623        helpBarPanel = new JPanel();
624        helpBarPanel.add(helpBar);
625
626        for (Component c : helpBar.getComponents()) {
627            if (c instanceof JTextArea) {
628                JTextArea j = (JTextArea) c;
629                j.setSize(new Dimension(toolbarWidth, j.getSize().height));
630                j.setLineWrap(toolBarIsVertical);
631                j.setWrapStyleWord(toolBarIsVertical);
632            }
633        }
634        contentPane.setLayout(new BoxLayout(contentPane, toolBarIsVertical ? BoxLayout.LINE_AXIS : BoxLayout.PAGE_AXIS));
635
636        switch (toolBarSide) {
637            case eTOP:
638            case eLEFT:
639                contentPane.add(editToolBarContainerPanel, 0);
640                break;
641
642            case eBOTTOM:
643            case eRIGHT:
644                contentPane.add(editToolBarContainerPanel);
645                break;
646
647            default:
648                // fall through
649                break;
650        }
651
652        if (toolBarIsVertical) {
653            editToolBarContainerPanel.add(helpBarPanel);
654        } else {
655            contentPane.add(helpBarPanel);
656        }
657        helpBarPanel.setVisible(isEditable() && getShowHelpBar());
658        editToolBarContainerPanel.setVisible(isEditable());
659    }
660
661    private void createfloatingEditToolBoxFrame() {
662        if (isEditable() && floatingEditToolBoxFrame == null) {
663            // Create a scroll pane to hold the window content.
664            leToolBarPanel = new LayoutEditorFloatingToolBarPanel(this);
665            floatingEditContentScrollPane = new JScrollPane(leToolBarPanel);
666            floatingEditContentScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
667            floatingEditContentScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
668            // Create the window and add the toolbox content
669            floatingEditToolBoxFrame = new JmriJFrame(Bundle.getMessage("ToolBox", getLayoutName()));
670            floatingEditToolBoxFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
671            floatingEditToolBoxFrame.setContentPane(floatingEditContentScrollPane);
672            floatingEditToolBoxFrame.pack();
673            floatingEditToolBoxFrame.setAlwaysOnTop(true);
674            floatingEditToolBoxFrame.setVisible(true);
675        }
676    }
677
678    private void deletefloatingEditToolBoxFrame() {
679        if (floatingEditContentScrollPane != null) {
680            floatingEditContentScrollPane.removeAll();
681            floatingEditContentScrollPane = null;
682        }
683        if (floatingEditToolBoxFrame != null) {
684            floatingEditToolBoxFrame.dispose();
685            floatingEditToolBoxFrame = null;
686        }
687    }
688
689    private void createFloatingHelpPanel() {
690
691        if (leToolBarPanel instanceof LayoutEditorFloatingToolBarPanel) {
692            LayoutEditorFloatingToolBarPanel leftbp = (LayoutEditorFloatingToolBarPanel) leToolBarPanel;
693            floatEditHelpPanel = new JPanel();
694            leToolBarPanel.add(floatEditHelpPanel);
695
696            // Notice: End tree structure indenting
697            // Force the help panel width to the same as the tabs section
698            int tabSectionWidth = (int) leftbp.getPreferredSize().getWidth();
699
700            // Change the textarea settings
701            for (Component c : helpBar.getComponents()) {
702                if (c instanceof JTextArea) {
703                    JTextArea j = (JTextArea) c;
704                    j.setSize(new Dimension(tabSectionWidth, j.getSize().height));
705                    j.setLineWrap(true);
706                    j.setWrapStyleWord(true);
707                }
708            }
709
710            // Change the width of the help panel section
711            floatEditHelpPanel.setMaximumSize(new Dimension(tabSectionWidth, Integer.MAX_VALUE));
712            floatEditHelpPanel.add(helpBar);
713            floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
714        }
715    }
716
717    @Override
718    public void init(String name) {
719    }
720
721    @Override
722    public void initView() {
723        editModeCheckBoxMenuItem.setSelected(isEditable());
724
725        positionableCheckBoxMenuItem.setSelected(allPositionable());
726        controlCheckBoxMenuItem.setSelected(allControlling());
727
728        if (isEditable()) {
729            if (!tooltipsAlwaysOrNever) {
730                setAllShowToolTip(tooltipsInEditMode);
731                setAllShowLayoutTurnoutToolTip(tooltipsInEditMode);
732            }
733        } else {
734            if (!tooltipsAlwaysOrNever) {
735                setAllShowToolTip(tooltipsWithoutEditMode);
736                setAllShowLayoutTurnoutToolTip(tooltipsWithoutEditMode);
737            }
738        }
739
740        scrollNoneMenuItem.setSelected(_scrollState == Editor.SCROLL_NONE);
741        scrollBothMenuItem.setSelected(_scrollState == Editor.SCROLL_BOTH);
742        scrollHorizontalMenuItem.setSelected(_scrollState == Editor.SCROLL_HORIZONTAL);
743        scrollVerticalMenuItem.setSelected(_scrollState == Editor.SCROLL_VERTICAL);
744    }
745
746    @Override
747    public void setSize(int w, int h) {
748        super.setSize(w, h);
749    }
750
751    @Override
752    public void targetWindowClosingEvent(WindowEvent e) {
753        boolean save = (isDirty() || (savedEditMode != isEditable())
754                || (savedPositionable != allPositionable())
755                || (savedControlLayout != allControlling())
756                || (savedAnimatingLayout != isAnimating())
757                || (savedShowHelpBar != getShowHelpBar()));
758
759        log.trace("Temp fix to disable CI errors: save = {}", save);
760        targetWindowClosing();
761    }
762
763    /**
764     * Set up NamedBeanComboBox
765     *
766     * @param nbComboBox     the NamedBeanComboBox to set up
767     * @param inValidateMode true to validate typed inputs; false otherwise
768     * @param inEnable       boolean to enable / disable the NamedBeanComboBox
769     * @param inEditable     boolean to make the NamedBeanComboBox editable
770     */
771    public static void setupComboBox(@Nonnull NamedBeanComboBox<?> nbComboBox,
772        boolean inValidateMode, boolean inEnable, boolean inEditable) {
773        log.debug("LE setupComboBox called");
774        NamedBeanComboBox<?> inComboBox = Objects.requireNonNull(nbComboBox);
775
776        inComboBox.setEnabled(inEnable);
777        inComboBox.setEditable(inEditable);
778        inComboBox.setValidatingInput(inValidateMode);
779        inComboBox.setSelectedIndex(-1);
780
781        // This has to be set before calling setupComboBoxMaxRows
782        // (otherwise if inFirstBlank then the  number of rows will be wrong)
783        inComboBox.setAllowNull(!inValidateMode);
784
785        // set the max number of rows that will fit onscreen
786        JComboBoxUtil.setupComboBoxMaxRows(inComboBox);
787
788        inComboBox.setSelectedIndex(-1);
789    }
790
791    /**
792     * Grabs a subset of the possible KeyEvent constants and puts them into a
793     * hash for fast lookups later. These lookups are used to enable bundles to
794     * specify keyboard shortcuts on a per-locale basis.
795     */
796    private void initStringsToVTCodes() {
797        Field[] fields = KeyEvent.class
798                .getFields();
799
800        for (Field field : fields) {
801            String name = field.getName();
802
803            if (name.startsWith("VK")) {
804                int code = 0;
805                try {
806                    code = field.getInt(null);
807                } catch (IllegalAccessException | IllegalArgumentException e) {
808                    // exceptions make me throw up...
809                }
810
811                String key = name.substring(3);
812
813                // log.debug("VTCode[{}]:'{}'", key, code);
814                stringsToVTCodes.put(key, code);
815            }
816        }
817    }
818
819    /**
820     * The Java run times for 11 and 12 running on macOS have a bug that causes double events for
821     * JCheckBoxMenuItem when invoked by an accelerator key combination.
822     * <p>
823     * The java.version property is parsed to determine the run time version.  If the event occurs
824     * on macOS and Java 11 or 12 and a modifier key was active, true is returned.  The five affected
825     * action events will drop the event and process the second occurrence.
826     * @aparam event The action event.
827     * @return true if the event is affected, otherwise return false.
828     */
829    private boolean fixMacBugOn11(ActionEvent event) {
830        boolean result = false;
831        if (SystemType.isMacOSX()) {
832            if (event.getModifiers() != 0) {
833                // MacOSX and modifier key, test Java version
834                String version = System.getProperty("java.version");
835                if (version.startsWith("1.")) {
836                    version = version.substring(2, 3);
837                } else {
838                    int dot = version.indexOf(".");
839                    if (dot != -1) {
840                        version = version.substring(0, dot);
841                    }
842                }
843                int vers = Integer.parseInt(version);
844                result = (vers == 11 || vers == 12);
845            }
846        }
847        return result;
848     }
849
850    /**
851     * Set up the Option menu.
852     *
853     * @param menuBar to add the option menu to
854     * @return option menu that was added
855     */
856    @SuppressWarnings("deprecation")  // getMenuShortcutKeyMask()
857    private JMenu setupOptionMenu(@Nonnull JMenuBar menuBar) {
858        assert menuBar != null;
859
860        JMenu optionMenu = new JMenu(Bundle.getMessage("MenuOptions"));
861
862        optionMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("OptionsMnemonic")));
863        menuBar.add(optionMenu);
864
865        //
866        //  edit mode
867        //
868        editModeCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("EditMode"));
869        optionMenu.add(editModeCheckBoxMenuItem);
870        editModeCheckBoxMenuItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("EditModeMnemonic")));
871        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
872        editModeCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(
873                stringsToVTCodes.get(Bundle.getMessage("EditModeAccelerator")), primary_modifier));
874        editModeCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
875
876            if (fixMacBugOn11(event)) {
877                editModeCheckBoxMenuItem.setSelected(!editModeCheckBoxMenuItem.isSelected());
878                return;
879            }
880
881            setAllEditable(editModeCheckBoxMenuItem.isSelected());
882
883            // show/hide the help bar
884            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
885                if (floatEditHelpPanel != null) {
886                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
887                }
888            } else {
889                helpBarPanel.setVisible(isEditable() && getShowHelpBar());
890            }
891
892            if (isEditable()) {
893                if (!tooltipsAlwaysOrNever) {
894                    setAllShowToolTip(tooltipsInEditMode);
895                    setAllShowLayoutTurnoutToolTip(tooltipsInEditMode);
896                }
897
898                // redo using the "Extra" color to highlight the selected block
899                if (highlightSelectedBlockFlag) {
900                    if (!highlightBlockInComboBox(leToolBarPanel.blockIDComboBox)) {
901                        highlightBlockInComboBox(leToolBarPanel.blockContentsComboBox);
902                    }
903                }
904            } else {
905                if (!tooltipsAlwaysOrNever) {
906                    setAllShowToolTip(tooltipsWithoutEditMode);
907                    setAllShowLayoutTurnoutToolTip(tooltipsWithoutEditMode);
908                }
909
910                // undo using the "Extra" color to highlight the selected block
911                if (highlightSelectedBlockFlag) {
912                    highlightBlock(null);
913                }
914            }
915            awaitingIconChange = false;
916        });
917        editModeCheckBoxMenuItem.setSelected(isEditable());
918
919        //
920        // toolbar
921        //
922        JMenu toolBarMenu = new JMenu(Bundle.getMessage("ToolBar")); // used for ToolBar SubMenu
923        optionMenu.add(toolBarMenu);
924
925        JMenu toolBarSideMenu = new JMenu(Bundle.getMessage("ToolBarSide"));
926        ButtonGroup toolBarSideGroup = new ButtonGroup();
927
928        //
929        // create toolbar side menu items: (top, left, bottom, right)
930        //
931        toolBarSideTopButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideTop"));
932        toolBarSideTopButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eTOP));
933        toolBarSideTopButton.setSelected(toolBarSide.equals(ToolBarSide.eTOP));
934        toolBarSideMenu.add(toolBarSideTopButton);
935        toolBarSideGroup.add(toolBarSideTopButton);
936
937        toolBarSideLeftButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideLeft"));
938        toolBarSideLeftButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eLEFT));
939        toolBarSideLeftButton.setSelected(toolBarSide.equals(ToolBarSide.eLEFT));
940        toolBarSideMenu.add(toolBarSideLeftButton);
941        toolBarSideGroup.add(toolBarSideLeftButton);
942
943        toolBarSideBottomButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideBottom"));
944        toolBarSideBottomButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eBOTTOM));
945        toolBarSideBottomButton.setSelected(toolBarSide.equals(ToolBarSide.eBOTTOM));
946        toolBarSideMenu.add(toolBarSideBottomButton);
947        toolBarSideGroup.add(toolBarSideBottomButton);
948
949        toolBarSideRightButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideRight"));
950        toolBarSideRightButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eRIGHT));
951        toolBarSideRightButton.setSelected(toolBarSide.equals(ToolBarSide.eRIGHT));
952        toolBarSideMenu.add(toolBarSideRightButton);
953        toolBarSideGroup.add(toolBarSideRightButton);
954
955        toolBarSideFloatButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideFloat"));
956        toolBarSideFloatButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eFLOAT));
957        toolBarSideFloatButton.setSelected(toolBarSide.equals(ToolBarSide.eFLOAT));
958        toolBarSideMenu.add(toolBarSideFloatButton);
959        toolBarSideGroup.add(toolBarSideFloatButton);
960
961        toolBarMenu.add(toolBarSideMenu);
962
963        //
964        // toolbar wide menu
965        //
966        toolBarMenu.add(wideToolBarCheckBoxMenuItem);
967        wideToolBarCheckBoxMenuItem.addActionListener((ActionEvent event) -> setToolBarWide(wideToolBarCheckBoxMenuItem.isSelected()));
968        wideToolBarCheckBoxMenuItem.setSelected(leToolBarPanel.toolBarIsWide);
969        wideToolBarCheckBoxMenuItem.setEnabled(toolBarSide.equals(ToolBarSide.eTOP) || toolBarSide.equals(ToolBarSide.eBOTTOM));
970
971        //
972        // Scroll Bars
973        //
974        scrollMenu = new JMenu(Bundle.getMessage("ComboBoxScrollable")); // used for ScrollBarsSubMenu
975        optionMenu.add(scrollMenu);
976        ButtonGroup scrollGroup = new ButtonGroup();
977        scrollBothMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollBoth"));
978        scrollGroup.add(scrollBothMenuItem);
979        scrollMenu.add(scrollBothMenuItem);
980        scrollBothMenuItem.setSelected(_scrollState == Editor.SCROLL_BOTH);
981        scrollBothMenuItem.addActionListener((ActionEvent event) -> {
982            _scrollState = Editor.SCROLL_BOTH;
983            setScroll(_scrollState);
984            redrawPanel();
985        });
986        scrollNoneMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollNone"));
987        scrollGroup.add(scrollNoneMenuItem);
988        scrollMenu.add(scrollNoneMenuItem);
989        scrollNoneMenuItem.setSelected(_scrollState == Editor.SCROLL_NONE);
990        scrollNoneMenuItem.addActionListener((ActionEvent event) -> {
991            _scrollState = Editor.SCROLL_NONE;
992            setScroll(_scrollState);
993            redrawPanel();
994        });
995        scrollHorizontalMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollHorizontal"));
996        scrollGroup.add(scrollHorizontalMenuItem);
997        scrollMenu.add(scrollHorizontalMenuItem);
998        scrollHorizontalMenuItem.setSelected(_scrollState == Editor.SCROLL_HORIZONTAL);
999        scrollHorizontalMenuItem.addActionListener((ActionEvent event) -> {
1000            _scrollState = Editor.SCROLL_HORIZONTAL;
1001            setScroll(_scrollState);
1002            redrawPanel();
1003        });
1004        scrollVerticalMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollVertical"));
1005        scrollGroup.add(scrollVerticalMenuItem);
1006        scrollMenu.add(scrollVerticalMenuItem);
1007        scrollVerticalMenuItem.setSelected(_scrollState == Editor.SCROLL_VERTICAL);
1008        scrollVerticalMenuItem.addActionListener((ActionEvent event) -> {
1009            _scrollState = Editor.SCROLL_VERTICAL;
1010            setScroll(_scrollState);
1011            redrawPanel();
1012        });
1013
1014        //
1015        // Tooltips
1016        //
1017        tooltipMenu = new JMenu(Bundle.getMessage("TooltipSubMenu"));
1018        optionMenu.add(tooltipMenu);
1019        ButtonGroup tooltipGroup = new ButtonGroup();
1020        tooltipNoneMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipNone"));
1021        tooltipGroup.add(tooltipNoneMenuItem);
1022        tooltipMenu.add(tooltipNoneMenuItem);
1023        tooltipNoneMenuItem.setSelected((!tooltipsInEditMode) && (!tooltipsWithoutEditMode));
1024        tooltipNoneMenuItem.addActionListener((ActionEvent event) -> {
1025            tooltipsInEditMode = false;
1026            tooltipsWithoutEditMode = false;
1027            tooltipsAlwaysOrNever = true;
1028            setAllShowToolTip(false);
1029            setAllShowLayoutTurnoutToolTip(false);
1030        });
1031        tooltipAlwaysMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipAlways"));
1032        tooltipGroup.add(tooltipAlwaysMenuItem);
1033        tooltipMenu.add(tooltipAlwaysMenuItem);
1034        tooltipAlwaysMenuItem.setSelected((tooltipsInEditMode) && (tooltipsWithoutEditMode));
1035        tooltipAlwaysMenuItem.addActionListener((ActionEvent event) -> {
1036            tooltipsInEditMode = true;
1037            tooltipsWithoutEditMode = true;
1038            tooltipsAlwaysOrNever = true;
1039            setAllShowToolTip(true);
1040            setAllShowLayoutTurnoutToolTip(true);
1041        });
1042        tooltipInEditMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipEdit"));
1043        tooltipGroup.add(tooltipInEditMenuItem);
1044        tooltipMenu.add(tooltipInEditMenuItem);
1045        tooltipInEditMenuItem.setSelected((tooltipsInEditMode) && (!tooltipsWithoutEditMode));
1046        tooltipInEditMenuItem.addActionListener((ActionEvent event) -> {
1047            tooltipsInEditMode = true;
1048            tooltipsWithoutEditMode = false;
1049            tooltipsAlwaysOrNever = false;
1050            setAllShowToolTip(isEditable());
1051            setAllShowLayoutTurnoutToolTip(isEditable());
1052        });
1053        tooltipNotInEditMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipNotEdit"));
1054        tooltipGroup.add(tooltipNotInEditMenuItem);
1055        tooltipMenu.add(tooltipNotInEditMenuItem);
1056        tooltipNotInEditMenuItem.setSelected((!tooltipsInEditMode) && (tooltipsWithoutEditMode));
1057        tooltipNotInEditMenuItem.addActionListener((ActionEvent event) -> {
1058            tooltipsInEditMode = false;
1059            tooltipsWithoutEditMode = true;
1060            tooltipsAlwaysOrNever = false;
1061            setAllShowToolTip(!isEditable());
1062            setAllShowLayoutTurnoutToolTip(!isEditable());
1063        });
1064
1065        //
1066        // show edit help
1067        //
1068        showHelpCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowEditHelp"));
1069        optionMenu.add(showHelpCheckBoxMenuItem);
1070        showHelpCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1071            boolean newShowHelpBar = showHelpCheckBoxMenuItem.isSelected();
1072            setShowHelpBar(newShowHelpBar);
1073        });
1074        showHelpCheckBoxMenuItem.setSelected(getShowHelpBar());
1075
1076        //
1077        // Allow Repositioning
1078        //
1079        positionableCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowRepositioning"));
1080        optionMenu.add(positionableCheckBoxMenuItem);
1081        positionableCheckBoxMenuItem.addActionListener((ActionEvent event) -> setAllPositionable(positionableCheckBoxMenuItem.isSelected()));
1082        positionableCheckBoxMenuItem.setSelected(allPositionable());
1083
1084        //
1085        // Allow Layout Control
1086        //
1087        controlCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowLayoutControl"));
1088        optionMenu.add(controlCheckBoxMenuItem);
1089        controlCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1090            setAllControlling(controlCheckBoxMenuItem.isSelected());
1091            redrawPanel();
1092        });
1093        controlCheckBoxMenuItem.setSelected(allControlling());
1094
1095        //
1096        // use direct turnout control
1097        //
1098        useDirectTurnoutControlCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("UseDirectTurnoutControl")); // NOI18N
1099        optionMenu.add(useDirectTurnoutControlCheckBoxMenuItem);
1100        useDirectTurnoutControlCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1101            setDirectTurnoutControl(useDirectTurnoutControlCheckBoxMenuItem.isSelected());
1102        });
1103        useDirectTurnoutControlCheckBoxMenuItem.setSelected(useDirectTurnoutControl);
1104
1105        //
1106        // antialiasing
1107        //
1108        antialiasingOnCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AntialiasingOn"));
1109        optionMenu.add(antialiasingOnCheckBoxMenuItem);
1110        antialiasingOnCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1111            setAntialiasingOn(antialiasingOnCheckBoxMenuItem.isSelected());
1112            redrawPanel();
1113        });
1114        antialiasingOnCheckBoxMenuItem.setSelected(antialiasingOn);
1115
1116        //
1117        // drawLayoutTracksLabel
1118        //
1119        drawLayoutTracksLabelCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("DrawLayoutTracksLabel"));
1120        optionMenu.add(drawLayoutTracksLabelCheckBoxMenuItem);
1121        drawLayoutTracksLabelCheckBoxMenuItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("DrawLayoutTracksMnemonic")));
1122        drawLayoutTracksLabelCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(
1123                stringsToVTCodes.get(Bundle.getMessage("DrawLayoutTracksAccelerator")), primary_modifier));
1124        drawLayoutTracksLabelCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1125
1126            if (fixMacBugOn11(event)) {
1127                drawLayoutTracksLabelCheckBoxMenuItem.setSelected(!drawLayoutTracksLabelCheckBoxMenuItem.isSelected());
1128                return;
1129            }
1130
1131            setDrawLayoutTracksLabel(drawLayoutTracksLabelCheckBoxMenuItem.isSelected());
1132            redrawPanel();
1133        });
1134        drawLayoutTracksLabelCheckBoxMenuItem.setSelected(drawLayoutTracksLabel);
1135
1136        // add "Highlight cursor position" - useful for touchscreens
1137        highlightCursorCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("HighlightCursor"));
1138        optionMenu.add(highlightCursorCheckBoxMenuItem);
1139        highlightCursorCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1140            highlightCursor = highlightCursorCheckBoxMenuItem.isSelected();
1141            redrawPanel();
1142        });
1143        highlightCursorCheckBoxMenuItem.setSelected(highlightCursor);
1144
1145        //
1146        // edit title
1147        //
1148        optionMenu.addSeparator();
1149        JMenuItem titleItem = new JMenuItem(Bundle.getMessage("EditTitle") + "...");
1150        optionMenu.add(titleItem);
1151        titleItem.addActionListener((ActionEvent event) -> {
1152            // prompt for name
1153            String newName = (String) JmriJOptionPane.showInputDialog(getTargetFrame(),
1154                    Bundle.getMessage("MakeLabel", Bundle.getMessage("EnterTitle")),
1155                    Bundle.getMessage("EditTitleMessageTitle"),
1156                    JmriJOptionPane.PLAIN_MESSAGE, null, null, getLayoutName());
1157
1158            if (newName != null) {
1159                if (!newName.equals(getLayoutName())) {
1160                    if (InstanceManager.getDefault(EditorManager.class).contains(newName)) {
1161                        JmriJOptionPane.showMessageDialog(null,
1162                            Bundle.getMessage("CanNotRename", Bundle.getMessage("Panel")),
1163                            Bundle.getMessage("AlreadyExist", Bundle.getMessage("Panel")),
1164                            JmriJOptionPane.ERROR_MESSAGE);
1165                    } else {
1166                        setTitle(newName);
1167                        setLayoutName(newName);
1168                        getLayoutTrackDrawingOptions().setName(newName);
1169                        setDirty();
1170
1171                        if (toolBarSide.equals(ToolBarSide.eFLOAT) && isEditable()) {
1172                            // Rebuild the toolbox after a name change.
1173                            deletefloatingEditToolBoxFrame();
1174                            createfloatingEditToolBoxFrame();
1175                            createFloatingHelpPanel();
1176                        }
1177                    }
1178                }
1179            }
1180        });
1181
1182        //
1183        // set background color
1184        //
1185        JMenuItem backgroundColorMenuItem = new JMenuItem(Bundle.getMessage("SetBackgroundColor", "..."));
1186        optionMenu.add(backgroundColorMenuItem);
1187        backgroundColorMenuItem.addActionListener((ActionEvent event) -> {
1188            Color desiredColor = JmriColorChooser.showDialog(this,
1189                    Bundle.getMessage("SetBackgroundColor", ""),
1190                    defaultBackgroundColor);
1191            if (desiredColor != null && !defaultBackgroundColor.equals(desiredColor)) {
1192                defaultBackgroundColor = desiredColor;
1193                setBackgroundColor(desiredColor);
1194                setDirty();
1195                redrawPanel();
1196            }
1197        });
1198
1199        //
1200        // set default text color
1201        //
1202        JMenuItem textColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultTextColor", "..."));
1203        optionMenu.add(textColorMenuItem);
1204        textColorMenuItem.addActionListener((ActionEvent event) -> {
1205            Color desiredColor = JmriColorChooser.showDialog(this,
1206                    Bundle.getMessage("DefaultTextColor", ""),
1207                    defaultTextColor);
1208            if (desiredColor != null && !defaultTextColor.equals(desiredColor)) {
1209                setDefaultTextColor(desiredColor);
1210                setDirty();
1211                redrawPanel();
1212            }
1213        });
1214
1215        if (editorUseOldLocSize) {
1216            //
1217            //  save location and size
1218            //
1219            JMenuItem locationItem = new JMenuItem(Bundle.getMessage("SetLocation"));
1220            optionMenu.add(locationItem);
1221            locationItem.addActionListener((ActionEvent event) -> {
1222                setCurrentPositionAndSize();
1223                log.debug("Bounds:{}, {}, {}, {}, {}, {}",
1224                        gContext.getUpperLeftX(), gContext.getUpperLeftY(),
1225                        gContext.getWindowWidth(), gContext.getWindowHeight(),
1226                        gContext.getLayoutWidth(), gContext.getLayoutHeight());
1227            });
1228        }
1229
1230        //
1231        // Add Options
1232        //
1233        JMenu optionsAddMenu = new JMenu(Bundle.getMessage("AddMenuTitle"));
1234        optionMenu.add(optionsAddMenu);
1235
1236        // add background image
1237        JMenuItem backgroundItem = new JMenuItem(Bundle.getMessage("AddBackground") + "...");
1238        optionsAddMenu.add(backgroundItem);
1239        backgroundItem.addActionListener((ActionEvent event) -> {
1240            addBackground();
1241            // note: panel resized in addBackground
1242            setDirty();
1243            redrawPanel();
1244        });
1245
1246        // add fast clock
1247        JMenuItem clockItem = new JMenuItem(Bundle.getMessage("AddItem", Bundle.getMessage("FastClock")));
1248        optionsAddMenu.add(clockItem);
1249        clockItem.addActionListener((ActionEvent event) -> {
1250            AnalogClock2Display c = addClock();
1251            unionToPanelBounds(c.getBounds());
1252            setDirty();
1253            redrawPanel();
1254        });
1255
1256        // add turntable
1257        JMenuItem turntableItem = new JMenuItem(Bundle.getMessage("AddTurntable"));
1258        optionsAddMenu.add(turntableItem);
1259        turntableItem.addActionListener((ActionEvent event) -> {
1260            Point2D pt = windowCenter();
1261            if (selectionActive) {
1262                pt = MathUtil.midPoint(getSelectionRect());
1263            }
1264            addTurntable(pt);
1265            // note: panel resized in addTurntable
1266            setDirty();
1267            redrawPanel();
1268        });
1269
1270        // add traverser
1271        JMenuItem traverserItem = new JMenuItem(Bundle.getMessage("AddTraverser"));
1272        optionsAddMenu.add(traverserItem);
1273        traverserItem.addActionListener((ActionEvent event) -> {
1274            Point2D pt = windowCenter();
1275            if (selectionActive) {
1276                pt = MathUtil.midPoint(getSelectionRect());
1277            }
1278            addTraverser(pt);
1279            // note: panel resized in addTraverser
1280            setDirty();
1281            redrawPanel();
1282        });
1283
1284        // add reporter
1285        JMenuItem reporterItem = new JMenuItem(Bundle.getMessage("AddReporter") + "...");
1286        optionsAddMenu.add(reporterItem);
1287        reporterItem.addActionListener((ActionEvent event) -> {
1288            Point2D pt = windowCenter();
1289            if (selectionActive) {
1290                pt = MathUtil.midPoint(getSelectionRect());
1291            }
1292            EnterReporterDialog d = new EnterReporterDialog(this);
1293            d.enterReporter((int) pt.getX(), (int) pt.getY());
1294            // note: panel resized in enterReporter
1295            setDirty();
1296            redrawPanel();
1297        });
1298
1299        //
1300        // location coordinates format menu
1301        //
1302        JMenu locationMenu = new JMenu(Bundle.getMessage("LocationMenuTitle")); // used for location format SubMenu
1303        optionMenu.add(locationMenu);
1304
1305        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
1306            String windowFrameRef = getWindowFrameRef();
1307            Object prefsProp = prefsMgr.getProperty(windowFrameRef, "LocationFormat");
1308            // log.debug("{}.LocationFormat is {}", windowFrameRef, prefsProp);
1309            if (prefsProp != null) {
1310                getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.valueOf((String) prefsProp));
1311            }
1312        });
1313
1314        // pixels (jmri classic)
1315        locationMenu.add(pixelsCheckBoxMenuItem);
1316        pixelsCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1317            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.ePIXELS);
1318            selectLocationFormatCheckBoxMenuItem();
1319            redrawPanel();
1320        });
1321
1322        // metric cm's
1323        locationMenu.add(metricCMCheckBoxMenuItem);
1324        metricCMCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1325            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.eMETRIC_CM);
1326            selectLocationFormatCheckBoxMenuItem();
1327            redrawPanel();
1328        });
1329
1330        // english feet/inches/16th's
1331        locationMenu.add(englishFeetInchesCheckBoxMenuItem);
1332        englishFeetInchesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1333            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.eENGLISH_FEET_INCHES);
1334            selectLocationFormatCheckBoxMenuItem();
1335            redrawPanel();
1336        });
1337        selectLocationFormatCheckBoxMenuItem();
1338
1339        //
1340        // grid menu
1341        //
1342        JMenu gridMenu = new JMenu(Bundle.getMessage("GridMenuTitle")); // used for Grid SubMenu
1343        optionMenu.add(gridMenu);
1344
1345        // show grid
1346        showGridCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowEditGrid"));
1347        showGridCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1348                Bundle.getMessage("ShowEditGridAccelerator")), primary_modifier));
1349        gridMenu.add(showGridCheckBoxMenuItem);
1350        showGridCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1351
1352            if (fixMacBugOn11(event)) {
1353                showGridCheckBoxMenuItem.setSelected(!showGridCheckBoxMenuItem.isSelected());
1354                return;
1355            }
1356
1357            drawGrid = showGridCheckBoxMenuItem.isSelected();
1358            redrawPanel();
1359        });
1360        showGridCheckBoxMenuItem.setSelected(getDrawGrid());
1361
1362        // snap to grid on add
1363        snapToGridOnAddCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SnapToGridOnAdd"));
1364        snapToGridOnAddCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1365                Bundle.getMessage("SnapToGridOnAddAccelerator")),
1366                primary_modifier | ActionEvent.SHIFT_MASK));
1367        gridMenu.add(snapToGridOnAddCheckBoxMenuItem);
1368        snapToGridOnAddCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1369
1370            if (fixMacBugOn11(event)) {
1371                snapToGridOnAddCheckBoxMenuItem.setSelected(!snapToGridOnAddCheckBoxMenuItem.isSelected());
1372                return;
1373            }
1374
1375            snapToGridOnAdd = snapToGridOnAddCheckBoxMenuItem.isSelected();
1376            redrawPanel();
1377        });
1378        snapToGridOnAddCheckBoxMenuItem.setSelected(snapToGridOnAdd);
1379
1380        // snap to grid on move
1381        snapToGridOnMoveCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SnapToGridOnMove"));
1382        snapToGridOnMoveCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1383                Bundle.getMessage("SnapToGridOnMoveAccelerator")),
1384                primary_modifier | ActionEvent.SHIFT_MASK));
1385        gridMenu.add(snapToGridOnMoveCheckBoxMenuItem);
1386        snapToGridOnMoveCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1387
1388            if (fixMacBugOn11(event)) {
1389                snapToGridOnMoveCheckBoxMenuItem.setSelected(!snapToGridOnMoveCheckBoxMenuItem.isSelected());
1390                return;
1391            }
1392
1393            snapToGridOnMove = snapToGridOnMoveCheckBoxMenuItem.isSelected();
1394            redrawPanel();
1395        });
1396        snapToGridOnMoveCheckBoxMenuItem.setSelected(snapToGridOnMove);
1397
1398        // specify grid square size
1399        JMenuItem gridSizeItem = new JMenuItem(Bundle.getMessage("SetGridSizes") + "...");
1400        gridMenu.add(gridSizeItem);
1401        gridSizeItem.addActionListener((ActionEvent event) -> {
1402            EnterGridSizesDialog d = new EnterGridSizesDialog(this);
1403            d.enterGridSizes();
1404        });
1405
1406        //
1407        // track menu
1408        //
1409        JMenu trackMenu = new JMenu(Bundle.getMessage("TrackMenuTitle"));
1410        optionMenu.add(trackMenu);
1411
1412        // set track drawing options menu item
1413        JMenuItem jmi = new JMenuItem(Bundle.getMessage("SetTrackDrawingOptions"));
1414        trackMenu.add(jmi);
1415        jmi.setToolTipText(Bundle.getMessage("SetTrackDrawingOptionsToolTip"));
1416        jmi.addActionListener((ActionEvent event) -> {
1417            LayoutTrackDrawingOptionsDialog ltdod
1418                    = new LayoutTrackDrawingOptionsDialog(
1419                            this, true, getLayoutTrackDrawingOptions());
1420            ltdod.setVisible(true);
1421        });
1422
1423        // track colors item menu item
1424        JMenu trkColourMenu = new JMenu(Bundle.getMessage("TrackColorSubMenu"));
1425        trackMenu.add(trkColourMenu);
1426
1427        JMenuItem trackColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultTrackColor"));
1428        trkColourMenu.add(trackColorMenuItem);
1429        trackColorMenuItem.addActionListener((ActionEvent event) -> {
1430            Color desiredColor = JmriColorChooser.showDialog(this,
1431                    Bundle.getMessage("DefaultTrackColor"),
1432                    defaultTrackColor);
1433            if (desiredColor != null && !defaultTrackColor.equals(desiredColor)) {
1434                setDefaultTrackColor(desiredColor);
1435                setDirty();
1436                redrawPanel();
1437            }
1438        });
1439
1440        JMenuItem trackOccupiedColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultOccupiedTrackColor"));
1441        trkColourMenu.add(trackOccupiedColorMenuItem);
1442        trackOccupiedColorMenuItem.addActionListener((ActionEvent event) -> {
1443            Color desiredColor = JmriColorChooser.showDialog(this,
1444                    Bundle.getMessage("DefaultOccupiedTrackColor"),
1445                    defaultOccupiedTrackColor);
1446            if (desiredColor != null && !defaultOccupiedTrackColor.equals(desiredColor)) {
1447                setDefaultOccupiedTrackColor(desiredColor);
1448                setDirty();
1449                redrawPanel();
1450            }
1451        });
1452
1453        JMenuItem trackAlternativeColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultAlternativeTrackColor"));
1454        trkColourMenu.add(trackAlternativeColorMenuItem);
1455        trackAlternativeColorMenuItem.addActionListener((ActionEvent event) -> {
1456            Color desiredColor = JmriColorChooser.showDialog(this,
1457                    Bundle.getMessage("DefaultAlternativeTrackColor"),
1458                    defaultAlternativeTrackColor);
1459            if (desiredColor != null && !defaultAlternativeTrackColor.equals(desiredColor)) {
1460                setDefaultAlternativeTrackColor(desiredColor);
1461                setDirty();
1462                redrawPanel();
1463            }
1464        });
1465
1466        // Set All Tracks To Default Colors
1467        JMenuItem setAllTracksToDefaultColorsMenuItem = new JMenuItem(Bundle.getMessage("SetAllTracksToDefaultColors"));
1468        trkColourMenu.add(setAllTracksToDefaultColorsMenuItem);
1469        setAllTracksToDefaultColorsMenuItem.addActionListener((ActionEvent event) -> {
1470            if (setAllTracksToDefaultColors() > 0) {
1471                setDirty();
1472                redrawPanel();
1473            }
1474        });
1475
1476        // Automatically Assign Blocks to Track
1477        autoAssignBlocksCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AutoAssignBlock"));
1478        trackMenu.add(autoAssignBlocksCheckBoxMenuItem);
1479        autoAssignBlocksCheckBoxMenuItem.addActionListener((ActionEvent event) -> autoAssignBlocks = autoAssignBlocksCheckBoxMenuItem.isSelected());
1480        autoAssignBlocksCheckBoxMenuItem.setSelected(autoAssignBlocks);
1481
1482        // add hideTrackSegmentConstructionLines menu item
1483        hideTrackSegmentConstructionLinesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("HideTrackConLines"));
1484        trackMenu.add(hideTrackSegmentConstructionLinesCheckBoxMenuItem);
1485        hideTrackSegmentConstructionLinesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1486            int show = TrackSegmentView.SHOWCON;
1487
1488            if (hideTrackSegmentConstructionLinesCheckBoxMenuItem.isSelected()) {
1489                show = TrackSegmentView.HIDECONALL;
1490            }
1491
1492            for (TrackSegmentView tsv : getTrackSegmentViews()) {
1493                tsv.hideConstructionLines(show);
1494            }
1495            redrawPanel();
1496        });
1497        hideTrackSegmentConstructionLinesCheckBoxMenuItem.setSelected(autoAssignBlocks);
1498
1499        //
1500        // add turnout options submenu
1501        //
1502        JMenu turnoutOptionsMenu = new JMenu(Bundle.getMessage("TurnoutOptions"));
1503        optionMenu.add(turnoutOptionsMenu);
1504
1505        // animation item
1506        animationCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowTurnoutAnimation"));
1507        turnoutOptionsMenu.add(animationCheckBoxMenuItem);
1508        animationCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1509            boolean mode = animationCheckBoxMenuItem.isSelected();
1510            setTurnoutAnimation(mode);
1511        });
1512        animationCheckBoxMenuItem.setSelected(true);
1513
1514        // circle on Turnouts
1515        turnoutCirclesOnCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutCirclesOn"));
1516        turnoutOptionsMenu.add(turnoutCirclesOnCheckBoxMenuItem);
1517        turnoutCirclesOnCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1518            turnoutCirclesWithoutEditMode = turnoutCirclesOnCheckBoxMenuItem.isSelected();
1519            redrawPanel();
1520        });
1521        turnoutCirclesOnCheckBoxMenuItem.setSelected(turnoutCirclesWithoutEditMode);
1522
1523        // select turnout circle color
1524        JMenuItem turnoutCircleColorMenuItem = new JMenuItem(Bundle.getMessage("TurnoutCircleColor"));
1525        turnoutCircleColorMenuItem.addActionListener((ActionEvent event) -> {
1526            Color desiredColor = JmriColorChooser.showDialog(this,
1527                    Bundle.getMessage("TurnoutCircleColor"),
1528                    turnoutCircleColor);
1529            if (desiredColor != null && !turnoutCircleColor.equals(desiredColor)) {
1530                setTurnoutCircleColor(desiredColor);
1531                setDirty();
1532                redrawPanel();
1533            }
1534        });
1535        turnoutOptionsMenu.add(turnoutCircleColorMenuItem);
1536
1537        // select turnout circle thrown color
1538        JMenuItem turnoutCircleThrownColorMenuItem = new JMenuItem(Bundle.getMessage("TurnoutCircleThrownColor"));
1539        turnoutCircleThrownColorMenuItem.addActionListener((ActionEvent event) -> {
1540            Color desiredColor = JmriColorChooser.showDialog(this,
1541                    Bundle.getMessage("TurnoutCircleThrownColor"),
1542                    turnoutCircleThrownColor);
1543            if (desiredColor != null && !turnoutCircleThrownColor.equals(desiredColor)) {
1544                setTurnoutCircleThrownColor(desiredColor);
1545                setDirty();
1546                redrawPanel();
1547            }
1548        });
1549        turnoutOptionsMenu.add(turnoutCircleThrownColorMenuItem);
1550
1551        turnoutFillControlCirclesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutFillControlCircles"));
1552        turnoutOptionsMenu.add(turnoutFillControlCirclesCheckBoxMenuItem);
1553        turnoutFillControlCirclesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1554            turnoutFillControlCircles = turnoutFillControlCirclesCheckBoxMenuItem.isSelected();
1555            redrawPanel();
1556        });
1557        turnoutFillControlCirclesCheckBoxMenuItem.setSelected(turnoutFillControlCircles);
1558
1559        // select turnout circle size
1560        JMenu turnoutCircleSizeMenu = new JMenu(Bundle.getMessage("TurnoutCircleSize"));
1561        turnoutCircleSizeButtonGroup = new ButtonGroup();
1562        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "1", 1);
1563        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "2", 2);
1564        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "3", 3);
1565        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "4", 4);
1566        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "5", 5);
1567        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "6", 6);
1568        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "7", 7);
1569        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "8", 8);
1570        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "9", 9);
1571        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "10", 10);
1572        turnoutOptionsMenu.add(turnoutCircleSizeMenu);
1573
1574        // add "enable drawing of unselected leg " menu item (helps when diverging angle is small)
1575        turnoutDrawUnselectedLegCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutDrawUnselectedLeg"));
1576        turnoutOptionsMenu.add(turnoutDrawUnselectedLegCheckBoxMenuItem);
1577        turnoutDrawUnselectedLegCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1578            turnoutDrawUnselectedLeg = turnoutDrawUnselectedLegCheckBoxMenuItem.isSelected();
1579            redrawPanel();
1580        });
1581        turnoutDrawUnselectedLegCheckBoxMenuItem.setSelected(turnoutDrawUnselectedLeg);
1582
1583        return optionMenu;
1584    }
1585
1586    private void selectLocationFormatCheckBoxMenuItem() {
1587        pixelsCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.ePIXELS);
1588        metricCMCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.eMETRIC_CM);
1589        englishFeetInchesCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.eENGLISH_FEET_INCHES);
1590    }
1591
1592    /*============================================*\
1593    |* LayoutTrackDrawingOptions accessor methods *|
1594    \*============================================*/
1595    private LayoutTrackDrawingOptions layoutTrackDrawingOptions = null;
1596
1597    /**
1598     *
1599     * Getter Layout Track Drawing Options. since 4.15.6 split variable
1600     * defaultTrackColor and mainlineTrackColor/sidelineTrackColor <br>
1601     * blockDefaultColor, blockOccupiedColor and blockAlternativeColor added to
1602     * LayoutTrackDrawingOptions <br>
1603     *
1604     * @return LayoutTrackDrawingOptions object
1605     */
1606    @Nonnull
1607    public LayoutTrackDrawingOptions getLayoutTrackDrawingOptions() {
1608        if (layoutTrackDrawingOptions == null) {
1609            layoutTrackDrawingOptions = new LayoutTrackDrawingOptions(getLayoutName());
1610            // integrate LayoutEditor drawing options with previous drawing options
1611            layoutTrackDrawingOptions.setMainBlockLineWidth(gContext.getMainlineTrackWidth());
1612            layoutTrackDrawingOptions.setSideBlockLineWidth(gContext.getSidelineTrackWidth());
1613            layoutTrackDrawingOptions.setMainRailWidth(gContext.getMainlineTrackWidth());
1614            layoutTrackDrawingOptions.setSideRailWidth(gContext.getSidelineTrackWidth());
1615            layoutTrackDrawingOptions.setMainRailColor(mainlineTrackColor);
1616            layoutTrackDrawingOptions.setSideRailColor(sidelineTrackColor);
1617            layoutTrackDrawingOptions.setBlockDefaultColor(defaultTrackColor);
1618            layoutTrackDrawingOptions.setBlockOccupiedColor(defaultOccupiedTrackColor);
1619            layoutTrackDrawingOptions.setBlockAlternativeColor(defaultAlternativeTrackColor);
1620        }
1621        return layoutTrackDrawingOptions;
1622    }
1623
1624    /**
1625     * since 4.15.6 split variable defaultTrackColor and
1626     * mainlineTrackColor/sidelineTrackColor
1627     *
1628     * @param ltdo LayoutTrackDrawingOptions object
1629     */
1630    public void setLayoutTrackDrawingOptions(LayoutTrackDrawingOptions ltdo) {
1631        layoutTrackDrawingOptions = ltdo;
1632
1633        // copy main/side line block widths
1634        gContext.setMainlineBlockWidth(layoutTrackDrawingOptions.getMainBlockLineWidth());
1635        gContext.setSidelineBlockWidth(layoutTrackDrawingOptions.getSideBlockLineWidth());
1636
1637        // copy main/side line track (rail) widths
1638        gContext.setMainlineTrackWidth(layoutTrackDrawingOptions.getMainRailWidth());
1639        gContext.setSidelineTrackWidth(layoutTrackDrawingOptions.getSideRailWidth());
1640
1641        mainlineTrackColor = layoutTrackDrawingOptions.getMainRailColor();
1642        sidelineTrackColor = layoutTrackDrawingOptions.getSideRailColor();
1643        redrawPanel();
1644    }
1645
1646    private JCheckBoxMenuItem skipTurnoutCheckBoxMenuItem = null;
1647    private AddEntryExitPairAction addEntryExitPairAction = null;
1648
1649    /**
1650     * setup the Layout Editor Tools menu
1651     *
1652     * @param menuBar the menu bar to add the Tools menu to
1653     */
1654    private void setupToolsMenu(@Nonnull JMenuBar menuBar) {
1655        JMenu toolsMenu = new JMenu(Bundle.getMessage("MenuTools"));
1656
1657        toolsMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuToolsMnemonic")));
1658        menuBar.add(toolsMenu);
1659
1660        // setup checks menu
1661        getLEChecks().setupChecksMenu(toolsMenu);
1662
1663        // assign blocks to selection
1664        assignBlockToSelectionMenuItem.setToolTipText(Bundle.getMessage("AssignBlockToSelectionToolTip"));
1665        toolsMenu.add(assignBlockToSelectionMenuItem);
1666        assignBlockToSelectionMenuItem.addActionListener((ActionEvent event) -> {
1667            // bring up scale track diagram dialog
1668            assignBlockToSelection();
1669        });
1670        assignBlockToSelectionMenuItem.setEnabled(!_layoutTrackSelection.isEmpty());
1671
1672        // scale track diagram
1673        JMenuItem jmi = new JMenuItem(Bundle.getMessage("ScaleTrackDiagram") + "...");
1674        jmi.setToolTipText(Bundle.getMessage("ScaleTrackDiagramToolTip"));
1675        toolsMenu.add(jmi);
1676        jmi.addActionListener((ActionEvent event) -> {
1677            // bring up scale track diagram dialog
1678            ScaleTrackDiagramDialog d = new ScaleTrackDiagramDialog(this);
1679            d.scaleTrackDiagram();
1680        });
1681
1682        // translate selection
1683        jmi = new JMenuItem(Bundle.getMessage("TranslateSelection") + "...");
1684        jmi.setToolTipText(Bundle.getMessage("TranslateSelectionToolTip"));
1685        toolsMenu.add(jmi);
1686        jmi.addActionListener((ActionEvent event) -> {
1687            // bring up translate selection dialog
1688            if (!selectionActive || (selectionWidth == 0.0) || (selectionHeight == 0.0)) {
1689                // no selection has been made - nothing to move
1690                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error12"),
1691                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1692            } else {
1693                // bring up move selection dialog
1694                MoveSelectionDialog d = new MoveSelectionDialog(this);
1695                d.moveSelection();
1696            }
1697        });
1698
1699        // undo translate selection
1700        undoTranslateSelectionMenuItem.setToolTipText(Bundle.getMessage("UndoTranslateSelectionToolTip"));
1701        toolsMenu.add(undoTranslateSelectionMenuItem);
1702        undoTranslateSelectionMenuItem.addActionListener((ActionEvent event) -> {
1703            // undo previous move selection
1704            undoMoveSelection();
1705        });
1706        undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
1707
1708        // rotate selection
1709        jmi = new JMenuItem(Bundle.getMessage("RotateSelection90MenuItemTitle"));
1710        jmi.setToolTipText(Bundle.getMessage("RotateSelection90MenuItemToolTip"));
1711        toolsMenu.add(jmi);
1712        jmi.addActionListener((ActionEvent event) -> rotateSelection90());
1713
1714        // rotate entire layout
1715        jmi = new JMenuItem(Bundle.getMessage("RotateLayout90MenuItemTitle"));
1716        jmi.setToolTipText(Bundle.getMessage("RotateLayout90MenuItemToolTip"));
1717        toolsMenu.add(jmi);
1718        jmi.addActionListener((ActionEvent event) -> rotateLayout90());
1719
1720        // align layout to grid
1721        jmi = new JMenuItem(Bundle.getMessage("AlignLayoutToGridMenuItemTitle") + "...");
1722        jmi.setToolTipText(Bundle.getMessage("AlignLayoutToGridMenuItemToolTip"));
1723        toolsMenu.add(jmi);
1724        jmi.addActionListener((ActionEvent event) -> alignLayoutToGrid());
1725
1726        // align selection to grid
1727        jmi = new JMenuItem(Bundle.getMessage("AlignSelectionToGridMenuItemTitle") + "...");
1728        jmi.setToolTipText(Bundle.getMessage("AlignSelectionToGridMenuItemToolTip"));
1729        toolsMenu.add(jmi);
1730        jmi.addActionListener((ActionEvent event) -> alignSelectionToGrid());
1731
1732        // reset turnout size to program defaults
1733        jmi = new JMenuItem(Bundle.getMessage("ResetTurnoutSize"));
1734        jmi.setToolTipText(Bundle.getMessage("ResetTurnoutSizeToolTip"));
1735        toolsMenu.add(jmi);
1736        jmi.addActionListener((ActionEvent event) -> {
1737            // undo previous move selection
1738            resetTurnoutSize();
1739        });
1740        toolsMenu.addSeparator();
1741
1742        // skip turnout
1743        skipTurnoutCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SkipInternalTurnout"));
1744        skipTurnoutCheckBoxMenuItem.setToolTipText(Bundle.getMessage("SkipInternalTurnoutToolTip"));
1745        toolsMenu.add(skipTurnoutCheckBoxMenuItem);
1746        skipTurnoutCheckBoxMenuItem.addActionListener((ActionEvent event) -> setIncludedTurnoutSkipped(skipTurnoutCheckBoxMenuItem.isSelected()));
1747        skipTurnoutCheckBoxMenuItem.setSelected(isIncludedTurnoutSkipped());
1748
1749        // set signals at turnout
1750        jmi = new JMenuItem(Bundle.getMessage("SignalsAtTurnout") + "...");
1751        jmi.setToolTipText(Bundle.getMessage("SignalsAtTurnoutToolTip"));
1752        toolsMenu.add(jmi);
1753        jmi.addActionListener((ActionEvent event) -> {
1754            // bring up signals at turnout tool dialog
1755            getLETools().setSignalsAtTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1756        });
1757
1758        // set signals at block boundary
1759        jmi = new JMenuItem(Bundle.getMessage("SignalsAtBoundary") + "...");
1760        jmi.setToolTipText(Bundle.getMessage("SignalsAtBoundaryToolTip"));
1761        toolsMenu.add(jmi);
1762        jmi.addActionListener((ActionEvent event) -> {
1763            // bring up signals at block boundary tool dialog
1764            getLETools().setSignalsAtBlockBoundary(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1765        });
1766
1767        // set signals at crossover turnout
1768        jmi = new JMenuItem(Bundle.getMessage("SignalsAtXoverTurnout") + "...");
1769        jmi.setToolTipText(Bundle.getMessage("SignalsAtXoverTurnoutToolTip"));
1770        toolsMenu.add(jmi);
1771        jmi.addActionListener((ActionEvent event) -> {
1772            // bring up signals at crossover tool dialog
1773            getLETools().setSignalsAtXoverTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1774        });
1775
1776        // set signals at level crossing
1777        jmi = new JMenuItem(Bundle.getMessage("SignalsAtLevelXing") + "...");
1778        jmi.setToolTipText(Bundle.getMessage("SignalsAtLevelXingToolTip"));
1779        toolsMenu.add(jmi);
1780        jmi.addActionListener((ActionEvent event) -> {
1781            // bring up signals at level crossing tool dialog
1782            getLETools().setSignalsAtLevelXing(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1783        });
1784
1785        // set signals at throat-to-throat turnouts
1786        jmi = new JMenuItem(Bundle.getMessage("SignalsAtTToTTurnout") + "...");
1787        jmi.setToolTipText(Bundle.getMessage("SignalsAtTToTTurnoutToolTip"));
1788        toolsMenu.add(jmi);
1789        jmi.addActionListener((ActionEvent event) -> {
1790            // bring up signals at throat-to-throat turnouts tool dialog
1791            getLETools().setSignalsAtThroatToThroatTurnouts(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1792        });
1793
1794        // set signals at 3-way turnout
1795        jmi = new JMenuItem(Bundle.getMessage("SignalsAt3WayTurnout") + "...");
1796        jmi.setToolTipText(Bundle.getMessage("SignalsAt3WayTurnoutToolTip"));
1797        toolsMenu.add(jmi);
1798        jmi.addActionListener((ActionEvent event) -> {
1799            // bring up signals at 3-way turnout tool dialog
1800            getLETools().setSignalsAt3WayTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1801        });
1802
1803        jmi = new JMenuItem(Bundle.getMessage("SignalsAtSlip") + "...");
1804        jmi.setToolTipText(Bundle.getMessage("SignalsAtSlipToolTip"));
1805        toolsMenu.add(jmi);
1806        jmi.addActionListener((ActionEvent event) -> {
1807            // bring up signals at throat-to-throat turnouts tool dialog
1808            getLETools().setSignalsAtSlip(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1809        });
1810
1811        jmi = new JMenuItem(Bundle.getMessage("EntryExitTitle") + "...");
1812        jmi.setToolTipText(Bundle.getMessage("EntryExitToolTip"));
1813        toolsMenu.add(jmi);
1814        jmi.addActionListener((ActionEvent event) -> {
1815            if (addEntryExitPairAction == null) {
1816                addEntryExitPairAction = new AddEntryExitPairAction("ENTRY EXIT", LayoutEditor.this);
1817            }
1818            addEntryExitPairAction.actionPerformed(event);
1819        });
1820//        if (true) {   // TODO: disable for production
1821//            jmi = new JMenuItem("GEORGE");
1822//            toolsMenu.add(jmi);
1823//            jmi.addActionListener((ActionEvent event) -> {
1824//                // do GEORGE stuff here!
1825//            });
1826//        }
1827    }   // setupToolsMenu
1828
1829    /**
1830     * get the toolbar side
1831     *
1832     * @return the side where to put the tool bar
1833     */
1834    public ToolBarSide getToolBarSide() {
1835        return toolBarSide;
1836    }
1837
1838    /**
1839     * set the tool bar side
1840     *
1841     * @param newToolBarSide on which side to put the toolbar
1842     */
1843    public void setToolBarSide(ToolBarSide newToolBarSide) {
1844        // null if edit toolbar is not setup yet...
1845        if (!newToolBarSide.equals(toolBarSide)) {
1846            toolBarSide = newToolBarSide;
1847            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setProperty(getWindowFrameRef(), "toolBarSide", toolBarSide.getName()));
1848            toolBarSideTopButton.setSelected(toolBarSide.equals(ToolBarSide.eTOP));
1849            toolBarSideLeftButton.setSelected(toolBarSide.equals(ToolBarSide.eLEFT));
1850            toolBarSideBottomButton.setSelected(toolBarSide.equals(ToolBarSide.eBOTTOM));
1851            toolBarSideRightButton.setSelected(toolBarSide.equals(ToolBarSide.eRIGHT));
1852            toolBarSideFloatButton.setSelected(toolBarSide.equals(ToolBarSide.eFLOAT));
1853
1854            setupToolBar(); // re-layout all the toolbar items
1855
1856            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
1857                if (editToolBarContainerPanel != null) {
1858                    editToolBarContainerPanel.setVisible(false);
1859                }
1860                if (floatEditHelpPanel != null) {
1861                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
1862                }
1863            } else {
1864                if (floatingEditToolBoxFrame != null) {
1865                    deletefloatingEditToolBoxFrame();
1866                }
1867                editToolBarContainerPanel.setVisible(isEditable());
1868                if (getShowHelpBar()) {
1869                    helpBarPanel.setVisible(isEditable());
1870                    // not sure why... but this is the only way I could
1871                    // get everything to layout correctly
1872                    // when the helpbar is visible...
1873                    boolean editMode = isEditable();
1874                    setAllEditable(!editMode);
1875                    setAllEditable(editMode);
1876                }
1877            }
1878            wideToolBarCheckBoxMenuItem.setEnabled(
1879                    toolBarSide.equals(ToolBarSide.eTOP)
1880                    || toolBarSide.equals(ToolBarSide.eBOTTOM));
1881        }
1882    }   // setToolBarSide
1883
1884    //
1885    //
1886    //
1887    private void setToolBarWide(boolean newToolBarIsWide) {
1888        // null if edit toolbar not setup yet...
1889        if (leToolBarPanel.toolBarIsWide != newToolBarIsWide) {
1890            leToolBarPanel.toolBarIsWide = newToolBarIsWide;
1891
1892            wideToolBarCheckBoxMenuItem.setSelected(leToolBarPanel.toolBarIsWide);
1893
1894            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
1895                // Note: since prefs default to false and we want wide to be the default
1896                // we invert it and save it as thin
1897                prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".toolBarThin", !leToolBarPanel.toolBarIsWide);
1898            });
1899
1900            setupToolBar(); // re-layout all the toolbar items
1901
1902            if (getShowHelpBar()) {
1903                // not sure why, but this is the only way I could
1904                // get everything to layout correctly
1905                // when the helpbar is visible...
1906                boolean editMode = isEditable();
1907                setAllEditable(!editMode);
1908                setAllEditable(editMode);
1909            } else {
1910                helpBarPanel.setVisible(isEditable() && getShowHelpBar());
1911            }
1912        }
1913    }   // setToolBarWide
1914
1915    //
1916    //
1917    //
1918    @SuppressWarnings("deprecation")  // getMenuShortcutKeyMask()
1919    private void setupZoomMenu(@Nonnull JMenuBar menuBar) {
1920        zoomMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuZoomMnemonic")));
1921        menuBar.add(zoomMenu);
1922        ButtonGroup zoomButtonGroup = new ButtonGroup();
1923
1924        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
1925
1926        // add zoom choices to menu
1927        JMenuItem zoomInItem = new JMenuItem(Bundle.getMessage("ZoomIn"));
1928        zoomInItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("zoomInMnemonic")));
1929        String zoomInAccelerator = Bundle.getMessage("zoomInAccelerator");
1930        // log.debug("zoomInAccelerator: " + zoomInAccelerator);
1931        zoomInItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomInAccelerator), primary_modifier));
1932        zoomMenu.add(zoomInItem);
1933        zoomInItem.addActionListener((ActionEvent event) -> setZoom(getZoom() * 1.1));
1934
1935        JMenuItem zoomOutItem = new JMenuItem(Bundle.getMessage("ZoomOut"));
1936        zoomOutItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("zoomOutMnemonic")));
1937        String zoomOutAccelerator = Bundle.getMessage("zoomOutAccelerator");
1938        // log.debug("zoomOutAccelerator: " + zoomOutAccelerator);
1939        zoomOutItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomOutAccelerator), primary_modifier));
1940        zoomMenu.add(zoomOutItem);
1941        zoomOutItem.addActionListener((ActionEvent event) -> setZoom(getZoom() / 1.1));
1942
1943        JMenuItem zoomFitItem = new JMenuItem(Bundle.getMessage("ZoomToFit"));
1944        zoomMenu.add(zoomFitItem);
1945        zoomFitItem.addActionListener((ActionEvent event) -> zoomToFit());
1946        zoomMenu.addSeparator();
1947
1948        // add zoom choices to menu
1949        zoomMenu.add(zoom025Item);
1950        zoom025Item.addActionListener((ActionEvent event) -> setZoom(0.25));
1951        zoomButtonGroup.add(zoom025Item);
1952
1953        zoomMenu.add(zoom05Item);
1954        zoom05Item.addActionListener((ActionEvent event) -> setZoom(0.5));
1955        zoomButtonGroup.add(zoom05Item);
1956
1957        zoomMenu.add(zoom075Item);
1958        zoom075Item.addActionListener((ActionEvent event) -> setZoom(0.75));
1959        zoomButtonGroup.add(zoom075Item);
1960
1961        String zoomNoneAccelerator = Bundle.getMessage("zoomNoneAccelerator");
1962        // log.debug("zoomNoneAccelerator: " + zoomNoneAccelerator);
1963        noZoomItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomNoneAccelerator), primary_modifier));
1964
1965        zoomMenu.add(noZoomItem);
1966        noZoomItem.addActionListener((ActionEvent event) -> setZoom(1.0));
1967        zoomButtonGroup.add(noZoomItem);
1968
1969        zoomMenu.add(zoom15Item);
1970        zoom15Item.addActionListener((ActionEvent event) -> setZoom(1.5));
1971        zoomButtonGroup.add(zoom15Item);
1972
1973        zoomMenu.add(zoom20Item);
1974        zoom20Item.addActionListener((ActionEvent event) -> setZoom(2.0));
1975        zoomButtonGroup.add(zoom20Item);
1976
1977        zoomMenu.add(zoom30Item);
1978        zoom30Item.addActionListener((ActionEvent event) -> setZoom(3.0));
1979        zoomButtonGroup.add(zoom30Item);
1980
1981        zoomMenu.add(zoom40Item);
1982        zoom40Item.addActionListener((ActionEvent event) -> setZoom(4.0));
1983        zoomButtonGroup.add(zoom40Item);
1984
1985        zoomMenu.add(zoom50Item);
1986        zoom50Item.addActionListener((ActionEvent event) -> setZoom(5.0));
1987        zoomButtonGroup.add(zoom50Item);
1988
1989        zoomMenu.add(zoom60Item);
1990        zoom60Item.addActionListener((ActionEvent event) -> setZoom(6.0));
1991        zoomButtonGroup.add(zoom60Item);
1992
1993        zoomMenu.add(zoom70Item);
1994        zoom70Item.addActionListener((ActionEvent event) -> setZoom(7.0));
1995        zoomButtonGroup.add(zoom70Item);
1996
1997        zoomMenu.add(zoom80Item);
1998        zoom80Item.addActionListener((ActionEvent event) -> setZoom(8.0));
1999        zoomButtonGroup.add(zoom80Item);
2000
2001        // note: because this LayoutEditor object was just instantiated its
2002        // zoom attribute is 1.0; if it's being instantiated from an XML file
2003        // that has a zoom attribute for this object then setZoom will be
2004        // called after this method returns and we'll select the appropriate
2005        // menu item then.
2006        noZoomItem.setSelected(true);
2007
2008        // Note: We have to invoke this stuff later because _targetPanel is not setup yet
2009        SwingUtilities.invokeLater(() -> {
2010            // get the window specific saved zoom user preference
2011            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
2012                Object zoomProp = prefsMgr.getProperty(getWindowFrameRef(), "zoom");
2013                log.debug("{} zoom is {}", getWindowFrameRef(), zoomProp);
2014                if (zoomProp != null) {
2015                    setZoom((Double) zoomProp);
2016                }
2017            }
2018            );
2019
2020            // get the scroll bars from the scroll pane
2021            JScrollPane scrollPane = getPanelScrollPane();
2022            if (scrollPane != null) {
2023                JScrollBar hsb = scrollPane.getHorizontalScrollBar();
2024                JScrollBar vsb = scrollPane.getVerticalScrollBar();
2025
2026                // Increase scroll bar unit increments!!!
2027                vsb.setUnitIncrement(gContext.getGridSize());
2028                hsb.setUnitIncrement(gContext.getGridSize());
2029
2030                // add scroll bar adjustment listeners
2031                vsb.addAdjustmentListener(this::scrollBarAdjusted);
2032                hsb.addAdjustmentListener(this::scrollBarAdjusted);
2033
2034                // remove all mouse wheel listeners
2035                mouseWheelListeners = scrollPane.getMouseWheelListeners();
2036                for (MouseWheelListener mwl : mouseWheelListeners) {
2037                    scrollPane.removeMouseWheelListener(mwl);
2038                }
2039
2040                // add my mouse wheel listener
2041                // (so mouseWheelMoved (below) will be called)
2042                scrollPane.addMouseWheelListener(this);
2043            }
2044        });
2045    }   // setupZoomMenu
2046
2047    private MouseWheelListener[] mouseWheelListeners;
2048
2049    // scroll bar listener to update x & y coordinates in toolbar on scroll
2050    public void scrollBarAdjusted(AdjustmentEvent event) {
2051        // log.warn("scrollBarAdjusted");
2052        if (isEditable()) {
2053            // get the location of the mouse
2054            PointerInfo mpi = MouseInfo.getPointerInfo();
2055            Point mouseLoc = mpi.getLocation();
2056            // convert to target panel coordinates
2057            SwingUtilities.convertPointFromScreen(mouseLoc, getTargetPanel());
2058            // correct for scaling...
2059            double theZoom = getZoom();
2060            xLoc = (int) (mouseLoc.getX() / theZoom);
2061            yLoc = (int) (mouseLoc.getY() / theZoom);
2062            dLoc = new Point2D.Double(xLoc, yLoc);
2063
2064            leToolBarPanel.setLocationText(dLoc);
2065        }
2066        adjustClip();
2067    }
2068
2069    private void adjustScrollBars() {
2070        // log.info("adjustScrollBars()");
2071
2072        // This is the bounds of what's on the screen
2073        JScrollPane scrollPane = getPanelScrollPane();
2074        Rectangle scrollBounds = scrollPane.getViewportBorderBounds();
2075        // log.info("  getViewportBorderBounds: {}", MathUtil.rectangle2DToString(scrollBounds));
2076
2077        // this is the size of the entire scaled layout panel
2078        Dimension targetPanelSize = getTargetPanelSize();
2079        // log.info("  getTargetPanelSize: {}", MathUtil.dimensionToString(targetPanelSize));
2080
2081        // double scale = getZoom();
2082        // determine the relative position of the current horizontal scrollbar
2083        JScrollBar horScroll = scrollPane.getHorizontalScrollBar();
2084        double oldX = horScroll.getValue();
2085        double oldMaxX = horScroll.getMaximum();
2086        double ratioX = (oldMaxX < 1) ? 0 : oldX / oldMaxX;
2087
2088        // calculate the new X maximum and value
2089        int panelWidth = (int) (targetPanelSize.getWidth());
2090        int scrollWidth = (int) scrollBounds.getWidth();
2091        int newMaxX = Math.max(panelWidth - scrollWidth, 0);
2092        int newX = (int) (newMaxX * ratioX);
2093        horScroll.setMaximum(newMaxX);
2094        horScroll.setValue(newX);
2095
2096        // determine the relative position of the current vertical scrollbar
2097        JScrollBar vertScroll = scrollPane.getVerticalScrollBar();
2098        double oldY = vertScroll.getValue();
2099        double oldMaxY = vertScroll.getMaximum();
2100        double ratioY = (oldMaxY < 1) ? 0 : oldY / oldMaxY;
2101
2102        // calculate the new X maximum and value
2103        int tempPanelHeight = (int) (targetPanelSize.getHeight());
2104        int tempScrollHeight = (int) scrollBounds.getHeight();
2105        int newMaxY = Math.max(tempPanelHeight - tempScrollHeight, 0);
2106        int newY = (int) (newMaxY * ratioY);
2107        vertScroll.setMaximum(newMaxY);
2108        vertScroll.setValue(newY);
2109
2110//        log.info("w: {}, x: {}, h: {}, y: {}", "" + newMaxX, "" + newX, "" + newMaxY, "" + newY);
2111        adjustClip();
2112    }
2113
2114    private void adjustClip() {
2115        // log.info("adjustClip()");
2116
2117        // This is the bounds of what's on the screen
2118        JScrollPane scrollPane = getPanelScrollPane();
2119        Rectangle scrollBounds = scrollPane.getViewportBorderBounds();
2120        // log.info("  ViewportBorderBounds: {}", MathUtil.rectangle2DToString(scrollBounds));
2121
2122        JScrollBar horScroll = scrollPane.getHorizontalScrollBar();
2123        int scrollX = horScroll.getValue();
2124        JScrollBar vertScroll = scrollPane.getVerticalScrollBar();
2125        int scrollY = vertScroll.getValue();
2126
2127        Rectangle2D newClipRect = MathUtil.offset(
2128                scrollBounds,
2129                scrollX - scrollBounds.getMinX(),
2130                scrollY - scrollBounds.getMinY());
2131        newClipRect = MathUtil.scale(newClipRect, 1.0 / getZoom());
2132        newClipRect = MathUtil.granulize(newClipRect, 1.0); // round to nearest pixel
2133        layoutEditorComponent.setClip(newClipRect);
2134
2135        redrawPanel();
2136    }
2137
2138    @Override
2139    public void mouseWheelMoved(@Nonnull MouseWheelEvent event) {
2140        // log.warn("mouseWheelMoved");
2141        if (event.isAltDown()) {
2142            // get the mouse position from the event and convert to target panel coordinates
2143            Component component = (Component) event.getSource();
2144            Point eventPoint = event.getPoint();
2145            JComponent targetPanel = getTargetPanel();
2146            Point2D mousePoint = SwingUtilities.convertPoint(component, eventPoint, targetPanel);
2147
2148            // get the old view port position
2149            JScrollPane scrollPane = getPanelScrollPane();
2150            JViewport viewPort = scrollPane.getViewport();
2151            Point2D viewPosition = viewPort.getViewPosition();
2152
2153            // convert from oldZoom (scaled) coordinates to image coordinates
2154            double zoom = getZoom();
2155            Point2D imageMousePoint = MathUtil.divide(mousePoint, zoom);
2156            Point2D imageViewPosition = MathUtil.divide(viewPosition, zoom);
2157            // compute the delta (in image coordinates)
2158            Point2D imageDelta = MathUtil.subtract(imageMousePoint, imageViewPosition);
2159
2160            // compute how much to change zoom
2161            double amount = Math.pow(1.1, event.getScrollAmount());
2162            if (event.getWheelRotation() < 0.0) {
2163                // reciprocal for zoom out
2164                amount = 1.0 / amount;
2165            }
2166            // set the new zoom
2167            double newZoom = setZoom(zoom * amount);
2168            // recalulate the amount (in case setZoom didn't zoom as much as we wanted)
2169            amount = newZoom / zoom;
2170
2171            // convert the old delta to the new
2172            Point2D newImageDelta = MathUtil.divide(imageDelta, amount);
2173            // calculate the new view position (in image coordinates)
2174            Point2D newImageViewPosition = MathUtil.subtract(imageMousePoint, newImageDelta);
2175            // convert from image coordinates to newZoom (scaled) coordinates
2176            Point2D newViewPosition = MathUtil.multiply(newImageViewPosition, newZoom);
2177
2178            // don't let origin go negative
2179            newViewPosition = MathUtil.max(newViewPosition, MathUtil.zeroPoint2D);
2180            // log.info("mouseWheelMoved: newViewPos2D: {}", newViewPosition);
2181
2182            // set new view position
2183            viewPort.setViewPosition(MathUtil.point2DToPoint(newViewPosition));
2184        } else {
2185            JScrollPane scrollPane = getPanelScrollPane();
2186            if (scrollPane != null) {
2187                if (scrollPane.getVerticalScrollBar().isVisible()) {
2188                    // Redispatch the event to the original MouseWheelListeners
2189                    for (MouseWheelListener mwl : mouseWheelListeners) {
2190                        mwl.mouseWheelMoved(event);
2191                    }
2192                } else {
2193                    // proprogate event to ancestor
2194                    Component ancestor = SwingUtilities.getAncestorOfClass(JScrollPane.class,
2195                            scrollPane);
2196                    if (ancestor != null) {
2197                        MouseWheelEvent mwe = new MouseWheelEvent(
2198                                ancestor,
2199                                event.getID(),
2200                                event.getWhen(),
2201                                event.getModifiersEx(),
2202                                event.getX(),
2203                                event.getY(),
2204                                event.getXOnScreen(),
2205                                event.getYOnScreen(),
2206                                event.getClickCount(),
2207                                event.isPopupTrigger(),
2208                                event.getScrollType(),
2209                                event.getScrollAmount(),
2210                                event.getWheelRotation());
2211
2212                        ancestor.dispatchEvent(mwe);
2213                    }
2214                }
2215            }
2216        }
2217    }
2218
2219    /**
2220     * Select the appropriate zoom menu item based on the zoomFactor.
2221     * @param zoomFactor eg. 0.5 ( 1/2 zoom ), 1.0 ( no zoom ), 2.0 ( 2x zoom )
2222     */
2223    private void selectZoomMenuItem(double zoomFactor) {
2224        double zoom = zoomFactor * 100;
2225
2226        // put zoomFactor on 100% increments
2227        int newZoomFactor = (int) MathUtil.granulize(zoom, 100);
2228        noZoomItem.setSelected(newZoomFactor == 100);
2229        zoom20Item.setSelected(newZoomFactor == 200);
2230        zoom30Item.setSelected(newZoomFactor == 300);
2231        zoom40Item.setSelected(newZoomFactor == 400);
2232        zoom50Item.setSelected(newZoomFactor == 500);
2233        zoom60Item.setSelected(newZoomFactor == 600);
2234        zoom70Item.setSelected(newZoomFactor == 700);
2235        zoom80Item.setSelected(newZoomFactor == 800);
2236
2237        // put zoomFactor on 50% increments
2238        newZoomFactor = (int) MathUtil.granulize(zoom, 50);
2239        zoom05Item.setSelected(newZoomFactor == 50);
2240        zoom15Item.setSelected(newZoomFactor == 150);
2241
2242        // put zoomFactor on 25% increments
2243        newZoomFactor = (int) MathUtil.granulize(zoom, 25);
2244        zoom025Item.setSelected(newZoomFactor == 25);
2245        zoom075Item.setSelected(newZoomFactor == 75);
2246    }
2247
2248    /**
2249     * Set panel Zoom factor.
2250     * @param zoomFactor the amount to scale, eg. 2.0 for 2x zoom.
2251     * @return the new scale amount (not necessarily the same as zoomFactor)
2252     */
2253    public double setZoom(double zoomFactor) {
2254        double newZoom = MathUtil.pin(zoomFactor, minZoom, maxZoom);
2255        selectZoomMenuItem(newZoom);
2256
2257        if (!MathUtil.equals(newZoom, getPaintScale())) {
2258            log.debug("zoom: {}", zoomFactor);
2259            // setPaintScale(newZoom);   //<<== don't call; messes up scrollbars
2260            _paintScale = newZoom;      // just set paint scale directly
2261            resetTargetSize();          // calculate new target panel size
2262            adjustScrollBars();         // and adjust the scrollbars ourselves
2263            // adjustClip();
2264
2265            leToolBarPanel.zoomLabel.setText(String.format(Locale.getDefault(), "x%1$,.2f", newZoom));
2266
2267            // save the window specific saved zoom user preference
2268            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent( prefsMgr ->
2269                prefsMgr.setProperty(getWindowFrameRef(), "zoom", zoomFactor));
2270        }
2271        return getPaintScale();
2272    }
2273
2274    /**
2275     * getZoom
2276     *
2277     * @return the zooming scale
2278     */
2279    public double getZoom() {
2280        return getPaintScale();
2281    }
2282
2283    /**
2284     * getMinZoom
2285     *
2286     * @return the minimum zoom scale
2287     */
2288    public double getMinZoom() {
2289        return minZoom;
2290    }
2291
2292    /**
2293     * getMaxZoom
2294     *
2295     * @return the maximum zoom scale
2296     */
2297    public double getMaxZoom() {
2298        return maxZoom;
2299    }
2300
2301    //
2302    // TODO: make this public? (might be useful!)
2303    //
2304    private Rectangle2D calculateMinimumLayoutBounds() {
2305        // calculate a union of the bounds of everything on the layout
2306        Rectangle2D result = new Rectangle2D.Double();
2307
2308        // combine all (onscreen) Components into a list of list of Components
2309        List<List<? extends Component>> listOfListsOfComponents = new ArrayList<>();
2310        listOfListsOfComponents.add(backgroundImage);
2311        listOfListsOfComponents.add(sensorImage);
2312        listOfListsOfComponents.add(turnoutImage);
2313        listOfListsOfComponents.add(signalHeadImage);
2314        listOfListsOfComponents.add(markerImage);
2315        listOfListsOfComponents.add(labelImage);
2316        listOfListsOfComponents.add(clocks);
2317        listOfListsOfComponents.add(multiSensors);
2318        listOfListsOfComponents.add(signalList);
2319        listOfListsOfComponents.add(memoryLabelList);
2320        listOfListsOfComponents.add(memoryInputList);
2321        listOfListsOfComponents.add(globalVariableLabelList);
2322        listOfListsOfComponents.add(blockContentsLabelList);
2323        listOfListsOfComponents.add(blockContentsInputList);
2324        listOfListsOfComponents.add(sensorList);
2325        listOfListsOfComponents.add(turnoutList);
2326        listOfListsOfComponents.add(signalMastList);
2327        // combine their bounds
2328        for (List<? extends Component> listOfComponents : listOfListsOfComponents) {
2329            for (Component o : listOfComponents) {
2330                if (result.isEmpty()) {
2331                    result = o.getBounds();
2332                } else {
2333                    result = result.createUnion(o.getBounds());
2334                }
2335            }
2336        }
2337
2338        for (LayoutTrackView ov : getLayoutTrackViews()) {
2339            if (result.isEmpty()) {
2340                result = ov.getBounds();
2341            } else {
2342                result = result.createUnion(ov.getBounds());
2343            }
2344        }
2345
2346        for (LayoutShape o : layoutShapes) {
2347            if (result.isEmpty()) {
2348                result = o.getBounds();
2349            } else {
2350                result = result.createUnion(o.getBounds());
2351            }
2352        }
2353
2354        // put a grid size margin around it
2355        result = MathUtil.inset(result, gContext.getGridSize() * gContext.getGridSize2nd() / -2.0);
2356
2357        return result;
2358    }
2359
2360    /**
2361     * resize panel bounds
2362     *
2363     * @param forceFlag if false only grow bigger
2364     * @return the new (?) panel bounds
2365     */
2366    private Rectangle2D resizePanelBounds(boolean forceFlag) {
2367        Rectangle2D panelBounds = getPanelBounds();
2368        Rectangle2D layoutBounds = calculateMinimumLayoutBounds();
2369
2370        // make sure it includes the origin
2371        layoutBounds.add(MathUtil.zeroPoint2D);
2372
2373        if (forceFlag) {
2374            panelBounds = layoutBounds;
2375        } else {
2376            panelBounds.add(layoutBounds);
2377        }
2378
2379        // don't let origin go negative
2380        panelBounds = panelBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2381
2382        // log.info("resizePanelBounds: {}", MathUtil.rectangle2DToString(panelBounds));
2383        setPanelBounds(panelBounds);
2384
2385        return panelBounds;
2386    }
2387
2388    private double zoomToFit() {
2389        Rectangle2D layoutBounds = resizePanelBounds(true);
2390
2391        // calculate the bounds for the scroll pane
2392        JScrollPane scrollPane = getPanelScrollPane();
2393        Rectangle2D scrollBounds = scrollPane.getViewportBorderBounds();
2394
2395        // don't let origin go negative
2396        scrollBounds = scrollBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2397
2398        // calculate the horzontial and vertical scales
2399        double scaleWidth = scrollPane.getWidth() / layoutBounds.getWidth();
2400        double scaleHeight = scrollPane.getHeight() / layoutBounds.getHeight();
2401
2402        // set the new zoom to the smallest of the two
2403        double result = setZoom(Math.min(scaleWidth, scaleHeight));
2404
2405        // set the new zoom (return value may be different)
2406        result = setZoom(result);
2407
2408        // calculate new scroll bounds
2409        scrollBounds = MathUtil.scale(layoutBounds, result);
2410
2411        // don't let origin go negative
2412        scrollBounds = scrollBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2413
2414        // make sure it includes the origin
2415        scrollBounds.add(MathUtil.zeroPoint2D);
2416
2417        // and scroll to it
2418        scrollPane.scrollRectToVisible(MathUtil.rectangle2DToRectangle(scrollBounds));
2419
2420        return result;
2421    }
2422
2423    private Point2D windowCenter() {
2424        // Returns window's center coordinates converted to layout space
2425        // Used for initial setup of turntables and reporters
2426        return MathUtil.divide(MathUtil.center(getBounds()), getZoom());
2427    }
2428
2429    private void setupMarkerMenu(@Nonnull JMenuBar menuBar) {
2430        JMenu markerMenu = new JMenu(Bundle.getMessage("MenuMarker"));
2431
2432        markerMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuMarkerMnemonic")));
2433        menuBar.add(markerMenu);
2434        markerMenu.add(new AbstractAction(Bundle.getMessage("AddLoco") + "...") {
2435            @Override
2436            public void actionPerformed(ActionEvent event) {
2437                locoMarkerFromInput();
2438            }
2439        });
2440        markerMenu.add(new AbstractAction(Bundle.getMessage("AddLocoRoster") + "...") {
2441            @Override
2442            public void actionPerformed(ActionEvent event) {
2443                locoMarkerFromRoster();
2444            }
2445        });
2446        markerMenu.add(new AbstractAction(Bundle.getMessage("RemoveMarkers")) {
2447            @Override
2448            public void actionPerformed(ActionEvent event) {
2449                removeMarkers();
2450            }
2451        });
2452    }
2453
2454    private void setupDispatcherMenu(@Nonnull JMenuBar menuBar) {
2455        JMenu dispMenu = new JMenu(Bundle.getMessage("MenuDispatcher"));
2456
2457        dispMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuDispatcherMnemonic")));
2458        dispMenu.add(new JMenuItem(new DispatcherAction(Bundle.getMessage("MenuItemOpen"))));
2459        menuBar.add(dispMenu);
2460        JMenuItem newTrainItem = new JMenuItem(Bundle.getMessage("MenuItemNewTrain"));
2461        dispMenu.add(newTrainItem);
2462        newTrainItem.addActionListener((ActionEvent event) -> {
2463            if (InstanceManager.getDefault(TransitManager.class).getNamedBeanSet().isEmpty()) {
2464                // Inform the user that there are no Transits available, and don't open the window
2465                JmriJOptionPane.showMessageDialog(
2466                        null,
2467                        ResourceBundle.getBundle("jmri.jmrit.dispatcher.DispatcherBundle").
2468                                getString("NoTransitsMessage"));
2469            } else {
2470                DispatcherFrame df = InstanceManager.getDefault(DispatcherFrame.class
2471                );
2472                if (!df.getNewTrainActive()) {
2473                    df.getActiveTrainFrame().initiateTrain(event, null, null);
2474                    df.setNewTrainActive(true);
2475                } else {
2476                    df.getActiveTrainFrame().showActivateFrame(null);
2477                }
2478            }
2479        });
2480        menuBar.add(dispMenu);
2481    }
2482
2483    private boolean includedTurnoutSkipped = false;
2484
2485    public boolean isIncludedTurnoutSkipped() {
2486        return includedTurnoutSkipped;
2487    }
2488
2489    public void setIncludedTurnoutSkipped(Boolean boo) {
2490        includedTurnoutSkipped = boo;
2491    }
2492
2493    boolean openDispatcherOnLoad = false;
2494
2495    // TODO: Java standard pattern for boolean getters is "isOpenDispatcherOnLoad()"
2496    public boolean getOpenDispatcherOnLoad() {
2497        return openDispatcherOnLoad;
2498    }
2499
2500    public void setOpenDispatcherOnLoad(Boolean boo) {
2501        openDispatcherOnLoad = boo;
2502    }
2503
2504    /**
2505     * Remove marker icons from panel
2506     */
2507    @Override
2508    public void removeMarkers() {
2509        for (int i = markerImage.size(); i > 0; i--) {
2510            LocoIcon il = markerImage.get(i - 1);
2511
2512            if ((il != null) && (il.isActive())) {
2513                markerImage.remove(i - 1);
2514                il.remove();
2515                il.dispose();
2516                setDirty();
2517            }
2518        }
2519        super.removeMarkers();
2520        redrawPanel();
2521    }
2522
2523    /**
2524     * Assign the block from the toolbar to all selected layout tracks
2525     */
2526    private void assignBlockToSelection() {
2527        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
2528        if (newName == null) {
2529            newName = "";
2530        }
2531        LayoutBlock b = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(newName);
2532        _layoutTrackSelection.forEach((lt) -> lt.setAllLayoutBlocks(b));
2533    }
2534
2535    public boolean translateTrack(float xDel, float yDel) {
2536        Point2D delta = new Point2D.Double(xDel, yDel);
2537        getLayoutTrackViews().forEach((ltv) -> ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), delta)));
2538        resizePanelBounds(true);
2539        return true;
2540    }
2541
2542    /**
2543     * scale all LayoutTracks coordinates by the x and y factors.
2544     *
2545     * @param xFactor the amount to scale X coordinates.
2546     * @param yFactor the amount to scale Y coordinates.
2547     * @return true when complete.
2548     */
2549    public boolean scaleTrack(float xFactor, float yFactor) {
2550        getLayoutTrackViews().forEach((ltv) -> ltv.scaleCoords(xFactor, yFactor));
2551
2552        // update the overall scale factors
2553        gContext.setXScale(gContext.getXScale() * xFactor);
2554        gContext.setYScale(gContext.getYScale() * yFactor);
2555
2556        resizePanelBounds(true);
2557        return true;
2558    }
2559
2560    /**
2561     * loop through all LayoutBlocks and set colors to the default colors from
2562     * this LayoutEditor
2563     *
2564     * @return count of changed blocks
2565     */
2566    public int setAllTracksToDefaultColors() {
2567        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class
2568        );
2569        SortedSet<LayoutBlock> lBList = lbm.getNamedBeanSet();
2570        int changed = 0;
2571        for (LayoutBlock lb : lBList) {
2572            lb.setBlockTrackColor(this.getDefaultTrackColorColor());
2573            lb.setBlockOccupiedColor(this.getDefaultOccupiedTrackColorColor());
2574            lb.setBlockExtraColor(this.getDefaultAlternativeTrackColorColor());
2575            changed++;
2576        }
2577        log.info("Track Colors set to default values for {} layoutBlocks.", changed);
2578        return changed;
2579    }
2580
2581    private Rectangle2D undoRect;
2582    private boolean canUndoMoveSelection = false;
2583    private Point2D undoDelta = MathUtil.zeroPoint2D;
2584
2585    /**
2586     * Translate entire layout by x and y amounts.
2587     *
2588     * @param xTranslation horizontal (X) translation value
2589     * @param yTranslation vertical (Y) translation value
2590     */
2591    public void translate(float xTranslation, float yTranslation) {
2592        // here when all numbers read in - translation if entered
2593        if ((xTranslation != 0.0F) || (yTranslation != 0.0F)) {
2594            Point2D delta = new Point2D.Double(xTranslation, yTranslation);
2595            Rectangle2D selectionRect = getSelectionRect();
2596
2597            // set up undo information
2598            undoRect = MathUtil.offset(selectionRect, delta);
2599            undoDelta = MathUtil.subtract(MathUtil.zeroPoint2D, delta);
2600            canUndoMoveSelection = true;
2601            undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
2602
2603            // apply translation to icon items within the selection
2604            for (Positionable c : _positionableSelection) {
2605                Point2D newPoint = MathUtil.add(c.getLocation(), delta);
2606                c.setLocation((int) newPoint.getX(), (int) newPoint.getY());
2607            }
2608
2609            for (LayoutTrack lt : _layoutTrackSelection) {
2610                LayoutTrackView ltv = getLayoutTrackView(lt);
2611                ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), delta));
2612            }
2613
2614            for (LayoutShape ls : _layoutShapeSelection) {
2615                ls.setCoordsCenter(MathUtil.add(ls.getCoordsCenter(), delta));
2616            }
2617
2618            selectionX = undoRect.getX();
2619            selectionY = undoRect.getY();
2620            selectionWidth = undoRect.getWidth();
2621            selectionHeight = undoRect.getHeight();
2622            resizePanelBounds(false);
2623            setDirty();
2624            redrawPanel();
2625        }
2626    }
2627
2628    /**
2629     * undo the move selection
2630     */
2631    void undoMoveSelection() {
2632        if (canUndoMoveSelection) {
2633            _positionableSelection.forEach((c) -> {
2634                Point2D newPoint = MathUtil.add(c.getLocation(), undoDelta);
2635                c.setLocation((int) newPoint.getX(), (int) newPoint.getY());
2636            });
2637
2638            _layoutTrackSelection.forEach(
2639                    (lt) -> {
2640                        LayoutTrackView ltv = getLayoutTrackView(lt);
2641                        ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), undoDelta));
2642                    }
2643            );
2644
2645            _layoutShapeSelection.forEach((ls) -> ls.setCoordsCenter(MathUtil.add(ls.getCoordsCenter(), undoDelta)));
2646
2647            undoRect = MathUtil.offset(undoRect, undoDelta);
2648            selectionX = undoRect.getX();
2649            selectionY = undoRect.getY();
2650            selectionWidth = undoRect.getWidth();
2651            selectionHeight = undoRect.getHeight();
2652
2653            resizePanelBounds(false);
2654            redrawPanel();
2655
2656            canUndoMoveSelection = false;
2657            undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
2658        }
2659    }
2660
2661    /**
2662     * Rotate selection by 90 degrees clockwise.
2663     */
2664    public void rotateSelection90() {
2665        Rectangle2D bounds = getSelectionRect();
2666        Point2D center = MathUtil.midPoint(bounds);
2667
2668        for (Positionable positionable : _positionableSelection) {
2669            Rectangle2D cBounds = positionable.getBounds(new Rectangle());
2670            Point2D oldBottomLeft = new Point2D.Double(cBounds.getMinX(), cBounds.getMaxY());
2671            Point2D newTopLeft = MathUtil.rotateDEG(oldBottomLeft, center, 90);
2672            boolean rotateFlag = true;
2673            if (positionable instanceof PositionableLabel) {
2674                PositionableLabel positionableLabel = (PositionableLabel) positionable;
2675                if (positionableLabel.isBackground()) {
2676                    rotateFlag = false;
2677                }
2678            }
2679            if (rotateFlag) {
2680                positionable.rotate(positionable.getDegrees() + 90);
2681                positionable.setLocation((int) newTopLeft.getX(), (int) newTopLeft.getY());
2682            }
2683        }
2684
2685        for (LayoutTrack lt : _layoutTrackSelection) {
2686            LayoutTrackView ltv = getLayoutTrackView(lt);
2687            ltv.setCoordsCenter(MathUtil.rotateDEG(ltv.getCoordsCenter(), center, 90));
2688            ltv.rotateCoords(90);
2689        }
2690
2691        for (LayoutShape ls : _layoutShapeSelection) {
2692            ls.setCoordsCenter(MathUtil.rotateDEG(ls.getCoordsCenter(), center, 90));
2693            ls.rotateCoords(90);
2694        }
2695
2696        resizePanelBounds(true);
2697        setDirty();
2698        redrawPanel();
2699    }
2700
2701    /**
2702     * Rotate the entire layout by 90 degrees clockwise.
2703     */
2704    public void rotateLayout90() {
2705        List<Positionable> positionables = new ArrayList<>(getContents());
2706        positionables.addAll(backgroundImage);
2707        positionables.addAll(blockContentsLabelList);
2708        positionables.addAll(blockContentsInputList);
2709        positionables.addAll(labelImage);
2710        positionables.addAll(memoryLabelList);
2711        positionables.addAll(memoryInputList);
2712        positionables.addAll(globalVariableLabelList);
2713        positionables.addAll(sensorImage);
2714        positionables.addAll(turnoutImage);
2715        positionables.addAll(sensorList);
2716        positionables.addAll(turnoutList);
2717        positionables.addAll(signalHeadImage);
2718        positionables.addAll(signalList);
2719        positionables.addAll(signalMastList);
2720
2721        // do this to remove duplicates that may be in more than one list
2722        positionables = positionables.stream().distinct().collect(Collectors.toList());
2723
2724        Rectangle2D bounds = getPanelBounds();
2725        Point2D lowerLeft = new Point2D.Double(bounds.getMinX(), bounds.getMaxY());
2726
2727        for (Positionable positionable : positionables) {
2728            Rectangle2D cBounds = positionable.getBounds(new Rectangle());
2729            Point2D newTopLeft = MathUtil.subtract(MathUtil.rotateDEG(positionable.getLocation(), lowerLeft, 90), lowerLeft);
2730            boolean reLocateFlag = true;
2731            if (positionable instanceof PositionableLabel) {
2732                try {
2733                    PositionableLabel positionableLabel = (PositionableLabel) positionable;
2734                    if (positionableLabel.isBackground()) {
2735                        reLocateFlag = false;
2736                    }
2737                    positionableLabel.rotate(positionableLabel.getDegrees() + 90);
2738                } catch (NullPointerException ex) {
2739                    log.warn("previously-ignored NPE", ex);
2740                }
2741            }
2742            if (reLocateFlag) {
2743                try {
2744                    positionable.setLocation((int) (newTopLeft.getX() - cBounds.getHeight()), (int) newTopLeft.getY());
2745                } catch (NullPointerException ex) {
2746                    log.warn("previously-ignored NPE", ex);
2747                }
2748            }
2749        }
2750
2751        for (LayoutTrackView ltv : getLayoutTrackViews()) {
2752            try {
2753                Point2D newPoint = MathUtil.subtract(MathUtil.rotateDEG(ltv.getCoordsCenter(), lowerLeft, 90), lowerLeft);
2754                ltv.setCoordsCenter(newPoint);
2755                ltv.rotateCoords(90);
2756            } catch (NullPointerException ex) {
2757                log.warn("previously-ignored NPE", ex);
2758            }
2759        }
2760
2761        for (LayoutShape ls : layoutShapes) {
2762            Point2D newPoint = MathUtil.subtract(MathUtil.rotateDEG(ls.getCoordsCenter(), lowerLeft, 90), lowerLeft);
2763            ls.setCoordsCenter(newPoint);
2764            ls.rotateCoords(90);
2765        }
2766
2767        resizePanelBounds(true);
2768        setDirty();
2769        redrawPanel();
2770    }
2771
2772    /**
2773     * align the layout to grid
2774     */
2775    public void alignLayoutToGrid() {
2776        // align to grid
2777        List<Positionable> positionables = new ArrayList<>(getContents());
2778        positionables.addAll(backgroundImage);
2779        positionables.addAll(blockContentsLabelList);
2780        positionables.addAll(labelImage);
2781        positionables.addAll(memoryLabelList);
2782        positionables.addAll(memoryInputList);
2783        positionables.addAll(globalVariableLabelList);
2784        positionables.addAll(sensorImage);
2785        positionables.addAll(turnoutImage);
2786        positionables.addAll(sensorList);
2787        positionables.addAll(turnoutList);
2788        positionables.addAll(signalHeadImage);
2789        positionables.addAll(signalList);
2790        positionables.addAll(signalMastList);
2791
2792        // do this to remove duplicates that may be in more than one list
2793        positionables = positionables.stream().distinct().collect(Collectors.toList());
2794        alignToGrid(positionables, getLayoutTracks(), layoutShapes);
2795    }
2796
2797    /**
2798     * align selection to grid
2799     */
2800    public void alignSelectionToGrid() {
2801        alignToGrid(_positionableSelection, _layoutTrackSelection, _layoutShapeSelection);
2802    }
2803
2804    private void alignToGrid(List<Positionable> positionables, List<LayoutTrack> tracks, List<LayoutShape> shapes) {
2805        for (Positionable positionable : positionables) {
2806            Point2D newLocation = MathUtil.granulize(positionable.getLocation(), gContext.getGridSize());
2807            positionable.setLocation((int) (newLocation.getX()), (int) newLocation.getY());
2808        }
2809        for (LayoutTrack lt : tracks) {
2810            LayoutTrackView ltv = getLayoutTrackView(lt);
2811            ltv.setCoordsCenter(MathUtil.granulize(ltv.getCoordsCenter(), gContext.getGridSize()));
2812            if (lt instanceof LayoutTurntable) {
2813                LayoutTurntable tt = (LayoutTurntable) lt;
2814                LayoutTurntableView ttv = getLayoutTurntableView(tt);
2815                for (LayoutTurntable.RayTrack rt : tt.getRayTrackList()) {
2816                    int rayIndex = rt.getConnectionIndex();
2817                    ttv.setRayCoordsIndexed(MathUtil.granulize(ttv.getRayCoordsIndexed(rayIndex), gContext.getGridSize()), rayIndex);
2818                }
2819            }
2820//             if (lt instanceof LayoutTraverser) {
2821//                 Placeholder comment:
2822//                 Do nothing since slot connection points are relative to the traverser center point.
2823//             }
2824        }
2825        for (LayoutShape ls : shapes) {
2826            ls.setCoordsCenter(MathUtil.granulize(ls.getCoordsCenter(), gContext.getGridSize()));
2827            for (int idx = 0; idx < ls.getNumberPoints(); idx++) {
2828                ls.setPoint(idx, MathUtil.granulize(ls.getPoint(idx), gContext.getGridSize()));
2829            }
2830        }
2831
2832        resizePanelBounds(true);
2833        setDirty();
2834        redrawPanel();
2835    }
2836
2837    public void setCurrentPositionAndSize() {
2838        // save current panel location and size
2839        Dimension dim = getSize();
2840
2841        // Compute window size based on LayoutEditor size
2842        gContext.setWindowHeight(dim.height);
2843        gContext.setWindowWidth(dim.width);
2844
2845        // Compute layout size based on LayoutPane size
2846        dim = getTargetPanelSize();
2847        gContext.setLayoutWidth((int) (dim.width / getZoom()));
2848        gContext.setLayoutHeight((int) (dim.height / getZoom()));
2849        adjustScrollBars();
2850
2851        Point pt = getLocationOnScreen();
2852        gContext.setUpperLeftY(pt.x);
2853        gContext.setUpperLeftY(pt.y);
2854
2855        log.debug("setCurrentPositionAndSize Position - {},{} WindowSize - {},{} PanelSize - {},{}", gContext.getUpperLeftX(), gContext.getUpperLeftY(), gContext.getWindowWidth(), gContext.getWindowHeight(), gContext.getLayoutWidth(), gContext.getLayoutHeight());
2856        setDirty();
2857    }
2858
2859    private JRadioButtonMenuItem addButtonGroupMenuEntry(
2860            @Nonnull JMenu inMenu,
2861            ButtonGroup inButtonGroup,
2862            final String inName,
2863            boolean inSelected,
2864            ActionListener inActionListener) {
2865        JRadioButtonMenuItem result = new JRadioButtonMenuItem(inName);
2866        if (inActionListener != null) {
2867            result.addActionListener(inActionListener);
2868        }
2869        if (inButtonGroup != null) {
2870            inButtonGroup.add(result);
2871        }
2872        result.setSelected(inSelected);
2873
2874        inMenu.add(result);
2875
2876        return result;
2877    }
2878
2879    private void addTurnoutCircleSizeMenuEntry(
2880            @Nonnull JMenu inMenu,
2881            @Nonnull String inName,
2882            final int inSize) {
2883        ActionListener a = (ActionEvent event) -> {
2884            if (getTurnoutCircleSize() != inSize) {
2885                setTurnoutCircleSize(inSize);
2886                setDirty();
2887                redrawPanel();
2888            }
2889        };
2890        addButtonGroupMenuEntry(inMenu,
2891                turnoutCircleSizeButtonGroup, inName,
2892                getTurnoutCircleSize() == inSize, a);
2893    }
2894
2895    private void setOptionMenuTurnoutCircleSize() {
2896        String tcs = Integer.toString(getTurnoutCircleSize());
2897        Enumeration<AbstractButton> e = turnoutCircleSizeButtonGroup.getElements();
2898        while (e.hasMoreElements()) {
2899            AbstractButton button = e.nextElement();
2900            String buttonName = button.getText();
2901            button.setSelected(buttonName.equals(tcs));
2902        }
2903    }
2904
2905    @Override
2906    public void setScroll(int state) {
2907        if (isEditable()) {
2908            // In edit mode the scroll bars are always displayed, however we will want to set the scroll for when we exit edit mode
2909            super.setScroll(Editor.SCROLL_BOTH);
2910            _scrollState = state;
2911        } else {
2912            super.setScroll(state);
2913        }
2914    }
2915
2916    /**
2917     * The LE xml load uses the string version of setScroll which went directly to
2918     * Editor.  The string version has been added here so that LE can set the scroll
2919     * selection.
2920     * @param value The new scroll value.
2921     */
2922    @Override
2923    public void setScroll(String value) {
2924        if (value != null) super.setScroll(value);
2925        scrollNoneMenuItem.setSelected(_scrollState == Editor.SCROLL_NONE);
2926        scrollBothMenuItem.setSelected(_scrollState == Editor.SCROLL_BOTH);
2927        scrollHorizontalMenuItem.setSelected(_scrollState == Editor.SCROLL_HORIZONTAL);
2928        scrollVerticalMenuItem.setSelected(_scrollState == Editor.SCROLL_VERTICAL);
2929    }
2930
2931    /**
2932     * Add a layout turntable at location specified
2933     *
2934     * @param pt x,y placement for turntable
2935     */
2936    public void addTurntable(@Nonnull Point2D pt) {
2937        // get unique name
2938        String name = finder.uniqueName("TUR", ++numLayoutTurntables);
2939        LayoutTurntable lt = new LayoutTurntable(name, this);
2940        LayoutTurntableView ltv = new LayoutTurntableView(lt, pt, this);
2941
2942        addLayoutTrack(lt, ltv);
2943
2944        lt.addRay(0.0);
2945        lt.addRay(90.0);
2946        lt.addRay(180.0);
2947        lt.addRay(270.0);
2948
2949        if (leToolBarPanel != null) {
2950            lt.setMainline(leToolBarPanel.mainlineTrack.isSelected());
2951            // check on layout block
2952            String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
2953            if (newName == null) {
2954                newName = "";
2955            }
2956            LayoutBlock b = provideLayoutBlock(newName);
2957
2958            if (b != null) {
2959                lt.setLayoutBlock(b);
2960
2961                // check on occupancy sensor
2962                String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
2963                if (sensorName == null) {
2964                    sensorName = "";
2965                }
2966
2967                if (!sensorName.isEmpty()) {
2968                    if (!validateSensor(sensorName, b, this)) {
2969                        b.setOccupancySensorName("");
2970                    } else {
2971                        leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
2972                    }
2973                }
2974            }
2975        }
2976        setDirty();
2977
2978    }     /**
2979     * Add a layout traverser at location specified
2980     *
2981     * @param pt x,y placement for traverser
2982     */
2983    public void addTraverser(@Nonnull Point2D pt) {
2984        // get unique name
2985        String name = finder.uniqueName("TRV", ++numLayoutTraversers);
2986        LayoutTraverser lt = new LayoutTraverser(name, this);
2987        LayoutTraverserView ltv = new LayoutTraverserView(lt, pt, this);
2988        addLayoutTrack(lt, ltv);
2989        // Initialise with a couple of tracks
2990        lt.addSlotPair();
2991        lt.addSlotPair();
2992
2993        if (leToolBarPanel != null) {
2994            lt.setMainline(leToolBarPanel.mainlineTrack.isSelected());
2995            // check on layout block
2996            String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
2997            if (newName == null) {
2998                newName = "";
2999            }
3000            LayoutBlock b = provideLayoutBlock(newName);
3001
3002            if (b != null) {
3003                lt.setLayoutBlock(b);
3004
3005                // check on occupancy sensor
3006                String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
3007                if (sensorName == null) {
3008                    sensorName = "";
3009                }
3010
3011                if (!sensorName.isEmpty()) {
3012                    if (!validateSensor(sensorName, b, this)) {
3013                        b.setOccupancySensorName("");
3014                    } else {
3015                        leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
3016                    }
3017                }
3018            }
3019        }
3020        setDirty();
3021    }
3022
3023    /**
3024     * Allow external trigger of re-drawHidden
3025     */
3026    @Override
3027    public void redrawPanel() {
3028        repaint();
3029    }
3030
3031    /**
3032     * Allow external set/reset of awaitingIconChange
3033     */
3034    public void setAwaitingIconChange() {
3035        awaitingIconChange = true;
3036    }
3037
3038    public void resetAwaitingIconChange() {
3039        awaitingIconChange = false;
3040    }
3041
3042    /**
3043     * Allow external reset of dirty bit
3044     */
3045    public void resetDirty() {
3046        setDirty(false);
3047        savedEditMode = isEditable();
3048        savedPositionable = allPositionable();
3049        savedControlLayout = allControlling();
3050        savedAnimatingLayout = isAnimating();
3051        savedShowHelpBar = getShowHelpBar();
3052    }
3053
3054    /**
3055     * Allow external set of dirty bit
3056     *
3057     * @param val true/false for panelChanged
3058     */
3059    public void setDirty(boolean val) {
3060        panelChanged = val;
3061    }
3062
3063    @Override
3064    public void setDirty() {
3065        setDirty(true);
3066    }
3067
3068    /**
3069     * Check the dirty state.
3070     *
3071     * @return true if panel has changed
3072     */
3073    @Override
3074    public boolean isDirty() {
3075        return panelChanged;
3076    }
3077
3078    /*
3079    * Get mouse coordinates and adjust for zoom.
3080    * <p>
3081    * Side effects on xLoc, yLoc and dLoc
3082     */
3083    @Nonnull
3084    private Point2D calcLocation(JmriMouseEvent event, int dX, int dY) {
3085        xLoc = (int) ((event.getX() + dX) / getZoom());
3086        yLoc = (int) ((event.getY() + dY) / getZoom());
3087        dLoc = new Point2D.Double(xLoc, yLoc);
3088        return dLoc;
3089    }
3090
3091    private Point2D calcLocation(JmriMouseEvent event) {
3092        return calcLocation(event, 0, 0);
3093    }
3094
3095    /**
3096     * Check for highlighting of cursor position.
3097     *
3098     * If in "highlight" mode, draw a square at the location of the
3099     * event. If there was already a square, just move its location.
3100     * In either case, redraw the panel so the previous square will
3101     * disappear and the new one will appear immediately.
3102     */
3103    private void checkHighlightCursor() {
3104        if (!isEditable() && highlightCursor) {
3105            // rectangle size based on turnout circle size: rectangle should
3106            // be bigger so it can more easily surround turnout on screen
3107            int halfSize = (int)(circleRadius) + 8;
3108            if (_highlightcomponent == null) {
3109                _highlightcomponent = new Rectangle(
3110                        xLoc - halfSize, yLoc - halfSize, halfSize * 2, halfSize * 2);
3111            } else {
3112                _highlightcomponent.setLocation(xLoc - halfSize, yLoc - halfSize);
3113            }
3114            redrawPanel();
3115        }
3116    }
3117
3118    /**
3119     * Check whether an input icon text field is or is becoming active.
3120     * This is based on:
3121     * - The event component is an input icon instance.
3122     * - The mouse event indicates a plain button press.
3123     * @param event The mouse event.
3124     * @return true when active.
3125     */
3126    private boolean isInputTextBox(JmriMouseEvent event) {
3127        if (!(event.getComponent() instanceof PositionableJPanel)) {
3128            return false;
3129        }
3130
3131        if (event.isAltDown() ||
3132                event.isControlDown() ||
3133                event.isMetaDown() ||
3134                event.isPopupTrigger() ||
3135                event.isShiftDown()) {
3136            return false;
3137        }
3138
3139        return true;
3140    }
3141
3142    /**
3143     * Handle a mouse pressed event
3144     * <p>
3145     * Side-effects on _anchorX, _anchorY,_lastX, _lastY, xLoc, yLoc, dLoc,
3146     * selectionActive, xLabel, yLabel
3147     *
3148     * @param event the JmriMouseEvent
3149     */
3150    @Override
3151    public void mousePressed(JmriMouseEvent event) {
3152        if (isInputTextBox(event)) {
3153            return;
3154        }
3155
3156        // initialize cursor position
3157        _anchorX = xLoc;
3158        _anchorY = yLoc;
3159        _lastX = _anchorX;
3160        _lastY = _anchorY;
3161        calcLocation(event);
3162
3163        checkHighlightCursor();
3164
3165        // TODO: Add command-click on nothing to pan view?
3166        if (isEditable()) {
3167            boolean prevSelectionActive = selectionActive;
3168            selectionActive = false;
3169            leToolBarPanel.setLocationText(dLoc);
3170
3171            if (event.isPopupTrigger()) {
3172                if (event.isMetaDown() || event.isAltDown()) {
3173                    // if requesting a popup and it might conflict with moving, delay the request to mouseReleased
3174                    delayedPopupTrigger = true;
3175                } else {
3176                    // no possible conflict with moving, display the popup now
3177                    showEditPopUps(event);
3178                }
3179            }
3180
3181            if (event.isMetaDown() || event.isAltDown()) {
3182                // If dragging an item, identify the item for mouseDragging
3183                selectedObject = null;
3184                selectedHitPointType = HitPointType.NONE;
3185
3186                if (findLayoutTracksHitPoint(dLoc)) {
3187                    selectedObject = foundTrack;
3188                    selectedHitPointType = foundHitPointType;
3189                    startDelta = MathUtil.subtract(foundLocation, dLoc);
3190                    foundTrack = null;
3191                    foundTrackView = null;
3192                } else {
3193                    // Track not hit, find any non LAYOUT_POS_LABEL objects.
3194                    CheckLabel: {
3195                        selectedObject = checkMarkerPopUps(dLoc);
3196                        if (selectedObject != null) {
3197                            selectedHitPointType = HitPointType.MARKER;
3198                            startDelta = MathUtil.subtract(((LocoIcon) selectedObject).getLocation(), dLoc);
3199                            break CheckLabel;
3200                        }
3201
3202                        selectedObject = checkClockPopUps(dLoc);
3203                        if (selectedObject != null) {
3204                            selectedHitPointType = HitPointType.LAYOUT_POS_JCOMP;
3205                            startDelta = MathUtil.subtract(((PositionableJComponent) selectedObject).getLocation(), dLoc);
3206                            break CheckLabel;
3207                        }
3208
3209                        selectedObject = checkMultiSensorPopUps(dLoc);
3210                        if (selectedObject != null) {
3211                            selectedHitPointType = HitPointType.MULTI_SENSOR;
3212                            startDelta = MathUtil.subtract(((MultiSensorIcon) selectedObject).getLocation(), dLoc);
3213                            break CheckLabel;
3214                        }
3215
3216                        selectedObject = checkJPanelPopUps(dLoc);
3217                        if (selectedObject != null) {
3218                            selectedHitPointType = HitPointType.LAYOUT_POS_JPNL;
3219                            startDelta = MathUtil.subtract(((PositionableJPanel) selectedObject).getLocation(), dLoc);
3220                        }
3221                    } // End CheckLabel
3222
3223                    if (selectedObject == null) {
3224                        // Specific objects were not found.
3225                        // The next group are potential LAYOUT_POS_LABEL objects.
3226                        selectedObject = checkSensorIconPopUps(dLoc);
3227                        if (selectedObject == null) {
3228                            selectedObject = checkTurnoutIconPopUps(dLoc);
3229                            if (selectedObject == null) {
3230                                selectedObject = checkSignalHeadIconPopUps(dLoc);
3231                                if (selectedObject == null) {
3232                                    selectedObject = checkLabelImagePopUps(dLoc);
3233                                    if (selectedObject == null) {
3234                                        selectedObject = checkSignalMastIconPopUps(dLoc);
3235                                    }
3236                                }
3237                            }
3238                        }
3239
3240                        if (selectedObject != null) {
3241                            selectedHitPointType = HitPointType.LAYOUT_POS_LABEL;
3242                            startDelta = MathUtil.subtract(((PositionableLabel) selectedObject).getLocation(), dLoc);
3243                            if (selectedObject instanceof MemoryIcon) {
3244                                MemoryIcon pm = (MemoryIcon) selectedObject;
3245
3246                                if (pm.getPopupUtility().getFixedWidth() == 0) {
3247                                    startDelta = new Point2D.Double((pm.getOriginalX() - dLoc.getX()),
3248                                            (pm.getOriginalY() - dLoc.getY()));
3249                                }
3250                            }
3251
3252                            if (selectedObject instanceof GlobalVariableIcon) {
3253                                GlobalVariableIcon pm = (GlobalVariableIcon) selectedObject;
3254
3255                                if (pm.getPopupUtility().getFixedWidth() == 0) {
3256                                    startDelta = new Point2D.Double((pm.getOriginalX() - dLoc.getX()),
3257                                            (pm.getOriginalY() - dLoc.getY()));
3258                                }
3259                            }
3260
3261                        } else {
3262                            // Still nothing found, look for background objects and then shape objects.
3263                            selectedObject = checkBackgroundPopUps(dLoc);
3264
3265                            if (selectedObject != null) {
3266                                selectedHitPointType = HitPointType.LAYOUT_POS_LABEL;
3267                                startDelta = MathUtil.subtract(((PositionableLabel) selectedObject).getLocation(), dLoc);
3268                            } else {
3269                                // dragging a shape?
3270                                ListIterator<LayoutShape> listIterator = layoutShapes.listIterator(layoutShapes.size());
3271                                // hit test in front to back order (reverse order of list)
3272                                while (listIterator.hasPrevious()) {
3273                                    LayoutShape ls = listIterator.previous();
3274                                    selectedHitPointType = ls.findHitPointType(dLoc, true);
3275                                    if (LayoutShape.isShapeHitPointType(selectedHitPointType)) {
3276                                        // log.warn("drag selectedObject: ", lt);
3277                                        selectedObject = ls;    // found one!
3278                                        beginLocation = dLoc;
3279                                        currentLocation = beginLocation;
3280                                        startDelta = MathUtil.zeroPoint2D;
3281                                        break;
3282                                    }
3283                                }
3284                            }
3285                        }
3286                    }
3287                }
3288            } else if (event.isShiftDown() && leToolBarPanel.trackButton.isSelected() && !event.isPopupTrigger()) {
3289                // starting a Track Segment, check for free connection point
3290                selectedObject = null;
3291
3292                if (findLayoutTracksHitPoint(dLoc, true)) {
3293                    // match to a free connection point
3294                    beginTrack = foundTrack;
3295                    beginHitPointType = foundHitPointType;
3296                    beginLocation = foundLocation;
3297                    // BUGFIX: prevents initial drawTrackSegmentInProgress to {0, 0}
3298                    currentLocation = beginLocation;
3299                } else {
3300                    // TODO: auto-add anchor point?
3301                    beginTrack = null;
3302                }
3303            } else if (event.isShiftDown() && leToolBarPanel.shapeButton.isSelected() && !event.isPopupTrigger()) {
3304                // adding or extending a shape
3305                selectedObject = null;  // assume we're adding...
3306                for (LayoutShape ls : layoutShapes) {
3307                    selectedHitPointType = ls.findHitPointType(dLoc, true);
3308                    if (HitPointType.isShapePointOffsetHitPointType(selectedHitPointType)) {
3309                        // log.warn("extend selectedObject: ", lt);
3310                        selectedObject = ls;    // nope, we're extending
3311                        beginLocation = dLoc;
3312                        currentLocation = beginLocation;
3313                        break;
3314                    }
3315                }
3316            } else if (!event.isShiftDown() && !event.isControlDown() && !event.isPopupTrigger()) {
3317                // check if controlling a turnout in edit mode
3318                selectedObject = null;
3319
3320                if (allControlling()) {
3321                    checkControls(false);
3322                }
3323                // initialize starting selection - cancel any previous selection rectangle
3324                selectionActive = true;
3325                selectionX = dLoc.getX();
3326                selectionY = dLoc.getY();
3327                selectionWidth = 0.0;
3328                selectionHeight = 0.0;
3329            }
3330
3331            if (prevSelectionActive) {
3332                redrawPanel();
3333            }
3334
3335        } else if (allControlling()
3336                && !event.isMetaDown() && !event.isPopupTrigger()
3337                && !event.isAltDown() && !event.isShiftDown() && !event.isControlDown()) {
3338            // not in edit mode - check if mouse is on a turnout (using wider search range)
3339            selectedObject = null;
3340            checkControls(true);
3341
3342        } else if ((event.isMetaDown() || event.isAltDown())
3343                && !event.isShiftDown() && !event.isControlDown()) {
3344            // Windows and Linux have meta down on right button press. This prevents isPopTrigger
3345            // reaching the next else-if.
3346
3347            // not in edit mode - check if moving a marker if there are any.  This applies to Windows, Linux and macOS.
3348            selectedObject = checkMarkerPopUps(dLoc);
3349            if (selectedObject != null) {
3350                selectedHitPointType = HitPointType.MARKER;
3351                startDelta = MathUtil.subtract(((LocoIcon) selectedObject).getLocation(), dLoc);
3352                log.debug("mousePressed: ++ MAC/Windows/Linux marker move request");
3353                if (SystemType.isLinux()) {
3354                    // Prepare for a marker popup if the marker move does not occur before mouseReleased.
3355                    // This is only needed for Linux.  Windows handles this in mouseClicked.
3356                    delayedPopupTrigger = true;
3357                    log.debug("mousePressed: ++ Linux marker popup delay");
3358                }
3359            }
3360            if (selectedObject == null) {
3361                selectedObject = checkBlockContentsPopUps(dLoc);
3362                if (selectedObject != null) {
3363                    selectedHitPointType = HitPointType.BLOCKCONTENTSICON;
3364                }
3365            }
3366
3367            // not in edit mode - check if a signal mast popup menu is being requested using Windows or Linux.
3368            var sm = checkSignalMastIconPopUps(dLoc);
3369            if (sm != null) {
3370                delayedPopupTrigger = true;
3371                log.debug("mousePressed: ++ Window/Linux mast popup delay");
3372             }
3373            if (selectedObject == null) {
3374                    selectedObject = checkBlockContentsPopUps(dLoc);
3375                    if (selectedObject != null) {
3376                        selectedHitPointType = HitPointType.BLOCKCONTENTSICON;
3377                    }
3378                }
3379
3380        } else if (event.isPopupTrigger() && !event.isShiftDown()) {
3381
3382            // not in edit mode - check if a marker popup menu is being requested using macOS.
3383            var lo = checkMarkerPopUps(dLoc);
3384            if (lo != null) {
3385                delayedPopupTrigger = true;
3386                log.debug("mousePressed: ++ MAC marker popup delay");
3387            }
3388
3389            // not in edit mode - check if a signal mast popup menu is being requested using macOS.
3390            var sm = checkSignalMastIconPopUps(dLoc);
3391            if (sm != null) {
3392                delayedPopupTrigger = true;
3393                log.debug("mousePressed: ++ MAC mast popup delay");
3394             }
3395
3396        }
3397
3398        if (!event.isPopupTrigger()) {
3399            List<Positionable> selections = getSelectedItems(event);
3400
3401            if (!selections.isEmpty()) {
3402                selections.get(0).doMousePressed(event);
3403            }
3404        }
3405
3406        requestFocusInWindow();
3407
3408    }   // mousePressed
3409
3410// this is a method to iterate over a list of lists of items
3411// calling the predicate tester.test on each one
3412// all matching items are then added to the resulting List
3413// note: currently unused; commented out to avoid findbugs warning
3414// private static List testEachItemInListOfLists(
3415//        @Nonnull List<List> listOfListsOfObjects,
3416//        @Nonnull Predicate<Object> tester) {
3417//    List result = new ArrayList<>();
3418//    for (List<Object> listOfObjects : listOfListsOfObjects) {
3419//        List<Object> l = listOfObjects.stream().filter(o -> tester.test(o)).collect(Collectors.toList());
3420//        result.addAll(l);
3421//    }
3422//    return result;
3423//}
3424// this is a method to iterate over a list of lists of items
3425// calling the predicate tester.test on each one
3426// and return the first one that matches
3427// TODO: make this public? (it is useful! ;-)
3428// note: currently unused; commented out to avoid findbugs warning
3429// private static Object findFirstMatchingItemInListOfLists(
3430//        @Nonnull List<List> listOfListsOfObjects,
3431//        @Nonnull Predicate<Object> tester) {
3432//    Object result = null;
3433//    for (List listOfObjects : listOfListsOfObjects) {
3434//        Optional<Object> opt = listOfObjects.stream().filter(o -> tester.test(o)).findFirst();
3435//        if (opt.isPresent()) {
3436//            result = opt.get();
3437//            break;
3438//        }
3439//    }
3440//    return result;
3441//}
3442    /**
3443     * Called by {@link #mousePressed} to determine if the mouse click was in a
3444     * turnout control location. If so, update selectedHitPointType and
3445     * selectedObject for use by {@link #mouseReleased}.
3446     * <p>
3447     * If there's no match, selectedObject is set to null and
3448     * selectedHitPointType is left referring to the results of the checking the
3449     * last track on the list.
3450     * <p>
3451     * Refers to the current value of {@link #getLayoutTracks()} and
3452     * {@link #dLoc}.
3453     *
3454     * @param useRectangles set true to use rectangle; false for circles.
3455     */
3456    private void checkControls(boolean useRectangles) {
3457        selectedObject = null;  // deliberate side-effect
3458        for (LayoutTrackView theTrackView : getLayoutTrackViews()) {
3459            selectedHitPointType = theTrackView.findHitPointType(dLoc, useRectangles); // deliberate side-effect
3460            if (HitPointType.isControlHitType(selectedHitPointType)) {
3461                selectedObject = theTrackView.getLayoutTrack(); // deliberate side-effect
3462                return;
3463            }
3464        }
3465    }
3466
3467    // This is a geometric search, and should be done with views.
3468    // Hence this form is inevitably temporary.
3469    //
3470    private boolean findLayoutTracksHitPoint(
3471            @Nonnull Point2D loc, boolean requireUnconnected) {
3472        return findLayoutTracksHitPoint(loc, requireUnconnected, null);
3473    }
3474
3475    // This is a geometric search, and should be done with views.
3476    // Hence this form is inevitably temporary.
3477    //
3478    // optional parameter requireUnconnected
3479    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc) {
3480        return findLayoutTracksHitPoint(loc, false, null);
3481    }
3482
3483    /**
3484     * Internal (private) method to find the track closest to a point, with some
3485     * modifiers to the search. The {@link #foundTrack} and
3486     * {@link #foundHitPointType} members are set from the search.
3487     * <p>
3488     * This is a geometric search, and should be done with views. Hence this
3489     * form is inevitably temporary.
3490     *
3491     * @param loc                Point to search from
3492     * @param requireUnconnected forwarded to {@link #getLayoutTrackView}; if
3493     *                           true, return only free connections
3494     * @param avoid              Don't return this track, keep searching. Note
3495     *                           that {@Link #selectedObject} is also always
3496     *                           avoided automatically
3497     * @returns true if values of {@link #foundTrack} and
3498     * {@link #foundHitPointType} correct; note they may have changed even if
3499     * false is returned.
3500     */
3501    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc,
3502            boolean requireUnconnected, @CheckForNull LayoutTrack avoid) {
3503        boolean result = false; // assume failure (pessimist!)
3504
3505        foundTrack = null;
3506        foundTrackView = null;
3507        foundHitPointType = HitPointType.NONE;
3508
3509        Optional<LayoutTrack> opt = getLayoutTracks().stream().filter(layoutTrack -> {  // != means can't (yet) loop over Views
3510            if ((layoutTrack != avoid) && (layoutTrack != selectedObject)) {
3511                foundHitPointType = getLayoutTrackView(layoutTrack).findHitPointType(loc, false, requireUnconnected);
3512            }
3513            return (HitPointType.NONE != foundHitPointType);
3514        }).findFirst();
3515
3516        LayoutTrack layoutTrack = null;
3517        if (opt.isPresent()) {
3518            layoutTrack = opt.get();
3519        }
3520
3521        if (layoutTrack != null) {
3522            foundTrack = layoutTrack;
3523            foundTrackView = this.getLayoutTrackView(layoutTrack);
3524
3525            // get screen coordinates
3526            foundLocation = foundTrackView.getCoordsForConnectionType(foundHitPointType);
3527            /// foundNeedsConnect = isDisconnected(foundHitPointType);
3528            result = true;
3529        }
3530        return result;
3531    }
3532
3533    private TrackSegment checkTrackSegmentPopUps(@Nonnull Point2D loc) {
3534        assert loc != null;
3535
3536        TrackSegment result = null;
3537
3538        // NOTE: Rather than calculate all the hit rectangles for all
3539        // the points below and test if this location is in any of those
3540        // rectangles just create a hit rectangle for the location and
3541        // see if any of the points below are in it instead...
3542        Rectangle2D r = layoutEditorControlCircleRectAt(loc);
3543
3544        // check Track Segments, if any
3545        for (TrackSegmentView tsv : getTrackSegmentViews()) {
3546            if (r.contains(tsv.getCentreSeg())) {
3547                result = tsv.getTrackSegment();
3548                break;
3549            }
3550        }
3551        return result;
3552    }
3553
3554    private PositionableLabel checkBackgroundPopUps(@Nonnull Point2D loc) {
3555        assert loc != null;
3556
3557        PositionableLabel result = null;
3558        // check background images, if any
3559        for (int i = backgroundImage.size() - 1; i >= 0; i--) {
3560            PositionableLabel b = backgroundImage.get(i);
3561            Rectangle2D r = b.getBounds();
3562            if (r.contains(loc)) {
3563                result = b;
3564                break;
3565            }
3566        }
3567        return result;
3568    }
3569
3570    private SensorIcon checkSensorIconPopUps(@Nonnull Point2D loc) {
3571        assert loc != null;
3572
3573        SensorIcon result = null;
3574        // check sensor images, if any
3575        for (int i = sensorImage.size() - 1; i >= 0; i--) {
3576            SensorIcon s = sensorImage.get(i);
3577            Rectangle2D r = s.getBounds();
3578            if (r.contains(loc)) {
3579                result = s;
3580            }
3581        }
3582        return result;
3583    }
3584
3585    private TurnoutIcon checkTurnoutIconPopUps(@Nonnull Point2D loc) {
3586        assert loc != null;
3587
3588        TurnoutIcon result = null;
3589        // check turnout images, if any
3590        for (int i = turnoutImage.size() - 1; i >= 0; i--) {
3591            TurnoutIcon s = turnoutImage.get(i);
3592            Rectangle2D r = s.getBounds();
3593            if (r.contains(loc)) {
3594                result = s;
3595            }
3596        }
3597        return result;
3598    }
3599
3600    private SignalHeadIcon checkSignalHeadIconPopUps(@Nonnull Point2D loc) {
3601        assert loc != null;
3602
3603        SignalHeadIcon result = null;
3604        // check signal head images, if any
3605        for (int i = signalHeadImage.size() - 1; i >= 0; i--) {
3606            SignalHeadIcon s = signalHeadImage.get(i);
3607            Rectangle2D r = s.getBounds();
3608            if (r.contains(loc)) {
3609                result = s;
3610                break;
3611            }
3612        }
3613        return result;
3614    }
3615
3616    private SignalMastIcon checkSignalMastIconPopUps(@Nonnull Point2D loc) {
3617        assert loc != null;
3618
3619        SignalMastIcon result = null;
3620        // check signal head images, if any
3621        for (int i = signalMastList.size() - 1; i >= 0; i--) {
3622            SignalMastIcon s = signalMastList.get(i);
3623            Rectangle2D r = s.getBounds();
3624            if (r.contains(loc)) {
3625                result = s;
3626                break;
3627            }
3628        }
3629        return result;
3630    }
3631
3632    private PositionableLabel checkLabelImagePopUps(@Nonnull Point2D loc) {
3633        assert loc != null;
3634
3635        PositionableLabel result = null;
3636        int level = 0;
3637
3638        for (int i = labelImage.size() - 1; i >= 0; i--) {
3639            PositionableLabel s = labelImage.get(i);
3640            double x = s.getX();
3641            double y = s.getY();
3642            double w = 10.0;
3643            double h = 5.0;
3644
3645            if (s.isIcon() || s.isRotated() || s.getPopupUtility().getOrientation() != PositionablePopupUtil.HORIZONTAL) {
3646                w = s.maxWidth();
3647                h = s.maxHeight();
3648            } else if (s.isText()) {
3649                h = s.getFont().getSize();
3650                w = (h * 2 * (s.getText().length())) / 3;
3651            }
3652
3653            Rectangle2D r = new Rectangle2D.Double(x, y, w, h);
3654            if (r.contains(loc)) {
3655                if (s.getDisplayLevel() >= level) {
3656                    // Check to make sure that we are returning the highest level label.
3657                    result = s;
3658                    level = s.getDisplayLevel();
3659                }
3660            }
3661        }
3662        return result;
3663    }
3664
3665    private PositionableJPanel checkJPanelPopUps(@Nonnull Point2D loc) {
3666        assert loc != null;
3667
3668        PositionableJPanel result = null;
3669        int level = 0;
3670
3671        for (int i = memoryInputList.size() - 1; i >= 0; i--) {
3672            PositionableJPanel s = memoryInputList.get(i);
3673            double x = s.getX();
3674            double y = s.getY();
3675            double w = s.getWidth();
3676            double h = s.getHeight();
3677
3678            Rectangle2D r = new Rectangle2D.Double(x, y, w, h);
3679
3680            if (r.contains(loc)) {
3681                if (s.getDisplayLevel() >= level) {
3682                    // Check to make sure that we are returning the highest level label.
3683                    result = s;
3684                    level = s.getDisplayLevel();
3685                }
3686            }
3687        }
3688
3689        if (result == null) {
3690            for (int i = blockContentsInputList.size() - 1; i >= 0; i--) {
3691                PositionableJPanel s = blockContentsInputList.get(i);
3692                double x = s.getX();
3693                double y = s.getY();
3694                double w = s.getWidth();
3695                double h = s.getHeight();
3696
3697                Rectangle2D r = new Rectangle2D.Double(x, y, w, h);
3698
3699                if (r.contains(loc)) {
3700                    if (s.getDisplayLevel() >= level) {
3701                        // Check to make sure that we are returning the highest level label.
3702                        result = s;
3703                        level = s.getDisplayLevel();
3704                    }
3705                }
3706            }
3707        }
3708
3709        return result;
3710    }
3711
3712    private AnalogClock2Display checkClockPopUps(@Nonnull Point2D loc) {
3713        assert loc != null;
3714
3715        AnalogClock2Display result = null;
3716        // check clocks, if any
3717        for (int i = clocks.size() - 1; i >= 0; i--) {
3718            AnalogClock2Display s = clocks.get(i);
3719            Rectangle2D r = s.getBounds();
3720            if (r.contains(loc)) {
3721                result = s;
3722                break;
3723            }
3724        }
3725        return result;
3726    }
3727
3728    private MultiSensorIcon checkMultiSensorPopUps(@Nonnull Point2D loc) {
3729        assert loc != null;
3730
3731        MultiSensorIcon result = null;
3732        // check multi sensor icons, if any
3733        for (int i = multiSensors.size() - 1; i >= 0; i--) {
3734            MultiSensorIcon s = multiSensors.get(i);
3735            Rectangle2D r = s.getBounds();
3736            if (r.contains(loc)) {
3737                result = s;
3738                break;
3739            }
3740        }
3741        return result;
3742    }
3743
3744    private LocoIcon checkMarkerPopUps(@Nonnull Point2D loc) {
3745        assert loc != null;
3746
3747        LocoIcon result = null;
3748        // check marker icons, if any
3749        for (int i = markerImage.size() - 1; i >= 0; i--) {
3750            LocoIcon l = markerImage.get(i);
3751            Rectangle2D r = l.getBounds();
3752            if (r.contains(loc)) {
3753                // mouse was pressed in marker icon
3754                result = l;
3755                break;
3756            }
3757        }
3758        return result;
3759    }
3760
3761    private BlockContentsIcon checkBlockContentsPopUps(@Nonnull Point2D loc) {
3762        assert loc != null;
3763
3764        BlockContentsIcon result = null;
3765        // check marker icons, if any
3766        for (int i = blockContentsLabelList.size() - 1; i >= 0; i--) {
3767            BlockContentsIcon l = blockContentsLabelList.get(i);
3768            Rectangle2D r = l.getBounds();
3769            if (r.contains(loc)) {
3770                // mouse was pressed in marker icon
3771                result = l;
3772                break;
3773            }
3774        }
3775        return result;
3776    }
3777
3778    private LayoutShape checkLayoutShapePopUps(@Nonnull Point2D loc) {
3779        assert loc != null;
3780
3781        LayoutShape result = null;
3782        for (LayoutShape ls : layoutShapes) {
3783            selectedHitPointType = ls.findHitPointType(loc, true);
3784            if (LayoutShape.isShapeHitPointType(selectedHitPointType)) {
3785                result = ls;
3786                break;
3787            }
3788        }
3789        return result;
3790    }
3791
3792    /**
3793     * Get the coordinates for the connection type of the specified LayoutTrack
3794     * or subtype.
3795     * <p>
3796     * This uses the current LayoutEditor object to map a LayoutTrack (no
3797     * coordinates) object to _a_ specific LayoutTrackView object in the current
3798     * LayoutEditor i.e. window. This allows the same model object in two
3799     * windows, but not twice in a single window.
3800     * <p>
3801     * This is temporary, and needs to go away as the LayoutTrack doesn't
3802     * logically have position; just the LayoutTrackView does, and multiple
3803     * LayoutTrackViews can refer to one specific LayoutTrack.
3804     *
3805     * @param track          the object (LayoutTrack subclass)
3806     * @param connectionType the type of connection
3807     * @return the coordinates for the connection type of the specified object
3808     */
3809    @Nonnull
3810    public Point2D getCoords(LayoutTrack track, HitPointType connectionType) {
3811        if (track == null) {
3812            log.warn("track is null, HitPointType={}", connectionType);
3813        }
3814        LayoutTrack trk = Objects.requireNonNull(track);
3815        return getCoords(getLayoutTrackView(trk), connectionType);
3816    }
3817
3818    /**
3819     * Get the coordinates for the connection type of the specified
3820     * LayoutTrackView or subtype.
3821     *
3822     * @param trkView        the object (LayoutTrackView subclass)
3823     * @param connectionType the type of connection
3824     * @return the coordinates for the connection type of the specified object
3825     */
3826    @Nonnull
3827    public Point2D getCoords(@Nonnull LayoutTrackView trkView, HitPointType connectionType) {
3828        LayoutTrackView trkv = Objects.requireNonNull(trkView);
3829
3830        return trkv.getCoordsForConnectionType(connectionType);
3831    }
3832
3833    @Override
3834    public void mouseReleased(JmriMouseEvent event) {
3835        super.setToolTip(null);
3836
3837        if (isInputTextBox(event)) {
3838            return;
3839        }
3840
3841        // initialize mouse position
3842        calcLocation(event);
3843
3844        if (!isEditable() && _highlightcomponent != null && highlightCursor) {
3845            _highlightcomponent = null;
3846            // see if we moused up on an object
3847            checkControls(true);
3848            redrawPanel();
3849        }
3850
3851        // if alt modifier is down invert the snap to grid behaviour
3852        snapToGridInvert = event.isAltDown();
3853
3854        if (isEditable()) {
3855            leToolBarPanel.setLocationText(dLoc);
3856
3857            // released the mouse with shift down... see what we're adding
3858            if (!event.isPopupTrigger() && !event.isMetaDown() && event.isShiftDown()) {
3859
3860                currentPoint = new Point2D.Double(xLoc, yLoc);
3861
3862                if (snapToGridOnAdd != snapToGridInvert) {
3863                    // this snaps the current point to the grid
3864                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
3865                    xLoc = (int) currentPoint.getX();
3866                    yLoc = (int) currentPoint.getY();
3867                    leToolBarPanel.setLocationText(currentPoint);
3868                }
3869
3870                if (leToolBarPanel.turnoutRHButton.isSelected()) {
3871                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_TURNOUT);
3872                } else if (leToolBarPanel.turnoutLHButton.isSelected()) {
3873                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_TURNOUT);
3874                } else if (leToolBarPanel.turnoutWYEButton.isSelected()) {
3875                    addLayoutTurnout(LayoutTurnout.TurnoutType.WYE_TURNOUT);
3876                } else if (leToolBarPanel.doubleXoverButton.isSelected()) {
3877                    addLayoutTurnout(LayoutTurnout.TurnoutType.DOUBLE_XOVER);
3878                } else if (leToolBarPanel.rhXoverButton.isSelected()) {
3879                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_XOVER);
3880                } else if (leToolBarPanel.lhXoverButton.isSelected()) {
3881                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_XOVER);
3882                } else if (leToolBarPanel.levelXingButton.isSelected()) {
3883                    addLevelXing();
3884                } else if (leToolBarPanel.layoutSingleSlipButton.isSelected()) {
3885                    addLayoutSlip(LayoutSlip.TurnoutType.SINGLE_SLIP);
3886                } else if (leToolBarPanel.layoutDoubleSlipButton.isSelected()) {
3887                    addLayoutSlip(LayoutSlip.TurnoutType.DOUBLE_SLIP);
3888                } else if (leToolBarPanel.endBumperButton.isSelected()) {
3889                    addEndBumper();
3890                } else if (leToolBarPanel.anchorButton.isSelected()) {
3891                    addAnchor();
3892                } else if (leToolBarPanel.edgeButton.isSelected()) {
3893                    addEdgeConnector();
3894                } else if (leToolBarPanel.trackButton.isSelected()) {
3895                    if ((beginTrack != null) && (foundTrack != null)
3896                            && (beginTrack != foundTrack)) {
3897                        addTrackSegment();
3898                        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3899                    }
3900                    beginTrack = null;
3901                    foundTrack = null;
3902                    foundTrackView = null;
3903                } else if (leToolBarPanel.multiSensorButton.isSelected()) {
3904                    startMultiSensor();
3905                } else if (leToolBarPanel.sensorButton.isSelected()) {
3906                    addSensor();
3907                } else if (leToolBarPanel.turnoutButton.isSelected()) {
3908                    addTurnout();
3909                } else if (leToolBarPanel.signalButton.isSelected()) {
3910                    addSignalHead();
3911                } else if (leToolBarPanel.textLabelButton.isSelected()) {
3912                    addLabel();
3913                } else if (leToolBarPanel.memoryButton.isSelected()) {
3914                    selectMemoryType();
3915                } else if (leToolBarPanel.globalVariableButton.isSelected()) {
3916                    addGlobalVariable();
3917                } else if (leToolBarPanel.blockContentsButton.isSelected()) {
3918                    selectBlockContentsType();
3919                } else if (leToolBarPanel.iconLabelButton.isSelected()) {
3920                    addIcon();
3921                } else if (leToolBarPanel.logixngButton.isSelected()) {
3922                    addLogixNGIcon();
3923                } else if (leToolBarPanel.audioButton.isSelected()) {
3924                    addAudioIcon();
3925                } else if (leToolBarPanel.shapeButton.isSelected()) {
3926                    LayoutShape ls = (LayoutShape) selectedObject;
3927                    if (ls == null) {
3928                        ls = addLayoutShape(currentPoint);
3929                    } else {
3930                        ls.addPoint(currentPoint, selectedHitPointType.shapePointIndex());
3931                    }
3932                    unionToPanelBounds(ls.getBounds());
3933                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
3934                } else if (leToolBarPanel.signalMastButton.isSelected()) {
3935                    addSignalMast();
3936                } else if (leToolBarPanel.turntableButton.isSelected()) {
3937                    addTurntable(currentPoint);
3938                } else if (leToolBarPanel.traverserButton.isSelected()) {
3939                    addTraverser(currentPoint);
3940                } else {
3941                    log.warn("No item selected in panel edit mode");
3942                }
3943                // resizePanelBounds(false);
3944                selectedObject = null;
3945                redrawPanel();
3946            } else if ((event.isPopupTrigger() || delayedPopupTrigger) && !isDragging) {
3947                selectedObject = null;
3948                selectedHitPointType = HitPointType.NONE;
3949                whenReleased = event.getWhen();
3950                showEditPopUps(event);
3951            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
3952                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3953                    && !event.isShiftDown() && !event.isControlDown()) {
3954                // controlling turnouts, in edit mode
3955                LayoutTurnout t = (LayoutTurnout) selectedObject;
3956                t.toggleTurnout();
3957            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
3958                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3959                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3960                    && !event.isShiftDown() && !event.isControlDown()) {
3961                // controlling slips, in edit mode
3962                LayoutSlip sl = (LayoutSlip) selectedObject;
3963                sl.toggleState(selectedHitPointType);
3964            } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
3965                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3966                    && !event.isShiftDown() && !event.isControlDown()) {
3967                // controlling turntable, in edit mode
3968                LayoutTurntable t = (LayoutTurntable) selectedObject;
3969                t.setPosition(selectedHitPointType.turntableTrackIndex());
3970            } else if ((selectedObject != null) && (HitPointType.isTraverserSlotHitType(selectedHitPointType))
3971                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3972                    && !event.isShiftDown() && !event.isControlDown()) {
3973                // controlling Traverser, in edit mode
3974                LayoutTraverser t = (LayoutTraverser) selectedObject;
3975                t.setPosition(selectedHitPointType.traverserTrackIndex());
3976            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.TURNOUT_CENTER)
3977                    || (selectedHitPointType == HitPointType.SLIP_CENTER)
3978                    || (selectedHitPointType == HitPointType.SLIP_LEFT)
3979                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3980                    && allControlling() && (event.isMetaDown() && !event.isAltDown())
3981                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3982                // We just dropped a turnout (or slip)... see if it will connect to anything
3983                hitPointCheckLayoutTurnouts((LayoutTurnout) selectedObject);
3984            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.POS_POINT)
3985                    && allControlling() && (event.isMetaDown())
3986                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3987                // We just dropped a PositionablePoint... see if it will connect to anything
3988                PositionablePoint p = (PositionablePoint) selectedObject;
3989                if ((p.getConnect1() == null) || (p.getConnect2() == null)) {
3990                    checkPointOfPositionable(p);
3991                }
3992            }
3993
3994            if ((leToolBarPanel.trackButton.isSelected()) && (beginTrack != null) && (foundTrack != null)) {
3995                // user let up shift key before releasing the mouse when creating a track segment
3996                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3997                beginTrack = null;
3998                foundTrack = null;
3999                foundTrackView = null;
4000                redrawPanel();
4001            }
4002            createSelectionGroups();
4003        } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
4004                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
4005                && !event.isShiftDown() && (!delayedPopupTrigger)) {
4006            // controlling turnout out of edit mode
4007            LayoutTurnout t = (LayoutTurnout) selectedObject;
4008            if (useDirectTurnoutControl) {
4009                t.setState(Turnout.CLOSED);
4010            } else {
4011                t.toggleTurnout();
4012                if (highlightCursor && !t.isDisabled()) {
4013                    // flash the turnout circle a few times so the user knows it's being toggled
4014                    javax.swing.Timer timer = new javax.swing.Timer(150, null);
4015                    timer.addActionListener(new ActionListener(){
4016                        int count = 1;
4017                        @Override
4018                        public void actionPerformed(ActionEvent ae){
4019                          if(count % 2 != 0) t.setDisabled(true);
4020                          else t.setDisabled(false);
4021                          if(++count > 8) timer.stop();
4022                        }
4023                    });
4024                    timer.start();
4025                }
4026            }
4027        } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
4028                || (selectedHitPointType == HitPointType.SLIP_RIGHT))
4029                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
4030                && !event.isShiftDown() && (!delayedPopupTrigger)) {
4031            // controlling slip out of edit mode
4032            LayoutSlip sl = (LayoutSlip) selectedObject;
4033            sl.toggleState(selectedHitPointType);
4034        } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
4035                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
4036                && !event.isShiftDown() && (!delayedPopupTrigger)) {
4037            // controlling turntable out of edit mode
4038            LayoutTurntable t = (LayoutTurntable) selectedObject;
4039            t.setPosition(selectedHitPointType.turntableTrackIndex());
4040        } else if ((selectedObject != null) && (HitPointType.isTraverserSlotHitType(selectedHitPointType))
4041                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
4042                && !event.isShiftDown() && (!delayedPopupTrigger)) {
4043            // controlling traverser out of edit mode
4044            LayoutTraverser t = (LayoutTraverser) selectedObject;
4045            t.setPosition(selectedHitPointType.traverserTrackIndex());
4046        } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.BLOCKCONTENTSICON))
4047                && allControlling()  && !event.isAltDown() && !event.isPopupTrigger()
4048                && !event.isShiftDown() && (!delayedPopupTrigger)) {
4049            BlockContentsIcon t = (BlockContentsIcon) selectedObject;
4050            if (t != null) {
4051                showPopUp(t, event);
4052            }
4053        } else if ((event.isPopupTrigger() || delayedPopupTrigger) && (!isDragging)) {
4054            // requesting marker popup out of edit mode
4055            LocoIcon lo = checkMarkerPopUps(dLoc);
4056            if (lo != null) {
4057                showPopUp(lo, event);
4058            } else {
4059                if (findLayoutTracksHitPoint(dLoc)) {
4060                    // show popup menu
4061                    switch (foundHitPointType) {
4062                        case TURNOUT_CENTER: {
4063                            if (useDirectTurnoutControl) {
4064                                LayoutTurnout t = (LayoutTurnout) foundTrack;
4065                                t.setState(Turnout.THROWN);
4066                            } else {
4067                                foundTrackView.showPopup(event);
4068                            }
4069                            break;
4070                        }
4071
4072                        case LEVEL_XING_CENTER:
4073                        case SLIP_RIGHT:
4074                        case SLIP_LEFT: {
4075                            foundTrackView.showPopup(event);
4076                            break;
4077                        }
4078
4079                        default: {
4080                            break;
4081                        }
4082                    }
4083                }
4084                AnalogClock2Display c = checkClockPopUps(dLoc);
4085                if (c != null) {
4086                    showPopUp(c, event);
4087                } else {
4088                    SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
4089                    if (sm != null) {
4090                        showPopUp(sm, event);
4091                    } else {
4092                        PositionableLabel im = checkLabelImagePopUps(dLoc);
4093                        if (im != null) {
4094                            showPopUp(im, event);
4095                        }
4096                    }
4097                }
4098            }
4099        }
4100
4101        if (!event.isPopupTrigger() && !isDragging) {
4102            List<Positionable> selections = getSelectedItems(event);
4103            if (!selections.isEmpty()) {
4104                selections.get(0).doMouseReleased(event);
4105                whenReleased = event.getWhen();
4106            }
4107        }
4108
4109        // train icon needs to know when moved
4110        if (event.isPopupTrigger() && isDragging) {
4111            List<Positionable> selections = getSelectedItems(event);
4112            if (!selections.isEmpty()) {
4113                selections.get(0).doMouseDragged(event);
4114            }
4115        }
4116
4117        if (selectedObject != null) {
4118            // An object was selected, deselect it
4119            prevSelectedObject = selectedObject;
4120            selectedObject = null;
4121        }
4122
4123        // clear these
4124        beginTrack = null;
4125        foundTrack = null;
4126        foundTrackView = null;
4127
4128        delayedPopupTrigger = false;
4129
4130        if (isDragging) {
4131            resizePanelBounds(true);
4132            isDragging = false;
4133        }
4134
4135        requestFocusInWindow();
4136    }   // mouseReleased
4137
4138    public void addPopupItems(@Nonnull JPopupMenu popup, @Nonnull JmriMouseEvent event) {
4139
4140        List<LayoutTrack> tracks = getLayoutTracks().stream().filter(layoutTrack -> {  // != means can't (yet) loop over Views
4141            HitPointType hitPointType = getLayoutTrackView(layoutTrack).findHitPointType(dLoc, false, false);
4142            return (HitPointType.NONE != hitPointType);
4143        }).collect(Collectors.toList());
4144
4145        List<Positionable> selections = getSelectedItems(event);
4146
4147        if ((tracks.size() > 1) || (selections.size() > 1)) {
4148            JMenu iconsBelowMenu = new JMenu(Bundle.getMessage("MenuItemIconsBelow"));
4149
4150            JMenuItem mi = new JMenuItem(Bundle.getMessage("MenuItemIconsBelow_InfoNotInOrder"));
4151            mi.setEnabled(false);
4152            iconsBelowMenu.add(mi);
4153
4154            if (tracks.size() > 1) {
4155                for (int i=0; i < tracks.size(); i++) {
4156                    LayoutTrack t = tracks.get(i);
4157                    iconsBelowMenu.add(new AbstractAction(Bundle.getMessage(
4158                            "LayoutTrackTypeAndName", t.getTypeName(), t.getName())) {
4159                        @Override
4160                        public void actionPerformed(ActionEvent e) {
4161                            LayoutTrackView ltv = getLayoutTrackView(t);
4162                            ltv.showPopup(event);
4163                        }
4164                    });
4165                }
4166            }
4167            if (selections.size() > 1) {
4168                for (int i=0; i < selections.size(); i++) {
4169                    Positionable pos = selections.get(i);
4170                    iconsBelowMenu.add(new AbstractAction(Bundle.getMessage(
4171                            "PositionableTypeAndName", pos.getTypeString(), pos.getNameString())) {
4172                        @Override
4173                        public void actionPerformed(ActionEvent e) {
4174                            showPopUp(pos, event, new ArrayList<>());
4175                        }
4176                    });
4177                }
4178            }
4179            popup.addSeparator();
4180            popup.add(iconsBelowMenu);
4181        }
4182    }
4183
4184    private void showEditPopUps(@Nonnull JmriMouseEvent event) {
4185        if (findLayoutTracksHitPoint(dLoc)) {
4186            if (HitPointType.isBezierHitType(foundHitPointType)) {
4187                getTrackSegmentView((TrackSegment) foundTrack).showBezierPopUp(event, foundHitPointType);
4188            } else if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
4189                LayoutTurntable t = (LayoutTurntable) foundTrack;
4190                if (t.isTurnoutControlled()) {
4191                    LayoutTurntableView ltview = getLayoutTurntableView((LayoutTurntable) foundTrack);
4192                    ltview.showRayPopUp(event, foundHitPointType.turntableTrackIndex());
4193                }
4194            }else if (HitPointType.isTraverserSlotHitType(foundHitPointType)) {
4195                LayoutTraverser t = (LayoutTraverser) foundTrack;
4196                if (t.isTurnoutControlled()) {
4197                    LayoutTraverserView ltview = getLayoutTraverserView((LayoutTraverser) foundTrack);
4198                    ltview.showSlotPopUp(event, foundHitPointType.traverserTrackIndex());
4199                }
4200            } else if (HitPointType.isPopupHitType(foundHitPointType)) {
4201                foundTrackView.showPopup(event);
4202            } else if (HitPointType.isTurnoutHitType(foundHitPointType)) {
4203                // don't curently have edit popup for these
4204            } else {
4205                log.warn("Unknown foundPointType:{}", foundHitPointType);
4206            }
4207        } else {
4208            do {
4209                TrackSegment ts = checkTrackSegmentPopUps(dLoc);
4210                if (ts != null) {
4211                    TrackSegmentView tsv = getTrackSegmentView(ts);
4212                    tsv.showPopup(event);
4213                    break;
4214                }
4215
4216                SensorIcon s = checkSensorIconPopUps(dLoc);
4217                if (s != null) {
4218                    showPopUp(s, event);
4219                    break;
4220                }
4221
4222                TurnoutIcon t = checkTurnoutIconPopUps(dLoc);
4223                if (t != null) {
4224                    showPopUp(t, event);
4225                    break;
4226                }
4227
4228                LocoIcon lo = checkMarkerPopUps(dLoc);
4229                if (lo != null) {
4230                    showPopUp(lo, event);
4231                    break;
4232                }
4233
4234                SignalHeadIcon sh = checkSignalHeadIconPopUps(dLoc);
4235                if (sh != null) {
4236                    showPopUp(sh, event);
4237                    break;
4238                }
4239
4240                AnalogClock2Display c = checkClockPopUps(dLoc);
4241                if (c != null) {
4242                    showPopUp(c, event);
4243                    break;
4244                }
4245
4246                MultiSensorIcon ms = checkMultiSensorPopUps(dLoc);
4247                if (ms != null) {
4248                    showPopUp(ms, event);
4249                    break;
4250                }
4251
4252                PositionableLabel lb = checkLabelImagePopUps(dLoc);
4253                if (lb != null) {
4254                    showPopUp(lb, event);
4255                    break;
4256                }
4257
4258                PositionableLabel b = checkBackgroundPopUps(dLoc);
4259                if (b != null) {
4260                    showPopUp(b, event);
4261                    break;
4262                }
4263
4264                PositionableJPanel jp = checkJPanelPopUps(dLoc);
4265                if (jp != null) {
4266                    showPopUp(jp, event);
4267                    break;
4268                }
4269
4270                SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
4271                if (sm != null) {
4272                    showPopUp(sm, event);
4273                    break;
4274                }
4275                LayoutShape ls = checkLayoutShapePopUps(dLoc);
4276                if (ls != null) {
4277                    ls.showShapePopUp(event, selectedHitPointType);
4278                    break;
4279                }
4280            } while (false);
4281        }
4282    }
4283
4284    /**
4285     * Select the menu items to display for the Positionable's popup.
4286     * @param pos   the item containing or requiring the context menu
4287     * @param event the event triggering the menu
4288     */
4289    public void showPopUp(@Nonnull Positionable pos, @Nonnull JmriMouseEvent event) {
4290        Positionable p = Objects.requireNonNull(pos);
4291
4292        if (!((Component) p).isVisible()) {
4293            return; // component must be showing on the screen to determine its location
4294        }
4295        JPopupMenu popup = new JPopupMenu();
4296
4297        if (p.isEditable()) {
4298            JMenuItem jmi;
4299
4300            if (showAlignPopup()) {
4301                setShowAlignmentMenu(popup);
4302                popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
4303                    @Override
4304                    public void actionPerformed(ActionEvent event) {
4305                        deleteSelectedItems();
4306                    }
4307                });
4308            } else {
4309                if (p.doViemMenu()) {
4310                    String objectType = p.getClass().getName();
4311                    objectType = objectType.substring(objectType.lastIndexOf('.') + 1);
4312                    jmi = popup.add(objectType);
4313                    jmi.setEnabled(false);
4314
4315                    jmi = popup.add(p.getNameString());
4316                    jmi.setEnabled(false);
4317
4318                    if (p.isPositionable()) {
4319                        setShowCoordinatesMenu(p, popup);
4320                    }
4321                    setDisplayLevelMenu(p, popup);
4322                    setPositionableMenu(p, popup);
4323                }
4324
4325                boolean popupSet = false;
4326                popupSet |= p.setRotateOrthogonalMenu(popup);
4327                popupSet |= p.setRotateMenu(popup);
4328                popupSet |= p.setScaleMenu(popup);
4329                if (popupSet) {
4330                    popup.addSeparator();
4331                    popupSet = false;
4332                }
4333                // Don't show the icon menu item for the MemoryInputIcon
4334                if (!(p instanceof PositionableJPanel)) {
4335                    popupSet |= p.setEditIconMenu(popup);
4336                }
4337                popupSet |= p.setTextEditMenu(popup);
4338
4339                PositionablePopupUtil util = p.getPopupUtility();
4340
4341                if (util != null) {
4342                    util.setFixedTextMenu(popup);
4343                    util.setTextMarginMenu(popup);
4344                    util.setTextBorderMenu(popup);
4345                    util.setTextFontMenu(popup);
4346                    util.setBackgroundMenu(popup);
4347                    util.setTextJustificationMenu(popup);
4348                    util.setTextOrientationMenu(popup);
4349                    popup.addSeparator();
4350                    util.propertyUtil(popup);
4351                    util.setAdditionalEditPopUpMenu(popup);
4352                    popupSet = true;
4353                }
4354
4355                if (popupSet) {
4356                    popup.addSeparator();
4357                    // popupSet = false;
4358                }
4359                p.setDisableControlMenu(popup);
4360                setShowAlignmentMenu(popup);
4361
4362                // for Positionables with unique settings
4363                p.showPopUp(popup);
4364                setShowToolTipMenu(p, popup);
4365
4366                setRemoveMenu(p, popup);
4367
4368                if (p.doViemMenu()) {
4369                    setHiddenMenu(p, popup);
4370                    setEmptyHiddenMenu(p, popup);
4371                    setValueEditDisabledMenu(p, popup);
4372                    setEditIdMenu(p, popup);
4373                    setEditClassesMenu(p, popup);
4374                    popup.addSeparator();
4375                    setLogixNGPositionableMenu(p, popup);
4376                }
4377            }
4378        } else {
4379            p.showPopUp(popup);
4380            PositionablePopupUtil util = p.getPopupUtility();
4381
4382            if (util != null) {
4383                util.setAdditionalViewPopUpMenu(popup);
4384            }
4385        }
4386
4387        addPopupItems(popup, event);
4388
4389        popup.show((Component) p, p.getWidth() / 2 + (int) ((getZoom() - 1.0) * p.getX()),
4390                p.getHeight() / 2 + (int) ((getZoom() - 1.0) * p.getY()));
4391
4392        /*popup.show((Component)pt, event.getX(), event.getY());*/
4393    }
4394
4395    private long whenReleased = 0; // used to identify event that was popup trigger
4396    private boolean awaitingIconChange = false;
4397
4398    @Override
4399    public void mouseClicked(@Nonnull JmriMouseEvent event) {
4400        if (isInputTextBox(event)) {
4401            return;
4402        }
4403
4404        // initialize mouse position
4405        calcLocation(event);
4406
4407        if (!isEditable() && _highlightcomponent != null && highlightCursor) {
4408            _highlightcomponent = null;
4409            redrawPanel();
4410        }
4411
4412        // if alt modifier is down invert the snap to grid behaviour
4413        snapToGridInvert = event.isAltDown();
4414
4415        if (!event.isMetaDown() && !event.isPopupTrigger() && !event.isAltDown()
4416                && !awaitingIconChange && !event.isShiftDown() && !event.isControlDown()) {
4417            List<Positionable> selections = getSelectedItems(event);
4418
4419            if (!selections.isEmpty()) {
4420                selections.get(0).doMouseClicked(event);
4421            }
4422        } else if (event.isPopupTrigger() && (whenReleased != event.getWhen())) {
4423
4424            if (isEditable()) {
4425                selectedObject = null;
4426                selectedHitPointType = HitPointType.NONE;
4427                showEditPopUps(event);
4428            } else {
4429                LocoIcon lo = checkMarkerPopUps(dLoc);
4430
4431                if (lo != null) {
4432                    showPopUp(lo, event);
4433                }
4434            }
4435        }
4436
4437        if (event.isControlDown() && !event.isPopupTrigger()) {
4438            if (findLayoutTracksHitPoint(dLoc)) {
4439                switch (foundHitPointType) {
4440                    case POS_POINT:
4441                    case TURNOUT_CENTER:
4442                    case LEVEL_XING_CENTER:
4443                    case SLIP_LEFT:
4444                    case SLIP_RIGHT:
4445            case TURNTABLE_CENTER:
4446            case TRAVERSER_CENTER: {
4447                        amendSelectionGroup(foundTrack);
4448                        break;
4449                    }
4450
4451                    default: {
4452                        break;
4453                    }
4454                }
4455            } else {
4456                PositionableLabel s = checkSensorIconPopUps(dLoc);
4457                if (s != null) {
4458                    amendSelectionGroup(s);
4459                } else {
4460                    PositionableLabel t = checkTurnoutIconPopUps(dLoc);
4461                    if (t != null) {
4462                        amendSelectionGroup(t);
4463                    } else {
4464                        PositionableLabel sh = checkSignalHeadIconPopUps(dLoc);
4465                        if (sh != null) {
4466                            amendSelectionGroup(sh);
4467                        } else {
4468                            PositionableLabel ms = checkMultiSensorPopUps(dLoc);
4469                            if (ms != null) {
4470                                amendSelectionGroup(ms);
4471                            } else {
4472                                PositionableLabel lb = checkLabelImagePopUps(dLoc);
4473                                if (lb != null) {
4474                                    amendSelectionGroup(lb);
4475                                } else {
4476                                    PositionableLabel b = checkBackgroundPopUps(dLoc);
4477                                    if (b != null) {
4478                                        amendSelectionGroup(b);
4479                                    } else {
4480                                        PositionableLabel sm = checkSignalMastIconPopUps(dLoc);
4481                                        if (sm != null) {
4482                                            amendSelectionGroup(sm);
4483                                        } else {
4484                                            LayoutShape ls = checkLayoutShapePopUps(dLoc);
4485                                            if (ls != null) {
4486                                                amendSelectionGroup(ls);
4487                                            } else {
4488                                                PositionableJPanel jp = checkJPanelPopUps(dLoc);
4489                                                if (jp != null) {
4490                                                    amendSelectionGroup(jp);
4491                                                }
4492                                            }
4493                                        }
4494                                    }
4495                                }
4496                            }
4497                        }
4498                    }
4499                }
4500            }
4501        } else if ((selectionWidth == 0) || (selectionHeight == 0)) {
4502            clearSelectionGroups();
4503        }
4504        requestFocusInWindow();
4505    }
4506
4507    private void checkPointOfPositionable(@Nonnull PositionablePoint p) {
4508        assert p != null;
4509
4510        TrackSegment t = p.getConnect1();
4511
4512        if (t == null) {
4513            t = p.getConnect2();
4514        }
4515
4516        // Nothing connected to this bit of track so ignore
4517        if (t == null) {
4518            return;
4519        }
4520        beginTrack = p;
4521        beginHitPointType = HitPointType.POS_POINT;
4522        PositionablePointView pv = getPositionablePointView(p);
4523        Point2D loc = pv.getCoordsCenter();
4524
4525        if (findLayoutTracksHitPoint(loc, true, p)) {
4526            switch (foundHitPointType) {
4527                case POS_POINT: {
4528                    PositionablePoint p2 = (PositionablePoint) foundTrack;
4529
4530                    if ((p2.getType() == PositionablePoint.PointType.ANCHOR) && p2.setTrackConnection(t)) {
4531                        if (t.getConnect1() == p) {
4532                            t.setNewConnect1(p2, foundHitPointType);
4533                        } else {
4534                            t.setNewConnect2(p2, foundHitPointType);
4535                        }
4536                        p.removeTrackConnection(t);
4537
4538                        if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4539                            removePositionablePoint(p);
4540                        }
4541                    }
4542                    break;
4543                }
4544                case TURNOUT_A:
4545                case TURNOUT_B:
4546                case TURNOUT_C:
4547                case TURNOUT_D:
4548                case SLIP_A:
4549                case SLIP_B:
4550                case SLIP_C:
4551                case SLIP_D:
4552                case LEVEL_XING_A:
4553                case LEVEL_XING_B:
4554                case LEVEL_XING_C:
4555                case LEVEL_XING_D: {
4556                    try {
4557                        if (foundTrack.getConnection(foundHitPointType) == null) {
4558                            foundTrack.setConnection(foundHitPointType, t, HitPointType.TRACK);
4559
4560                            if (t.getConnect1() == p) {
4561                                t.setNewConnect1(foundTrack, foundHitPointType);
4562                            } else {
4563                                t.setNewConnect2(foundTrack, foundHitPointType);
4564                            }
4565                            p.removeTrackConnection(t);
4566
4567                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4568                                removePositionablePoint(p);
4569                            }
4570                        }
4571                    } catch (JmriException e) {
4572                        log.debug("Unable to set location");
4573                    }
4574                    break;
4575                }
4576
4577                default: {
4578                    if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
4579                        LayoutTurntable tt = (LayoutTurntable) foundTrack;
4580                        int ray = foundHitPointType.turntableTrackIndex();
4581
4582                        if (tt.getRayConnectIndexed(ray) == null) {
4583                            tt.setRayConnect(t, ray);
4584
4585                            if (t.getConnect1() == p) {
4586                                t.setNewConnect1(tt, foundHitPointType);
4587                            } else {
4588                                t.setNewConnect2(tt, foundHitPointType);
4589                            }
4590                            p.removeTrackConnection(t);
4591
4592                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4593                                removePositionablePoint(p);
4594                            }
4595                        }
4596                    } else if (HitPointType.isTraverserSlotHitType(foundHitPointType)) {
4597                        LayoutTraverser tt = (LayoutTraverser) foundTrack;
4598                        int slot = foundHitPointType.traverserTrackIndex();
4599
4600                        if (tt.getSlotConnectIndexed(slot) == null) {
4601                            tt.setSlotConnect(t, slot);
4602
4603                            if (t.getConnect1() == p) {
4604                                t.setNewConnect1(tt, foundHitPointType);
4605                            } else {
4606                                t.setNewConnect2(tt, foundHitPointType);
4607                            }
4608                            p.removeTrackConnection(t);
4609
4610                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4611                                removePositionablePoint(p);
4612                            }
4613                        }
4614                    } else {
4615                        log.debug("No valid point, so will quit");
4616                        return;
4617                    }
4618                    break;
4619                }
4620            }
4621            redrawPanel();
4622
4623            if (t.getLayoutBlock() != null) {
4624                getLEAuxTools().setBlockConnectivityChanged();
4625            }
4626        }
4627        beginTrack = null;
4628    }
4629
4630    // We just dropped a turnout... see if it will connect to anything
4631    private void hitPointCheckLayoutTurnouts(@Nonnull LayoutTurnout lt) {
4632        beginTrack = lt;
4633
4634        LayoutTurnoutView ltv = getLayoutTurnoutView(lt);
4635
4636        if (lt.getConnectA() == null) {
4637            if (lt instanceof LayoutSlip) {
4638                beginHitPointType = HitPointType.SLIP_A;
4639            } else {
4640                beginHitPointType = HitPointType.TURNOUT_A;
4641            }
4642            dLoc = ltv.getCoordsA();
4643            hitPointCheckLayoutTurnoutSubs(dLoc);
4644        }
4645
4646        if (lt.getConnectB() == null) {
4647            if (lt instanceof LayoutSlip) {
4648                beginHitPointType = HitPointType.SLIP_B;
4649            } else {
4650                beginHitPointType = HitPointType.TURNOUT_B;
4651            }
4652            dLoc = ltv.getCoordsB();
4653            hitPointCheckLayoutTurnoutSubs(dLoc);
4654        }
4655
4656        if (lt.getConnectC() == null) {
4657            if (lt instanceof LayoutSlip) {
4658                beginHitPointType = HitPointType.SLIP_C;
4659            } else {
4660                beginHitPointType = HitPointType.TURNOUT_C;
4661            }
4662            dLoc = ltv.getCoordsC();
4663            hitPointCheckLayoutTurnoutSubs(dLoc);
4664        }
4665
4666        if ((lt.getConnectD() == null) && (lt.isTurnoutTypeXover() || lt.isTurnoutTypeSlip())) {
4667            if (lt instanceof LayoutSlip) {
4668                beginHitPointType = HitPointType.SLIP_D;
4669            } else {
4670                beginHitPointType = HitPointType.TURNOUT_D;
4671            }
4672            dLoc = ltv.getCoordsD();
4673            hitPointCheckLayoutTurnoutSubs(dLoc);
4674        }
4675        beginTrack = null;
4676        foundTrack = null;
4677        foundTrackView = null;
4678    }
4679
4680    private void hitPointCheckLayoutTurnoutSubs(@Nonnull Point2D dLoc) {
4681        assert dLoc != null;
4682
4683        if (findLayoutTracksHitPoint(dLoc, true)) {
4684            switch (foundHitPointType) {
4685                case POS_POINT: {
4686                    PositionablePoint p2 = (PositionablePoint) foundTrack;
4687
4688                    if (((p2.getConnect1() == null) && (p2.getConnect2() != null))
4689                            || ((p2.getConnect1() != null) && (p2.getConnect2() == null))) {
4690                        TrackSegment t = p2.getConnect1();
4691
4692                        if (t == null) {
4693                            t = p2.getConnect2();
4694                        }
4695
4696                        if (t == null) {
4697                            return;
4698                        }
4699                        LayoutTurnout lt = (LayoutTurnout) beginTrack;
4700                        try {
4701                            if (lt.getConnection(beginHitPointType) == null) {
4702                                lt.setConnection(beginHitPointType, t, HitPointType.TRACK);
4703                                p2.removeTrackConnection(t);
4704
4705                                if (t.getConnect1() == p2) {
4706                                    t.setNewConnect1(lt, beginHitPointType);
4707                                } else {
4708                                    t.setNewConnect2(lt, beginHitPointType);
4709                                }
4710                                removePositionablePoint(p2);
4711                            }
4712
4713                            if (t.getLayoutBlock() != null) {
4714                                getLEAuxTools().setBlockConnectivityChanged();
4715                            }
4716                        } catch (JmriException e) {
4717                            log.debug("Unable to set location");
4718                        }
4719                    }
4720                    break;
4721                }
4722
4723                case TURNOUT_A:
4724                case TURNOUT_B:
4725                case TURNOUT_C:
4726                case TURNOUT_D:
4727                case SLIP_A:
4728                case SLIP_B:
4729                case SLIP_C:
4730                case SLIP_D: {
4731                    LayoutTurnout ft = (LayoutTurnout) foundTrack;
4732                    addTrackSegment();
4733
4734                    if ((ft.getTurnoutType() == LayoutTurnout.TurnoutType.RH_TURNOUT) || (ft.getTurnoutType() == LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4735                        rotateTurnout(ft);
4736                    }
4737
4738                    // Assign a block to the new zero length track segment.
4739                    ((LayoutTurnoutView) foundTrackView).setTrackSegmentBlock(foundHitPointType, true);
4740                    break;
4741                }
4742
4743                default: {
4744                    log.warn("Unexpected foundPointType {} in hitPointCheckLayoutTurnoutSubs", foundHitPointType);
4745                    break;
4746                }
4747            }
4748        }
4749    }
4750
4751    private void rotateTurnout(@Nonnull LayoutTurnout t) {
4752        assert t != null;
4753
4754        LayoutTurnoutView tv = getLayoutTurnoutView(t);
4755
4756        LayoutTurnout be = (LayoutTurnout) beginTrack;
4757        LayoutTurnoutView bev = getLayoutTurnoutView(be);
4758
4759        if (((beginHitPointType == HitPointType.TURNOUT_A) && ((be.getConnectB() != null) || (be.getConnectC() != null)))
4760                || ((beginHitPointType == HitPointType.TURNOUT_B) && ((be.getConnectA() != null) || (be.getConnectC() != null)))
4761                || ((beginHitPointType == HitPointType.TURNOUT_C) && ((be.getConnectB() != null) || (be.getConnectA() != null)))) {
4762            return;
4763        }
4764
4765        if ((be.getTurnoutType() != LayoutTurnout.TurnoutType.RH_TURNOUT) && (be.getTurnoutType() != LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4766            return;
4767        }
4768
4769        Point2D c, diverg, xy2;
4770
4771        if ((foundHitPointType == HitPointType.TURNOUT_C) && (beginHitPointType == HitPointType.TURNOUT_C)) {
4772            c = tv.getCoordsA();
4773            diverg = tv.getCoordsB();
4774            xy2 = MathUtil.subtract(c, diverg);
4775        } else if ((foundHitPointType == HitPointType.TURNOUT_C)
4776                && ((beginHitPointType == HitPointType.TURNOUT_A) || (beginHitPointType == HitPointType.TURNOUT_B))) {
4777
4778            c = tv.getCoordsCenter();
4779            diverg = tv.getCoordsC();
4780
4781            if (beginHitPointType == HitPointType.TURNOUT_A) {
4782                xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4783            } else {
4784                xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4785            }
4786        } else if (foundHitPointType == HitPointType.TURNOUT_B) {
4787            c = tv.getCoordsA();
4788            diverg = tv.getCoordsB();
4789
4790            switch (beginHitPointType) {
4791                case TURNOUT_B:
4792                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4793                    break;
4794                case TURNOUT_A:
4795                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4796                    break;
4797                case TURNOUT_C:
4798                default:
4799                    xy2 = MathUtil.subtract(bev.getCoordsCenter(), bev.getCoordsC());
4800                    break;
4801            }
4802        } else if (foundHitPointType == HitPointType.TURNOUT_A) {
4803            c = tv.getCoordsA();
4804            diverg = tv.getCoordsB();
4805
4806            switch (beginHitPointType) {
4807                case TURNOUT_A:
4808                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4809                    break;
4810                case TURNOUT_B:
4811                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4812                    break;
4813                case TURNOUT_C:
4814                default:
4815                    xy2 = MathUtil.subtract(bev.getCoordsC(), bev.getCoordsCenter());
4816                    break;
4817            }
4818        } else {
4819            return;
4820        }
4821        Point2D xy = MathUtil.subtract(diverg, c);
4822        double radius = Math.toDegrees(Math.atan2(xy.getY(), xy.getX()));
4823        double eRadius = Math.toDegrees(Math.atan2(xy2.getY(), xy2.getX()));
4824        bev.rotateCoords(radius - eRadius);
4825
4826        Point2D conCord = bev.getCoordsA();
4827        Point2D tCord = tv.getCoordsC();
4828
4829        if (foundHitPointType == HitPointType.TURNOUT_B) {
4830            tCord = tv.getCoordsB();
4831        }
4832
4833        if (foundHitPointType == HitPointType.TURNOUT_A) {
4834            tCord = tv.getCoordsA();
4835        }
4836
4837        switch (beginHitPointType) {
4838            case TURNOUT_A:
4839                conCord = bev.getCoordsA();
4840                break;
4841            case TURNOUT_B:
4842                conCord = bev.getCoordsB();
4843                break;
4844            case TURNOUT_C:
4845                conCord = bev.getCoordsC();
4846                break;
4847            default:
4848                break;
4849        }
4850        xy = MathUtil.subtract(conCord, tCord);
4851        Point2D offset = MathUtil.subtract(bev.getCoordsCenter(), xy);
4852        bev.setCoordsCenter(offset);
4853    }
4854
4855    public List<Positionable> _positionableSelection = new ArrayList<>();
4856    public List<LayoutTrack> _layoutTrackSelection = new ArrayList<>();
4857    public List<LayoutShape> _layoutShapeSelection = new ArrayList<>();
4858
4859    @Nonnull
4860    public List<Positionable> getPositionalSelection() {
4861        return _positionableSelection;
4862    }
4863
4864    @Nonnull
4865    public List<LayoutTrack> getLayoutTrackSelection() {
4866        return _layoutTrackSelection;
4867    }
4868
4869    @Nonnull
4870    public List<LayoutShape> getLayoutShapeSelection() {
4871        return _layoutShapeSelection;
4872    }
4873
4874    private void createSelectionGroups() {
4875        Rectangle2D selectionRect = getSelectionRect();
4876
4877        getContents().forEach((o) -> {
4878            if (selectionRect.contains(o.getLocation())) {
4879
4880                log.trace("found item o of class {}", o.getClass());
4881                if (!_positionableSelection.contains(o)) {
4882                    _positionableSelection.add(o);
4883                }
4884            }
4885        });
4886
4887        getLayoutTracks().forEach((lt) -> {
4888            LayoutTrackView ltv = getLayoutTrackView(lt);
4889            Point2D center = ltv.getCoordsCenter();
4890            if (selectionRect.contains(center)) {
4891                if (!_layoutTrackSelection.contains(lt)) {
4892                    _layoutTrackSelection.add(lt);
4893                }
4894            }
4895        });
4896        assignBlockToSelectionMenuItem.setEnabled(!_layoutTrackSelection.isEmpty());
4897
4898        layoutShapes.forEach((ls) -> {
4899            if (selectionRect.intersects(ls.getBounds())) {
4900                if (!_layoutShapeSelection.contains(ls)) {
4901                    _layoutShapeSelection.add(ls);
4902                }
4903            }
4904        });
4905        redrawPanel();
4906    }
4907
4908    public void clearSelectionGroups() {
4909        selectionActive = false;
4910        _positionableSelection.clear();
4911        _layoutTrackSelection.clear();
4912        assignBlockToSelectionMenuItem.setEnabled(false);
4913        _layoutShapeSelection.clear();
4914    }
4915
4916    private boolean noWarnGlobalDelete = false;
4917
4918    private void deleteSelectedItems() {
4919        if (!noWarnGlobalDelete) {
4920            int selectedValue = JmriJOptionPane.showOptionDialog(this,
4921                    Bundle.getMessage("Question6"), Bundle.getMessage("WarningTitle"),
4922                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
4923                    new Object[]{Bundle.getMessage("ButtonYes"),
4924                        Bundle.getMessage("ButtonNo"),
4925                        Bundle.getMessage("ButtonYesPlus")},
4926                    Bundle.getMessage("ButtonNo"));
4927
4928            // array position 1, ButtonNo or Dialog closed.
4929            if (selectedValue == 1 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
4930                return; // return without creating if "No" response
4931            }
4932
4933            if (selectedValue == 2) { // array positio 2, ButtonYesPlus
4934                // Suppress future warnings, and continue
4935                noWarnGlobalDelete = true;
4936            }
4937        }
4938
4939        _positionableSelection.forEach(this::remove);
4940
4941        _layoutTrackSelection.forEach((lt) -> {
4942            if (lt instanceof PositionablePoint) {
4943                boolean oldWarning = noWarnPositionablePoint;
4944                noWarnPositionablePoint = true;
4945                removePositionablePoint((PositionablePoint) lt);
4946                noWarnPositionablePoint = oldWarning;
4947            } else if (lt instanceof LevelXing) {
4948                boolean oldWarning = noWarnLevelXing;
4949                noWarnLevelXing = true;
4950                removeLevelXing((LevelXing) lt);
4951                noWarnLevelXing = oldWarning;
4952            } else if (lt instanceof LayoutSlip) {
4953                boolean oldWarning = noWarnSlip;
4954                noWarnSlip = true;
4955                removeLayoutSlip((LayoutSlip) lt);
4956                noWarnSlip = oldWarning;
4957            } else if (lt instanceof LayoutTurntable) {
4958                boolean oldWarning = noWarnTurntable;
4959                noWarnTurntable = true;
4960                removeTurntable((LayoutTurntable) lt);
4961                noWarnTurntable = oldWarning;
4962            } else if (lt instanceof LayoutTraverser) {
4963                boolean oldWarning = noWarnTraverser;
4964                noWarnTraverser = true;
4965                removeTraverser((LayoutTraverser) lt);
4966                noWarnTraverser = oldWarning;
4967            } else if (lt instanceof LayoutTurnout) {  //<== this includes LayoutSlips
4968                boolean oldWarning = noWarnLayoutTurnout;
4969                noWarnLayoutTurnout = true;
4970                removeLayoutTurnout((LayoutTurnout) lt);
4971                noWarnLayoutTurnout = oldWarning;
4972            }
4973        });
4974
4975        layoutShapes.removeAll(_layoutShapeSelection);
4976
4977        clearSelectionGroups();
4978        redrawPanel();
4979    }
4980
4981    private void amendSelectionGroup(@Nonnull Positionable pos) {
4982        Positionable p = Objects.requireNonNull(pos);
4983
4984        if (_positionableSelection.contains(p)) {
4985            _positionableSelection.remove(p);
4986        } else {
4987            _positionableSelection.add(p);
4988        }
4989        redrawPanel();
4990    }
4991
4992    public void amendSelectionGroup(@Nonnull LayoutTrack track) {
4993        LayoutTrack p = Objects.requireNonNull(track);
4994
4995        if (_layoutTrackSelection.contains(p)) {
4996            _layoutTrackSelection.remove(p);
4997        } else {
4998            _layoutTrackSelection.add(p);
4999        }
5000        assignBlockToSelectionMenuItem.setEnabled(!_layoutTrackSelection.isEmpty());
5001        redrawPanel();
5002    }
5003
5004    public void amendSelectionGroup(@Nonnull LayoutShape shape) {
5005        LayoutShape ls = Objects.requireNonNull(shape);
5006
5007        if (_layoutShapeSelection.contains(ls)) {
5008            _layoutShapeSelection.remove(ls);
5009        } else {
5010            _layoutShapeSelection.add(ls);
5011        }
5012        redrawPanel();
5013    }
5014
5015    public void alignSelection(boolean alignX) {
5016        Point2D minPoint = MathUtil.infinityPoint2D;
5017        Point2D maxPoint = MathUtil.zeroPoint2D;
5018        Point2D sumPoint = MathUtil.zeroPoint2D;
5019        int cnt = 0;
5020
5021        for (Positionable comp : _positionableSelection) {
5022            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
5023                continue;   // skip non-positionables
5024            }
5025            Point2D p = MathUtil.pointToPoint2D(comp.getLocation());
5026            minPoint = MathUtil.min(minPoint, p);
5027            maxPoint = MathUtil.max(maxPoint, p);
5028            sumPoint = MathUtil.add(sumPoint, p);
5029            cnt++;
5030        }
5031
5032        for (LayoutTrack lt : _layoutTrackSelection) {
5033            LayoutTrackView ltv = getLayoutTrackView(lt);
5034            Point2D p = ltv.getCoordsCenter();
5035            minPoint = MathUtil.min(minPoint, p);
5036            maxPoint = MathUtil.max(maxPoint, p);
5037            sumPoint = MathUtil.add(sumPoint, p);
5038            cnt++;
5039        }
5040
5041        for (LayoutShape ls : _layoutShapeSelection) {
5042            Point2D p = ls.getCoordsCenter();
5043            minPoint = MathUtil.min(minPoint, p);
5044            maxPoint = MathUtil.max(maxPoint, p);
5045            sumPoint = MathUtil.add(sumPoint, p);
5046            cnt++;
5047        }
5048
5049        Point2D avePoint = MathUtil.divide(sumPoint, cnt);
5050        int aveX = (int) avePoint.getX();
5051        int aveY = (int) avePoint.getY();
5052
5053        for (Positionable comp : _positionableSelection) {
5054            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
5055                continue;   // skip non-positionables
5056            }
5057
5058            if (alignX) {
5059                comp.setLocation(aveX, comp.getY());
5060            } else {
5061                comp.setLocation(comp.getX(), aveY);
5062            }
5063        }
5064
5065        _layoutTrackSelection.forEach((lt) -> {
5066            LayoutTrackView ltv = getLayoutTrackView(lt);
5067            if (alignX) {
5068                ltv.setCoordsCenter(new Point2D.Double(aveX, ltv.getCoordsCenter().getY()));
5069            } else {
5070                ltv.setCoordsCenter(new Point2D.Double(ltv.getCoordsCenter().getX(), aveY));
5071            }
5072        });
5073
5074        _layoutShapeSelection.forEach((ls) -> {
5075            if (alignX) {
5076                ls.setCoordsCenter(new Point2D.Double(aveX, ls.getCoordsCenter().getY()));
5077            } else {
5078                ls.setCoordsCenter(new Point2D.Double(ls.getCoordsCenter().getX(), aveY));
5079            }
5080        });
5081
5082        redrawPanel();
5083    }
5084
5085    private boolean showAlignPopup() {
5086        return ((!_positionableSelection.isEmpty())
5087                || (!_layoutTrackSelection.isEmpty())
5088                || (!_layoutShapeSelection.isEmpty()));
5089    }
5090
5091    /**
5092     * Offer actions to align the selected Positionable items either
5093     * Horizontally (at average y coord) or Vertically (at average x coord).
5094     *
5095     * @param popup the JPopupMenu to add alignment menu to
5096     * @return true if alignment menu added
5097     */
5098    public boolean setShowAlignmentMenu(@Nonnull JPopupMenu popup) {
5099        if (showAlignPopup()) {
5100            JMenu edit = new JMenu(Bundle.getMessage("EditAlignment"));
5101            edit.add(new AbstractAction(Bundle.getMessage("AlignX")) {
5102                @Override
5103                public void actionPerformed(ActionEvent event) {
5104                    alignSelection(true);
5105                }
5106            });
5107            edit.add(new AbstractAction(Bundle.getMessage("AlignY")) {
5108                @Override
5109                public void actionPerformed(ActionEvent event) {
5110                    alignSelection(false);
5111                }
5112            });
5113            popup.add(edit);
5114
5115            return true;
5116        }
5117        return false;
5118    }
5119
5120    @Override
5121    public void keyPressed(@Nonnull KeyEvent event) {
5122        if (event.getKeyCode() == KeyEvent.VK_DELETE) {
5123            deleteSelectedItems();
5124            return;
5125        }
5126
5127        double deltaX = returnDeltaPositionX(event);
5128        double deltaY = returnDeltaPositionY(event);
5129
5130        if ((deltaX != 0) || (deltaY != 0)) {
5131            selectionX += deltaX;
5132            selectionY += deltaY;
5133
5134            Point2D delta = new Point2D.Double(deltaX, deltaY);
5135            _positionableSelection.forEach((c) -> {
5136                Point2D newPoint = c.getLocation();
5137                if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
5138                    MemoryIcon pm = (MemoryIcon) c;
5139                    newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
5140                }
5141                newPoint = MathUtil.add(newPoint, delta);
5142                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
5143                c.setLocation(MathUtil.point2DToPoint(newPoint));
5144            });
5145
5146            _layoutTrackSelection.forEach((lt) -> {
5147                LayoutTrackView ltv = getLayoutTrackView(lt);
5148                Point2D newPoint = MathUtil.add(ltv.getCoordsCenter(), delta);
5149                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
5150                getLayoutTrackView(lt).setCoordsCenter(newPoint);
5151            });
5152
5153            _layoutShapeSelection.forEach((ls) -> {
5154                Point2D newPoint = MathUtil.add(ls.getCoordsCenter(), delta);
5155                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
5156                ls.setCoordsCenter(newPoint);
5157            });
5158            redrawPanel();
5159            return;
5160        }
5161        getLayoutEditorToolBarPanel().keyPressed(event);
5162    }
5163
5164    private double returnDeltaPositionX(@Nonnull KeyEvent event) {
5165        double result = 0.0;
5166        double amount = event.isShiftDown() ? 5.0 : 1.0;
5167
5168        switch (event.getKeyCode()) {
5169            case KeyEvent.VK_LEFT: {
5170                result = -amount;
5171                break;
5172            }
5173
5174            case KeyEvent.VK_RIGHT: {
5175                result = +amount;
5176                break;
5177            }
5178
5179            default: {
5180                break;
5181            }
5182        }
5183        return result;
5184    }
5185
5186    private double returnDeltaPositionY(@Nonnull KeyEvent event) {
5187        double result = 0.0;
5188        double amount = event.isShiftDown() ? 5.0 : 1.0;
5189
5190        switch (event.getKeyCode()) {
5191            case KeyEvent.VK_UP: {
5192                result = -amount;
5193                break;
5194            }
5195
5196            case KeyEvent.VK_DOWN: {
5197                result = +amount;
5198                break;
5199            }
5200
5201            default: {
5202                break;
5203            }
5204        }
5205        return result;
5206    }
5207
5208    int _prevNumSel = 0;
5209
5210    @Override
5211    public void mouseMoved(@Nonnull JmriMouseEvent event) {
5212        // initialize mouse position
5213        calcLocation(event);
5214
5215        // if alt modifier is down invert the snap to grid behaviour
5216        snapToGridInvert = event.isAltDown();
5217
5218        if (isEditable()) {
5219            leToolBarPanel.setLocationText(dLoc);
5220        }
5221        List<Positionable> selections = getSelectedItems(event);
5222        Positionable selection = null;
5223        int numSel = selections.size();
5224
5225        if (numSel > 0) {
5226            selection = selections.get(0);
5227        }
5228
5229        if ((selection != null) && (selection.getDisplayLevel() > Editor.BKG) && selection.showToolTip()) {
5230            showToolTip(selection, event);
5231        } else if (_targetPanel.getCursor() != Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) {
5232            super.setToolTip(null);
5233        }
5234
5235        if (numSel != _prevNumSel) {
5236            redrawPanel();
5237            _prevNumSel = numSel;
5238        }
5239
5240        if (findLayoutTracksHitPoint(dLoc)) {
5241            // log.debug("foundTrack: {}", foundTrack);
5242            if (HitPointType.isControlHitType(foundHitPointType)) {
5243                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
5244                setTurnoutTooltip();
5245            } else {
5246                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
5247            }
5248            foundTrack = null;
5249            foundHitPointType = HitPointType.NONE;
5250        } else {
5251            _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
5252        }
5253    }   // mouseMoved
5254
5255    private void setTurnoutTooltip() {
5256        if (foundTrackView instanceof LayoutTurnoutView) {
5257            var ltv = (LayoutTurnoutView) foundTrackView;
5258            var lt = ltv.getLayoutTurnout();
5259            if (lt.showToolTip()) {
5260                var tt = lt.getToolTip();
5261                if (tt != null) {
5262                    tt.setText(lt.getNameString());
5263                    var coords = ltv.getCoordsCenter();
5264                    int offsetY = (int) (getTurnoutCircleSize() * SIZE);
5265                    tt.setLocation((int) coords.getX(), (int) coords.getY() + offsetY);
5266                    setToolTip(tt);
5267                }
5268            }
5269        }
5270    }
5271
5272    public void setAllShowLayoutTurnoutToolTip(boolean state) {
5273        log.debug("setAllShowLayoutTurnoutToolTip: {}", state);
5274        for (LayoutTurnout lt : getLayoutTurnoutsAndSlips()) {
5275            lt.setShowToolTip(state);
5276        }
5277    }
5278
5279    private boolean isDragging = false;
5280
5281    @Override
5282    public void mouseDragged(@Nonnull JmriMouseEvent event) {
5283        // initialize mouse position
5284        calcLocation(event);
5285
5286        checkHighlightCursor();
5287
5288        // ignore this event if still at the original point
5289        if ((!isDragging) && (xLoc == getAnchorX()) && (yLoc == getAnchorY())) {
5290            return;
5291        }
5292
5293        // if alt modifier is down invert the snap to grid behaviour
5294        snapToGridInvert = event.isAltDown();
5295
5296        // process this mouse dragged event
5297        if (isEditable()) {
5298            leToolBarPanel.setLocationText(dLoc);
5299        }
5300        currentPoint = MathUtil.add(dLoc, startDelta);
5301        // don't allow negative placement, objects could become unreachable
5302        currentPoint = MathUtil.max(currentPoint, MathUtil.zeroPoint2D);
5303
5304        if ((selectedObject != null) && (event.isMetaDown() || event.isAltDown())
5305                && (selectedHitPointType == HitPointType.MARKER)) {
5306            // marker moves regardless of editMode or positionable
5307            PositionableLabel pl = (PositionableLabel) selectedObject;
5308            pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5309            isDragging = true;
5310            redrawPanel();
5311            return;
5312        }
5313
5314        if (isEditable()) {
5315            if ((selectedObject != null) && event.isMetaDown() && allPositionable()) {
5316                if (snapToGridOnMove != snapToGridInvert) {
5317                    // this snaps currentPoint to the grid
5318                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
5319                    xLoc = (int) currentPoint.getX();
5320                    yLoc = (int) currentPoint.getY();
5321                    leToolBarPanel.setLocationText(currentPoint);
5322                }
5323
5324                if ((!_positionableSelection.isEmpty())
5325                        || (!_layoutTrackSelection.isEmpty())
5326                        || (!_layoutShapeSelection.isEmpty())) {
5327                    Point2D lastPoint = new Point2D.Double(_lastX, _lastY);
5328                    Point2D offset = MathUtil.subtract(currentPoint, lastPoint);
5329                    Point2D newPoint;
5330
5331                    for (Positionable c : _positionableSelection) {
5332                        if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
5333                            MemoryIcon pm = (MemoryIcon) c;
5334                            newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
5335                        } else {
5336                            newPoint = c.getLocation();
5337                        }
5338                        newPoint = MathUtil.add(newPoint, offset);
5339                        // don't allow negative placement, objects could become unreachable
5340                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5341                        c.setLocation(MathUtil.point2DToPoint(newPoint));
5342                    }
5343
5344                    for (LayoutTrack lt : _layoutTrackSelection) {
5345                        LayoutTrackView ltv = getLayoutTrackView(lt);
5346                        Point2D center = ltv.getCoordsCenter();
5347                        newPoint = MathUtil.add(center, offset);
5348                        // don't allow negative placement, objects could become unreachable
5349                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5350                        getLayoutTrackView(lt).setCoordsCenter(newPoint);
5351                    }
5352
5353                    for (LayoutShape ls : _layoutShapeSelection) {
5354                        Point2D center = ls.getCoordsCenter();
5355                        newPoint = MathUtil.add(center, offset);
5356                        // don't allow negative placement, objects could become unreachable
5357                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5358                        ls.setCoordsCenter(newPoint);
5359                    }
5360
5361                    _lastX = xLoc;
5362                    _lastY = yLoc;
5363                } else {
5364                    switch (selectedHitPointType) {
5365                        case POS_POINT:
5366                        case TURNOUT_CENTER:
5367                        case LEVEL_XING_CENTER:
5368                        case SLIP_LEFT:
5369                        case SLIP_RIGHT:
5370                        case TURNTABLE_CENTER:
5371                        case TRAVERSER_CENTER: {
5372                            getLayoutTrackView((LayoutTrack) selectedObject).setCoordsCenter(currentPoint);
5373                            isDragging = true;
5374                            break;
5375                        }
5376
5377                        case TURNOUT_A: {
5378                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsA(currentPoint);
5379                            break;
5380                        }
5381
5382                        case TURNOUT_B: {
5383                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsB(currentPoint);
5384                            break;
5385                        }
5386
5387                        case TURNOUT_C: {
5388                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsC(currentPoint);
5389                            break;
5390                        }
5391
5392                        case TURNOUT_D: {
5393                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsD(currentPoint);
5394                            break;
5395                        }
5396
5397                        case LEVEL_XING_A: {
5398                            getLevelXingView((LevelXing) selectedObject).setCoordsA(currentPoint);
5399                            break;
5400                        }
5401
5402                        case LEVEL_XING_B: {
5403                            getLevelXingView((LevelXing) selectedObject).setCoordsB(currentPoint);
5404                            break;
5405                        }
5406
5407                        case LEVEL_XING_C: {
5408                            getLevelXingView((LevelXing) selectedObject).setCoordsC(currentPoint);
5409                            break;
5410                        }
5411
5412                        case LEVEL_XING_D: {
5413                            getLevelXingView((LevelXing) selectedObject).setCoordsD(currentPoint);
5414                            break;
5415                        }
5416
5417                        case SLIP_A: {
5418                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsA(currentPoint);
5419                            break;
5420                        }
5421
5422                        case SLIP_B: {
5423                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsB(currentPoint);
5424                            break;
5425                        }
5426
5427                        case SLIP_C: {
5428                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsC(currentPoint);
5429                            break;
5430                        }
5431
5432                        case SLIP_D: {
5433                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsD(currentPoint);
5434                            break;
5435                        }
5436
5437                        case LAYOUT_POS_LABEL:
5438                        case MULTI_SENSOR: {
5439                            PositionableLabel pl = (PositionableLabel) selectedObject;
5440                            if (pl.isPositionable()) {
5441                                pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5442                                isDragging = true;
5443                            }
5444                            break;
5445                        }
5446
5447                        case LAYOUT_POS_JCOMP: {
5448                            PositionableJComponent c = (PositionableJComponent) selectedObject;
5449
5450                            if (c.isPositionable()) {
5451                                c.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5452                                isDragging = true;
5453                            }
5454                            break;
5455                        }
5456
5457                        case LAYOUT_POS_JPNL: {
5458                            PositionableJPanel c = (PositionableJPanel) selectedObject;
5459
5460                            if (c.isPositionable()) {
5461                                c.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5462                                isDragging = true;
5463                            }
5464                            break;
5465                        }
5466
5467                        case TRACK_CIRCLE_CENTRE: {
5468                            TrackSegmentView tv = getTrackSegmentView((TrackSegment) selectedObject);
5469                            tv.reCalculateTrackSegmentAngle(currentPoint.getX(), currentPoint.getY());
5470                            break;
5471                        }
5472
5473                        default: {
5474                            if (HitPointType.isBezierHitType(foundHitPointType)) {
5475                                int index = selectedHitPointType.bezierPointIndex();
5476                                getTrackSegmentView((TrackSegment) selectedObject).setBezierControlPoint(currentPoint, index);
5477                            } else if ((selectedHitPointType == HitPointType.SHAPE_CENTER)) {
5478                                ((LayoutShape) selectedObject).setCoordsCenter(currentPoint);
5479                            } else if (HitPointType.isShapePointOffsetHitPointType(selectedHitPointType)) {
5480                                int index = selectedHitPointType.shapePointIndex();
5481                                ((LayoutShape) selectedObject).setPoint(index, currentPoint);
5482                            } else if (HitPointType.isTurntableRayHitType(selectedHitPointType)) {
5483                                LayoutTurntable turn = (LayoutTurntable) selectedObject;
5484                                LayoutTurntableView turnView = getLayoutTurntableView(turn);
5485                                turnView.setRayCoordsIndexed(currentPoint.getX(), currentPoint.getY(),
5486                                        selectedHitPointType.turntableTrackIndex());
5487//                             } else if (HitPointType.isTraverserSlotHitType(selectedHitPointType)) {
5488//                                 Placeholder comment:
5489//                                 The ability to drag the slot connection points is disabled.
5490//                                 Connection point locations are relative to the traverser center point.
5491                            }
5492                            break;
5493                        }
5494                    }
5495                }
5496            } else if ((beginTrack != null)
5497                    && event.isShiftDown()
5498                    && leToolBarPanel.trackButton.isSelected()) {
5499                // dragging from first end of Track Segment
5500                currentLocation = new Point2D.Double(xLoc, yLoc);
5501                boolean needResetCursor = (foundTrack != null);
5502
5503                if (findLayoutTracksHitPoint(currentLocation, true)) {
5504                    // have match to free connection point, change cursor
5505                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
5506                } else if (needResetCursor) {
5507                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
5508                }
5509            } else if (event.isShiftDown()
5510                    && leToolBarPanel.shapeButton.isSelected() && (selectedObject != null)) {
5511                // dragging from end of shape
5512                currentLocation = new Point2D.Double(xLoc, yLoc);
5513            } else if (selectionActive && !event.isShiftDown() && !event.isMetaDown()) {
5514                selectionWidth = xLoc - selectionX;
5515                selectionHeight = yLoc - selectionY;
5516            }
5517            redrawPanel();
5518        } else {
5519            Rectangle r = new Rectangle(event.getX(), event.getY(), 1, 1);
5520            ((JComponent) event.getSource()).scrollRectToVisible(r);
5521        }   // if (isEditable())
5522    }   // mouseDragged
5523
5524    @Override
5525    public void mouseEntered(@Nonnull JmriMouseEvent event) {
5526        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
5527    }
5528
5529    /**
5530     * Add an Anchor point.
5531     */
5532    public void addAnchor() {
5533        addAnchor(currentPoint);
5534    }
5535
5536    @Nonnull
5537    public PositionablePoint addAnchor(@Nonnull Point2D point) {
5538        Point2D p = Objects.requireNonNull(point);
5539
5540        // get unique name
5541        String name = finder.uniqueName("A", ++numAnchors);
5542
5543        // create object
5544        PositionablePoint o = new PositionablePoint(name,
5545                PositionablePoint.PointType.ANCHOR, this);
5546        PositionablePointView pv = new PositionablePointView(o, p, this);
5547        addLayoutTrack(o, pv);
5548
5549        setDirty();
5550
5551        return o;
5552    }
5553
5554    /**
5555     * Add an End Bumper point.
5556     */
5557    public void addEndBumper() {
5558        // get unique name
5559        String name = finder.uniqueName("EB", ++numEndBumpers);
5560
5561        // create object
5562        PositionablePoint o = new PositionablePoint(name,
5563                PositionablePoint.PointType.END_BUMPER, this);
5564        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
5565        addLayoutTrack(o, pv);
5566
5567        setDirty();
5568    }
5569
5570    /**
5571     * Add an Edge Connector point.
5572     */
5573    public void addEdgeConnector() {
5574        // get unique name
5575        String name = finder.uniqueName("EC", ++numEdgeConnectors);
5576
5577        // create object
5578        PositionablePoint o = new PositionablePoint(name,
5579                PositionablePoint.PointType.EDGE_CONNECTOR, this);
5580        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
5581        addLayoutTrack(o, pv);
5582
5583        setDirty();
5584    }
5585
5586    /**
5587     * Add a Track Segment
5588     */
5589    public void addTrackSegment() {
5590        // get unique name
5591        String name = finder.uniqueName("T", ++numTrackSegments);
5592
5593        // create object
5594        newTrack = new TrackSegment(name, beginTrack, beginHitPointType,
5595                foundTrack, foundHitPointType,
5596                leToolBarPanel.mainlineTrack.isSelected(), this);
5597
5598        TrackSegmentView tsv = new TrackSegmentView(
5599                newTrack,
5600                this
5601        );
5602        addLayoutTrack(newTrack, tsv);
5603
5604        setDirty();
5605
5606        // link to connected objects
5607        setLink(beginTrack, beginHitPointType, newTrack, HitPointType.TRACK);
5608        setLink(foundTrack, foundHitPointType, newTrack, HitPointType.TRACK);
5609
5610        // check on layout block
5611        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5612        if (newName == null) {
5613            newName = "";
5614        }
5615        LayoutBlock b = provideLayoutBlock(newName);
5616
5617        if (b != null) {
5618            newTrack.setLayoutBlock(b);
5619            getLEAuxTools().setBlockConnectivityChanged();
5620
5621            // check on occupancy sensor
5622            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5623            if (sensorName == null) {
5624                sensorName = "";
5625            }
5626
5627            if (!sensorName.isEmpty()) {
5628                if (!validateSensor(sensorName, b, this)) {
5629                    b.setOccupancySensorName("");
5630                } else {
5631                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5632                }
5633            }
5634            newTrack.updateBlockInfo();
5635        }
5636    }
5637
5638    /**
5639     * Add a Level Crossing
5640     */
5641    public void addLevelXing() {
5642        // get unique name
5643        String name = finder.uniqueName("X", ++numLevelXings);
5644
5645        // create object
5646        LevelXing o = new LevelXing(name, this);
5647        LevelXingView ov = new LevelXingView(o, currentPoint, this);
5648        addLayoutTrack(o, ov);
5649
5650        setDirty();
5651
5652        // check on layout block
5653        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5654        if (newName == null) {
5655            newName = "";
5656        }
5657        LayoutBlock b = provideLayoutBlock(newName);
5658
5659        if (b != null) {
5660            o.setLayoutBlockAC(b);
5661            o.setLayoutBlockBD(b);
5662
5663            // check on occupancy sensor
5664            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5665            if (sensorName == null) {
5666                sensorName = "";
5667            }
5668
5669            if (!sensorName.isEmpty()) {
5670                if (!validateSensor(sensorName, b, this)) {
5671                    b.setOccupancySensorName("");
5672                } else {
5673                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5674                }
5675            }
5676        }
5677    }
5678
5679    /**
5680     * Add a LayoutSlip
5681     *
5682     * @param type the slip type
5683     */
5684    public void addLayoutSlip(LayoutTurnout.TurnoutType type) {
5685        // get the rotation entry
5686        double rot;
5687        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
5688
5689        if (s.isEmpty()) {
5690            rot = 0.0;
5691        } else {
5692            try {
5693                rot = IntlUtilities.doubleValue(s);
5694            } catch (ParseException e) {
5695                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5696                        + e, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5697
5698                return;
5699            }
5700        }
5701
5702        // get unique name
5703        String name = finder.uniqueName("SL", ++numLayoutSlips);
5704
5705        // create object
5706        LayoutSlip o;
5707        LayoutSlipView ov;
5708
5709        switch (type) {
5710            case DOUBLE_SLIP:
5711                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5712                o = lds;
5713                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5714                break;
5715            case SINGLE_SLIP:
5716                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5717                o = lss;
5718                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5719                break;
5720            default:
5721                log.error("can't create slip {} with type {}", name, type);
5722                return; // without creating
5723        }
5724
5725        addLayoutTrack(o, ov);
5726
5727        setDirty();
5728
5729        // check on layout block
5730        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5731        if (newName == null) {
5732            newName = "";
5733        }
5734        LayoutBlock b = provideLayoutBlock(newName);
5735
5736        if (b != null) {
5737            ov.setLayoutBlock(b);
5738
5739            // check on occupancy sensor
5740            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5741            if (sensorName == null) {
5742                sensorName = "";
5743            }
5744
5745            if (!sensorName.isEmpty()) {
5746                if (!validateSensor(sensorName, b, this)) {
5747                    b.setOccupancySensorName("");
5748                } else {
5749                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5750                }
5751            }
5752        }
5753
5754        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5755        if (turnoutName == null) {
5756            turnoutName = "";
5757        }
5758
5759        if (validatePhysicalTurnout(turnoutName, this)) {
5760            // turnout is valid and unique.
5761            o.setTurnout(turnoutName);
5762
5763            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5764                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5765            }
5766        } else {
5767            o.setTurnout("");
5768            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5769            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5770        }
5771        turnoutName = leToolBarPanel.extraTurnoutNameComboBox.getSelectedItemDisplayName();
5772        if (turnoutName == null) {
5773            turnoutName = "";
5774        }
5775
5776        if (validatePhysicalTurnout(turnoutName, this)) {
5777            // turnout is valid and unique.
5778            o.setTurnoutB(turnoutName);
5779
5780            if (o.getTurnoutB().getSystemName().equals(turnoutName)) {
5781                leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(o.getTurnoutB());
5782            }
5783        } else {
5784            o.setTurnoutB("");
5785            leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(null);
5786            leToolBarPanel.extraTurnoutNameComboBox.setSelectedIndex(-1);
5787        }
5788    }
5789
5790    /**
5791     * Add a Layout Turnout
5792     *
5793     * @param type the turnout type
5794     */
5795    public void addLayoutTurnout(LayoutTurnout.TurnoutType type) {
5796        // get the rotation entry
5797        double rot;
5798        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
5799
5800        if (s.isEmpty()) {
5801            rot = 0.0;
5802        } else {
5803            try {
5804                rot = IntlUtilities.doubleValue(s);
5805            } catch (ParseException e) {
5806                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5807                        + e, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5808
5809                return;
5810            }
5811        }
5812
5813        // get unique name
5814        String name = finder.uniqueName("TO", ++numLayoutTurnouts);
5815
5816        // create object - check all types, although not clear all actually reach here
5817        LayoutTurnout o;
5818        LayoutTurnoutView ov;
5819
5820        switch (type) {
5821
5822            case RH_TURNOUT:
5823                LayoutRHTurnout lrht = new LayoutRHTurnout(name, this);
5824                o = lrht;
5825                ov = new LayoutRHTurnoutView(lrht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5826                break;
5827            case LH_TURNOUT:
5828                LayoutLHTurnout llht = new LayoutLHTurnout(name, this);
5829                o = llht;
5830                ov = new LayoutLHTurnoutView(llht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5831                break;
5832            case WYE_TURNOUT:
5833                LayoutWye lw = new LayoutWye(name, this);
5834                o = lw;
5835                ov = new LayoutWyeView(lw, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5836                break;
5837            case DOUBLE_XOVER:
5838                LayoutDoubleXOver ldx = new LayoutDoubleXOver(name, this);
5839                o = ldx;
5840                ov = new LayoutDoubleXOverView(ldx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5841                break;
5842            case RH_XOVER:
5843                LayoutRHXOver lrx = new LayoutRHXOver(name, this);
5844                o = lrx;
5845                ov = new LayoutRHXOverView(lrx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5846                break;
5847            case LH_XOVER:
5848                LayoutLHXOver llx = new LayoutLHXOver(name, this);
5849                o = llx;
5850                ov = new LayoutLHXOverView(llx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5851                break;
5852
5853            case DOUBLE_SLIP:
5854                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5855                o = lds;
5856                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5857                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5858                break;
5859            case SINGLE_SLIP:
5860                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5861                o = lss;
5862                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5863                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5864                break;
5865
5866            default:
5867                log.error("can't create LayoutTrack {} with type {}", name, type);
5868                return; // without creating
5869        }
5870
5871        addLayoutTrack(o, ov);
5872
5873        setDirty();
5874
5875        // check on layout block
5876        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5877        if (newName == null) {
5878            newName = "";
5879        }
5880        LayoutBlock b = provideLayoutBlock(newName);
5881
5882        if (b != null) {
5883            ov.setLayoutBlock(b);
5884
5885            // check on occupancy sensor
5886            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5887            if (sensorName == null) {
5888                sensorName = "";
5889            }
5890
5891            if (!sensorName.isEmpty()) {
5892                if (!validateSensor(sensorName, b, this)) {
5893                    b.setOccupancySensorName("");
5894                } else {
5895                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5896                }
5897            }
5898        }
5899
5900        // set default continuing route Turnout State
5901        o.setContinuingSense(Turnout.CLOSED);
5902
5903        // check on a physical turnout
5904        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5905        if (turnoutName == null) {
5906            turnoutName = "";
5907        }
5908
5909        if (validatePhysicalTurnout(turnoutName, this)) {
5910            // turnout is valid and unique.
5911            o.setTurnout(turnoutName);
5912
5913            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5914                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5915            }
5916        } else {
5917            o.setTurnout("");
5918            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5919            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5920        }
5921    }
5922
5923    /**
5924     * Validates that a physical turnout exists and is unique among Layout
5925     * Turnouts Returns true if valid turnout was entered, false otherwise
5926     *
5927     * @param inTurnoutName the (system or user) name of the turnout
5928     * @param inOpenPane    the pane over which to show dialogs (null to
5929     *                      suppress dialogs)
5930     * @return true if valid
5931     */
5932    public boolean validatePhysicalTurnout(
5933            @Nonnull String inTurnoutName,
5934            @CheckForNull Component inOpenPane) {
5935        // check if turnout name was entered
5936        if (inTurnoutName.isEmpty()) {
5937            // no turnout entered
5938            return false;
5939        }
5940
5941        // check that the unique turnout name corresponds to a defined physical turnout
5942        Turnout t = InstanceManager.turnoutManagerInstance().getTurnout(inTurnoutName);
5943        if (t == null) {
5944            // There is no turnout corresponding to this name
5945            if (inOpenPane != null) {
5946                JmriJOptionPane.showMessageDialog(inOpenPane,
5947                        MessageFormat.format(Bundle.getMessage("Error8"), inTurnoutName),
5948                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5949            }
5950            return false;
5951        }
5952
5953        log.debug("validatePhysicalTurnout('{}')", inTurnoutName);
5954        boolean result = true;  // assume success (optimist!)
5955
5956        // ensure that this turnout is unique among Layout Turnouts in this Layout
5957        for (LayoutTurnout lt : getLayoutTurnouts()) {
5958            t = lt.getTurnout();
5959            if (t != null) {
5960                String sname = t.getSystemName();
5961                String uname = t.getUserName();
5962                log.debug("{}: Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5963                if ((sname.equals(inTurnoutName))
5964                        || ((uname != null) && (uname.equals(inTurnoutName)))) {
5965                    result = false;
5966                    break;
5967                }
5968            }
5969
5970            // Only check for the second turnout if the type is a double cross over
5971            // otherwise the second turnout is used to throw an additional turnout at
5972            // the same time.
5973            if (lt.isTurnoutTypeXover()) {
5974                t = lt.getSecondTurnout();
5975                if (t != null) {
5976                    String sname = t.getSystemName();
5977                    String uname = t.getUserName();
5978                    log.debug("{}: 2nd Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5979                    if ((sname.equals(inTurnoutName))
5980                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5981                        result = false;
5982                        break;
5983                    }
5984                }
5985            }
5986        }
5987
5988        if (result) {   // only need to test slips if we haven't failed yet...
5989            // ensure that this turnout is unique among Layout slips in this Layout
5990            for (LayoutSlip sl : getLayoutSlips()) {
5991                t = sl.getTurnout();
5992                if (t != null) {
5993                    String sname = t.getSystemName();
5994                    String uname = t.getUserName();
5995                    log.debug("{}: slip Turnout tested '{}' and '{}'.", sl.getName(), sname, uname);
5996                    if ((sname.equals(inTurnoutName))
5997                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5998                        result = false;
5999                        break;
6000                    }
6001                }
6002
6003                t = sl.getTurnoutB();
6004                if (t != null) {
6005                    String sname = t.getSystemName();
6006                    String uname = t.getUserName();
6007                    log.debug("{}: slip Turnout B tested '{}' and '{}'.", sl.getName(), sname, uname);
6008                    if ((sname.equals(inTurnoutName))
6009                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
6010                        result = false;
6011                        break;
6012                    }
6013                }
6014            }
6015        }
6016
6017        if (result) {   // only need to test Turntable turnouts if we haven't failed yet...
6018            // ensure that this turntable turnout is unique among turnouts in this Layout
6019            for (LayoutTurntable tt : getLayoutTurntables()) {
6020                for (LayoutTurntable.RayTrack ray : tt.getRayTrackList()) {
6021                    t = ray.getTurnout();
6022                    if (t != null) {
6023                        String sname = t.getSystemName();
6024                        String uname = t.getUserName();
6025                        log.debug("{}: Turntable turnout tested '{}' and '{}'.", ray.getTurnoutName(), sname, uname);
6026                        if ((sname.equals(inTurnoutName))
6027                                || ((uname != null) && (uname.equals(inTurnoutName)))) {
6028                            result = false;
6029                            break;
6030                        }
6031                    }
6032                }
6033            }
6034        }
6035
6036        if (result) {   // only need to test Traverser turnouts if we haven't failed yet...
6037            // ensure that this traverser turnout is unique among turnouts in this Layout
6038            for (LayoutTraverser tt : getLayoutTraversers()) {
6039                for (LayoutTraverser.SlotTrack ray : tt.getSlotList()) {
6040                    t = ray.getTurnout();
6041                    if (t != null) {
6042                        String sname = t.getSystemName();
6043                        String uname = t.getUserName();
6044                        log.debug("{}: Traverser turnout tested '{}' and '{}'.", ray.getTurnoutName(), sname, uname);
6045                        if ((sname.equals(inTurnoutName))
6046                                || ((uname != null) && (uname.equals(inTurnoutName)))) {
6047                            result = false;
6048                            break;
6049                        }
6050                    }
6051                }
6052            }
6053        }
6054
6055        if (!result && (inOpenPane != null)) {
6056            JmriJOptionPane.showMessageDialog(inOpenPane,
6057                    MessageFormat.format(Bundle.getMessage("Error4"), inTurnoutName),
6058                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6059        }
6060        return result;
6061    }
6062
6063    /**
6064     * link the 'from' object and type to the 'to' object and type
6065     *
6066     * @param fromObject    the object to link from
6067     * @param fromPointType the object type to link from
6068     * @param toObject      the object to link to
6069     * @param toPointType   the object type to link to
6070     */
6071    public void setLink(@Nonnull LayoutTrack fromObject, HitPointType fromPointType,
6072            @Nonnull LayoutTrack toObject, HitPointType toPointType) {
6073        switch (fromPointType) {
6074            case POS_POINT: {
6075                if ((toPointType == HitPointType.TRACK) && (fromObject instanceof PositionablePoint)) {
6076                    ((PositionablePoint) fromObject).setTrackConnection((TrackSegment) toObject);
6077                } else {
6078                    log.error("Attempt to link a non-TRACK connection ('{}')to a Positionable Point ('{}')",
6079                            toObject.getName(), fromObject.getName());
6080                }
6081                break;
6082            }
6083
6084            case TURNOUT_A:
6085            case TURNOUT_B:
6086            case TURNOUT_C:
6087            case TURNOUT_D:
6088            case SLIP_A:
6089            case SLIP_B:
6090            case SLIP_C:
6091            case SLIP_D:
6092            case LEVEL_XING_A:
6093            case LEVEL_XING_B:
6094            case LEVEL_XING_C:
6095            case LEVEL_XING_D: {
6096                try {
6097                    fromObject.setConnection(fromPointType, toObject, toPointType);
6098                } catch (JmriException e) {
6099                    // ignore (log.error in setConnection method)
6100                }
6101                break;
6102            }
6103
6104            case TRACK: {
6105                // should never happen, Track Segment links are set in ctor
6106                log.error("Illegal request to set a Track Segment link");
6107                break;
6108            }
6109
6110            default: {
6111                if (HitPointType.isTurntableRayHitType(fromPointType) && (fromObject instanceof LayoutTurntable)) {
6112                    if (toObject instanceof TrackSegment) {
6113                        ((LayoutTurntable) fromObject).setRayConnect((TrackSegment) toObject,
6114                                fromPointType.turntableTrackIndex());
6115                    } else {
6116                        log.warn("setLink found expected toObject type {} with fromPointType {} fromObject type {}",
6117                                toObject.getClass(), fromPointType, fromObject.getClass(), new Exception("traceback"));
6118                    }
6119                } else if (HitPointType.isTraverserSlotHitType(fromPointType) && (fromObject instanceof LayoutTraverser)) {
6120                    if (toObject instanceof TrackSegment) {
6121                        ((LayoutTraverser) fromObject).setSlotConnect((TrackSegment) toObject,
6122                                fromPointType.traverserTrackIndex());
6123                    } else {
6124                        log.warn("setLink found expected toObject type {} with fromPointType {} fromObject type {}",
6125                                toObject.getClass(), fromPointType, fromObject.getClass(), new Exception("traceback"));
6126                    }
6127                } else {
6128                    log.warn("setLink found expected fromObject type {} with fromPointType {} toObject type {}",
6129                            fromObject.getClass(), fromPointType, toObject.getClass(), new Exception("traceback"));
6130                }
6131                break;
6132            }
6133        }
6134    }
6135
6136    /**
6137     * Return a layout block with the entered name, creating a new one if
6138     * needed. Note that the entered name becomes the user name of the
6139     * LayoutBlock, and a system name is automatically created by
6140     * LayoutBlockManager if needed.
6141     * <p>
6142     * If the block name is a system name, then the user will have to supply a
6143     * user name for the block.
6144     * <p>
6145     * Some, but not all, errors pop a Swing error dialog in addition to
6146     * logging.
6147     *
6148     * @param inBlockName the entered name
6149     * @return the provided LayoutBlock
6150     */
6151    public LayoutBlock provideLayoutBlock(@Nonnull String inBlockName) {
6152        LayoutBlock result = null; // assume failure (pessimist!)
6153        LayoutBlock newBlk = null; // assume failure (pessimist!)
6154
6155        if (inBlockName.isEmpty()) {
6156            // nothing entered, try autoAssign
6157            if (autoAssignBlocks) {
6158                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock();
6159                if (null == newBlk) {
6160                    log.error("provideLayoutBlock: Failure to auto-assign for empty LayoutBlock name");
6161                }
6162            } else {
6163                log.debug("provideLayoutBlock: no name given and not assigning auto block names");
6164            }
6165        } else {
6166            // check if this Layout Block already exists
6167            result = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(inBlockName);
6168            if (result == null) { //(no)
6169                // The combo box name can be either a block system name or a block user name
6170                Block checkBlock = InstanceManager.getDefault(BlockManager.class).getBlock(inBlockName);
6171                if (checkBlock == null) {
6172                    log.error("provideLayoutBlock: The block name '{}' does not return a block.", inBlockName);
6173                } else {
6174                    String checkUserName = checkBlock.getUserName();
6175                    if (checkUserName != null && checkUserName.equals(inBlockName)) {
6176                        // Go ahead and use the name for the layout block
6177                        newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, inBlockName);
6178                        if (newBlk == null) {
6179                            log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}'.", inBlockName);
6180                        }
6181                    } else {
6182                        // Appears to be a system name, request a user name
6183                        String blkUserName = (String)JmriJOptionPane.showInputDialog(getTargetFrame(),
6184                                Bundle.getMessage("BlkUserNameMsg"),
6185                                Bundle.getMessage("BlkUserNameTitle"),
6186                                JmriJOptionPane.PLAIN_MESSAGE, null, null, "");
6187                        if (blkUserName != null && !blkUserName.isEmpty()) {
6188                            // Verify the user name
6189                            Block checkDuplicate = InstanceManager.getDefault(BlockManager.class).getByUserName(blkUserName);
6190                            if (checkDuplicate != null) {
6191                                JmriJOptionPane.showMessageDialog(getTargetFrame(),
6192                                        Bundle.getMessage("BlkUserNameInUse", blkUserName),
6193                                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6194                            } else {
6195                                // OK to use as a block user name
6196                                checkBlock.setUserName(blkUserName);
6197                                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, blkUserName);
6198                                if (newBlk == null) {
6199                                    log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}' with a new user name.", blkUserName);
6200                                }
6201                            }
6202                        }
6203                    }
6204                }
6205            }
6206        }
6207
6208        // if we created a new block
6209        if (newBlk != null) {
6210            // initialize the new block
6211            // log.debug("provideLayoutBlock :: Init new block {}", inBlockName);
6212            newBlk.initializeLayoutBlock();
6213            newBlk.initializeLayoutBlockRouting();
6214            newBlk.setBlockTrackColor(defaultTrackColor);
6215            newBlk.setBlockOccupiedColor(defaultOccupiedTrackColor);
6216            newBlk.setBlockExtraColor(defaultAlternativeTrackColor);
6217            result = newBlk;
6218        }
6219
6220        if (result != null) {
6221            // set both new and previously existing block
6222            result.addLayoutEditor(this);
6223            result.incrementUse();
6224            setDirty();
6225        }
6226        return result;
6227    }
6228
6229    /**
6230     * Validates that the supplied occupancy sensor name corresponds to an
6231     * existing sensor and is unique among all blocks. If valid, returns true
6232     * and sets the block sensor name in the block. Else returns false, and does
6233     * nothing to the block.
6234     *
6235     * @param sensorName the sensor name to validate
6236     * @param blk        the LayoutBlock in which to set it
6237     * @param openFrame  the frame (Component) it is in
6238     * @return true if sensor is valid
6239     */
6240    public boolean validateSensor(
6241            @Nonnull String sensorName,
6242            @Nonnull LayoutBlock blk,
6243            @Nonnull Component openFrame) {
6244        boolean result = false; // assume failure (pessimist!)
6245
6246        // check if anything entered
6247        if (!sensorName.isEmpty()) {
6248            // get a validated sensor corresponding to this name and assigned to block
6249            if (blk.getOccupancySensorName().equals(sensorName)) {
6250                result = true;
6251            } else {
6252                Sensor s = blk.validateSensor(sensorName, openFrame);
6253                result = (s != null); // if sensor returned result is true.
6254            }
6255        }
6256        return result;
6257    }
6258
6259    /**
6260     * Return a layout block with the given name if one exists. Registers this
6261     * LayoutEditor with the layout block. This method is designed to be used
6262     * when a panel is loaded. The calling method must handle whether the use
6263     * count should be incremented.
6264     *
6265     * @param blockID the given name
6266     * @return null if blockID does not already exist
6267     */
6268    public LayoutBlock getLayoutBlock(@Nonnull String blockID) {
6269        // check if this Layout Block already exists
6270        LayoutBlock blk = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(blockID);
6271        if (blk == null) {
6272            log.error("LayoutBlock '{}' not found when panel loaded", blockID);
6273            return null;
6274        }
6275        blk.addLayoutEditor(this);
6276        return blk;
6277    }
6278
6279    /**
6280     * Remove object from all Layout Editor temporary lists of items not part of
6281     * track schematic
6282     *
6283     * @param s the object to remove
6284     * @return true if found
6285     */
6286    private boolean remove(@Nonnull Object s) {
6287        boolean found = false;
6288
6289        if (backgroundImage.contains(s)) {
6290            backgroundImage.remove(s);
6291            found = true;
6292        }
6293        if (memoryLabelList.contains(s)) {
6294            memoryLabelList.remove(s);
6295            found = true;
6296        }
6297        if (memoryInputList.contains(s)) {
6298            memoryInputList.remove(s);
6299            found = true;
6300        }
6301        if (globalVariableLabelList.contains(s)) {
6302            globalVariableLabelList.remove(s);
6303            found = true;
6304        }
6305        if (blockContentsLabelList.contains(s)) {
6306            blockContentsLabelList.remove(s);
6307            found = true;
6308        }
6309        if (blockContentsInputList.contains(s)) {
6310            blockContentsInputList.remove(s);
6311            found = true;
6312        }
6313        if (multiSensors.contains(s)) {
6314            multiSensors.remove(s);
6315            found = true;
6316        }
6317        if (clocks.contains(s)) {
6318            clocks.remove(s);
6319            found = true;
6320        }
6321        if (labelImage.contains(s)) {
6322            labelImage.remove(s);
6323            found = true;
6324        }
6325
6326        if (sensorImage.contains(s) || sensorList.contains(s)) {
6327            Sensor sensor = ((SensorIcon) s).getSensor();
6328            if (sensor != null) {
6329                if (removeAttachedBean((sensor))) {
6330                    sensorImage.remove(s);
6331                    sensorList.remove(s);
6332                    found = true;
6333                } else {
6334                    return false;
6335                }
6336            }
6337        }
6338
6339        if (turnoutImage.contains(s) || turnoutList.contains(s)) {
6340            Turnout turnout = ((TurnoutIcon) s).getTurnout();
6341            if (turnout != null) {
6342                if (removeAttachedBean((turnout))) {
6343                    turnoutImage.remove(s);
6344                    turnoutList.remove(s);
6345                    found = true;
6346                } else {
6347                    return false;
6348                }
6349            }
6350        }
6351
6352        if (signalHeadImage.contains(s) || signalList.contains(s)) {
6353            SignalHead head = ((SignalHeadIcon) s).getSignalHead();
6354            if (head != null) {
6355                if (removeAttachedBean((head))) {
6356                    signalHeadImage.remove(s);
6357                    signalList.remove(s);
6358                    found = true;
6359                } else {
6360                    return false;
6361                }
6362            }
6363        }
6364
6365        if (signalMastList.contains(s)) {
6366            SignalMast mast = ((SignalMastIcon) s).getSignalMast();
6367            if (mast != null) {
6368                if (removeAttachedBean((mast))) {
6369                    signalMastList.remove(s);
6370                    found = true;
6371                } else {
6372                    return false;
6373                }
6374            }
6375        }
6376
6377        super.removeFromContents((Positionable) s);
6378
6379        if (found) {
6380            setDirty();
6381            redrawPanel();
6382        }
6383        return found;
6384    }
6385
6386    @Override
6387    public boolean removeFromContents(@Nonnull Positionable l) {
6388        return remove(l);
6389    }
6390
6391    private String findBeanUsage(@Nonnull NamedBean bean) {
6392        PositionablePoint pe;
6393        PositionablePoint pw;
6394        LayoutTurnout lt;
6395        LevelXing lx;
6396        LayoutSlip ls;
6397        boolean found = false;
6398        StringBuilder sb = new StringBuilder();
6399        String msgKey = "DeleteReference";  // NOI18N
6400        String beanKey = "None";  // NOI18N
6401        String beanValue = bean.getDisplayName();
6402
6403        if (bean instanceof SignalMast) {
6404            beanKey = "BeanNameSignalMast";  // NOI18N
6405
6406            if (InstanceManager.getDefault(SignalMastLogicManager.class).isSignalMastUsed((SignalMast) bean)) {
6407                SignalMastLogic sml = InstanceManager.getDefault(
6408                        SignalMastLogicManager.class).getSignalMastLogic((SignalMast) bean);
6409                if ((sml != null) && sml.useLayoutEditor(sml.getDestinationList().get(0))) {
6410                    msgKey = "DeleteSmlReference";  // NOI18N
6411                }
6412            }
6413        } else if (bean instanceof Sensor) {
6414            beanKey = "BeanNameSensor";  // NOI18N
6415        } else if (bean instanceof Turnout) {
6416            beanKey = "BeanNameTurnout";  // NOI18N
6417        } else if (bean instanceof SignalHead) {
6418            beanKey = "BeanNameSignalHead";  // NOI18N
6419        }
6420        if (!beanKey.equals("None")) {  // NOI18N
6421            sb.append(Bundle.getMessage(msgKey, Bundle.getMessage(beanKey), beanValue));
6422        }
6423
6424        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
6425            TrackSegment t1 = pw.getConnect1();
6426            TrackSegment t2 = pw.getConnect2();
6427            if (t1 != null) {
6428                if (t2 != null) {
6429                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6430                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
6431                } else {
6432                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6433                }
6434            }
6435            found = true;
6436        }
6437
6438        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
6439            TrackSegment t1 = pe.getConnect1();
6440            TrackSegment t2 = pe.getConnect2();
6441
6442            if (t1 != null) {
6443                if (t2 != null) {
6444                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6445                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
6446                } else {
6447                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6448                }
6449            }
6450            found = true;
6451        }
6452
6453        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
6454            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("BeanNameTurnout"), lt.getTurnoutName()));   // NOI18N
6455            found = true;
6456        }
6457
6458        if ((lx = finder.findLevelXingByBean(bean)) != null) {
6459            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("LevelCrossing"), lx.getId()));   // NOI18N
6460            found = true;
6461        }
6462
6463        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
6464            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("Slip"), ls.getTurnoutName()));   // NOI18N
6465            found = true;
6466        }
6467
6468        if (!found) {
6469            return null;
6470        }
6471        return sb.toString();
6472    }
6473
6474    /**
6475     * NX Sensors, Signal Heads and Signal Masts can be attached to positional
6476     * points, turnouts and level crossings. If an attachment exists, present an
6477     * option to cancel the remove action, remove the attachement or retain the
6478     * attachment.
6479     *
6480     * @param bean The named bean to be removed.
6481     * @return true if OK to remove the related icon.
6482     */
6483    private boolean removeAttachedBean(@Nonnull NamedBean bean) {
6484        String usage = findBeanUsage(bean);
6485
6486        if (usage != null) {
6487            usage = String.format("<html>%s</html>", usage);
6488            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6489                    usage, Bundle.getMessage("WarningTitle"),
6490                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6491                    new Object[]{Bundle.getMessage("ButtonYes"),
6492                        Bundle.getMessage("ButtonNo"),
6493                        Bundle.getMessage("ButtonCancel")},
6494                    Bundle.getMessage("ButtonYes"));
6495
6496            if (selectedValue == 1 ) { // array pos 1, No
6497                return true; // return leaving the references in place but allow the icon to be deleted.
6498            }
6499            // array pos 2, cancel or Dialog closed
6500            if (selectedValue == 2 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
6501                return false; // do not delete the item
6502            }
6503            if (bean instanceof Sensor) {
6504                // Additional actions for NX sensor pairs
6505                return getLETools().removeSensorAssignment((Sensor) bean);
6506            } else {
6507                removeBeanRefs(bean);
6508            }
6509        }
6510        return true;
6511    }
6512
6513    private void removeBeanRefs(@Nonnull NamedBean bean) {
6514        PositionablePoint pe;
6515        PositionablePoint pw;
6516        LayoutTurnout lt;
6517        LevelXing lx;
6518        LayoutSlip ls;
6519
6520        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
6521            pw.removeBeanReference(bean);
6522        }
6523
6524        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
6525            pe.removeBeanReference(bean);
6526        }
6527
6528        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
6529            lt.removeBeanReference(bean);
6530        }
6531
6532        if ((lx = finder.findLevelXingByBean(bean)) != null) {
6533            lx.removeBeanReference(bean);
6534        }
6535
6536        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
6537            ls.removeBeanReference(bean);
6538        }
6539    }
6540
6541    private boolean noWarnPositionablePoint = false;
6542
6543    /**
6544     * Remove a PositionablePoint -- an Anchor or an End Bumper.
6545     *
6546     * @param o the PositionablePoint to remove
6547     * @return true if removed
6548     */
6549    public boolean removePositionablePoint(@Nonnull PositionablePoint o) {
6550        // First verify with the user that this is really wanted, only show message if there is a bit of track connected
6551        if ((o.getConnect1() != null) || (o.getConnect2() != null)) {
6552            if (!noWarnPositionablePoint) {
6553                int selectedValue = JmriJOptionPane.showOptionDialog(this,
6554                        Bundle.getMessage("Question2"), Bundle.getMessage("WarningTitle"),
6555                        JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6556                        new Object[]{Bundle.getMessage("ButtonYes"),
6557                            Bundle.getMessage("ButtonNo"),
6558                            Bundle.getMessage("ButtonYesPlus")},
6559                        Bundle.getMessage("ButtonNo"));
6560
6561                // array position 1, ButtonNo , or Dialog Closed.
6562                if (selectedValue == 1 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
6563                    return false; // return without creating if "No" response
6564                }
6565
6566                if (selectedValue == 2) { // array position 2, ButtonYesPlus
6567                    // Suppress future warnings, and continue
6568                    noWarnPositionablePoint = true;
6569                }
6570            }
6571
6572            // remove from selection information
6573            if (selectedObject == o) {
6574                selectedObject = null;
6575            }
6576
6577            if (prevSelectedObject == o) {
6578                prevSelectedObject = null;
6579            }
6580
6581            // remove connections if any
6582            TrackSegment t1 = o.getConnect1();
6583            TrackSegment t2 = o.getConnect2();
6584
6585            if (t1 != null) {
6586                removeTrackSegment(t1);
6587            }
6588
6589            if (t2 != null) {
6590                removeTrackSegment(t2);
6591            }
6592
6593            // delete from array
6594        }
6595
6596        return removeLayoutTrackAndRedraw(o);
6597    }
6598
6599    private boolean noWarnLayoutTurnout = false;
6600
6601    /**
6602     * Remove a LayoutTurnout
6603     *
6604     * @param o the LayoutTurnout to remove
6605     * @return true if removed
6606     */
6607    public boolean removeLayoutTurnout(@Nonnull LayoutTurnout o) {
6608        // First verify with the user that this is really wanted
6609        if (!noWarnLayoutTurnout) {
6610            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6611                    Bundle.getMessage("Question1r"), Bundle.getMessage("WarningTitle"),
6612                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6613                    new Object[]{Bundle.getMessage("ButtonYes"),
6614                        Bundle.getMessage("ButtonNo"),
6615                        Bundle.getMessage("ButtonYesPlus")},
6616                    Bundle.getMessage("ButtonNo"));
6617
6618            // return without removing if array position 1 "No" response or Dialog closed
6619            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6620                return false;
6621            }
6622
6623            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6624                // Suppress future warnings, and continue
6625                noWarnLayoutTurnout = true;
6626            }
6627        }
6628
6629        // remove from selection information
6630        if (selectedObject == o) {
6631            selectedObject = null;
6632        }
6633
6634        if (prevSelectedObject == o) {
6635            prevSelectedObject = null;
6636        }
6637
6638        // remove connections if any
6639        TrackSegment t = (TrackSegment) o.getConnectA();
6640
6641        if (t != null) {
6642            substituteAnchor(getLayoutTurnoutView(o).getCoordsA(), o, t);
6643        }
6644        t = (TrackSegment) o.getConnectB();
6645
6646        if (t != null) {
6647            substituteAnchor(getLayoutTurnoutView(o).getCoordsB(), o, t);
6648        }
6649        t = (TrackSegment) o.getConnectC();
6650
6651        if (t != null) {
6652            substituteAnchor(getLayoutTurnoutView(o).getCoordsC(), o, t);
6653        }
6654        t = (TrackSegment) o.getConnectD();
6655
6656        if (t != null) {
6657            substituteAnchor(getLayoutTurnoutView(o).getCoordsD(), o, t);
6658        }
6659
6660        // decrement Block use count(s)
6661        LayoutBlock b = o.getLayoutBlock();
6662
6663        if (b != null) {
6664            b.decrementUse();
6665        }
6666
6667        if (o.isTurnoutTypeXover() || o.isTurnoutTypeSlip()) {
6668            LayoutBlock b2 = o.getLayoutBlockB();
6669
6670            if ((b2 != null) && (b2 != b)) {
6671                b2.decrementUse();
6672            }
6673            LayoutBlock b3 = o.getLayoutBlockC();
6674
6675            if ((b3 != null) && (b3 != b) && (b3 != b2)) {
6676                b3.decrementUse();
6677            }
6678            LayoutBlock b4 = o.getLayoutBlockD();
6679
6680            if ((b4 != null) && (b4 != b)
6681                    && (b4 != b2) && (b4 != b3)) {
6682                b4.decrementUse();
6683            }
6684        }
6685
6686        return removeLayoutTrackAndRedraw(o);
6687    }
6688
6689    private void substituteAnchor(@Nonnull Point2D loc,
6690            @Nonnull LayoutTrack o, @Nonnull TrackSegment t) {
6691        PositionablePoint p = addAnchor(loc);
6692
6693        if (t.getConnect1() == o) {
6694            t.setNewConnect1(p, HitPointType.POS_POINT);
6695        }
6696
6697        if (t.getConnect2() == o) {
6698            t.setNewConnect2(p, HitPointType.POS_POINT);
6699        }
6700        p.setTrackConnection(t);
6701    }
6702
6703    private boolean noWarnLevelXing = false;
6704
6705    /**
6706     * Remove a Level Crossing
6707     *
6708     * @param o the LevelXing to remove
6709     * @return true if removed
6710     */
6711    public boolean removeLevelXing(@Nonnull LevelXing o) {
6712        // First verify with the user that this is really wanted
6713        if (!noWarnLevelXing) {
6714            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6715                    Bundle.getMessage("Question3r"), Bundle.getMessage("WarningTitle"),
6716                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6717                    new Object[]{Bundle.getMessage("ButtonYes"),
6718                        Bundle.getMessage("ButtonNo"),
6719                        Bundle.getMessage("ButtonYesPlus")},
6720                    Bundle.getMessage("ButtonNo"));
6721
6722             // array position 1 Button No, or Dialog closed.
6723            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6724                return false;
6725            }
6726
6727            if (selectedValue == 2 ) { // array position 2 ButtonYesPlus
6728                // Suppress future warnings, and continue
6729                noWarnLevelXing = true;
6730            }
6731        }
6732
6733        // remove from selection information
6734        if (selectedObject == o) {
6735            selectedObject = null;
6736        }
6737
6738        if (prevSelectedObject == o) {
6739            prevSelectedObject = null;
6740        }
6741
6742        // remove connections if any
6743        LevelXingView ov = getLevelXingView(o);
6744
6745        TrackSegment t = (TrackSegment) o.getConnectA();
6746        if (t != null) {
6747            substituteAnchor(ov.getCoordsA(), o, t);
6748        }
6749        t = (TrackSegment) o.getConnectB();
6750
6751        if (t != null) {
6752            substituteAnchor(ov.getCoordsB(), o, t);
6753        }
6754        t = (TrackSegment) o.getConnectC();
6755
6756        if (t != null) {
6757            substituteAnchor(ov.getCoordsC(), o, t);
6758        }
6759        t = (TrackSegment) o.getConnectD();
6760
6761        if (t != null) {
6762            substituteAnchor(ov.getCoordsD(), o, t);
6763        }
6764
6765        // decrement block use count if any blocks in use
6766        LayoutBlock lb = o.getLayoutBlockAC();
6767
6768        if (lb != null) {
6769            lb.decrementUse();
6770        }
6771        LayoutBlock lbx = o.getLayoutBlockBD();
6772
6773        if ((lbx != null) && (lb != null) && (lbx != lb)) {
6774            lb.decrementUse();
6775        }
6776
6777        return removeLayoutTrackAndRedraw(o);
6778    }
6779
6780    private boolean noWarnSlip = false;
6781
6782    /**
6783     * Remove a slip
6784     *
6785     * @param o the LayoutSlip to remove
6786     * @return true if removed
6787     */
6788    public boolean removeLayoutSlip(@Nonnull LayoutTurnout o) {
6789        if (!(o instanceof LayoutSlip)) {
6790            return false;
6791        }
6792
6793        // First verify with the user that this is really wanted
6794        if (!noWarnSlip) {
6795            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6796                    Bundle.getMessage("Question5r"), Bundle.getMessage("WarningTitle"),
6797                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6798                    new Object[]{Bundle.getMessage("ButtonYes"),
6799                        Bundle.getMessage("ButtonNo"),
6800                        Bundle.getMessage("ButtonYesPlus")},
6801                    Bundle.getMessage("ButtonNo"));
6802
6803             // return without removing if array position 1 "No" response or Dialog closed
6804            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6805                return false;
6806            }
6807
6808            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6809                // Suppress future warnings, and continue
6810                noWarnSlip = true;
6811            }
6812        }
6813
6814        LayoutTurnoutView ov = getLayoutTurnoutView(o);
6815
6816        // remove from selection information
6817        if (selectedObject == o) {
6818            selectedObject = null;
6819        }
6820
6821        if (prevSelectedObject == o) {
6822            prevSelectedObject = null;
6823        }
6824
6825        // remove connections if any
6826        TrackSegment t = (TrackSegment) o.getConnectA();
6827
6828        if (t != null) {
6829            substituteAnchor(ov.getCoordsA(), o, t);
6830        }
6831        t = (TrackSegment) o.getConnectB();
6832
6833        if (t != null) {
6834            substituteAnchor(ov.getCoordsB(), o, t);
6835        }
6836        t = (TrackSegment) o.getConnectC();
6837
6838        if (t != null) {
6839            substituteAnchor(ov.getCoordsC(), o, t);
6840        }
6841        t = (TrackSegment) o.getConnectD();
6842
6843        if (t != null) {
6844            substituteAnchor(ov.getCoordsD(), o, t);
6845        }
6846
6847        // decrement block use count if any blocks in use
6848        LayoutBlock lb = o.getLayoutBlock();
6849
6850        if (lb != null) {
6851            lb.decrementUse();
6852        }
6853
6854        return removeLayoutTrackAndRedraw(o);
6855    }
6856
6857    private boolean noWarnTurntable = false;
6858    private boolean noWarnTraverser = false;
6859
6860    /**
6861     * Remove a Layout Turntable
6862     *
6863     * @param o the LayoutTurntable to remove
6864     * @return true if removed
6865     */
6866    public boolean removeTurntable(@Nonnull LayoutTurntable o) {
6867        // First verify with the user that this is really wanted
6868        if (!noWarnTurntable) {
6869            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6870                    Bundle.getMessage("Question4r"), Bundle.getMessage("WarningTitle"),
6871                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6872                    new Object[]{Bundle.getMessage("ButtonYes"),
6873                        Bundle.getMessage("ButtonNo"),
6874                        Bundle.getMessage("ButtonYesPlus")},
6875                    Bundle.getMessage("ButtonNo"));
6876
6877            // return without removing if array position 1 "No" response or Dialog closed
6878            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6879                return false;
6880            }
6881
6882            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6883                // Suppress future warnings, and continue
6884                noWarnTurntable = true;
6885            }
6886        }
6887
6888        // Check if removing the turntable will cause errors.
6889        if (!o.isRemoveAllowed()) {
6890            return false;
6891        }
6892
6893        // remove from selection information
6894        if (selectedObject == o) {
6895            selectedObject = null;
6896        }
6897
6898        if (prevSelectedObject == o) {
6899            prevSelectedObject = null;
6900        }
6901
6902        // remove connections if any
6903        LayoutTurntableView ov = getLayoutTurntableView(o);
6904        for (int j = 0; j < o.getNumberRays(); j++) {
6905            TrackSegment t = ov.getRayConnectOrdered(j);
6906
6907            if (t != null) {
6908                substituteAnchor(ov.getRayCoordsIndexed(j), o, t);
6909            }
6910        }
6911
6912        return removeLayoutTrackAndRedraw(o);
6913    }
6914
6915    /**
6916     * Remove a Layout Traverser
6917     *
6918     * @param o the LayoutTraverser to remove
6919     * @return true if removed
6920     */
6921    public boolean removeTraverser(@Nonnull LayoutTraverser o) {
6922        // First verify with the user that this is really wanted
6923        if (!noWarnTraverser) {
6924            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6925                    Bundle.getMessage("Question8r"), Bundle.getMessage("WarningTitle"),
6926                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6927                    new Object[]{Bundle.getMessage("ButtonYes"),
6928                            Bundle.getMessage("ButtonNo"),
6929                            Bundle.getMessage("ButtonYesPlus")},
6930                    Bundle.getMessage("ButtonNo"));
6931
6932            // return without removing if array position 1 "No" response or Dialog closed
6933            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6934                return false;
6935            }
6936
6937            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6938                // Suppress future warnings, and continue
6939                noWarnTraverser = true;
6940            }
6941        }
6942
6943        // Check if removing the traverser will cause errors.
6944        if (!o.isRemoveAllowed()) {
6945            return false;
6946        }
6947
6948        // remove from selection information
6949        if (selectedObject == o) {
6950            selectedObject = null;
6951        }
6952
6953        if (prevSelectedObject == o) {
6954            prevSelectedObject = null;
6955        }
6956
6957        // remove connections if any
6958        LayoutTraverserView ov = getLayoutTraverserView(o);
6959        for (int j = 0; j < o.getNumberSlots(); j++) {
6960            TrackSegment t = ov.getSlotConnectOrdered(j);
6961            if (t != null) {
6962                substituteAnchor(ov.getSlotCoordsIndexed(j), o, t);
6963            }
6964        }
6965        return removeLayoutTrackAndRedraw(o);
6966    }
6967
6968    /**
6969     * Remove a Track Segment
6970     *
6971     * @param o the TrackSegment to remove
6972     */
6973    public void removeTrackSegment(@Nonnull TrackSegment o) {
6974        // save affected blocks
6975        LayoutBlock block1 = null;
6976        LayoutBlock block2 = null;
6977        LayoutBlock block = o.getLayoutBlock();
6978
6979        // remove any connections
6980        HitPointType type = o.getType1();
6981
6982        if (type == HitPointType.POS_POINT) {
6983            PositionablePoint p = (PositionablePoint) (o.getConnect1());
6984
6985            if (p != null) {
6986                p.removeTrackConnection(o);
6987
6988                if (p.getConnect1() != null) {
6989                    block1 = p.getConnect1().getLayoutBlock();
6990                } else if (p.getConnect2() != null) {
6991                    block1 = p.getConnect2().getLayoutBlock();
6992                }
6993            }
6994        } else {
6995            block1 = getAffectedBlock(o.getConnect1(), type);
6996            disconnect(o.getConnect1(), type);
6997        }
6998        type = o.getType2();
6999
7000        if (type == HitPointType.POS_POINT) {
7001            PositionablePoint p = (PositionablePoint) (o.getConnect2());
7002
7003            if (p != null) {
7004                p.removeTrackConnection(o);
7005
7006                if (p.getConnect1() != null) {
7007                    block2 = p.getConnect1().getLayoutBlock();
7008                } else if (p.getConnect2() != null) {
7009                    block2 = p.getConnect2().getLayoutBlock();
7010                }
7011            }
7012        } else {
7013            block2 = getAffectedBlock(o.getConnect2(), type);
7014            disconnect(o.getConnect2(), type);
7015        }
7016
7017        // delete from array
7018        removeLayoutTrack(o);
7019
7020        // update affected blocks
7021        if (block != null) {
7022            // decrement Block use count
7023            block.decrementUse();
7024            getLEAuxTools().setBlockConnectivityChanged();
7025            block.updatePaths();
7026        }
7027
7028        if ((block1 != null) && (block1 != block)) {
7029            block1.updatePaths();
7030        }
7031
7032        if ((block2 != null) && (block2 != block) && (block2 != block1)) {
7033            block2.updatePaths();
7034        }
7035
7036        //
7037        setDirty();
7038        redrawPanel();
7039    }
7040
7041    private void disconnect(@Nonnull LayoutTrack o, HitPointType type) {
7042        switch (type) {
7043            case TURNOUT_A:
7044            case TURNOUT_B:
7045            case TURNOUT_C:
7046            case TURNOUT_D:
7047            case SLIP_A:
7048            case SLIP_B:
7049            case SLIP_C:
7050            case SLIP_D:
7051            case LEVEL_XING_A:
7052            case LEVEL_XING_B:
7053            case LEVEL_XING_C:
7054            case LEVEL_XING_D: {
7055                try {
7056                    o.setConnection(type, null, HitPointType.NONE);
7057                } catch (JmriException e) {
7058                    // ignore (log.error in setConnection method)
7059                }
7060                break;
7061            }
7062
7063            default: {
7064                if (HitPointType.isTurntableRayHitType(type)) {
7065                    ((LayoutTurntable) o).setRayConnect(null, type.turntableTrackIndex());
7066                }
7067                if (HitPointType.isTraverserSlotHitType(type)) {
7068                    ((LayoutTraverser) o).setSlotConnect(null, type.traverserTrackIndex());
7069                }
7070                break;
7071            }
7072        }
7073    }
7074
7075    /**
7076     * Depending on the given type, and the real class of the given LayoutTrack,
7077     * determine the connected LayoutTrack. This provides a variable-indirect
7078     * form of e.g. trk.getLayoutBlockC() for example. Perhaps "Connected Block"
7079     * captures the idea better, but that method name is being used for
7080     * something else.
7081     *
7082     *
7083     * @param track The track who's connected blocks are being examined
7084     * @param type  This point to check for connected blocks, i.e. TURNOUT_B
7085     * @return The block at a particular point on the track object, or null if
7086     *         none.
7087     */
7088    // Temporary - this should certainly be a LayoutTrack method.
7089    public LayoutBlock getAffectedBlock(@Nonnull LayoutTrack track, HitPointType type) {
7090        LayoutBlock result = null;
7091
7092        switch (type) {
7093            case TURNOUT_A:
7094            case SLIP_A: {
7095                if (track instanceof LayoutTurnout) {
7096                    LayoutTurnout lt = (LayoutTurnout) track;
7097                    result = lt.getLayoutBlock();
7098                }
7099                break;
7100            }
7101
7102            case TURNOUT_B:
7103            case SLIP_B: {
7104                if (track instanceof LayoutTurnout) {
7105                    LayoutTurnout lt = (LayoutTurnout) track;
7106                    result = lt.getLayoutBlockB();
7107                }
7108                break;
7109            }
7110
7111            case TURNOUT_C:
7112            case SLIP_C: {
7113                if (track instanceof LayoutTurnout) {
7114                    LayoutTurnout lt = (LayoutTurnout) track;
7115                    result = lt.getLayoutBlockC();
7116                }
7117                break;
7118            }
7119
7120            case TURNOUT_D:
7121            case SLIP_D: {
7122                if (track instanceof LayoutTurnout) {
7123                    LayoutTurnout lt = (LayoutTurnout) track;
7124                    result = lt.getLayoutBlockD();
7125                }
7126                break;
7127            }
7128
7129            case LEVEL_XING_A:
7130            case LEVEL_XING_C: {
7131                if (track instanceof LevelXing) {
7132                    LevelXing lx = (LevelXing) track;
7133                    result = lx.getLayoutBlockAC();
7134                }
7135                break;
7136            }
7137
7138            case LEVEL_XING_B:
7139            case LEVEL_XING_D: {
7140                if (track instanceof LevelXing) {
7141                    LevelXing lx = (LevelXing) track;
7142                    result = lx.getLayoutBlockBD();
7143                }
7144                break;
7145            }
7146
7147            case TRACK: {
7148                if (track instanceof TrackSegment) {
7149                    TrackSegment ts = (TrackSegment) track;
7150                    result = ts.getLayoutBlock();
7151                }
7152                break;
7153            }
7154            default: {
7155                if (HitPointType.isTurntableRayHitType(type) ||
7156                        HitPointType.isTraverserSlotHitType(type)) {
7157                    break;
7158                }
7159                log.warn("Unhandled track type: {}", type);
7160                break;
7161            }
7162        }
7163        return result;
7164    }
7165
7166    /**
7167     * Add a sensor indicator to the Draw Panel
7168     */
7169    void addSensor() {
7170        String newName = leToolBarPanel.sensorComboBox.getSelectedItemDisplayName();
7171        if (newName == null) {
7172            newName = "";
7173        }
7174
7175        if (newName.isEmpty()) {
7176            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error10"),
7177                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7178            return;
7179        }
7180        SensorIcon l = new SensorIcon(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
7181                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), this);
7182
7183        l.setIcon("SensorStateActive", leToolBarPanel.sensorIconEditor.getIcon(0));
7184        l.setIcon("SensorStateInactive", leToolBarPanel.sensorIconEditor.getIcon(1));
7185        l.setIcon("BeanStateInconsistent", leToolBarPanel.sensorIconEditor.getIcon(2));
7186        l.setIcon("BeanStateUnknown", leToolBarPanel.sensorIconEditor.getIcon(3));
7187        l.setSensor(newName);
7188        l.setDisplayLevel(Editor.SENSORS);
7189
7190        leToolBarPanel.sensorComboBox.setSelectedItem(l.getSensor());
7191        setNextLocation(l);
7192        try {
7193            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7194        } catch (Positionable.DuplicateIdException e) {
7195            // This should never happen
7196            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7197        }
7198    }
7199
7200    public void putSensor(@Nonnull SensorIcon l) {
7201        l.updateSize();
7202        l.setDisplayLevel(Editor.SENSORS);
7203        try {
7204            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7205        } catch (Positionable.DuplicateIdException e) {
7206            // This should never happen
7207            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7208        }
7209    }
7210
7211    /**
7212     * Add a turnout indicator to the Draw Panel
7213     */
7214    void addTurnout() {
7215        String newName = leToolBarPanel.turnoutComboBox.getSelectedItemDisplayName();
7216        if (newName == null) {
7217            newName = "";
7218        }
7219
7220        if (newName.isEmpty()) {
7221            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error10"),
7222                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7223            return;
7224        }
7225        TurnoutIcon l = new OutputIndicator(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
7226                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), this);
7227
7228        l.setTurnout(newName);
7229
7230        l.setIcon("TurnoutStateThrown", leToolBarPanel.turnoutIconEditor.getIcon(0));
7231        l.setIcon("TurnoutStateClosed", leToolBarPanel.turnoutIconEditor.getIcon(1));
7232        l.setIcon("BeanStateInconsistent", leToolBarPanel.turnoutIconEditor.getIcon(2));
7233        l.setIcon("BeanStateUnknown", leToolBarPanel.turnoutIconEditor.getIcon(3));
7234        l.setDisplayLevel(Editor.TURNOUTS);
7235
7236        leToolBarPanel.turnoutComboBox.setSelectedItem(l.getTurnout());
7237        setNextLocation(l);
7238        try {
7239            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7240        } catch (Positionable.DuplicateIdException e) {
7241            // This should never happen
7242            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7243        }
7244    }
7245
7246    public void putTurnout(@Nonnull TurnoutIcon l) {
7247        l.updateSize();
7248        l.setDisplayLevel(Editor.TURNOUTS);
7249        try {
7250            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7251        } catch (Positionable.DuplicateIdException e) {
7252            // This should never happen
7253            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7254        }
7255    }
7256
7257    /**
7258     * Add a signal head to the Panel
7259     */
7260    void addSignalHead() {
7261        // check for valid signal head entry
7262        String newName = leToolBarPanel.signalHeadComboBox.getSelectedItemDisplayName();
7263        if (newName == null) {
7264            newName = "";
7265        }
7266        SignalHead mHead = null;
7267
7268        if (!newName.isEmpty()) {
7269            mHead = InstanceManager.getDefault(SignalHeadManager.class).getSignalHead(newName);
7270
7271            /*if (mHead == null)
7272            mHead = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(newName);
7273            else */
7274            leToolBarPanel.signalHeadComboBox.setSelectedItem(mHead);
7275        }
7276
7277        if (mHead == null) {
7278            // There is no signal head corresponding to this name
7279            JmriJOptionPane.showMessageDialog(this,
7280                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
7281                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7282            return;
7283        }
7284
7285        // create and set up signal icon
7286        SignalHeadIcon l = new SignalHeadIcon(this);
7287        l.setSignalHead(newName);
7288        l.setIcon("SignalHeadStateRed", leToolBarPanel.signalIconEditor.getIcon(0));
7289        l.setIcon("SignalHeadStateFlashingRed", leToolBarPanel.signalIconEditor.getIcon(1));
7290        l.setIcon("SignalHeadStateYellow", leToolBarPanel.signalIconEditor.getIcon(2));
7291        l.setIcon("SignalHeadStateFlashingYellow", leToolBarPanel.signalIconEditor.getIcon(3));
7292        l.setIcon("SignalHeadStateGreen", leToolBarPanel.signalIconEditor.getIcon(4));
7293        l.setIcon("SignalHeadStateFlashingGreen", leToolBarPanel.signalIconEditor.getIcon(5));
7294        l.setIcon("SignalHeadStateDark", leToolBarPanel.signalIconEditor.getIcon(6));
7295        l.setIcon("SignalHeadStateHeld", leToolBarPanel.signalIconEditor.getIcon(7));
7296        l.setIcon("SignalHeadStateLunar", leToolBarPanel.signalIconEditor.getIcon(8));
7297        l.setIcon("SignalHeadStateFlashingLunar", leToolBarPanel.signalIconEditor.getIcon(9));
7298        unionToPanelBounds(l.getBounds());
7299        setNextLocation(l);
7300        setDirty();
7301        putSignal(l);
7302    }
7303
7304    public void putSignal(@Nonnull SignalHeadIcon l) {
7305        l.updateSize();
7306        l.setDisplayLevel(Editor.SIGNALS);
7307        try {
7308            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7309        } catch (Positionable.DuplicateIdException e) {
7310            // This should never happen
7311            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7312        }
7313    }
7314
7315    @CheckForNull
7316    SignalHead getSignalHead(@Nonnull String name) {
7317        SignalHead sh = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
7318
7319        if (sh == null) {
7320            sh = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(name);
7321        }
7322
7323        if (sh == null) {
7324            log.warn("did not find a SignalHead named {}", name);
7325        }
7326        return sh;
7327    }
7328
7329    public boolean containsSignalHead(@CheckForNull SignalHead head) {
7330        if (head != null) {
7331            for (SignalHeadIcon h : signalList) {
7332                if (h.getSignalHead() == head) {
7333                    return true;
7334                }
7335            }
7336        }
7337        return false;
7338    }
7339
7340    public void removeSignalHead(@CheckForNull SignalHead head) {
7341        if (head != null) {
7342            for (SignalHeadIcon h : signalList) {
7343                if (h.getSignalHead() == head) {
7344                    signalList.remove(h);
7345                    h.remove();
7346                    h.dispose();
7347                    setDirty();
7348                    redrawPanel();
7349                    break;
7350                }
7351            }
7352        }
7353    }
7354
7355    void addSignalMast() {
7356        // check for valid signal head entry
7357        String newName = leToolBarPanel.signalMastComboBox.getSelectedItemDisplayName();
7358        if (newName == null) {
7359            newName = "";
7360        }
7361        SignalMast mMast = null;
7362
7363        if (!newName.isEmpty()) {
7364            mMast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(newName);
7365            leToolBarPanel.signalMastComboBox.setSelectedItem(mMast);
7366        }
7367
7368        if (mMast == null) {
7369            // There is no signal head corresponding to this name
7370            JmriJOptionPane.showMessageDialog(this,
7371                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
7372                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7373
7374            return;
7375        }
7376
7377        // create and set up signal icon
7378        SignalMastIcon l = new SignalMastIcon(this);
7379        l.setSignalMast(newName);
7380        unionToPanelBounds(l.getBounds());
7381        setNextLocation(l);
7382        setDirty();
7383        putSignalMast(l);
7384    }
7385
7386    public void putSignalMast(@Nonnull SignalMastIcon l) {
7387        l.updateSize();
7388        l.setDisplayLevel(Editor.SIGNALS);
7389        try {
7390            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7391        } catch (Positionable.DuplicateIdException e) {
7392            // This should never happen
7393            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7394        }
7395    }
7396
7397    SignalMast getSignalMast(@Nonnull String name) {
7398        SignalMast sh = InstanceManager.getDefault(SignalMastManager.class).getBySystemName(name);
7399
7400        if (sh == null) {
7401            sh = InstanceManager.getDefault(SignalMastManager.class).getByUserName(name);
7402        }
7403
7404        if (sh == null) {
7405            log.warn("did not find a SignalMast named {}", name);
7406        }
7407        return sh;
7408    }
7409
7410    public boolean containsSignalMast(@Nonnull SignalMast mast) {
7411        for (SignalMastIcon h : signalMastList) {
7412            if (h.getSignalMast() == mast) {
7413                return true;
7414            }
7415        }
7416        return false;
7417    }
7418
7419    /**
7420     * Add a label to the Draw Panel
7421     */
7422    void addLabel() {
7423        String labelText = leToolBarPanel.textLabelTextField.getText();
7424        labelText = (labelText != null) ? labelText.trim() : "";
7425
7426        if (labelText.isEmpty()) {
7427            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11"),
7428                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7429            return;
7430        }
7431        PositionableLabel l = super.addLabel(labelText);
7432        unionToPanelBounds(l.getBounds());
7433        setDirty();
7434        l.setForeground(defaultTextColor);
7435    }
7436
7437    @Override
7438    public void putItem(@Nonnull Positionable l) throws Positionable.DuplicateIdException {
7439        super.putItem(l);
7440
7441        if (l instanceof SensorIcon) {
7442            sensorImage.add((SensorIcon) l);
7443            sensorList.add((SensorIcon) l);
7444        } else if (l instanceof TurnoutIcon) {
7445            turnoutImage.add((TurnoutIcon) l);
7446            turnoutList.add((TurnoutIcon) l);
7447        } else if (l instanceof LocoIcon) {
7448            markerImage.add((LocoIcon) l);
7449        } else if (l instanceof SignalHeadIcon) {
7450            signalHeadImage.add((SignalHeadIcon) l);
7451            signalList.add((SignalHeadIcon) l);
7452        } else if (l instanceof SignalMastIcon) {
7453            signalMastList.add((SignalMastIcon) l);
7454        } else if (l instanceof BlockContentsIcon) {
7455            blockContentsLabelList.add((BlockContentsIcon) l);
7456        } else if (l instanceof MemoryIcon) {
7457            memoryLabelList.add((MemoryIcon) l);
7458        } else if (l instanceof BlockContentsInputIcon) {
7459            blockContentsInputList.add((BlockContentsInputIcon) l);
7460        } else if (l instanceof MemoryInputIcon) {
7461            memoryInputList.add((MemoryInputIcon) l);
7462        } else if (l instanceof GlobalVariableIcon) {
7463            globalVariableLabelList.add((GlobalVariableIcon) l);
7464        } else if (l instanceof AnalogClock2Display) {
7465            clocks.add((AnalogClock2Display) l);
7466        } else if (l instanceof MultiSensorIcon) {
7467            multiSensors.add((MultiSensorIcon) l);
7468        }
7469
7470        if (l instanceof PositionableLabel) {
7471            if (((PositionableLabel) l).isBackground()) {
7472                backgroundImage.add((PositionableLabel) l);
7473            } else {
7474                labelImage.add((PositionableLabel) l);
7475            }
7476        }
7477        unionToPanelBounds(l.getBounds(new Rectangle()));
7478        setDirty();
7479    }
7480
7481    /**
7482     * When adding a memory variable, provide an option to create the normal label
7483     * or create an input text field.  The label requires a pop-up dialog to change the value
7484     * while the text field makes it possible to change the value on the panel.  This also makes
7485     * it possible to change the value using the web server.
7486     */
7487    void selectMemoryType() {
7488        int response = JmriJOptionPane.showConfirmDialog(null,
7489            Bundle.getMessage("MemorySelectType"),
7490            Bundle.getMessage("MemorySelectTitle"),
7491            JmriJOptionPane.YES_NO_OPTION);
7492
7493        if (response == JmriJOptionPane.YES_OPTION) {
7494            addMemory();
7495            return;
7496        }
7497
7498        var length = JmriJOptionPane.showInputDialog(null,
7499            Bundle.getMessage("MemorySelectSize"),
7500            "5");
7501
7502        int textLength;
7503        try {
7504           textLength = Integer.parseInt(length);
7505        }
7506        catch (NumberFormatException e) {
7507           textLength = 5;
7508        }
7509
7510        addInputMemory(textLength);
7511    }
7512
7513    /**
7514     * Add a memory label to the Draw Panel
7515     */
7516    void addMemory() {
7517        String memoryName = leToolBarPanel.textMemoryComboBox.getSelectedItemDisplayName();
7518        if (memoryName == null) {
7519            memoryName = "";
7520        }
7521
7522        if (memoryName.isEmpty()) {
7523            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11a"),
7524                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7525            return;
7526        }
7527        MemoryIcon l = new MemoryIcon(" ", this);
7528        l.setMemory(memoryName);
7529        setNextLocation(l);
7530        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7531        l.setDisplayLevel(Editor.LABELS);
7532        l.setForeground(defaultTextColor);
7533        unionToPanelBounds(l.getBounds());
7534        try {
7535            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7536        } catch (Positionable.DuplicateIdException e) {
7537            // This should never happen
7538            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7539        }
7540    }
7541
7542    void addInputMemory(int textFieldLength) {
7543        String memoryName = leToolBarPanel.textMemoryComboBox.getSelectedItemDisplayName();
7544        if (memoryName == null) {
7545            memoryName = "";
7546        }
7547
7548        if (memoryName.isEmpty()) {
7549            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11a"),
7550                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7551            return;
7552        }
7553
7554        MemoryInputIcon l = new MemoryInputIcon(textFieldLength, this);
7555        l.setMemory(memoryName);
7556        setNextLocation(l);
7557        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7558        l.setDisplayLevel(Editor.MEMORIES);
7559        l.setForeground(defaultTextColor);
7560        unionToPanelBounds(l.getBounds());
7561        try {
7562            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7563        } catch (Positionable.DuplicateIdException e) {
7564            // This should never happen
7565            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7566        }
7567    }
7568
7569
7570    void addGlobalVariable() {
7571        String globalVariableName = leToolBarPanel.textGlobalVariableComboBox.getSelectedItemDisplayName();
7572        if (globalVariableName == null) {
7573            globalVariableName = "";
7574        }
7575
7576        if (globalVariableName.isEmpty()) {
7577            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11c"),
7578                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7579            return;
7580        }
7581        GlobalVariableIcon l = new GlobalVariableIcon(" ", this);
7582        l.setGlobalVariable(globalVariableName);
7583        GlobalVariable xGlobalVariable = l.getGlobalVariable();
7584
7585        if (xGlobalVariable != null) {
7586            String uname = xGlobalVariable.getDisplayName();
7587            if (!uname.equals(globalVariableName)) {
7588                // put the system name in the memory field
7589                leToolBarPanel.textGlobalVariableComboBox.setSelectedItem(xGlobalVariable);
7590            }
7591        }
7592        setNextLocation(l);
7593        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7594        l.setDisplayLevel(Editor.LABELS);
7595        l.setForeground(defaultTextColor);
7596        unionToPanelBounds(l.getBounds());
7597        try {
7598            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7599        } catch (Positionable.DuplicateIdException e) {
7600            // This should never happen
7601            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7602        }
7603    }
7604
7605    /**
7606     * When adding a blopck contents object, provide an option to create the normal label
7607     * or create an input text field.  The label requires a pop-up dialog to change the value
7608     * while the text field makes it possible to change the value on the panel.  This also makes
7609     * it possible to change the value using the web server.
7610     */
7611    void selectBlockContentsType() {
7612        int response = JmriJOptionPane.showConfirmDialog(null,
7613            Bundle.getMessage("BlockSelectType"),
7614            Bundle.getMessage("BlockSelectTitle"),
7615            JmriJOptionPane.YES_NO_OPTION);
7616
7617        if (response == JmriJOptionPane.YES_OPTION) {
7618            addBlockContents();
7619            return;
7620        }
7621
7622        var length = JmriJOptionPane.showInputDialog(null,
7623            Bundle.getMessage("BlockSelectSize"),
7624            "5");
7625
7626        int textLength;
7627        try {
7628           textLength = Integer.parseInt(length);
7629        }
7630        catch (NumberFormatException e) {
7631           textLength = 5;
7632        }
7633
7634        addInputBlockContents(textLength);
7635    }
7636
7637    void addBlockContents() {
7638        String newName = leToolBarPanel.blockContentsComboBox.getSelectedItemDisplayName();
7639        if (newName == null) {
7640            newName = "";
7641        }
7642
7643        if (newName.isEmpty()) {
7644            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11b"),
7645                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7646            return;
7647        }
7648        BlockContentsIcon l = new BlockContentsIcon(" ", this);
7649        l.setBlock(newName);
7650        Block xMemory = l.getBlock();
7651
7652        if (xMemory != null) {
7653            String uname = xMemory.getDisplayName();
7654            if (!uname.equals(newName)) {
7655                // put the system name in the memory field
7656                leToolBarPanel.blockContentsComboBox.setSelectedItem(xMemory);
7657            }
7658        }
7659        setNextLocation(l);
7660        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7661        l.setDisplayLevel(Editor.LABELS);
7662        l.setForeground(defaultTextColor);
7663        try {
7664            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7665        } catch (Positionable.DuplicateIdException e) {
7666            // This should never happen
7667            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7668        }
7669    }
7670
7671    void addInputBlockContents(int textFieldLength) {
7672        String newName = leToolBarPanel.blockContentsComboBox.getSelectedItemDisplayName();
7673        if (newName == null) {
7674            newName = "";
7675        }
7676
7677        if (newName.isEmpty()) {
7678            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11b"),
7679                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7680            return;
7681        }
7682        BlockContentsInputIcon l = new BlockContentsInputIcon(textFieldLength, this);
7683        l.setBlock(newName);
7684        setNextLocation(l);
7685        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7686        l.setDisplayLevel(Editor.MEMORIES);
7687        l.setForeground(defaultTextColor);
7688        try {
7689            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7690        } catch (Positionable.DuplicateIdException e) {
7691            // This should never happen
7692            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7693        }
7694    }
7695
7696    /**
7697     * Add a Reporter Icon to the panel.
7698     *
7699     * @param reporter the reporter icon to add.
7700     * @param xx       the horizontal location.
7701     * @param yy       the vertical location.
7702     */
7703    public void addReporter(@Nonnull Reporter reporter, int xx, int yy) {
7704        ReporterIcon l = new ReporterIcon(this);
7705        l.setReporter(reporter);
7706        l.setLocation(xx, yy);
7707        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7708        l.setDisplayLevel(Editor.LABELS);
7709        unionToPanelBounds(l.getBounds());
7710        try {
7711            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7712        } catch (Positionable.DuplicateIdException e) {
7713            // This should never happen
7714            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7715        }
7716    }
7717
7718    /**
7719     * Add an icon to the target
7720     */
7721    void addIcon() {
7722        PositionableLabel l = new PositionableLabel(leToolBarPanel.iconEditor.getIcon(0), this);
7723        setNextLocation(l);
7724        l.setDisplayLevel(Editor.ICONS);
7725        unionToPanelBounds(l.getBounds());
7726        l.updateSize();
7727        try {
7728            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7729        } catch (Positionable.DuplicateIdException e) {
7730            // This should never happen
7731            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7732        }
7733    }
7734
7735    /**
7736     * Add a LogixNG icon to the target
7737     */
7738    void addLogixNGIcon() {
7739        LogixNGIcon l = new LogixNGIcon(leToolBarPanel.logixngEditor.getIcon(0), this);
7740        setNextLocation(l);
7741        l.setDisplayLevel(Editor.ICONS);
7742        unionToPanelBounds(l.getBounds());
7743        l.updateSize();
7744        try {
7745            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7746        } catch (Positionable.DuplicateIdException e) {
7747            // This should never happen
7748            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7749        }
7750    }
7751
7752    /**
7753     * Add a LogixNG icon to the target
7754     */
7755    void addAudioIcon() {
7756        String audioName = leToolBarPanel.textAudioComboBox.getSelectedItemDisplayName();
7757        if (audioName == null) {
7758            audioName = "";
7759        }
7760
7761        if (audioName.isEmpty()) {
7762            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11d"),
7763                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7764            return;
7765        }
7766
7767        AudioIcon l = new AudioIcon(leToolBarPanel.audioEditor.getIcon(0), this);
7768        l.setAudio(audioName);
7769        Audio xAudio = l.getAudio();
7770
7771        if (xAudio != null) {
7772            String uname = xAudio.getDisplayName();
7773            if (!uname.equals(audioName)) {
7774                // put the system name in the memory field
7775                leToolBarPanel.textAudioComboBox.setSelectedItem(xAudio);
7776            }
7777        }
7778
7779        setNextLocation(l);
7780        l.setDisplayLevel(Editor.ICONS);
7781        unionToPanelBounds(l.getBounds());
7782        l.updateSize();
7783        try {
7784            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7785        } catch (Positionable.DuplicateIdException e) {
7786            // This should never happen
7787            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7788        }
7789    }
7790
7791    /**
7792     * Add a loco marker to the target
7793     */
7794    @Override
7795    public LocoIcon addLocoIcon(@Nonnull String name) {
7796        LocoIcon l = new LocoIcon(this);
7797        Point2D pt = windowCenter();
7798        if (selectionActive) {
7799            pt = MathUtil.midPoint(getSelectionRect());
7800        }
7801        l.setLocation((int) pt.getX(), (int) pt.getY());
7802        putLocoIcon(l, name);
7803        l.setPositionable(true);
7804        unionToPanelBounds(l.getBounds());
7805        return l;
7806    }
7807
7808    @Override
7809    public void putLocoIcon(@Nonnull LocoIcon l, @Nonnull String name) {
7810        super.putLocoIcon(l, name);
7811        markerImage.add(l);
7812        unionToPanelBounds(l.getBounds());
7813    }
7814
7815    private JFileChooser inputFileChooser = null;
7816
7817    /**
7818     * Add a background image
7819     */
7820    public void addBackground() {
7821        if (inputFileChooser == null) {
7822            inputFileChooser = new jmri.util.swing.JmriJFileChooser(
7823                    String.format("%s%sresources%sicons",
7824                            System.getProperty("user.dir"),
7825                            File.separator,
7826                            File.separator));
7827
7828            inputFileChooser.setFileFilter(new FileNameExtensionFilter("Graphics Files", "gif", "jpg", "png"));
7829        }
7830        inputFileChooser.rescanCurrentDirectory();
7831
7832        int retVal = inputFileChooser.showOpenDialog(this);
7833
7834        if (retVal != JFileChooser.APPROVE_OPTION) {
7835            return; // give up if no file selected
7836        }
7837
7838        // NamedIcon icon = new NamedIcon(inputFileChooser.getSelectedFile().getPath(),
7839        // inputFileChooser.getSelectedFile().getPath());
7840        String name = inputFileChooser.getSelectedFile().getPath();
7841
7842        // convert to portable path
7843        name = FileUtil.getPortableFilename(name);
7844
7845        // setup icon
7846        PositionableLabel o = super.setUpBackground(name);
7847        backgroundImage.add(o);
7848        unionToPanelBounds(o.getBounds());
7849        setDirty();
7850    }
7851
7852    // there is no way to call this; could that
7853    //    private boolean remove(@Nonnull Object s)
7854    // is being used instead.
7855    //
7856    ///**
7857    // * Remove a background image from the list of background images
7858    // *
7859    // * @param b PositionableLabel to remove
7860    // */
7861    //private void removeBackground(@Nonnull PositionableLabel b) {
7862    //    if (backgroundImage.contains(b)) {
7863    //        backgroundImage.remove(b);
7864    //        setDirty();
7865    //    }
7866    //}
7867    /**
7868     * add a layout shape to the list of layout shapes
7869     *
7870     * @param p Point2D where the shape should be
7871     * @return the LayoutShape
7872     */
7873    @Nonnull
7874    private LayoutShape addLayoutShape(@Nonnull Point2D p) {
7875        // get unique name
7876        String name = finder.uniqueName("S", getLayoutShapes().size() + 1);
7877
7878        // create object
7879        LayoutShape o = new LayoutShape(name, p, this);
7880        layoutShapes.add(o);
7881        unionToPanelBounds(o.getBounds());
7882        setDirty();
7883        return o;
7884    }
7885
7886    /**
7887     * Remove a layout shape from the list of layout shapes
7888     *
7889     * @param s the LayoutShape to add
7890     * @return true if added
7891     */
7892    public boolean removeLayoutShape(@Nonnull LayoutShape s) {
7893        boolean result = false;
7894        if (layoutShapes.contains(s)) {
7895            layoutShapes.remove(s);
7896            setDirty();
7897            result = true;
7898            redrawPanel();
7899        }
7900        return result;
7901    }
7902
7903    /**
7904     * Invoke a window to allow you to add a MultiSensor indicator to the target
7905     */
7906    private int multiLocX;
7907    private int multiLocY;
7908
7909    void startMultiSensor() {
7910        multiLocX = xLoc;
7911        multiLocY = yLoc;
7912
7913        if (leToolBarPanel.multiSensorFrame == null) {
7914            // create a common edit frame
7915            leToolBarPanel.multiSensorFrame = new MultiSensorIconFrame(this);
7916            leToolBarPanel.multiSensorFrame.initComponents();
7917            leToolBarPanel.multiSensorFrame.pack();
7918        }
7919        leToolBarPanel.multiSensorFrame.setVisible(true);
7920    }
7921
7922    // Invoked when window has new multi-sensor ready
7923    public void addMultiSensor(@Nonnull MultiSensorIcon l) {
7924        l.setLocation(multiLocX, multiLocY);
7925        try {
7926            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7927        } catch (Positionable.DuplicateIdException e) {
7928            // This should never happen
7929            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7930        }
7931        leToolBarPanel.multiSensorFrame.dispose();
7932        leToolBarPanel.multiSensorFrame = null;
7933    }
7934
7935    /**
7936     * Set object location and size for icon and label object as it is created.
7937     * Size comes from the preferredSize; location comes from the fields where
7938     * the user can spec it.
7939     *
7940     * @param obj the positionable object.
7941     */
7942    @Override
7943    public void setNextLocation(@Nonnull Positionable obj) {
7944        obj.setLocation(xLoc, yLoc);
7945    }
7946
7947    //
7948    // singleton (one per-LayoutEditor) accessors
7949    //
7950    private ConnectivityUtil conTools = null;
7951
7952    @Nonnull
7953    public ConnectivityUtil getConnectivityUtil() {
7954        if (conTools == null) {
7955            conTools = new ConnectivityUtil(this);
7956        }
7957        return conTools;
7958    }
7959
7960    private LayoutEditorTools tools = null;
7961
7962    @Nonnull
7963    public LayoutEditorTools getLETools() {
7964        if (tools == null) {
7965            tools = new LayoutEditorTools(this);
7966        }
7967        return tools;
7968    }
7969
7970    private LayoutEditorAuxTools auxTools = null;
7971
7972    @Override
7973    @Nonnull
7974    public LayoutEditorAuxTools getLEAuxTools() {
7975        if (auxTools == null) {
7976            auxTools = new LayoutEditorAuxTools(this);
7977        }
7978        return auxTools;
7979    }
7980
7981    private LayoutEditorChecks layoutEditorChecks = null;
7982
7983    @Nonnull
7984    public LayoutEditorChecks getLEChecks() {
7985        if (layoutEditorChecks == null) {
7986            layoutEditorChecks = new LayoutEditorChecks(this);
7987        }
7988        return layoutEditorChecks;
7989    }
7990
7991    /**
7992     * Invoked by DeletePanel menu item Validate user intent before deleting
7993     */
7994    @Override
7995    public boolean deletePanel() {
7996        if (canDeletePanel()) {
7997            // verify deletion
7998            if (!super.deletePanel()) {
7999                return false; // return without deleting if "No" response
8000            }
8001            clearLayoutTracks();
8002            return true;
8003        }
8004        return false;
8005    }
8006
8007    /**
8008     * Check for conditions that prevent a delete.
8009     * <ul>
8010     * <li>The panel has active edge connector links</li>
8011     * <li>The panel is used by EntryExit</li>
8012     * </ul>
8013     * @return true if ok to delete
8014     */
8015    public boolean canDeletePanel() {
8016        var messages = new ArrayList<String>();
8017
8018        var points = getPositionablePoints();
8019        for (PositionablePoint point : points) {
8020            if (point.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
8021                var panelName = point.getLinkedEditorName();
8022                if (!panelName.isEmpty()) {
8023                    messages.add(Bundle.getMessage("ActiveEdgeConnector", point.getId(), point.getLinkedEditorName()));
8024                }
8025            }
8026        }
8027
8028        var entryExitPairs = InstanceManager.getDefault(jmri.jmrit.entryexit.EntryExitPairs.class);
8029        if (!entryExitPairs.getNxSource(this).isEmpty()) {
8030            messages.add(Bundle.getMessage("ActiveEntryExit"));
8031        }
8032
8033        if (!messages.isEmpty()) {
8034            StringBuilder msg = new StringBuilder(Bundle.getMessage("PanelRelationshipsError"));
8035            for (String message : messages) {
8036                msg.append(message);
8037            }
8038            JmriJOptionPane.showMessageDialog(null,
8039                    msg.toString(),
8040                    Bundle.getMessage("ErrorTitle"), // NOI18N
8041                    JmriJOptionPane.ERROR_MESSAGE);
8042        }
8043
8044        return messages.isEmpty();
8045    }
8046
8047    /**
8048     * Control whether target panel items are editable. Does this by invoking
8049     * the {@link Editor#setAllEditable} function of the parent class. This also
8050     * controls the relevant pop-up menu items (which are the primary way that
8051     * items are edited).
8052     *
8053     * @param editable true for editable.
8054     */
8055    @Override
8056    public void setAllEditable(boolean editable) {
8057        int restoreScroll = _scrollState;
8058
8059        super.setAllEditable(editable);
8060
8061        if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
8062            if (editable) {
8063                createfloatingEditToolBoxFrame();
8064                createFloatingHelpPanel();
8065            } else {
8066                deletefloatingEditToolBoxFrame();
8067            }
8068        } else {
8069            editToolBarContainerPanel.setVisible(editable);
8070        }
8071        setShowHidden(editable);
8072
8073        if (editable) {
8074            setScroll(Editor.SCROLL_BOTH);
8075            _scrollState = restoreScroll;
8076        } else {
8077            setScroll(_scrollState);
8078        }
8079
8080        // these may not be set up yet...
8081        if (helpBarPanel != null) {
8082            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
8083                if (floatEditHelpPanel != null) {
8084                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
8085                }
8086            } else {
8087                helpBarPanel.setVisible(editable && getShowHelpBar());
8088            }
8089        }
8090        awaitingIconChange = false;
8091        editModeCheckBoxMenuItem.setSelected(editable);
8092        redrawPanel();
8093    }
8094
8095    /**
8096     * Control whether panel items are positionable. Markers are always
8097     * positionable.
8098     *
8099     * @param state true for positionable.
8100     */
8101    @Override
8102    public void setAllPositionable(boolean state) {
8103        super.setAllPositionable(state);
8104
8105        markerImage.forEach((p) -> p.setPositionable(true));
8106    }
8107
8108    /**
8109     * Control whether target panel items are controlling layout items. Does
8110     * this by invoke the {@link Positionable#setControlling} function of each
8111     * item on the target panel. This also controls the relevant pop-up menu
8112     * items.
8113     *
8114     * @param state true for controlling.
8115     */
8116    public void setTurnoutAnimation(boolean state) {
8117        if (animationCheckBoxMenuItem.isSelected() != state) {
8118            animationCheckBoxMenuItem.setSelected(state);
8119        }
8120
8121        if (animatingLayout != state) {
8122            animatingLayout = state;
8123            redrawPanel();
8124        }
8125    }
8126
8127    public boolean isAnimating() {
8128        return animatingLayout;
8129    }
8130
8131    public boolean getScroll() {
8132        // deprecated but kept to allow opening files
8133        // on version 2.5.1 and earlier
8134        return _scrollState != Editor.SCROLL_NONE;
8135    }
8136
8137//    public Color getDefaultBackgroundColor() {
8138//        return defaultBackgroundColor;
8139//    }
8140    public String getDefaultTrackColor() {
8141        return ColorUtil.colorToColorName(defaultTrackColor);
8142    }
8143
8144    /**
8145     *
8146     * Getter defaultTrackColor.
8147     *
8148     * @return block default color as Color
8149     */
8150    @Nonnull
8151    public Color getDefaultTrackColorColor() {
8152        return defaultTrackColor;
8153    }
8154
8155    @Nonnull
8156    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
8157    public String getDefaultOccupiedTrackColor() {
8158        return ColorUtil.colorToColorName(defaultOccupiedTrackColor);
8159    }
8160
8161    /**
8162     *
8163     * Getter defaultOccupiedTrackColor.
8164     *
8165     * @return block default occupied color as Color
8166     */
8167    @Nonnull
8168    public Color getDefaultOccupiedTrackColorColor() {
8169        return defaultOccupiedTrackColor;
8170    }
8171
8172    @Nonnull
8173    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
8174    public String getDefaultAlternativeTrackColor() {
8175        return ColorUtil.colorToColorName(defaultAlternativeTrackColor);
8176    }
8177
8178    /**
8179     *
8180     * Getter defaultAlternativeTrackColor.
8181     *
8182     * @return block default alternative color as Color
8183     */
8184    @Nonnull
8185    public Color getDefaultAlternativeTrackColorColor() {
8186        return defaultAlternativeTrackColor;
8187    }
8188
8189    @Nonnull
8190    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
8191    public String getDefaultTextColor() {
8192        return ColorUtil.colorToColorName(defaultTextColor);
8193    }
8194
8195    @Nonnull
8196    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
8197    public String getTurnoutCircleColor() {
8198        return ColorUtil.colorToColorName(turnoutCircleColor);
8199    }
8200
8201    @Nonnull
8202    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
8203    public String getTurnoutCircleThrownColor() {
8204        return ColorUtil.colorToColorName(turnoutCircleThrownColor);
8205    }
8206
8207    public boolean isTurnoutFillControlCircles() {
8208        return turnoutFillControlCircles;
8209    }
8210
8211    public int getTurnoutCircleSize() {
8212        return turnoutCircleSize;
8213    }
8214
8215    public boolean isTurnoutDrawUnselectedLeg() {
8216        return turnoutDrawUnselectedLeg;
8217    }
8218
8219    public boolean isHighlightCursor() {
8220        return highlightCursor;
8221    }
8222
8223    public String getLayoutName() {
8224        return layoutName;
8225    }
8226
8227    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8228    public boolean getShowHelpBar() {
8229        return showHelpBar;
8230    }
8231
8232    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8233    public boolean getDrawGrid() {
8234        return drawGrid;
8235    }
8236
8237    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8238    public boolean getSnapOnAdd() {
8239        return snapToGridOnAdd;
8240    }
8241
8242    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8243    public boolean getSnapOnMove() {
8244        return snapToGridOnMove;
8245    }
8246
8247    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8248    public boolean getAntialiasingOn() {
8249        return antialiasingOn;
8250    }
8251
8252    public boolean isDrawLayoutTracksLabel() {
8253        return drawLayoutTracksLabel;
8254    }
8255
8256    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8257    public boolean getHighlightSelectedBlock() {
8258        return highlightSelectedBlockFlag;
8259    }
8260
8261    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8262    public boolean getTurnoutCircles() {
8263        return turnoutCirclesWithoutEditMode;
8264    }
8265
8266    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8267    public boolean getTooltipsNotEdit() {
8268        return tooltipsWithoutEditMode;
8269    }
8270
8271    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8272    public boolean getTooltipsInEdit() {
8273        return tooltipsInEditMode;
8274    }
8275
8276    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8277    public boolean getAutoBlockAssignment() {
8278        return autoAssignBlocks;
8279    }
8280
8281    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight) {
8282        setLayoutDimensions(windowWidth, windowHeight, windowX, windowY, panelWidth, panelHeight, false);
8283    }
8284
8285    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight, boolean merge) {
8286
8287        gContext.setUpperLeftX(windowX);
8288        gContext.setUpperLeftY(windowY);
8289        setLocation(gContext.getUpperLeftX(), gContext.getUpperLeftY());
8290
8291        gContext.setWindowWidth(windowWidth);
8292        gContext.setWindowHeight(windowHeight);
8293        setSize(windowWidth, windowHeight);
8294
8295        Rectangle2D panelBounds = new Rectangle2D.Double(0.0, 0.0, panelWidth, panelHeight);
8296
8297        if (merge) {
8298            panelBounds.add(calculateMinimumLayoutBounds());
8299        }
8300        setPanelBounds(panelBounds);
8301    }
8302
8303    @Nonnull
8304    public Rectangle2D getPanelBounds() {
8305        return new Rectangle2D.Double(0.0, 0.0, gContext.getLayoutWidth(), gContext.getLayoutHeight());
8306    }
8307
8308    public void setPanelBounds(@Nonnull Rectangle2D newBounds) {
8309        // don't let origin go negative
8310        newBounds = newBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8311
8312        if (!getPanelBounds().equals(newBounds)) {
8313            gContext.setLayoutWidth((int) newBounds.getWidth());
8314            gContext.setLayoutHeight((int) newBounds.getHeight());
8315            resetTargetSize();
8316        }
8317        log.debug("setPanelBounds(({})", newBounds);
8318    }
8319
8320    private void resetTargetSize() {
8321        int newTargetWidth = (int) (gContext.getLayoutWidth() * getZoom());
8322        int newTargetHeight = (int) (gContext.getLayoutHeight() * getZoom());
8323
8324        Dimension targetPanelSize = getTargetPanelSize();
8325        int oldTargetWidth = (int) targetPanelSize.getWidth();
8326        int oldTargetHeight = (int) targetPanelSize.getHeight();
8327
8328        if ((newTargetWidth != oldTargetWidth) || (newTargetHeight != oldTargetHeight)) {
8329            setTargetPanelSize(newTargetWidth, newTargetHeight);
8330            adjustScrollBars();
8331        }
8332    }
8333
8334    // this will grow the panel bounds based on items added to the layout
8335    @Nonnull
8336    public Rectangle2D unionToPanelBounds(@Nonnull Rectangle2D bounds) {
8337        Rectangle2D result = getPanelBounds();
8338
8339        // make room to expand
8340        Rectangle2D b = MathUtil.inset(bounds, gContext.getGridSize() * gContext.getGridSize2nd() / -2.0);
8341
8342        // don't let origin go negative
8343        b = b.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8344
8345        result.add(b);
8346
8347        setPanelBounds(result);
8348        return result;
8349    }
8350
8351    /**
8352     * @param color value to set the default track color to.
8353     */
8354    public void setDefaultTrackColor(@Nonnull Color color) {
8355        defaultTrackColor = color;
8356        JmriColorChooser.addRecentColor(color);
8357    }
8358
8359    /**
8360     * @param color value to set the default occupied track color to.
8361     */
8362    public void setDefaultOccupiedTrackColor(@Nonnull Color color) {
8363        defaultOccupiedTrackColor = color;
8364        JmriColorChooser.addRecentColor(color);
8365    }
8366
8367    /**
8368     * @param color value to set the default alternate track color to.
8369     */
8370    public void setDefaultAlternativeTrackColor(@Nonnull Color color) {
8371        defaultAlternativeTrackColor = color;
8372        JmriColorChooser.addRecentColor(color);
8373    }
8374
8375    /**
8376     * @param color new color for turnout circle.
8377     */
8378    public void setTurnoutCircleColor(@CheckForNull Color color) {
8379        if (color == null) {
8380            turnoutCircleColor = getDefaultTrackColorColor();
8381        } else {
8382            turnoutCircleColor = color;
8383            JmriColorChooser.addRecentColor(color);
8384        }
8385    }
8386
8387    /**
8388     * @param color new color for turnout circle.
8389     */
8390    public void setTurnoutCircleThrownColor(@CheckForNull Color color) {
8391        if (color == null) {
8392            turnoutCircleThrownColor = getDefaultTrackColorColor();
8393        } else {
8394            turnoutCircleThrownColor = color;
8395            JmriColorChooser.addRecentColor(color);
8396        }
8397    }
8398
8399    /**
8400     * Should only be invoked on the GUI (Swing) thread.
8401     *
8402     * @param state true to fill in turnout control circles, else false.
8403     */
8404    @InvokeOnGuiThread
8405    public void setTurnoutFillControlCircles(boolean state) {
8406        if (turnoutFillControlCircles != state) {
8407            turnoutFillControlCircles = state;
8408            turnoutFillControlCirclesCheckBoxMenuItem.setSelected(turnoutFillControlCircles);
8409        }
8410    }
8411
8412    public void setTurnoutCircleSize(int size) {
8413        // this is an int
8414        turnoutCircleSize = size;
8415
8416        // these are doubles
8417        circleRadius = SIZE * size;
8418        circleDiameter = 2.0 * circleRadius;
8419
8420        setOptionMenuTurnoutCircleSize();
8421    }
8422
8423    /**
8424     * Should only be invoked on the GUI (Swing) thread.
8425     *
8426     * @param state true to draw unselected legs, else false.
8427     */
8428    @InvokeOnGuiThread
8429    public void setTurnoutDrawUnselectedLeg(boolean state) {
8430        if (turnoutDrawUnselectedLeg != state) {
8431            turnoutDrawUnselectedLeg = state;
8432            turnoutDrawUnselectedLegCheckBoxMenuItem.setSelected(turnoutDrawUnselectedLeg);
8433        }
8434    }
8435
8436    /**
8437     * Should only be invoked on the GUI (Swing) thread.
8438     *
8439     * @param state true to enable highlighting the cursor (mouse/finger press/drag)
8440     */
8441    @InvokeOnGuiThread
8442    public void setHighlightCursor(boolean state) {
8443        if (highlightCursor != state) {
8444            highlightCursor = state;
8445            highlightCursorCheckBoxMenuItem.setSelected(highlightCursor);
8446        }
8447    }
8448
8449    /**
8450     * @param color value to set the default text color to.
8451     */
8452    public void setDefaultTextColor(@Nonnull Color color) {
8453        defaultTextColor = color;
8454        JmriColorChooser.addRecentColor(color);
8455    }
8456
8457    /**
8458     * @param color value to set the panel background to.
8459     */
8460    public void setDefaultBackgroundColor(@Nonnull Color color) {
8461        defaultBackgroundColor = color;
8462        JmriColorChooser.addRecentColor(color);
8463    }
8464
8465    public void setLayoutName(@Nonnull String name) {
8466        layoutName = name;
8467    }
8468
8469    /**
8470     * Should only be invoked on the GUI (Swing) thread.
8471     *
8472     * @param state true to show the help bar, else false.
8473     */
8474    @InvokeOnGuiThread  // due to the setSelected call on a possibly-visible item
8475    public void setShowHelpBar(boolean state) {
8476        if (showHelpBar != state) {
8477            showHelpBar = state;
8478
8479            // these may not be set up yet...
8480            if (showHelpCheckBoxMenuItem != null) {
8481                showHelpCheckBoxMenuItem.setSelected(showHelpBar);
8482            }
8483
8484            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
8485                if (floatEditHelpPanel != null) {
8486                    floatEditHelpPanel.setVisible(isEditable() && showHelpBar);
8487                }
8488            } else {
8489                if (helpBarPanel != null) {
8490                    helpBarPanel.setVisible(isEditable() && showHelpBar);
8491
8492                }
8493            }
8494            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".showHelpBar", showHelpBar));
8495        }
8496    }
8497
8498    /**
8499     * Should only be invoked on the GUI (Swing) thread.
8500     *
8501     * @param state true to show the draw grid, else false.
8502     */
8503    @InvokeOnGuiThread
8504    public void setDrawGrid(boolean state) {
8505        if (drawGrid != state) {
8506            drawGrid = state;
8507            showGridCheckBoxMenuItem.setSelected(drawGrid);
8508        }
8509    }
8510
8511    /**
8512     * Should only be invoked on the GUI (Swing) thread.
8513     *
8514     * @param state true to set snap to grid on add, else false.
8515     */
8516    @InvokeOnGuiThread
8517    public void setSnapOnAdd(boolean state) {
8518        if (snapToGridOnAdd != state) {
8519            snapToGridOnAdd = state;
8520            snapToGridOnAddCheckBoxMenuItem.setSelected(snapToGridOnAdd);
8521        }
8522    }
8523
8524    /**
8525     * Should only be invoked on the GUI (Swing) thread.
8526     *
8527     * @param state true to set snap on move, else false.
8528     */
8529    @InvokeOnGuiThread
8530    public void setSnapOnMove(boolean state) {
8531        if (snapToGridOnMove != state) {
8532            snapToGridOnMove = state;
8533            snapToGridOnMoveCheckBoxMenuItem.setSelected(snapToGridOnMove);
8534        }
8535    }
8536
8537    /**
8538     * Should only be invoked on the GUI (Swing) thread.
8539     *
8540     * @param state true to set anti-aliasing flag on, else false.
8541     */
8542    @InvokeOnGuiThread
8543    public void setAntialiasingOn(boolean state) {
8544        if (antialiasingOn != state) {
8545            antialiasingOn = state;
8546
8547            // this may not be set up yet...
8548            if (antialiasingOnCheckBoxMenuItem != null) {
8549                antialiasingOnCheckBoxMenuItem.setSelected(antialiasingOn);
8550
8551            }
8552            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".antialiasingOn", antialiasingOn));
8553        }
8554    }
8555
8556    /**
8557     *
8558     * @param state true to set anti-aliasing flag on, else false.
8559     */
8560    public void setDrawLayoutTracksLabel(boolean state) {
8561        if (drawLayoutTracksLabel != state) {
8562            drawLayoutTracksLabel = state;
8563
8564            // this may not be set up yet...
8565            if (drawLayoutTracksLabelCheckBoxMenuItem != null) {
8566                drawLayoutTracksLabelCheckBoxMenuItem.setSelected(drawLayoutTracksLabel);
8567
8568            }
8569            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".drawLayoutTracksLabel", drawLayoutTracksLabel));
8570        }
8571    }
8572
8573    // enable/disable using the "Extra" color to highlight the selected block
8574    public void setHighlightSelectedBlock(boolean state) {
8575        if (highlightSelectedBlockFlag != state) {
8576            highlightSelectedBlockFlag = state;
8577
8578            // this may not be set up yet...
8579            if (leToolBarPanel.highlightBlockCheckBox != null) {
8580                leToolBarPanel.highlightBlockCheckBox.setSelected(highlightSelectedBlockFlag);
8581
8582            }
8583
8584            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".highlightSelectedBlock", highlightSelectedBlockFlag));
8585
8586            // thread this so it won't break the AppVeyor checks
8587            ThreadingUtil.newThread(() -> {
8588                if (highlightSelectedBlockFlag) {
8589                    // use the "Extra" color to highlight the selected block
8590                    if (!highlightBlockInComboBox(leToolBarPanel.blockIDComboBox)) {
8591                        highlightBlockInComboBox(leToolBarPanel.blockContentsComboBox);
8592                    }
8593                } else {
8594                    // undo using the "Extra" color to highlight the selected block
8595                    Block block = leToolBarPanel.blockIDComboBox.getSelectedItem();
8596                    highlightBlock(null);
8597                    leToolBarPanel.blockIDComboBox.setSelectedItem(block);
8598                }
8599            }).start();
8600        }
8601    }
8602
8603    //
8604    // highlight the block selected by the specified combo Box
8605    //
8606    public boolean highlightBlockInComboBox(@Nonnull NamedBeanComboBox<Block> inComboBox) {
8607        return highlightBlock(inComboBox.getSelectedItem());
8608    }
8609
8610    /**
8611     * highlight the specified block
8612     *
8613     * @param inBlock the block
8614     * @return true if block was highlighted
8615     */
8616    public boolean highlightBlock(@CheckForNull Block inBlock) {
8617        boolean result = false; // assume failure (pessimist!)
8618
8619        if (leToolBarPanel.blockIDComboBox.getSelectedItem() != inBlock) {
8620            leToolBarPanel.blockIDComboBox.setSelectedItem(inBlock);
8621        }
8622
8623        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class
8624        );
8625        Set<Block> l = leToolBarPanel.blockIDComboBox.getManager().getNamedBeanSet();
8626        for (Block b : l) {
8627            LayoutBlock lb = lbm.getLayoutBlock(b);
8628            if (lb != null) {
8629                boolean enable = ((inBlock != null) && b.equals(inBlock));
8630                lb.setUseExtraColor(enable);
8631                result |= enable;
8632            }
8633        }
8634        return result;
8635    }
8636
8637    /**
8638     * highlight the specified layout block
8639     *
8640     * @param inLayoutBlock the layout block
8641     * @return true if layout block was highlighted
8642     */
8643    public boolean highlightLayoutBlock(@Nonnull LayoutBlock inLayoutBlock) {
8644        return highlightBlock(inLayoutBlock.getBlock());
8645    }
8646
8647    public void setTurnoutCircles(boolean state) {
8648        if (turnoutCirclesWithoutEditMode != state) {
8649            turnoutCirclesWithoutEditMode = state;
8650            if (turnoutCirclesOnCheckBoxMenuItem != null) {
8651                turnoutCirclesOnCheckBoxMenuItem.setSelected(turnoutCirclesWithoutEditMode);
8652            }
8653        }
8654    }
8655
8656    public void setAutoBlockAssignment(boolean boo) {
8657        if (autoAssignBlocks != boo) {
8658            autoAssignBlocks = boo;
8659            if (autoAssignBlocksCheckBoxMenuItem != null) {
8660                autoAssignBlocksCheckBoxMenuItem.setSelected(autoAssignBlocks);
8661            }
8662        }
8663    }
8664
8665    public void setTooltipsNotEdit(boolean state) {
8666        if (tooltipsWithoutEditMode != state) {
8667            tooltipsWithoutEditMode = state;
8668            setTooltipSubMenu();
8669            setTooltipsAlwaysOrNever();
8670        }
8671    }
8672
8673    public void setTooltipsInEdit(boolean state) {
8674        if (tooltipsInEditMode != state) {
8675            tooltipsInEditMode = state;
8676            setTooltipSubMenu();
8677            setTooltipsAlwaysOrNever();
8678        }
8679    }
8680
8681    private void setTooltipsAlwaysOrNever() {
8682        tooltipsAlwaysOrNever = ((tooltipsInEditMode && tooltipsWithoutEditMode) ||
8683                    (!tooltipsInEditMode && !tooltipsWithoutEditMode));
8684    }
8685
8686    private void setTooltipSubMenu() {
8687        if (tooltipNoneMenuItem != null) {
8688            tooltipNoneMenuItem.setSelected((!tooltipsInEditMode) && (!tooltipsWithoutEditMode));
8689            tooltipAlwaysMenuItem.setSelected((tooltipsInEditMode) && (tooltipsWithoutEditMode));
8690            tooltipInEditMenuItem.setSelected((tooltipsInEditMode) && (!tooltipsWithoutEditMode));
8691            tooltipNotInEditMenuItem.setSelected((!tooltipsInEditMode) && (tooltipsWithoutEditMode));
8692        }
8693    }
8694
8695    // accessor routines for turnout size parameters
8696    public void setTurnoutBX(double bx) {
8697        turnoutBX = bx;
8698        setDirty();
8699    }
8700
8701    public double getTurnoutBX() {
8702        return turnoutBX;
8703    }
8704
8705    public void setTurnoutCX(double cx) {
8706        turnoutCX = cx;
8707        setDirty();
8708    }
8709
8710    public double getTurnoutCX() {
8711        return turnoutCX;
8712    }
8713
8714    public void setTurnoutWid(double wid) {
8715        turnoutWid = wid;
8716        setDirty();
8717    }
8718
8719    public double getTurnoutWid() {
8720        return turnoutWid;
8721    }
8722
8723    public void setXOverLong(double lg) {
8724        xOverLong = lg;
8725        setDirty();
8726    }
8727
8728    public double getXOverLong() {
8729        return xOverLong;
8730    }
8731
8732    public void setXOverHWid(double hwid) {
8733        xOverHWid = hwid;
8734        setDirty();
8735    }
8736
8737    public double getXOverHWid() {
8738        return xOverHWid;
8739    }
8740
8741    public void setXOverShort(double sh) {
8742        xOverShort = sh;
8743        setDirty();
8744    }
8745
8746    public double getXOverShort() {
8747        return xOverShort;
8748    }
8749
8750    // reset turnout sizes to program defaults
8751    private void resetTurnoutSize() {
8752        turnoutBX = LayoutTurnout.turnoutBXDefault;
8753        turnoutCX = LayoutTurnout.turnoutCXDefault;
8754        turnoutWid = LayoutTurnout.turnoutWidDefault;
8755        xOverLong = LayoutTurnout.xOverLongDefault;
8756        xOverHWid = LayoutTurnout.xOverHWidDefault;
8757        xOverShort = LayoutTurnout.xOverShortDefault;
8758        setDirty();
8759    }
8760
8761    public void setDirectTurnoutControl(boolean boo) {
8762        useDirectTurnoutControl = boo;
8763        useDirectTurnoutControlCheckBoxMenuItem.setSelected(useDirectTurnoutControl);
8764    }
8765
8766    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8767    public boolean getDirectTurnoutControl() {
8768        return useDirectTurnoutControl;
8769    }
8770
8771    // final initialization routine for loading a LayoutEditor
8772    public void setConnections() {
8773        getLayoutTracks().forEach((lt) -> lt.setObjects(this));
8774        getLEAuxTools().initializeBlockConnectivity();
8775        log.debug("Initializing Block Connectivity for {}", getLayoutName());
8776
8777        // reset the panel changed bit
8778        resetDirty();
8779    }
8780
8781    // these are convenience methods to return rectangles
8782    // to use when (hit point-in-rect testing
8783    //
8784    // compute the control point rect at inPoint
8785    public @Nonnull
8786    Rectangle2D layoutEditorControlRectAt(@Nonnull Point2D inPoint) {
8787        return new Rectangle2D.Double(inPoint.getX() - SIZE,
8788                inPoint.getY() - SIZE, SIZE2, SIZE2);
8789    }
8790
8791    // compute the turnout circle control rect at inPoint
8792    public @Nonnull
8793    Rectangle2D layoutEditorControlCircleRectAt(@Nonnull Point2D inPoint) {
8794        return new Rectangle2D.Double(inPoint.getX() - circleRadius,
8795                inPoint.getY() - circleRadius, circleDiameter, circleDiameter);
8796    }
8797
8798    /**
8799     * Special internal class to allow drawing of layout to a JLayeredPane This
8800     * is the 'target' pane where the layout is displayed
8801     */
8802    @Override
8803    public void paintTargetPanel(@Nonnull Graphics g) {
8804        // Nothing to do here
8805        // All drawing has been moved into LayoutEditorComponent
8806        // which calls draw.
8807        // This is so the layout is drawn at level three
8808        // (above or below the Positionables)
8809    }
8810
8811    // get selection rectangle
8812    @Nonnull
8813    public Rectangle2D getSelectionRect() {
8814        double selX = Math.min(selectionX, selectionX + selectionWidth);
8815        double selY = Math.min(selectionY, selectionY + selectionHeight);
8816        return new Rectangle2D.Double(selX, selY,
8817                Math.abs(selectionWidth), Math.abs(selectionHeight));
8818    }
8819
8820    // set selection rectangle
8821    public void setSelectionRect(@Nonnull Rectangle2D selectionRect) {
8822        // selectionRect = selectionRect.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8823        selectionX = selectionRect.getX();
8824        selectionY = selectionRect.getY();
8825        selectionWidth = selectionRect.getWidth();
8826        selectionHeight = selectionRect.getHeight();
8827
8828        // There's already code in the super class (Editor) to draw
8829        // the selection rect... We just have to set _selectRect
8830        _selectRect = MathUtil.rectangle2DToRectangle(selectionRect);
8831
8832        selectionRect = MathUtil.scale(selectionRect, getZoom());
8833
8834        JComponent targetPanel = getTargetPanel();
8835        Rectangle targetRect = targetPanel.getVisibleRect();
8836        // this will make it the size of the targetRect
8837        // (effectively centering it onscreen)
8838        Rectangle2D selRect2D = MathUtil.inset(selectionRect,
8839                (selectionRect.getWidth() - targetRect.getWidth()) / 2.0,
8840                (selectionRect.getHeight() - targetRect.getHeight()) / 2.0);
8841        // don't let the origin go negative
8842        selRect2D = selRect2D.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8843        Rectangle selRect = MathUtil.rectangle2DToRectangle(selRect2D);
8844        if (!targetRect.contains(selRect)) {
8845            targetPanel.scrollRectToVisible(selRect);
8846        }
8847
8848        clearSelectionGroups();
8849        selectionActive = true;
8850        createSelectionGroups();
8851        // redrawPanel(); // createSelectionGroups already calls this
8852    }
8853
8854    public void setSelectRect(Rectangle rectangle) {
8855        _selectRect = rectangle;
8856    }
8857
8858    /*
8859    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
8860    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<LayoutTrack> layoutTrackClass) {
8861    return getLayoutTracks().stream()
8862    .filter(item -> item instanceof PositionablePoint)
8863    .filter(layoutTrackClass::isInstance)
8864    //.map(layoutTrackClass::cast)  // TODO: Do we need this? if not dead-code-strip
8865    .collect(Collectors.toList());
8866    }
8867
8868    // TODO: This compiles but I can't get the syntax correct to pass the array of (sub-)classes
8869    public List<LayoutTrack> getLayoutTracksOfClasses(@Nonnull List<Class<? extends LayoutTrack>> layoutTrackClasses) {
8870    return getLayoutTracks().stream()
8871    .filter(o -> layoutTrackClasses.contains(o.getClass()))
8872    .collect(Collectors.toList());
8873    }
8874
8875    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
8876    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<? extends LayoutTrack> layoutTrackClass) {
8877    return getLayoutTracksOfClasses(new ArrayList<>(Arrays.asList(layoutTrackClass)));
8878    }
8879
8880    public List<PositionablePoint> getPositionablePoints() {
8881    return getLayoutTracksOfClass(PositionablePoint);
8882    }
8883     */
8884    @Override
8885    public @Nonnull
8886    Stream<LayoutTrack> getLayoutTracksOfClass(Class<? extends LayoutTrack> layoutTrackClass) {
8887        return getLayoutTracks().stream()
8888                .filter(layoutTrackClass::isInstance)
8889                .map(layoutTrackClass::cast);
8890    }
8891
8892    @Override
8893    public @Nonnull
8894    Stream<LayoutTrackView> getLayoutTrackViewsOfClass(Class<? extends LayoutTrackView> layoutTrackViewClass) {
8895        return getLayoutTrackViews().stream()
8896                .filter(layoutTrackViewClass::isInstance)
8897                .map(layoutTrackViewClass::cast);
8898    }
8899
8900    @Override
8901    public @Nonnull
8902    List<PositionablePointView> getPositionablePointViews() {
8903        return getLayoutTrackViewsOfClass(PositionablePointView.class)
8904                .map(PositionablePointView.class::cast)
8905                .collect(Collectors.toCollection(ArrayList::new));
8906    }
8907
8908    @Override
8909    public @Nonnull
8910    List<PositionablePoint> getPositionablePoints() {
8911        return getLayoutTracksOfClass(PositionablePoint.class)
8912                .map(PositionablePoint.class::cast)
8913                .collect(Collectors.toCollection(ArrayList::new));
8914    }
8915
8916    public @Nonnull
8917    List<LayoutSlipView> getLayoutSlipViews() {
8918        return getLayoutTrackViewsOfClass(LayoutSlipView.class)
8919                .map(LayoutSlipView.class::cast)
8920                .collect(Collectors.toCollection(ArrayList::new));
8921    }
8922
8923    @Override
8924    public @Nonnull
8925    List<LayoutSlip> getLayoutSlips() {
8926        return getLayoutTracksOfClass(LayoutSlip.class)
8927                .map(LayoutSlip.class::cast)
8928                .collect(Collectors.toCollection(ArrayList::new));
8929    }
8930
8931    @Override
8932    public @Nonnull
8933    List<TrackSegmentView> getTrackSegmentViews() {
8934        return getLayoutTrackViewsOfClass(TrackSegmentView.class)
8935                .map(TrackSegmentView.class::cast)
8936                .collect(Collectors.toCollection(ArrayList::new));
8937    }
8938
8939    @Override
8940    public @Nonnull
8941    List<TrackSegment> getTrackSegments() {
8942        return getLayoutTracksOfClass(TrackSegment.class)
8943                .map(TrackSegment.class::cast)
8944                .collect(Collectors.toCollection(ArrayList::new));
8945    }
8946
8947    public @Nonnull
8948    List<LayoutTurnoutView> getLayoutTurnoutViews() { // this specifically does not include slips
8949        return getLayoutTrackViews().stream() // next line excludes LayoutSlips
8950                .filter((o) -> (!(o instanceof LayoutSlipView) && (o instanceof LayoutTurnoutView)))
8951                .map(LayoutTurnoutView.class::cast)
8952                .collect(Collectors.toCollection(ArrayList::new));
8953    }
8954
8955    @Override
8956    public @Nonnull
8957    List<LayoutTurnout> getLayoutTurnouts() { // this specifically does not include slips
8958        return getLayoutTracks().stream() // next line excludes LayoutSlips
8959                .filter((o) -> (!(o instanceof LayoutSlip) && (o instanceof LayoutTurnout)))
8960                .map(LayoutTurnout.class::cast)
8961                .collect(Collectors.toCollection(ArrayList::new));
8962    }
8963
8964    @Override
8965    public @Nonnull
8966    List<LayoutTurntable> getLayoutTurntables() {
8967        return getLayoutTracksOfClass(LayoutTurntable.class)
8968                .map(LayoutTurntable.class::cast)
8969                .collect(Collectors.toCollection(ArrayList::new));
8970    }
8971
8972    public @Nonnull
8973    List<LayoutTurntableView> getLayoutTurntableViews() {
8974        return getLayoutTrackViewsOfClass(LayoutTurntableView.class)
8975                .map(LayoutTurntableView.class::cast)
8976                .collect(Collectors.toCollection(ArrayList::new));
8977    }
8978
8979    @Override
8980    public @Nonnull
8981    List<LayoutTraverser> getLayoutTraversers() {
8982        return getLayoutTracksOfClass(LayoutTraverser.class)
8983                .map(LayoutTraverser.class::cast)
8984                .collect(Collectors.toCollection(ArrayList::new));
8985    }
8986
8987    public @Nonnull
8988    List<LayoutTraverserView> getLayoutTraverserViews() {
8989        return getLayoutTrackViewsOfClass(LayoutTraverserView.class)
8990                .map(LayoutTraverserView.class::cast)
8991                .collect(Collectors.toCollection(ArrayList::new));
8992    }
8993
8994    @Override
8995    public @Nonnull
8996    List<LevelXing> getLevelXings() {
8997        return getLayoutTracksOfClass(LevelXing.class)
8998                .map(LevelXing.class::cast)
8999                .collect(Collectors.toCollection(ArrayList::new));
9000    }
9001
9002    @Override
9003    public @Nonnull
9004    List<LevelXingView> getLevelXingViews() {
9005        return getLayoutTrackViewsOfClass(LevelXingView.class)
9006                .map(LevelXingView.class::cast)
9007                .collect(Collectors.toCollection(ArrayList::new));
9008    }
9009
9010    /**
9011     * Read-only access to the list of LayoutTrack family objects. The returned
9012     * list will throw UnsupportedOperationException if you attempt to modify
9013     * it.
9014     *
9015     * @return unmodifiable copy of layout track list.
9016     */
9017    @Override
9018    @Nonnull
9019    public final List<LayoutTrack> getLayoutTracks() {
9020        return Collections.unmodifiableList(layoutTrackList);
9021    }
9022
9023    public @Nonnull
9024    List<LayoutTurnoutView> getLayoutTurnoutAndSlipViews() {
9025        return getLayoutTrackViewsOfClass(LayoutTurnoutView.class
9026        )
9027                .map(LayoutTurnoutView.class::cast)
9028                .collect(Collectors.toCollection(ArrayList::new));
9029    }
9030
9031    @Override
9032    public @Nonnull
9033    List<LayoutTurnout> getLayoutTurnoutsAndSlips() {
9034        return getLayoutTracksOfClass(LayoutTurnout.class
9035        )
9036                .map(LayoutTurnout.class::cast)
9037                .collect(Collectors.toCollection(ArrayList::new));
9038    }
9039
9040    /**
9041     * Read-only access to the list of LayoutTrackView family objects. The
9042     * returned list will throw UnsupportedOperationException if you attempt to
9043     * modify it.
9044     *
9045     * @return unmodifiable copy of track views.
9046     */
9047    @Override
9048    @Nonnull
9049    public final List<LayoutTrackView> getLayoutTrackViews() {
9050        return Collections.unmodifiableList(layoutTrackViewList);
9051    }
9052
9053    private final List<LayoutTrack> layoutTrackList = new ArrayList<>();
9054    private final List<LayoutTrackView> layoutTrackViewList = new ArrayList<>();
9055    private final Map<LayoutTrack, LayoutTrackView> trkToView = new HashMap<>();
9056    private final Map<LayoutTrackView, LayoutTrack> viewToTrk = new HashMap<>();
9057
9058    // temporary
9059    @Override
9060    public final LayoutTrackView getLayoutTrackView(LayoutTrack trk) {
9061        LayoutTrackView lv = trkToView.get(trk);
9062        if (lv == null) {
9063            log.warn("No View found for {} class {}", trk, trk.getClass());
9064            throw new IllegalArgumentException("No View found: " + trk.getClass());
9065        }
9066        return lv;
9067    }
9068
9069    // temporary
9070    @Override
9071    public final LevelXingView getLevelXingView(LevelXing xing) {
9072        LayoutTrackView lv = trkToView.get(xing);
9073        if (lv == null) {
9074            log.warn("No View found for {} class {}", xing, xing.getClass());
9075            throw new IllegalArgumentException("No View found: " + xing.getClass());
9076        }
9077        if (lv instanceof LevelXingView) {
9078            return (LevelXingView) lv;
9079        } else {
9080            log.error("wrong type {} {} found {}", xing, xing.getClass(), lv);
9081        }
9082        throw new IllegalArgumentException("Wrong type: " + xing.getClass());
9083    }
9084
9085    // temporary
9086    @Override
9087    public final LayoutTurnoutView getLayoutTurnoutView(LayoutTurnout to) {
9088        LayoutTrackView lv = trkToView.get(to);
9089        if (lv == null) {
9090            log.warn("No View found for {} class {}", to, to.getClass());
9091            throw new IllegalArgumentException("No View found: " + to);
9092        }
9093        if (lv instanceof LayoutTurnoutView) {
9094            return (LayoutTurnoutView) lv;
9095        } else {
9096            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9097        }
9098        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9099    }
9100
9101    // temporary
9102    @Override
9103    public final LayoutTurntableView getLayoutTurntableView(LayoutTurntable to) {
9104        LayoutTrackView lv = trkToView.get(to);
9105        if (lv == null) {
9106            log.warn("No View found for {} class {}", to, to.getClass());
9107            throw new IllegalArgumentException("No matching View found: " + to);
9108        }
9109        if (lv instanceof LayoutTurntableView) {
9110            return (LayoutTurntableView) lv;
9111        } else {
9112            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9113        }
9114        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9115    }
9116
9117    // temporary
9118    @Override
9119    public final LayoutTraverserView getLayoutTraverserView(LayoutTraverser to) {
9120        LayoutTrackView lv = trkToView.get(to);
9121        if (lv == null) {
9122            log.warn("No View found for {} class {}", to, to.getClass());
9123            throw new IllegalArgumentException("No matching View found: " + to);
9124        }
9125        if (lv instanceof LayoutTraverserView) {
9126            return (LayoutTraverserView) lv;
9127        } else {
9128            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9129        }
9130        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9131    }
9132
9133    // temporary
9134    public final LayoutSlipView getLayoutSlipView(LayoutSlip to) {
9135        LayoutTrackView lv = trkToView.get(to);
9136        if (lv == null) {
9137            log.warn("No View found for {} class {}", to, to.getClass());
9138            throw new IllegalArgumentException("No matching View found: " + to);
9139        }
9140        if (lv instanceof LayoutSlipView) {
9141            return (LayoutSlipView) lv;
9142        } else {
9143            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9144        }
9145        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9146    }
9147
9148    // temporary
9149    @Override
9150    public final TrackSegmentView getTrackSegmentView(TrackSegment to) {
9151        LayoutTrackView lv = trkToView.get(to);
9152        if (lv == null) {
9153            log.warn("No View found for {} class {}", to, to.getClass());
9154            throw new IllegalArgumentException("No matching View found: " + to);
9155        }
9156        if (lv instanceof TrackSegmentView) {
9157            return (TrackSegmentView) lv;
9158        } else {
9159            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9160        }
9161        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9162    }
9163
9164    // temporary
9165    @Override
9166    public final PositionablePointView getPositionablePointView(PositionablePoint to) {
9167        LayoutTrackView lv = trkToView.get(to);
9168        if (lv == null) {
9169            log.warn("No View found for {} class {}", to, to.getClass());
9170            throw new IllegalArgumentException("No matching View found: " + to);
9171        }
9172        if (lv instanceof PositionablePointView) {
9173            return (PositionablePointView) lv;
9174        } else {
9175            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
9176        }
9177        throw new IllegalArgumentException("Wrong type: " + to.getClass());
9178    }
9179
9180    /**
9181     * Add a LayoutTrack and LayoutTrackView to the list of LayoutTrack family
9182     * objects.
9183     *
9184     * @param trk the layout track to add.
9185     */
9186    @Override
9187    public final void addLayoutTrack(@Nonnull LayoutTrack trk, @Nonnull LayoutTrackView v) {
9188        log.trace("addLayoutTrack {}", trk);
9189        if (layoutTrackList.contains(trk)) {
9190            log.warn("LayoutTrack {} already being maintained", trk.getName());
9191        }
9192
9193        layoutTrackList.add(trk);
9194        layoutTrackViewList.add(v);
9195        trkToView.put(trk, v);
9196        viewToTrk.put(v, trk);
9197
9198        unionToPanelBounds(v.getBounds()); // temporary - this should probably _not_ be in the topological part
9199
9200    }
9201
9202    /**
9203     * If item present, delete from the list of LayoutTracks and force a dirty
9204     * redraw.
9205     *
9206     * @param trk the layout track to remove and redraw.
9207     * @return true is item was deleted and a redraw done.
9208     */
9209    public final boolean removeLayoutTrackAndRedraw(@Nonnull LayoutTrack trk) {
9210        log.trace("removeLayoutTrackAndRedraw {}", trk);
9211        if (layoutTrackList.contains(trk)) {
9212            removeLayoutTrack(trk);
9213            setDirty();
9214            redrawPanel();
9215            log.trace("removeLayoutTrackAndRedraw present {}", trk);
9216            return true;
9217        }
9218        log.trace("removeLayoutTrackAndRedraw absent {}", trk);
9219        return false;
9220    }
9221
9222    /**
9223     * If item present, delete from the list of LayoutTracks and force a dirty
9224     * redraw.
9225     *
9226     * @param trk the layout track to remove.
9227     */
9228    @Override
9229    public final void removeLayoutTrack(@Nonnull LayoutTrack trk) {
9230        log.trace("removeLayoutTrack {}", trk);
9231        layoutTrackList.remove(trk);
9232        LayoutTrackView v = trkToView.get(trk);
9233        layoutTrackViewList.remove(v);
9234        trkToView.remove(trk);
9235        viewToTrk.remove(v);
9236    }
9237
9238    /**
9239     * Clear the list of layout tracks. Not intended for general use.
9240     * <p>
9241     */
9242    private void clearLayoutTracks() {
9243        layoutTrackList.clear();
9244        layoutTrackViewList.clear();
9245        trkToView.clear();
9246        viewToTrk.clear();
9247    }
9248
9249    @Override
9250    public @Nonnull
9251    List<LayoutShape> getLayoutShapes() {
9252        return layoutShapes;
9253    }
9254
9255    public void sortLayoutShapesByLevel() {
9256        layoutShapes.sort((lhs, rhs) -> {
9257            // -1 == less than, 0 == equal, +1 == greater than
9258            return Integer.signum(lhs.getLevel() - rhs.getLevel());
9259        });
9260    }
9261
9262    /**
9263     * {@inheritDoc}
9264     * <p>
9265     * This implementation is temporary, using the on-screen points from the
9266     * LayoutTrackViews via @{link LayoutEditor#getCoords}.
9267     */
9268    @Override
9269    public int computeDirection(LayoutTrack trk1, HitPointType h1, LayoutTrack trk2, HitPointType h2) {
9270        return Path.computeDirection(
9271                getCoords(trk1, h1),
9272                getCoords(trk2, h2)
9273        );
9274    }
9275
9276    @Override
9277    public int computeDirectionToCenter(@Nonnull LayoutTrack trk1, @Nonnull HitPointType h1, @Nonnull PositionablePoint p) {
9278        return Path.computeDirection(
9279                getCoords(trk1, h1),
9280                getPositionablePointView(p).getCoordsCenter()
9281        );
9282    }
9283
9284    @Override
9285    public int computeDirectionFromCenter(@Nonnull PositionablePoint p, @Nonnull LayoutTrack trk1, @Nonnull HitPointType h1) {
9286        return Path.computeDirection(
9287                getPositionablePointView(p).getCoordsCenter(),
9288                getCoords(trk1, h1)
9289        );
9290    }
9291
9292    @Override
9293    public boolean showAlignPopup(@Nonnull Positionable l) {
9294        return false;
9295    }
9296
9297    @Override
9298    public void showToolTip(
9299            @Nonnull Positionable selection,
9300            @Nonnull JmriMouseEvent event) {
9301        ToolTip tip = selection.getToolTip();
9302        tip.setLocation(selection.getX() + selection.getWidth() / 2, selection.getY() + selection.getHeight());
9303        setToolTip(tip);
9304    }
9305
9306    @Override
9307    public void addToPopUpMenu(
9308            @Nonnull NamedBean nb,
9309            @Nonnull JMenuItem item,
9310            int menu) {
9311        if ((nb == null) || (item == null)) {
9312            return;
9313        }
9314
9315        List<?> theList = null;
9316
9317        if (nb instanceof Sensor) {
9318            theList = sensorList;
9319        } else if (nb instanceof Turnout) {
9320            theList = turnoutList;
9321        } else if (nb instanceof SignalHead) {
9322            theList = signalList;
9323        } else if (nb instanceof SignalMast) {
9324            theList = signalMastList;
9325        } else if (nb instanceof Block) {
9326            theList = blockContentsLabelList;
9327        } else if (nb instanceof Memory) {
9328            theList = memoryLabelList;    // Memory Input Icon not supported at this time.
9329        } else if (nb instanceof GlobalVariable) {
9330            theList = globalVariableLabelList;
9331        }
9332        if (theList != null) {
9333            for (Object o : theList) {
9334                PositionableLabel si = (PositionableLabel) o;
9335                if ((si.getNamedBean() == nb) && (si.getPopupUtility() != null)) {
9336                    if (menu != Editor.VIEWPOPUPONLY) {
9337                        si.getPopupUtility().addEditPopUpMenu(item);
9338                    }
9339                    if (menu != Editor.EDITPOPUPONLY) {
9340                        si.getPopupUtility().addViewPopUpMenu(item);
9341                    }
9342                }
9343            }
9344        } else if (nb instanceof Turnout) {
9345            for (LayoutTurnoutView ltv : getLayoutTurnoutAndSlipViews()) {
9346                if (ltv.getTurnout().equals(nb)) {
9347                    if (menu != Editor.VIEWPOPUPONLY) {
9348                        ltv.addEditPopUpMenu(item);
9349                    }
9350                    if (menu != Editor.EDITPOPUPONLY) {
9351                        ltv.addViewPopUpMenu(item);
9352                    }
9353                }
9354            }
9355        }
9356    }
9357
9358    @Override
9359    public @Nonnull
9360    String toString() {
9361        return String.format("LayoutEditor: %s", getLayoutName());
9362    }
9363
9364    @Override
9365    public void vetoableChange(
9366            @Nonnull PropertyChangeEvent evt)
9367            throws PropertyVetoException {
9368        NamedBean nb = (NamedBean) evt.getOldValue();
9369
9370        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
9371            StringBuilder message = new StringBuilder();
9372            message.append(Bundle.getMessage("VetoInUseLayoutEditorHeader", toString())); // NOI18N
9373            message.append("<ul>");
9374            boolean found = false;
9375
9376            if (nb instanceof SignalHead) {
9377                if (containsSignalHead((SignalHead) nb)) {
9378                    found = true;
9379                    message.append("<li>");
9380                    message.append(Bundle.getMessage("VetoSignalHeadIconFound"));
9381                    message.append("</li>");
9382                }
9383                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
9384
9385                if (lt != null) {
9386                    message.append("<li>");
9387                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToTurnout", lt.getTurnoutName()));
9388                    message.append("</li>");
9389                }
9390                PositionablePoint p = finder.findPositionablePointByBean(nb);
9391
9392                if (p != null) {
9393                    message.append("<li>");
9394                    // Need to expand to get the names of blocks
9395                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToPoint"));
9396                    message.append("</li>");
9397                }
9398                LevelXing lx = finder.findLevelXingByBean(nb);
9399
9400                if (lx != null) {
9401                    message.append("<li>");
9402                    // Need to expand to get the names of blocks
9403                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLevelXing"));
9404                    message.append("</li>");
9405                }
9406                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
9407
9408                if (ls != null) {
9409                    message.append("<li>");
9410                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLayoutSlip", ls.getTurnoutName()));
9411                    message.append("</li>");
9412                }
9413            } else if (nb instanceof Turnout) {
9414                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
9415
9416                if (lt != null) {
9417                    found = true;
9418                    message.append("<li>");
9419                    message.append(Bundle.getMessage("VetoTurnoutIconFound"));
9420                    message.append("</li>");
9421                }
9422
9423                for (LayoutTurnout t : getLayoutTurnouts()) {
9424                    if (t.getLinkedTurnoutName() != null) {
9425                        String uname = nb.getUserName();
9426
9427                        if (nb.getSystemName().equals(t.getLinkedTurnoutName())
9428                                || ((uname != null) && uname.equals(t.getLinkedTurnoutName()))) {
9429                            found = true;
9430                            message.append("<li>");
9431                            message.append(Bundle.getMessage("VetoLinkedTurnout", t.getTurnoutName()));
9432                            message.append("</li>");
9433                        }
9434                    }
9435
9436                    if (nb.equals(t.getSecondTurnout())) {
9437                        found = true;
9438                        message.append("<li>");
9439                        message.append(Bundle.getMessage("VetoSecondTurnout", t.getTurnoutName()));
9440                        message.append("</li>");
9441                    }
9442                }
9443                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
9444
9445                if (ls != null) {
9446                    found = true;
9447                    message.append("<li>");
9448                    message.append(Bundle.getMessage("VetoSlipIconFound", ls.getDisplayName()));
9449                    message.append("</li>");
9450                }
9451
9452                for (LayoutTurntable lx : getLayoutTurntables()) {
9453                    if (lx.isTurnoutControlled()) {
9454                        for (int i = 0; i < lx.getNumberRays(); i++) {
9455                            if (nb.equals(lx.getRayTurnout(i))) {
9456                                found = true;
9457                                message.append("<li>");
9458                                message.append(Bundle.getMessage("VetoRayTurntableControl", lx.getId()));
9459                                message.append("</li>");
9460                                break;
9461                            }
9462                        }
9463                    }
9464                }
9465                for (LayoutTraverser lx : getLayoutTraversers()) {
9466                    if (lx.isTurnoutControlled()) {
9467                        for (int i = 0; i < lx.getNumberSlots(); i++) {
9468                            if (nb.equals(lx.getSlotTurnout(i))) {
9469                                found = true;
9470                                message.append("<li>");
9471                                message.append(Bundle.getMessage("VetoSlotTraverserControl", lx.getId()));
9472                                message.append("</li>");
9473                                break;
9474                            }
9475                        }
9476                    }
9477                }
9478            }
9479
9480            if (nb instanceof SignalMast) {
9481                if (containsSignalMast((SignalMast) nb)) {
9482                    message.append("<li>");
9483                    message.append("As an Icon");
9484                    message.append("</li>");
9485                    found = true;
9486                }
9487                String foundelsewhere = findBeanUsage(nb);
9488
9489                if (foundelsewhere != null) {
9490                    message.append(foundelsewhere);
9491                    found = true;
9492                }
9493            }
9494
9495            if (nb instanceof Sensor) {
9496                int count = 0;
9497
9498                for (SensorIcon si : sensorList) {
9499                    if (nb.equals(si.getNamedBean())) {
9500                        count++;
9501                        found = true;
9502                    }
9503                }
9504
9505                if (count > 0) {
9506                    message.append("<li>");
9507                    message.append(String.format("As an Icon %s times", count));
9508                    message.append("</li>");
9509                }
9510                String foundelsewhere = findBeanUsage(nb);
9511
9512                if (foundelsewhere != null) {
9513                    message.append(foundelsewhere);
9514                    found = true;
9515                }
9516            }
9517
9518            if (nb instanceof Turnout) {
9519                int count = 0;
9520
9521                for (TurnoutIcon si : turnoutList) {
9522                    if (nb.equals(si.getNamedBean())) {
9523                        count++;
9524                        found = true;
9525                    }
9526                }
9527
9528                if (count > 0) {
9529                    message.append("<li>");
9530                    message.append(String.format("As an Icon %s times", count));
9531                    message.append("</li>");
9532                }
9533                String foundelsewhere = findBeanUsage(nb);
9534
9535                if (foundelsewhere != null) {
9536                    message.append(foundelsewhere);
9537                    found = true;
9538                }
9539            }
9540
9541            if (nb instanceof Memory) {
9542                for (MemoryIcon si : memoryLabelList) {
9543                    if (nb.equals(si.getMemory())) {
9544                        found = true;
9545                        message.append("<li>");
9546                        message.append(Bundle.getMessage("VetoMemoryIconFound"));
9547                        message.append("</li>");
9548                    }
9549                }
9550                for (MemoryInputIcon si : memoryInputList) {
9551                    if (nb.equals(si.getMemory())) {
9552                        found = true;
9553                        message.append("<li>");
9554                        message.append(Bundle.getMessage("VetoMemoryIconFound"));
9555                        message.append("</li>");
9556                    }
9557                }
9558            }
9559
9560            if (nb instanceof GlobalVariable) {
9561                for (GlobalVariableIcon si : globalVariableLabelList) {
9562                    if (nb.equals(si.getGlobalVariable())) {
9563                        found = true;
9564                        message.append("<li>");
9565                        message.append(Bundle.getMessage("VetoGlobalVariableIconFound"));
9566                        message.append("</li>");
9567                    }
9568                }
9569            }
9570
9571            if (found) {
9572                message.append("</ul>");
9573                message.append(Bundle.getMessage("VetoReferencesWillBeRemoved")); // NOI18N
9574                throw new PropertyVetoException(message.toString(), evt);
9575            }
9576        } else if ("DoDelete".equals(evt.getPropertyName())) { // NOI18N
9577            if (nb instanceof SignalHead) {
9578                removeSignalHead((SignalHead) nb);
9579                removeBeanRefs(nb);
9580            }
9581
9582            if (nb instanceof Turnout) {
9583                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
9584
9585                if (lt != null) {
9586                    lt.setTurnout("");
9587                }
9588
9589                for (LayoutTurnout t : getLayoutTurnouts()) {
9590                    if (t.getLinkedTurnoutName() != null) {
9591                        if (t.getLinkedTurnoutName().equals(nb.getSystemName())
9592                                || ((nb.getUserName() != null) && t.getLinkedTurnoutName().equals(nb.getUserName()))) {
9593                            t.setLinkedTurnoutName("");
9594                        }
9595                    }
9596
9597                    if (nb.equals(t.getSecondTurnout())) {
9598                        t.setSecondTurnout("");
9599                    }
9600                }
9601
9602                for (LayoutSlip sl : getLayoutSlips()) {
9603                    if (nb.equals(sl.getTurnout())) {
9604                        sl.setTurnout("");
9605                    }
9606
9607                    if (nb.equals(sl.getTurnoutB())) {
9608                        sl.setTurnoutB("");
9609                    }
9610                }
9611
9612                for (LayoutTurntable lx : getLayoutTurntables()) {
9613                    if (lx.isTurnoutControlled()) {
9614                        for (int i = 0; i < lx.getNumberRays(); i++) {
9615                            if (nb.equals(lx.getRayTurnout(i))) {
9616                                lx.setRayTurnout(i, null, NamedBean.UNKNOWN);
9617                            }
9618                        }
9619                    }
9620                }
9621
9622                for (LayoutTraverser lx : getLayoutTraversers()) {
9623                    if (lx.isTurnoutControlled()) {
9624                        for (int i = 0; i < lx.getNumberSlots(); i++) {
9625                            if (nb.equals(lx.getSlotTurnout(i))) {
9626                                lx.setSlotTurnout(i, null, NamedBean.UNKNOWN);
9627                            }
9628                        }
9629                    }
9630                }
9631            }
9632
9633            if (nb instanceof SignalMast) {
9634                removeBeanRefs(nb);
9635
9636                if (containsSignalMast((SignalMast) nb)) {
9637                    Iterator<SignalMastIcon> icon = signalMastList.iterator();
9638
9639                    while (icon.hasNext()) {
9640                        SignalMastIcon i = icon.next();
9641
9642                        if (i.getSignalMast().equals(nb)) {
9643                            icon.remove();
9644                            super.removeFromContents(i);
9645                        }
9646                    }
9647                    setDirty();
9648                    redrawPanel();
9649                }
9650            }
9651
9652            if (nb instanceof Sensor) {
9653                removeBeanRefs(nb);
9654                Iterator<SensorIcon> icon = sensorImage.iterator();
9655
9656                while (icon.hasNext()) {
9657                    SensorIcon i = icon.next();
9658
9659                    if (nb.equals(i.getSensor())) {
9660                        icon.remove();
9661                        super.removeFromContents(i);
9662                    }
9663                }
9664                setDirty();
9665                redrawPanel();
9666            }
9667
9668            if (nb instanceof Turnout) {
9669                removeBeanRefs(nb);
9670                Iterator<TurnoutIcon> icon = turnoutImage.iterator();
9671
9672                while (icon.hasNext()) {
9673                    TurnoutIcon i = icon.next();
9674
9675                    if (nb.equals(i.getTurnout())) {
9676                        icon.remove();
9677                        super.removeFromContents(i);
9678                    }
9679                }
9680                setDirty();
9681                redrawPanel();
9682            }
9683
9684            if (nb instanceof Memory) {
9685                Iterator<MemoryIcon> icon = memoryLabelList.iterator();
9686
9687                while (icon.hasNext()) {
9688                    MemoryIcon i = icon.next();
9689
9690                    if (nb.equals(i.getMemory())) {
9691                        icon.remove();
9692                        super.removeFromContents(i);
9693                    }
9694                }
9695
9696                Iterator<MemoryInputIcon> input = memoryInputList.iterator();
9697
9698                while (input.hasNext()) {
9699                    MemoryInputIcon ipt = input.next();
9700
9701                    if (nb.equals(ipt.getMemory())) {
9702                        input.remove();
9703                        super.removeFromContents(ipt);
9704                    }
9705                }
9706            }
9707
9708            if (nb instanceof GlobalVariable) {
9709                Iterator<GlobalVariableIcon> icon = globalVariableLabelList.iterator();
9710
9711                while (icon.hasNext()) {
9712                    GlobalVariableIcon i = icon.next();
9713
9714                    if (nb.equals(i.getGlobalVariable())) {
9715                        icon.remove();
9716                        super.removeFromContents(i);
9717                    }
9718                }
9719            }
9720        }
9721    }
9722
9723    @Override
9724    public void dispose() {
9725        if (leToolBarPanel != null) {
9726            leToolBarPanel.dispose();
9727        }
9728        super.dispose();
9729
9730    }
9731
9732    // package protected
9733    class TurnoutComboBoxPopupMenuListener implements PopupMenuListener {
9734
9735        private final NamedBeanComboBox<Turnout> comboBox;
9736        private final List<Turnout> currentTurnouts;
9737
9738        public TurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
9739            this.comboBox = comboBox;
9740            this.currentTurnouts = currentTurnouts;
9741        }
9742
9743        @Override
9744        public void popupMenuWillBecomeVisible(PopupMenuEvent event) {
9745            // This method is called before the popup menu becomes visible.
9746            log.debug("PopupMenuWillBecomeVisible");
9747            Set<Turnout> l = new HashSet<>();
9748            comboBox.getManager().getNamedBeanSet().forEach((turnout) -> {
9749                if (!currentTurnouts.contains(turnout)) {
9750                    if (!validatePhysicalTurnout(turnout.getDisplayName(), null)) {
9751                        l.add(turnout);
9752                    }
9753                }
9754            });
9755            comboBox.setExcludedItems(l);
9756        }
9757
9758        @Override
9759        public void popupMenuWillBecomeInvisible(PopupMenuEvent event) {
9760            // This method is called before the popup menu becomes invisible
9761            log.debug("PopupMenuWillBecomeInvisible");
9762        }
9763
9764        @Override
9765        public void popupMenuCanceled(PopupMenuEvent event) {
9766            // This method is called when the popup menu is canceled
9767            log.debug("PopupMenuCanceled");
9768        }
9769    }
9770
9771    /**
9772     * Create a listener that will exclude turnouts that are present in the
9773     * current panel.
9774     *
9775     * @param comboBox The NamedBeanComboBox that contains the turnout list.
9776     * @return A PopupMenuListener
9777     */
9778    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox) {
9779        return new TurnoutComboBoxPopupMenuListener(comboBox, new ArrayList<>());
9780    }
9781
9782    /**
9783     * Create a listener that will exclude turnouts that are present in the
9784     * current panel. The list of current turnouts are not excluded.
9785     *
9786     * @param comboBox        The NamedBeanComboBox that contains the turnout
9787     *                        list.
9788     * @param currentTurnouts The turnouts to be left in the turnout list.
9789     * @return A PopupMenuListener
9790     */
9791    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
9792        return new TurnoutComboBoxPopupMenuListener(comboBox, currentTurnouts);
9793    }
9794
9795    List<NamedBeanUsageReport> usageReport;
9796
9797    @Override
9798    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
9799        usageReport = new ArrayList<>();
9800        if (bean != null) {
9801            usageReport = super.getUsageReport(bean);
9802
9803            // LE Specific checks
9804            // Turnouts
9805            findTurnoutUsage(bean);
9806
9807            // Check A, EB, EC for sensors, masts, heads
9808            findPositionalUsage(bean);
9809
9810            // Level Crossings
9811            findXingWhereUsed(bean);
9812
9813            // Track segments
9814            findSegmentWhereUsed(bean);
9815        }
9816        return usageReport;
9817    }
9818
9819    void findTurnoutUsage(NamedBean bean) {
9820        for (LayoutTurnout turnout : getLayoutTurnoutsAndSlips()) {
9821            String data = getUsageData(turnout);
9822
9823            if (bean.equals(turnout.getTurnout())) {
9824                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout", data));
9825            }
9826            if (bean.equals(turnout.getSecondTurnout())) {
9827                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout2", data));
9828            }
9829
9830            if (isLBLockUsed(bean, turnout.getLayoutBlock())) {
9831                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9832            }
9833            if (turnout.hasEnteringDoubleTrack()) {
9834                if (isLBLockUsed(bean, turnout.getLayoutBlockB())) {
9835                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9836                }
9837                if (isLBLockUsed(bean, turnout.getLayoutBlockC())) {
9838                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9839                }
9840                if (isLBLockUsed(bean, turnout.getLayoutBlockD())) {
9841                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9842                }
9843            }
9844
9845            if (bean.equals(turnout.getSensorA())) {
9846                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9847            }
9848            if (bean.equals(turnout.getSensorB())) {
9849                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9850            }
9851            if (bean.equals(turnout.getSensorC())) {
9852                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9853            }
9854            if (bean.equals(turnout.getSensorD())) {
9855                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9856            }
9857
9858            if (bean.equals(turnout.getSignalAMast())) {
9859                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9860            }
9861            if (bean.equals(turnout.getSignalBMast())) {
9862                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9863            }
9864            if (bean.equals(turnout.getSignalCMast())) {
9865                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9866            }
9867            if (bean.equals(turnout.getSignalDMast())) {
9868                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9869            }
9870
9871            if (bean.equals(turnout.getSignalA1())) {
9872                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9873            }
9874            if (bean.equals(turnout.getSignalA2())) {
9875                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9876            }
9877            if (bean.equals(turnout.getSignalA3())) {
9878                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9879            }
9880            if (bean.equals(turnout.getSignalB1())) {
9881                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9882            }
9883            if (bean.equals(turnout.getSignalB2())) {
9884                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9885            }
9886            if (bean.equals(turnout.getSignalC1())) {
9887                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9888            }
9889            if (bean.equals(turnout.getSignalC2())) {
9890                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9891            }
9892            if (bean.equals(turnout.getSignalD1())) {
9893                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9894            }
9895            if (bean.equals(turnout.getSignalD2())) {
9896                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9897            }
9898        }
9899    }
9900
9901    void findPositionalUsage(NamedBean bean) {
9902        for (PositionablePoint point : getPositionablePoints()) {
9903            String data = getUsageData(point);
9904            if (bean.equals(point.getEastBoundSensor())) {
9905                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
9906            }
9907            if (bean.equals(point.getWestBoundSensor())) {
9908                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
9909            }
9910            if (bean.equals(point.getEastBoundSignalHead())) {
9911                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
9912            }
9913            if (bean.equals(point.getWestBoundSignalHead())) {
9914                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
9915            }
9916            if (bean.equals(point.getEastBoundSignalMast())) {
9917                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
9918            }
9919            if (bean.equals(point.getWestBoundSignalMast())) {
9920                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
9921            }
9922        }
9923    }
9924
9925    void findSegmentWhereUsed(NamedBean bean) {
9926        for (TrackSegment segment : getTrackSegments()) {
9927            if (isLBLockUsed(bean, segment.getLayoutBlock())) {
9928                String data = getUsageData(segment);
9929                usageReport.add(new NamedBeanUsageReport("LayoutEditorSegmentBlock", data));
9930            }
9931        }
9932    }
9933
9934    void findXingWhereUsed(NamedBean bean) {
9935        for (LevelXing xing : getLevelXings()) {
9936            String data = getUsageData(xing);
9937            if (isLBLockUsed(bean, xing.getLayoutBlockAC())) {
9938                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
9939            }
9940            if (isLBLockUsed(bean, xing.getLayoutBlockBD())) {
9941                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
9942            }
9943            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTA)) {
9944                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9945            }
9946            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTB)) {
9947                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9948            }
9949            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTC)) {
9950                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9951            }
9952            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTD)) {
9953                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9954            }
9955        }
9956    }
9957
9958    String getUsageData(LayoutTrack track) {
9959        LayoutTrackView trackView = getLayoutTrackView(track);
9960        Point2D point = trackView.getCoordsCenter();
9961        if (trackView instanceof TrackSegmentView) {
9962            TrackSegmentView segmentView = (TrackSegmentView) trackView;
9963            point = new Point2D.Double(segmentView.getCentreSegX(), segmentView.getCentreSegY());
9964        }
9965        return String.format("%s :: x=%d, y=%d",
9966                track.getClass().getSimpleName(),
9967                Math.round(point.getX()),
9968                Math.round(point.getY()));
9969    }
9970
9971    boolean isLBLockUsed(NamedBean bean, LayoutBlock lblock) {
9972        boolean result = false;
9973        if (lblock != null) {
9974            if (bean.equals(lblock.getBlock())) {
9975                result = true;
9976            }
9977        }
9978        return result;
9979    }
9980
9981    boolean isUsedInXing(NamedBean bean, LevelXing xing, LevelXing.Geometry point) {
9982        boolean result = false;
9983        if (bean.equals(xing.getSensor(point))) {
9984            result = true;
9985        }
9986        if (bean.equals(xing.getSignalHead(point))) {
9987            result = true;
9988        }
9989        if (bean.equals(xing.getSignalMast(point))) {
9990            result = true;
9991        }
9992        return result;
9993    }
9994
9995    // initialize logging
9996    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutEditor.class);
9997}