java/src/jmri/jmrit/display/Editor.java

Summary

Maintainability
F
3 wks
Test Coverage
F
46%
package jmri.jmrit.display;

import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;

import javax.annotation.Nonnull;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionEvent;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import jmri.*;
import jmri.jmrit.catalog.CatalogPanel;
import jmri.jmrit.catalog.DirectorySearcher;
import jmri.jmrit.catalog.ImageIndexEditor;
import jmri.jmrit.catalog.NamedIcon;
import jmri.jmrit.display.controlPanelEditor.shape.PositionableShape;
import jmri.jmrit.logixng.*;
import jmri.jmrit.logixng.tools.swing.DeleteBean;
import jmri.jmrit.logixng.tools.swing.LogixNGEditor;
import jmri.jmrit.operations.trains.TrainIcon;
import jmri.jmrit.picker.PickListModel;
import jmri.jmrit.roster.Roster;
import jmri.jmrit.roster.RosterEntry;
import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
import jmri.util.DnDStringImportHandler;
import jmri.util.JmriJFrame;
import jmri.util.swing.JmriColorChooser;
import jmri.util.swing.JmriJOptionPane;
import jmri.util.swing.JmriMouseEvent;
import jmri.util.swing.JmriMouseListener;
import jmri.util.swing.JmriMouseMotionListener;

/**
 * This is the Model and a Controller for panel editor Views. (Panel Editor,
 * Layout Editor or any subsequent editors) The Model is simply a list of
 * Positionable objects added to a "target panel". Control of the display
 * attributes of the Positionable objects is done here. However, control of
 * mouse events is passed to the editor views, so control is also done by the
 * editor views.
 * <p>
 * The "contents" List keeps track of all the objects added to the target frame
 * for later manipulation. This class only locates and moves "target panel"
 * items, and does not control their appearance - that is left for the editor
 * views.
 * <p>
 * The Editor has tri-state "flags" to control the display of Positionable
 * object attributes globally - i.e. "on" or "off" for all - or as a third
 * state, permits the display control "locally" by corresponding flags in each
 * Positionable object
 * <p>
 * The title of the target and the editor panel are kept consistent via the
 * {#setTitle} method.
 * <p>
 * Mouse events are initial handled here, rather than in the individual
 * displayed objects, so that selection boxes for moving multiple objects can be
 * provided.
 * <p>
 * This class also implements an effective ToolTipManager replacement, because
 * the standard Swing one can't deal with the coordinate changes used to zoom a
 * panel. It works by controlling the contents of the _tooltip instance
 * variable, and triggering repaint of the target window when the tooltip
 * changes. The window painting then explicitly draws the tooltip for the
 * underlying object.
 *
 * @author Bob Jacobsen Copyright: Copyright (c) 2002, 2003, 2007
 * @author Dennis Miller 2004
 * @author Howard G. Penny Copyright: Copyright (c) 2005
 * @author Matthew Harris Copyright: Copyright (c) 2009
 * @author Pete Cressman Copyright: Copyright (c) 2009, 2010, 2011
 *
 */
abstract public class Editor extends JmriJFrame implements JmriMouseListener, JmriMouseMotionListener,
        ActionListener, KeyListener, VetoableChangeListener {

    final public static int BKG = 1;
    final public static int TEMP = 2;
    final public static int ICONS = 3;
    final public static int LABELS = 4;
    final public static int MEMORIES = 5;
    final public static int REPORTERS = 5;
    final public static int SECURITY = 6;
    final public static int TURNOUTS = 7;
    final public static int LIGHTS = 8;
    final public static int SIGNALS = 9;
    final public static int SENSORS = 10;
    final public static int CLOCK = 10;
    final public static int MARKERS = 10;
    final public static int NUM_LEVELS = 10;

    final public static int SCROLL_NONE = 0;
    final public static int SCROLL_BOTH = 1;
    final public static int SCROLL_HORIZONTAL = 2;
    final public static int SCROLL_VERTICAL = 3;

    final public static Color HIGHLIGHT_COLOR = new Color(204, 207, 88);

    public static final String POSITIONABLE_FLAVOR = DataFlavor.javaJVMLocalObjectMimeType
            + ";class=jmri.jmrit.display.Positionable";

    private boolean _loadFailed = false;

    private ArrayList<Positionable> _contents = new ArrayList<>();
    private Map<String, Positionable> _idContents = new HashMap<>();
    private Map<String, Set<Positionable>> _classContents = new HashMap<>();
    protected JLayeredPane _targetPanel;
    private JFrame _targetFrame;
    private JScrollPane _panelScrollPane;

    // Option menu items
    protected int _scrollState = SCROLL_NONE;
    protected boolean _editable = true;
    private boolean _positionable = true;
    private boolean _controlLayout = true;
    private boolean _showHidden = true;
    private boolean _showToolTip = true;
//    private boolean _showCoordinates = true;

    final public static int OPTION_POSITION = 1;
    final public static int OPTION_CONTROLS = 2;
    final public static int OPTION_HIDDEN = 3;
    final public static int OPTION_TOOLTIP = 4;
//    final public static int OPTION_COORDS = 5;

    private boolean _globalSetsLocal = true;    // pre 2.9.6 behavior
    private boolean _useGlobalFlag = false;     // pre 2.9.6 behavior

    // mouse methods variables
    protected int _lastX;
    protected int _lastY;
    BasicStroke DASHED_LINE = new BasicStroke(1f, BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_BEVEL,
            10f, new float[]{10f, 10f}, 0f);

    protected Rectangle _selectRect = null;
    protected Rectangle _highlightcomponent = null;
    protected boolean _dragging = false;
    protected ArrayList<Positionable> _selectionGroup = null;  // items gathered inside fence

    protected Positionable _currentSelection;
    private ToolTip _defaultToolTip;
    private ToolTip _tooltip = null;

    // Accessible to editor views
    protected int xLoc = 0;     // x coord of selected Positionable
    protected int yLoc = 0;     // y coord of selected Positionable
    protected int _anchorX;     // x coord when mousePressed
    protected int _anchorY;     // y coord when mousePressed

//    private boolean delayedPopupTrigger = false; // Used to delay the request of a popup, on a mouse press as this may conflict with a drag event
    protected double _paintScale = 1.0;   // scale for _targetPanel drawing

    protected Color defaultBackgroundColor = Color.lightGray;
    protected boolean _pastePending = false;

    // map of icon editor frames (incl, icon editor) keyed by name
    protected HashMap<String, JFrameItem> _iconEditorFrame = new HashMap<>();

    // store panelMenu state so preference is retained on headless systems
    private boolean panelMenuIsVisible = true;

    private boolean _inEditInlineLogixNGMode = false;
    private LogixNGEditor _inlineLogixNGEdit;

    public Editor() {
    }

    public Editor(String name, boolean saveSize, boolean savePosition) {
        super(name, saveSize, savePosition);
        setName(name);
        _defaultToolTip = new ToolTip(null, 0, 0, null);
        setVisible(false);
        InstanceManager.getDefault(SignalHeadManager.class).addVetoableChangeListener(this);
        InstanceManager.getDefault(SignalMastManager.class).addVetoableChangeListener(this);
        InstanceManager.turnoutManagerInstance().addVetoableChangeListener(this);
        InstanceManager.sensorManagerInstance().addVetoableChangeListener(this);
        InstanceManager.memoryManagerInstance().addVetoableChangeListener(this);
        InstanceManager.getDefault(BlockManager.class).addVetoableChangeListener(this);
        InstanceManager.getDefault(EditorManager.class).add(this);
    }

    public Editor(String name) {
        this(name, true, true);
    }

    /**
     * Set <strong>white</strong> as the default background color for panels created using the <strong>New Panel</strong> menu item.
     * Overriden by LE to use a different default background color and set other initial defaults.
     */
    public void newPanelDefaults() {
        setBackgroundColor(Color.WHITE);
    }

    public void loadFailed() {
        _loadFailed = true;
    }

    NamedIcon _newIcon;
    boolean _ignore = false;
    boolean _delete;
    HashMap<String, String> _urlMap = new HashMap<>();

    public NamedIcon loadFailed(String msg, String url) {
        log.debug("loadFailed _ignore= {} {}", _ignore, msg);
        if (_urlMap == null) {
            _urlMap = new HashMap<>();
        }
        String goodUrl = _urlMap.get(url);
        if (goodUrl != null) {
            return NamedIcon.getIconByName(goodUrl);
        }
        if (_ignore) {
            _loadFailed = true;
            return NamedIcon.getIconByName(url);
        }
        _newIcon = null;
        _delete = false;
        (new UrlErrorDialog(msg, url)).setVisible(true);

        if (_delete) {
            return null;
        }
        if (_newIcon == null) {
            _loadFailed = true;
            _newIcon = NamedIcon.getIconByName(url);
        }
        return _newIcon;
    }

    public class UrlErrorDialog extends JDialog {

        JTextField _urlField;
        CatalogPanel _catalog;
        String _badUrl;

        UrlErrorDialog(String msg, String url) {
            super(_targetFrame, Bundle.getMessage("BadIcon"), true);
            _badUrl = url;
            JPanel content = new JPanel();
            JPanel panel = new JPanel();
            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
            panel.add(Box.createVerticalStrut(10));
            panel.add(new JLabel(MessageFormat.format(Bundle.getMessage("IconUrlError"), msg)));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt1")));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt1A")));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt1B")));
            panel.add(Box.createVerticalStrut(10));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt2", Bundle.getMessage("ButtonContinue"))));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt3", Bundle.getMessage("ButtonDelete"))));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt3A")));
            panel.add(Box.createVerticalStrut(10));
            panel.add(new JLabel(Bundle.getMessage("UrlErrorPrompt4", Bundle.getMessage("ButtonIgnore"))));
            panel.add(Box.createVerticalStrut(10));
            _urlField = new JTextField(url);
            _urlField.setDragEnabled(true);
            _urlField.setTransferHandler(new DnDStringImportHandler());
            panel.add(_urlField);
            panel.add(makeDoneButtonPanel());
            _urlField.setToolTipText(Bundle.getMessage("TooltipFixUrl"));
            panel.setToolTipText(Bundle.getMessage("TooltipFixUrl"));
            _catalog = CatalogPanel.makeDefaultCatalog();
            _catalog.setToolTipText(Bundle.getMessage("ToolTipDragIconToText"));
            panel.add(_catalog);
            content.add(panel);
            setContentPane(content);
            setLocation(200, 100);
            pack();
        }

        protected JPanel makeDoneButtonPanel() {
            JPanel result = new JPanel();
            result.setLayout(new FlowLayout());
            JButton doneButton = new JButton(Bundle.getMessage("ButtonContinue"));
            doneButton.addActionListener(a -> {
                _newIcon = NamedIcon.getIconByName(_urlField.getText());
                if (_newIcon != null) {
                    _urlMap.put(_badUrl, _urlField.getText());
                }
                dispose();
            });
            doneButton.setToolTipText(Bundle.getMessage("TooltipContinue"));
            result.add(doneButton);

            JButton deleteButton = new JButton(Bundle.getMessage("ButtonDelete"));
            deleteButton.addActionListener(a -> {
                _delete = true;
                dispose();
            });
            result.add(deleteButton);
            deleteButton.setToolTipText(Bundle.getMessage("TooltipDelete"));

            JButton cancelButton = new JButton(Bundle.getMessage("ButtonIgnore"));
            cancelButton.addActionListener(a -> {
                _ignore = true;
                dispose();
            });
            result.add(cancelButton);
            cancelButton.setToolTipText(Bundle.getMessage("TooltipIgnore"));
            return result;
        }
    }

    public void disposeLoadData() {
        _urlMap = null;
    }

    public boolean loadOK() {
        return !_loadFailed;
    }

    public List<Positionable> getContents() {
        return Collections.unmodifiableList(_contents);
    }

    public Map<String, Positionable> getIdContents() {
        return Collections.unmodifiableMap(_idContents);
    }

    public Set<String> getClassNames() {
        return Collections.unmodifiableSet(_classContents.keySet());
    }

    public Set<Positionable> getPositionablesByClassName(String className) {
        Set<Positionable> set = _classContents.get(className);
        if (set == null) return null;
        return Collections.unmodifiableSet(set);
    }

    public void setDefaultToolTip(ToolTip dtt) {
        _defaultToolTip = dtt;
    }

    //
    // *************** setting the main panel and frame ***************
    //
    /**
     * Set the target panel.
     * <p>
     * An Editor may or may not choose to use 'this' as its frame or the
     * interior class 'TargetPane' for its targetPanel.
     *
     * @param targetPanel the panel to be edited
     * @param frame       the frame to embed the panel in
     */
    protected void setTargetPanel(JLayeredPane targetPanel, JmriJFrame frame) {
        if (targetPanel == null) {
            _targetPanel = new TargetPane();
        } else {
            _targetPanel = targetPanel;
        }
        // If on a headless system, set heavyweight components to null
        // and don't attach mouse and keyboard listeners to the panel
        if (GraphicsEnvironment.isHeadless()) {
            _panelScrollPane = null;
            _targetFrame = null;
            return;
        }
        if (frame == null) {
            _targetFrame = this;
        } else {
            _targetFrame = frame;
        }
        _targetFrame.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
        _panelScrollPane = new JScrollPane(_targetPanel);
        Container contentPane = _targetFrame.getContentPane();
        contentPane.add(_panelScrollPane);
        _targetFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                targetWindowClosingEvent(e);
            }
        });
        _targetPanel.addMouseListener(JmriMouseListener.adapt(this));
        _targetPanel.addMouseMotionListener(JmriMouseMotionListener.adapt(this));
        _targetPanel.setFocusable(true);
        _targetPanel.addKeyListener(this);
        //_targetFrame.pack();
    }

    protected void setTargetPanelSize(int w, int h) {
//        log.debug("setTargetPanelSize now w={}, h={}", w, h);
        _targetPanel.setSize(w, h);
        _targetPanel.invalidate();
    }

    protected Dimension getTargetPanelSize() {
        return _targetPanel.getSize();
    }

    /**
     * Allow public access to the target (content) panel for external
     * modification, particularly from scripts.
     *
     * @return the target panel
     */
    public final JComponent getTargetPanel() {
        return _targetPanel;
    }

    /**
     * Allow public access to the scroll pane for external control of position,
     * particularly from scripts.
     *
     * @return the scroll pane containing the target panel
     */
    public final JScrollPane getPanelScrollPane() {
        return _panelScrollPane;
    }

    public final JFrame getTargetFrame() {
        return _targetFrame;
    }

    public Color getBackgroundColor() {
        if (_targetPanel instanceof TargetPane) {
            TargetPane tmp = (TargetPane) _targetPanel;
            return tmp.getBackgroundColor();
        } else {
            return null;
        }
    }

    public void setBackgroundColor(Color col) {
        if (_targetPanel instanceof TargetPane) {
            TargetPane tmp = (TargetPane) _targetPanel;
            tmp.setBackgroundColor(col);
        }
        JmriColorChooser.addRecentColor(col);
    }

    public void clearBackgroundColor() {
        if (_targetPanel instanceof TargetPane) {
            TargetPane tmp = (TargetPane) _targetPanel;
            tmp.clearBackgroundColor();
        }
    }

    /**
     * Get scale for TargetPane drawing.
     *
     * @return the scale
     */
    public final double getPaintScale() {
        return _paintScale;
    }

    protected final void setPaintScale(double newScale) {
        double ratio = newScale / _paintScale;
        _paintScale = newScale;
        setScrollbarScale(ratio);
    }

    ToolTipTimer _tooltipTimer;

    protected void setToolTip(ToolTip tt) {
        if (tt != null) {
            var pos = tt.getPositionable();
            if (pos != null) {  // LE turnout tooltips do not have a Positionable
                if (pos.isHidden() && !isEditable()) {
                    // Skip hidden objects
                    return;
                }
            }
        }

        if (tt == null) {
            _tooltip = null;
            if (_tooltipTimer != null) {
                _tooltipTimer.stop();
                _tooltipTimer = null;
                _targetPanel.repaint();
            }

        } else if (_tooltip == null && _tooltipTimer == null) {
            log.debug("start :: tt = {}, tooltip = {}, timer = {}", tt, _tooltip, _tooltipTimer);
            _tooltipTimer = new ToolTipTimer(TOOLTIPSHOWDELAY, this, tt);
            _tooltipTimer.setRepeats(false);
            _tooltipTimer.start();
        }
    }

    static int TOOLTIPSHOWDELAY = 1000; // msec
    static int TOOLTIPDISMISSDELAY = 4000;  // msec

    /*
     * Wait TOOLTIPSHOWDELAY then show tooltip. Wait TOOLTIPDISMISSDELAY and
     * disappear.
     */
    @Override
    public void actionPerformed(ActionEvent event) {
        //log.debug("_tooltipTimer actionPerformed: Timer on= {}", (_tooltipTimer!=null));
        if (_tooltipTimer != null) {
            _tooltip = _tooltipTimer.getToolTip();
            _tooltipTimer.stop();
        }
        if (_tooltip != null) {
            _tooltipTimer = new ToolTipTimer(TOOLTIPDISMISSDELAY, this, null);
            _tooltipTimer.setRepeats(false);
            _tooltipTimer.start();
        } else {
            _tooltipTimer = null;
        }
        _targetPanel.repaint();
    }

    static class ToolTipTimer extends Timer {

        ToolTip tooltip;

        ToolTipTimer(int delay, ActionListener listener, ToolTip tip) {
            super(delay, listener);
            tooltip = tip;
        }

        ToolTip getToolTip() {
            return tooltip;
        }
    }

    /**
     * Special internal class to allow drawing of layout to a JLayeredPane. This
     * is the 'target' pane where the layout is displayed.
     */
    public class TargetPane extends JLayeredPane {

        int h = 100;
        int w = 150;

        public TargetPane() {
            setLayout(null);
        }

        @Override
        public void setSize(int width, int height) {
//            log.debug("size now w={}, h={}", width, height);
            this.h = height;
            this.w = width;
            super.setSize(width, height);
        }

        @Override
        public Dimension getSize() {
            return new Dimension(w, h);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(w, h);
        }

        @Override
        public Dimension getMinimumSize() {
            return getPreferredSize();
        }

        @Override
        public Dimension getMaximumSize() {
            return getPreferredSize();
        }

        @Override
        public Component add(@Nonnull Component c, int i) {
            int hnew = Math.max(this.h, c.getLocation().y + c.getSize().height);
            int wnew = Math.max(this.w, c.getLocation().x + c.getSize().width);
            if (hnew > h || wnew > w) {
//                log.debug("size was {},{} - i ={}", w, h, i);
                setSize(wnew, hnew);
            }
            return super.add(c, i);
        }

        @Override
        public void add(@Nonnull Component c, Object o) {
            super.add(c, o);
            int hnew = Math.max(h, c.getLocation().y + c.getSize().height);
            int wnew = Math.max(w, c.getLocation().x + c.getSize().width);
            if (hnew > h || wnew > w) {
                // log.debug("adding of {} with Object - i=", c.getSize(), o);
                setSize(wnew, hnew);
            }
        }

        private Color _highlightColor = HIGHLIGHT_COLOR;
        private Color _selectGroupColor = HIGHLIGHT_COLOR;
        private Color _selectRectColor = Color.red;
        private transient Stroke _selectRectStroke = DASHED_LINE;

        public void setHighlightColor(Color color) {
            _highlightColor = color;
        }

        public Color getHighlightColor() {
            return _highlightColor;
        }

        public void setSelectGroupColor(Color color) {
            _selectGroupColor = color;
        }

        public void setSelectRectColor(Color color) {
            _selectRectColor = color;
        }

        public void setSelectRectStroke(Stroke stroke) {
            _selectRectStroke = stroke;
        }

        public void setDefaultColors() {
            _highlightColor = HIGHLIGHT_COLOR;
            _selectGroupColor = HIGHLIGHT_COLOR;
            _selectRectColor = Color.red;
            _selectRectStroke = DASHED_LINE;
        }

        @Override
        public void paint(Graphics g) {
            Graphics2D g2d = null;
            if (g instanceof Graphics2D) {
                g2d = (Graphics2D) g;
                g2d.scale(_paintScale, _paintScale);
            }
            super.paint(g);

            Stroke stroke = new BasicStroke();
            if (g2d != null) {
                stroke = g2d.getStroke();
            }
            Color color = g.getColor();
            if (_selectRect != null) {
                //Draw a rectangle on top of the image.
                if (g2d != null) {
                    g2d.setStroke(_selectRectStroke);
                }
                g.setColor(_selectRectColor);
                g.drawRect(_selectRect.x, _selectRect.y, _selectRect.width, _selectRect.height);
            }
            if (_selectionGroup != null) {
                g.setColor(_selectGroupColor);
                if (g2d != null) {
                    g2d.setStroke(new BasicStroke(2.0f));
                }
                for (Positionable p : _selectionGroup) {
                    if (p != null) {
                        if (!(p instanceof PositionableShape)) {
                            g.drawRect(p.getX(), p.getY(), p.maxWidth(), p.maxHeight());
                        } else {
                            PositionableShape s = (PositionableShape) p;
                            s.drawHandles();
                        }
                    }
                }
            }
            //Draws a border around the highlighted component
            if (_highlightcomponent != null) {
                g.setColor(_highlightColor);
                if (g2d != null) {
                    g2d.setStroke(new BasicStroke(2.0f));
                }
                g.drawRect(_highlightcomponent.x, _highlightcomponent.y,
                        _highlightcomponent.width, _highlightcomponent.height);
            }
            paintTargetPanel(g);

            g.setColor(color);
            if (g2d != null) {
                g2d.setStroke(stroke);
            }
            if (_tooltip != null) {
                _tooltip.paint(g2d, _paintScale);
            }
        }

        public void setBackgroundColor(Color col) {
            setBackground(col);
            setOpaque(true);
            JmriColorChooser.addRecentColor(col);
        }

        public void clearBackgroundColor() {
            setOpaque(false);
        }

        public Color getBackgroundColor() {
            if (isOpaque()) {
                return getBackground();
            }
            return null;
        }
    }

    private void setScrollbarScale(double ratio) {
        //resize the panel to reflect scaling
        Dimension dim = _targetPanel.getSize();
        int tpWidth = (int) ((dim.width) * ratio);
        int tpHeight = (int) ((dim.height) * ratio);
        _targetPanel.setSize(tpWidth, tpHeight);
        log.debug("setScrollbarScale: ratio= {}, tpWidth= {}, tpHeight= {}", ratio, tpWidth, tpHeight);
        // compute new scroll bar positions to keep upper left same
        JScrollBar horScroll = _panelScrollPane.getHorizontalScrollBar();
        JScrollBar vertScroll = _panelScrollPane.getVerticalScrollBar();
        int hScroll = (int) (horScroll.getValue() * ratio);
        int vScroll = (int) (vertScroll.getValue() * ratio);
        // set scrollbars maximum range (otherwise setValue may fail);
        horScroll.setMaximum((int) ((horScroll.getMaximum()) * ratio));
        vertScroll.setMaximum((int) ((vertScroll.getMaximum()) * ratio));
        // set scroll bar positions
        horScroll.setValue(hScroll);
        vertScroll.setValue(vScroll);
    }

    /*
     * ********************** Options setup *********************
     */
    /**
     * Control whether target panel items are editable. Does this by invoke the
     * {@link Positionable#setEditable(boolean)} function of each item on the
     * target panel. This also controls the relevant pop-up menu items (which
     * are the primary way that items are edited).
     *
     * @param state true for editable.
     */
    public void setAllEditable(boolean state) {
        _editable = state;
        for (Positionable _content : _contents) {
            _content.setEditable(state);
        }
        if (!_editable) {
            _highlightcomponent = null;
            deselectSelectionGroup();
        }
    }

    public void deselectSelectionGroup() {
        if (_selectionGroup == null) {
            return;
        }
        for (Positionable p : _selectionGroup) {
            if (p instanceof PositionableShape) {
                PositionableShape s = (PositionableShape) p;
                s.removeHandles();
            }
        }
        _selectionGroup = null;
    }

    // accessor routines for persistent information
    public boolean isEditable() {
        return _editable;
    }

    /**
     * Set which flag should be used, global or local for Positioning and
     * Control of individual items. Items call getFlag() to return the
     * appropriate flag it should use.
     *
     * @param set True if global flags should be used for positioning.
     */
    public void setUseGlobalFlag(boolean set) {
        _useGlobalFlag = set;
    }

    public boolean useGlobalFlag() {
        return _useGlobalFlag;
    }

    /**
     * Get the setting for the specified option.
     *
     * @param whichOption The option to get
     * @param localFlag   is the current setting of the item
     * @return The setting for the option
     */
    public boolean getFlag(int whichOption, boolean localFlag) {
        //log.debug("getFlag Option= {}, _useGlobalFlag={} localFlag={}", whichOption, _useGlobalFlag, localFlag);
        if (_useGlobalFlag) {
            switch (whichOption) {
                case OPTION_POSITION:
                    return _positionable;
                case OPTION_CONTROLS:
                    return _controlLayout;
                case OPTION_HIDDEN:
                    return _showHidden;
                case OPTION_TOOLTIP:
                    return _showToolTip;
//                case OPTION_COORDS:
//                    return _showCoordinates;
                default:
                    log.warn("Unhandled which option code: {}", whichOption);
                    break;
            }
        }
        return localFlag;
    }

    /**
     * Set if {@link #setAllControlling(boolean)} and
     * {@link #setAllPositionable(boolean)} are set for existing as well as new
     * items.
     *
     * @param set true if setAllControlling() and setAllPositionable() are set
     *            for existing items
     */
    public void setGlobalSetsLocalFlag(boolean set) {
        _globalSetsLocal = set;
    }

    /**
     * Control whether panel items can be positioned. Markers can always be
     * positioned.
     *
     * @param state true to set all items positionable; false otherwise
     */
    public void setAllPositionable(boolean state) {
        _positionable = state;
        if (_globalSetsLocal) {
            for (Positionable p : _contents) {
                // don't allow backgrounds to be set positionable by global flag
                if (!state || p.getDisplayLevel() != BKG) {
                    p.setPositionable(state);
                }
            }
        }
    }

    public boolean allPositionable() {
        return _positionable;
    }

    /**
     * Control whether target panel items are controlling layout items.
     * <p>
     * Does this by invoking the {@link Positionable#setControlling} function of
     * each item on the target panel. This also controls the relevant pop-up
     * menu items.
     *
     * @param state true for controlling.
     */
    public void setAllControlling(boolean state) {
        _controlLayout = state;
        if (_globalSetsLocal) {
            for (Positionable _content : _contents) {
                _content.setControlling(state);
            }
        }
    }

    public boolean allControlling() {
        return _controlLayout;
    }

    /**
     * Control whether target panel hidden items are visible or not. Does this
     * by invoke the {@link Positionable#setHidden} function of each item on the
     * target panel.
     *
     * @param state true for Visible.
     */
    public void setShowHidden(boolean state) {
        _showHidden = state;
        if (_showHidden) {
            for (Positionable _content : _contents) {
                _content.setVisible(true);
            }
        } else {
            for (Positionable _content : _contents) {
                _content.showHidden();
            }
        }
    }

    public boolean showHidden() {
        return _showHidden;
    }

    public void setAllShowToolTip(boolean state) {
        _showToolTip = state;
        for (Positionable _content : _contents) {
            _content.setShowToolTip(state);
        }
    }

    public boolean showToolTip() {
        return _showToolTip;
    }

    /*
     * Control whether target panel items will show their coordinates in their
     * popup menu.
     *
     * @param state true for show coordinates.
     */
 /*
     public void setShowCoordinates(boolean state) {
     _showCoordinates = state;
     for (int i = 0; i<_contents.size(); i++) {
     _contents.get(i).setViewCoordinates(state);
     }
     }
     public boolean showCoordinates() {
     return _showCoordinates;
     }
     */

    /**
     * Hide or show menus on the target panel.
     *
     * @param state true to show menus; false to hide menus
     * @since 3.9.5
     */
    public void setPanelMenuVisible(boolean state) {
        this.panelMenuIsVisible = state;
        if (!GraphicsEnvironment.isHeadless() && this._targetFrame != null) {
            _targetFrame.getJMenuBar().setVisible(state);
            this.revalidate();
        }
    }

    /**
     * Is the menu on the target panel shown?
     *
     * @return true if menu is visible
     * @since 3.9.5
     */
    public boolean isPanelMenuVisible() {
        if (!GraphicsEnvironment.isHeadless() && this._targetFrame != null) {
            this.panelMenuIsVisible = _targetFrame.getJMenuBar().isVisible();
        }
        return this.panelMenuIsVisible;
    }

    protected void setScroll(int state) {
        log.debug("setScroll {}", state);
        switch (state) {
            case SCROLL_NONE:
                _panelScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
                _panelScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
                break;
            case SCROLL_BOTH:
                _panelScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
                _panelScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
                break;
            case SCROLL_HORIZONTAL:
                _panelScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
                _panelScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
                break;
            case SCROLL_VERTICAL:
                _panelScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
                _panelScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
                break;
            default:
                log.warn("Unexpected  setScroll state of {}", state);
                break;
        }
        _scrollState = state;
    }

    public void setScroll(String strState) {
        int state = SCROLL_BOTH;
        if (strState.equalsIgnoreCase("none") || strState.equalsIgnoreCase("no")) {
            state = SCROLL_NONE;
        } else if (strState.equals("horizontal")) {
            state = SCROLL_HORIZONTAL;
        } else if (strState.equals("vertical")) {
            state = SCROLL_VERTICAL;
        }
        log.debug("setScroll: strState= {}, state= {}", strState, state);
        setScroll(state);
    }

    public String getScrollable() {
        String value = "";
        switch (_scrollState) {
            case SCROLL_NONE:
                value = "none";
                break;
            case SCROLL_BOTH:
                value = "both";
                break;
            case SCROLL_HORIZONTAL:
                value = "horizontal";
                break;
            case SCROLL_VERTICAL:
                value = "vertical";
                break;
            default:
                log.warn("Unexpected _scrollState of {}", _scrollState);
                break;
        }
        return value;
    }
    /*
     * *********************** end Options setup **********************
     */
    /*
     * Handle closing (actually hiding due to HIDE_ON_CLOSE) the target window.
     * <p>
     * The target window has been requested to close, don't delete it at this
     * time. Deletion must be accomplished via the Delete this panel menu item.
     */
    protected void targetWindowClosing() {
        String name = "Panel";
        Container ancestor = _targetPanel.getTopLevelAncestor();
        if (ancestor instanceof JFrame) {
            name = ((JFrame) ancestor).getTitle();
        }
        if (!InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()) {
            InstanceManager.getDefault(jmri.UserPreferencesManager.class).showInfoMessage(
                    Bundle.getMessage("PanelHideTitle"), Bundle.getMessage("PanelHideNotice", name),  // NOI18N
                    "jmri.jmrit.display.EditorManager", "skipHideDialog"); // NOI18N
            InstanceManager.getDefault(jmri.UserPreferencesManager.class).setPreferenceItemDetails(
                    "jmri.jmrit.display.EditorManager", "skipHideDialog", Bundle.getMessage("PanelHideSkip"));  // NOI18N
        }
    }

    protected Editor changeView(String className) {
        JFrame frame = getTargetFrame();

        try {
            Editor ed = (Editor) Class.forName(className).getDeclaredConstructor().newInstance();

            ed.setName(getName());
            ed.init(getName());

            ed._contents = new ArrayList<>(_contents);
            ed._idContents = new HashMap<>(_idContents);
            ed._classContents = new HashMap<>(_classContents);

            for (Positionable p : _contents) {
                p.setEditor(ed);
                ed.addToTarget(p);
                if (log.isDebugEnabled()) {
                    log.debug("changeView: {} addToTarget class= {}", p.getNameString(), p.getClass().getName());
                }
            }
            ed.setAllEditable(isEditable());
            //ed.setAllPositionable(allPositionable());
            //ed.setShowCoordinates(showCoordinates());
            ed.setAllShowToolTip(showToolTip());
            //ed.setAllControlling(allControlling());
            ed.setShowHidden(isVisible());
            ed.setPanelMenuVisible(frame.getJMenuBar().isVisible());
            ed.setScroll(getScrollable());
            ed.setTitle();
            ed.setBackgroundColor(getBackgroundColor());
            ed.getTargetFrame().setLocation(frame.getLocation());
            ed.getTargetFrame().setSize(frame.getSize());
            ed.setSize(getSize());
//            ed.pack();
            ed.setVisible(true);
            dispose();
            return ed;
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException cnfe) {
            log.error("changeView exception {}", cnfe.toString());
        }
        return null;
    }

    /*
     * *********************** Popup Item Methods **********************
     *
     * These methods are to be called from the editor view's showPopUp method
     */
    /**
     * Add a checkbox to lock the position of the Positionable item.
     *
     * @param p     the item
     * @param popup the menu to add the lock menu item to
     */
    public void setPositionableMenu(Positionable p, JPopupMenu popup) {
        JCheckBoxMenuItem lockItem = new JCheckBoxMenuItem(Bundle.getMessage("LockPosition"));
        lockItem.setSelected(!p.isPositionable());
        lockItem.addActionListener(new ActionListener() {
            Positionable comp;
            JCheckBoxMenuItem checkBox;

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.setPositionable(!checkBox.isSelected());
                setSelectionsPositionable(!checkBox.isSelected(), comp);
            }

            ActionListener init(Positionable pos, JCheckBoxMenuItem cb) {
                comp = pos;
                checkBox = cb;
                return this;
            }
        }.init(p, lockItem));
        popup.add(lockItem);
    }

    /**
     * Display the {@literal X & Y} coordinates of the Positionable item and
     * provide a dialog menu item to edit them.
     *
     * @param p     The item to add the menu item to
     * @param popup The menu item to add the action to
     * @return always returns true
     */
    public boolean setShowCoordinatesMenu(Positionable p, JPopupMenu popup) {
        //if (showCoordinates()) {
        JMenuItem edit;
        if ((p instanceof MemoryOrGVIcon) && (p.getPopupUtility().getFixedWidth() == 0)) {
            MemoryOrGVIcon pm = (MemoryOrGVIcon) p;

            edit = new JMenuItem(Bundle.getMessage(
                "EditLocationXY", pm.getOriginalX(), pm.getOriginalY()));

            edit.addActionListener(MemoryIconCoordinateEdit.getCoordinateEditAction(pm));
        } else {
            edit = new JMenuItem(Bundle.getMessage(
                "EditLocationXY", p.getX(), p.getY()));
            edit.addActionListener(CoordinateEdit.getCoordinateEditAction(p));
        }
        popup.add(edit);
        return true;
        //}
        //return false;
    }

    /**
     * Offer actions to align the selected Positionable items either
     * Horizontally (at average y coordinates) or Vertically (at average x
     * coordinates).
     *
     * @param p     The positionable item
     * @param popup The menu to add entries to
     * @return true if entries added to menu
     */
    public boolean setShowAlignmentMenu(Positionable p, JPopupMenu popup) {
        if (showAlignPopup(p)) {
            JMenu edit = new JMenu(Bundle.getMessage("EditAlignment"));
            edit.add(new AbstractAction(Bundle.getMessage("AlignX")) {
                int _x;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(_x, comp.getY());
                    }
                }

                AbstractAction init(int x) {
                    _x = x;
                    return this;
                }
            }.init(p.getX()));
            edit.add(new AbstractAction(Bundle.getMessage("AlignMiddleX")) {
                int _x;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(_x - comp.getWidth() / 2, comp.getY());
                    }
                }

                AbstractAction init(int x) {
                    _x = x;
                    return this;
                }
            }.init(p.getX() + p.getWidth() / 2));
            edit.add(new AbstractAction(Bundle.getMessage("AlignOtherX")) {
                int _x;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(_x - comp.getWidth(), comp.getY());
                    }
                }

                AbstractAction init(int x) {
                    _x = x;
                    return this;
                }
            }.init(p.getX() + p.getWidth()));
            edit.add(new AbstractAction(Bundle.getMessage("AlignY")) {
                int _y;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(comp.getX(), _y);
                    }
                }

                AbstractAction init(int y) {
                    _y = y;
                    return this;
                }
            }.init(p.getY()));
            edit.add(new AbstractAction(Bundle.getMessage("AlignMiddleY")) {
                int _y;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(comp.getX(), _y - comp.getHeight() / 2);
                    }
                }

                AbstractAction init(int y) {
                    _y = y;
                    return this;
                }
            }.init(p.getY() + p.getHeight() / 2));
            edit.add(new AbstractAction(Bundle.getMessage("AlignOtherY")) {
                int _y;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    for (Positionable comp : _selectionGroup) {
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(comp.getX(), _y - comp.getHeight());
                    }
                }

                AbstractAction init(int y) {
                    _y = y;
                    return this;
                }
            }.init(p.getY() + p.getHeight()));
            edit.add(new AbstractAction(Bundle.getMessage("AlignXFirst")) {

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    int x = _selectionGroup.get(0).getX();
                    for (int i = 1; i < _selectionGroup.size(); i++) {
                        Positionable comp = _selectionGroup.get(i);
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(x, comp.getY());
                    }
                }
            });
            edit.add(new AbstractAction(Bundle.getMessage("AlignYFirst")) {

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (_selectionGroup == null) {
                        return;
                    }
                    int y = _selectionGroup.get(0).getX();
                    for (int i = 1; i < _selectionGroup.size(); i++) {
                        Positionable comp = _selectionGroup.get(i);
                        if (!getFlag(OPTION_POSITION, comp.isPositionable())) {
                            continue;
                        }
                        comp.setLocation(comp.getX(), y);
                    }
                }
            });
            popup.add(edit);
            return true;
        }
        return false;
    }

    /**
     * Display 'z' level of the Positionable item and provide a dialog
     * menu item to edit it.
     *
     * @param p     The item
     * @param popup the menu to add entries to
     */
    public void setDisplayLevelMenu(Positionable p, JPopupMenu popup) {
        JMenuItem edit = new JMenuItem(Bundle.getMessage("EditLevel_", p.getDisplayLevel()));
        edit.addActionListener(CoordinateEdit.getLevelEditAction(p));
        popup.add(edit);
    }

    /**
     * Add a menu entry to set visibility of the Positionable item
     *
     * @param p     the item
     * @param popup the menu to add the entry to
     */
    public void setHiddenMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }
        JCheckBoxMenuItem hideItem = new JCheckBoxMenuItem(Bundle.getMessage("SetHidden"));
        hideItem.setSelected(p.isHidden());
        hideItem.addActionListener(new ActionListener() {
            Positionable comp;
            JCheckBoxMenuItem checkBox;

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.setHidden(checkBox.isSelected());
                setSelectionsHidden(checkBox.isSelected(), comp);
            }

            ActionListener init(Positionable pos, JCheckBoxMenuItem cb) {
                comp = pos;
                checkBox = cb;
                return this;
            }
        }.init(p, hideItem));
        popup.add(hideItem);
    }

    /**
     * Add a menu entry to set visibility of the Positionable item based on the presence of contents.
     * If the value is null or empty, the icon is not visible.
     * This is applicable to memory,  block content and LogixNG global variable labels.
     *
     * @param p     the item
     * @param popup the menu to add the entry to
     */
    public void setEmptyHiddenMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }
        if (p instanceof BlockContentsIcon || p instanceof MemoryIcon || p instanceof GlobalVariableIcon) {
            JCheckBoxMenuItem hideEmptyItem = new JCheckBoxMenuItem(Bundle.getMessage("SetEmptyHidden"));
            hideEmptyItem.setSelected(p.isEmptyHidden());
            hideEmptyItem.addActionListener(new ActionListener() {
                Positionable comp;
                JCheckBoxMenuItem checkBox;

                @Override
                public void actionPerformed(ActionEvent e) {
                    comp.setEmptyHidden(checkBox.isSelected());
                }

                ActionListener init(Positionable pos, JCheckBoxMenuItem cb) {
                    comp = pos;
                    checkBox = cb;
                    return this;
                }
            }.init(p, hideEmptyItem));
            popup.add(hideEmptyItem);
        }
    }

    /**
     * Add a menu entry to edit Id of the Positionable item
     *
     * @param p     the item
     * @param popup the menu to add the entry to
     */
    public void setEditIdMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }

        popup.add(CoordinateEdit.getIdEditAction(p, "EditId", this));
    }

    /**
     * Add a menu entry to edit Classes of the Positionable item
     *
     * @param p     the item
     * @param popup the menu to add the entry to
     */
    public void setEditClassesMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }

        popup.add(CoordinateEdit.getClassesEditAction(p, "EditClasses", this));
    }

    /**
     * Check if edit of a conditional is in progress.
     *
     * @return true if this is the case, after showing dialog to user
     */
    private boolean checkEditConditionalNG() {
        if (_inEditInlineLogixNGMode) {
            // Already editing a LogixNG, ask for completion of that edit
            JmriJOptionPane.showMessageDialog(null,
                    Bundle.getMessage("Error_InlineLogixNGInEditMode"), // NOI18N
                    Bundle.getMessage("ErrorTitle"), // NOI18N
                    JmriJOptionPane.ERROR_MESSAGE);
            _inlineLogixNGEdit.bringToFront();
            return true;
        }
        return false;
    }

    /**
     * Add a menu entry to edit Id of the Positionable item
     *
     * @param p     the item
     * @param popup the menu to add the entry to
     */
    public void setLogixNGPositionableMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }

        JMenu logixNG_Menu = new JMenu("LogixNG");
        popup.add(logixNG_Menu);

        logixNG_Menu.add(new AbstractAction(Bundle.getMessage("LogixNG_Inline")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (checkEditConditionalNG()) return;

                if (p.getLogixNG() == null) {
                    LogixNG logixNG = InstanceManager.getDefault(LogixNG_Manager.class)
                            .createLogixNG(null, true);
                    logixNG.setInlineLogixNG(p);
                    logixNG.activate();
                    logixNG.setEnabled(true);
                    logixNG.clearStartup();
                    p.setLogixNG(logixNG);
                }
                LogixNGEditor logixNGEditor = new LogixNGEditor(null, p.getLogixNG().getSystemName());
                logixNGEditor.addEditorEventListener((HashMap<String, String> data) -> {
                    _inEditInlineLogixNGMode = false;
                    data.forEach((key, value) -> {
                        if (key.equals("Finish")) {                  // NOI18N
                            _inlineLogixNGEdit = null;
                            _inEditInlineLogixNGMode = false;
                        } else if (key.equals("Delete")) {           // NOI18N
                            _inEditInlineLogixNGMode = false;
                            deleteLogixNG(p.getLogixNG());
                        } else if (key.equals("chgUname")) {         // NOI18N
                            p.getLogixNG().setUserName(value);
                        }
                    });
                    if (p.getLogixNG() != null && p.getLogixNG().getNumConditionalNGs() == 0) {
                        deleteLogixNG_Internal(p.getLogixNG());
                    }
                });
                logixNGEditor.bringToFront();
                _inEditInlineLogixNGMode = true;
                _inlineLogixNGEdit = logixNGEditor;
            }
        });
    }

    private void deleteLogixNG(LogixNG logixNG) {
        DeleteBean<LogixNG> deleteBean = new DeleteBean<>(
                InstanceManager.getDefault(LogixNG_Manager.class));

        boolean hasChildren = logixNG.getNumConditionalNGs() > 0;

        deleteBean.delete(logixNG, hasChildren, (t)->{deleteLogixNG_Internal(t);},
                (t,list)->{logixNG.getListenerRefsIncludingChildren(list);},
                jmri.jmrit.logixng.LogixNG_UserPreferences.class.getName());
    }

    private void deleteLogixNG_Internal(LogixNG logixNG) {
        logixNG.setEnabled(false);
        try {
            InstanceManager.getDefault(LogixNG_Manager.class).deleteBean(logixNG, "DoDelete");
            logixNG.getInlineLogixNG().setLogixNG(null);
        } catch (PropertyVetoException e) {
            //At this stage the DoDelete shouldn't fail, as we have already done a can delete, which would trigger a veto
            log.error("{} : Could not Delete.", e.getMessage());
        }
    }

    /**
     * Check if it's possible to change the id of the Positionable to the
     * desired string.
     * @param p the Positionable
     * @param newId the desired new id
     * @throws jmri.jmrit.display.Positionable.DuplicateIdException if another
     *         Positionable in the editor already has this id
     */
    public void positionalIdChange(Positionable p, String newId)
            throws Positionable.DuplicateIdException {

        if (Objects.equals(newId, p.getId())) return;

        if ((newId != null) && (_idContents.containsKey(newId))) {
            throw new Positionable.DuplicateIdException();
        }

        if (p.getId() != null) _idContents.remove(p.getId());
        if (newId != null) _idContents.put(newId, p);
    }

    /**
     * Add a class name to the Positionable
     * @param p the Positionable
     * @param className the class name
     * @throws IllegalArgumentException if the name contains a comma
     */
    public void positionalAddClass(Positionable p, String className) {

        if (className == null) {
            throw new IllegalArgumentException("Class name must not be null");
        }
        if (className.isBlank()) {
            throw new IllegalArgumentException("Class name must not be blank");
        }
        if (className.contains(",")) {
            throw new IllegalArgumentException("Class name must not contain a comma");
        }

        if (p.getClasses().contains(className)) return;

        _classContents.computeIfAbsent(className, o -> new HashSet<>()).add(p);
    }

    /**
     * Removes a class name from the Positionable
     * @param p the Positionable
     * @param className the class name
     */
    public void positionalRemoveClass(Positionable p, String className) {

        if (p.getClasses().contains(className)) return;

        _classContents.get(className).remove(p);
    }

    /**
     * Add a checkbox to display a tooltip for the Positionable item and if
     * showable, provide a dialog menu to edit it.
     *
     * @param p     the item to set the menu for
     * @param popup the menu to add for p
     */
    public void setShowToolTipMenu(Positionable p, JPopupMenu popup) {
        if (p.getDisplayLevel() == BKG) {
            return;
        }

        JMenu edit = new JMenu(Bundle.getMessage("EditTooltip"));

        JCheckBoxMenuItem showToolTipItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowTooltip"));
        showToolTipItem.setSelected(p.showToolTip());
        showToolTipItem.addActionListener(new ActionListener() {
            Positionable comp;
            JCheckBoxMenuItem checkBox;

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.setShowToolTip(checkBox.isSelected());
            }

            ActionListener init(Positionable pos, JCheckBoxMenuItem cb) {
                comp = pos;
                checkBox = cb;
                return this;
            }
        }.init(p, showToolTipItem));
        edit.add(showToolTipItem);

        edit.add(CoordinateEdit.getToolTipEditAction(p));

        JCheckBoxMenuItem prependToolTipWithDisplayNameItem = new JCheckBoxMenuItem(Bundle.getMessage("PrependTooltipWithDisplayName"));
        prependToolTipWithDisplayNameItem.setSelected(p.getToolTip().getPrependToolTipWithDisplayName());
        prependToolTipWithDisplayNameItem.addActionListener(new ActionListener() {
            Positionable comp;
            JCheckBoxMenuItem checkBox;

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.getToolTip().setPrependToolTipWithDisplayName(checkBox.isSelected());
            }

            ActionListener init(Positionable pos, JCheckBoxMenuItem cb) {
                comp = pos;
                checkBox = cb;
                return this;
            }
        }.init(p, prependToolTipWithDisplayNameItem));
        edit.add(prependToolTipWithDisplayNameItem);

        popup.add(edit);
    }

    /**
     * Add an action to remove the Positionable item.
     *
     * @param p     the item to set the menu for
     * @param popup the menu to add for p
     */
    public void setRemoveMenu(Positionable p, JPopupMenu popup) {
        popup.add(new AbstractAction(Bundle.getMessage("Remove")) {
            Positionable comp;

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.remove();
                removeSelections(comp);
            }

            AbstractAction init(Positionable pos) {
                comp = pos;
                return this;
            }
        }.init(p));
    }

    /*
     * *********************** End Popup Methods **********************
     */
 /*
     * ****************** Marker Menu ***************************
     */
    protected void locoMarkerFromRoster() {
        final JmriJFrame locoRosterFrame = new JmriJFrame();
        locoRosterFrame.getContentPane().setLayout(new FlowLayout());
        locoRosterFrame.setTitle(Bundle.getMessage("LocoFromRoster"));
        JLabel mtext = new JLabel();
        mtext.setText(Bundle.getMessage("SelectLoco") + ":");
        locoRosterFrame.getContentPane().add(mtext);
        final RosterEntrySelectorPanel rosterBox = new RosterEntrySelectorPanel();
        rosterBox.addPropertyChangeListener("selectedRosterEntries", pce -> {
            if (rosterBox.getSelectedRosterEntries().length != 0) {
                selectLoco(rosterBox.getSelectedRosterEntries()[0]);
            }
        });
        locoRosterFrame.getContentPane().add(rosterBox);
        locoRosterFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                locoRosterFrame.dispose();
            }
        });
        locoRosterFrame.pack();
        locoRosterFrame.setVisible(true);
    }

    protected LocoIcon selectLoco(String rosterEntryTitle) {
        if ("".equals(rosterEntryTitle)) {
            return null;
        }
        return selectLoco(Roster.getDefault().entryFromTitle(rosterEntryTitle));
    }

    protected LocoIcon selectLoco(RosterEntry entry) {
        LocoIcon l = null;
        if (entry == null) {
            return null;
        }
        // try getting road number, else use DCC address
        String rn = entry.getRoadNumber();
        if ((rn == null) || rn.equals("")) {
            rn = entry.getDccAddress();
        }
        if (rn != null) {
            l = addLocoIcon(rn);
            l.setRosterEntry(entry);
        }
        return l;
    }

    protected void locoMarkerFromInput() {
        final JmriJFrame locoFrame = new JmriJFrame();
        locoFrame.getContentPane().setLayout(new FlowLayout());
        locoFrame.setTitle(Bundle.getMessage("EnterLocoMarker"));

        JLabel textId = new JLabel();
        textId.setText(Bundle.getMessage("LocoID") + ":");
        locoFrame.getContentPane().add(textId);

        final JTextField locoId = new JTextField(7);
        locoFrame.getContentPane().add(locoId);
        locoId.setText("");
        locoId.setToolTipText(Bundle.getMessage("EnterLocoID"));
        JButton okay = new JButton();
        okay.setText(Bundle.getMessage("ButtonOK"));
        okay.addActionListener(e -> {
            String nameID = locoId.getText();
            if ((nameID != null) && !(nameID.trim().equals(""))) {
                addLocoIcon(nameID.trim());
            } else {
                JmriJOptionPane.showMessageDialog(locoFrame, Bundle.getMessage("ErrorEnterLocoID"),
                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
            }
        });
        locoFrame.getContentPane().add(okay);
        locoFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                locoFrame.dispose();
            }
        });
        locoFrame.pack();
        if (_targetFrame != null) {
            locoFrame.setLocation(_targetFrame.getLocation());
        }
        locoFrame.setVisible(true);
    }

    /**
     * Remove marker icons from panel
     */
    protected void removeMarkers() {
        log.debug("Remove markers");
        for (int i = _contents.size() - 1; i >= 0; i--) {
            Positionable il = _contents.get(i);
            if (il instanceof LocoIcon) {
                il.remove();
                if (il.getId() != null) _idContents.remove(il.getId());
                for (String className : il.getClasses()) {
                    _classContents.get(className).remove(il);
                }
            }
        }
    }

    /*
     * *********************** End Marker Menu Methods **********************
     */
 /*
     * ************ Adding content to the panel **********************
     */
    public PositionableLabel setUpBackground(String name) {
        NamedIcon icon = NamedIcon.getIconByName(name);
        PositionableLabel l = new PositionableLabel(icon, this);
        l.setPopupUtility(null);        // no text
        l.setPositionable(false);
        l.setShowToolTip(false);
        l.setSize(icon.getIconWidth(), icon.getIconHeight());
        l.setDisplayLevel(BKG);
        l.setLocation(getNextBackgroundLeft(), 0);
        try {
            putItem(l);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return l;
    }

    protected PositionableLabel addLabel(String text) {
        PositionableLabel l = new PositionableLabel(text, this);
        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
        l.setDisplayLevel(LABELS);
        setNextLocation(l);
        try {
            putItem(l);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return l;
    }

    /**
     * Determine right side x of furthest right background
     */
    private int getNextBackgroundLeft() {
        int left = 0;
        // place to right of background images, if any
        for (Positionable p : _contents) {
            if (p instanceof PositionableLabel) {
                PositionableLabel l = (PositionableLabel) p;
                if (l.isBackground()) {
                    int test = l.getX() + l.maxWidth();
                    if (test > left) {
                        left = test;
                    }
                }
            }
        }
        return left;
    }

    /* Positionable has set a new level.  Editor must change it in the target panel.
     */
    public void displayLevelChange(Positionable l) {
        removeFromTarget(l);
        addToTarget(l);
    }

    public TrainIcon addTrainIcon(String name) {
        TrainIcon l = new TrainIcon(this);
        putLocoIcon(l, name);
        return l;
    }

    public LocoIcon addLocoIcon(String name) {
        LocoIcon l = new LocoIcon(this);
        putLocoIcon(l, name);
        return l;
    }

    public void putLocoIcon(LocoIcon l, String name) {
        l.setText(name);
        l.setHorizontalTextPosition(SwingConstants.CENTER);
        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
        l.setEditable(isEditable());    // match popup mode to editor mode
        try {
            putItem(l);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
    }

    public void putItem(Positionable l) throws Positionable.DuplicateIdException {
        l.invalidate();
        l.setPositionable(true);
        l.setVisible(true);
        if (l.getToolTip() == null) {
            l.setToolTip(new ToolTip(_defaultToolTip, l));
        }
        addToTarget(l);
        if (!_contents.add(l)) {
            log.error("Unable to add {} to _contents", l.getNameString());
        }
        if (l.getId() != null) {
            if (_idContents.containsKey(l.getId())) {
                throw new Positionable.DuplicateIdException();
            }
            _idContents.put(l.getId(), l);
        }
        for (String className : l.getClasses()) {
            _classContents.get(className).add(l);
        }
        if (log.isDebugEnabled()) {
            log.debug("putItem {} to _contents. level= {}", l.getNameString(), l.getDisplayLevel());
        }
    }

    protected void addToTarget(Positionable l) {
        JComponent c = (JComponent) l;
        c.invalidate();
        _targetPanel.remove(c);
        _targetPanel.add(c, Integer.valueOf(l.getDisplayLevel()));
        _targetPanel.moveToFront(c);
        c.repaint();
        _targetPanel.revalidate();
    }

    /*
     * ************ Icon editors for adding content ***********
     */
    static final String[] ICON_EDITORS = {"Sensor", "RightTurnout", "LeftTurnout",
        "SlipTOEditor", "SignalHead", "SignalMast", "Memory", "Light",
        "Reporter", "Background", "MultiSensor", "Icon", "Text", "Block Contents"};

    /**
     * Create editor for a given item type.
     * Paths to default icons are fixed in code. Compare to respective icon package,
     * eg. {@link #addSensorEditor()} and {@link SensorIcon}
     *
     * @param name Icon editor's name
     * @return a window
     */
    public JFrameItem getIconFrame(String name) {
        JFrameItem frame = _iconEditorFrame.get(name);
        if (frame == null) {
            if ("Sensor".equals(name)) {
                addSensorEditor();
            } else if ("RightTurnout".equals(name)) {
                addRightTOEditor();
            } else if ("LeftTurnout".equals(name)) {
                addLeftTOEditor();
            } else if ("SlipTOEditor".equals(name)) {
                addSlipTOEditor();
            } else if ("SignalHead".equals(name)) {
                addSignalHeadEditor();
            } else if ("SignalMast".equals(name)) {
                addSignalMastEditor();
            } else if ("Memory".equals(name)) {
                addMemoryEditor();
            } else if ("GlobalVariable".equals(name)) {
                addGlobalVariableEditor();
            } else if ("Reporter".equals(name)) {
                addReporterEditor();
            } else if ("Light".equals(name)) {
                addLightEditor();
            } else if ("Background".equals(name)) {
                addBackgroundEditor();
            } else if ("MultiSensor".equals(name)) {
                addMultiSensorEditor();
            } else if ("Icon".equals(name)) {
                addIconEditor();
            } else if ("Text".equals(name)) {
                addTextEditor();
            } else if ("BlockLabel".equals(name)) {
                addBlockContentsEditor();
            } else if ("Audio".equals(name)) {
                addAudioEditor();
            } else if ("LogixNG".equals(name)) {
                addLogixNGEditor();
            } else {
                // log.error("No such Icon Editor \"{}\"", name);
                return null;
            }
            // frame added in the above switch
            frame = _iconEditorFrame.get(name);

            if (frame == null) { // addTextEditor does not create a usable frame
                return null;
            }
            //frame.setLocationRelativeTo(this);
            frame.setLocation(frameLocationX, frameLocationY);
            frameLocationX += DELTA;
            frameLocationY += DELTA;
        }
        frame.setVisible(true);
        return frame;
    }
    public int frameLocationX = 0;
    public int frameLocationY = 0;
    static final int DELTA = 20;

    public IconAdder getIconEditor(String name) {
        return _iconEditorFrame.get(name).getEditor();
    }

    /**
     * Add a label to the target.
     */
    protected void addTextEditor() {
        String newLabel = JmriJOptionPane.showInputDialog(this, Bundle.getMessage("PromptNewLabel"),"");
        if (newLabel == null) {
            return;  // canceled
        }
        PositionableLabel l = addLabel(newLabel);
        // always allow new items to be moved
        l.setPositionable(true);
    }

    protected void addRightTOEditor() {
        IconAdder editor = new IconAdder("RightTurnout");
        editor.setIcon(3, "TurnoutStateClosed",
                "resources/icons/smallschematics/tracksegments/os-righthand-west-closed.gif");
        editor.setIcon(2, "TurnoutStateThrown",
                "resources/icons/smallschematics/tracksegments/os-righthand-west-thrown.gif");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/smallschematics/tracksegments/os-righthand-west-error.gif");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/smallschematics/tracksegments/os-righthand-west-unknown.gif");

        JFrameItem frame = makeAddIconFrame("RightTurnout", true, true, editor);
        _iconEditorFrame.put("RightTurnout", frame);
        editor.setPickList(PickListModel.turnoutPickModelInstance());

        ActionListener addIconAction = a -> addTurnoutR();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addLeftTOEditor() {
        IconAdder editor = new IconAdder("LeftTurnout");
        editor.setIcon(3, "TurnoutStateClosed",
                "resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif");
        editor.setIcon(2, "TurnoutStateThrown",
                "resources/icons/smallschematics/tracksegments/os-lefthand-east-thrown.gif");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/smallschematics/tracksegments/os-lefthand-east-error.gif");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/smallschematics/tracksegments/os-lefthand-east-unknown.gif");

        JFrameItem frame = makeAddIconFrame("LeftTurnout", true, true, editor);
        _iconEditorFrame.put("LeftTurnout", frame);
        editor.setPickList(PickListModel.turnoutPickModelInstance());

        ActionListener addIconAction = a -> addTurnoutL();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addSlipTOEditor() {
        SlipIconAdder editor = new SlipIconAdder("SlipTOEditor");
        editor.setIcon(3, "LowerWestToUpperEast",
                "resources/icons/smallschematics/tracksegments/os-slip-lower-west-upper-east.gif");
        editor.setIcon(2, "UpperWestToLowerEast",
                "resources/icons/smallschematics/tracksegments/os-slip-upper-west-lower-east.gif");
        editor.setIcon(4, "LowerWestToLowerEast",
                "resources/icons/smallschematics/tracksegments/os-slip-lower-west-lower-east.gif");
        editor.setIcon(5, "UpperWestToUpperEast",
                "resources/icons/smallschematics/tracksegments/os-slip-upper-west-upper-east.gif");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/smallschematics/tracksegments/os-slip-error-full.gif");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/smallschematics/tracksegments/os-slip-unknown-full.gif");
        editor.setTurnoutType(SlipTurnoutIcon.DOUBLESLIP);
        JFrameItem frame = makeAddIconFrame("SlipTOEditor", true, true, editor);
        _iconEditorFrame.put("SlipTOEditor", frame);
        editor.setPickList(PickListModel.turnoutPickModelInstance());

        ActionListener addIconAction = a -> addSlip();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.SlipTurnoutIcon", true);
    }

    protected void addSensorEditor() {
        IconAdder editor = new IconAdder("Sensor");
        editor.setIcon(3, "SensorStateActive",
                "resources/icons/smallschematics/tracksegments/circuit-occupied.gif");
        editor.setIcon(2, "SensorStateInactive",
                "resources/icons/smallschematics/tracksegments/circuit-empty.gif");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/smallschematics/tracksegments/circuit-error.gif");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/smallschematics/tracksegments/circuit-error.gif");

        JFrameItem frame = makeAddIconFrame("Sensor", true, true, editor);
        _iconEditorFrame.put("Sensor", frame);
        editor.setPickList(PickListModel.sensorPickModelInstance());

        ActionListener addIconAction = a -> putSensor();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addSignalHeadEditor() {
        IconAdder editor = getSignalHeadEditor();
        JFrameItem frame = makeAddIconFrame("SignalHead", true, true, editor);
        _iconEditorFrame.put("SignalHead", frame);
        editor.setPickList(PickListModel.signalHeadPickModelInstance());

        ActionListener addIconAction = a -> putSignalHead();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected IconAdder getSignalHeadEditor() {
        // note that all these icons will be refreshed when user clicks a specific signal head in the table
        IconAdder editor = new IconAdder("SignalHead");
        editor.setIcon(0, "SignalHeadStateRed",
                "resources/icons/smallschematics/searchlights/left-red-marker.gif");
        editor.setIcon(1, "SignalHeadStateYellow",
                "resources/icons/smallschematics/searchlights/left-yellow-marker.gif");
        editor.setIcon(2, "SignalHeadStateGreen",
                "resources/icons/smallschematics/searchlights/left-green-marker.gif");
        editor.setIcon(3, "SignalHeadStateDark",
                "resources/icons/smallschematics/searchlights/left-dark-marker.gif");
        editor.setIcon(4, "SignalHeadStateHeld",
                "resources/icons/smallschematics/searchlights/left-held-marker.gif");
        editor.setIcon(5, "SignalHeadStateLunar",
                "resources/icons/smallschematics/searchlights/left-lunar-marker.gif");
        editor.setIcon(6, "SignalHeadStateFlashingRed",
                "resources/icons/smallschematics/searchlights/left-flashred-marker.gif");
        editor.setIcon(7, "SignalHeadStateFlashingYellow",
                "resources/icons/smallschematics/searchlights/left-flashyellow-marker.gif");
        editor.setIcon(8, "SignalHeadStateFlashingGreen",
                "resources/icons/smallschematics/searchlights/left-flashgreen-marker.gif");
        editor.setIcon(9, "SignalHeadStateFlashingLunar",
                "resources/icons/smallschematics/searchlights/left-flashlunar-marker.gif");
        return editor;
    }

    protected void addSignalMastEditor() {
        IconAdder editor = new IconAdder("SignalMast");

        JFrameItem frame = makeAddIconFrame("SignalMast", true, true, editor);
        _iconEditorFrame.put("SignalMast", frame);
        editor.setPickList(PickListModel.signalMastPickModelInstance());

        ActionListener addIconAction = a -> putSignalMast();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    private final SpinnerNumberModel _spinCols = new SpinnerNumberModel(3, 1, 100, 1);

    protected void addMemoryEditor() {
        IconAdder editor = new IconAdder("Memory") {
            final JButton bSpin = new JButton(Bundle.getMessage("AddSpinner"));
            final JButton bBox = new JButton(Bundle.getMessage("AddInputBox"));
            final JSpinner spinner = new JSpinner(_spinCols);

            @Override
            protected void addAdditionalButtons(JPanel p) {
                bSpin.addActionListener(a -> addMemorySpinner());
                JPanel p1 = new JPanel();
                //p1.setLayout(new BoxLayout(p1, BoxLayout.X_AXIS));
                bBox.addActionListener(a -> addMemoryInputBox());
                ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(2);
                spinner.setMaximumSize(spinner.getPreferredSize());
                JPanel p2 = new JPanel();
                p2.add(new JLabel(Bundle.getMessage("NumColsLabel")));
                p2.add(spinner);
                p1.add(p2);
                p1.add(bBox);
                p.add(p1);
                p1 = new JPanel();
                p1.add(bSpin);
                p.add(p1);
            }

            @Override
            public void valueChanged(ListSelectionEvent e) {
                super.valueChanged(e);
                bSpin.setEnabled(addIconIsEnabled());
                bBox.setEnabled(addIconIsEnabled());
            }
        };
        ActionListener addIconAction = a -> putMemory();
        JFrameItem frame = makeAddIconFrame("Memory", true, true, editor);
        _iconEditorFrame.put("Memory", frame);
        editor.setPickList(PickListModel.memoryPickModelInstance());
        editor.makeIconPanel(true);
        editor.complete(addIconAction, false, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addGlobalVariableEditor() {
        IconAdder editor = new IconAdder("GlobalVariable") {
            final JButton bSpin = new JButton(Bundle.getMessage("AddSpinner"));
            final JButton bBox = new JButton(Bundle.getMessage("AddInputBox"));
            final JSpinner spinner = new JSpinner(_spinCols);

            @Override
            protected void addAdditionalButtons(JPanel p) {
                bSpin.addActionListener(a -> addGlobalVariableSpinner());
                JPanel p1 = new JPanel();
                //p1.setLayout(new BoxLayout(p1, BoxLayout.X_AXIS));
                bBox.addActionListener(a -> addGlobalVariableInputBox());
                ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(2);
                spinner.setMaximumSize(spinner.getPreferredSize());
                JPanel p2 = new JPanel();
                p2.add(new JLabel(Bundle.getMessage("NumColsLabel")));
                p2.add(spinner);
                p1.add(p2);
                p1.add(bBox);
                p.add(p1);
                p1 = new JPanel();
                p1.add(bSpin);
                p.add(p1);
            }

            @Override
            public void valueChanged(ListSelectionEvent e) {
                super.valueChanged(e);
                bSpin.setEnabled(addIconIsEnabled());
                bBox.setEnabled(addIconIsEnabled());
            }
        };
        ActionListener addIconAction = a -> putGlobalVariable();
        JFrameItem frame = makeAddIconFrame("GlobalVariable", true, true, editor);
        _iconEditorFrame.put("GlobalVariable", frame);
        editor.setPickList(PickListModel.globalVariablePickModelInstance());
        editor.makeIconPanel(true);
        editor.complete(addIconAction, false, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addBlockContentsEditor() {
        IconAdder editor = new IconAdder("Block Contents");
        ActionListener addIconAction = a -> putBlockContents();
        JFrameItem frame = makeAddIconFrame("BlockLabel", true, true, editor);
        _iconEditorFrame.put("BlockLabel", frame);
        editor.setPickList(PickListModel.blockPickModelInstance());
        editor.makeIconPanel(true);
        editor.complete(addIconAction, false, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addReporterEditor() {
        IconAdder editor = new IconAdder("Reporter");
        ActionListener addIconAction = a -> addReporter();
        JFrameItem frame = makeAddIconFrame("Reporter", true, true, editor);
        _iconEditorFrame.put("Reporter", frame);
        editor.setPickList(PickListModel.reporterPickModelInstance());
        editor.makeIconPanel(true);
        editor.complete(addIconAction, false, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addLightEditor() {
        IconAdder editor = new IconAdder("Light");
        editor.setIcon(3, "StateOff",
                "resources/icons/smallschematics/lights/cross-on.png");
        editor.setIcon(2, "StateOn",
                "resources/icons/smallschematics/lights/cross-off.png");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/smallschematics/lights/cross-inconsistent.png");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/smallschematics/lights/cross-unknown.png");

        JFrameItem frame = makeAddIconFrame("Light", true, true, editor);
        _iconEditorFrame.put("Light", frame);
        editor.setPickList(PickListModel.lightPickModelInstance());

        ActionListener addIconAction = a -> addLight();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addBackgroundEditor() {
        IconAdder editor = new IconAdder("Background");
        editor.setIcon(0, "background", "resources/PanelPro.gif");

        JFrameItem frame = makeAddIconFrame("Background", true, false, editor);
        _iconEditorFrame.put("Background", frame);

        ActionListener addIconAction = a -> putBackground();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected JFrameItem addMultiSensorEditor() {
        MultiSensorIconAdder editor = new MultiSensorIconAdder("MultiSensor");
        editor.setIcon(0, "BeanStateInconsistent",
                "resources/icons/USS/plate/levers/l-inconsistent.gif");
        editor.setIcon(1, "BeanStateUnknown",
                "resources/icons/USS/plate/levers/l-unknown.gif");
        editor.setIcon(2, "SensorStateInactive",
                "resources/icons/USS/plate/levers/l-inactive.gif");
        editor.setIcon(3, "MultiSensorPosition 0",
                "resources/icons/USS/plate/levers/l-left.gif");
        editor.setIcon(4, "MultiSensorPosition 1",
                "resources/icons/USS/plate/levers/l-vertical.gif");
        editor.setIcon(5, "MultiSensorPosition 2",
                "resources/icons/USS/plate/levers/l-right.gif");

        JFrameItem frame = makeAddIconFrame("MultiSensor", true, false, editor);
        _iconEditorFrame.put("MultiSensor", frame);
        frame.addHelpMenu("package.jmri.jmrit.display.MultiSensorIconAdder", true);

        editor.setPickList(PickListModel.sensorPickModelInstance());

        ActionListener addIconAction = a -> addMultiSensor();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, true, false);
        return frame;
    }

    protected void addIconEditor() {
        IconAdder editor = new IconAdder("Icon");
        editor.setIcon(0, "plainIcon", "resources/icons/smallschematics/tracksegments/block.gif");
        JFrameItem frame = makeAddIconFrame("Icon", true, false, editor);
        _iconEditorFrame.put("Icon", frame);

        ActionListener addIconAction = a -> putIcon();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addAudioEditor() {
        IconAdder editor = new IconAdder("Audio");
        editor.setIcon(0, "plainIcon", "resources/icons/audio_icon.gif");
        JFrameItem frame = makeAddIconFrame("Audio", true, false, editor);
        _iconEditorFrame.put("Audio", frame);
        editor.setPickList(PickListModel.audioPickModelInstance());

        ActionListener addIconAction = a -> putAudio();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    protected void addLogixNGEditor() {
        IconAdder editor = new IconAdder("LogixNG");
        editor.setIcon(0, "plainIcon", "resources/icons/logixng/logixng_icon.gif");
        JFrameItem frame = makeAddIconFrame("LogixNG", true, false, editor);
        _iconEditorFrame.put("LogixNG", frame);

        ActionListener addIconAction = a -> putLogixNG();
        editor.makeIconPanel(true);
        editor.complete(addIconAction, true, false, false);
        frame.addHelpMenu("package.jmri.jmrit.display.IconAdder", true);
    }

    /*
     * ************** add content items from Icon Editors *******************
     */
    /**
     * Add a sensor indicator to the target.
     *
     * @return The sensor that was added to the panel.
     */
    protected SensorIcon putSensor() {
        SensorIcon result = new SensorIcon(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), this);
        IconAdder editor = getIconEditor("Sensor");
        Hashtable<String, NamedIcon> map = editor.getIconMap();
        Enumeration<String> e = map.keys();
        while (e.hasMoreElements()) {
            String key = e.nextElement();
            result.setIcon(key, map.get(key));
        }
//        l.setActiveIcon(editor.getIcon("SensorStateActive"));
//        l.setInactiveIcon(editor.getIcon("SensorStateInactive"));
//        l.setInconsistentIcon(editor.getIcon("BeanStateInconsistent"));
//        l.setUnknownIcon(editor.getIcon("BeanStateUnknown"));
        NamedBean b = editor.getTableSelection();
        if (b != null) {
            result.setSensor(b.getDisplayName());
        }
        result.setDisplayLevel(SENSORS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException ex) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", ex);
        }
        return result;
    }

    /**
     * Add a turnout indicator to the target
     */
    void addTurnoutR() {
        IconAdder editor = getIconEditor("RightTurnout");
        addTurnout(editor);
    }

    void addTurnoutL() {
        IconAdder editor = getIconEditor("LeftTurnout");
        addTurnout(editor);
    }

    protected TurnoutIcon addTurnout(IconAdder editor) {
        TurnoutIcon result = new TurnoutIcon(this);
        result.setTurnout(editor.getTableSelection().getDisplayName());
        Hashtable<String, NamedIcon> map = editor.getIconMap();
        Enumeration<String> e = map.keys();
        while (e.hasMoreElements()) {
            String key = e.nextElement();
            result.setIcon(key, map.get(key));
        }
        result.setDisplayLevel(TURNOUTS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException ex) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", ex);
        }
        return result;
    }

    @SuppressFBWarnings(value="BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification="iconEditor requested as exact type")
    SlipTurnoutIcon addSlip() {
        SlipTurnoutIcon result = new SlipTurnoutIcon(this);
        SlipIconAdder editor = (SlipIconAdder) getIconEditor("SlipTOEditor");
        result.setSingleSlipRoute(editor.getSingleSlipRoute());

        switch (editor.getTurnoutType()) {
            case SlipTurnoutIcon.DOUBLESLIP:
                result.setLowerWestToUpperEastIcon(editor.getIcon("LowerWestToUpperEast"));
                result.setUpperWestToLowerEastIcon(editor.getIcon("UpperWestToLowerEast"));
                result.setLowerWestToLowerEastIcon(editor.getIcon("LowerWestToLowerEast"));
                result.setUpperWestToUpperEastIcon(editor.getIcon("UpperWestToUpperEast"));
                break;
            case SlipTurnoutIcon.SINGLESLIP:
                result.setLowerWestToUpperEastIcon(editor.getIcon("LowerWestToUpperEast"));
                result.setUpperWestToLowerEastIcon(editor.getIcon("UpperWestToLowerEast"));
                result.setLowerWestToLowerEastIcon(editor.getIcon("Slip"));
                result.setSingleSlipRoute(editor.getSingleSlipRoute());
                break;
            case SlipTurnoutIcon.THREEWAY:
                result.setLowerWestToUpperEastIcon(editor.getIcon("Upper"));
                result.setUpperWestToLowerEastIcon(editor.getIcon("Middle"));
                result.setLowerWestToLowerEastIcon(editor.getIcon("Lower"));
                result.setSingleSlipRoute(editor.getSingleSlipRoute());
                break;
            case SlipTurnoutIcon.SCISSOR: //Scissor is the same as a Double for icon storing.
                result.setLowerWestToUpperEastIcon(editor.getIcon("LowerWestToUpperEast"));
                result.setUpperWestToLowerEastIcon(editor.getIcon("UpperWestToLowerEast"));
                result.setLowerWestToLowerEastIcon(editor.getIcon("LowerWestToLowerEast"));
                //l.setUpperWestToUpperEastIcon(editor.getIcon("UpperWestToUpperEast"));
                break;
            default:
                log.warn("Unexpected addSlip editor.getTurnoutType() of {}", editor.getTurnoutType());
                break;
        }

        if ((editor.getTurnoutType() == SlipTurnoutIcon.SCISSOR) && (!editor.getSingleSlipRoute())) {
            result.setTurnout(editor.getTurnout("lowerwest").getName(), SlipTurnoutIcon.LOWERWEST);
            result.setTurnout(editor.getTurnout("lowereast").getName(), SlipTurnoutIcon.LOWEREAST);
        }
        result.setInconsistentIcon(editor.getIcon("BeanStateInconsistent"));
        result.setUnknownIcon(editor.getIcon("BeanStateUnknown"));
        result.setTurnoutType(editor.getTurnoutType());
        result.setTurnout(editor.getTurnout("west").getName(), SlipTurnoutIcon.WEST);
        result.setTurnout(editor.getTurnout("east").getName(), SlipTurnoutIcon.EAST);
        result.setDisplayLevel(TURNOUTS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    /**
     * Add a signal head to the target.
     *
     * @return The signal head that was added to the target.
     */
    protected SignalHeadIcon putSignalHead() {
        SignalHeadIcon result = new SignalHeadIcon(this);
        IconAdder editor = getIconEditor("SignalHead");
        result.setSignalHead(editor.getTableSelection().getDisplayName());
        Hashtable<String, NamedIcon> map = editor.getIconMap();
        Enumeration<String> e = map.keys();
        while (e.hasMoreElements()) {
            String key = e.nextElement();
            result.setIcon(key, map.get(key));
        }
        result.setDisplayLevel(SIGNALS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException ex) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", ex);
        }
        return result;
    }

    /**
     * Add a signal mast to the target.
     *
     * @return The signal mast that was added to the target.
     */
    protected SignalMastIcon putSignalMast() {
        SignalMastIcon result = new SignalMastIcon(this);
        IconAdder editor = _iconEditorFrame.get("SignalMast").getEditor();
        result.setSignalMast(editor.getTableSelection().getDisplayName());
        result.setDisplayLevel(SIGNALS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected MemoryIcon putMemory() {
        MemoryIcon result = new MemoryIcon(new NamedIcon("resources/icons/misc/X-red.gif",
                "resources/icons/misc/X-red.gif"), this);
        IconAdder memoryIconEditor = getIconEditor("Memory");
        result.setMemory(memoryIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected MemorySpinnerIcon addMemorySpinner() {
        MemorySpinnerIcon result = new MemorySpinnerIcon(this);
        IconAdder memoryIconEditor = getIconEditor("Memory");
        result.setMemory(memoryIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected MemoryInputIcon addMemoryInputBox() {
        MemoryInputIcon result = new MemoryInputIcon(_spinCols.getNumber().intValue(), this);
        IconAdder memoryIconEditor = getIconEditor("Memory");
        result.setMemory(memoryIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected GlobalVariableIcon putGlobalVariable() {
        GlobalVariableIcon result = new GlobalVariableIcon(new NamedIcon("resources/icons/misc/X-red.gif",
                "resources/icons/misc/X-red.gif"), this);
        IconAdder globalVariableIconEditor = getIconEditor("GlobalVariable");
        result.setGlobalVariable(globalVariableIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected GlobalVariableSpinnerIcon addGlobalVariableSpinner() {
        GlobalVariableSpinnerIcon result = new GlobalVariableSpinnerIcon(this);
        IconAdder globalVariableIconEditor = getIconEditor("GlobalVariable");
        result.setGlobalVariable(globalVariableIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected GlobalVariableInputIcon addGlobalVariableInputBox() {
        GlobalVariableInputIcon result = new GlobalVariableInputIcon(_spinCols.getNumber().intValue(), this);
        IconAdder globalVariableIconEditor = getIconEditor("GlobalVariable");
        result.setGlobalVariable(globalVariableIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected BlockContentsIcon putBlockContents() {
        BlockContentsIcon result = new BlockContentsIcon(new NamedIcon("resources/icons/misc/X-red.gif",
                "resources/icons/misc/X-red.gif"), this);
        IconAdder blockIconEditor = getIconEditor("BlockLabel");
        result.setBlock(blockIconEditor.getTableSelection().getDisplayName());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(MEMORIES);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    /**
     * Add a Light indicator to the target
     *
     * @return The light indicator that was added to the target.
     */
    protected LightIcon addLight() {
        LightIcon result = new LightIcon(this);
        IconAdder editor = getIconEditor("Light");
        result.setOffIcon(editor.getIcon("StateOff"));
        result.setOnIcon(editor.getIcon("StateOn"));
        result.setInconsistentIcon(editor.getIcon("BeanStateInconsistent"));
        result.setUnknownIcon(editor.getIcon("BeanStateUnknown"));
        result.setLight((Light) editor.getTableSelection());
        result.setDisplayLevel(LIGHTS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected ReporterIcon addReporter() {
        ReporterIcon result = new ReporterIcon(this);
        IconAdder reporterIconEditor = getIconEditor("Reporter");
        result.setReporter((Reporter) reporterIconEditor.getTableSelection());
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(REPORTERS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    /**
     * Button pushed, add a background image. Note that a background image
     * differs from a regular icon only in the level at which it's presented.
     */
    void putBackground() {
        // most likely the image is scaled.  get full size from URL
        IconAdder bkgrndEditor = getIconEditor("Background");
        String url = bkgrndEditor.getIcon("background").getURL();
        setUpBackground(url);
    }

    /**
     * Add an icon to the target.
     *
     * @return The icon that was added to the target.
     */
    protected Positionable putIcon() {
        IconAdder iconEditor = getIconEditor("Icon");
        String url = iconEditor.getIcon("plainIcon").getURL();
        NamedIcon icon = NamedIcon.getIconByName(url);
        if (log.isDebugEnabled()) {
            log.debug("putIcon: {} url= {}", (icon == null ? "null" : "icon"), url);
        }
        PositionableLabel result = new PositionableLabel(icon, this);
//        l.setPopupUtility(null);        // no text
        result.setDisplayLevel(ICONS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        result.updateSize();
        return result;
    }

    /**
     * Add a LogixNG icon to the target.
     *
     * @return The LogixNG icon that was added to the target.
     */
    protected Positionable putAudio() {
        IconAdder iconEditor = getIconEditor("Audio");
        String url = iconEditor.getIcon("plainIcon").getURL();
        NamedIcon icon = NamedIcon.getIconByName(url);
        if (log.isDebugEnabled()) {
            log.debug("putAudio: {} url= {}", (icon == null ? "null" : "icon"), url);
        }
        AudioIcon result = new AudioIcon(icon, this);
        NamedBean b = iconEditor.getTableSelection();
        if (b != null) {
            result.setAudio(b.getDisplayName());
        }
//        l.setPopupUtility(null);        // no text
        result.setDisplayLevel(ICONS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putAudio() with null id has thrown DuplicateIdException", e);
        }
        result.updateSize();
        return result;
    }

    /**
     * Add a LogixNG icon to the target.
     *
     * @return The LogixNG icon that was added to the target.
     */
    protected Positionable putLogixNG() {
        IconAdder iconEditor = getIconEditor("LogixNG");
        String url = iconEditor.getIcon("plainIcon").getURL();
        NamedIcon icon = NamedIcon.getIconByName(url);
        if (log.isDebugEnabled()) {
            log.debug("putLogixNG: {} url= {}", (icon == null ? "null" : "icon"), url);
        }
        LogixNGIcon result = new LogixNGIcon(icon, this);
//        l.setPopupUtility(null);        // no text
        result.setDisplayLevel(ICONS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putLogixNG() with null id has thrown DuplicateIdException", e);
        }
        result.updateSize();
        return result;
    }

    @SuppressFBWarnings(value="BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification="iconEditor requested as exact type")
    public MultiSensorIcon addMultiSensor() {
        MultiSensorIcon result = new MultiSensorIcon(this);
        MultiSensorIconAdder editor = (MultiSensorIconAdder) getIconEditor("MultiSensor");
        result.setUnknownIcon(editor.getIcon("BeanStateUnknown"));
        result.setInconsistentIcon(editor.getIcon("BeanStateInconsistent"));
        result.setInactiveIcon(editor.getIcon("SensorStateInactive"));
        int numPositions = editor.getNumIcons();
        for (int i = 3; i < numPositions; i++) {
            NamedIcon icon = editor.getIcon(i);
            String sensor = editor.getSensor(i).getName();
            result.addEntry(sensor, icon);
        }
        result.setUpDown(editor.getUpDown());
        result.setDisplayLevel(SENSORS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected AnalogClock2Display addClock() {
        AnalogClock2Display result = new AnalogClock2Display(this);
        result.setOpaque(false);
        result.update();
        result.setDisplayLevel(CLOCK);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    protected RpsPositionIcon addRpsReporter() {
        RpsPositionIcon result = new RpsPositionIcon(this);
        result.setSize(result.getPreferredSize().width, result.getPreferredSize().height);
        result.setDisplayLevel(SENSORS);
        setNextLocation(result);
        try {
            putItem(result);
        } catch (Positionable.DuplicateIdException e) {
            // This should never happen
            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
        }
        return result;
    }

    /*
     * ****************** end adding content ********************
     */
 /*
     * ********************* Icon Editors utils ***************************
     */
    public static class JFrameItem extends JmriJFrame {

        IconAdder _editor;

        JFrameItem(String name, IconAdder editor) {
            super(name);
            _editor = editor;
            setName(name);
        }

        public IconAdder getEditor() {
            return _editor;
        }

        @Override
        public String toString() {
            return this.getName();
        }
    }

    public void setTitle() {
        String name = "";
        Container ancestor = _targetPanel.getTopLevelAncestor();
        if (ancestor instanceof JFrame) {
            name = ((JFrame) ancestor).getTitle();
        }
        if (name == null || name.equals("")) {
            super.setTitle(Bundle.getMessage("LabelEditor"));
        } else {
            super.setTitle(name + " " + Bundle.getMessage("LabelEditor"));
        }
        for (JFrameItem frame : _iconEditorFrame.values()) {
            frame.setTitle(frame.getName() + " (" + name + ")");
        }
        setName(name);
    }

    /**
     * Create a frame showing all images in the set used for an icon.
     * Opened when editItemInPanel button is clicked in the Edit Icon Panel,
     * shown after icon's context menu Edit Icon... item is selected.
     *
     * @param name bean type name
     * @param add true when used to add a new item on panel, false when used to edit an item already on the panel
     * @param table true for bean types presented as table instead of icons
     * @param editor parent frame of the image frame
     * @return JFrame connected to the editor,  to be filled with icons
     */
    protected JFrameItem makeAddIconFrame(String name, boolean add, boolean table, IconAdder editor) {
        log.debug("makeAddIconFrame for {}, add= {}, table= {}", name, add, table);
        String txt;
        String BundleName;
        JFrameItem frame = new JFrameItem(name, editor);
        // use NamedBeanBundle property for basic beans like "Turnout" I18N
        if ("Sensor".equals(name)) {
            BundleName = "BeanNameSensor";
        } else if ("SignalHead".equals(name)) {
            BundleName = "BeanNameSignalHead";
        } else if ("SignalMast".equals(name)) {
            BundleName = "BeanNameSignalMast";
        } else if ("Memory".equals(name)) {
            BundleName = "BeanNameMemory";
        } else if ("Reporter".equals(name)) {
            BundleName = "BeanNameReporter";
        } else if ("Light".equals(name)) {
            BundleName = "BeanNameLight";
        } else if ("Turnout".equals(name)) {
            BundleName = "BeanNameTurnout"; // called by RightTurnout and LeftTurnout objects in TurnoutIcon.java edit() method
        } else if ("Block".equals(name)) {
            BundleName = "BeanNameBlock";
        } else if ("GlobalVariable".equals(name)) {
            BundleName = "BeanNameGlobalVariable";
        } else if ("Audio".equals(name)) {
            BundleName = "BeanNameAudio";
        } else {
            BundleName = name;
        }
        if (editor != null) {
            JPanel p = new JPanel();
            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
            if (add) {
                txt = MessageFormat.format(Bundle.getMessage("addItemToPanel"), Bundle.getMessage(BundleName));
            } else {
                txt = MessageFormat.format(Bundle.getMessage("editItemInPanel"), Bundle.getMessage(BundleName));
            }
            p.add(new JLabel(txt));
            if (table) {
                txt = MessageFormat.format(Bundle.getMessage("TableSelect"), Bundle.getMessage(BundleName),
                        (add ? Bundle.getMessage("ButtonAddIcon") : Bundle.getMessage("ButtonUpdateIcon")));
            } else {
                if ("MultiSensor".equals(name)) {
                    txt = MessageFormat.format(Bundle.getMessage("SelectMultiSensor", Bundle.getMessage("ButtonAddIcon")),
                            (add ? Bundle.getMessage("ButtonAddIcon") : Bundle.getMessage("ButtonUpdateIcon")));
                } else {
                    txt = MessageFormat.format(Bundle.getMessage("IconSelect"), Bundle.getMessage(BundleName),
                            (add ? Bundle.getMessage("ButtonAddIcon") : Bundle.getMessage("ButtonUpdateIcon")));
                }
            }
            p.add(new JLabel(txt));
            p.add(new JLabel("    ")); // add a bit of space on pane above icons
            frame.getContentPane().add(p, BorderLayout.NORTH);
            frame.getContentPane().add(editor);

            JMenuBar menuBar = new JMenuBar();
            JMenu findIcon = new JMenu(Bundle.getMessage("findIconMenu"));
            menuBar.add(findIcon);

            JMenuItem editItem = new JMenuItem(Bundle.getMessage("editIndexMenu"));
            editItem.addActionListener(e -> {
                ImageIndexEditor ii = InstanceManager.getDefault(ImageIndexEditor.class);
                ii.pack();
                ii.setVisible(true);
            });
            findIcon.add(editItem);
            findIcon.addSeparator();

            JMenuItem searchItem = new JMenuItem(Bundle.getMessage("searchFSMenu"));
            searchItem.addActionListener(new ActionListener() {
                IconAdder ea;

                @Override
                public void actionPerformed(ActionEvent e) {
                    InstanceManager.getDefault(DirectorySearcher.class).searchFS();
                    ea.addDirectoryToCatalog();
                }

                ActionListener init(IconAdder ed) {
                    ea = ed;
                    return this;
                }
            }.init(editor));

            findIcon.add(searchItem);
            frame.setJMenuBar(menuBar);
            editor.setParent(frame);
            // when this window closes, check for saving
            if (add) {
                frame.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
                        if (log.isDebugEnabled()) {
                            log.debug("windowClosing: HIDE {}", toString());
                        }
                    }
                });
            }
        } else {
            log.error("No icon editor specified for {}", name); // NOI18N
        }
        if (add) {
            txt = MessageFormat.format(Bundle.getMessage("AddItem"), Bundle.getMessage(BundleName));
            _iconEditorFrame.put(name, frame);
        } else {
            txt = MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage(BundleName));
        }
        frame.setTitle(txt + " (" + getTitle() + ")");
        frame.pack();
        return frame;
    }

    /*
     * ******************* cleanup ************************
     */
    protected void removeFromTarget(Positionable l) {
        _targetPanel.remove((Component) l);
        _highlightcomponent = null;
        Point p = l.getLocation();
        int w = l.getWidth();
        int h = l.getHeight();
        _targetPanel.revalidate();
        _targetPanel.repaint(p.x, p.y, w, h);
    }

    public boolean removeFromContents(Positionable l) {
        removeFromTarget(l);
        //todo check that parent == _targetPanel
        //Container parent = this.getParent();
        // force redisplay
        if (l.getId() != null) _idContents.remove(l.getId());
        for (String className : l.getClasses()) {
            _classContents.get(className).remove(l);
        }
        return _contents.remove(l);
    }

    /**
     * Ask user if panel should be deleted. The caller should dispose the panel
     * to delete it.
     *
     * @return true if panel should be deleted.
     */
    public boolean deletePanel() {
        log.debug("deletePanel");
        // verify deletion
        int selectedValue = JmriJOptionPane.showOptionDialog(_targetPanel,
                Bundle.getMessage("QuestionA") + "\n" + Bundle.getMessage("QuestionA2", Bundle.getMessage("FileMenuItemStore")),
                Bundle.getMessage("DeleteVerifyTitle"), JmriJOptionPane.DEFAULT_OPTION,
                JmriJOptionPane.QUESTION_MESSAGE, null,
                new Object[]{Bundle.getMessage("ButtonYesDelete"), Bundle.getMessage("ButtonCancel")},
                Bundle.getMessage("ButtonCancel"));
        // return without deleting if "Cancel" or Cancel Dialog response
        return (selectedValue == 0 ); // array position 0 = Yes, Delete.
    }

    /**
     * Dispose of the editor.
     */
    @Override
    public void dispose() {
        for (JFrameItem frame : _iconEditorFrame.values()) {
            frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
            frame.dispose();
        }
        // delete panel - deregister the panel for saving
        ConfigureManager cm = InstanceManager.getNullableDefault(ConfigureManager.class);
        if (cm != null) {
            cm.deregister(this);
        }
        InstanceManager.getDefault(EditorManager.class).remove(this);
        setVisible(false);
        _contents.clear();
        _idContents.clear();
        for (var list : _classContents.values()) list.clear();
        _classContents.clear();
        removeAll();
        super.dispose();
    }

    /*
     * **************** Mouse Methods **********************
     */
    public void showToolTip(Positionable selection, JmriMouseEvent event) {
        ToolTip tip = selection.getToolTip();
        tip.setLocation(selection.getX() + selection.getWidth() / 2, selection.getY() + selection.getHeight());
        setToolTip(tip);
    }

    protected int getItemX(Positionable p, int deltaX) {
        if ((p instanceof MemoryOrGVIcon) && (p.getPopupUtility().getFixedWidth() == 0)) {
            MemoryOrGVIcon pm = (MemoryOrGVIcon) p;
            return pm.getOriginalX() + (int) Math.round(deltaX / getPaintScale());
        } else {
            return p.getX() + (int) Math.round(deltaX / getPaintScale());
        }
    }

    protected int getItemY(Positionable p, int deltaY) {
        if ((p instanceof MemoryOrGVIcon) && (p.getPopupUtility().getFixedWidth() == 0)) {
            MemoryOrGVIcon pm = (MemoryOrGVIcon) p;
            return pm.getOriginalY() + (int) Math.round(deltaY / getPaintScale());
        } else {
            return p.getY() + (int) Math.round(deltaY / getPaintScale());
        }
    }

    /**
     * Provide a method for external code to add items to context menus.
     *
     * @param nb   The namedBean associated with the postionable item.
     * @param item The entry to add to the menu.
     * @param menu The menu to add the entry to.
     */
    public void addToPopUpMenu(NamedBean nb, JMenuItem item, int menu) {
        if (nb == null || item == null) {
            return;
        }
        for (Positionable pos : _contents) {
            if (pos.getNamedBean() == nb && pos.getPopupUtility() != null) {
                switch (menu) {
                    case VIEWPOPUPONLY:
                        pos.getPopupUtility().addViewPopUpMenu(item);
                        break;
                    case EDITPOPUPONLY:
                        pos.getPopupUtility().addEditPopUpMenu(item);
                        break;
                    default:
                        pos.getPopupUtility().addEditPopUpMenu(item);
                        pos.getPopupUtility().addViewPopUpMenu(item);
                }
                return;
            } else if (pos instanceof SlipTurnoutIcon) {
                if (pos.getPopupUtility() != null) {
                    SlipTurnoutIcon sti = (SlipTurnoutIcon) pos;
                    if (sti.getTurnout(SlipTurnoutIcon.EAST) == nb || sti.getTurnout(SlipTurnoutIcon.WEST) == nb
                            || sti.getTurnout(SlipTurnoutIcon.LOWEREAST) == nb || sti.getTurnout(SlipTurnoutIcon.LOWERWEST) == nb) {
                        switch (menu) {
                            case VIEWPOPUPONLY:
                                pos.getPopupUtility().addViewPopUpMenu(item);
                                break;
                            case EDITPOPUPONLY:
                                pos.getPopupUtility().addEditPopUpMenu(item);
                                break;
                            default:
                                pos.getPopupUtility().addEditPopUpMenu(item);
                                pos.getPopupUtility().addViewPopUpMenu(item);
                        }
                        return;
                    }
                }
            } else if (pos instanceof MultiSensorIcon) {
                if (pos.getPopupUtility() != null) {
                    MultiSensorIcon msi = (MultiSensorIcon) pos;
                    boolean match = false;
                    for (int i = 0; i < msi.getNumEntries(); i++) {
                        if (msi.getSensorName(i).equals(nb.getUserName())) {
                            match = true;
                            break;
                        } else if (msi.getSensorName(i).equals(nb.getSystemName())) {
                            match = true;
                            break;
                        }
                    }
                    if (match) {
                        switch (menu) {
                            case VIEWPOPUPONLY:
                                pos.getPopupUtility().addViewPopUpMenu(item);
                                break;
                            case EDITPOPUPONLY:
                                pos.getPopupUtility().addEditPopUpMenu(item);
                                break;
                            default:
                                pos.getPopupUtility().addEditPopUpMenu(item);
                                pos.getPopupUtility().addViewPopUpMenu(item);
                        }
                        return;
                    }
                }
            }
        }
    }

    public final static int VIEWPOPUPONLY = 0x00;
    public final static int EDITPOPUPONLY = 0x01;
    public final static int BOTHPOPUPS = 0x02;

    /**
     * Relocate item.
     * <p>
     * Note that items can not be moved past the left or top edges of the panel.
     *
     * @param p      The item to move.
     * @param deltaX The horizontal displacement.
     * @param deltaY The vertical displacement.
     */
    public void moveItem(Positionable p, int deltaX, int deltaY) {
        //log.debug("moveItem at ({},{}) delta ({},{})", p.getX(), p.getY(), deltaX, deltaY);
        if (getFlag(OPTION_POSITION, p.isPositionable())) {
            int xObj = getItemX(p, deltaX);
            int yObj = getItemY(p, deltaY);
            // don't allow negative placement, icon can become unreachable
            if (xObj < 0) {
                xObj = 0;
            }
            if (yObj < 0) {
                yObj = 0;
            }
            p.setLocation(xObj, yObj);
            // and show!
            p.repaint();
        }
    }

    /**
     * Return a List of all items whose bounding rectangle contain the mouse
     * position. ordered from top level to bottom
     *
     * @param event contains the mouse position.
     * @return a list of positionable items or an empty list.
     */
    protected List<Positionable> getSelectedItems(JmriMouseEvent event) {
        Rectangle rect = new Rectangle();
        ArrayList<Positionable> selections = new ArrayList<>();
        for (Positionable p : _contents) {
            double x = event.getX();
            double y = event.getY();
            rect = p.getBounds(rect);
            if (p instanceof jmri.jmrit.display.controlPanelEditor.shape.PositionableShape
                    && p.getDegrees() != 0) {
                double rad = p.getDegrees() * Math.PI / 180.0;
                java.awt.geom.AffineTransform t = java.awt.geom.AffineTransform.getRotateInstance(-rad);
                double[] pt = new double[2];
                // bit shift to avoid SpotBugs paranoia
                pt[0] = x - rect.x - (rect.width >>> 1);
                pt[1] = y - rect.y - (rect.height >>> 1);
                t.transform(pt, 0, pt, 0, 1);
                x = pt[0] + rect.x + (rect.width >>> 1);
                y = pt[1] + rect.y + (rect.height >>> 1);
            }
            Rectangle2D.Double rect2D = new Rectangle2D.Double(rect.x * _paintScale,
                    rect.y * _paintScale,
                    rect.width * _paintScale,
                    rect.height * _paintScale);
            if (rect2D.contains(x, y) && (p.getDisplayLevel() > BKG || event.isControlDown())) {
                boolean added = false;
                int level = p.getDisplayLevel();
                for (int k = 0; k < selections.size(); k++) {
                    if (level >= selections.get(k).getDisplayLevel()) {
                        selections.add(k, p);
                        added = true;       // OK to lie in the case of background icon
                        break;
                    }
                }
                if (!added) {
                    selections.add(p);
                }
            }
        }
        //log.debug("getSelectedItems at ({},{}) {} found,", x, y, selections.size());
        return selections;
    }

    /*
     * Gather all items inside _selectRect
     * Keep old group if Control key is down
     */
    protected void makeSelectionGroup(JmriMouseEvent event) {
        if (!event.isControlDown() || _selectionGroup == null) {
            _selectionGroup = new ArrayList<>();
        }
        Rectangle test = new Rectangle();
        List<Positionable> list = getContents();
        if (event.isShiftDown()) {
            for (Positionable comp : list) {
                if (_selectRect.intersects(comp.getBounds(test))
                        && (event.isControlDown() || comp.getDisplayLevel() > BKG)) {
                    _selectionGroup.add(comp);
                    //log.debug("makeSelectionGroup: selection: {}, class= {}", comp.getNameString(), comp.getClass().getName());
                }
            }
        } else {
            for (Positionable comp : list) {
                if (_selectRect.contains(comp.getBounds(test))
                        && (event.isControlDown() || comp.getDisplayLevel() > BKG)) {
                    _selectionGroup.add(comp);
                    //log.debug("makeSelectionGroup: selection: {}, class= {}", comp.getNameString(), comp.getClass().getName());
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("makeSelectionGroup: {} selected.", _selectionGroup.size());
        }
        if (_selectionGroup.size() < 1) {
            _selectRect = null;
            deselectSelectionGroup();
        }
    }

    /*
     * For the param, selection, Add to or delete from _selectionGroup.
     * If not there, add.
     * If there, delete.
     * make new group if Cntl key is not held down
     */
    protected void modifySelectionGroup(Positionable selection, JmriMouseEvent event) {
        if (!event.isControlDown() || _selectionGroup == null) {
            _selectionGroup = new ArrayList<>();
        }
        boolean removed = false;
        if (event.isControlDown()) {
            if (selection.getDisplayLevel() > BKG) {
                if (_selectionGroup.contains(selection)) {
                    removed = _selectionGroup.remove(selection);
                } else {
                    _selectionGroup.add(selection);
                }
            } else if (event.isShiftDown()) {
                if (_selectionGroup.contains(selection)) {
                    removed = _selectionGroup.remove(selection);
                } else {
                    _selectionGroup.add(selection);
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("modifySelectionGroup: size= {}, selection {}", _selectionGroup.size(), (removed ? "removed" : "added"));
        }
    }

    /**
     * Set attributes of a Positionable.
     *
     * @param newUtil helper from which to get attributes
     * @param p       the item to set attributes of
     *
     */
    public void setAttributes(PositionablePopupUtil newUtil, Positionable p) {
        p.setPopupUtility(newUtil.clone(p, p.getTextComponent()));
        int mar = newUtil.getMargin();
        int bor = newUtil.getBorderSize();
        Border outlineBorder;
        if (bor == 0) {
            outlineBorder = BorderFactory.createEmptyBorder(0, 0, 0, 0);
        } else {
            outlineBorder = new LineBorder(newUtil.getBorderColor(), bor);
        }
        Border borderMargin;
        if (newUtil.hasBackground()) {
            borderMargin = new LineBorder(p.getBackground(), mar);
        } else {
            borderMargin = BorderFactory.createEmptyBorder(mar, mar, mar, mar);
        }
        p.setBorder(new CompoundBorder(outlineBorder, borderMargin));

        if (p instanceof PositionableLabel) {
            PositionableLabel pos = (PositionableLabel) p;
            if (pos.isText()) {
                int deg = pos.getDegrees();
                pos.rotate(0);
                if (deg == 0) {
                    p.setOpaque(newUtil.hasBackground());
                } else {
                    pos.rotate(deg);
                }
            }
        } else if (p instanceof PositionableJPanel) {
            p.setOpaque(newUtil.hasBackground());
            p.getTextComponent().setOpaque(newUtil.hasBackground());
        }
        p.updateSize();
        p.repaint();
        if (p instanceof PositionableIcon) {
            NamedBean bean = p.getNamedBean();
            if (bean != null) {
                ((PositionableIcon) p).displayState(bean.getState());
            }
        }
    }

    protected void setSelectionsAttributes(PositionablePopupUtil util, Positionable pos) {
        if (_selectionGroup != null && _selectionGroup.contains(pos)) {
            for (Positionable p : _selectionGroup) {
                if (p instanceof PositionableLabel) {
                    setAttributes(util, p);
                }
            }
        }
    }

    protected void setSelectionsHidden(boolean enabled, Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.setHidden(enabled);
            }
        }
    }

    protected boolean setSelectionsPositionable(boolean enabled, Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.setPositionable(enabled);
            }
            return true;
        } else {
            return false;
        }
    }

    protected void removeSelections(Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.remove();
            }
            deselectSelectionGroup();
        }
    }

    protected void setSelectionsScale(double s, Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.setScale(s);
            }
        } else {
            p.setScale(s);
        }
    }

    protected void setSelectionsRotation(int k, Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.rotate(k);
            }
        } else {
            p.rotate(k);
        }
    }

    protected void setSelectionsDisplayLevel(int k, Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable comp : _selectionGroup) {
                comp.setDisplayLevel(k);
            }
        } else {
            p.setDisplayLevel(k);
        }
    }

    protected void setSelectionsDockingLocation(Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable pos : _selectionGroup) {
                if (pos instanceof LocoIcon) {
                    ((LocoIcon) pos).setDockingLocation(pos.getX(), pos.getY());
                }
            }
        } else if (p instanceof LocoIcon) {
            ((LocoIcon) p).setDockingLocation(p.getX(), p.getY());
        }
    }

    protected void dockSelections(Positionable p) {
        if (_selectionGroup != null && _selectionGroup.contains(p)) {
            for (Positionable pos : _selectionGroup) {
                if (pos instanceof LocoIcon) {
                    ((LocoIcon) pos).dock();
                }
            }
        } else if (p instanceof LocoIcon) {
            ((LocoIcon) p).dock();
        }
    }

    protected boolean showAlignPopup(Positionable p) {
        return _selectionGroup != null && _selectionGroup.contains(p);
    }

    public Rectangle getSelectRect() {
        return _selectRect;
    }

    public void drawSelectRect(int x, int y) {
        int aX = getAnchorX();
        int aY = getAnchorY();
        int w = x - aX;
        int h = y - aY;
        if (x < aX) {
            aX = x;
            w = -w;
        }
        if (y < aY) {
            aY = y;
            h = -h;
        }
        _selectRect = new Rectangle((int) Math.round(aX / _paintScale), (int) Math.round(aY / _paintScale),
                (int) Math.round(w / _paintScale), (int) Math.round(h / _paintScale));
    }

    public final int getAnchorX() {
        return _anchorX;
    }

    public final int getAnchorY() {
        return _anchorY;
    }

    public final int getLastX() {
        return _lastX;
    }

    public final int getLastY() {
        return _lastY;
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (_selectionGroup == null) {
            return;
        }
        int x = 0;
        int y = 0;
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
                y = -1;
                break;
            case KeyEvent.VK_DOWN:
                y = 1;
                break;
            case KeyEvent.VK_LEFT:
                x = -1;
                break;
            case KeyEvent.VK_RIGHT:
                x = 1;
                break;
            default:
                log.warn("Unexpected e.getKeyCode() of {}", e.getKeyCode());
                break;
        }
        //A cheat if the shift key isn't pressed then we move 5 pixels at a time.
        if (!e.isShiftDown()) {
            y = y * 5;
            x = x * 5;
        }
        for (Positionable comp : _selectionGroup) {
            moveItem(comp, x, y);
        }
        _targetPanel.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
        NamedBean nb = (NamedBean) evt.getOldValue();
        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
            StringBuilder message = new StringBuilder();
            message.append(Bundle.getMessage("VetoInUseEditorHeader", getName())); // NOI18N
            message.append("<br>");
            boolean found = false;
            int count = 0;
            for (Positionable p : _contents) {
                if (nb.equals(p.getNamedBean())) {
                    found = true;
                    count++;
                }
            }
            if (found) {
                message.append(Bundle.getMessage("VetoFoundInPanel", count));
                message.append("<br>");
                message.append(Bundle.getMessage("VetoReferencesWillBeRemoved")); // NOI18N
                message.append("<br>");
                throw new PropertyVetoException(message.toString(), evt);
            }
        } else if ("DoDelete".equals(evt.getPropertyName())) { // NOI18N
            ArrayList<Positionable> toDelete = new ArrayList<>();
            for (Positionable p : _contents) {
                if (nb.equals(p.getNamedBean())) {
                    toDelete.add(p);
                }
            }
            for (Positionable p : toDelete) {
                removeFromContents(p);
                _targetPanel.repaint();
            }
        }
    }

    /*
     * ********************* Abstract Methods ***********************
     */
    @Override
    abstract public void mousePressed(JmriMouseEvent event);

    @Override
    abstract public void mouseReleased(JmriMouseEvent event);

    @Override
    abstract public void mouseClicked(JmriMouseEvent event);

    @Override
    abstract public void mouseDragged(JmriMouseEvent event);

    @Override
    abstract public void mouseMoved(JmriMouseEvent event);

    @Override
    abstract public void mouseEntered(JmriMouseEvent event);

    @Override
    abstract public void mouseExited(JmriMouseEvent event);

    /*
     * set up target panel, frame etc.
     */
    abstract protected void init(String name);

    /*
     * Closing of Target frame window.
     */
    abstract protected void targetWindowClosingEvent(WindowEvent e);

    /**
     * Called from TargetPanel's paint method for additional drawing by editor
     * view.
     *
     * @param g the context to paint within
     */
    abstract protected void paintTargetPanel(Graphics g);

    /**
     * Set an object's location when it is created.
     *
     * @param obj the object to locate
     */
    abstract protected void setNextLocation(Positionable obj);

    /**
     * After construction, initialize all the widgets to their saved config
     * settings.
     */
    abstract public void initView();

    /**
     * Set up item(s) to be copied by paste.
     *
     * @param p the item to copy
     */
    abstract protected void copyItem(Positionable p);

    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
        List<NamedBeanUsageReport> report = new ArrayList<>();
        if (bean != null) {
            getContents().forEach((pos) -> {
                String data = getUsageData(pos);
                if (pos instanceof MultiSensorIcon) {
                    MultiSensorIcon multi = (MultiSensorIcon) pos;
                    multi.getSensors().forEach((sensor) -> {
                        if (bean.equals(sensor)) {
                            report.add(new NamedBeanUsageReport("PositionalIcon", data));
                        }
                    });

                } else if (pos instanceof SlipTurnoutIcon) {
                    SlipTurnoutIcon slip3Scissor = (SlipTurnoutIcon) pos;
                    if (bean.equals(slip3Scissor.getTurnout(SlipTurnoutIcon.EAST))) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }
                    if (bean.equals(slip3Scissor.getTurnout(SlipTurnoutIcon.WEST))) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }
                    if (slip3Scissor.getNamedTurnout(SlipTurnoutIcon.LOWEREAST) != null) {
                        if (bean.equals(slip3Scissor.getTurnout(SlipTurnoutIcon.LOWEREAST))) {
                            report.add(new NamedBeanUsageReport("PositionalIcon", data));
                        }
                    }
                    if (slip3Scissor.getNamedTurnout(SlipTurnoutIcon.LOWERWEST) != null) {
                        if (bean.equals(slip3Scissor.getTurnout(SlipTurnoutIcon.LOWERWEST))) {
                            report.add(new NamedBeanUsageReport("PositionalIcon", data));
                        }
                    }

                } else if (pos instanceof LightIcon) {
                    LightIcon icon = (LightIcon) pos;
                    if (bean.equals(icon.getLight())) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }

                } else if (pos instanceof ReporterIcon) {
                    ReporterIcon icon = (ReporterIcon) pos;
                    if (bean.equals(icon.getReporter())) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }

                } else if (pos instanceof AudioIcon) {
                    AudioIcon icon = (AudioIcon) pos;
                    if (bean.equals(icon.getAudio())) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }

                } else {
                    if (bean.equals(pos.getNamedBean())) {
                        report.add(new NamedBeanUsageReport("PositionalIcon", data));
                    }
               }
            });
        }
        return report;
    }

    String getUsageData(Positionable pos) {
        Point point = pos.getLocation();
        return String.format("%s :: x=%d, y=%d",
                pos.getClass().getSimpleName(),
                Math.round(point.getX()),
                Math.round(point.getY()));
    }

    // initialize logging
    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Editor.class);
}