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