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