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