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