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