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