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            if (selectedObject == null) {
3194                selectedObject = checkBlockContentsPopUps(dLoc);
3195                if (selectedObject != null) {
3196                    selectedHitPointType = HitPointType.BLOCKCONTENTSICON;
3197                }
3198            }
3199
3200            // not in edit mode - check if a signal mast popup menu is being requested using Windows or Linux.
3201            var sm = checkSignalMastIconPopUps(dLoc);
3202            if (sm != null) {
3203                delayedPopupTrigger = true;
3204                log.debug("mousePressed: ++ Window/Linux mast popup delay");
3205             }
3206            if (selectedObject == null) {
3207                    selectedObject = checkBlockContentsPopUps(dLoc);
3208                    if (selectedObject != null) {
3209                        selectedHitPointType = HitPointType.BLOCKCONTENTSICON;
3210                    }
3211                }
3212
3213        } else if (event.isPopupTrigger() && !event.isShiftDown()) {
3214
3215            // not in edit mode - check if a marker popup menu is being requested using macOS.
3216            var lo = checkMarkerPopUps(dLoc);
3217            if (lo != null) {
3218                delayedPopupTrigger = true;
3219                log.debug("mousePressed: ++ MAC marker popup delay");
3220            }
3221
3222            // not in edit mode - check if a signal mast popup menu is being requested using macOS.
3223            var sm = checkSignalMastIconPopUps(dLoc);
3224            if (sm != null) {
3225                delayedPopupTrigger = true;
3226                log.debug("mousePressed: ++ MAC mast popup delay");
3227             }
3228
3229        }
3230
3231        if (!event.isPopupTrigger()) {
3232            List<Positionable> selections = getSelectedItems(event);
3233
3234            if (!selections.isEmpty()) {
3235                selections.get(0).doMousePressed(event);
3236            }
3237        }
3238
3239        requestFocusInWindow();
3240    }   // mousePressed
3241
3242// this is a method to iterate over a list of lists of items
3243// calling the predicate tester.test on each one
3244// all matching items are then added to the resulting List
3245// note: currently unused; commented out to avoid findbugs warning
3246// private static List testEachItemInListOfLists(
3247//        @Nonnull List<List> listOfListsOfObjects,
3248//        @Nonnull Predicate<Object> tester) {
3249//    List result = new ArrayList<>();
3250//    for (List<Object> listOfObjects : listOfListsOfObjects) {
3251//        List<Object> l = listOfObjects.stream().filter(o -> tester.test(o)).collect(Collectors.toList());
3252//        result.addAll(l);
3253//    }
3254//    return result;
3255//}
3256// this is a method to iterate over a list of lists of items
3257// calling the predicate tester.test on each one
3258// and return the first one that matches
3259// TODO: make this public? (it is useful! ;-)
3260// note: currently unused; commented out to avoid findbugs warning
3261// private static Object findFirstMatchingItemInListOfLists(
3262//        @Nonnull List<List> listOfListsOfObjects,
3263//        @Nonnull Predicate<Object> tester) {
3264//    Object result = null;
3265//    for (List listOfObjects : listOfListsOfObjects) {
3266//        Optional<Object> opt = listOfObjects.stream().filter(o -> tester.test(o)).findFirst();
3267//        if (opt.isPresent()) {
3268//            result = opt.get();
3269//            break;
3270//        }
3271//    }
3272//    return result;
3273//}
3274    /**
3275     * Called by {@link #mousePressed} to determine if the mouse click was in a
3276     * turnout control location. If so, update selectedHitPointType and
3277     * selectedObject for use by {@link #mouseReleased}.
3278     * <p>
3279     * If there's no match, selectedObject is set to null and
3280     * selectedHitPointType is left referring to the results of the checking the
3281     * last track on the list.
3282     * <p>
3283     * Refers to the current value of {@link #getLayoutTracks()} and
3284     * {@link #dLoc}.
3285     *
3286     * @param useRectangles set true to use rectangle; false for circles.
3287     */
3288    private void checkControls(boolean useRectangles) {
3289        selectedObject = null;  // deliberate side-effect
3290        for (LayoutTrackView theTrackView : getLayoutTrackViews()) {
3291            selectedHitPointType = theTrackView.findHitPointType(dLoc, useRectangles); // deliberate side-effect
3292            if (HitPointType.isControlHitType(selectedHitPointType)) {
3293                selectedObject = theTrackView.getLayoutTrack(); // deliberate side-effect
3294                return;
3295            }
3296        }
3297    }
3298
3299    // This is a geometric search, and should be done with views.
3300    // Hence this form is inevitably temporary.
3301    //
3302    private boolean findLayoutTracksHitPoint(
3303            @Nonnull Point2D loc, boolean requireUnconnected) {
3304        return findLayoutTracksHitPoint(loc, requireUnconnected, null);
3305    }
3306
3307    // This is a geometric search, and should be done with views.
3308    // Hence this form is inevitably temporary.
3309    //
3310    // optional parameter requireUnconnected
3311    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc) {
3312        return findLayoutTracksHitPoint(loc, false, null);
3313    }
3314
3315    /**
3316     * Internal (private) method to find the track closest to a point, with some
3317     * modifiers to the search. The {@link #foundTrack} and
3318     * {@link #foundHitPointType} members are set from the search.
3319     * <p>
3320     * This is a geometric search, and should be done with views. Hence this
3321     * form is inevitably temporary.
3322     *
3323     * @param loc                Point to search from
3324     * @param requireUnconnected forwarded to {@link #getLayoutTrackView}; if
3325     *                           true, return only free connections
3326     * @param avoid              Don't return this track, keep searching. Note
3327     *                           that {@Link #selectedObject} is also always
3328     *                           avoided automatically
3329     * @returns true if values of {@link #foundTrack} and
3330     * {@link #foundHitPointType} correct; note they may have changed even if
3331     * false is returned.
3332     */
3333    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc,
3334            boolean requireUnconnected, @CheckForNull LayoutTrack avoid) {
3335        boolean result = false; // assume failure (pessimist!)
3336
3337        foundTrack = null;
3338        foundTrackView = null;
3339        foundHitPointType = HitPointType.NONE;
3340
3341        Optional<LayoutTrack> opt = getLayoutTracks().stream().filter(layoutTrack -> {  // != means can't (yet) loop over Views
3342            if ((layoutTrack != avoid) && (layoutTrack != selectedObject)) {
3343                foundHitPointType = getLayoutTrackView(layoutTrack).findHitPointType(loc, false, requireUnconnected);
3344            }
3345            return (HitPointType.NONE != foundHitPointType);
3346        }).findFirst();
3347
3348        LayoutTrack layoutTrack = null;
3349        if (opt.isPresent()) {
3350            layoutTrack = opt.get();
3351        }
3352
3353        if (layoutTrack != null) {
3354            foundTrack = layoutTrack;
3355            foundTrackView = this.getLayoutTrackView(layoutTrack);
3356
3357            // get screen coordinates
3358            foundLocation = foundTrackView.getCoordsForConnectionType(foundHitPointType);
3359            /// foundNeedsConnect = isDisconnected(foundHitPointType);
3360            result = true;
3361        }
3362        return result;
3363    }
3364
3365    private TrackSegment checkTrackSegmentPopUps(@Nonnull Point2D loc) {
3366        assert loc != null;
3367
3368        TrackSegment result = null;
3369
3370        // NOTE: Rather than calculate all the hit rectangles for all
3371        // the points below and test if this location is in any of those
3372        // rectangles just create a hit rectangle for the location and
3373        // see if any of the points below are in it instead...
3374        Rectangle2D r = layoutEditorControlCircleRectAt(loc);
3375
3376        // check Track Segments, if any
3377        for (TrackSegmentView tsv : getTrackSegmentViews()) {
3378            if (r.contains(tsv.getCentreSeg())) {
3379                result = tsv.getTrackSegment();
3380                break;
3381            }
3382        }
3383        return result;
3384    }
3385
3386    private PositionableLabel checkBackgroundPopUps(@Nonnull Point2D loc) {
3387        assert loc != null;
3388
3389        PositionableLabel result = null;
3390        // check background images, if any
3391        for (int i = backgroundImage.size() - 1; i >= 0; i--) {
3392            PositionableLabel b = backgroundImage.get(i);
3393            Rectangle2D r = b.getBounds();
3394            if (r.contains(loc)) {
3395                result = b;
3396                break;
3397            }
3398        }
3399        return result;
3400    }
3401
3402    private SensorIcon checkSensorIconPopUps(@Nonnull Point2D loc) {
3403        assert loc != null;
3404
3405        SensorIcon result = null;
3406        // check sensor images, if any
3407        for (int i = sensorImage.size() - 1; i >= 0; i--) {
3408            SensorIcon s = sensorImage.get(i);
3409            Rectangle2D r = s.getBounds();
3410            if (r.contains(loc)) {
3411                result = s;
3412            }
3413        }
3414        return result;
3415    }
3416
3417    private SignalHeadIcon checkSignalHeadIconPopUps(@Nonnull Point2D loc) {
3418        assert loc != null;
3419
3420        SignalHeadIcon result = null;
3421        // check signal head images, if any
3422        for (int i = signalHeadImage.size() - 1; i >= 0; i--) {
3423            SignalHeadIcon s = signalHeadImage.get(i);
3424            Rectangle2D r = s.getBounds();
3425            if (r.contains(loc)) {
3426                result = s;
3427                break;
3428            }
3429        }
3430        return result;
3431    }
3432
3433    private SignalMastIcon checkSignalMastIconPopUps(@Nonnull Point2D loc) {
3434        assert loc != null;
3435
3436        SignalMastIcon result = null;
3437        // check signal head images, if any
3438        for (int i = signalMastList.size() - 1; i >= 0; i--) {
3439            SignalMastIcon s = signalMastList.get(i);
3440            Rectangle2D r = s.getBounds();
3441            if (r.contains(loc)) {
3442                result = s;
3443                break;
3444            }
3445        }
3446        return result;
3447    }
3448
3449    private PositionableLabel checkLabelImagePopUps(@Nonnull Point2D loc) {
3450        assert loc != null;
3451
3452        PositionableLabel result = null;
3453        int level = 0;
3454
3455        for (int i = labelImage.size() - 1; i >= 0; i--) {
3456            PositionableLabel s = labelImage.get(i);
3457            double x = s.getX();
3458            double y = s.getY();
3459            double w = 10.0;
3460            double h = 5.0;
3461
3462            if (s.isIcon() || s.isRotated() || s.getPopupUtility().getOrientation() != PositionablePopupUtil.HORIZONTAL) {
3463                w = s.maxWidth();
3464                h = s.maxHeight();
3465            } else if (s.isText()) {
3466                h = s.getFont().getSize();
3467                w = (h * 2 * (s.getText().length())) / 3;
3468            }
3469
3470            Rectangle2D r = new Rectangle2D.Double(x, y, w, h);
3471            if (r.contains(loc)) {
3472                if (s.getDisplayLevel() >= level) {
3473                    // Check to make sure that we are returning the highest level label.
3474                    result = s;
3475                    level = s.getDisplayLevel();
3476                }
3477            }
3478        }
3479        return result;
3480    }
3481
3482    private AnalogClock2Display checkClockPopUps(@Nonnull Point2D loc) {
3483        assert loc != null;
3484
3485        AnalogClock2Display result = null;
3486        // check clocks, if any
3487        for (int i = clocks.size() - 1; i >= 0; i--) {
3488            AnalogClock2Display s = clocks.get(i);
3489            Rectangle2D r = s.getBounds();
3490            if (r.contains(loc)) {
3491                result = s;
3492                break;
3493            }
3494        }
3495        return result;
3496    }
3497
3498    private MultiSensorIcon checkMultiSensorPopUps(@Nonnull Point2D loc) {
3499        assert loc != null;
3500
3501        MultiSensorIcon result = null;
3502        // check multi sensor icons, if any
3503        for (int i = multiSensors.size() - 1; i >= 0; i--) {
3504            MultiSensorIcon s = multiSensors.get(i);
3505            Rectangle2D r = s.getBounds();
3506            if (r.contains(loc)) {
3507                result = s;
3508                break;
3509            }
3510        }
3511        return result;
3512    }
3513
3514    private LocoIcon checkMarkerPopUps(@Nonnull Point2D loc) {
3515        assert loc != null;
3516
3517        LocoIcon result = null;
3518        // check marker icons, if any
3519        for (int i = markerImage.size() - 1; i >= 0; i--) {
3520            LocoIcon l = markerImage.get(i);
3521            Rectangle2D r = l.getBounds();
3522            if (r.contains(loc)) {
3523                // mouse was pressed in marker icon
3524                result = l;
3525                break;
3526            }
3527        }
3528        return result;
3529    }
3530
3531    private BlockContentsIcon checkBlockContentsPopUps(@Nonnull Point2D loc) {
3532        assert loc != null;
3533
3534        BlockContentsIcon result = null;
3535        // check marker icons, if any
3536        for (int i = blockContentsLabelList.size() - 1; i >= 0; i--) {
3537            BlockContentsIcon l = blockContentsLabelList.get(i);
3538            Rectangle2D r = l.getBounds();
3539            if (r.contains(loc)) {
3540                // mouse was pressed in marker icon
3541                result = l;
3542                break;
3543            }
3544        }
3545        return result;
3546    }
3547
3548    private LayoutShape checkLayoutShapePopUps(@Nonnull Point2D loc) {
3549        assert loc != null;
3550
3551        LayoutShape result = null;
3552        for (LayoutShape ls : layoutShapes) {
3553            selectedHitPointType = ls.findHitPointType(loc, true);
3554            if (LayoutShape.isShapeHitPointType(selectedHitPointType)) {
3555                result = ls;
3556                break;
3557            }
3558        }
3559        return result;
3560    }
3561
3562    /**
3563     * Get the coordinates for the connection type of the specified LayoutTrack
3564     * or subtype.
3565     * <p>
3566     * This uses the current LayoutEditor object to map a LayoutTrack (no
3567     * coordinates) object to _a_ specific LayoutTrackView object in the current
3568     * LayoutEditor i.e. window. This allows the same model object in two
3569     * windows, but not twice in a single window.
3570     * <p>
3571     * This is temporary, and needs to go away as the LayoutTrack doesn't
3572     * logically have position; just the LayoutTrackView does, and multiple
3573     * LayoutTrackViews can refer to one specific LayoutTrack.
3574     *
3575     * @param track          the object (LayoutTrack subclass)
3576     * @param connectionType the type of connection
3577     * @return the coordinates for the connection type of the specified object
3578     */
3579    @Nonnull
3580    public Point2D getCoords(@Nonnull LayoutTrack track, HitPointType connectionType) {
3581        LayoutTrack trk = Objects.requireNonNull(track);
3582
3583        return getCoords(getLayoutTrackView(trk), connectionType);
3584    }
3585
3586    /**
3587     * Get the coordinates for the connection type of the specified
3588     * LayoutTrackView or subtype.
3589     *
3590     * @param trkView        the object (LayoutTrackView subclass)
3591     * @param connectionType the type of connection
3592     * @return the coordinates for the connection type of the specified object
3593     */
3594    @Nonnull
3595    public Point2D getCoords(@Nonnull LayoutTrackView trkView, HitPointType connectionType) {
3596        LayoutTrackView trkv = Objects.requireNonNull(trkView);
3597
3598        return trkv.getCoordsForConnectionType(connectionType);
3599    }
3600
3601    @Override
3602    public void mouseReleased(JmriMouseEvent event) {
3603        super.setToolTip(null);
3604
3605        // initialize mouse position
3606        calcLocation(event);
3607
3608        if (!isEditable() && _highlightcomponent != null && highlightCursor) {
3609            _highlightcomponent = null;
3610            // see if we moused up on an object
3611            checkControls(true);
3612            redrawPanel();
3613        }
3614
3615        // if alt modifier is down invert the snap to grid behaviour
3616        snapToGridInvert = event.isAltDown();
3617
3618        if (isEditable()) {
3619            leToolBarPanel.setLocationText(dLoc);
3620
3621            // released the mouse with shift down... see what we're adding
3622            if (!event.isPopupTrigger() && !event.isMetaDown() && event.isShiftDown()) {
3623
3624                currentPoint = new Point2D.Double(xLoc, yLoc);
3625
3626                if (snapToGridOnAdd != snapToGridInvert) {
3627                    // this snaps the current point to the grid
3628                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
3629                    xLoc = (int) currentPoint.getX();
3630                    yLoc = (int) currentPoint.getY();
3631                    leToolBarPanel.setLocationText(currentPoint);
3632                }
3633
3634                if (leToolBarPanel.turnoutRHButton.isSelected()) {
3635                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_TURNOUT);
3636                } else if (leToolBarPanel.turnoutLHButton.isSelected()) {
3637                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_TURNOUT);
3638                } else if (leToolBarPanel.turnoutWYEButton.isSelected()) {
3639                    addLayoutTurnout(LayoutTurnout.TurnoutType.WYE_TURNOUT);
3640                } else if (leToolBarPanel.doubleXoverButton.isSelected()) {
3641                    addLayoutTurnout(LayoutTurnout.TurnoutType.DOUBLE_XOVER);
3642                } else if (leToolBarPanel.rhXoverButton.isSelected()) {
3643                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_XOVER);
3644                } else if (leToolBarPanel.lhXoverButton.isSelected()) {
3645                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_XOVER);
3646                } else if (leToolBarPanel.levelXingButton.isSelected()) {
3647                    addLevelXing();
3648                } else if (leToolBarPanel.layoutSingleSlipButton.isSelected()) {
3649                    addLayoutSlip(LayoutSlip.TurnoutType.SINGLE_SLIP);
3650                } else if (leToolBarPanel.layoutDoubleSlipButton.isSelected()) {
3651                    addLayoutSlip(LayoutSlip.TurnoutType.DOUBLE_SLIP);
3652                } else if (leToolBarPanel.endBumperButton.isSelected()) {
3653                    addEndBumper();
3654                } else if (leToolBarPanel.anchorButton.isSelected()) {
3655                    addAnchor();
3656                } else if (leToolBarPanel.edgeButton.isSelected()) {
3657                    addEdgeConnector();
3658                } else if (leToolBarPanel.trackButton.isSelected()) {
3659                    if ((beginTrack != null) && (foundTrack != null)
3660                            && (beginTrack != foundTrack)) {
3661                        addTrackSegment();
3662                        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3663                    }
3664                    beginTrack = null;
3665                    foundTrack = null;
3666                    foundTrackView = null;
3667                } else if (leToolBarPanel.multiSensorButton.isSelected()) {
3668                    startMultiSensor();
3669                } else if (leToolBarPanel.sensorButton.isSelected()) {
3670                    addSensor();
3671                } else if (leToolBarPanel.signalButton.isSelected()) {
3672                    addSignalHead();
3673                } else if (leToolBarPanel.textLabelButton.isSelected()) {
3674                    addLabel();
3675                } else if (leToolBarPanel.memoryButton.isSelected()) {
3676                    addMemory();
3677                } else if (leToolBarPanel.globalVariableButton.isSelected()) {
3678                    addGlobalVariable();
3679                } else if (leToolBarPanel.blockContentsButton.isSelected()) {
3680                    addBlockContents();
3681                } else if (leToolBarPanel.iconLabelButton.isSelected()) {
3682                    addIcon();
3683                } else if (leToolBarPanel.logixngButton.isSelected()) {
3684                    addLogixNGIcon();
3685                } else if (leToolBarPanel.audioButton.isSelected()) {
3686                    addAudioIcon();
3687                } else if (leToolBarPanel.shapeButton.isSelected()) {
3688                    LayoutShape ls = (LayoutShape) selectedObject;
3689                    if (ls == null) {
3690                        ls = addLayoutShape(currentPoint);
3691                    } else {
3692                        ls.addPoint(currentPoint, selectedHitPointType.shapePointIndex());
3693                    }
3694                    unionToPanelBounds(ls.getBounds());
3695                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
3696                } else if (leToolBarPanel.signalMastButton.isSelected()) {
3697                    addSignalMast();
3698                } else {
3699                    log.warn("No item selected in panel edit mode");
3700                }
3701                // resizePanelBounds(false);
3702                selectedObject = null;
3703                redrawPanel();
3704            } else if ((event.isPopupTrigger() || delayedPopupTrigger) && !isDragging) {
3705                selectedObject = null;
3706                selectedHitPointType = HitPointType.NONE;
3707                whenReleased = event.getWhen();
3708                showEditPopUps(event);
3709            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
3710                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3711                    && !event.isShiftDown() && !event.isControlDown()) {
3712                // controlling turnouts, in edit mode
3713                LayoutTurnout t = (LayoutTurnout) selectedObject;
3714                t.toggleTurnout();
3715            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
3716                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3717                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3718                    && !event.isShiftDown() && !event.isControlDown()) {
3719                // controlling slips, in edit mode
3720                LayoutSlip sl = (LayoutSlip) selectedObject;
3721                sl.toggleState(selectedHitPointType);
3722            } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
3723                    && allControlling() && (!event.isMetaDown() && !event.isAltDown()) && !event.isPopupTrigger()
3724                    && !event.isShiftDown() && !event.isControlDown()) {
3725                // controlling turntable, in edit mode
3726                LayoutTurntable t = (LayoutTurntable) selectedObject;
3727                t.setPosition(selectedHitPointType.turntableTrackIndex());
3728            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.TURNOUT_CENTER)
3729                    || (selectedHitPointType == HitPointType.SLIP_CENTER)
3730                    || (selectedHitPointType == HitPointType.SLIP_LEFT)
3731                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3732                    && allControlling() && (event.isMetaDown() && !event.isAltDown())
3733                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3734                // We just dropped a turnout (or slip)... see if it will connect to anything
3735                hitPointCheckLayoutTurnouts((LayoutTurnout) selectedObject);
3736            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.POS_POINT)
3737                    && allControlling() && (event.isMetaDown())
3738                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3739                // We just dropped a PositionablePoint... see if it will connect to anything
3740                PositionablePoint p = (PositionablePoint) selectedObject;
3741                if ((p.getConnect1() == null) || (p.getConnect2() == null)) {
3742                    checkPointOfPositionable(p);
3743                }
3744            }
3745
3746            if ((leToolBarPanel.trackButton.isSelected()) && (beginTrack != null) && (foundTrack != null)) {
3747                // user let up shift key before releasing the mouse when creating a track segment
3748                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3749                beginTrack = null;
3750                foundTrack = null;
3751                foundTrackView = null;
3752                redrawPanel();
3753            }
3754            createSelectionGroups();
3755        } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
3756                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
3757                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3758            // controlling turnout out of edit mode
3759            LayoutTurnout t = (LayoutTurnout) selectedObject;
3760            if (useDirectTurnoutControl) {
3761                t.setState(Turnout.CLOSED);
3762            } else {
3763                t.toggleTurnout();
3764                if (highlightCursor && !t.isDisabled()) {
3765                    // flash the turnout circle a few times so the user knows it's being toggled
3766                    javax.swing.Timer timer = new javax.swing.Timer(150, null);
3767                    timer.addActionListener(new ActionListener(){
3768                        int count = 1;
3769                        @Override
3770                        public void actionPerformed(ActionEvent ae){
3771                          if(count % 2 != 0) t.setDisabled(true);
3772                          else t.setDisabled(false);
3773                          if(++count > 8) timer.stop();
3774                        }
3775                    });
3776                    timer.start();
3777                }
3778            }
3779        } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
3780                || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3781                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
3782                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3783            // controlling slip out of edit mode
3784            LayoutSlip sl = (LayoutSlip) selectedObject;
3785            sl.toggleState(selectedHitPointType);
3786        } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
3787                && allControlling() && !event.isMetaDown() && !event.isAltDown() && !event.isPopupTrigger()
3788                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3789            // controlling turntable out of edit mode
3790            LayoutTurntable t = (LayoutTurntable) selectedObject;
3791            t.setPosition(selectedHitPointType.turntableTrackIndex());
3792        } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.BLOCKCONTENTSICON))
3793                && allControlling()  && !event.isAltDown() && !event.isPopupTrigger()
3794                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3795            BlockContentsIcon t = (BlockContentsIcon) selectedObject;
3796            if (t != null) {
3797                showPopUp(t, event);
3798            }
3799        } else if ((event.isPopupTrigger() || delayedPopupTrigger) && (!isDragging)) {
3800            // requesting marker popup out of edit mode
3801            LocoIcon lo = checkMarkerPopUps(dLoc);
3802            if (lo != null) {
3803                showPopUp(lo, event);
3804            } else {
3805                if (findLayoutTracksHitPoint(dLoc)) {
3806                    // show popup menu
3807                    switch (foundHitPointType) {
3808                        case TURNOUT_CENTER: {
3809                            if (useDirectTurnoutControl) {
3810                                LayoutTurnout t = (LayoutTurnout) foundTrack;
3811                                t.setState(Turnout.THROWN);
3812                            } else {
3813                                foundTrackView.showPopup(event);
3814                            }
3815                            break;
3816                        }
3817
3818                        case LEVEL_XING_CENTER:
3819                        case SLIP_RIGHT:
3820                        case SLIP_LEFT: {
3821                            foundTrackView.showPopup(event);
3822                            break;
3823                        }
3824
3825                        default: {
3826                            break;
3827                        }
3828                    }
3829                }
3830                AnalogClock2Display c = checkClockPopUps(dLoc);
3831                if (c != null) {
3832                    showPopUp(c, event);
3833                } else {
3834                    SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
3835                    if (sm != null) {
3836                        showPopUp(sm, event);
3837                    } else {
3838                        PositionableLabel im = checkLabelImagePopUps(dLoc);
3839                        if (im != null) {
3840                            showPopUp(im, event);
3841                        }
3842                    }
3843                }
3844            }
3845        }
3846
3847        if (!event.isPopupTrigger() && !isDragging) {
3848            List<Positionable> selections = getSelectedItems(event);
3849            if (!selections.isEmpty()) {
3850                selections.get(0).doMouseReleased(event);
3851                whenReleased = event.getWhen();
3852            }
3853        }
3854
3855        // train icon needs to know when moved
3856        if (event.isPopupTrigger() && isDragging) {
3857            List<Positionable> selections = getSelectedItems(event);
3858            if (!selections.isEmpty()) {
3859                selections.get(0).doMouseDragged(event);
3860            }
3861        }
3862
3863        if (selectedObject != null) {
3864            // An object was selected, deselect it
3865            prevSelectedObject = selectedObject;
3866            selectedObject = null;
3867        }
3868
3869        // clear these
3870        beginTrack = null;
3871        foundTrack = null;
3872        foundTrackView = null;
3873
3874        delayedPopupTrigger = false;
3875
3876        if (isDragging) {
3877            resizePanelBounds(true);
3878            isDragging = false;
3879        }
3880
3881        requestFocusInWindow();
3882    }   // mouseReleased
3883
3884    public void addPopupItems(@Nonnull JPopupMenu popup, @Nonnull JmriMouseEvent event) {
3885
3886        List<LayoutTrack> tracks = getLayoutTracks().stream().filter(layoutTrack -> {  // != means can't (yet) loop over Views
3887            HitPointType hitPointType = getLayoutTrackView(layoutTrack).findHitPointType(dLoc, false, false);
3888            return (HitPointType.NONE != hitPointType);
3889        }).collect(Collectors.toList());
3890
3891        List<Positionable> selections = getSelectedItems(event);
3892
3893        if ((tracks.size() > 1) || (selections.size() > 1)) {
3894            JMenu iconsBelowMenu = new JMenu(Bundle.getMessage("MenuItemIconsBelow"));
3895
3896            JMenuItem mi = new JMenuItem(Bundle.getMessage("MenuItemIconsBelow_InfoNotInOrder"));
3897            mi.setEnabled(false);
3898            iconsBelowMenu.add(mi);
3899
3900            if (tracks.size() > 1) {
3901                for (int i=0; i < tracks.size(); i++) {
3902                    LayoutTrack t = tracks.get(i);
3903                    iconsBelowMenu.add(new AbstractAction(Bundle.getMessage(
3904                            "LayoutTrackTypeAndName", t.getTypeName(), t.getName())) {
3905                        @Override
3906                        public void actionPerformed(ActionEvent e) {
3907                            LayoutTrackView ltv = getLayoutTrackView(t);
3908                            ltv.showPopup(event);
3909                        }
3910                    });
3911                }
3912            }
3913            if (selections.size() > 1) {
3914                for (int i=0; i < selections.size(); i++) {
3915                    Positionable pos = selections.get(i);
3916                    iconsBelowMenu.add(new AbstractAction(Bundle.getMessage(
3917                            "PositionableTypeAndName", pos.getTypeString(), pos.getNameString())) {
3918                        @Override
3919                        public void actionPerformed(ActionEvent e) {
3920                            showPopUp(pos, event, new ArrayList<>());
3921                        }
3922                    });
3923                }
3924            }
3925            popup.addSeparator();
3926            popup.add(iconsBelowMenu);
3927        }
3928    }
3929
3930    private void showEditPopUps(@Nonnull JmriMouseEvent event) {
3931        if (findLayoutTracksHitPoint(dLoc)) {
3932            if (HitPointType.isBezierHitType(foundHitPointType)) {
3933                getTrackSegmentView((TrackSegment) foundTrack).showBezierPopUp(event, foundHitPointType);
3934            } else if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
3935                LayoutTurntable t = (LayoutTurntable) foundTrack;
3936                if (t.isTurnoutControlled()) {
3937                    LayoutTurntableView ltview = getLayoutTurntableView((LayoutTurntable) foundTrack);
3938                    ltview.showRayPopUp(event, foundHitPointType.turntableTrackIndex());
3939                }
3940            } else if (HitPointType.isPopupHitType(foundHitPointType)) {
3941                foundTrackView.showPopup(event);
3942            } else if (HitPointType.isTurnoutHitType(foundHitPointType)) {
3943                // don't curently have edit popup for these
3944            } else {
3945                log.warn("Unknown foundPointType:{}", foundHitPointType);
3946            }
3947        } else {
3948            do {
3949                TrackSegment ts = checkTrackSegmentPopUps(dLoc);
3950                if (ts != null) {
3951                    TrackSegmentView tsv = getTrackSegmentView(ts);
3952                    tsv.showPopup(event);
3953                    break;
3954                }
3955
3956                SensorIcon s = checkSensorIconPopUps(dLoc);
3957                if (s != null) {
3958                    showPopUp(s, event);
3959                    break;
3960                }
3961
3962                LocoIcon lo = checkMarkerPopUps(dLoc);
3963                if (lo != null) {
3964                    showPopUp(lo, event);
3965                    break;
3966                }
3967
3968                SignalHeadIcon sh = checkSignalHeadIconPopUps(dLoc);
3969                if (sh != null) {
3970                    showPopUp(sh, event);
3971                    break;
3972                }
3973
3974                AnalogClock2Display c = checkClockPopUps(dLoc);
3975                if (c != null) {
3976                    showPopUp(c, event);
3977                    break;
3978                }
3979
3980                MultiSensorIcon ms = checkMultiSensorPopUps(dLoc);
3981                if (ms != null) {
3982                    showPopUp(ms, event);
3983                    break;
3984                }
3985
3986                PositionableLabel lb = checkLabelImagePopUps(dLoc);
3987                if (lb != null) {
3988                    showPopUp(lb, event);
3989                    break;
3990                }
3991
3992                PositionableLabel b = checkBackgroundPopUps(dLoc);
3993                if (b != null) {
3994                    showPopUp(b, event);
3995                    break;
3996                }
3997
3998                SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
3999                if (sm != null) {
4000                    showPopUp(sm, event);
4001                    break;
4002                }
4003                LayoutShape ls = checkLayoutShapePopUps(dLoc);
4004                if (ls != null) {
4005                    ls.showShapePopUp(event, selectedHitPointType);
4006                    break;
4007                }
4008            } while (false);
4009        }
4010    }
4011
4012    /**
4013     * Select the menu items to display for the Positionable's popup.
4014     * @param pos   the item containing or requiring the context menu
4015     * @param event the event triggering the menu
4016     */
4017    public void showPopUp(@Nonnull Positionable pos, @Nonnull JmriMouseEvent event) {
4018        Positionable p = Objects.requireNonNull(pos);
4019
4020        if (!((Component) p).isVisible()) {
4021            return; // component must be showing on the screen to determine its location
4022        }
4023        JPopupMenu popup = new JPopupMenu();
4024
4025        if (p.isEditable()) {
4026            JMenuItem jmi;
4027
4028            if (showAlignPopup()) {
4029                setShowAlignmentMenu(popup);
4030                popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
4031                    @Override
4032                    public void actionPerformed(ActionEvent event) {
4033                        deleteSelectedItems();
4034                    }
4035                });
4036            } else {
4037                if (p.doViemMenu()) {
4038                    String objectType = p.getClass().getName();
4039                    objectType = objectType.substring(objectType.lastIndexOf('.') + 1);
4040                    jmi = popup.add(objectType);
4041                    jmi.setEnabled(false);
4042
4043                    jmi = popup.add(p.getNameString());
4044                    jmi.setEnabled(false);
4045
4046                    if (p.isPositionable()) {
4047                        setShowCoordinatesMenu(p, popup);
4048                    }
4049                    setDisplayLevelMenu(p, popup);
4050                    setPositionableMenu(p, popup);
4051                }
4052
4053                boolean popupSet = false;
4054                popupSet |= p.setRotateOrthogonalMenu(popup);
4055                popupSet |= p.setRotateMenu(popup);
4056                popupSet |= p.setScaleMenu(popup);
4057                if (popupSet) {
4058                    popup.addSeparator();
4059                    popupSet = false;
4060                }
4061                popupSet |= p.setEditIconMenu(popup);
4062                popupSet |= p.setTextEditMenu(popup);
4063
4064                PositionablePopupUtil util = p.getPopupUtility();
4065
4066                if (util != null) {
4067                    util.setFixedTextMenu(popup);
4068                    util.setTextMarginMenu(popup);
4069                    util.setTextBorderMenu(popup);
4070                    util.setTextFontMenu(popup);
4071                    util.setBackgroundMenu(popup);
4072                    util.setTextJustificationMenu(popup);
4073                    util.setTextOrientationMenu(popup);
4074                    popup.addSeparator();
4075                    util.propertyUtil(popup);
4076                    util.setAdditionalEditPopUpMenu(popup);
4077                    popupSet = true;
4078                }
4079
4080                if (popupSet) {
4081                    popup.addSeparator();
4082                    // popupSet = false;
4083                }
4084                p.setDisableControlMenu(popup);
4085                setShowAlignmentMenu(popup);
4086
4087                // for Positionables with unique settings
4088                p.showPopUp(popup);
4089                setShowToolTipMenu(p, popup);
4090
4091                setRemoveMenu(p, popup);
4092
4093                if (p.doViemMenu()) {
4094                    setHiddenMenu(p, popup);
4095                    setEmptyHiddenMenu(p, popup);
4096                    setValueEditDisabledMenu(p, popup);
4097                    setEditIdMenu(p, popup);
4098                    setEditClassesMenu(p, popup);
4099                    popup.addSeparator();
4100                    setLogixNGPositionableMenu(p, popup);
4101                }
4102            }
4103        } else {
4104            p.showPopUp(popup);
4105            PositionablePopupUtil util = p.getPopupUtility();
4106
4107            if (util != null) {
4108                util.setAdditionalViewPopUpMenu(popup);
4109            }
4110        }
4111
4112        addPopupItems(popup, event);
4113
4114        popup.show((Component) p, p.getWidth() / 2 + (int) ((getZoom() - 1.0) * p.getX()),
4115                p.getHeight() / 2 + (int) ((getZoom() - 1.0) * p.getY()));
4116
4117        /*popup.show((Component)pt, event.getX(), event.getY());*/
4118    }
4119
4120    private long whenReleased = 0; // used to identify event that was popup trigger
4121    private boolean awaitingIconChange = false;
4122
4123    @Override
4124    public void mouseClicked(@Nonnull JmriMouseEvent event) {
4125        // initialize mouse position
4126        calcLocation(event);
4127
4128        if (!isEditable() && _highlightcomponent != null && highlightCursor) {
4129            _highlightcomponent = null;
4130            redrawPanel();
4131        }
4132
4133        // if alt modifier is down invert the snap to grid behaviour
4134        snapToGridInvert = event.isAltDown();
4135
4136        if (!event.isMetaDown() && !event.isPopupTrigger() && !event.isAltDown()
4137                && !awaitingIconChange && !event.isShiftDown() && !event.isControlDown()) {
4138            List<Positionable> selections = getSelectedItems(event);
4139
4140            if (!selections.isEmpty()) {
4141                selections.get(0).doMouseClicked(event);
4142            }
4143        } else if (event.isPopupTrigger() && (whenReleased != event.getWhen())) {
4144
4145            if (isEditable()) {
4146                selectedObject = null;
4147                selectedHitPointType = HitPointType.NONE;
4148                showEditPopUps(event);
4149            } else {
4150                LocoIcon lo = checkMarkerPopUps(dLoc);
4151
4152                if (lo != null) {
4153                    showPopUp(lo, event);
4154                }
4155            }
4156        }
4157
4158        if (event.isControlDown() && !event.isPopupTrigger()) {
4159            if (findLayoutTracksHitPoint(dLoc)) {
4160                switch (foundHitPointType) {
4161                    case POS_POINT:
4162                    case TURNOUT_CENTER:
4163                    case LEVEL_XING_CENTER:
4164                    case SLIP_LEFT:
4165                    case SLIP_RIGHT:
4166                    case TURNTABLE_CENTER: {
4167                        amendSelectionGroup(foundTrack);
4168                        break;
4169                    }
4170
4171                    default: {
4172                        break;
4173                    }
4174                }
4175            } else {
4176                PositionableLabel s = checkSensorIconPopUps(dLoc);
4177                if (s != null) {
4178                    amendSelectionGroup(s);
4179                } else {
4180                    PositionableLabel sh = checkSignalHeadIconPopUps(dLoc);
4181                    if (sh != null) {
4182                        amendSelectionGroup(sh);
4183                    } else {
4184                        PositionableLabel ms = checkMultiSensorPopUps(dLoc);
4185                        if (ms != null) {
4186                            amendSelectionGroup(ms);
4187                        } else {
4188                            PositionableLabel lb = checkLabelImagePopUps(dLoc);
4189                            if (lb != null) {
4190                                amendSelectionGroup(lb);
4191                            } else {
4192                                PositionableLabel b = checkBackgroundPopUps(dLoc);
4193                                if (b != null) {
4194                                    amendSelectionGroup(b);
4195                                } else {
4196                                    PositionableLabel sm = checkSignalMastIconPopUps(dLoc);
4197                                    if (sm != null) {
4198                                        amendSelectionGroup(sm);
4199                                    } else {
4200                                        LayoutShape ls = checkLayoutShapePopUps(dLoc);
4201                                        if (ls != null) {
4202                                            amendSelectionGroup(ls);
4203                                        }
4204                                    }
4205                                }
4206                            }
4207                        }
4208                    }
4209                }
4210            }
4211        } else if ((selectionWidth == 0) || (selectionHeight == 0)) {
4212            clearSelectionGroups();
4213        }
4214        requestFocusInWindow();
4215    }
4216
4217    private void checkPointOfPositionable(@Nonnull PositionablePoint p) {
4218        assert p != null;
4219
4220        TrackSegment t = p.getConnect1();
4221
4222        if (t == null) {
4223            t = p.getConnect2();
4224        }
4225
4226        // Nothing connected to this bit of track so ignore
4227        if (t == null) {
4228            return;
4229        }
4230        beginTrack = p;
4231        beginHitPointType = HitPointType.POS_POINT;
4232        PositionablePointView pv = getPositionablePointView(p);
4233        Point2D loc = pv.getCoordsCenter();
4234
4235        if (findLayoutTracksHitPoint(loc, true, p)) {
4236            switch (foundHitPointType) {
4237                case POS_POINT: {
4238                    PositionablePoint p2 = (PositionablePoint) foundTrack;
4239
4240                    if ((p2.getType() == PositionablePoint.PointType.ANCHOR) && p2.setTrackConnection(t)) {
4241                        if (t.getConnect1() == p) {
4242                            t.setNewConnect1(p2, foundHitPointType);
4243                        } else {
4244                            t.setNewConnect2(p2, foundHitPointType);
4245                        }
4246                        p.removeTrackConnection(t);
4247
4248                        if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4249                            removePositionablePoint(p);
4250                        }
4251                    }
4252                    break;
4253                }
4254                case TURNOUT_A:
4255                case TURNOUT_B:
4256                case TURNOUT_C:
4257                case TURNOUT_D:
4258                case SLIP_A:
4259                case SLIP_B:
4260                case SLIP_C:
4261                case SLIP_D:
4262                case LEVEL_XING_A:
4263                case LEVEL_XING_B:
4264                case LEVEL_XING_C:
4265                case LEVEL_XING_D: {
4266                    try {
4267                        if (foundTrack.getConnection(foundHitPointType) == null) {
4268                            foundTrack.setConnection(foundHitPointType, t, HitPointType.TRACK);
4269
4270                            if (t.getConnect1() == p) {
4271                                t.setNewConnect1(foundTrack, foundHitPointType);
4272                            } else {
4273                                t.setNewConnect2(foundTrack, foundHitPointType);
4274                            }
4275                            p.removeTrackConnection(t);
4276
4277                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4278                                removePositionablePoint(p);
4279                            }
4280                        }
4281                    } catch (JmriException e) {
4282                        log.debug("Unable to set location");
4283                    }
4284                    break;
4285                }
4286
4287                default: {
4288                    if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
4289                        LayoutTurntable tt = (LayoutTurntable) foundTrack;
4290                        int ray = foundHitPointType.turntableTrackIndex();
4291
4292                        if (tt.getRayConnectIndexed(ray) == null) {
4293                            tt.setRayConnect(t, ray);
4294
4295                            if (t.getConnect1() == p) {
4296                                t.setNewConnect1(tt, foundHitPointType);
4297                            } else {
4298                                t.setNewConnect2(tt, foundHitPointType);
4299                            }
4300                            p.removeTrackConnection(t);
4301
4302                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
4303                                removePositionablePoint(p);
4304                            }
4305                        }
4306                    } else {
4307                        log.debug("No valid point, so will quit");
4308                        return;
4309                    }
4310                    break;
4311                }
4312            }
4313            redrawPanel();
4314
4315            if (t.getLayoutBlock() != null) {
4316                getLEAuxTools().setBlockConnectivityChanged();
4317            }
4318        }
4319        beginTrack = null;
4320    }
4321
4322    // We just dropped a turnout... see if it will connect to anything
4323    private void hitPointCheckLayoutTurnouts(@Nonnull LayoutTurnout lt) {
4324        beginTrack = lt;
4325
4326        LayoutTurnoutView ltv = getLayoutTurnoutView(lt);
4327
4328        if (lt.getConnectA() == null) {
4329            if (lt instanceof LayoutSlip) {
4330                beginHitPointType = HitPointType.SLIP_A;
4331            } else {
4332                beginHitPointType = HitPointType.TURNOUT_A;
4333            }
4334            dLoc = ltv.getCoordsA();
4335            hitPointCheckLayoutTurnoutSubs(dLoc);
4336        }
4337
4338        if (lt.getConnectB() == null) {
4339            if (lt instanceof LayoutSlip) {
4340                beginHitPointType = HitPointType.SLIP_B;
4341            } else {
4342                beginHitPointType = HitPointType.TURNOUT_B;
4343            }
4344            dLoc = ltv.getCoordsB();
4345            hitPointCheckLayoutTurnoutSubs(dLoc);
4346        }
4347
4348        if (lt.getConnectC() == null) {
4349            if (lt instanceof LayoutSlip) {
4350                beginHitPointType = HitPointType.SLIP_C;
4351            } else {
4352                beginHitPointType = HitPointType.TURNOUT_C;
4353            }
4354            dLoc = ltv.getCoordsC();
4355            hitPointCheckLayoutTurnoutSubs(dLoc);
4356        }
4357
4358        if ((lt.getConnectD() == null) && (lt.isTurnoutTypeXover() || lt.isTurnoutTypeSlip())) {
4359            if (lt instanceof LayoutSlip) {
4360                beginHitPointType = HitPointType.SLIP_D;
4361            } else {
4362                beginHitPointType = HitPointType.TURNOUT_D;
4363            }
4364            dLoc = ltv.getCoordsD();
4365            hitPointCheckLayoutTurnoutSubs(dLoc);
4366        }
4367        beginTrack = null;
4368        foundTrack = null;
4369        foundTrackView = null;
4370    }
4371
4372    private void hitPointCheckLayoutTurnoutSubs(@Nonnull Point2D dLoc) {
4373        assert dLoc != null;
4374
4375        if (findLayoutTracksHitPoint(dLoc, true)) {
4376            switch (foundHitPointType) {
4377                case POS_POINT: {
4378                    PositionablePoint p2 = (PositionablePoint) foundTrack;
4379
4380                    if (((p2.getConnect1() == null) && (p2.getConnect2() != null))
4381                            || ((p2.getConnect1() != null) && (p2.getConnect2() == null))) {
4382                        TrackSegment t = p2.getConnect1();
4383
4384                        if (t == null) {
4385                            t = p2.getConnect2();
4386                        }
4387
4388                        if (t == null) {
4389                            return;
4390                        }
4391                        LayoutTurnout lt = (LayoutTurnout) beginTrack;
4392                        try {
4393                            if (lt.getConnection(beginHitPointType) == null) {
4394                                lt.setConnection(beginHitPointType, t, HitPointType.TRACK);
4395                                p2.removeTrackConnection(t);
4396
4397                                if (t.getConnect1() == p2) {
4398                                    t.setNewConnect1(lt, beginHitPointType);
4399                                } else {
4400                                    t.setNewConnect2(lt, beginHitPointType);
4401                                }
4402                                removePositionablePoint(p2);
4403                            }
4404
4405                            if (t.getLayoutBlock() != null) {
4406                                getLEAuxTools().setBlockConnectivityChanged();
4407                            }
4408                        } catch (JmriException e) {
4409                            log.debug("Unable to set location");
4410                        }
4411                    }
4412                    break;
4413                }
4414
4415                case TURNOUT_A:
4416                case TURNOUT_B:
4417                case TURNOUT_C:
4418                case TURNOUT_D:
4419                case SLIP_A:
4420                case SLIP_B:
4421                case SLIP_C:
4422                case SLIP_D: {
4423                    LayoutTurnout ft = (LayoutTurnout) foundTrack;
4424                    addTrackSegment();
4425
4426                    if ((ft.getTurnoutType() == LayoutTurnout.TurnoutType.RH_TURNOUT) || (ft.getTurnoutType() == LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4427                        rotateTurnout(ft);
4428                    }
4429
4430                    // Assign a block to the new zero length track segment.
4431                    ((LayoutTurnoutView) foundTrackView).setTrackSegmentBlock(foundHitPointType, true);
4432                    break;
4433                }
4434
4435                default: {
4436                    log.warn("Unexpected foundPointType {} in hitPointCheckLayoutTurnoutSubs", foundHitPointType);
4437                    break;
4438                }
4439            }
4440        }
4441    }
4442
4443    private void rotateTurnout(@Nonnull LayoutTurnout t) {
4444        assert t != null;
4445
4446        LayoutTurnoutView tv = getLayoutTurnoutView(t);
4447
4448        LayoutTurnout be = (LayoutTurnout) beginTrack;
4449        LayoutTurnoutView bev = getLayoutTurnoutView(be);
4450
4451        if (((beginHitPointType == HitPointType.TURNOUT_A) && ((be.getConnectB() != null) || (be.getConnectC() != null)))
4452                || ((beginHitPointType == HitPointType.TURNOUT_B) && ((be.getConnectA() != null) || (be.getConnectC() != null)))
4453                || ((beginHitPointType == HitPointType.TURNOUT_C) && ((be.getConnectB() != null) || (be.getConnectA() != null)))) {
4454            return;
4455        }
4456
4457        if ((be.getTurnoutType() != LayoutTurnout.TurnoutType.RH_TURNOUT) && (be.getTurnoutType() != LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4458            return;
4459        }
4460
4461        Point2D c, diverg, xy2;
4462
4463        if ((foundHitPointType == HitPointType.TURNOUT_C) && (beginHitPointType == HitPointType.TURNOUT_C)) {
4464            c = tv.getCoordsA();
4465            diverg = tv.getCoordsB();
4466            xy2 = MathUtil.subtract(c, diverg);
4467        } else if ((foundHitPointType == HitPointType.TURNOUT_C)
4468                && ((beginHitPointType == HitPointType.TURNOUT_A) || (beginHitPointType == HitPointType.TURNOUT_B))) {
4469
4470            c = tv.getCoordsCenter();
4471            diverg = tv.getCoordsC();
4472
4473            if (beginHitPointType == HitPointType.TURNOUT_A) {
4474                xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4475            } else {
4476                xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4477            }
4478        } else if (foundHitPointType == HitPointType.TURNOUT_B) {
4479            c = tv.getCoordsA();
4480            diverg = tv.getCoordsB();
4481
4482            switch (beginHitPointType) {
4483                case TURNOUT_B:
4484                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4485                    break;
4486                case TURNOUT_A:
4487                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4488                    break;
4489                case TURNOUT_C:
4490                default:
4491                    xy2 = MathUtil.subtract(bev.getCoordsCenter(), bev.getCoordsC());
4492                    break;
4493            }
4494        } else if (foundHitPointType == HitPointType.TURNOUT_A) {
4495            c = tv.getCoordsA();
4496            diverg = tv.getCoordsB();
4497
4498            switch (beginHitPointType) {
4499                case TURNOUT_A:
4500                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4501                    break;
4502                case TURNOUT_B:
4503                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4504                    break;
4505                case TURNOUT_C:
4506                default:
4507                    xy2 = MathUtil.subtract(bev.getCoordsC(), bev.getCoordsCenter());
4508                    break;
4509            }
4510        } else {
4511            return;
4512        }
4513        Point2D xy = MathUtil.subtract(diverg, c);
4514        double radius = Math.toDegrees(Math.atan2(xy.getY(), xy.getX()));
4515        double eRadius = Math.toDegrees(Math.atan2(xy2.getY(), xy2.getX()));
4516        bev.rotateCoords(radius - eRadius);
4517
4518        Point2D conCord = bev.getCoordsA();
4519        Point2D tCord = tv.getCoordsC();
4520
4521        if (foundHitPointType == HitPointType.TURNOUT_B) {
4522            tCord = tv.getCoordsB();
4523        }
4524
4525        if (foundHitPointType == HitPointType.TURNOUT_A) {
4526            tCord = tv.getCoordsA();
4527        }
4528
4529        switch (beginHitPointType) {
4530            case TURNOUT_A:
4531                conCord = bev.getCoordsA();
4532                break;
4533            case TURNOUT_B:
4534                conCord = bev.getCoordsB();
4535                break;
4536            case TURNOUT_C:
4537                conCord = bev.getCoordsC();
4538                break;
4539            default:
4540                break;
4541        }
4542        xy = MathUtil.subtract(conCord, tCord);
4543        Point2D offset = MathUtil.subtract(bev.getCoordsCenter(), xy);
4544        bev.setCoordsCenter(offset);
4545    }
4546
4547    public List<Positionable> _positionableSelection = new ArrayList<>();
4548    public List<LayoutTrack> _layoutTrackSelection = new ArrayList<>();
4549    public List<LayoutShape> _layoutShapeSelection = new ArrayList<>();
4550
4551    @Nonnull
4552    public List<Positionable> getPositionalSelection() {
4553        return _positionableSelection;
4554    }
4555
4556    @Nonnull
4557    public List<LayoutTrack> getLayoutTrackSelection() {
4558        return _layoutTrackSelection;
4559    }
4560
4561    @Nonnull
4562    public List<LayoutShape> getLayoutShapeSelection() {
4563        return _layoutShapeSelection;
4564    }
4565
4566    private void createSelectionGroups() {
4567        Rectangle2D selectionRect = getSelectionRect();
4568
4569        getContents().forEach((o) -> {
4570            if (selectionRect.contains(o.getLocation())) {
4571
4572                log.trace("found item o of class {}", o.getClass());
4573                if (!_positionableSelection.contains(o)) {
4574                    _positionableSelection.add(o);
4575                }
4576            }
4577        });
4578
4579        getLayoutTracks().forEach((lt) -> {
4580            LayoutTrackView ltv = getLayoutTrackView(lt);
4581            Point2D center = ltv.getCoordsCenter();
4582            if (selectionRect.contains(center)) {
4583                if (!_layoutTrackSelection.contains(lt)) {
4584                    _layoutTrackSelection.add(lt);
4585                }
4586            }
4587        });
4588        assignBlockToSelectionMenuItem.setEnabled(!_layoutTrackSelection.isEmpty());
4589
4590        layoutShapes.forEach((ls) -> {
4591            if (selectionRect.intersects(ls.getBounds())) {
4592                if (!_layoutShapeSelection.contains(ls)) {
4593                    _layoutShapeSelection.add(ls);
4594                }
4595            }
4596        });
4597        redrawPanel();
4598    }
4599
4600    public void clearSelectionGroups() {
4601        selectionActive = false;
4602        _positionableSelection.clear();
4603        _layoutTrackSelection.clear();
4604        assignBlockToSelectionMenuItem.setEnabled(false);
4605        _layoutShapeSelection.clear();
4606    }
4607
4608    private boolean noWarnGlobalDelete = false;
4609
4610    private void deleteSelectedItems() {
4611        if (!noWarnGlobalDelete) {
4612            int selectedValue = JmriJOptionPane.showOptionDialog(this,
4613                    Bundle.getMessage("Question6"), Bundle.getMessage("WarningTitle"),
4614                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
4615                    new Object[]{Bundle.getMessage("ButtonYes"),
4616                        Bundle.getMessage("ButtonNo"),
4617                        Bundle.getMessage("ButtonYesPlus")},
4618                    Bundle.getMessage("ButtonNo"));
4619
4620            // array position 1, ButtonNo or Dialog closed.
4621            if (selectedValue == 1 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
4622                return; // return without creating if "No" response
4623            }
4624
4625            if (selectedValue == 2) { // array positio 2, ButtonYesPlus
4626                // Suppress future warnings, and continue
4627                noWarnGlobalDelete = true;
4628            }
4629        }
4630
4631        _positionableSelection.forEach(this::remove);
4632
4633        _layoutTrackSelection.forEach((lt) -> {
4634            if (lt instanceof PositionablePoint) {
4635                boolean oldWarning = noWarnPositionablePoint;
4636                noWarnPositionablePoint = true;
4637                removePositionablePoint((PositionablePoint) lt);
4638                noWarnPositionablePoint = oldWarning;
4639            } else if (lt instanceof LevelXing) {
4640                boolean oldWarning = noWarnLevelXing;
4641                noWarnLevelXing = true;
4642                removeLevelXing((LevelXing) lt);
4643                noWarnLevelXing = oldWarning;
4644            } else if (lt instanceof LayoutSlip) {
4645                boolean oldWarning = noWarnSlip;
4646                noWarnSlip = true;
4647                removeLayoutSlip((LayoutSlip) lt);
4648                noWarnSlip = oldWarning;
4649            } else if (lt instanceof LayoutTurntable) {
4650                boolean oldWarning = noWarnTurntable;
4651                noWarnTurntable = true;
4652                removeTurntable((LayoutTurntable) lt);
4653                noWarnTurntable = oldWarning;
4654            } else if (lt instanceof LayoutTurnout) {  //<== this includes LayoutSlips
4655                boolean oldWarning = noWarnLayoutTurnout;
4656                noWarnLayoutTurnout = true;
4657                removeLayoutTurnout((LayoutTurnout) lt);
4658                noWarnLayoutTurnout = oldWarning;
4659            }
4660        });
4661
4662        layoutShapes.removeAll(_layoutShapeSelection);
4663
4664        clearSelectionGroups();
4665        redrawPanel();
4666    }
4667
4668    private void amendSelectionGroup(@Nonnull Positionable pos) {
4669        Positionable p = Objects.requireNonNull(pos);
4670
4671        if (_positionableSelection.contains(p)) {
4672            _positionableSelection.remove(p);
4673        } else {
4674            _positionableSelection.add(p);
4675        }
4676        redrawPanel();
4677    }
4678
4679    public void amendSelectionGroup(@Nonnull LayoutTrack track) {
4680        LayoutTrack p = Objects.requireNonNull(track);
4681
4682        if (_layoutTrackSelection.contains(p)) {
4683            _layoutTrackSelection.remove(p);
4684        } else {
4685            _layoutTrackSelection.add(p);
4686        }
4687        assignBlockToSelectionMenuItem.setEnabled(!_layoutTrackSelection.isEmpty());
4688        redrawPanel();
4689    }
4690
4691    public void amendSelectionGroup(@Nonnull LayoutShape shape) {
4692        LayoutShape ls = Objects.requireNonNull(shape);
4693
4694        if (_layoutShapeSelection.contains(ls)) {
4695            _layoutShapeSelection.remove(ls);
4696        } else {
4697            _layoutShapeSelection.add(ls);
4698        }
4699        redrawPanel();
4700    }
4701
4702    public void alignSelection(boolean alignX) {
4703        Point2D minPoint = MathUtil.infinityPoint2D;
4704        Point2D maxPoint = MathUtil.zeroPoint2D;
4705        Point2D sumPoint = MathUtil.zeroPoint2D;
4706        int cnt = 0;
4707
4708        for (Positionable comp : _positionableSelection) {
4709            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
4710                continue;   // skip non-positionables
4711            }
4712            Point2D p = MathUtil.pointToPoint2D(comp.getLocation());
4713            minPoint = MathUtil.min(minPoint, p);
4714            maxPoint = MathUtil.max(maxPoint, p);
4715            sumPoint = MathUtil.add(sumPoint, p);
4716            cnt++;
4717        }
4718
4719        for (LayoutTrack lt : _layoutTrackSelection) {
4720            LayoutTrackView ltv = getLayoutTrackView(lt);
4721            Point2D p = ltv.getCoordsCenter();
4722            minPoint = MathUtil.min(minPoint, p);
4723            maxPoint = MathUtil.max(maxPoint, p);
4724            sumPoint = MathUtil.add(sumPoint, p);
4725            cnt++;
4726        }
4727
4728        for (LayoutShape ls : _layoutShapeSelection) {
4729            Point2D p = ls.getCoordsCenter();
4730            minPoint = MathUtil.min(minPoint, p);
4731            maxPoint = MathUtil.max(maxPoint, p);
4732            sumPoint = MathUtil.add(sumPoint, p);
4733            cnt++;
4734        }
4735
4736        Point2D avePoint = MathUtil.divide(sumPoint, cnt);
4737        int aveX = (int) avePoint.getX();
4738        int aveY = (int) avePoint.getY();
4739
4740        for (Positionable comp : _positionableSelection) {
4741            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
4742                continue;   // skip non-positionables
4743            }
4744
4745            if (alignX) {
4746                comp.setLocation(aveX, comp.getY());
4747            } else {
4748                comp.setLocation(comp.getX(), aveY);
4749            }
4750        }
4751
4752        _layoutTrackSelection.forEach((lt) -> {
4753            LayoutTrackView ltv = getLayoutTrackView(lt);
4754            if (alignX) {
4755                ltv.setCoordsCenter(new Point2D.Double(aveX, ltv.getCoordsCenter().getY()));
4756            } else {
4757                ltv.setCoordsCenter(new Point2D.Double(ltv.getCoordsCenter().getX(), aveY));
4758            }
4759        });
4760
4761        _layoutShapeSelection.forEach((ls) -> {
4762            if (alignX) {
4763                ls.setCoordsCenter(new Point2D.Double(aveX, ls.getCoordsCenter().getY()));
4764            } else {
4765                ls.setCoordsCenter(new Point2D.Double(ls.getCoordsCenter().getX(), aveY));
4766            }
4767        });
4768
4769        redrawPanel();
4770    }
4771
4772    private boolean showAlignPopup() {
4773        return ((!_positionableSelection.isEmpty())
4774                || (!_layoutTrackSelection.isEmpty())
4775                || (!_layoutShapeSelection.isEmpty()));
4776    }
4777
4778    /**
4779     * Offer actions to align the selected Positionable items either
4780     * Horizontally (at average y coord) or Vertically (at average x coord).
4781     *
4782     * @param popup the JPopupMenu to add alignment menu to
4783     * @return true if alignment menu added
4784     */
4785    public boolean setShowAlignmentMenu(@Nonnull JPopupMenu popup) {
4786        if (showAlignPopup()) {
4787            JMenu edit = new JMenu(Bundle.getMessage("EditAlignment"));
4788            edit.add(new AbstractAction(Bundle.getMessage("AlignX")) {
4789                @Override
4790                public void actionPerformed(ActionEvent event) {
4791                    alignSelection(true);
4792                }
4793            });
4794            edit.add(new AbstractAction(Bundle.getMessage("AlignY")) {
4795                @Override
4796                public void actionPerformed(ActionEvent event) {
4797                    alignSelection(false);
4798                }
4799            });
4800            popup.add(edit);
4801
4802            return true;
4803        }
4804        return false;
4805    }
4806
4807    @Override
4808    public void keyPressed(@Nonnull KeyEvent event) {
4809        if (event.getKeyCode() == KeyEvent.VK_DELETE) {
4810            deleteSelectedItems();
4811            return;
4812        }
4813
4814        double deltaX = returnDeltaPositionX(event);
4815        double deltaY = returnDeltaPositionY(event);
4816
4817        if ((deltaX != 0) || (deltaY != 0)) {
4818            selectionX += deltaX;
4819            selectionY += deltaY;
4820
4821            Point2D delta = new Point2D.Double(deltaX, deltaY);
4822            _positionableSelection.forEach((c) -> {
4823                Point2D newPoint = c.getLocation();
4824                if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
4825                    MemoryIcon pm = (MemoryIcon) c;
4826                    newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
4827                }
4828                newPoint = MathUtil.add(newPoint, delta);
4829                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4830                c.setLocation(MathUtil.point2DToPoint(newPoint));
4831            });
4832
4833            _layoutTrackSelection.forEach((lt) -> {
4834                LayoutTrackView ltv = getLayoutTrackView(lt);
4835                Point2D newPoint = MathUtil.add(ltv.getCoordsCenter(), delta);
4836                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4837                getLayoutTrackView(lt).setCoordsCenter(newPoint);
4838            });
4839
4840            _layoutShapeSelection.forEach((ls) -> {
4841                Point2D newPoint = MathUtil.add(ls.getCoordsCenter(), delta);
4842                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4843                ls.setCoordsCenter(newPoint);
4844            });
4845            redrawPanel();
4846            return;
4847        }
4848        getLayoutEditorToolBarPanel().keyPressed(event);
4849    }
4850
4851    private double returnDeltaPositionX(@Nonnull KeyEvent event) {
4852        double result = 0.0;
4853        double amount = event.isShiftDown() ? 5.0 : 1.0;
4854
4855        switch (event.getKeyCode()) {
4856            case KeyEvent.VK_LEFT: {
4857                result = -amount;
4858                break;
4859            }
4860
4861            case KeyEvent.VK_RIGHT: {
4862                result = +amount;
4863                break;
4864            }
4865
4866            default: {
4867                break;
4868            }
4869        }
4870        return result;
4871    }
4872
4873    private double returnDeltaPositionY(@Nonnull KeyEvent event) {
4874        double result = 0.0;
4875        double amount = event.isShiftDown() ? 5.0 : 1.0;
4876
4877        switch (event.getKeyCode()) {
4878            case KeyEvent.VK_UP: {
4879                result = -amount;
4880                break;
4881            }
4882
4883            case KeyEvent.VK_DOWN: {
4884                result = +amount;
4885                break;
4886            }
4887
4888            default: {
4889                break;
4890            }
4891        }
4892        return result;
4893    }
4894
4895    int _prevNumSel = 0;
4896
4897    @Override
4898    public void mouseMoved(@Nonnull JmriMouseEvent event) {
4899        // initialize mouse position
4900        calcLocation(event);
4901
4902        // if alt modifier is down invert the snap to grid behaviour
4903        snapToGridInvert = event.isAltDown();
4904
4905        if (isEditable()) {
4906            leToolBarPanel.setLocationText(dLoc);
4907        }
4908        List<Positionable> selections = getSelectedItems(event);
4909        Positionable selection = null;
4910        int numSel = selections.size();
4911
4912        if (numSel > 0) {
4913            selection = selections.get(0);
4914        }
4915
4916        if ((selection != null) && (selection.getDisplayLevel() > Editor.BKG) && selection.showToolTip()) {
4917            showToolTip(selection, event);
4918        } else if (_targetPanel.getCursor() != Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) {
4919            super.setToolTip(null);
4920        }
4921
4922        if (numSel != _prevNumSel) {
4923            redrawPanel();
4924            _prevNumSel = numSel;
4925        }
4926
4927        if (findLayoutTracksHitPoint(dLoc)) {
4928            // log.debug("foundTrack: {}", foundTrack);
4929            if (HitPointType.isControlHitType(foundHitPointType)) {
4930                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
4931                setTurnoutTooltip();
4932            } else {
4933                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
4934            }
4935            foundTrack = null;
4936            foundHitPointType = HitPointType.NONE;
4937        } else {
4938            _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
4939        }
4940    }   // mouseMoved
4941
4942    private void setTurnoutTooltip() {
4943        if (foundTrackView instanceof LayoutTurnoutView) {
4944            var ltv = (LayoutTurnoutView) foundTrackView;
4945            var lt = ltv.getLayoutTurnout();
4946            if (lt.showToolTip()) {
4947                var tt = lt.getToolTip();
4948                if (tt != null) {
4949                    tt.setText(lt.getNameString());
4950                    var coords = ltv.getCoordsCenter();
4951                    int offsetY = (int) (getTurnoutCircleSize() * SIZE);
4952                    tt.setLocation((int) coords.getX(), (int) coords.getY() + offsetY);
4953                    setToolTip(tt);
4954                }
4955            }
4956        }
4957    }
4958
4959    public void setAllShowLayoutTurnoutToolTip(boolean state) {
4960        log.debug("setAllShowLayoutTurnoutToolTip: {}", state);
4961        for (LayoutTurnout lt : getLayoutTurnoutsAndSlips()) {
4962            lt.setShowToolTip(state);
4963        }
4964    }
4965
4966    private boolean isDragging = false;
4967
4968    @Override
4969    public void mouseDragged(@Nonnull JmriMouseEvent event) {
4970        // initialize mouse position
4971        calcLocation(event);
4972
4973        checkHighlightCursor();
4974
4975        // ignore this event if still at the original point
4976        if ((!isDragging) && (xLoc == getAnchorX()) && (yLoc == getAnchorY())) {
4977            return;
4978        }
4979
4980        // if alt modifier is down invert the snap to grid behaviour
4981        snapToGridInvert = event.isAltDown();
4982
4983        // process this mouse dragged event
4984        if (isEditable()) {
4985            leToolBarPanel.setLocationText(dLoc);
4986        }
4987        currentPoint = MathUtil.add(dLoc, startDelta);
4988        // don't allow negative placement, objects could become unreachable
4989        currentPoint = MathUtil.max(currentPoint, MathUtil.zeroPoint2D);
4990
4991        if ((selectedObject != null) && (event.isMetaDown() || event.isAltDown())
4992                && (selectedHitPointType == HitPointType.MARKER)) {
4993            // marker moves regardless of editMode or positionable
4994            PositionableLabel pl = (PositionableLabel) selectedObject;
4995            pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
4996            isDragging = true;
4997            redrawPanel();
4998            return;
4999        }
5000
5001        if (isEditable()) {
5002            if ((selectedObject != null) && event.isMetaDown() && allPositionable()) {
5003                if (snapToGridOnMove != snapToGridInvert) {
5004                    // this snaps currentPoint to the grid
5005                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
5006                    xLoc = (int) currentPoint.getX();
5007                    yLoc = (int) currentPoint.getY();
5008                    leToolBarPanel.setLocationText(currentPoint);
5009                }
5010
5011                if ((!_positionableSelection.isEmpty())
5012                        || (!_layoutTrackSelection.isEmpty())
5013                        || (!_layoutShapeSelection.isEmpty())) {
5014                    Point2D lastPoint = new Point2D.Double(_lastX, _lastY);
5015                    Point2D offset = MathUtil.subtract(currentPoint, lastPoint);
5016                    Point2D newPoint;
5017
5018                    for (Positionable c : _positionableSelection) {
5019                        if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
5020                            MemoryIcon pm = (MemoryIcon) c;
5021                            newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
5022                        } else {
5023                            newPoint = c.getLocation();
5024                        }
5025                        newPoint = MathUtil.add(newPoint, offset);
5026                        // don't allow negative placement, objects could become unreachable
5027                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5028                        c.setLocation(MathUtil.point2DToPoint(newPoint));
5029                    }
5030
5031                    for (LayoutTrack lt : _layoutTrackSelection) {
5032                        LayoutTrackView ltv = getLayoutTrackView(lt);
5033                        Point2D center = ltv.getCoordsCenter();
5034                        newPoint = MathUtil.add(center, offset);
5035                        // don't allow negative placement, objects could become unreachable
5036                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5037                        getLayoutTrackView(lt).setCoordsCenter(newPoint);
5038                    }
5039
5040                    for (LayoutShape ls : _layoutShapeSelection) {
5041                        Point2D center = ls.getCoordsCenter();
5042                        newPoint = MathUtil.add(center, offset);
5043                        // don't allow negative placement, objects could become unreachable
5044                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
5045                        ls.setCoordsCenter(newPoint);
5046                    }
5047
5048                    _lastX = xLoc;
5049                    _lastY = yLoc;
5050                } else {
5051                    switch (selectedHitPointType) {
5052                        case POS_POINT:
5053                        case TURNOUT_CENTER:
5054                        case LEVEL_XING_CENTER:
5055                        case SLIP_LEFT:
5056                        case SLIP_RIGHT:
5057                        case TURNTABLE_CENTER: {
5058                            getLayoutTrackView((LayoutTrack) selectedObject).setCoordsCenter(currentPoint);
5059                            isDragging = true;
5060                            break;
5061                        }
5062
5063                        case TURNOUT_A: {
5064                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsA(currentPoint);
5065                            break;
5066                        }
5067
5068                        case TURNOUT_B: {
5069                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsB(currentPoint);
5070                            break;
5071                        }
5072
5073                        case TURNOUT_C: {
5074                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsC(currentPoint);
5075                            break;
5076                        }
5077
5078                        case TURNOUT_D: {
5079                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsD(currentPoint);
5080                            break;
5081                        }
5082
5083                        case LEVEL_XING_A: {
5084                            getLevelXingView((LevelXing) selectedObject).setCoordsA(currentPoint);
5085                            break;
5086                        }
5087
5088                        case LEVEL_XING_B: {
5089                            getLevelXingView((LevelXing) selectedObject).setCoordsB(currentPoint);
5090                            break;
5091                        }
5092
5093                        case LEVEL_XING_C: {
5094                            getLevelXingView((LevelXing) selectedObject).setCoordsC(currentPoint);
5095                            break;
5096                        }
5097
5098                        case LEVEL_XING_D: {
5099                            getLevelXingView((LevelXing) selectedObject).setCoordsD(currentPoint);
5100                            break;
5101                        }
5102
5103                        case SLIP_A: {
5104                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsA(currentPoint);
5105                            break;
5106                        }
5107
5108                        case SLIP_B: {
5109                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsB(currentPoint);
5110                            break;
5111                        }
5112
5113                        case SLIP_C: {
5114                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsC(currentPoint);
5115                            break;
5116                        }
5117
5118                        case SLIP_D: {
5119                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsD(currentPoint);
5120                            break;
5121                        }
5122
5123                        case LAYOUT_POS_LABEL:
5124                        case MULTI_SENSOR: {
5125                            PositionableLabel pl = (PositionableLabel) selectedObject;
5126
5127                            if (pl.isPositionable()) {
5128                                pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5129                                isDragging = true;
5130                            }
5131                            break;
5132                        }
5133
5134                        case LAYOUT_POS_JCOMP: {
5135                            PositionableJComponent c = (PositionableJComponent) selectedObject;
5136
5137                            if (c.isPositionable()) {
5138                                c.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
5139                                isDragging = true;
5140                            }
5141                            break;
5142                        }
5143
5144                        case TRACK_CIRCLE_CENTRE: {
5145                            TrackSegmentView tv = getTrackSegmentView((TrackSegment) selectedObject);
5146                            tv.reCalculateTrackSegmentAngle(currentPoint.getX(), currentPoint.getY());
5147                            break;
5148                        }
5149
5150                        default: {
5151                            if (HitPointType.isBezierHitType(foundHitPointType)) {
5152                                int index = selectedHitPointType.bezierPointIndex();
5153                                getTrackSegmentView((TrackSegment) selectedObject).setBezierControlPoint(currentPoint, index);
5154                            } else if ((selectedHitPointType == HitPointType.SHAPE_CENTER)) {
5155                                ((LayoutShape) selectedObject).setCoordsCenter(currentPoint);
5156                            } else if (HitPointType.isShapePointOffsetHitPointType(selectedHitPointType)) {
5157                                int index = selectedHitPointType.shapePointIndex();
5158                                ((LayoutShape) selectedObject).setPoint(index, currentPoint);
5159                            } else if (HitPointType.isTurntableRayHitType(selectedHitPointType)) {
5160                                LayoutTurntable turn = (LayoutTurntable) selectedObject;
5161                                LayoutTurntableView turnView = getLayoutTurntableView(turn);
5162                                turnView.setRayCoordsIndexed(currentPoint.getX(), currentPoint.getY(),
5163                                        selectedHitPointType.turntableTrackIndex());
5164                            }
5165                            break;
5166                        }
5167                    }
5168                }
5169            } else if ((beginTrack != null)
5170                    && event.isShiftDown()
5171                    && leToolBarPanel.trackButton.isSelected()) {
5172                // dragging from first end of Track Segment
5173                currentLocation = new Point2D.Double(xLoc, yLoc);
5174                boolean needResetCursor = (foundTrack != null);
5175
5176                if (findLayoutTracksHitPoint(currentLocation, true)) {
5177                    // have match to free connection point, change cursor
5178                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
5179                } else if (needResetCursor) {
5180                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
5181                }
5182            } else if (event.isShiftDown()
5183                    && leToolBarPanel.shapeButton.isSelected() && (selectedObject != null)) {
5184                // dragging from end of shape
5185                currentLocation = new Point2D.Double(xLoc, yLoc);
5186            } else if (selectionActive && !event.isShiftDown() && !event.isMetaDown()) {
5187                selectionWidth = xLoc - selectionX;
5188                selectionHeight = yLoc - selectionY;
5189            }
5190            redrawPanel();
5191        } else {
5192            Rectangle r = new Rectangle(event.getX(), event.getY(), 1, 1);
5193            ((JComponent) event.getSource()).scrollRectToVisible(r);
5194        }   // if (isEditable())
5195    }   // mouseDragged
5196
5197    @Override
5198    public void mouseEntered(@Nonnull JmriMouseEvent event) {
5199        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
5200    }
5201
5202    /**
5203     * Add an Anchor point.
5204     */
5205    public void addAnchor() {
5206        addAnchor(currentPoint);
5207    }
5208
5209    @Nonnull
5210    public PositionablePoint addAnchor(@Nonnull Point2D point) {
5211        Point2D p = Objects.requireNonNull(point);
5212
5213        // get unique name
5214        String name = finder.uniqueName("A", ++numAnchors);
5215
5216        // create object
5217        PositionablePoint o = new PositionablePoint(name,
5218                PositionablePoint.PointType.ANCHOR, this);
5219        PositionablePointView pv = new PositionablePointView(o, p, this);
5220        addLayoutTrack(o, pv);
5221
5222        setDirty();
5223
5224        return o;
5225    }
5226
5227    /**
5228     * Add an End Bumper point.
5229     */
5230    public void addEndBumper() {
5231        // get unique name
5232        String name = finder.uniqueName("EB", ++numEndBumpers);
5233
5234        // create object
5235        PositionablePoint o = new PositionablePoint(name,
5236                PositionablePoint.PointType.END_BUMPER, this);
5237        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
5238        addLayoutTrack(o, pv);
5239
5240        setDirty();
5241    }
5242
5243    /**
5244     * Add an Edge Connector point.
5245     */
5246    public void addEdgeConnector() {
5247        // get unique name
5248        String name = finder.uniqueName("EC", ++numEdgeConnectors);
5249
5250        // create object
5251        PositionablePoint o = new PositionablePoint(name,
5252                PositionablePoint.PointType.EDGE_CONNECTOR, this);
5253        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
5254        addLayoutTrack(o, pv);
5255
5256        setDirty();
5257    }
5258
5259    /**
5260     * Add a Track Segment
5261     */
5262    public void addTrackSegment() {
5263        // get unique name
5264        String name = finder.uniqueName("T", ++numTrackSegments);
5265
5266        // create object
5267        newTrack = new TrackSegment(name, beginTrack, beginHitPointType,
5268                foundTrack, foundHitPointType,
5269                leToolBarPanel.mainlineTrack.isSelected(), this);
5270
5271        TrackSegmentView tsv = new TrackSegmentView(
5272                newTrack,
5273                this
5274        );
5275        addLayoutTrack(newTrack, tsv);
5276
5277        setDirty();
5278
5279        // link to connected objects
5280        setLink(beginTrack, beginHitPointType, newTrack, HitPointType.TRACK);
5281        setLink(foundTrack, foundHitPointType, newTrack, HitPointType.TRACK);
5282
5283        // check on layout block
5284        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5285        if (newName == null) {
5286            newName = "";
5287        }
5288        LayoutBlock b = provideLayoutBlock(newName);
5289
5290        if (b != null) {
5291            newTrack.setLayoutBlock(b);
5292            getLEAuxTools().setBlockConnectivityChanged();
5293
5294            // check on occupancy sensor
5295            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5296            if (sensorName == null) {
5297                sensorName = "";
5298            }
5299
5300            if (!sensorName.isEmpty()) {
5301                if (!validateSensor(sensorName, b, this)) {
5302                    b.setOccupancySensorName("");
5303                } else {
5304                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5305                }
5306            }
5307            newTrack.updateBlockInfo();
5308        }
5309    }
5310
5311    /**
5312     * Add a Level Crossing
5313     */
5314    public void addLevelXing() {
5315        // get unique name
5316        String name = finder.uniqueName("X", ++numLevelXings);
5317
5318        // create object
5319        LevelXing o = new LevelXing(name, this);
5320        LevelXingView ov = new LevelXingView(o, currentPoint, this);
5321        addLayoutTrack(o, ov);
5322
5323        setDirty();
5324
5325        // check on layout block
5326        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5327        if (newName == null) {
5328            newName = "";
5329        }
5330        LayoutBlock b = provideLayoutBlock(newName);
5331
5332        if (b != null) {
5333            o.setLayoutBlockAC(b);
5334            o.setLayoutBlockBD(b);
5335
5336            // check on occupancy sensor
5337            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5338            if (sensorName == null) {
5339                sensorName = "";
5340            }
5341
5342            if (!sensorName.isEmpty()) {
5343                if (!validateSensor(sensorName, b, this)) {
5344                    b.setOccupancySensorName("");
5345                } else {
5346                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5347                }
5348            }
5349        }
5350    }
5351
5352    /**
5353     * Add a LayoutSlip
5354     *
5355     * @param type the slip type
5356     */
5357    public void addLayoutSlip(LayoutTurnout.TurnoutType type) {
5358        // get the rotation entry
5359        double rot;
5360        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
5361
5362        if (s.isEmpty()) {
5363            rot = 0.0;
5364        } else {
5365            try {
5366                rot = IntlUtilities.doubleValue(s);
5367            } catch (ParseException e) {
5368                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5369                        + e, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5370
5371                return;
5372            }
5373        }
5374
5375        // get unique name
5376        String name = finder.uniqueName("SL", ++numLayoutSlips);
5377
5378        // create object
5379        LayoutSlip o;
5380        LayoutSlipView ov;
5381
5382        switch (type) {
5383            case DOUBLE_SLIP:
5384                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5385                o = lds;
5386                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5387                break;
5388            case SINGLE_SLIP:
5389                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5390                o = lss;
5391                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5392                break;
5393            default:
5394                log.error("can't create slip {} with type {}", name, type);
5395                return; // without creating
5396        }
5397
5398        addLayoutTrack(o, ov);
5399
5400        setDirty();
5401
5402        // check on layout block
5403        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5404        if (newName == null) {
5405            newName = "";
5406        }
5407        LayoutBlock b = provideLayoutBlock(newName);
5408
5409        if (b != null) {
5410            ov.setLayoutBlock(b);
5411
5412            // check on occupancy sensor
5413            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5414            if (sensorName == null) {
5415                sensorName = "";
5416            }
5417
5418            if (!sensorName.isEmpty()) {
5419                if (!validateSensor(sensorName, b, this)) {
5420                    b.setOccupancySensorName("");
5421                } else {
5422                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5423                }
5424            }
5425        }
5426
5427        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5428        if (turnoutName == null) {
5429            turnoutName = "";
5430        }
5431
5432        if (validatePhysicalTurnout(turnoutName, this)) {
5433            // turnout is valid and unique.
5434            o.setTurnout(turnoutName);
5435
5436            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5437                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5438            }
5439        } else {
5440            o.setTurnout("");
5441            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5442            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5443        }
5444        turnoutName = leToolBarPanel.extraTurnoutNameComboBox.getSelectedItemDisplayName();
5445        if (turnoutName == null) {
5446            turnoutName = "";
5447        }
5448
5449        if (validatePhysicalTurnout(turnoutName, this)) {
5450            // turnout is valid and unique.
5451            o.setTurnoutB(turnoutName);
5452
5453            if (o.getTurnoutB().getSystemName().equals(turnoutName)) {
5454                leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(o.getTurnoutB());
5455            }
5456        } else {
5457            o.setTurnoutB("");
5458            leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(null);
5459            leToolBarPanel.extraTurnoutNameComboBox.setSelectedIndex(-1);
5460        }
5461    }
5462
5463    /**
5464     * Add a Layout Turnout
5465     *
5466     * @param type the turnout type
5467     */
5468    public void addLayoutTurnout(LayoutTurnout.TurnoutType type) {
5469        // get the rotation entry
5470        double rot;
5471        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
5472
5473        if (s.isEmpty()) {
5474            rot = 0.0;
5475        } else {
5476            try {
5477                rot = IntlUtilities.doubleValue(s);
5478            } catch (ParseException e) {
5479                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5480                        + e, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5481
5482                return;
5483            }
5484        }
5485
5486        // get unique name
5487        String name = finder.uniqueName("TO", ++numLayoutTurnouts);
5488
5489        // create object - check all types, although not clear all actually reach here
5490        LayoutTurnout o;
5491        LayoutTurnoutView ov;
5492
5493        switch (type) {
5494
5495            case RH_TURNOUT:
5496                LayoutRHTurnout lrht = new LayoutRHTurnout(name, this);
5497                o = lrht;
5498                ov = new LayoutRHTurnoutView(lrht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5499                break;
5500            case LH_TURNOUT:
5501                LayoutLHTurnout llht = new LayoutLHTurnout(name, this);
5502                o = llht;
5503                ov = new LayoutLHTurnoutView(llht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5504                break;
5505            case WYE_TURNOUT:
5506                LayoutWye lw = new LayoutWye(name, this);
5507                o = lw;
5508                ov = new LayoutWyeView(lw, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5509                break;
5510            case DOUBLE_XOVER:
5511                LayoutDoubleXOver ldx = new LayoutDoubleXOver(name, this);
5512                o = ldx;
5513                ov = new LayoutDoubleXOverView(ldx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5514                break;
5515            case RH_XOVER:
5516                LayoutRHXOver lrx = new LayoutRHXOver(name, this);
5517                o = lrx;
5518                ov = new LayoutRHXOverView(lrx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5519                break;
5520            case LH_XOVER:
5521                LayoutLHXOver llx = new LayoutLHXOver(name, this);
5522                o = llx;
5523                ov = new LayoutLHXOverView(llx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5524                break;
5525
5526            case DOUBLE_SLIP:
5527                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5528                o = lds;
5529                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5530                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5531                break;
5532            case SINGLE_SLIP:
5533                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5534                o = lss;
5535                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5536                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5537                break;
5538
5539            default:
5540                log.error("can't create LayoutTrack {} with type {}", name, type);
5541                return; // without creating
5542        }
5543
5544        addLayoutTrack(o, ov);
5545
5546        setDirty();
5547
5548        // check on layout block
5549        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5550        if (newName == null) {
5551            newName = "";
5552        }
5553        LayoutBlock b = provideLayoutBlock(newName);
5554
5555        if (b != null) {
5556            ov.setLayoutBlock(b);
5557
5558            // check on occupancy sensor
5559            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5560            if (sensorName == null) {
5561                sensorName = "";
5562            }
5563
5564            if (!sensorName.isEmpty()) {
5565                if (!validateSensor(sensorName, b, this)) {
5566                    b.setOccupancySensorName("");
5567                } else {
5568                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5569                }
5570            }
5571        }
5572
5573        // set default continuing route Turnout State
5574        o.setContinuingSense(Turnout.CLOSED);
5575
5576        // check on a physical turnout
5577        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5578        if (turnoutName == null) {
5579            turnoutName = "";
5580        }
5581
5582        if (validatePhysicalTurnout(turnoutName, this)) {
5583            // turnout is valid and unique.
5584            o.setTurnout(turnoutName);
5585
5586            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5587                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5588            }
5589        } else {
5590            o.setTurnout("");
5591            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5592            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5593        }
5594    }
5595
5596    /**
5597     * Validates that a physical turnout exists and is unique among Layout
5598     * Turnouts Returns true if valid turnout was entered, false otherwise
5599     *
5600     * @param inTurnoutName the (system or user) name of the turnout
5601     * @param inOpenPane    the pane over which to show dialogs (null to
5602     *                      suppress dialogs)
5603     * @return true if valid
5604     */
5605    public boolean validatePhysicalTurnout(
5606            @Nonnull String inTurnoutName,
5607            @CheckForNull Component inOpenPane) {
5608        // check if turnout name was entered
5609        if (inTurnoutName.isEmpty()) {
5610            // no turnout entered
5611            return false;
5612        }
5613
5614        // check that the unique turnout name corresponds to a defined physical turnout
5615        Turnout t = InstanceManager.turnoutManagerInstance().getTurnout(inTurnoutName);
5616        if (t == null) {
5617            // There is no turnout corresponding to this name
5618            if (inOpenPane != null) {
5619                JmriJOptionPane.showMessageDialog(inOpenPane,
5620                        MessageFormat.format(Bundle.getMessage("Error8"), inTurnoutName),
5621                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5622            }
5623            return false;
5624        }
5625
5626        log.debug("validatePhysicalTurnout('{}')", inTurnoutName);
5627        boolean result = true;  // assume success (optimist!)
5628
5629        // ensure that this turnout is unique among Layout Turnouts in this Layout
5630        for (LayoutTurnout lt : getLayoutTurnouts()) {
5631            t = lt.getTurnout();
5632            if (t != null) {
5633                String sname = t.getSystemName();
5634                String uname = t.getUserName();
5635                log.debug("{}: Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5636                if ((sname.equals(inTurnoutName))
5637                        || ((uname != null) && (uname.equals(inTurnoutName)))) {
5638                    result = false;
5639                    break;
5640                }
5641            }
5642
5643            // Only check for the second turnout if the type is a double cross over
5644            // otherwise the second turnout is used to throw an additional turnout at
5645            // the same time.
5646            if (lt.isTurnoutTypeXover()) {
5647                t = lt.getSecondTurnout();
5648                if (t != null) {
5649                    String sname = t.getSystemName();
5650                    String uname = t.getUserName();
5651                    log.debug("{}: 2nd Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5652                    if ((sname.equals(inTurnoutName))
5653                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5654                        result = false;
5655                        break;
5656                    }
5657                }
5658            }
5659        }
5660
5661        if (result) {   // only need to test slips if we haven't failed yet...
5662            // ensure that this turnout is unique among Layout slips in this Layout
5663            for (LayoutSlip sl : getLayoutSlips()) {
5664                t = sl.getTurnout();
5665                if (t != null) {
5666                    String sname = t.getSystemName();
5667                    String uname = t.getUserName();
5668                    log.debug("{}: slip Turnout tested '{}' and '{}'.", sl.getName(), sname, uname);
5669                    if ((sname.equals(inTurnoutName))
5670                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5671                        result = false;
5672                        break;
5673                    }
5674                }
5675
5676                t = sl.getTurnoutB();
5677                if (t != null) {
5678                    String sname = t.getSystemName();
5679                    String uname = t.getUserName();
5680                    log.debug("{}: slip Turnout B tested '{}' and '{}'.", sl.getName(), sname, uname);
5681                    if ((sname.equals(inTurnoutName))
5682                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5683                        result = false;
5684                        break;
5685                    }
5686                }
5687            }
5688        }
5689
5690        if (result) {   // only need to test Turntable turnouts if we haven't failed yet...
5691            // ensure that this turntable turnout is unique among turnouts in this Layout
5692            for (LayoutTurntable tt : getLayoutTurntables()) {
5693                for (LayoutTurntable.RayTrack ray : tt.getRayTrackList()) {
5694                    t = ray.getTurnout();
5695                    if (t != null) {
5696                        String sname = t.getSystemName();
5697                        String uname = t.getUserName();
5698                        log.debug("{}: Turntable turnout tested '{}' and '{}'.", ray.getTurnoutName(), sname, uname);
5699                        if ((sname.equals(inTurnoutName))
5700                                || ((uname != null) && (uname.equals(inTurnoutName)))) {
5701                            result = false;
5702                            break;
5703                        }
5704                    }
5705                }
5706            }
5707        }
5708
5709        if (!result && (inOpenPane != null)) {
5710            JmriJOptionPane.showMessageDialog(inOpenPane,
5711                    MessageFormat.format(Bundle.getMessage("Error4"), inTurnoutName),
5712                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5713        }
5714        return result;
5715    }
5716
5717    /**
5718     * link the 'from' object and type to the 'to' object and type
5719     *
5720     * @param fromObject    the object to link from
5721     * @param fromPointType the object type to link from
5722     * @param toObject      the object to link to
5723     * @param toPointType   the object type to link to
5724     */
5725    public void setLink(@Nonnull LayoutTrack fromObject, HitPointType fromPointType,
5726            @Nonnull LayoutTrack toObject, HitPointType toPointType) {
5727        switch (fromPointType) {
5728            case POS_POINT: {
5729                if ((toPointType == HitPointType.TRACK) && (fromObject instanceof PositionablePoint)) {
5730                    ((PositionablePoint) fromObject).setTrackConnection((TrackSegment) toObject);
5731                } else {
5732                    log.error("Attempt to link a non-TRACK connection ('{}')to a Positionable Point ('{}')",
5733                            toObject.getName(), fromObject.getName());
5734                }
5735                break;
5736            }
5737
5738            case TURNOUT_A:
5739            case TURNOUT_B:
5740            case TURNOUT_C:
5741            case TURNOUT_D:
5742            case SLIP_A:
5743            case SLIP_B:
5744            case SLIP_C:
5745            case SLIP_D:
5746            case LEVEL_XING_A:
5747            case LEVEL_XING_B:
5748            case LEVEL_XING_C:
5749            case LEVEL_XING_D: {
5750                try {
5751                    fromObject.setConnection(fromPointType, toObject, toPointType);
5752                } catch (JmriException e) {
5753                    // ignore (log.error in setConnection method)
5754                }
5755                break;
5756            }
5757
5758            case TRACK: {
5759                // should never happen, Track Segment links are set in ctor
5760                log.error("Illegal request to set a Track Segment link");
5761                break;
5762            }
5763
5764            default: {
5765                if (HitPointType.isTurntableRayHitType(fromPointType) && (fromObject instanceof LayoutTurntable)) {
5766                    if (toObject instanceof TrackSegment) {
5767                        ((LayoutTurntable) fromObject).setRayConnect((TrackSegment) toObject,
5768                                fromPointType.turntableTrackIndex());
5769                    } else {
5770                        log.warn("setLink found expected toObject type {} with fromPointType {} fromObject type {}",
5771                                toObject.getClass(), fromPointType, fromObject.getClass(), new Exception("traceback"));
5772                    }
5773                } else {
5774                    log.warn("setLink found expected fromObject type {} with fromPointType {} toObject type {}",
5775                            fromObject.getClass(), fromPointType, toObject.getClass(), new Exception("traceback"));
5776                }
5777                break;
5778            }
5779        }
5780    }
5781
5782    /**
5783     * Return a layout block with the entered name, creating a new one if
5784     * needed. Note that the entered name becomes the user name of the
5785     * LayoutBlock, and a system name is automatically created by
5786     * LayoutBlockManager if needed.
5787     * <p>
5788     * If the block name is a system name, then the user will have to supply a
5789     * user name for the block.
5790     * <p>
5791     * Some, but not all, errors pop a Swing error dialog in addition to
5792     * logging.
5793     *
5794     * @param inBlockName the entered name
5795     * @return the provided LayoutBlock
5796     */
5797    public LayoutBlock provideLayoutBlock(@Nonnull String inBlockName) {
5798        LayoutBlock result = null; // assume failure (pessimist!)
5799        LayoutBlock newBlk = null; // assume failure (pessimist!)
5800
5801        if (inBlockName.isEmpty()) {
5802            // nothing entered, try autoAssign
5803            if (autoAssignBlocks) {
5804                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock();
5805                if (null == newBlk) {
5806                    log.error("provideLayoutBlock: Failure to auto-assign for empty LayoutBlock name");
5807                }
5808            } else {
5809                log.debug("provideLayoutBlock: no name given and not assigning auto block names");
5810            }
5811        } else {
5812            // check if this Layout Block already exists
5813            result = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(inBlockName);
5814            if (result == null) { //(no)
5815                // The combo box name can be either a block system name or a block user name
5816                Block checkBlock = InstanceManager.getDefault(BlockManager.class).getBlock(inBlockName);
5817                if (checkBlock == null) {
5818                    log.error("provideLayoutBlock: The block name '{}' does not return a block.", inBlockName);
5819                } else {
5820                    String checkUserName = checkBlock.getUserName();
5821                    if (checkUserName != null && checkUserName.equals(inBlockName)) {
5822                        // Go ahead and use the name for the layout block
5823                        newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, inBlockName);
5824                        if (newBlk == null) {
5825                            log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}'.", inBlockName);
5826                        }
5827                    } else {
5828                        // Appears to be a system name, request a user name
5829                        String blkUserName = (String)JmriJOptionPane.showInputDialog(getTargetFrame(),
5830                                Bundle.getMessage("BlkUserNameMsg"),
5831                                Bundle.getMessage("BlkUserNameTitle"),
5832                                JmriJOptionPane.PLAIN_MESSAGE, null, null, "");
5833                        if (blkUserName != null && !blkUserName.isEmpty()) {
5834                            // Verify the user name
5835                            Block checkDuplicate = InstanceManager.getDefault(BlockManager.class).getByUserName(blkUserName);
5836                            if (checkDuplicate != null) {
5837                                JmriJOptionPane.showMessageDialog(getTargetFrame(),
5838                                        Bundle.getMessage("BlkUserNameInUse", blkUserName),
5839                                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
5840                            } else {
5841                                // OK to use as a block user name
5842                                checkBlock.setUserName(blkUserName);
5843                                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, blkUserName);
5844                                if (newBlk == null) {
5845                                    log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}' with a new user name.", blkUserName);
5846                                }
5847                            }
5848                        }
5849                    }
5850                }
5851            }
5852        }
5853
5854        // if we created a new block
5855        if (newBlk != null) {
5856            // initialize the new block
5857            // log.debug("provideLayoutBlock :: Init new block {}", inBlockName);
5858            newBlk.initializeLayoutBlock();
5859            newBlk.initializeLayoutBlockRouting();
5860            newBlk.setBlockTrackColor(defaultTrackColor);
5861            newBlk.setBlockOccupiedColor(defaultOccupiedTrackColor);
5862            newBlk.setBlockExtraColor(defaultAlternativeTrackColor);
5863            result = newBlk;
5864        }
5865
5866        if (result != null) {
5867            // set both new and previously existing block
5868            result.addLayoutEditor(this);
5869            result.incrementUse();
5870            setDirty();
5871        }
5872        return result;
5873    }
5874
5875    /**
5876     * Validates that the supplied occupancy sensor name corresponds to an
5877     * existing sensor and is unique among all blocks. If valid, returns true
5878     * and sets the block sensor name in the block. Else returns false, and does
5879     * nothing to the block.
5880     *
5881     * @param sensorName the sensor name to validate
5882     * @param blk        the LayoutBlock in which to set it
5883     * @param openFrame  the frame (Component) it is in
5884     * @return true if sensor is valid
5885     */
5886    public boolean validateSensor(
5887            @Nonnull String sensorName,
5888            @Nonnull LayoutBlock blk,
5889            @Nonnull Component openFrame) {
5890        boolean result = false; // assume failure (pessimist!)
5891
5892        // check if anything entered
5893        if (!sensorName.isEmpty()) {
5894            // get a validated sensor corresponding to this name and assigned to block
5895            if (blk.getOccupancySensorName().equals(sensorName)) {
5896                result = true;
5897            } else {
5898                Sensor s = blk.validateSensor(sensorName, openFrame);
5899                result = (s != null); // if sensor returned result is true.
5900            }
5901        }
5902        return result;
5903    }
5904
5905    /**
5906     * Return a layout block with the given name if one exists. Registers this
5907     * LayoutEditor with the layout block. This method is designed to be used
5908     * when a panel is loaded. The calling method must handle whether the use
5909     * count should be incremented.
5910     *
5911     * @param blockID the given name
5912     * @return null if blockID does not already exist
5913     */
5914    public LayoutBlock getLayoutBlock(@Nonnull String blockID) {
5915        // check if this Layout Block already exists
5916        LayoutBlock blk = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(blockID);
5917        if (blk == null) {
5918            log.error("LayoutBlock '{}' not found when panel loaded", blockID);
5919            return null;
5920        }
5921        blk.addLayoutEditor(this);
5922        return blk;
5923    }
5924
5925    /**
5926     * Remove object from all Layout Editor temporary lists of items not part of
5927     * track schematic
5928     *
5929     * @param s the object to remove
5930     * @return true if found
5931     */
5932    private boolean remove(@Nonnull Object s) {
5933        boolean found = false;
5934
5935        if (backgroundImage.contains(s)) {
5936            backgroundImage.remove(s);
5937            found = true;
5938        }
5939        if (memoryLabelList.contains(s)) {
5940            memoryLabelList.remove(s);
5941            found = true;
5942        }
5943        if (globalVariableLabelList.contains(s)) {
5944            globalVariableLabelList.remove(s);
5945            found = true;
5946        }
5947        if (blockContentsLabelList.contains(s)) {
5948            blockContentsLabelList.remove(s);
5949            found = true;
5950        }
5951        if (multiSensors.contains(s)) {
5952            multiSensors.remove(s);
5953            found = true;
5954        }
5955        if (clocks.contains(s)) {
5956            clocks.remove(s);
5957            found = true;
5958        }
5959        if (labelImage.contains(s)) {
5960            labelImage.remove(s);
5961            found = true;
5962        }
5963
5964        if (sensorImage.contains(s) || sensorList.contains(s)) {
5965            Sensor sensor = ((SensorIcon) s).getSensor();
5966            if (sensor != null) {
5967                if (removeAttachedBean((sensor))) {
5968                    sensorImage.remove(s);
5969                    sensorList.remove(s);
5970                    found = true;
5971                } else {
5972                    return false;
5973                }
5974            }
5975        }
5976
5977        if (signalHeadImage.contains(s) || signalList.contains(s)) {
5978            SignalHead head = ((SignalHeadIcon) s).getSignalHead();
5979            if (head != null) {
5980                if (removeAttachedBean((head))) {
5981                    signalHeadImage.remove(s);
5982                    signalList.remove(s);
5983                    found = true;
5984                } else {
5985                    return false;
5986                }
5987            }
5988        }
5989
5990        if (signalMastList.contains(s)) {
5991            SignalMast mast = ((SignalMastIcon) s).getSignalMast();
5992            if (mast != null) {
5993                if (removeAttachedBean((mast))) {
5994                    signalMastList.remove(s);
5995                    found = true;
5996                } else {
5997                    return false;
5998                }
5999            }
6000        }
6001
6002        super.removeFromContents((Positionable) s);
6003
6004        if (found) {
6005            setDirty();
6006            redrawPanel();
6007        }
6008        return found;
6009    }
6010
6011    @Override
6012    public boolean removeFromContents(@Nonnull Positionable l) {
6013        return remove(l);
6014    }
6015
6016    private String findBeanUsage(@Nonnull NamedBean bean) {
6017        PositionablePoint pe;
6018        PositionablePoint pw;
6019        LayoutTurnout lt;
6020        LevelXing lx;
6021        LayoutSlip ls;
6022        boolean found = false;
6023        StringBuilder sb = new StringBuilder();
6024        String msgKey = "DeleteReference";  // NOI18N
6025        String beanKey = "None";  // NOI18N
6026        String beanValue = bean.getDisplayName();
6027
6028        if (bean instanceof SignalMast) {
6029            beanKey = "BeanNameSignalMast";  // NOI18N
6030
6031            if (InstanceManager.getDefault(SignalMastLogicManager.class).isSignalMastUsed((SignalMast) bean)) {
6032                SignalMastLogic sml = InstanceManager.getDefault(
6033                        SignalMastLogicManager.class).getSignalMastLogic((SignalMast) bean);
6034                if ((sml != null) && sml.useLayoutEditor(sml.getDestinationList().get(0))) {
6035                    msgKey = "DeleteSmlReference";  // NOI18N
6036                }
6037            }
6038        } else if (bean instanceof Sensor) {
6039            beanKey = "BeanNameSensor";  // NOI18N
6040        } else if (bean instanceof SignalHead) {
6041            beanKey = "BeanNameSignalHead";  // NOI18N
6042        }
6043        if (!beanKey.equals("None")) {  // NOI18N
6044            sb.append(Bundle.getMessage(msgKey, Bundle.getMessage(beanKey), beanValue));
6045        }
6046
6047        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
6048            TrackSegment t1 = pw.getConnect1();
6049            TrackSegment t2 = pw.getConnect2();
6050            if (t1 != null) {
6051                if (t2 != null) {
6052                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6053                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
6054                } else {
6055                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6056                }
6057            }
6058            found = true;
6059        }
6060
6061        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
6062            TrackSegment t1 = pe.getConnect1();
6063            TrackSegment t2 = pe.getConnect2();
6064
6065            if (t1 != null) {
6066                if (t2 != null) {
6067                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6068                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
6069                } else {
6070                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
6071                }
6072            }
6073            found = true;
6074        }
6075
6076        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
6077            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("BeanNameTurnout"), lt.getTurnoutName()));   // NOI18N
6078            found = true;
6079        }
6080
6081        if ((lx = finder.findLevelXingByBean(bean)) != null) {
6082            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("LevelCrossing"), lx.getId()));   // NOI18N
6083            found = true;
6084        }
6085
6086        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
6087            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("Slip"), ls.getTurnoutName()));   // NOI18N
6088            found = true;
6089        }
6090
6091        if (!found) {
6092            return null;
6093        }
6094        return sb.toString();
6095    }
6096
6097    /**
6098     * NX Sensors, Signal Heads and Signal Masts can be attached to positional
6099     * points, turnouts and level crossings. If an attachment exists, present an
6100     * option to cancel the remove action, remove the attachement or retain the
6101     * attachment.
6102     *
6103     * @param bean The named bean to be removed.
6104     * @return true if OK to remove the related icon.
6105     */
6106    private boolean removeAttachedBean(@Nonnull NamedBean bean) {
6107        String usage = findBeanUsage(bean);
6108
6109        if (usage != null) {
6110            usage = String.format("<html>%s</html>", usage);
6111            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6112                    usage, Bundle.getMessage("WarningTitle"),
6113                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6114                    new Object[]{Bundle.getMessage("ButtonYes"),
6115                        Bundle.getMessage("ButtonNo"),
6116                        Bundle.getMessage("ButtonCancel")},
6117                    Bundle.getMessage("ButtonYes"));
6118
6119            if (selectedValue == 1 ) { // array pos 1, No
6120                return true; // return leaving the references in place but allow the icon to be deleted.
6121            }
6122            // array pos 2, cancel or Dialog closed
6123            if (selectedValue == 2 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
6124                return false; // do not delete the item
6125            }
6126            if (bean instanceof Sensor) {
6127                // Additional actions for NX sensor pairs
6128                return getLETools().removeSensorAssignment((Sensor) bean);
6129            } else {
6130                removeBeanRefs(bean);
6131            }
6132        }
6133        return true;
6134    }
6135
6136    private void removeBeanRefs(@Nonnull NamedBean bean) {
6137        PositionablePoint pe;
6138        PositionablePoint pw;
6139        LayoutTurnout lt;
6140        LevelXing lx;
6141        LayoutSlip ls;
6142
6143        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
6144            pw.removeBeanReference(bean);
6145        }
6146
6147        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
6148            pe.removeBeanReference(bean);
6149        }
6150
6151        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
6152            lt.removeBeanReference(bean);
6153        }
6154
6155        if ((lx = finder.findLevelXingByBean(bean)) != null) {
6156            lx.removeBeanReference(bean);
6157        }
6158
6159        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
6160            ls.removeBeanReference(bean);
6161        }
6162    }
6163
6164    private boolean noWarnPositionablePoint = false;
6165
6166    /**
6167     * Remove a PositionablePoint -- an Anchor or an End Bumper.
6168     *
6169     * @param o the PositionablePoint to remove
6170     * @return true if removed
6171     */
6172    public boolean removePositionablePoint(@Nonnull PositionablePoint o) {
6173        // First verify with the user that this is really wanted, only show message if there is a bit of track connected
6174        if ((o.getConnect1() != null) || (o.getConnect2() != null)) {
6175            if (!noWarnPositionablePoint) {
6176                int selectedValue = JmriJOptionPane.showOptionDialog(this,
6177                        Bundle.getMessage("Question2"), Bundle.getMessage("WarningTitle"),
6178                        JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6179                        new Object[]{Bundle.getMessage("ButtonYes"),
6180                            Bundle.getMessage("ButtonNo"),
6181                            Bundle.getMessage("ButtonYesPlus")},
6182                        Bundle.getMessage("ButtonNo"));
6183
6184                // array position 1, ButtonNo , or Dialog Closed.
6185                if (selectedValue == 1 || selectedValue == JmriJOptionPane.CLOSED_OPTION ) {
6186                    return false; // return without creating if "No" response
6187                }
6188
6189                if (selectedValue == 2) { // array position 2, ButtonYesPlus
6190                    // Suppress future warnings, and continue
6191                    noWarnPositionablePoint = true;
6192                }
6193            }
6194
6195            // remove from selection information
6196            if (selectedObject == o) {
6197                selectedObject = null;
6198            }
6199
6200            if (prevSelectedObject == o) {
6201                prevSelectedObject = null;
6202            }
6203
6204            // remove connections if any
6205            TrackSegment t1 = o.getConnect1();
6206            TrackSegment t2 = o.getConnect2();
6207
6208            if (t1 != null) {
6209                removeTrackSegment(t1);
6210            }
6211
6212            if (t2 != null) {
6213                removeTrackSegment(t2);
6214            }
6215
6216            // delete from array
6217        }
6218
6219        return removeLayoutTrackAndRedraw(o);
6220    }
6221
6222    private boolean noWarnLayoutTurnout = false;
6223
6224    /**
6225     * Remove a LayoutTurnout
6226     *
6227     * @param o the LayoutTurnout to remove
6228     * @return true if removed
6229     */
6230    public boolean removeLayoutTurnout(@Nonnull LayoutTurnout o) {
6231        // First verify with the user that this is really wanted
6232        if (!noWarnLayoutTurnout) {
6233            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6234                    Bundle.getMessage("Question1r"), Bundle.getMessage("WarningTitle"),
6235                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6236                    new Object[]{Bundle.getMessage("ButtonYes"),
6237                        Bundle.getMessage("ButtonNo"),
6238                        Bundle.getMessage("ButtonYesPlus")},
6239                    Bundle.getMessage("ButtonNo"));
6240
6241            // return without removing if array position 1 "No" response or Dialog closed
6242            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6243                return false;
6244            }
6245
6246            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6247                // Suppress future warnings, and continue
6248                noWarnLayoutTurnout = true;
6249            }
6250        }
6251
6252        // remove from selection information
6253        if (selectedObject == o) {
6254            selectedObject = null;
6255        }
6256
6257        if (prevSelectedObject == o) {
6258            prevSelectedObject = null;
6259        }
6260
6261        // remove connections if any
6262        TrackSegment t = (TrackSegment) o.getConnectA();
6263
6264        if (t != null) {
6265            substituteAnchor(getLayoutTurnoutView(o).getCoordsA(), o, t);
6266        }
6267        t = (TrackSegment) o.getConnectB();
6268
6269        if (t != null) {
6270            substituteAnchor(getLayoutTurnoutView(o).getCoordsB(), o, t);
6271        }
6272        t = (TrackSegment) o.getConnectC();
6273
6274        if (t != null) {
6275            substituteAnchor(getLayoutTurnoutView(o).getCoordsC(), o, t);
6276        }
6277        t = (TrackSegment) o.getConnectD();
6278
6279        if (t != null) {
6280            substituteAnchor(getLayoutTurnoutView(o).getCoordsD(), o, t);
6281        }
6282
6283        // decrement Block use count(s)
6284        LayoutBlock b = o.getLayoutBlock();
6285
6286        if (b != null) {
6287            b.decrementUse();
6288        }
6289
6290        if (o.isTurnoutTypeXover() || o.isTurnoutTypeSlip()) {
6291            LayoutBlock b2 = o.getLayoutBlockB();
6292
6293            if ((b2 != null) && (b2 != b)) {
6294                b2.decrementUse();
6295            }
6296            LayoutBlock b3 = o.getLayoutBlockC();
6297
6298            if ((b3 != null) && (b3 != b) && (b3 != b2)) {
6299                b3.decrementUse();
6300            }
6301            LayoutBlock b4 = o.getLayoutBlockD();
6302
6303            if ((b4 != null) && (b4 != b)
6304                    && (b4 != b2) && (b4 != b3)) {
6305                b4.decrementUse();
6306            }
6307        }
6308
6309        return removeLayoutTrackAndRedraw(o);
6310    }
6311
6312    private void substituteAnchor(@Nonnull Point2D loc,
6313            @Nonnull LayoutTrack o, @Nonnull TrackSegment t) {
6314        PositionablePoint p = addAnchor(loc);
6315
6316        if (t.getConnect1() == o) {
6317            t.setNewConnect1(p, HitPointType.POS_POINT);
6318        }
6319
6320        if (t.getConnect2() == o) {
6321            t.setNewConnect2(p, HitPointType.POS_POINT);
6322        }
6323        p.setTrackConnection(t);
6324    }
6325
6326    private boolean noWarnLevelXing = false;
6327
6328    /**
6329     * Remove a Level Crossing
6330     *
6331     * @param o the LevelXing to remove
6332     * @return true if removed
6333     */
6334    public boolean removeLevelXing(@Nonnull LevelXing o) {
6335        // First verify with the user that this is really wanted
6336        if (!noWarnLevelXing) {
6337            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6338                    Bundle.getMessage("Question3r"), Bundle.getMessage("WarningTitle"),
6339                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6340                    new Object[]{Bundle.getMessage("ButtonYes"),
6341                        Bundle.getMessage("ButtonNo"),
6342                        Bundle.getMessage("ButtonYesPlus")},
6343                    Bundle.getMessage("ButtonNo"));
6344
6345             // array position 1 Button No, or Dialog closed.
6346            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6347                return false;
6348            }
6349
6350            if (selectedValue == 2 ) { // array position 2 ButtonYesPlus
6351                // Suppress future warnings, and continue
6352                noWarnLevelXing = true;
6353            }
6354        }
6355
6356        // remove from selection information
6357        if (selectedObject == o) {
6358            selectedObject = null;
6359        }
6360
6361        if (prevSelectedObject == o) {
6362            prevSelectedObject = null;
6363        }
6364
6365        // remove connections if any
6366        LevelXingView ov = getLevelXingView(o);
6367
6368        TrackSegment t = (TrackSegment) o.getConnectA();
6369        if (t != null) {
6370            substituteAnchor(ov.getCoordsA(), o, t);
6371        }
6372        t = (TrackSegment) o.getConnectB();
6373
6374        if (t != null) {
6375            substituteAnchor(ov.getCoordsB(), o, t);
6376        }
6377        t = (TrackSegment) o.getConnectC();
6378
6379        if (t != null) {
6380            substituteAnchor(ov.getCoordsC(), o, t);
6381        }
6382        t = (TrackSegment) o.getConnectD();
6383
6384        if (t != null) {
6385            substituteAnchor(ov.getCoordsD(), o, t);
6386        }
6387
6388        // decrement block use count if any blocks in use
6389        LayoutBlock lb = o.getLayoutBlockAC();
6390
6391        if (lb != null) {
6392            lb.decrementUse();
6393        }
6394        LayoutBlock lbx = o.getLayoutBlockBD();
6395
6396        if ((lbx != null) && (lb != null) && (lbx != lb)) {
6397            lb.decrementUse();
6398        }
6399
6400        return removeLayoutTrackAndRedraw(o);
6401    }
6402
6403    private boolean noWarnSlip = false;
6404
6405    /**
6406     * Remove a slip
6407     *
6408     * @param o the LayoutSlip to remove
6409     * @return true if removed
6410     */
6411    public boolean removeLayoutSlip(@Nonnull LayoutTurnout o) {
6412        if (!(o instanceof LayoutSlip)) {
6413            return false;
6414        }
6415
6416        // First verify with the user that this is really wanted
6417        if (!noWarnSlip) {
6418            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6419                    Bundle.getMessage("Question5r"), Bundle.getMessage("WarningTitle"),
6420                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6421                    new Object[]{Bundle.getMessage("ButtonYes"),
6422                        Bundle.getMessage("ButtonNo"),
6423                        Bundle.getMessage("ButtonYesPlus")},
6424                    Bundle.getMessage("ButtonNo"));
6425
6426             // return without removing if array position 1 "No" response or Dialog closed
6427            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6428                return false;
6429            }
6430
6431            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6432                // Suppress future warnings, and continue
6433                noWarnSlip = true;
6434            }
6435        }
6436
6437        LayoutTurnoutView ov = getLayoutTurnoutView(o);
6438
6439        // remove from selection information
6440        if (selectedObject == o) {
6441            selectedObject = null;
6442        }
6443
6444        if (prevSelectedObject == o) {
6445            prevSelectedObject = null;
6446        }
6447
6448        // remove connections if any
6449        TrackSegment t = (TrackSegment) o.getConnectA();
6450
6451        if (t != null) {
6452            substituteAnchor(ov.getCoordsA(), o, t);
6453        }
6454        t = (TrackSegment) o.getConnectB();
6455
6456        if (t != null) {
6457            substituteAnchor(ov.getCoordsB(), o, t);
6458        }
6459        t = (TrackSegment) o.getConnectC();
6460
6461        if (t != null) {
6462            substituteAnchor(ov.getCoordsC(), o, t);
6463        }
6464        t = (TrackSegment) o.getConnectD();
6465
6466        if (t != null) {
6467            substituteAnchor(ov.getCoordsD(), o, t);
6468        }
6469
6470        // decrement block use count if any blocks in use
6471        LayoutBlock lb = o.getLayoutBlock();
6472
6473        if (lb != null) {
6474            lb.decrementUse();
6475        }
6476
6477        return removeLayoutTrackAndRedraw(o);
6478    }
6479
6480    private boolean noWarnTurntable = false;
6481
6482    /**
6483     * Remove a Layout Turntable
6484     *
6485     * @param o the LayoutTurntable to remove
6486     * @return true if removed
6487     */
6488    public boolean removeTurntable(@Nonnull LayoutTurntable o) {
6489        // First verify with the user that this is really wanted
6490        if (!noWarnTurntable) {
6491            int selectedValue = JmriJOptionPane.showOptionDialog(this,
6492                    Bundle.getMessage("Question4r"), Bundle.getMessage("WarningTitle"),
6493                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
6494                    new Object[]{Bundle.getMessage("ButtonYes"),
6495                        Bundle.getMessage("ButtonNo"),
6496                        Bundle.getMessage("ButtonYesPlus")},
6497                    Bundle.getMessage("ButtonNo"));
6498
6499            // return without removing if array position 1 "No" response or Dialog closed
6500            if (selectedValue == 1 || selectedValue==JmriJOptionPane.CLOSED_OPTION ) {
6501                return false;
6502            }
6503
6504            if (selectedValue == 2 ) { // ButtonYesPlus in array position 2
6505                // Suppress future warnings, and continue
6506                noWarnTurntable = true;
6507            }
6508        }
6509
6510        // remove from selection information
6511        if (selectedObject == o) {
6512            selectedObject = null;
6513        }
6514
6515        if (prevSelectedObject == o) {
6516            prevSelectedObject = null;
6517        }
6518
6519        // remove connections if any
6520        LayoutTurntableView ov = getLayoutTurntableView(o);
6521        for (int j = 0; j < o.getNumberRays(); j++) {
6522            TrackSegment t = ov.getRayConnectOrdered(j);
6523
6524            if (t != null) {
6525                substituteAnchor(ov.getRayCoordsIndexed(j), o, t);
6526            }
6527        }
6528
6529        return removeLayoutTrackAndRedraw(o);
6530    }
6531
6532    /**
6533     * Remove a Track Segment
6534     *
6535     * @param o the TrackSegment to remove
6536     */
6537    public void removeTrackSegment(@Nonnull TrackSegment o) {
6538        // save affected blocks
6539        LayoutBlock block1 = null;
6540        LayoutBlock block2 = null;
6541        LayoutBlock block = o.getLayoutBlock();
6542
6543        // remove any connections
6544        HitPointType type = o.getType1();
6545
6546        if (type == HitPointType.POS_POINT) {
6547            PositionablePoint p = (PositionablePoint) (o.getConnect1());
6548
6549            if (p != null) {
6550                p.removeTrackConnection(o);
6551
6552                if (p.getConnect1() != null) {
6553                    block1 = p.getConnect1().getLayoutBlock();
6554                } else if (p.getConnect2() != null) {
6555                    block1 = p.getConnect2().getLayoutBlock();
6556                }
6557            }
6558        } else {
6559            block1 = getAffectedBlock(o.getConnect1(), type);
6560            disconnect(o.getConnect1(), type);
6561        }
6562        type = o.getType2();
6563
6564        if (type == HitPointType.POS_POINT) {
6565            PositionablePoint p = (PositionablePoint) (o.getConnect2());
6566
6567            if (p != null) {
6568                p.removeTrackConnection(o);
6569
6570                if (p.getConnect1() != null) {
6571                    block2 = p.getConnect1().getLayoutBlock();
6572                } else if (p.getConnect2() != null) {
6573                    block2 = p.getConnect2().getLayoutBlock();
6574                }
6575            }
6576        } else {
6577            block2 = getAffectedBlock(o.getConnect2(), type);
6578            disconnect(o.getConnect2(), type);
6579        }
6580
6581        // delete from array
6582        removeLayoutTrack(o);
6583
6584        // update affected blocks
6585        if (block != null) {
6586            // decrement Block use count
6587            block.decrementUse();
6588            getLEAuxTools().setBlockConnectivityChanged();
6589            block.updatePaths();
6590        }
6591
6592        if ((block1 != null) && (block1 != block)) {
6593            block1.updatePaths();
6594        }
6595
6596        if ((block2 != null) && (block2 != block) && (block2 != block1)) {
6597            block2.updatePaths();
6598        }
6599
6600        //
6601        setDirty();
6602        redrawPanel();
6603    }
6604
6605    private void disconnect(@Nonnull LayoutTrack o, HitPointType type) {
6606        switch (type) {
6607            case TURNOUT_A:
6608            case TURNOUT_B:
6609            case TURNOUT_C:
6610            case TURNOUT_D:
6611            case SLIP_A:
6612            case SLIP_B:
6613            case SLIP_C:
6614            case SLIP_D:
6615            case LEVEL_XING_A:
6616            case LEVEL_XING_B:
6617            case LEVEL_XING_C:
6618            case LEVEL_XING_D: {
6619                try {
6620                    o.setConnection(type, null, HitPointType.NONE);
6621                } catch (JmriException e) {
6622                    // ignore (log.error in setConnection method)
6623                }
6624                break;
6625            }
6626
6627            default: {
6628                if (HitPointType.isTurntableRayHitType(type)) {
6629                    ((LayoutTurntable) o).setRayConnect(null, type.turntableTrackIndex());
6630                }
6631                break;
6632            }
6633        }
6634    }
6635
6636    /**
6637     * Depending on the given type, and the real class of the given LayoutTrack,
6638     * determine the connected LayoutTrack. This provides a variable-indirect
6639     * form of e.g. trk.getLayoutBlockC() for example. Perhaps "Connected Block"
6640     * captures the idea better, but that method name is being used for
6641     * something else.
6642     *
6643     *
6644     * @param track The track who's connected blocks are being examined
6645     * @param type  This point to check for connected blocks, i.e. TURNOUT_B
6646     * @return The block at a particular point on the track object, or null if
6647     *         none.
6648     */
6649    // Temporary - this should certainly be a LayoutTrack method.
6650    public LayoutBlock getAffectedBlock(@Nonnull LayoutTrack track, HitPointType type) {
6651        LayoutBlock result = null;
6652
6653        switch (type) {
6654            case TURNOUT_A:
6655            case SLIP_A: {
6656                if (track instanceof LayoutTurnout) {
6657                    LayoutTurnout lt = (LayoutTurnout) track;
6658                    result = lt.getLayoutBlock();
6659                }
6660                break;
6661            }
6662
6663            case TURNOUT_B:
6664            case SLIP_B: {
6665                if (track instanceof LayoutTurnout) {
6666                    LayoutTurnout lt = (LayoutTurnout) track;
6667                    result = lt.getLayoutBlockB();
6668                }
6669                break;
6670            }
6671
6672            case TURNOUT_C:
6673            case SLIP_C: {
6674                if (track instanceof LayoutTurnout) {
6675                    LayoutTurnout lt = (LayoutTurnout) track;
6676                    result = lt.getLayoutBlockC();
6677                }
6678                break;
6679            }
6680
6681            case TURNOUT_D:
6682            case SLIP_D: {
6683                if (track instanceof LayoutTurnout) {
6684                    LayoutTurnout lt = (LayoutTurnout) track;
6685                    result = lt.getLayoutBlockD();
6686                }
6687                break;
6688            }
6689
6690            case LEVEL_XING_A:
6691            case LEVEL_XING_C: {
6692                if (track instanceof LevelXing) {
6693                    LevelXing lx = (LevelXing) track;
6694                    result = lx.getLayoutBlockAC();
6695                }
6696                break;
6697            }
6698
6699            case LEVEL_XING_B:
6700            case LEVEL_XING_D: {
6701                if (track instanceof LevelXing) {
6702                    LevelXing lx = (LevelXing) track;
6703                    result = lx.getLayoutBlockBD();
6704                }
6705                break;
6706            }
6707
6708            case TRACK: {
6709                if (track instanceof TrackSegment) {
6710                    TrackSegment ts = (TrackSegment) track;
6711                    result = ts.getLayoutBlock();
6712                }
6713                break;
6714            }
6715            default: {
6716                log.warn("Unhandled track type: {}", type);
6717                break;
6718            }
6719        }
6720        return result;
6721    }
6722
6723    /**
6724     * Add a sensor indicator to the Draw Panel
6725     */
6726    void addSensor() {
6727        String newName = leToolBarPanel.sensorComboBox.getSelectedItemDisplayName();
6728        if (newName == null) {
6729            newName = "";
6730        }
6731
6732        if (newName.isEmpty()) {
6733            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error10"),
6734                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6735            return;
6736        }
6737        SensorIcon l = new SensorIcon(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
6738                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), this);
6739
6740        l.setIcon("SensorStateActive", leToolBarPanel.sensorIconEditor.getIcon(0));
6741        l.setIcon("SensorStateInactive", leToolBarPanel.sensorIconEditor.getIcon(1));
6742        l.setIcon("BeanStateInconsistent", leToolBarPanel.sensorIconEditor.getIcon(2));
6743        l.setIcon("BeanStateUnknown", leToolBarPanel.sensorIconEditor.getIcon(3));
6744        l.setSensor(newName);
6745        l.setDisplayLevel(Editor.SENSORS);
6746
6747        leToolBarPanel.sensorComboBox.setSelectedItem(l.getSensor());
6748        setNextLocation(l);
6749        try {
6750            putItem(l); // note: this calls unionToPanelBounds & setDirty()
6751        } catch (Positionable.DuplicateIdException e) {
6752            // This should never happen
6753            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
6754        }
6755    }
6756
6757    public void putSensor(@Nonnull SensorIcon l) {
6758        l.updateSize();
6759        l.setDisplayLevel(Editor.SENSORS);
6760        try {
6761            putItem(l); // note: this calls unionToPanelBounds & setDirty()
6762        } catch (Positionable.DuplicateIdException e) {
6763            // This should never happen
6764            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
6765        }
6766    }
6767
6768    /**
6769     * Add a signal head to the Panel
6770     */
6771    void addSignalHead() {
6772        // check for valid signal head entry
6773        String newName = leToolBarPanel.signalHeadComboBox.getSelectedItemDisplayName();
6774        if (newName == null) {
6775            newName = "";
6776        }
6777        SignalHead mHead = null;
6778
6779        if (!newName.isEmpty()) {
6780            mHead = InstanceManager.getDefault(SignalHeadManager.class).getSignalHead(newName);
6781
6782            /*if (mHead == null)
6783            mHead = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(newName);
6784            else */
6785            leToolBarPanel.signalHeadComboBox.setSelectedItem(mHead);
6786        }
6787
6788        if (mHead == null) {
6789            // There is no signal head corresponding to this name
6790            JmriJOptionPane.showMessageDialog(this,
6791                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
6792                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6793            return;
6794        }
6795
6796        // create and set up signal icon
6797        SignalHeadIcon l = new SignalHeadIcon(this);
6798        l.setSignalHead(newName);
6799        l.setIcon("SignalHeadStateRed", leToolBarPanel.signalIconEditor.getIcon(0));
6800        l.setIcon("SignalHeadStateFlashingRed", leToolBarPanel.signalIconEditor.getIcon(1));
6801        l.setIcon("SignalHeadStateYellow", leToolBarPanel.signalIconEditor.getIcon(2));
6802        l.setIcon("SignalHeadStateFlashingYellow", leToolBarPanel.signalIconEditor.getIcon(3));
6803        l.setIcon("SignalHeadStateGreen", leToolBarPanel.signalIconEditor.getIcon(4));
6804        l.setIcon("SignalHeadStateFlashingGreen", leToolBarPanel.signalIconEditor.getIcon(5));
6805        l.setIcon("SignalHeadStateDark", leToolBarPanel.signalIconEditor.getIcon(6));
6806        l.setIcon("SignalHeadStateHeld", leToolBarPanel.signalIconEditor.getIcon(7));
6807        l.setIcon("SignalHeadStateLunar", leToolBarPanel.signalIconEditor.getIcon(8));
6808        l.setIcon("SignalHeadStateFlashingLunar", leToolBarPanel.signalIconEditor.getIcon(9));
6809        unionToPanelBounds(l.getBounds());
6810        setNextLocation(l);
6811        setDirty();
6812        putSignal(l);
6813    }
6814
6815    public void putSignal(@Nonnull SignalHeadIcon l) {
6816        l.updateSize();
6817        l.setDisplayLevel(Editor.SIGNALS);
6818        try {
6819            putItem(l); // note: this calls unionToPanelBounds & setDirty()
6820        } catch (Positionable.DuplicateIdException e) {
6821            // This should never happen
6822            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
6823        }
6824    }
6825
6826    @CheckForNull
6827    SignalHead getSignalHead(@Nonnull String name) {
6828        SignalHead sh = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
6829
6830        if (sh == null) {
6831            sh = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(name);
6832        }
6833
6834        if (sh == null) {
6835            log.warn("did not find a SignalHead named {}", name);
6836        }
6837        return sh;
6838    }
6839
6840    public boolean containsSignalHead(@CheckForNull SignalHead head) {
6841        if (head != null) {
6842            for (SignalHeadIcon h : signalList) {
6843                if (h.getSignalHead() == head) {
6844                    return true;
6845                }
6846            }
6847        }
6848        return false;
6849    }
6850
6851    public void removeSignalHead(@CheckForNull SignalHead head) {
6852        if (head != null) {
6853            for (SignalHeadIcon h : signalList) {
6854                if (h.getSignalHead() == head) {
6855                    signalList.remove(h);
6856                    h.remove();
6857                    h.dispose();
6858                    setDirty();
6859                    redrawPanel();
6860                    break;
6861                }
6862            }
6863        }
6864    }
6865
6866    void addSignalMast() {
6867        // check for valid signal head entry
6868        String newName = leToolBarPanel.signalMastComboBox.getSelectedItemDisplayName();
6869        if (newName == null) {
6870            newName = "";
6871        }
6872        SignalMast mMast = null;
6873
6874        if (!newName.isEmpty()) {
6875            mMast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(newName);
6876            leToolBarPanel.signalMastComboBox.setSelectedItem(mMast);
6877        }
6878
6879        if (mMast == null) {
6880            // There is no signal head corresponding to this name
6881            JmriJOptionPane.showMessageDialog(this,
6882                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
6883                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6884
6885            return;
6886        }
6887
6888        // create and set up signal icon
6889        SignalMastIcon l = new SignalMastIcon(this);
6890        l.setSignalMast(newName);
6891        unionToPanelBounds(l.getBounds());
6892        setNextLocation(l);
6893        setDirty();
6894        putSignalMast(l);
6895    }
6896
6897    public void putSignalMast(@Nonnull SignalMastIcon l) {
6898        l.updateSize();
6899        l.setDisplayLevel(Editor.SIGNALS);
6900        try {
6901            putItem(l); // note: this calls unionToPanelBounds & setDirty()
6902        } catch (Positionable.DuplicateIdException e) {
6903            // This should never happen
6904            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
6905        }
6906    }
6907
6908    SignalMast getSignalMast(@Nonnull String name) {
6909        SignalMast sh = InstanceManager.getDefault(SignalMastManager.class).getBySystemName(name);
6910
6911        if (sh == null) {
6912            sh = InstanceManager.getDefault(SignalMastManager.class).getByUserName(name);
6913        }
6914
6915        if (sh == null) {
6916            log.warn("did not find a SignalMast named {}", name);
6917        }
6918        return sh;
6919    }
6920
6921    public boolean containsSignalMast(@Nonnull SignalMast mast) {
6922        for (SignalMastIcon h : signalMastList) {
6923            if (h.getSignalMast() == mast) {
6924                return true;
6925            }
6926        }
6927        return false;
6928    }
6929
6930    /**
6931     * Add a label to the Draw Panel
6932     */
6933    void addLabel() {
6934        String labelText = leToolBarPanel.textLabelTextField.getText();
6935        labelText = (labelText != null) ? labelText.trim() : "";
6936
6937        if (labelText.isEmpty()) {
6938            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11"),
6939                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6940            return;
6941        }
6942        PositionableLabel l = super.addLabel(labelText);
6943        unionToPanelBounds(l.getBounds());
6944        setDirty();
6945        l.setForeground(defaultTextColor);
6946    }
6947
6948    @Override
6949    public void putItem(@Nonnull Positionable l) throws Positionable.DuplicateIdException {
6950        super.putItem(l);
6951
6952        if (l instanceof SensorIcon) {
6953            sensorImage.add((SensorIcon) l);
6954            sensorList.add((SensorIcon) l);
6955        } else if (l instanceof LocoIcon) {
6956            markerImage.add((LocoIcon) l);
6957        } else if (l instanceof SignalHeadIcon) {
6958            signalHeadImage.add((SignalHeadIcon) l);
6959            signalList.add((SignalHeadIcon) l);
6960        } else if (l instanceof SignalMastIcon) {
6961            signalMastList.add((SignalMastIcon) l);
6962        } else if (l instanceof MemoryIcon) {
6963            memoryLabelList.add((MemoryIcon) l);
6964        } else if (l instanceof GlobalVariableIcon) {
6965            globalVariableLabelList.add((GlobalVariableIcon) l);
6966        } else if (l instanceof BlockContentsIcon) {
6967            blockContentsLabelList.add((BlockContentsIcon) l);
6968        } else if (l instanceof AnalogClock2Display) {
6969            clocks.add((AnalogClock2Display) l);
6970        } else if (l instanceof MultiSensorIcon) {
6971            multiSensors.add((MultiSensorIcon) l);
6972        }
6973
6974        if (l instanceof PositionableLabel) {
6975            if (((PositionableLabel) l).isBackground()) {
6976                backgroundImage.add((PositionableLabel) l);
6977            } else {
6978                labelImage.add((PositionableLabel) l);
6979            }
6980        }
6981        unionToPanelBounds(l.getBounds(new Rectangle()));
6982        setDirty();
6983    }
6984
6985    /**
6986     * Add a memory label to the Draw Panel
6987     */
6988    void addMemory() {
6989        String memoryName = leToolBarPanel.textMemoryComboBox.getSelectedItemDisplayName();
6990        if (memoryName == null) {
6991            memoryName = "";
6992        }
6993
6994        if (memoryName.isEmpty()) {
6995            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11a"),
6996                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
6997            return;
6998        }
6999        MemoryIcon l = new MemoryIcon(" ", this);
7000        l.setMemory(memoryName);
7001        Memory xMemory = l.getMemory();
7002
7003        if (xMemory != null) {
7004            String uname = xMemory.getDisplayName();
7005            if (!uname.equals(memoryName)) {
7006                // put the system name in the memory field
7007                leToolBarPanel.textMemoryComboBox.setSelectedItem(xMemory);
7008            }
7009        }
7010        setNextLocation(l);
7011        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7012        l.setDisplayLevel(Editor.LABELS);
7013        l.setForeground(defaultTextColor);
7014        unionToPanelBounds(l.getBounds());
7015        try {
7016            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7017        } catch (Positionable.DuplicateIdException e) {
7018            // This should never happen
7019            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7020        }
7021    }
7022
7023    void addGlobalVariable() {
7024        String globalVariableName = leToolBarPanel.textGlobalVariableComboBox.getSelectedItemDisplayName();
7025        if (globalVariableName == null) {
7026            globalVariableName = "";
7027        }
7028
7029        if (globalVariableName.isEmpty()) {
7030            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11c"),
7031                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7032            return;
7033        }
7034        GlobalVariableIcon l = new GlobalVariableIcon(" ", this);
7035        l.setGlobalVariable(globalVariableName);
7036        GlobalVariable xGlobalVariable = l.getGlobalVariable();
7037
7038        if (xGlobalVariable != null) {
7039            String uname = xGlobalVariable.getDisplayName();
7040            if (!uname.equals(globalVariableName)) {
7041                // put the system name in the memory field
7042                leToolBarPanel.textGlobalVariableComboBox.setSelectedItem(xGlobalVariable);
7043            }
7044        }
7045        setNextLocation(l);
7046        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7047        l.setDisplayLevel(Editor.LABELS);
7048        l.setForeground(defaultTextColor);
7049        unionToPanelBounds(l.getBounds());
7050        try {
7051            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7052        } catch (Positionable.DuplicateIdException e) {
7053            // This should never happen
7054            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7055        }
7056    }
7057
7058    void addBlockContents() {
7059        String newName = leToolBarPanel.blockContentsComboBox.getSelectedItemDisplayName();
7060        if (newName == null) {
7061            newName = "";
7062        }
7063
7064        if (newName.isEmpty()) {
7065            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11b"),
7066                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7067            return;
7068        }
7069        BlockContentsIcon l = new BlockContentsIcon(" ", this);
7070        l.setBlock(newName);
7071        Block xMemory = l.getBlock();
7072
7073        if (xMemory != null) {
7074            String uname = xMemory.getDisplayName();
7075            if (!uname.equals(newName)) {
7076                // put the system name in the memory field
7077                leToolBarPanel.blockContentsComboBox.setSelectedItem(xMemory);
7078            }
7079        }
7080        setNextLocation(l);
7081        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7082        l.setDisplayLevel(Editor.LABELS);
7083        l.setForeground(defaultTextColor);
7084        try {
7085            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7086        } catch (Positionable.DuplicateIdException e) {
7087            // This should never happen
7088            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7089        }
7090    }
7091
7092    /**
7093     * Add a Reporter Icon to the panel.
7094     *
7095     * @param reporter the reporter icon to add.
7096     * @param xx       the horizontal location.
7097     * @param yy       the vertical location.
7098     */
7099    public void addReporter(@Nonnull Reporter reporter, int xx, int yy) {
7100        ReporterIcon l = new ReporterIcon(this);
7101        l.setReporter(reporter);
7102        l.setLocation(xx, yy);
7103        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
7104        l.setDisplayLevel(Editor.LABELS);
7105        unionToPanelBounds(l.getBounds());
7106        try {
7107            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7108        } catch (Positionable.DuplicateIdException e) {
7109            // This should never happen
7110            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7111        }
7112    }
7113
7114    /**
7115     * Add an icon to the target
7116     */
7117    void addIcon() {
7118        PositionableLabel l = new PositionableLabel(leToolBarPanel.iconEditor.getIcon(0), this);
7119        setNextLocation(l);
7120        l.setDisplayLevel(Editor.ICONS);
7121        unionToPanelBounds(l.getBounds());
7122        l.updateSize();
7123        try {
7124            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7125        } catch (Positionable.DuplicateIdException e) {
7126            // This should never happen
7127            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7128        }
7129    }
7130
7131    /**
7132     * Add a LogixNG icon to the target
7133     */
7134    void addLogixNGIcon() {
7135        LogixNGIcon l = new LogixNGIcon(leToolBarPanel.logixngEditor.getIcon(0), this);
7136        setNextLocation(l);
7137        l.setDisplayLevel(Editor.ICONS);
7138        unionToPanelBounds(l.getBounds());
7139        l.updateSize();
7140        try {
7141            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7142        } catch (Positionable.DuplicateIdException e) {
7143            // This should never happen
7144            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7145        }
7146    }
7147
7148    /**
7149     * Add a LogixNG icon to the target
7150     */
7151    void addAudioIcon() {
7152        String audioName = leToolBarPanel.textAudioComboBox.getSelectedItemDisplayName();
7153        if (audioName == null) {
7154            audioName = "";
7155        }
7156
7157        if (audioName.isEmpty()) {
7158            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("Error11d"),
7159                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
7160            return;
7161        }
7162
7163        AudioIcon l = new AudioIcon(leToolBarPanel.audioEditor.getIcon(0), this);
7164        l.setAudio(audioName);
7165        Audio xAudio = l.getAudio();
7166
7167        if (xAudio != null) {
7168            String uname = xAudio.getDisplayName();
7169            if (!uname.equals(audioName)) {
7170                // put the system name in the memory field
7171                leToolBarPanel.textAudioComboBox.setSelectedItem(xAudio);
7172            }
7173        }
7174
7175        setNextLocation(l);
7176        l.setDisplayLevel(Editor.ICONS);
7177        unionToPanelBounds(l.getBounds());
7178        l.updateSize();
7179        try {
7180            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7181        } catch (Positionable.DuplicateIdException e) {
7182            // This should never happen
7183            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7184        }
7185    }
7186
7187    /**
7188     * Add a loco marker to the target
7189     */
7190    @Override
7191    public LocoIcon addLocoIcon(@Nonnull String name) {
7192        LocoIcon l = new LocoIcon(this);
7193        Point2D pt = windowCenter();
7194        if (selectionActive) {
7195            pt = MathUtil.midPoint(getSelectionRect());
7196        }
7197        l.setLocation((int) pt.getX(), (int) pt.getY());
7198        putLocoIcon(l, name);
7199        l.setPositionable(true);
7200        unionToPanelBounds(l.getBounds());
7201        return l;
7202    }
7203
7204    @Override
7205    public void putLocoIcon(@Nonnull LocoIcon l, @Nonnull String name) {
7206        super.putLocoIcon(l, name);
7207        markerImage.add(l);
7208        unionToPanelBounds(l.getBounds());
7209    }
7210
7211    private JFileChooser inputFileChooser = null;
7212
7213    /**
7214     * Add a background image
7215     */
7216    public void addBackground() {
7217        if (inputFileChooser == null) {
7218            inputFileChooser = new jmri.util.swing.JmriJFileChooser(
7219                    String.format("%s%sresources%sicons",
7220                            System.getProperty("user.dir"),
7221                            File.separator,
7222                            File.separator));
7223
7224            inputFileChooser.setFileFilter(new FileNameExtensionFilter("Graphics Files", "gif", "jpg", "png"));
7225        }
7226        inputFileChooser.rescanCurrentDirectory();
7227
7228        int retVal = inputFileChooser.showOpenDialog(this);
7229
7230        if (retVal != JFileChooser.APPROVE_OPTION) {
7231            return; // give up if no file selected
7232        }
7233
7234        // NamedIcon icon = new NamedIcon(inputFileChooser.getSelectedFile().getPath(),
7235        // inputFileChooser.getSelectedFile().getPath());
7236        String name = inputFileChooser.getSelectedFile().getPath();
7237
7238        // convert to portable path
7239        name = FileUtil.getPortableFilename(name);
7240
7241        // setup icon
7242        PositionableLabel o = super.setUpBackground(name);
7243        backgroundImage.add(o);
7244        unionToPanelBounds(o.getBounds());
7245        setDirty();
7246    }
7247
7248    // there is no way to call this; could that
7249    //    private boolean remove(@Nonnull Object s)
7250    // is being used instead.
7251    //
7252    ///**
7253    // * Remove a background image from the list of background images
7254    // *
7255    // * @param b PositionableLabel to remove
7256    // */
7257    //private void removeBackground(@Nonnull PositionableLabel b) {
7258    //    if (backgroundImage.contains(b)) {
7259    //        backgroundImage.remove(b);
7260    //        setDirty();
7261    //    }
7262    //}
7263    /**
7264     * add a layout shape to the list of layout shapes
7265     *
7266     * @param p Point2D where the shape should be
7267     * @return the LayoutShape
7268     */
7269    @Nonnull
7270    private LayoutShape addLayoutShape(@Nonnull Point2D p) {
7271        // get unique name
7272        String name = finder.uniqueName("S", getLayoutShapes().size() + 1);
7273
7274        // create object
7275        LayoutShape o = new LayoutShape(name, p, this);
7276        layoutShapes.add(o);
7277        unionToPanelBounds(o.getBounds());
7278        setDirty();
7279        return o;
7280    }
7281
7282    /**
7283     * Remove a layout shape from the list of layout shapes
7284     *
7285     * @param s the LayoutShape to add
7286     * @return true if added
7287     */
7288    public boolean removeLayoutShape(@Nonnull LayoutShape s) {
7289        boolean result = false;
7290        if (layoutShapes.contains(s)) {
7291            layoutShapes.remove(s);
7292            setDirty();
7293            result = true;
7294            redrawPanel();
7295        }
7296        return result;
7297    }
7298
7299    /**
7300     * Invoke a window to allow you to add a MultiSensor indicator to the target
7301     */
7302    private int multiLocX;
7303    private int multiLocY;
7304
7305    void startMultiSensor() {
7306        multiLocX = xLoc;
7307        multiLocY = yLoc;
7308
7309        if (leToolBarPanel.multiSensorFrame == null) {
7310            // create a common edit frame
7311            leToolBarPanel.multiSensorFrame = new MultiSensorIconFrame(this);
7312            leToolBarPanel.multiSensorFrame.initComponents();
7313            leToolBarPanel.multiSensorFrame.pack();
7314        }
7315        leToolBarPanel.multiSensorFrame.setVisible(true);
7316    }
7317
7318    // Invoked when window has new multi-sensor ready
7319    public void addMultiSensor(@Nonnull MultiSensorIcon l) {
7320        l.setLocation(multiLocX, multiLocY);
7321        try {
7322            putItem(l); // note: this calls unionToPanelBounds & setDirty()
7323        } catch (Positionable.DuplicateIdException e) {
7324            // This should never happen
7325            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
7326        }
7327        leToolBarPanel.multiSensorFrame.dispose();
7328        leToolBarPanel.multiSensorFrame = null;
7329    }
7330
7331    /**
7332     * Set object location and size for icon and label object as it is created.
7333     * Size comes from the preferredSize; location comes from the fields where
7334     * the user can spec it.
7335     *
7336     * @param obj the positionable object.
7337     */
7338    @Override
7339    public void setNextLocation(@Nonnull Positionable obj) {
7340        obj.setLocation(xLoc, yLoc);
7341    }
7342
7343    //
7344    // singleton (one per-LayoutEditor) accessors
7345    //
7346    private ConnectivityUtil conTools = null;
7347
7348    @Nonnull
7349    public ConnectivityUtil getConnectivityUtil() {
7350        if (conTools == null) {
7351            conTools = new ConnectivityUtil(this);
7352        }
7353        return conTools;
7354    }
7355
7356    private LayoutEditorTools tools = null;
7357
7358    @Nonnull
7359    public LayoutEditorTools getLETools() {
7360        if (tools == null) {
7361            tools = new LayoutEditorTools(this);
7362        }
7363        return tools;
7364    }
7365
7366    private LayoutEditorAuxTools auxTools = null;
7367
7368    @Override
7369    @Nonnull
7370    public LayoutEditorAuxTools getLEAuxTools() {
7371        if (auxTools == null) {
7372            auxTools = new LayoutEditorAuxTools(this);
7373        }
7374        return auxTools;
7375    }
7376
7377    private LayoutEditorChecks layoutEditorChecks = null;
7378
7379    @Nonnull
7380    public LayoutEditorChecks getLEChecks() {
7381        if (layoutEditorChecks == null) {
7382            layoutEditorChecks = new LayoutEditorChecks(this);
7383        }
7384        return layoutEditorChecks;
7385    }
7386
7387    /**
7388     * Invoked by DeletePanel menu item Validate user intent before deleting
7389     */
7390    @Override
7391    public boolean deletePanel() {
7392        if (canDeletePanel()) {
7393            // verify deletion
7394            if (!super.deletePanel()) {
7395                return false; // return without deleting if "No" response
7396            }
7397            clearLayoutTracks();
7398            return true;
7399        }
7400        return false;
7401    }
7402
7403    /**
7404     * Check for conditions that prevent a delete.
7405     * <ul>
7406     * <li>The panel has active edge connector links</li>
7407     * <li>The panel is used by EntryExit</li>
7408     * </ul>
7409     * @return true if ok to delete
7410     */
7411    public boolean canDeletePanel() {
7412        var messages = new ArrayList<String>();
7413
7414        var points = getPositionablePoints();
7415        for (PositionablePoint point : points) {
7416            if (point.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
7417                var panelName = point.getLinkedEditorName();
7418                if (!panelName.isEmpty()) {
7419                    messages.add(Bundle.getMessage("ActiveEdgeConnector", point.getId(), point.getLinkedEditorName()));
7420                }
7421            }
7422        }
7423
7424        var entryExitPairs = InstanceManager.getDefault(jmri.jmrit.entryexit.EntryExitPairs.class);
7425        if (!entryExitPairs.getNxSource(this).isEmpty()) {
7426            messages.add(Bundle.getMessage("ActiveEntryExit"));
7427        }
7428
7429        if (!messages.isEmpty()) {
7430            StringBuilder msg = new StringBuilder(Bundle.getMessage("PanelRelationshipsError"));
7431            for (String message : messages) {
7432                msg.append(message);
7433            }
7434            JmriJOptionPane.showMessageDialog(null,
7435                    msg.toString(),
7436                    Bundle.getMessage("ErrorTitle"), // NOI18N
7437                    JmriJOptionPane.ERROR_MESSAGE);
7438        }
7439
7440        return messages.isEmpty();
7441    }
7442
7443    /**
7444     * Control whether target panel items are editable. Does this by invoking
7445     * the {@link Editor#setAllEditable} function of the parent class. This also
7446     * controls the relevant pop-up menu items (which are the primary way that
7447     * items are edited).
7448     *
7449     * @param editable true for editable.
7450     */
7451    @Override
7452    public void setAllEditable(boolean editable) {
7453        int restoreScroll = _scrollState;
7454
7455        super.setAllEditable(editable);
7456
7457        if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
7458            if (editable) {
7459                createfloatingEditToolBoxFrame();
7460                createFloatingHelpPanel();
7461            } else {
7462                deletefloatingEditToolBoxFrame();
7463            }
7464        } else {
7465            editToolBarContainerPanel.setVisible(editable);
7466        }
7467        setShowHidden(editable);
7468
7469        if (editable) {
7470            setScroll(Editor.SCROLL_BOTH);
7471            _scrollState = restoreScroll;
7472        } else {
7473            setScroll(_scrollState);
7474        }
7475
7476        // these may not be set up yet...
7477        if (helpBarPanel != null) {
7478            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
7479                if (floatEditHelpPanel != null) {
7480                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
7481                }
7482            } else {
7483                helpBarPanel.setVisible(editable && getShowHelpBar());
7484            }
7485        }
7486        awaitingIconChange = false;
7487        editModeCheckBoxMenuItem.setSelected(editable);
7488        redrawPanel();
7489    }
7490
7491    /**
7492     * Control whether panel items are positionable. Markers are always
7493     * positionable.
7494     *
7495     * @param state true for positionable.
7496     */
7497    @Override
7498    public void setAllPositionable(boolean state) {
7499        super.setAllPositionable(state);
7500
7501        markerImage.forEach((p) -> p.setPositionable(true));
7502    }
7503
7504    /**
7505     * Control whether target panel items are controlling layout items. Does
7506     * this by invoke the {@link Positionable#setControlling} function of each
7507     * item on the target panel. This also controls the relevant pop-up menu
7508     * items.
7509     *
7510     * @param state true for controlling.
7511     */
7512    public void setTurnoutAnimation(boolean state) {
7513        if (animationCheckBoxMenuItem.isSelected() != state) {
7514            animationCheckBoxMenuItem.setSelected(state);
7515        }
7516
7517        if (animatingLayout != state) {
7518            animatingLayout = state;
7519            redrawPanel();
7520        }
7521    }
7522
7523    public boolean isAnimating() {
7524        return animatingLayout;
7525    }
7526
7527    public boolean getScroll() {
7528        // deprecated but kept to allow opening files
7529        // on version 2.5.1 and earlier
7530        return _scrollState != Editor.SCROLL_NONE;
7531    }
7532
7533//    public Color getDefaultBackgroundColor() {
7534//        return defaultBackgroundColor;
7535//    }
7536    public String getDefaultTrackColor() {
7537        return ColorUtil.colorToColorName(defaultTrackColor);
7538    }
7539
7540    /**
7541     *
7542     * Getter defaultTrackColor.
7543     *
7544     * @return block default color as Color
7545     */
7546    @Nonnull
7547    public Color getDefaultTrackColorColor() {
7548        return defaultTrackColor;
7549    }
7550
7551    @Nonnull
7552    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7553    public String getDefaultOccupiedTrackColor() {
7554        return ColorUtil.colorToColorName(defaultOccupiedTrackColor);
7555    }
7556
7557    /**
7558     *
7559     * Getter defaultOccupiedTrackColor.
7560     *
7561     * @return block default occupied color as Color
7562     */
7563    @Nonnull
7564    public Color getDefaultOccupiedTrackColorColor() {
7565        return defaultOccupiedTrackColor;
7566    }
7567
7568    @Nonnull
7569    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7570    public String getDefaultAlternativeTrackColor() {
7571        return ColorUtil.colorToColorName(defaultAlternativeTrackColor);
7572    }
7573
7574    /**
7575     *
7576     * Getter defaultAlternativeTrackColor.
7577     *
7578     * @return block default alternative color as Color
7579     */
7580    @Nonnull
7581    public Color getDefaultAlternativeTrackColorColor() {
7582        return defaultAlternativeTrackColor;
7583    }
7584
7585    @Nonnull
7586    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7587    public String getDefaultTextColor() {
7588        return ColorUtil.colorToColorName(defaultTextColor);
7589    }
7590
7591    @Nonnull
7592    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7593    public String getTurnoutCircleColor() {
7594        return ColorUtil.colorToColorName(turnoutCircleColor);
7595    }
7596
7597    @Nonnull
7598    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7599    public String getTurnoutCircleThrownColor() {
7600        return ColorUtil.colorToColorName(turnoutCircleThrownColor);
7601    }
7602
7603    public boolean isTurnoutFillControlCircles() {
7604        return turnoutFillControlCircles;
7605    }
7606
7607    public int getTurnoutCircleSize() {
7608        return turnoutCircleSize;
7609    }
7610
7611    public boolean isTurnoutDrawUnselectedLeg() {
7612        return turnoutDrawUnselectedLeg;
7613    }
7614
7615    public boolean isHighlightCursor() {
7616        return highlightCursor;
7617    }
7618
7619    public String getLayoutName() {
7620        return layoutName;
7621    }
7622
7623    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7624    public boolean getShowHelpBar() {
7625        return showHelpBar;
7626    }
7627
7628    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7629    public boolean getDrawGrid() {
7630        return drawGrid;
7631    }
7632
7633    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7634    public boolean getSnapOnAdd() {
7635        return snapToGridOnAdd;
7636    }
7637
7638    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7639    public boolean getSnapOnMove() {
7640        return snapToGridOnMove;
7641    }
7642
7643    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7644    public boolean getAntialiasingOn() {
7645        return antialiasingOn;
7646    }
7647
7648    public boolean isDrawLayoutTracksLabel() {
7649        return drawLayoutTracksLabel;
7650    }
7651
7652    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7653    public boolean getHighlightSelectedBlock() {
7654        return highlightSelectedBlockFlag;
7655    }
7656
7657    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7658    public boolean getTurnoutCircles() {
7659        return turnoutCirclesWithoutEditMode;
7660    }
7661
7662    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7663    public boolean getTooltipsNotEdit() {
7664        return tooltipsWithoutEditMode;
7665    }
7666
7667    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7668    public boolean getTooltipsInEdit() {
7669        return tooltipsInEditMode;
7670    }
7671
7672    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
7673    public boolean getAutoBlockAssignment() {
7674        return autoAssignBlocks;
7675    }
7676
7677    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight) {
7678        setLayoutDimensions(windowWidth, windowHeight, windowX, windowY, panelWidth, panelHeight, false);
7679    }
7680
7681    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight, boolean merge) {
7682
7683        gContext.setUpperLeftX(windowX);
7684        gContext.setUpperLeftY(windowY);
7685        setLocation(gContext.getUpperLeftX(), gContext.getUpperLeftY());
7686
7687        gContext.setWindowWidth(windowWidth);
7688        gContext.setWindowHeight(windowHeight);
7689        setSize(windowWidth, windowHeight);
7690
7691        Rectangle2D panelBounds = new Rectangle2D.Double(0.0, 0.0, panelWidth, panelHeight);
7692
7693        if (merge) {
7694            panelBounds.add(calculateMinimumLayoutBounds());
7695        }
7696        setPanelBounds(panelBounds);
7697    }
7698
7699    @Nonnull
7700    public Rectangle2D getPanelBounds() {
7701        return new Rectangle2D.Double(0.0, 0.0, gContext.getLayoutWidth(), gContext.getLayoutHeight());
7702    }
7703
7704    public void setPanelBounds(@Nonnull Rectangle2D newBounds) {
7705        // don't let origin go negative
7706        newBounds = newBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7707
7708        if (!getPanelBounds().equals(newBounds)) {
7709            gContext.setLayoutWidth((int) newBounds.getWidth());
7710            gContext.setLayoutHeight((int) newBounds.getHeight());
7711            resetTargetSize();
7712        }
7713        log.debug("setPanelBounds(({})", newBounds);
7714    }
7715
7716    private void resetTargetSize() {
7717        int newTargetWidth = (int) (gContext.getLayoutWidth() * getZoom());
7718        int newTargetHeight = (int) (gContext.getLayoutHeight() * getZoom());
7719
7720        Dimension targetPanelSize = getTargetPanelSize();
7721        int oldTargetWidth = (int) targetPanelSize.getWidth();
7722        int oldTargetHeight = (int) targetPanelSize.getHeight();
7723
7724        if ((newTargetWidth != oldTargetWidth) || (newTargetHeight != oldTargetHeight)) {
7725            setTargetPanelSize(newTargetWidth, newTargetHeight);
7726            adjustScrollBars();
7727        }
7728    }
7729
7730    // this will grow the panel bounds based on items added to the layout
7731    @Nonnull
7732    public Rectangle2D unionToPanelBounds(@Nonnull Rectangle2D bounds) {
7733        Rectangle2D result = getPanelBounds();
7734
7735        // make room to expand
7736        Rectangle2D b = MathUtil.inset(bounds, gContext.getGridSize() * gContext.getGridSize2nd() / -2.0);
7737
7738        // don't let origin go negative
7739        b = b.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7740
7741        result.add(b);
7742
7743        setPanelBounds(result);
7744        return result;
7745    }
7746
7747    /**
7748     * @param color value to set the default track color to.
7749     */
7750    public void setDefaultTrackColor(@Nonnull Color color) {
7751        defaultTrackColor = color;
7752        JmriColorChooser.addRecentColor(color);
7753    }
7754
7755    /**
7756     * @param color value to set the default occupied track color to.
7757     */
7758    public void setDefaultOccupiedTrackColor(@Nonnull Color color) {
7759        defaultOccupiedTrackColor = color;
7760        JmriColorChooser.addRecentColor(color);
7761    }
7762
7763    /**
7764     * @param color value to set the default alternate track color to.
7765     */
7766    public void setDefaultAlternativeTrackColor(@Nonnull Color color) {
7767        defaultAlternativeTrackColor = color;
7768        JmriColorChooser.addRecentColor(color);
7769    }
7770
7771    /**
7772     * @param color new color for turnout circle.
7773     */
7774    public void setTurnoutCircleColor(@CheckForNull Color color) {
7775        if (color == null) {
7776            turnoutCircleColor = getDefaultTrackColorColor();
7777        } else {
7778            turnoutCircleColor = color;
7779            JmriColorChooser.addRecentColor(color);
7780        }
7781    }
7782
7783    /**
7784     * @param color new color for turnout circle.
7785     */
7786    public void setTurnoutCircleThrownColor(@CheckForNull Color color) {
7787        if (color == null) {
7788            turnoutCircleThrownColor = getDefaultTrackColorColor();
7789        } else {
7790            turnoutCircleThrownColor = color;
7791            JmriColorChooser.addRecentColor(color);
7792        }
7793    }
7794
7795    /**
7796     * Should only be invoked on the GUI (Swing) thread.
7797     *
7798     * @param state true to fill in turnout control circles, else false.
7799     */
7800    @InvokeOnGuiThread
7801    public void setTurnoutFillControlCircles(boolean state) {
7802        if (turnoutFillControlCircles != state) {
7803            turnoutFillControlCircles = state;
7804            turnoutFillControlCirclesCheckBoxMenuItem.setSelected(turnoutFillControlCircles);
7805        }
7806    }
7807
7808    public void setTurnoutCircleSize(int size) {
7809        // this is an int
7810        turnoutCircleSize = size;
7811
7812        // these are doubles
7813        circleRadius = SIZE * size;
7814        circleDiameter = 2.0 * circleRadius;
7815
7816        setOptionMenuTurnoutCircleSize();
7817    }
7818
7819    /**
7820     * Should only be invoked on the GUI (Swing) thread.
7821     *
7822     * @param state true to draw unselected legs, else false.
7823     */
7824    @InvokeOnGuiThread
7825    public void setTurnoutDrawUnselectedLeg(boolean state) {
7826        if (turnoutDrawUnselectedLeg != state) {
7827            turnoutDrawUnselectedLeg = state;
7828            turnoutDrawUnselectedLegCheckBoxMenuItem.setSelected(turnoutDrawUnselectedLeg);
7829        }
7830    }
7831
7832    /**
7833     * Should only be invoked on the GUI (Swing) thread.
7834     *
7835     * @param state true to enable highlighting the cursor (mouse/finger press/drag)
7836     */
7837    @InvokeOnGuiThread
7838    public void setHighlightCursor(boolean state) {
7839        if (highlightCursor != state) {
7840            highlightCursor = state;
7841            highlightCursorCheckBoxMenuItem.setSelected(highlightCursor);
7842        }
7843    }
7844
7845    /**
7846     * @param color value to set the default text color to.
7847     */
7848    public void setDefaultTextColor(@Nonnull Color color) {
7849        defaultTextColor = color;
7850        JmriColorChooser.addRecentColor(color);
7851    }
7852
7853    /**
7854     * @param color value to set the panel background to.
7855     */
7856    public void setDefaultBackgroundColor(@Nonnull Color color) {
7857        defaultBackgroundColor = color;
7858        JmriColorChooser.addRecentColor(color);
7859    }
7860
7861    public void setLayoutName(@Nonnull String name) {
7862        layoutName = name;
7863    }
7864
7865    /**
7866     * Should only be invoked on the GUI (Swing) thread.
7867     *
7868     * @param state true to show the help bar, else false.
7869     */
7870    @InvokeOnGuiThread  // due to the setSelected call on a possibly-visible item
7871    public void setShowHelpBar(boolean state) {
7872        if (showHelpBar != state) {
7873            showHelpBar = state;
7874
7875            // these may not be set up yet...
7876            if (showHelpCheckBoxMenuItem != null) {
7877                showHelpCheckBoxMenuItem.setSelected(showHelpBar);
7878            }
7879
7880            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
7881                if (floatEditHelpPanel != null) {
7882                    floatEditHelpPanel.setVisible(isEditable() && showHelpBar);
7883                }
7884            } else {
7885                if (helpBarPanel != null) {
7886                    helpBarPanel.setVisible(isEditable() && showHelpBar);
7887
7888                }
7889            }
7890            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".showHelpBar", showHelpBar));
7891        }
7892    }
7893
7894    /**
7895     * Should only be invoked on the GUI (Swing) thread.
7896     *
7897     * @param state true to show the draw grid, else false.
7898     */
7899    @InvokeOnGuiThread
7900    public void setDrawGrid(boolean state) {
7901        if (drawGrid != state) {
7902            drawGrid = state;
7903            showGridCheckBoxMenuItem.setSelected(drawGrid);
7904        }
7905    }
7906
7907    /**
7908     * Should only be invoked on the GUI (Swing) thread.
7909     *
7910     * @param state true to set snap to grid on add, else false.
7911     */
7912    @InvokeOnGuiThread
7913    public void setSnapOnAdd(boolean state) {
7914        if (snapToGridOnAdd != state) {
7915            snapToGridOnAdd = state;
7916            snapToGridOnAddCheckBoxMenuItem.setSelected(snapToGridOnAdd);
7917        }
7918    }
7919
7920    /**
7921     * Should only be invoked on the GUI (Swing) thread.
7922     *
7923     * @param state true to set snap on move, else false.
7924     */
7925    @InvokeOnGuiThread
7926    public void setSnapOnMove(boolean state) {
7927        if (snapToGridOnMove != state) {
7928            snapToGridOnMove = state;
7929            snapToGridOnMoveCheckBoxMenuItem.setSelected(snapToGridOnMove);
7930        }
7931    }
7932
7933    /**
7934     * Should only be invoked on the GUI (Swing) thread.
7935     *
7936     * @param state true to set anti-aliasing flag on, else false.
7937     */
7938    @InvokeOnGuiThread
7939    public void setAntialiasingOn(boolean state) {
7940        if (antialiasingOn != state) {
7941            antialiasingOn = state;
7942
7943            // this may not be set up yet...
7944            if (antialiasingOnCheckBoxMenuItem != null) {
7945                antialiasingOnCheckBoxMenuItem.setSelected(antialiasingOn);
7946
7947            }
7948            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".antialiasingOn", antialiasingOn));
7949        }
7950    }
7951
7952    /**
7953     *
7954     * @param state true to set anti-aliasing flag on, else false.
7955     */
7956    public void setDrawLayoutTracksLabel(boolean state) {
7957        if (drawLayoutTracksLabel != state) {
7958            drawLayoutTracksLabel = state;
7959
7960            // this may not be set up yet...
7961            if (drawLayoutTracksLabelCheckBoxMenuItem != null) {
7962                drawLayoutTracksLabelCheckBoxMenuItem.setSelected(drawLayoutTracksLabel);
7963
7964            }
7965            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".drawLayoutTracksLabel", drawLayoutTracksLabel));
7966        }
7967    }
7968
7969    // enable/disable using the "Extra" color to highlight the selected block
7970    public void setHighlightSelectedBlock(boolean state) {
7971        if (highlightSelectedBlockFlag != state) {
7972            highlightSelectedBlockFlag = state;
7973
7974            // this may not be set up yet...
7975            if (leToolBarPanel.highlightBlockCheckBox != null) {
7976                leToolBarPanel.highlightBlockCheckBox.setSelected(highlightSelectedBlockFlag);
7977
7978            }
7979
7980            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".highlightSelectedBlock", highlightSelectedBlockFlag));
7981
7982            // thread this so it won't break the AppVeyor checks
7983            ThreadingUtil.newThread(() -> {
7984                if (highlightSelectedBlockFlag) {
7985                    // use the "Extra" color to highlight the selected block
7986                    if (!highlightBlockInComboBox(leToolBarPanel.blockIDComboBox)) {
7987                        highlightBlockInComboBox(leToolBarPanel.blockContentsComboBox);
7988                    }
7989                } else {
7990                    // undo using the "Extra" color to highlight the selected block
7991                    Block block = leToolBarPanel.blockIDComboBox.getSelectedItem();
7992                    highlightBlock(null);
7993                    leToolBarPanel.blockIDComboBox.setSelectedItem(block);
7994                }
7995            }).start();
7996        }
7997    }
7998
7999    //
8000    // highlight the block selected by the specified combo Box
8001    //
8002    public boolean highlightBlockInComboBox(@Nonnull NamedBeanComboBox<Block> inComboBox) {
8003        return highlightBlock(inComboBox.getSelectedItem());
8004    }
8005
8006    /**
8007     * highlight the specified block
8008     *
8009     * @param inBlock the block
8010     * @return true if block was highlighted
8011     */
8012    public boolean highlightBlock(@CheckForNull Block inBlock) {
8013        boolean result = false; // assume failure (pessimist!)
8014
8015        if (leToolBarPanel.blockIDComboBox.getSelectedItem() != inBlock) {
8016            leToolBarPanel.blockIDComboBox.setSelectedItem(inBlock);
8017        }
8018
8019        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class
8020        );
8021        Set<Block> l = leToolBarPanel.blockIDComboBox.getManager().getNamedBeanSet();
8022        for (Block b : l) {
8023            LayoutBlock lb = lbm.getLayoutBlock(b);
8024            if (lb != null) {
8025                boolean enable = ((inBlock != null) && b.equals(inBlock));
8026                lb.setUseExtraColor(enable);
8027                result |= enable;
8028            }
8029        }
8030        return result;
8031    }
8032
8033    /**
8034     * highlight the specified layout block
8035     *
8036     * @param inLayoutBlock the layout block
8037     * @return true if layout block was highlighted
8038     */
8039    public boolean highlightLayoutBlock(@Nonnull LayoutBlock inLayoutBlock) {
8040        return highlightBlock(inLayoutBlock.getBlock());
8041    }
8042
8043    public void setTurnoutCircles(boolean state) {
8044        if (turnoutCirclesWithoutEditMode != state) {
8045            turnoutCirclesWithoutEditMode = state;
8046            if (turnoutCirclesOnCheckBoxMenuItem != null) {
8047                turnoutCirclesOnCheckBoxMenuItem.setSelected(turnoutCirclesWithoutEditMode);
8048            }
8049        }
8050    }
8051
8052    public void setAutoBlockAssignment(boolean boo) {
8053        if (autoAssignBlocks != boo) {
8054            autoAssignBlocks = boo;
8055            if (autoAssignBlocksCheckBoxMenuItem != null) {
8056                autoAssignBlocksCheckBoxMenuItem.setSelected(autoAssignBlocks);
8057            }
8058        }
8059    }
8060
8061    public void setTooltipsNotEdit(boolean state) {
8062        if (tooltipsWithoutEditMode != state) {
8063            tooltipsWithoutEditMode = state;
8064            setTooltipSubMenu();
8065            setTooltipsAlwaysOrNever();
8066        }
8067    }
8068
8069    public void setTooltipsInEdit(boolean state) {
8070        if (tooltipsInEditMode != state) {
8071            tooltipsInEditMode = state;
8072            setTooltipSubMenu();
8073            setTooltipsAlwaysOrNever();
8074        }
8075    }
8076
8077    private void setTooltipsAlwaysOrNever() {
8078        tooltipsAlwaysOrNever = ((tooltipsInEditMode && tooltipsWithoutEditMode) ||
8079                    (!tooltipsInEditMode && !tooltipsWithoutEditMode));
8080    }
8081
8082    private void setTooltipSubMenu() {
8083        if (tooltipNoneMenuItem != null) {
8084            tooltipNoneMenuItem.setSelected((!tooltipsInEditMode) && (!tooltipsWithoutEditMode));
8085            tooltipAlwaysMenuItem.setSelected((tooltipsInEditMode) && (tooltipsWithoutEditMode));
8086            tooltipInEditMenuItem.setSelected((tooltipsInEditMode) && (!tooltipsWithoutEditMode));
8087            tooltipNotInEditMenuItem.setSelected((!tooltipsInEditMode) && (tooltipsWithoutEditMode));
8088        }
8089    }
8090
8091    // accessor routines for turnout size parameters
8092    public void setTurnoutBX(double bx) {
8093        turnoutBX = bx;
8094        setDirty();
8095    }
8096
8097    public double getTurnoutBX() {
8098        return turnoutBX;
8099    }
8100
8101    public void setTurnoutCX(double cx) {
8102        turnoutCX = cx;
8103        setDirty();
8104    }
8105
8106    public double getTurnoutCX() {
8107        return turnoutCX;
8108    }
8109
8110    public void setTurnoutWid(double wid) {
8111        turnoutWid = wid;
8112        setDirty();
8113    }
8114
8115    public double getTurnoutWid() {
8116        return turnoutWid;
8117    }
8118
8119    public void setXOverLong(double lg) {
8120        xOverLong = lg;
8121        setDirty();
8122    }
8123
8124    public double getXOverLong() {
8125        return xOverLong;
8126    }
8127
8128    public void setXOverHWid(double hwid) {
8129        xOverHWid = hwid;
8130        setDirty();
8131    }
8132
8133    public double getXOverHWid() {
8134        return xOverHWid;
8135    }
8136
8137    public void setXOverShort(double sh) {
8138        xOverShort = sh;
8139        setDirty();
8140    }
8141
8142    public double getXOverShort() {
8143        return xOverShort;
8144    }
8145
8146    // reset turnout sizes to program defaults
8147    private void resetTurnoutSize() {
8148        turnoutBX = LayoutTurnout.turnoutBXDefault;
8149        turnoutCX = LayoutTurnout.turnoutCXDefault;
8150        turnoutWid = LayoutTurnout.turnoutWidDefault;
8151        xOverLong = LayoutTurnout.xOverLongDefault;
8152        xOverHWid = LayoutTurnout.xOverHWidDefault;
8153        xOverShort = LayoutTurnout.xOverShortDefault;
8154        setDirty();
8155    }
8156
8157    public void setDirectTurnoutControl(boolean boo) {
8158        useDirectTurnoutControl = boo;
8159        useDirectTurnoutControlCheckBoxMenuItem.setSelected(useDirectTurnoutControl);
8160    }
8161
8162    // TODO: Java standard pattern for boolean getters is "isShowHelpBar()"
8163    public boolean getDirectTurnoutControl() {
8164        return useDirectTurnoutControl;
8165    }
8166
8167    // final initialization routine for loading a LayoutEditor
8168    public void setConnections() {
8169        getLayoutTracks().forEach((lt) -> lt.setObjects(this));
8170        getLEAuxTools().initializeBlockConnectivity();
8171        log.debug("Initializing Block Connectivity for {}", getLayoutName());
8172
8173        // reset the panel changed bit
8174        resetDirty();
8175    }
8176
8177    // these are convenience methods to return rectangles
8178    // to use when (hit point-in-rect testing
8179    //
8180    // compute the control point rect at inPoint
8181    public @Nonnull
8182    Rectangle2D layoutEditorControlRectAt(@Nonnull Point2D inPoint) {
8183        return new Rectangle2D.Double(inPoint.getX() - SIZE,
8184                inPoint.getY() - SIZE, SIZE2, SIZE2);
8185    }
8186
8187    // compute the turnout circle control rect at inPoint
8188    public @Nonnull
8189    Rectangle2D layoutEditorControlCircleRectAt(@Nonnull Point2D inPoint) {
8190        return new Rectangle2D.Double(inPoint.getX() - circleRadius,
8191                inPoint.getY() - circleRadius, circleDiameter, circleDiameter);
8192    }
8193
8194    /**
8195     * Special internal class to allow drawing of layout to a JLayeredPane This
8196     * is the 'target' pane where the layout is displayed
8197     */
8198    @Override
8199    public void paintTargetPanel(@Nonnull Graphics g) {
8200        // Nothing to do here
8201        // All drawing has been moved into LayoutEditorComponent
8202        // which calls draw.
8203        // This is so the layout is drawn at level three
8204        // (above or below the Positionables)
8205    }
8206
8207    // get selection rectangle
8208    @Nonnull
8209    public Rectangle2D getSelectionRect() {
8210        double selX = Math.min(selectionX, selectionX + selectionWidth);
8211        double selY = Math.min(selectionY, selectionY + selectionHeight);
8212        return new Rectangle2D.Double(selX, selY,
8213                Math.abs(selectionWidth), Math.abs(selectionHeight));
8214    }
8215
8216    // set selection rectangle
8217    public void setSelectionRect(@Nonnull Rectangle2D selectionRect) {
8218        // selectionRect = selectionRect.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8219        selectionX = selectionRect.getX();
8220        selectionY = selectionRect.getY();
8221        selectionWidth = selectionRect.getWidth();
8222        selectionHeight = selectionRect.getHeight();
8223
8224        // There's already code in the super class (Editor) to draw
8225        // the selection rect... We just have to set _selectRect
8226        _selectRect = MathUtil.rectangle2DToRectangle(selectionRect);
8227
8228        selectionRect = MathUtil.scale(selectionRect, getZoom());
8229
8230        JComponent targetPanel = getTargetPanel();
8231        Rectangle targetRect = targetPanel.getVisibleRect();
8232        // this will make it the size of the targetRect
8233        // (effectively centering it onscreen)
8234        Rectangle2D selRect2D = MathUtil.inset(selectionRect,
8235                (selectionRect.getWidth() - targetRect.getWidth()) / 2.0,
8236                (selectionRect.getHeight() - targetRect.getHeight()) / 2.0);
8237        // don't let the origin go negative
8238        selRect2D = selRect2D.createIntersection(MathUtil.zeroToInfinityRectangle2D);
8239        Rectangle selRect = MathUtil.rectangle2DToRectangle(selRect2D);
8240        if (!targetRect.contains(selRect)) {
8241            targetPanel.scrollRectToVisible(selRect);
8242        }
8243
8244        clearSelectionGroups();
8245        selectionActive = true;
8246        createSelectionGroups();
8247        // redrawPanel(); // createSelectionGroups already calls this
8248    }
8249
8250    public void setSelectRect(Rectangle rectangle) {
8251        _selectRect = rectangle;
8252    }
8253
8254    /*
8255    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
8256    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<LayoutTrack> layoutTrackClass) {
8257    return getLayoutTracks().stream()
8258    .filter(item -> item instanceof PositionablePoint)
8259    .filter(layoutTrackClass::isInstance)
8260    //.map(layoutTrackClass::cast)  // TODO: Do we need this? if not dead-code-strip
8261    .collect(Collectors.toList());
8262    }
8263
8264    // TODO: This compiles but I can't get the syntax correct to pass the array of (sub-)classes
8265    public List<LayoutTrack> getLayoutTracksOfClasses(@Nonnull List<Class<? extends LayoutTrack>> layoutTrackClasses) {
8266    return getLayoutTracks().stream()
8267    .filter(o -> layoutTrackClasses.contains(o.getClass()))
8268    .collect(Collectors.toList());
8269    }
8270
8271    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
8272    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<? extends LayoutTrack> layoutTrackClass) {
8273    return getLayoutTracksOfClasses(new ArrayList<>(Arrays.asList(layoutTrackClass)));
8274    }
8275
8276    public List<PositionablePoint> getPositionablePoints() {
8277    return getLayoutTracksOfClass(PositionablePoint);
8278    }
8279     */
8280    @Override
8281    public @Nonnull
8282    Stream<LayoutTrack> getLayoutTracksOfClass(Class<? extends LayoutTrack> layoutTrackClass) {
8283        return getLayoutTracks().stream()
8284                .filter(layoutTrackClass::isInstance)
8285                .map(layoutTrackClass::cast);
8286    }
8287
8288    @Override
8289    public @Nonnull
8290    Stream<LayoutTrackView> getLayoutTrackViewsOfClass(Class<? extends LayoutTrackView> layoutTrackViewClass) {
8291        return getLayoutTrackViews().stream()
8292                .filter(layoutTrackViewClass::isInstance)
8293                .map(layoutTrackViewClass::cast);
8294    }
8295
8296    @Override
8297    public @Nonnull
8298    List<PositionablePointView> getPositionablePointViews() {
8299        return getLayoutTrackViewsOfClass(PositionablePointView.class)
8300                .map(PositionablePointView.class::cast)
8301                .collect(Collectors.toCollection(ArrayList::new));
8302    }
8303
8304    @Override
8305    public @Nonnull
8306    List<PositionablePoint> getPositionablePoints() {
8307        return getLayoutTracksOfClass(PositionablePoint.class)
8308                .map(PositionablePoint.class::cast)
8309                .collect(Collectors.toCollection(ArrayList::new));
8310    }
8311
8312    public @Nonnull
8313    List<LayoutSlipView> getLayoutSlipViews() {
8314        return getLayoutTrackViewsOfClass(LayoutSlipView.class)
8315                .map(LayoutSlipView.class::cast)
8316                .collect(Collectors.toCollection(ArrayList::new));
8317    }
8318
8319    @Override
8320    public @Nonnull
8321    List<LayoutSlip> getLayoutSlips() {
8322        return getLayoutTracksOfClass(LayoutSlip.class)
8323                .map(LayoutSlip.class::cast)
8324                .collect(Collectors.toCollection(ArrayList::new));
8325    }
8326
8327    @Override
8328    public @Nonnull
8329    List<TrackSegmentView> getTrackSegmentViews() {
8330        return getLayoutTrackViewsOfClass(TrackSegmentView.class)
8331                .map(TrackSegmentView.class::cast)
8332                .collect(Collectors.toCollection(ArrayList::new));
8333    }
8334
8335    @Override
8336    public @Nonnull
8337    List<TrackSegment> getTrackSegments() {
8338        return getLayoutTracksOfClass(TrackSegment.class)
8339                .map(TrackSegment.class::cast)
8340                .collect(Collectors.toCollection(ArrayList::new));
8341    }
8342
8343    public @Nonnull
8344    List<LayoutTurnoutView> getLayoutTurnoutViews() { // this specifically does not include slips
8345        return getLayoutTrackViews().stream() // next line excludes LayoutSlips
8346                .filter((o) -> (!(o instanceof LayoutSlipView) && (o instanceof LayoutTurnoutView)))
8347                .map(LayoutTurnoutView.class::cast)
8348                .collect(Collectors.toCollection(ArrayList::new));
8349    }
8350
8351    @Override
8352    public @Nonnull
8353    List<LayoutTurnout> getLayoutTurnouts() { // this specifically does not include slips
8354        return getLayoutTracks().stream() // next line excludes LayoutSlips
8355                .filter((o) -> (!(o instanceof LayoutSlip) && (o instanceof LayoutTurnout)))
8356                .map(LayoutTurnout.class::cast)
8357                .collect(Collectors.toCollection(ArrayList::new));
8358    }
8359
8360    @Override
8361    public @Nonnull
8362    List<LayoutTurntable> getLayoutTurntables() {
8363        return getLayoutTracksOfClass(LayoutTurntable.class)
8364                .map(LayoutTurntable.class::cast)
8365                .collect(Collectors.toCollection(ArrayList::new));
8366    }
8367
8368    public @Nonnull
8369    List<LayoutTurntableView> getLayoutTurntableViews() {
8370        return getLayoutTrackViewsOfClass(LayoutTurntableView.class)
8371                .map(LayoutTurntableView.class::cast)
8372                .collect(Collectors.toCollection(ArrayList::new));
8373    }
8374
8375    @Override
8376    public @Nonnull
8377    List<LevelXing> getLevelXings() {
8378        return getLayoutTracksOfClass(LevelXing.class)
8379                .map(LevelXing.class::cast)
8380                .collect(Collectors.toCollection(ArrayList::new));
8381    }
8382
8383    @Override
8384    public @Nonnull
8385    List<LevelXingView> getLevelXingViews() {
8386        return getLayoutTrackViewsOfClass(LevelXingView.class)
8387                .map(LevelXingView.class::cast)
8388                .collect(Collectors.toCollection(ArrayList::new));
8389    }
8390
8391    /**
8392     * Read-only access to the list of LayoutTrack family objects. The returned
8393     * list will throw UnsupportedOperationException if you attempt to modify
8394     * it.
8395     *
8396     * @return unmodifiable copy of layout track list.
8397     */
8398    @Override
8399    @Nonnull
8400    final public List<LayoutTrack> getLayoutTracks() {
8401        return Collections.unmodifiableList(layoutTrackList);
8402    }
8403
8404    public @Nonnull
8405    List<LayoutTurnoutView> getLayoutTurnoutAndSlipViews() {
8406        return getLayoutTrackViewsOfClass(LayoutTurnoutView.class
8407        )
8408                .map(LayoutTurnoutView.class::cast)
8409                .collect(Collectors.toCollection(ArrayList::new));
8410    }
8411
8412    @Override
8413    public @Nonnull
8414    List<LayoutTurnout> getLayoutTurnoutsAndSlips() {
8415        return getLayoutTracksOfClass(LayoutTurnout.class
8416        )
8417                .map(LayoutTurnout.class::cast)
8418                .collect(Collectors.toCollection(ArrayList::new));
8419    }
8420
8421    /**
8422     * Read-only access to the list of LayoutTrackView family objects. The
8423     * returned list will throw UnsupportedOperationException if you attempt to
8424     * modify it.
8425     *
8426     * @return unmodifiable copy of track views.
8427     */
8428    @Override
8429    @Nonnull
8430    final public List<LayoutTrackView> getLayoutTrackViews() {
8431        return Collections.unmodifiableList(layoutTrackViewList);
8432    }
8433
8434    private final List<LayoutTrack> layoutTrackList = new ArrayList<>();
8435    private final List<LayoutTrackView> layoutTrackViewList = new ArrayList<>();
8436    private final Map<LayoutTrack, LayoutTrackView> trkToView = new HashMap<>();
8437    private final Map<LayoutTrackView, LayoutTrack> viewToTrk = new HashMap<>();
8438
8439    // temporary
8440    @Override
8441    final public LayoutTrackView getLayoutTrackView(LayoutTrack trk) {
8442        LayoutTrackView lv = trkToView.get(trk);
8443        if (lv == null) {
8444            log.warn("No View found for {} class {}", trk, trk.getClass());
8445            throw new IllegalArgumentException("No View found: " + trk.getClass());
8446        }
8447        return lv;
8448    }
8449
8450    // temporary
8451    @Override
8452    final public LevelXingView getLevelXingView(LevelXing xing) {
8453        LayoutTrackView lv = trkToView.get(xing);
8454        if (lv == null) {
8455            log.warn("No View found for {} class {}", xing, xing.getClass());
8456            throw new IllegalArgumentException("No View found: " + xing.getClass());
8457        }
8458        if (lv instanceof LevelXingView) {
8459            return (LevelXingView) lv;
8460        } else {
8461            log.error("wrong type {} {} found {}", xing, xing.getClass(), lv);
8462        }
8463        throw new IllegalArgumentException("Wrong type: " + xing.getClass());
8464    }
8465
8466    // temporary
8467    @Override
8468    final public LayoutTurnoutView getLayoutTurnoutView(LayoutTurnout to) {
8469        LayoutTrackView lv = trkToView.get(to);
8470        if (lv == null) {
8471            log.warn("No View found for {} class {}", to, to.getClass());
8472            throw new IllegalArgumentException("No View found: " + to);
8473        }
8474        if (lv instanceof LayoutTurnoutView) {
8475            return (LayoutTurnoutView) lv;
8476        } else {
8477            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
8478        }
8479        throw new IllegalArgumentException("Wrong type: " + to.getClass());
8480    }
8481
8482    // temporary
8483    @Override
8484    final public LayoutTurntableView getLayoutTurntableView(LayoutTurntable to) {
8485        LayoutTrackView lv = trkToView.get(to);
8486        if (lv == null) {
8487            log.warn("No View found for {} class {}", to, to.getClass());
8488            throw new IllegalArgumentException("No matching View found: " + to);
8489        }
8490        if (lv instanceof LayoutTurntableView) {
8491            return (LayoutTurntableView) lv;
8492        } else {
8493            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
8494        }
8495        throw new IllegalArgumentException("Wrong type: " + to.getClass());
8496    }
8497
8498    // temporary
8499    final public LayoutSlipView getLayoutSlipView(LayoutSlip to) {
8500        LayoutTrackView lv = trkToView.get(to);
8501        if (lv == null) {
8502            log.warn("No View found for {} class {}", to, to.getClass());
8503            throw new IllegalArgumentException("No matching View found: " + to);
8504        }
8505        if (lv instanceof LayoutSlipView) {
8506            return (LayoutSlipView) lv;
8507        } else {
8508            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
8509        }
8510        throw new IllegalArgumentException("Wrong type: " + to.getClass());
8511    }
8512
8513    // temporary
8514    @Override
8515    final public TrackSegmentView getTrackSegmentView(TrackSegment to) {
8516        LayoutTrackView lv = trkToView.get(to);
8517        if (lv == null) {
8518            log.warn("No View found for {} class {}", to, to.getClass());
8519            throw new IllegalArgumentException("No matching View found: " + to);
8520        }
8521        if (lv instanceof TrackSegmentView) {
8522            return (TrackSegmentView) lv;
8523        } else {
8524            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
8525        }
8526        throw new IllegalArgumentException("Wrong type: " + to.getClass());
8527    }
8528
8529    // temporary
8530    @Override
8531    final public PositionablePointView getPositionablePointView(PositionablePoint to) {
8532        LayoutTrackView lv = trkToView.get(to);
8533        if (lv == null) {
8534            log.warn("No View found for {} class {}", to, to.getClass());
8535            throw new IllegalArgumentException("No matching View found: " + to);
8536        }
8537        if (lv instanceof PositionablePointView) {
8538            return (PositionablePointView) lv;
8539        } else {
8540            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
8541        }
8542        throw new IllegalArgumentException("Wrong type: " + to.getClass());
8543    }
8544
8545    /**
8546     * Add a LayoutTrack and LayoutTrackView to the list of LayoutTrack family
8547     * objects.
8548     *
8549     * @param trk the layout track to add.
8550     */
8551    @Override
8552    final public void addLayoutTrack(@Nonnull LayoutTrack trk, @Nonnull LayoutTrackView v) {
8553        log.trace("addLayoutTrack {}", trk);
8554        if (layoutTrackList.contains(trk)) {
8555            log.warn("LayoutTrack {} already being maintained", trk.getName());
8556        }
8557
8558        layoutTrackList.add(trk);
8559        layoutTrackViewList.add(v);
8560        trkToView.put(trk, v);
8561        viewToTrk.put(v, trk);
8562
8563        unionToPanelBounds(v.getBounds()); // temporary - this should probably _not_ be in the topological part
8564
8565    }
8566
8567    /**
8568     * If item present, delete from the list of LayoutTracks and force a dirty
8569     * redraw.
8570     *
8571     * @param trk the layout track to remove and redraw.
8572     * @return true is item was deleted and a redraw done.
8573     */
8574    final public boolean removeLayoutTrackAndRedraw(@Nonnull LayoutTrack trk) {
8575        log.trace("removeLayoutTrackAndRedraw {}", trk);
8576        if (layoutTrackList.contains(trk)) {
8577            removeLayoutTrack(trk);
8578            setDirty();
8579            redrawPanel();
8580            log.trace("removeLayoutTrackAndRedraw present {}", trk);
8581            return true;
8582        }
8583        log.trace("removeLayoutTrackAndRedraw absent {}", trk);
8584        return false;
8585    }
8586
8587    /**
8588     * If item present, delete from the list of LayoutTracks and force a dirty
8589     * redraw.
8590     *
8591     * @param trk the layout track to remove.
8592     */
8593    @Override
8594    final public void removeLayoutTrack(@Nonnull LayoutTrack trk) {
8595        log.trace("removeLayoutTrack {}", trk);
8596        layoutTrackList.remove(trk);
8597        LayoutTrackView v = trkToView.get(trk);
8598        layoutTrackViewList.remove(v);
8599        trkToView.remove(trk);
8600        viewToTrk.remove(v);
8601    }
8602
8603    /**
8604     * Clear the list of layout tracks. Not intended for general use.
8605     * <p>
8606     */
8607    private void clearLayoutTracks() {
8608        layoutTrackList.clear();
8609        layoutTrackViewList.clear();
8610        trkToView.clear();
8611        viewToTrk.clear();
8612    }
8613
8614    @Override
8615    public @Nonnull
8616    List<LayoutShape> getLayoutShapes() {
8617        return layoutShapes;
8618    }
8619
8620    public void sortLayoutShapesByLevel() {
8621        layoutShapes.sort((lhs, rhs) -> {
8622            // -1 == less than, 0 == equal, +1 == greater than
8623            return Integer.signum(lhs.getLevel() - rhs.getLevel());
8624        });
8625    }
8626
8627    /**
8628     * {@inheritDoc}
8629     * <p>
8630     * This implementation is temporary, using the on-screen points from the
8631     * LayoutTrackViews via @{link LayoutEditor#getCoords}.
8632     */
8633    @Override
8634    public int computeDirection(LayoutTrack trk1, HitPointType h1, LayoutTrack trk2, HitPointType h2) {
8635        return Path.computeDirection(
8636                getCoords(trk1, h1),
8637                getCoords(trk2, h2)
8638        );
8639    }
8640
8641    @Override
8642    public int computeDirectionToCenter(@Nonnull LayoutTrack trk1, @Nonnull HitPointType h1, @Nonnull PositionablePoint p) {
8643        return Path.computeDirection(
8644                getCoords(trk1, h1),
8645                getPositionablePointView(p).getCoordsCenter()
8646        );
8647    }
8648
8649    @Override
8650    public int computeDirectionFromCenter(@Nonnull PositionablePoint p, @Nonnull LayoutTrack trk1, @Nonnull HitPointType h1) {
8651        return Path.computeDirection(
8652                getPositionablePointView(p).getCoordsCenter(),
8653                getCoords(trk1, h1)
8654        );
8655    }
8656
8657    @Override
8658    public boolean showAlignPopup(@Nonnull Positionable l) {
8659        return false;
8660    }
8661
8662    @Override
8663    public void showToolTip(
8664            @Nonnull Positionable selection,
8665            @Nonnull JmriMouseEvent event) {
8666        ToolTip tip = selection.getToolTip();
8667        tip.setLocation(selection.getX() + selection.getWidth() / 2, selection.getY() + selection.getHeight());
8668        setToolTip(tip);
8669    }
8670
8671    @Override
8672    public void addToPopUpMenu(
8673            @Nonnull NamedBean nb,
8674            @Nonnull JMenuItem item,
8675            int menu) {
8676        if ((nb == null) || (item == null)) {
8677            return;
8678        }
8679
8680        List<?> theList = null;
8681
8682        if (nb instanceof Sensor) {
8683            theList = sensorList;
8684        } else if (nb instanceof SignalHead) {
8685            theList = signalList;
8686        } else if (nb instanceof SignalMast) {
8687            theList = signalMastList;
8688        } else if (nb instanceof Block) {
8689            theList = blockContentsLabelList;
8690        } else if (nb instanceof Memory) {
8691            theList = memoryLabelList;
8692        } else if (nb instanceof GlobalVariable) {
8693            theList = globalVariableLabelList;
8694        }
8695        if (theList != null) {
8696            for (Object o : theList) {
8697                PositionableLabel si = (PositionableLabel) o;
8698                if ((si.getNamedBean() == nb) && (si.getPopupUtility() != null)) {
8699                    if (menu != Editor.VIEWPOPUPONLY) {
8700                        si.getPopupUtility().addEditPopUpMenu(item);
8701                    }
8702                    if (menu != Editor.EDITPOPUPONLY) {
8703                        si.getPopupUtility().addViewPopUpMenu(item);
8704                    }
8705                }
8706            }
8707        } else if (nb instanceof Turnout) {
8708            for (LayoutTurnoutView ltv : getLayoutTurnoutAndSlipViews()) {
8709                if (ltv.getTurnout().equals(nb)) {
8710                    if (menu != Editor.VIEWPOPUPONLY) {
8711                        ltv.addEditPopUpMenu(item);
8712                    }
8713                    if (menu != Editor.EDITPOPUPONLY) {
8714                        ltv.addViewPopUpMenu(item);
8715                    }
8716                }
8717            }
8718        }
8719    }
8720
8721    @Override
8722    public @Nonnull
8723    String toString() {
8724        return String.format("LayoutEditor: %s", getLayoutName());
8725    }
8726
8727    @Override
8728    public void vetoableChange(
8729            @Nonnull PropertyChangeEvent evt)
8730            throws PropertyVetoException {
8731        NamedBean nb = (NamedBean) evt.getOldValue();
8732
8733        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
8734            StringBuilder message = new StringBuilder();
8735            message.append(Bundle.getMessage("VetoInUseLayoutEditorHeader", toString())); // NOI18N
8736            message.append("<ul>");
8737            boolean found = false;
8738
8739            if (nb instanceof SignalHead) {
8740                if (containsSignalHead((SignalHead) nb)) {
8741                    found = true;
8742                    message.append("<li>");
8743                    message.append(Bundle.getMessage("VetoSignalHeadIconFound"));
8744                    message.append("</li>");
8745                }
8746                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8747
8748                if (lt != null) {
8749                    message.append("<li>");
8750                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToTurnout", lt.getTurnoutName()));
8751                    message.append("</li>");
8752                }
8753                PositionablePoint p = finder.findPositionablePointByBean(nb);
8754
8755                if (p != null) {
8756                    message.append("<li>");
8757                    // Need to expand to get the names of blocks
8758                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToPoint"));
8759                    message.append("</li>");
8760                }
8761                LevelXing lx = finder.findLevelXingByBean(nb);
8762
8763                if (lx != null) {
8764                    message.append("<li>");
8765                    // Need to expand to get the names of blocks
8766                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLevelXing"));
8767                    message.append("</li>");
8768                }
8769                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
8770
8771                if (ls != null) {
8772                    message.append("<li>");
8773                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLayoutSlip", ls.getTurnoutName()));
8774                    message.append("</li>");
8775                }
8776            } else if (nb instanceof Turnout) {
8777                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8778
8779                if (lt != null) {
8780                    found = true;
8781                    message.append("<li>");
8782                    message.append(Bundle.getMessage("VetoTurnoutIconFound"));
8783                    message.append("</li>");
8784                }
8785
8786                for (LayoutTurnout t : getLayoutTurnouts()) {
8787                    if (t.getLinkedTurnoutName() != null) {
8788                        String uname = nb.getUserName();
8789
8790                        if (nb.getSystemName().equals(t.getLinkedTurnoutName())
8791                                || ((uname != null) && uname.equals(t.getLinkedTurnoutName()))) {
8792                            found = true;
8793                            message.append("<li>");
8794                            message.append(Bundle.getMessage("VetoLinkedTurnout", t.getTurnoutName()));
8795                            message.append("</li>");
8796                        }
8797                    }
8798
8799                    if (nb.equals(t.getSecondTurnout())) {
8800                        found = true;
8801                        message.append("<li>");
8802                        message.append(Bundle.getMessage("VetoSecondTurnout", t.getTurnoutName()));
8803                        message.append("</li>");
8804                    }
8805                }
8806                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
8807
8808                if (ls != null) {
8809                    found = true;
8810                    message.append("<li>");
8811                    message.append(Bundle.getMessage("VetoSlipIconFound", ls.getDisplayName()));
8812                    message.append("</li>");
8813                }
8814
8815                for (LayoutTurntable lx : getLayoutTurntables()) {
8816                    if (lx.isTurnoutControlled()) {
8817                        for (int i = 0; i < lx.getNumberRays(); i++) {
8818                            if (nb.equals(lx.getRayTurnout(i))) {
8819                                found = true;
8820                                message.append("<li>");
8821                                message.append(Bundle.getMessage("VetoRayTurntableControl", lx.getId()));
8822                                message.append("</li>");
8823                                break;
8824                            }
8825                        }
8826                    }
8827                }
8828            }
8829
8830            if (nb instanceof SignalMast) {
8831                if (containsSignalMast((SignalMast) nb)) {
8832                    message.append("<li>");
8833                    message.append("As an Icon");
8834                    message.append("</li>");
8835                    found = true;
8836                }
8837                String foundelsewhere = findBeanUsage(nb);
8838
8839                if (foundelsewhere != null) {
8840                    message.append(foundelsewhere);
8841                    found = true;
8842                }
8843            }
8844
8845            if (nb instanceof Sensor) {
8846                int count = 0;
8847
8848                for (SensorIcon si : sensorList) {
8849                    if (nb.equals(si.getNamedBean())) {
8850                        count++;
8851                        found = true;
8852                    }
8853                }
8854
8855                if (count > 0) {
8856                    message.append("<li>");
8857                    message.append(String.format("As an Icon %s times", count));
8858                    message.append("</li>");
8859                }
8860                String foundelsewhere = findBeanUsage(nb);
8861
8862                if (foundelsewhere != null) {
8863                    message.append(foundelsewhere);
8864                    found = true;
8865                }
8866            }
8867
8868            if (nb instanceof Memory) {
8869                for (MemoryIcon si : memoryLabelList) {
8870                    if (nb.equals(si.getMemory())) {
8871                        found = true;
8872                        message.append("<li>");
8873                        message.append(Bundle.getMessage("VetoMemoryIconFound"));
8874                        message.append("</li>");
8875                    }
8876                }
8877            }
8878
8879            if (nb instanceof GlobalVariable) {
8880                for (GlobalVariableIcon si : globalVariableLabelList) {
8881                    if (nb.equals(si.getGlobalVariable())) {
8882                        found = true;
8883                        message.append("<li>");
8884                        message.append(Bundle.getMessage("VetoGlobalVariableIconFound"));
8885                        message.append("</li>");
8886                    }
8887                }
8888            }
8889
8890            if (found) {
8891                message.append("</ul>");
8892                message.append(Bundle.getMessage("VetoReferencesWillBeRemoved")); // NOI18N
8893                throw new PropertyVetoException(message.toString(), evt);
8894            }
8895        } else if ("DoDelete".equals(evt.getPropertyName())) { // NOI18N
8896            if (nb instanceof SignalHead) {
8897                removeSignalHead((SignalHead) nb);
8898                removeBeanRefs(nb);
8899            }
8900
8901            if (nb instanceof Turnout) {
8902                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8903
8904                if (lt != null) {
8905                    lt.setTurnout("");
8906                }
8907
8908                for (LayoutTurnout t : getLayoutTurnouts()) {
8909                    if (t.getLinkedTurnoutName() != null) {
8910                        if (t.getLinkedTurnoutName().equals(nb.getSystemName())
8911                                || ((nb.getUserName() != null) && t.getLinkedTurnoutName().equals(nb.getUserName()))) {
8912                            t.setLinkedTurnoutName("");
8913                        }
8914                    }
8915
8916                    if (nb.equals(t.getSecondTurnout())) {
8917                        t.setSecondTurnout("");
8918                    }
8919                }
8920
8921                for (LayoutSlip sl : getLayoutSlips()) {
8922                    if (nb.equals(sl.getTurnout())) {
8923                        sl.setTurnout("");
8924                    }
8925
8926                    if (nb.equals(sl.getTurnoutB())) {
8927                        sl.setTurnoutB("");
8928                    }
8929                }
8930
8931                for (LayoutTurntable lx : getLayoutTurntables()) {
8932                    if (lx.isTurnoutControlled()) {
8933                        for (int i = 0; i < lx.getNumberRays(); i++) {
8934                            if (nb.equals(lx.getRayTurnout(i))) {
8935                                lx.setRayTurnout(i, null, NamedBean.UNKNOWN);
8936                            }
8937                        }
8938                    }
8939                }
8940            }
8941
8942            if (nb instanceof SignalMast) {
8943                removeBeanRefs(nb);
8944
8945                if (containsSignalMast((SignalMast) nb)) {
8946                    Iterator<SignalMastIcon> icon = signalMastList.iterator();
8947
8948                    while (icon.hasNext()) {
8949                        SignalMastIcon i = icon.next();
8950
8951                        if (i.getSignalMast().equals(nb)) {
8952                            icon.remove();
8953                            super.removeFromContents(i);
8954                        }
8955                    }
8956                    setDirty();
8957                    redrawPanel();
8958                }
8959            }
8960
8961            if (nb instanceof Sensor) {
8962                removeBeanRefs(nb);
8963                Iterator<SensorIcon> icon = sensorImage.iterator();
8964
8965                while (icon.hasNext()) {
8966                    SensorIcon i = icon.next();
8967
8968                    if (nb.equals(i.getSensor())) {
8969                        icon.remove();
8970                        super.removeFromContents(i);
8971                    }
8972                }
8973                setDirty();
8974                redrawPanel();
8975            }
8976
8977            if (nb instanceof Memory) {
8978                Iterator<MemoryIcon> icon = memoryLabelList.iterator();
8979
8980                while (icon.hasNext()) {
8981                    MemoryIcon i = icon.next();
8982
8983                    if (nb.equals(i.getMemory())) {
8984                        icon.remove();
8985                        super.removeFromContents(i);
8986                    }
8987                }
8988            }
8989
8990            if (nb instanceof GlobalVariable) {
8991                Iterator<GlobalVariableIcon> icon = globalVariableLabelList.iterator();
8992
8993                while (icon.hasNext()) {
8994                    GlobalVariableIcon i = icon.next();
8995
8996                    if (nb.equals(i.getGlobalVariable())) {
8997                        icon.remove();
8998                        super.removeFromContents(i);
8999                    }
9000                }
9001            }
9002        }
9003    }
9004
9005    @Override
9006    public void dispose() {
9007        if (leToolBarPanel != null) {
9008            leToolBarPanel.dispose();
9009        }
9010        super.dispose();
9011
9012    }
9013
9014    // package protected
9015    class TurnoutComboBoxPopupMenuListener implements PopupMenuListener {
9016
9017        private final NamedBeanComboBox<Turnout> comboBox;
9018        private final List<Turnout> currentTurnouts;
9019
9020        public TurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
9021            this.comboBox = comboBox;
9022            this.currentTurnouts = currentTurnouts;
9023        }
9024
9025        @Override
9026        public void popupMenuWillBecomeVisible(PopupMenuEvent event) {
9027            // This method is called before the popup menu becomes visible.
9028            log.debug("PopupMenuWillBecomeVisible");
9029            Set<Turnout> l = new HashSet<>();
9030            comboBox.getManager().getNamedBeanSet().forEach((turnout) -> {
9031                if (!currentTurnouts.contains(turnout)) {
9032                    if (!validatePhysicalTurnout(turnout.getDisplayName(), null)) {
9033                        l.add(turnout);
9034                    }
9035                }
9036            });
9037            comboBox.setExcludedItems(l);
9038        }
9039
9040        @Override
9041        public void popupMenuWillBecomeInvisible(PopupMenuEvent event) {
9042            // This method is called before the popup menu becomes invisible
9043            log.debug("PopupMenuWillBecomeInvisible");
9044        }
9045
9046        @Override
9047        public void popupMenuCanceled(PopupMenuEvent event) {
9048            // This method is called when the popup menu is canceled
9049            log.debug("PopupMenuCanceled");
9050        }
9051    }
9052
9053    /**
9054     * Create a listener that will exclude turnouts that are present in the
9055     * current panel.
9056     *
9057     * @param comboBox The NamedBeanComboBox that contains the turnout list.
9058     * @return A PopupMenuListener
9059     */
9060    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox) {
9061        return new TurnoutComboBoxPopupMenuListener(comboBox, new ArrayList<>());
9062    }
9063
9064    /**
9065     * Create a listener that will exclude turnouts that are present in the
9066     * current panel. The list of current turnouts are not excluded.
9067     *
9068     * @param comboBox        The NamedBeanComboBox that contains the turnout
9069     *                        list.
9070     * @param currentTurnouts The turnouts to be left in the turnout list.
9071     * @return A PopupMenuListener
9072     */
9073    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
9074        return new TurnoutComboBoxPopupMenuListener(comboBox, currentTurnouts);
9075    }
9076
9077    List<NamedBeanUsageReport> usageReport;
9078
9079    @Override
9080    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
9081        usageReport = new ArrayList<>();
9082        if (bean != null) {
9083            usageReport = super.getUsageReport(bean);
9084
9085            // LE Specific checks
9086            // Turnouts
9087            findTurnoutUsage(bean);
9088
9089            // Check A, EB, EC for sensors, masts, heads
9090            findPositionalUsage(bean);
9091
9092            // Level Crossings
9093            findXingWhereUsed(bean);
9094
9095            // Track segments
9096            findSegmentWhereUsed(bean);
9097        }
9098        return usageReport;
9099    }
9100
9101    void findTurnoutUsage(NamedBean bean) {
9102        for (LayoutTurnout turnout : getLayoutTurnoutsAndSlips()) {
9103            String data = getUsageData(turnout);
9104
9105            if (bean.equals(turnout.getTurnout())) {
9106                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout", data));
9107            }
9108            if (bean.equals(turnout.getSecondTurnout())) {
9109                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout2", data));
9110            }
9111
9112            if (isLBLockUsed(bean, turnout.getLayoutBlock())) {
9113                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9114            }
9115            if (turnout.hasEnteringDoubleTrack()) {
9116                if (isLBLockUsed(bean, turnout.getLayoutBlockB())) {
9117                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9118                }
9119                if (isLBLockUsed(bean, turnout.getLayoutBlockC())) {
9120                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9121                }
9122                if (isLBLockUsed(bean, turnout.getLayoutBlockD())) {
9123                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
9124                }
9125            }
9126
9127            if (bean.equals(turnout.getSensorA())) {
9128                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9129            }
9130            if (bean.equals(turnout.getSensorB())) {
9131                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9132            }
9133            if (bean.equals(turnout.getSensorC())) {
9134                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9135            }
9136            if (bean.equals(turnout.getSensorD())) {
9137                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
9138            }
9139
9140            if (bean.equals(turnout.getSignalAMast())) {
9141                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9142            }
9143            if (bean.equals(turnout.getSignalBMast())) {
9144                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9145            }
9146            if (bean.equals(turnout.getSignalCMast())) {
9147                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9148            }
9149            if (bean.equals(turnout.getSignalDMast())) {
9150                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
9151            }
9152
9153            if (bean.equals(turnout.getSignalA1())) {
9154                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9155            }
9156            if (bean.equals(turnout.getSignalA2())) {
9157                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9158            }
9159            if (bean.equals(turnout.getSignalA3())) {
9160                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9161            }
9162            if (bean.equals(turnout.getSignalB1())) {
9163                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9164            }
9165            if (bean.equals(turnout.getSignalB2())) {
9166                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9167            }
9168            if (bean.equals(turnout.getSignalC1())) {
9169                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9170            }
9171            if (bean.equals(turnout.getSignalC2())) {
9172                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9173            }
9174            if (bean.equals(turnout.getSignalD1())) {
9175                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9176            }
9177            if (bean.equals(turnout.getSignalD2())) {
9178                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
9179            }
9180        }
9181    }
9182
9183    void findPositionalUsage(NamedBean bean) {
9184        for (PositionablePoint point : getPositionablePoints()) {
9185            String data = getUsageData(point);
9186            if (bean.equals(point.getEastBoundSensor())) {
9187                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
9188            }
9189            if (bean.equals(point.getWestBoundSensor())) {
9190                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
9191            }
9192            if (bean.equals(point.getEastBoundSignalHead())) {
9193                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
9194            }
9195            if (bean.equals(point.getWestBoundSignalHead())) {
9196                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
9197            }
9198            if (bean.equals(point.getEastBoundSignalMast())) {
9199                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
9200            }
9201            if (bean.equals(point.getWestBoundSignalMast())) {
9202                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
9203            }
9204        }
9205    }
9206
9207    void findSegmentWhereUsed(NamedBean bean) {
9208        for (TrackSegment segment : getTrackSegments()) {
9209            if (isLBLockUsed(bean, segment.getLayoutBlock())) {
9210                String data = getUsageData(segment);
9211                usageReport.add(new NamedBeanUsageReport("LayoutEditorSegmentBlock", data));
9212            }
9213        }
9214    }
9215
9216    void findXingWhereUsed(NamedBean bean) {
9217        for (LevelXing xing : getLevelXings()) {
9218            String data = getUsageData(xing);
9219            if (isLBLockUsed(bean, xing.getLayoutBlockAC())) {
9220                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
9221            }
9222            if (isLBLockUsed(bean, xing.getLayoutBlockBD())) {
9223                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
9224            }
9225            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTA)) {
9226                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9227            }
9228            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTB)) {
9229                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9230            }
9231            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTC)) {
9232                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9233            }
9234            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTD)) {
9235                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
9236            }
9237        }
9238    }
9239
9240    String getUsageData(LayoutTrack track) {
9241        LayoutTrackView trackView = getLayoutTrackView(track);
9242        Point2D point = trackView.getCoordsCenter();
9243        if (trackView instanceof TrackSegmentView) {
9244            TrackSegmentView segmentView = (TrackSegmentView) trackView;
9245            point = new Point2D.Double(segmentView.getCentreSegX(), segmentView.getCentreSegY());
9246        }
9247        return String.format("%s :: x=%d, y=%d",
9248                track.getClass().getSimpleName(),
9249                Math.round(point.getX()),
9250                Math.round(point.getY()));
9251    }
9252
9253    boolean isLBLockUsed(NamedBean bean, LayoutBlock lblock) {
9254        boolean result = false;
9255        if (lblock != null) {
9256            if (bean.equals(lblock.getBlock())) {
9257                result = true;
9258            }
9259        }
9260        return result;
9261    }
9262
9263    boolean isUsedInXing(NamedBean bean, LevelXing xing, LevelXing.Geometry point) {
9264        boolean result = false;
9265        if (bean.equals(xing.getSensor(point))) {
9266            result = true;
9267        }
9268        if (bean.equals(xing.getSignalHead(point))) {
9269            result = true;
9270        }
9271        if (bean.equals(xing.getSignalMast(point))) {
9272            result = true;
9273        }
9274        return result;
9275    }
9276
9277    // initialize logging
9278    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutEditor.class);
9279}