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