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