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