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