package nxm.sys.libg;

import static nxm.sys.libg.GraphicsUtil.invokeAndWait;
import static nxm.sys.libg.GraphicsUtil.invokeLater;

import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.Keyable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Command;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Registry;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Util;
import nxm.sys.prim.panel;
import nxm.sys.prim.panel.Pane;

/** A class to open and maintain a graphical window with a Midas context. <br>
    <br>
    A {@link MWindow} does not necessarily represent a top-level window and
    may just represent one of many windows that have been added to another
    MWindow (see {@link #addTo(Object)}). <br>
    <br>
    Each {@link MWindow} has a <code>Panel</code> and a <code>Parent</code>:
    <code>
      Panel  - The underlying graphical panel (usually is either {@link MPanel}
               or {@link MJPanel}).

      Parent - The parent this is being displayed in (usually is another
               {@link MWindow} or a {@link MidasFrame}).
    </code>
    <h2>A Note About Positions</h2>
    All positions (unless otherwise noted) are measured in pixels from the upper left corner of
    the display to the *inside corner* of the window (this choice was made for historical reasons).
    This means that when pos is set to (0,0,w,h) for a PLOT the title bar of the window should be
    off the top of the screen and the upper-left corner of the PLOT (inside the window) will be at
    the upper left of the screen (i.e. at (0,0)). Some window managers will automatically reposition
    windows that would be off the display (others do vertical repositioning only and some do nothing
    at all). For this reason it is strongly suggested that windows not be set along the edges of the
    display (usually a gap of 25 pixels works well).<br>
    <br>
    Take note that this means that MWindow's pos values differ from those of the Java frame the
    top-level MWindow is in by an amount equal to the insets of the Java frame.

    @author  Jeff Schoen
*/
public class MWindow implements MessageHandler, Keyable, MouseListener,
                                MouseMotionListener, KeyListener,
                                ComponentListener, FocusListener {
  private static final boolean DEBUG = false; // Turn on debugging for addTo(..) and addOn(..)

  /** The minimum allowable panel height (in pixels). */
  protected static final int MINIMUM_PANEL_HEIGHT = 1;

  /** The minimum allowable panel width (in pixels). */
  protected static final int MINIMUM_PANEL_WIDTH  = 1;

  /** The list of available cursors (see {@link Cursor}). Unlike most lists the first index is -1.*/
  public static final String cursorList = "Custom,Default,Crosshair,Text,Wait,"
                                        + "SWResize,SEResize,NWResize,NEResize,"
                                        + "NResize,SResize,WResize,EResize,Hand,Move";

  /** Timer for resetting cursor, used only by setCursor(..) methods. */
  private javax.swing.Timer cursorTimer = null; // null = no active timer

  /** The list of valid Theme names. */
  public static final String themeList = Theme.nameList;

  /** constant for left mouse button (LMB) clicked. Since 3.3.1. */
  public static final int LEFT_MOUSE_BUTTON   = 1;
  /** constant for middle mouse button (MMB) clicked. Since 3.3.1. */
  public static final int MIDDLE_MOUSE_BUTTON = 2;
  /** constant for right mouse button (RMB) clicked. Since 3.3.1. */
  public static final int RIGHT_MOUSE_BUTTON  = 3;

  /** CENTERed drawString flag */
  public static final int CENTER = 0x001;
  /** RIGHT    drawString flag */
  public static final int RIGHT  = 0x002;
  /** BOLD     drawString flag */
  public static final int BOLD   = 0x004;
  /** HILIghT  drawString flag */
  public static final int HILIT  = 0x008;
  /** LOLIghT  drawString flag */
  public static final int LOLIT  = 0x010;

  /** Constant that specifies this window is displayed maximized. */
  public static final int MAXIM = 2;

  /** Constant that specifies this window is displayed in front of other windows. */
  public static final int FRONT = 1;

  /** Constant that specifies this window is displayed in back of other windows. */
  public static final int BACK = 0;

  /** Constant that specifies this window is iconified. */
  public static final int ICON = -1;

  /** Constant that specifies this window is initializing. */
  public static final int INIT = -2;

  /** Constant that specifies this window has been exited. */
  public static final int EXIT = -3;

  /** Comma-delimited list of valid display states for this window. */
  public static final String stateList = "Exit,Init,Icon,Back,Front,Maximized";

  /** Event filter flag indicating mouse events should be ignored. */
  public static final int NOMOUSE = 0x1;

  /** Event filter flag indicating key events should be ignored. */
  public static final int NOKEY = 0x2;

  /** Event filter flag indicating this window cannot be iconified. */
  public static final int NOICON = 0x4;

  /** Event filter flag indicating this window cannot be moved. */
  public static final int NOMOVE=0x8;

  /** Event filter flag indicating this window cannot be refreshed. */
  public static final int NOREFRESH=0x10;

  /** Event filter flag indicating this window cannot be pushed/poped out of PANEL. */
  public static final int NOPUSHPOP=0x20;

  /** Event filter flag indicating middle mouse button (MMB) events should be ignored. Since 3.3.1. */
  public static final int NOMIDDLEMOUSE=0x40;

  /** Comma-delimited string of valid event filter flags for this window. */
  public static final String eventFilterList =
    "NoMouse,NoKeyBoard,NoIconify,NoMove,NoRefresh,NoPushPop,NoMiddleMouse";

  /** Warn when AWT Component or Container is being added to Swing
   *  @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static final int ADD_AWT_TO_SWING = 1;

  /** Warn when overriding existing plot options (rather than adding to or subtracting)
   *  @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static final int OVERRIDING_EXISTING_PLOT_OPTIONS = 2;

  /** Comma-delimited string of valid warning mask options.
   *  @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static final String warningsList =
    "AddAWTToSwing,OverridingExistingPlotOptions";

  /** Boolean value to inform MWindow that the panel is a Midas Display
   *  @since NeXtMidas 3.8.1
   */
  boolean isMidasDisplayWithSwingPopups = false;

  /** Indicates which event filter flags are set (see {@link #eventFilterList}). */
  @InternalUseOnly
  public int eventFilter;

  /** The window name. */
  @InternalUseOnly("since 3.5.2")
  public String name;

  /** A User defined window to use.  */
  private String userDisplay = null;

  /** The parent window (see {@link #pushParent}). */
  @InternalUseOnly("since 3.5.2")
  public Object parent;

  /** The parent window, prior to this window being popped (see {@link #parent}). */
  @InternalUseOnly("since 3.5.2")
  public Object pushParent;

  /** The top-level MWindow. */
  @InternalUseOnly("since 3.5.2")
  public MWindow MW;

  /** The name of this window. */
  protected String title;

  /** The message handler for this window. */
  @InternalUseOnly("since 3.5.2")
  public MessageHandler mh;

  /** The command embedded in this window.
      @since NeXtMidas 3.5.4 (Bug 2895)
    */
  private String embeddedCommandID;

  /** The graphics Panel for this window. */
  @InternalUseOnly("since 3.5.2")
  public Container panel;

  /** The Graphics Context for this window.
   *  @deprecated in 3.9.0 to solve graphical timing issues (NXM-3213)
   */
  @Deprecated
  public Graphics g;

  /** The Graphics Font for this window. */
  @InternalUseOnly("since 3.5.2")
  public Font font;

  /** The FontMetrics for the current Font. */
  @InternalUseOnly("since 3.5.2")
  public FontMetrics fm;

  /** Boolean indicating the headless state of this window. Defaults to false (not headless). */
  @InternalUseOnly("since 3.5.2")
  public boolean headless;

  /** Boolean indicating that this Window has been popped out
   *  @since NeXtMidas 3.8.1 for NXM-3213
   */
  private volatile boolean isPoppedOut = false;

  /** Boolean indicating that this window's graphics should be drawn on a Swing
      panel rather than an AWT panel.
   */
  @InternalUseOnly("since 3.5.2")
  public boolean useSwing = (Theme.getOption(Theme.SWING) || Theme.getOption(Theme.MOBILE));

  /** Which JLayeredPane layer to use */
  protected boolean isPopup;

  /** The bounds of the previously popped window... null after the first push->pop */
  private Rectangle popBounds = null;

  /** The display (monitor) where the MWindow will appear when popped */
  private GraphicsDevice popDev = null;  // Since 3.1.0 (Bug 2458)

  /** Defaults to true. Set to false to disable the CTRL+click popouts */
  private boolean enableAdvancedPopOut = true;  // Since 3.1.0 (Bug 2458)

  /** The default window position (see {@link #pos}).  */
  @InternalUseOnly("since 3.5.2")
  public static final MBox DEFAULT_WINDOW_POSITION = new MBox (100,100,700,500);

  /** Current Window position (see note at top of {@link MWindow} class). */
  @InternalUseOnly("since 3.5.2")
  public MBox pos = new MBox (DEFAULT_WINDOW_POSITION);

  /** The x, y, and z-coordinate of the current pointer position. */
  @InternalUseOnly("since 3.5.2")
  public int px,py,pz;

  /** Boolean flag indicating whether the pointer is currently active over this MWindow. */
  @InternalUseOnly("since 3.5.2")
  public boolean pactive;

  /** Boolean flag indicating whether the pointer is currently active over this MWindow. */
  @InternalUseOnly("since 3.5.2")
  public boolean hasFocus;

  /** Current drag box rectangle (used by {@link #rubberbox(int)}, see {@link #rbox}). */
  protected MBox box = new MBox(-1,-1,0,0);

  /** Stores location and size of previous drag box (used by {@link #rubberbox(int)}, see
      {@link #box}).. Used to redraw drag box in <code>XOR</code> Mode to restore pixels. Once
      pixels are restored, values are reset to <code>(0,0,0,0)</code>.
   */
  protected MBox rbox = new MBox(0,0,0,0);

  /** Indicates dragging is currently taking place on this window. */
  protected int dragging = 0;

  /** Current Theme of this window (BG/FG and widget colors). Defaults to using
      the theme specified by {@link Theme#global()}.
   */
  @InternalUseOnly("since 3.5.2")
  public Theme theme = (Theme) Theme.global();

  /** Indicates current display status. */
  @InternalUseOnly("since 3.5.2")
  public int status;

  /** Represents the mouse button pressed (1=Left, 2=Middle, or 3=Right) during the last mouseReleased MouseEvent. */
  @InternalUseOnly("since 3.5.2")
  public int button;

  /** the button mask of the mouse button pressed during the last mouseReleased MouseEvent.  */
  @InternalUseOnly("since 3.5.2")
  public int modifiers;

  /** Indicates the key being pressed or released in a KeyEvent. */
  @InternalUseOnly("since 3.5.2")
  public int keycode;

  /** The time at which a mouse button is pressed. */
  @InternalUseOnly("since 3.5.2")
  public long dtime; // TODO: make private, protected or package-private in 3.9+

  /** Indicates the amount of time a mouse button is held down. */
  @InternalUseOnly("since 3.5.2")
  public long htime; // TODO: make private, protected or package-private in 3.9+

  /** Help Key Listener **/
  @InternalUseOnly("since 3.5.2")
  public HelpKeyListener hkl;

  /** Graphics clip rectangle
   *  @deprecated in 3.9.0 to fix graphical timing issues
   */
  @Deprecated
  Rectangle clip; // package-private: used only in nxm.sys.libg

  /** current cursor for container panel */
  private Cursor cursor = null;

  /** This is used to tell if we are scrolling to prevent dragbox from drawing. */
  protected boolean isScrolling;

  /** default color of the crosshairs. Since NeXtMidas 2.9.1 for Bug 2347 */
  protected Color crossHairsColor = theme.cbg;

  /** location of mouse press in {@link #mousePressed(MouseEvent)}, see also {@link #mouseReleased(MouseEvent)}. */
  private Point press = null;

  private boolean shareTheme = false;

  /** Allow the temporary changes (e.g. mouse hovers) to the Title of the frame */
  private boolean noAltTitle  = false;

  /** Size (in pixels) of the scrollbar knob (along scrolling direction). Since NeXtMidas 3.7.0 for Bug 2825 */
  static final int SCROLL_KNOB_SIZE = 16;

  /** mask of current lower-level MWindow and sub-class warnings that are enabled */
  static int warningsMask = 0;

  /** Initialize the class, no parameters means the graphics panel will not be created. */
  public MWindow () {
    this(null,null,null);
  }

  /** Initialize the class using default SWING value (from Theme) and create the graphics panel.
      @param name   The name of the MWindow
      @param mh     The messageHandler
   */
  public MWindow (String name, MessageHandler mh) {
    this(name,mh,null);
  }

  /** Initialize the class using default SWING value (from Theme) and create the graphics panel.
      @param name   The name of the MWindow
      @param mh     The messageHandler
      @param parent The parent object (if known at instantiation - always is for popups)
   */
  public MWindow (String name, MessageHandler mh, Object parent) {
    isPopup = (parent instanceof MWindow);
    if (isPopup) theme = ((MWindow)parent).theme;
    useSwing = Theme.getOption(Theme.SWING);
    if (this instanceof GWidget) useSwing &= Theme.getOption(isPopup?Theme.SWINGPOPUPS:Theme.SWINGWIDGETS);
    if (Theme.getOption(Theme.MOBILE)) useSwing = true;

    status = INIT;
    hkl = new HelpKeyListener(this);
    if (Theme.isHeadless()) { // since 3.5.0 for BUG 2817 (Change to Theme.isHeadless() as it skips an extra method call.
      headless = true;
      boolean headlessSwitch = false;
      if (mh instanceof Command) {
        this.embeddedCommandID =  ((Command)mh).getID(); // since NeXtMidas 3.5.4 for BUG 2859
        headlessSwitch = ((Command)mh).args.getState("/HEADLESS"); // Check if user specified /HEADLESS
      }
      boolean userSpecHeadless = headlessSwitch || Theme.getOption(Theme.HEADLESS); // Check if Headless set in environment
      if (!userSpecHeadless && Theme.getHeadlessInitStatus() != 0) { //if Headless, and Headless not specified
        String warningText = "/HEADLESS not set on a headless system. Error Code: "+Theme.getHeadlessInitStatus();
        HeadlessException headlessEx = new HeadlessException(warningText);
        throw headlessEx;
      }
      // The following is from Jeff's changes. It creates a swing panel if in headless mode since 3.9.0
      MJPanel mpanel = new MJPanel();
      mpanel.setMWindow(this);
      panel = (Container)mpanel;
    } else if (useSwing) {
      MJPanel mpanel = new MJPanel();
      mpanel.setMWindow(this);
      panel = mpanel;
    } else { // AWT
      MPanel mpanel = new MPanel();
      mpanel.setMWindow(this);
      panel = mpanel;
    }

    this.title = name;
    this.name = name;
    this.mh = mh;
  }

  /** Open the graphics window */
  public void open () {
    if (headless || Shell.isHeadless()) return;
    font = new Font ("Monospaced",Font.PLAIN,12);
    fm = panel.getFontMetrics(font);
  }

  /** Set the headless state of this window
      @param value the boolean representing the headless state.
   */
  public void setHeadless (boolean value) {
    headless = value;
  }

  /** Sets the window name.
      @param name The window name.
   */
  public void setName (String name) {
    this.name = name;
  }

  /** Set the class to handle action messages.
      @param mh The MessageHandler for this MWindow.
   */
  public void setHandler (MessageHandler mh) {
    this.mh = mh;
  }

  /** Attempt to set the Swing Component ToolTipText
      @param text The text to set
      @return True if the tool tip was set, else False
      @since NeXtMidas 2.9.0 (Bug 2240)
   */
  public boolean setToolTipText (String text) {
    if (panel instanceof JPanel) {
      ((JComponent)panel).setToolTipText(text);
      return true;
    } else return false;
  }

  /** Get the Tool Tip Text, if any.
      @return The text or  null if no text or not a Swing Component
   */
  public String getToolTipText () {
    if (panel instanceof JPanel) return ((JPanel)panel).getToolTipText();
    else return null;
  }

  /** Sets the size and location of the window.
      @param mb the MBox containing the size and location for this window.
   */
  public void setBounds (MBox mb) {
    if (mb != null) setBounds (mb.x,mb.y,mb.w,mb.h);   // check for null added in 2.9.2 (see Bug 1713)
    else Shell.warning("Cannot set bounds from a null MBox");
  }

  /** Sets the size of the window.
      @param w the width of this window.
      @param h the height of this window.
   */
  public void setSize (int w, int h) {
    pos.w=w; pos.h=h;
    if ((MW!=null) && (panel!=null)) panel.setSize(w,h);

    if (parent instanceof Frame) {
      ((Frame)parent).setSize(w,h);
    }
  }

  /** Moves this window to a new location. The top-left corner of the new
      location is specified by the <code>x</code> and <code>y</code> parameters.
      @param x the <I>x</I>-coordinate of the new window location.
      @param y the <I>y</I>-coordinate of the new window location.
   */
  public void setLocation (int x, int y) {
    pos.x=x; pos.y=y;
    if ((MW!=null) && (panel!=null)) {
      panel.setLocation (x,y);
    }
    if (parent instanceof Frame) ((Frame)parent).setLocation(x,y);
  }

  /** Sets the size and location of the window.
      @param x The <I>x</I>-coordinate of the new window location.
      @param y The <I>y</I>-coordinate of the new window location.
      @param w The new width of the window (if negative, it will be set to 0)
      @param h The new height of the window (if negative, it will be set to 0)
   */
  public void setBounds (int x, int y, int w, int h) {

    if(parent instanceof GDialog) setBoundsLater(x, y, w, h); // When GDialog is created, it blocks. This solved the problem by using the invokeLater
    else setBoundsWait(x,y,w,h); // Uses the invokeAndWait
  }

  /**
   * This is used to set the bounds of the gwidget using the invokeAndWait function. This will block until the Event
   * Dispatch Thread has completed the runnable provided.
   * @since NeXtMidas 3.9.0
   * @param x The <I>x</I>-coordinate of the new window location.
   * @param y The <I>y</I>-coordinate of the new window location.
   * @param w The new width of the window (if negative, it will be set to 0)
   * @param h The new height of the window (if negative, it will be set to 0)
   */
  private void setBoundsWait(int x, int y, int w, int h) {
    pos.x=x; pos.y=y; pos.w=w; pos.h=h;

    if (MW != null) { // invokeAndWait added in 3.9.0 for graphical timing issues
      invokeAndWait( () -> {
        if (panel != null) panel.setBounds(x, y, w, h);
        if (parent instanceof Frame) ((Component) parent).setBounds(x, y, w, h);

      });
    }
    else
      if (parent instanceof Frame) {
        invokeAndWait( () -> {
          ((Component) parent).setBounds(x, y, w, h);
        });
      }
  }

  /**
   * This is used to set the bounds of the gwidget using the invokeLater function. This will not block until returned.
   * @since NeXtMidas 3.9.0
   * @param x The <I>x</I>-coordinate of the new window location.
   * @param y The <I>y</I>-coordinate of the new window location.
   * @param w The new width of the window (if negative, it will be set to 0)
   * @param h The new height of the window (if negative, it will be set to 0)
   */
  private void setBoundsLater (int x, int y, int w, int h) {
    pos.x=x; pos.y=y; pos.w=w; pos.h=h;

    if (MW != null) { // invokeAndWait added in 3.8.1 for graphical timing issues
      invokeLater( () -> {
        if (panel != null) panel.setBounds(x, y, w, h);
        if (parent instanceof Frame) ((Component) parent).setBounds(x, y, w, h);

      });
    }
    else
      if (parent instanceof Frame) {
        invokeLater( () -> {
          ((Component) parent).setBounds(x, y, w, h);
        });
      }
  }

//This is the old way that the bounds were set - Did not use the Event Dispatch Queue
//  public void setBounds (int x, int y, int w, int h) {
//          pos.x=x; pos.y=y; pos.w=w; pos.h=h;
//          if ((MW!=null) && (panel!=null)) {
//            panel.setBounds (x,y,w,h);
//            if (g!=null) {
//              g.setClip(null);
//              g.setClip (0,0,w,h);
//            }
//          }
//          if (parent instanceof Frame) ((Frame)parent).setBounds(x,y,w,h);
//        }

  /** Sets the color scheme by name with the specified inline mode.
      @param themeName The String representing one of the valid theme names as
                       defined in the Theme class (see {@link Theme#nameList}).
      @param inline    Boolean value representing whether the theme is inline
                       or not (see {@link Theme#setInline(boolean)}).
   */
  public void setTheme (String themeName, boolean inline) {
    theme.setInline(inline);
    setTheme(themeName);
  }

  /** Sets the color scheme by name with the specified inline mode.
      @param themeObj The Java Object representing one of the valid themes as
                       defined in the Theme class (see {@link Theme#nameList}).
      @param inline    Boolean value representing whether the theme is inline
                       or not (see {@link Theme#setInline(boolean)}).
   */
  public void setTheme (Object themeObj, boolean inline) {
    theme.setInline(inline);
         if (themeObj instanceof Theme) setTheme((Theme)themeObj);
    else if (themeObj instanceof Table) setTheme((Table)themeObj);
    else if (themeObj instanceof String) setTheme((String)themeObj);
  }

  /** Sets the color scheme by name with the default inline mode.
      @param themeName The String representing one of the valid theme names as
                       defined in the Theme class (see {@link Theme#nameList}).
   */
  public void setTheme (String themeName) {
    if (themeName==null || themeName.length()==0) return;
    theme.setByName(themeName);
    if (themeName.startsWith("MNM")) Theme.setOptions("+MOBILE"); // for Jeff's Bug 1779 and Bug 1989
    resetTheme(theme);
  }

  /** Sets the theme for this Window
      @param obj new theme for this Window as a Java Object
   */
  public void setTheme (Object obj) {
    if (obj instanceof String) setTheme((String)obj);
    if (!(obj instanceof Table)) return;
    Table themeTable = (Table)obj;
    if (themeTable==null || themeTable.getSize()==0) return;
    theme.createTheme(themeTable);
    resetTheme(theme);
  }

  private void resetTheme (Theme theme) {
    setTheme(theme);
    if (status>=FRONT) panel.repaint();
  }

  /** Sets the theme for this Window
      @param theme new theme for this Window
   */
  public void setTheme (Theme theme) {
    this.theme = theme;
    if (!headless && panel != null) { // since 3.5.0. Skip if in Headless state. BUG 2817
      panel.setBackground(theme.cbg);
      panel.setForeground(theme.cfg);
    }
  }

  /** Set event filtering for disabling mouse|keyboard events.
      @param mask the mask to use to set event filtering (from {@link #eventFilterList}
   */
  public void setEventFilter (String mask) {
    int ef = 0;
    if (mask.equals("ENABLE")) ef=0;        // turn off all filters (none)
    else if (mask.equals("DISABLE")) ef=-1; // turn on all filters
    else ef = Parser.mask(eventFilterList,mask,eventFilter);
    setEventFilter(ef);
  }

  /** Set event filtering for disabling mouse|keyboard events.
      @param mask the mask to use to set event filtering.
   */
  public void setEventFilter (int mask) {
    eventFilter = mask;
  }

  /** Perform necessary steps to set up this window graphically, such as
      calculate dimensions of display components.
   */
  public void setup() {
  }

  /** Add the specified component to this container at top index. <br>
      This is the same as <code>addOn(comp,0)</code>.
      @param comp the component to be added to this container.
   */
  public void addOn (Component comp) {
    addOn(comp,0);
  }

  /** Move the display to the User Defined display. <br>
      <br>
      For example:
      <pre>
        moveDisplay("0")     set the user display to screen 0.
        moveDisplay("1")     set the user display to screen 1.
      </pre>
      @param disp Specifies the Window DISPlay to use (i.e. specifies which monitor
                  for systems with 2+ monitors to open the window on).
      @since NeXtMidas 1.9.3
  */
  public void moveDisplay (String disp) {
    userDisplay = disp;
    Shell.info("Moving display to "+userDisplay);
  }

  /** Add the specified component to this container at given index.
      @param comp  the component to be added to this container.
      @param index the index at which to add the component to this container.
      @see #addOn(Container,Component,int)
   */
  public void addOn (Component comp, int index) {
    addOn(panel,comp,index);
  }

  /** Add the specified component to the specified container at the top index.
      This is the same as <code>addOn(cont,comp,0)</code>.
      @param cont the container to which to add the specified component.
      @param comp the component to be added to the specified container.
   */
  public static void addOn (Container cont, Component comp) {
    addOn(cont,comp,0);
  }

  /** Add the specified component to the specified container at specified index.
      This is similar to using <code>cont.add(comp,index)</code> but handles
      swing and AWT components separately.
      @param parent  the container to which to add the specified component.
      @param child   the component to be added to the specified container.
      @param index   the index at which to add the specified component to the specified container.
                     The index actually means little, since the last component
                     added will be placed on top (except for GAlert popups, which
                     are always on top).
   */
  public static void addOn (final Container parent, final Component child, final int index) {
    if (DEBUG) {
      System.out.println("MW.addOn(parent, child, index)");
      System.out.println("  parent        ~= "+Util.getClassName(parent));
      System.out.println("  child         ~= "+Util.getClassName(child));
      System.out.println("  index          = "+index);
    }

    final JLayeredPane jlp = Theme.getOption(Theme.MOBILE)? null : findLayeredPane(parent,child);

    // Bug 1929 - Add JLayeredPane's component's in Swing Thread to prevent Exceptions
    //            during restarts while MWindow/panel is closing
    invokeAndWait(() -> addComp(parent, child, index, jlp)); // invokeAndWait change made in 3.9.0 for graphical timing issues

    pullGAlertToFront(child.getParent());   // ensure any GAlerts are still on top
  }

  /** Add a Component to a Container. This method was created to reduce the clutter
      in {@link #addOn(Container, Component, int)}
      @param parent The parent Container
      @param child The child component to add to the Container
      @param index The index of the component
      @param jlp   The JLayeredPane, if applicable (null if not)
      @since NeXtMidas 2.7.2
   */
  private static void addComp (Container parent, Component child, int index, JLayeredPane jlp) {
    if (DEBUG) {
      System.out.println("MW.addComp(parent, child, index, jlp)");
      System.out.println("  parent        ~= "+Util.getClassName(parent));
      System.out.println("  child         ~= "+Util.getClassName(child));
      System.out.println("  index          = "+index);
      System.out.println("  jlp           ~= "+Util.getClassName(jlp));
    }

    if (jlp != null) {  // first try adding to Swing JLayeredPane
      //System.out.println("optimized:"+jlp.isOptimizedDrawingEnabled()+" child name:"+child.getName());
      if (jlp.isOptimizedDrawingEnabled() && (child instanceof MJPanel)) {
        // null.JLayeredPane is optimized by default - trick it to draw layers in proper order to avoid graphical issues
        MWindow mwlocal = ((MJPanel)child).getMWindow();
        MJPanel ch2 = new MJPanel();
        ch2.setBounds(child.getBounds());
        ch2.MW = mwlocal;
        jlp.add(ch2);
        jlp.add(child,Integer.valueOf(index));
        jlp.moveToFront(child);  // removes the need for adjusting the index based on layer
        jlp.remove(ch2);
        //System.out.println("turning off optimization");
      }
      else {
        jlp.add(child,Integer.valueOf(index));
        jlp.moveToFront(child);  // removes the need for adjusting the index based on layer
      }
    } else {
      // Check to see if the component is being added to a Swing or an AWT component.
      // If an AWT is going on a Swing and we are not in Realtime mode, it will send a warning message. 3.8.1
      if(((warningsMask&ADD_AWT_TO_SWING) != 0) && !Theme.getOption(Theme.REALTIME) &&
          (child instanceof MPanel) && (parent instanceof MJPanel)) {
        String message = "MWindow - Putting AWT component on top of Swing component may cause graphical lock-up or slugglishness\n"
            + "\tparent="+parent.getClass()+"\n"
            + "\tchild="+child.getClass();
        Shell.getMidasContext().warning(message);
      }
      parent.add(child,index);
    }
    // the next lines are to prevent any graphical glitches when using all-swing mode
    // or when putting the component within another component within another component...
    child.validate();
    child.repaint();
  }

  /** Find the LayeredPane of a Container. This method is only called by
      {@link #addOn(Container, Component, int)}
      @param parent The Container
      @param child What is being added to the Cont. This is only needed due to a bug which
                   makes the mouse position erratic
      @return The JLayeredPane or NULL if none.
   */
  private static JLayeredPane findLayeredPane (Container parent, Component child) {
    if (parent instanceof JLayeredPane) return (JLayeredPane) parent;
    // Check if already functioning like layeredpane (since 3.9.1) - check should not be done for MPanel 3.9.3
    //if ((parent instanceof JPanel) && !((JPanel) parent).isOptimizedDrawingEnabled()) return null;
    // if not using swing popups, no need to use the LayeredPane
    if ( !(Theme.getOption(Theme.SWINGPOPUPS) || Theme.getOption(Theme.MOBILE))) return null;

    // if we are popped out, stay off JLayeredPane - since 3.8.1 for NXM-3213
    if(parent instanceof MJPanel) {
      if(((MJPanel) parent).MW.isPoppedOut) return null;
    }

    // When to use the LayeredPane
    boolean findLayeredPane;

    // Change the checking of instance from MJPanel to JPanel because MJText creates a JPanel instead of MJPanel Since 3.8.1
    findLayeredPane = ((parent instanceof JPanel && // parent is SWING
        !(parent.getParent() instanceof JLayeredPane) && // "grandparent" not LayeredPane
        !(parent.getParent() instanceof MidasDisplay) && // not "buried" panels under displays
        ((JPanel) parent).getRootPane() != null));      // parent is a top-level Container

    if (parent instanceof MidasDisplay && findLayeredPane) { // MidasDisplays are special because they may not have a parent frame
      if (child instanceof JPanel) {      // temp check until mouse position bug is resolved
        return ((JPanel)parent).getRootPane().getLayeredPane();
      }
    }

    if (findLayeredPane) {
      // Change the checking of instance from MJPanel to JPanel because MJText creates a JPanel instead of MJPanel Since 3.8.1
      return ((JPanel)parent).getRootPane().getLayeredPane(); // common case
    }

    return null; // catch all
  }

  /** Get the Name of the MWindow for a given MJPanel/ MPanel
      @param comp Component (should be either a MJPanel or a MPanel)
      @return The {@link #name} of the MWindow linked to the Comp, or "" if not a MJPanel/MPanel
      @since NeXtMidas 2.7.2
   */
  public static String getMWName (Component comp ) {
    String name = "";
    if      (comp instanceof MJPanel) name = ((MJPanel)comp).MW.name;
    else if (comp instanceof MPanel)  name = ((MPanel)comp).MW.name;
    if (name == null) name = "";
    return name;
  }

  /**
   * Get the ID of the Command embedded in this MWindow
     @return embedded Command ID
     @since NeXtMidas 3.5.4 (Bug 2895)
   */
  public String getEmbeddedCommandID(){
    return embeddedCommandID;
  }

  /** Bring any GAlerts in a container to the front
      @param cont container containing GAlerts
      @since NeXtMidas 2.7.2
   */
  public static void pullGAlertToFront (Container cont) {
    for (int index=0; index < cont.getComponentCount(); index++) {
      Component comp = cont.getComponent(index);
      if (comp instanceof MPanel || comp instanceof MJPanel) {
        String name = getMWName(comp);
        if (name.equalsIgnoreCase("GALERT")) {
          //cont.setComponentZOrder(comp, 0); //JWB: this line replaced by the line below to prevent sporadic NPEs
          cont.add(comp,0);
        }
      }
    }
  }

  /** Add this window to the name object. This is identical to <code>addTo(entry,null)</code>.
      @param entry The object to which this MWindow should be added to
   */
  public void addTo (Object entry) {
    addTo(entry, null);
  }

  /** Add this window to the named object. <br> <br>
      If this is a command the following switches are processed:
      <pre>
        /DISPLAY= - Make window come up in specified display mode: Icon, Back, Front, or Maximized. (Since 3.1.1)
        /EVENTFILTER= - Window event filter mask (e.g. NoMouse, NoMove, NoPushPop,
                    NoMiddleMouse). For complete list of mask values see {@link #eventFilterList}. (Since 3.3.1)
        /ICON     - Specifies the Window should initially be iconified (i.e. minimized). (Since 3.1.0)
        /NOALTTITLE - Disables automatic updates to the status/title bar text from events such as mouse hovers. (Since 3.3.0)
        /NOPUSHPOP  - Disable window push/pop out/in of panel.
        /SHARETHEME - Whether or not to have all graphical components share the same Theme. (Since 3.3.0)
        /THEME=   - Specifies the THEME to use.
        /WDISP=   - Specifies the Window DISPlay to use (i.e. specifies which
                    monitor for systems with 2+ monitors to open the window on).
        /WFS=     - Specifies the Window Font Size to use.
        /WIN=     - Specifies the name of WINdow (or panel) in the registry that
                    will host this {@link MWindow}. If the window name is not
                    specified, or included in panel config, a new frame is
                    created.
        /WIN.&lt;cmd_id&gt;= - <i>Similar to /WIN.</i>
        /WIN.NEW  - Open window in a new frame.
        /WPOS=(x,y[,w,h]) - Specifies the Window POSition in pixels. Width and height are optional.
        /WTITLE=  - Specifies the Window's TITLE (ignored for GWidget's).
      </pre>
      @param entry The object to which this {@link MWindow} should be added to.
                   This can be an instance of {@link MPanel},
                   {@link MJPanel}, {@link MFrame}, {@link MJFrame},
                   {@link GWidget}, {@link GPrimitive}, {@link Command}, or
                   registry entry name (as a {@link String}).
      @param dev   The GraphicsDevice to use if a MJFrame must be created
      @since NeXtMidas 2.7.2 (moved most of addTo(Object) into this method)
   */
  @SuppressWarnings("deprecation")
  public void addTo (Object entry, GraphicsDevice dev) {
    boolean hasTitle = false;
    String display = null; // The display to use, null is the default display
    int initState = FRONT;

    // 1: IDENTIFY DESTINATION (PANEL=ENTRY) ===================================
    if (DEBUG) {
      System.out.println("MW.addTo(entry,container,dev)");
      System.out.println("  entry         ~= "+Util.getClassName(entry));
      System.out.println("  dev           ~= "+Util.getClassName(dev));
      System.out.println("  this          ~= "+Util.getClassName(this));
      System.out.println("  panel         ~= "+Util.getClassName(panel));
      System.out.println("  parent        ~= "+Util.getClassName(parent));
      System.out.println("  userDisplay    = "+userDisplay);
      System.out.println("  useSwing       = "+useSwing);
    }

    if (entry instanceof Command) {
      Command cmd = (Command)entry;
      this.embeddedCommandID = cmd.getID(); // Since NeXtMidas 3.5.4 for Bug 2895
      int wfs = cmd.args.getL("/WFS");
      if (wfs>0) setFontSize(wfs);
      Data wpos = cmd.args.getData("/WPOS");
      if (wpos != null) {
        int[] wp = new int[4]; wpos.toArray(wp);
        if (wpos.spa>=4) setBounds(wp[0],wp[1],wp[2],wp[3]);
        else if (wpos.spa>=2) setLocation(wp[0],wp[1]);
        resize(1);
      }
      if (!(this instanceof GWidget)) {
        title = cmd.args.getS("/WTITLE",name);
      }

      noAltTitle = cmd.args.getState("/NOALTTITLE",noAltTitle); // usually on panel or plot

      display = cmd.args.getCS("/WDISP",display);
      if (cmd.args.getState("/NOPUSHPOP")) eventFilter |= NOPUSHPOP;
      if (cmd.args.isPresent("/EVENTFILTER")) { // since 3.3.1 for BUG 2712
        eventFilter = cmd.args.getOptionMask("/EVENTFILTER", eventFilterList, eventFilter);
      }
      userDisplay = display;

      String win = cmd.args.getS("/WIN");
      if (win.length()==0) win = cmd.args.getS("/WIN."+cmd.id);
      entry = cmd.M.registry.get("WIN."+cmd.id);
      if (entry==null && cmd.M.macro!=null && cmd instanceof panel) {
        entry = ((panel)cmd).getMidasControls();
        Registry parentRegistry = cmd.M.getParent().registry;
        if (entry==null) entry = parentRegistry.get("WIN."+cmd.M.macro.id);
        if (entry==null && win.length()>0 && win.indexOf('.')>0) {
          String tmpWin = win.substring(0, win.indexOf('.'));
          entry = parentRegistry.get(tmpWin+"."+cmd.M.macro.id);
        }
        if (entry==null && win.indexOf('.')<0 && parentRegistry.get("WIN")!=null) { // since 3.3.0 (Bug 2485)
          String tmpWin = "WIN."+win;
          if (!tmpWin.equals("WIN.NEW")) entry = parentRegistry.get(tmpWin);
        }
      }
      if (win.length()>0 && entry==null) {
        if (win.indexOf('.')<0 && cmd.M.registry.get("WIN")!=null) win="WIN."+win;
        if (!win.equals("WIN.NEW")) entry = cmd.M.registry.get(win,5);
      }
      if (entry instanceof nxm.sys.prim.panel) {
        // PANEL is special, don't treat like the other GPrimitives
      }
      else if (GPrimitive.isInstance(entry)) {
        entry = GPrimitive.getMWindowViaReflection(entry).MW;
      }
      String themeName = cmd.MR.getS("ENV.THEME");
      Object themeObj = cmd.args.getO("/THEME", themeName);
      if (!cmd.args.getState("/SHARETHEME",shareTheme)) theme = (Theme) theme.copy();
      setTheme(themeObj);
      hasTitle = cmd.args.find("/WTITLE");
      if (cmd.args.getState("/ICON")) initState=ICON; // 2011-10-05 to iconify on startup
      initState = cmd.args.getSelectionIndex("/DISPLAY", stateList, initState, -4);
    }
    else if (entry instanceof String) {
      String window = (String)entry;
      entry = (new Registry()).get(window,5);
      if (entry==null) Shell.warning("Timeout looking for window: "+window);
    }

    // 2: CREATE FRAME (IF NEEDED) AND ADD TO ==================================
    if (DEBUG) {
      System.out.println("  ~~~~~~~~~~~~~~~~~~~~~~~~");
      System.out.println("  parent=entry  ~= "+Util.getClassName(entry));
      System.out.println("  userDisplay    = "+userDisplay);
      System.out.println("  useSwing       = "+useSwing);
    }
    parent = entry;
    addListeners();
    Container panelOrig = panel;
    if (parent instanceof GPanel) {
      MW = (MWindow)parent;
      ((GPanel)parent).addWindow (this);
    }
    else if (parent instanceof nxm.sys.prim.panel) {
      ((nxm.sys.prim.panel)parent).addWindow (this);
    }
    else if (parent instanceof MWindow) {
      MW = (MWindow)parent;
      // Go up to the top level panel so that the GMenu/GWidget/etc can be moved
      // anywhere within that window. Stop if we get to a place where the pane
      // has been popped because we want the menu to show up in the popped window.
      while ((MW.MW != null) && (MW.MW != MW) && !MW.isPopped() && !Theme.getOption(Theme.MOBILE)) {
        MW.MW.px = MW.px+MW.pos.x;
        MW.MW.py = MW.py+MW.pos.y;
        MW = MW.MW;
      }
      if (this instanceof GWidget) place (pos,MW.pos,MW.px,MW.py);
      resize(1);
      if (this instanceof GWidget) addOn(MW.panel,panel,0);
      else                         addOn(MW.panel,panel,-1);
    }
    else if (parent instanceof MFrame) {
      MFrame mframe = (MFrame)parent;
      if (this instanceof GWidget) addOn(mframe,panel);
      else { mframe.setMWindow (this); panel = (Container)mframe; }
    }
    else if (parent instanceof MJFrame) {
      MJFrame mframe = (MJFrame)parent;
      if (this instanceof GWidget) addOn(mframe,panel);
      else { mframe.setMWindow (this); panel = (Container)mframe; }
    }
    else if (parent instanceof Pane) {
      Pane pane = (Pane)parent;
      if (hasTitle) pane.setTitle(title);
      MW = pane.MW;
      pane.add(this);
    }
    else if (parent instanceof MidasDisplay) {
      crossHairsColor = Color.white;  // otherwise they do not show up on plots (Bug 2347)
      MidasDisplay md = (MidasDisplay)parent;
      //MW = md.MW;  // BUG 2148 : Do we need this (it breaks some popup positioning)???
      md.add(this);
    }
    else if (parent instanceof MPanel) {
      MPanel mpanel = (MPanel)parent;
      if (this instanceof GWidget) {
        addOn(mpanel,panel);
      } else {
        mpanel.setMWindow (this);
        panel = (Container)mpanel;
      }
    }
    else if (parent instanceof MJPanel) {
      MJPanel mpanel = (MJPanel)parent;
      if (this instanceof GWidget) {
        addOn(mpanel,panel);
      } else {
        mpanel.setMWindow (this);
        panel = (Container)mpanel;
      }
    }
    else if (!Shell.isHeadless()) { // since 3.5.0. Skip if in Headless state. BUG 2817
      if (parent!=null) {
        Shell.warning("Could not use window: "+parent+", "+parent.getClass()+".  Creating new frame");
      }
      if (userDisplay!=null) display=userDisplay;
      Frame frame;
      if (useSwing) {
        MJFrame mframe = (dev==null)? new MJFrame(name, pos, display)
                                    : new MJFrame(name, pos, dev.getDefaultConfiguration());
        mframe.setMWindow(this);
        parent = frame = mframe; // the frame is also the parent for this MWindow
      } else {
        MFrame mframe = new MFrame(name,pos,display);
        mframe.setMWindow(this);
        parent = frame = mframe; // the frame is also the parent for this MWindow
      }
      frame.setUndecorated(this instanceof GWidget); // GWidgets have NO frame decorations
      frame.setVisible(true);    // frame MUST be visible before adding panel to it
      frame.setTitle(title);     // since 3.1.0 set window/frame's title on stand-alone graphical command
      if (frame instanceof JFrame) {
        // add panel to Swing frame
        ((JFrame)frame).getContentPane().add(panel);
      }
      else {
        // add panel to AWT/Other frame
        frame.add(panel, BorderLayout.CENTER);
      }
    }

    // 3: COMMON INIT STEPS ====================================================
    if (DEBUG) {
      System.out.println("  ~~~~~~~~~~~~~~~~~~~~~~~~");
      System.out.println("  panel         ~= "+Util.getClassName(panel));
      System.out.println("  parent        ~= "+Util.getClassName(parent));
    }

    // commented out for Bug 1159: should not call panel.setBackground/setForeground as
    // (since 2.5.0)               it can cause deadlock with the AWT/Swing thread
    // panel.setBackground(theme.cbg);
    // panel.setForeground(theme.cfg);

    // don't possibly lose events by momentarily dropping listeners
    if (panel != panelOrig) addListeners();

    if (panel != null) { // since 3.5.0. Skip if in Headless state. BUG 2817
      panel.setFont(font);

      // fix for swing panels initial size bug
      if (parent instanceof MidasDisplay) {
        Dimension d = panel.getSize();
        if (d.width>10 && d.height>10) {
          pos.x = panel.getLocation().x;
          pos.y = panel.getLocation().y;
          pos.w = d.width;
          pos.h = d.height;
        }
      }
    }

    // since 3.3.0: ONLY set DISPLAY state (e.g. ICON) if parent is a Frame (BUG 2690)
    if (!(parent instanceof Frame)) {
      initState = FRONT; // parent is NOT a Frame (revert display state to FRONT)
    }
    display(initState);

  }

  /** Set the double-buffering on/off for the MWindow graphics panel. The SWING
      default is <code>TRUE</code>, but is disabled in <code>MJFrame</code> and the
      <code>Panel</code> primitive to allow for graphics card acceleration
      and prevent certain issues caused when SWING and non-SWING panels are
      both displayed.
      @param value true if double-buffering is on for the MWindow graphics panel
   */
  public void setDoubleBuffered (boolean value) {
    if (panel instanceof JPanel) {
      RepaintManager.currentManager(panel).setDoubleBufferingEnabled(value);
    } else {
      Shell.warning("Cannot set double buffering on a non-SWING panel");
    }
  }

  /** Get whether or not the Repaint Manager for the graphics panel is using
      Swing DoubleBuffering.
      @return true if the Repaint Manager is using Swing DoubleBuffering
   */
  public boolean isDoubleBuffered () {
    if (panel instanceof JPanel) {
      return RepaintManager.currentManager(panel).isDoubleBufferingEnabled();
    } else {
      return false;
    }
  }

  /** Adds the default event listeners for this window.
      @see #removeListeners()
   */
  protected void addListeners () {
    removeListeners();
    if (panel != null) { // since 3.5.0. Skip if in Headless state. BUG 2817
      panel.addMouseListener(this);
      panel.addMouseMotionListener(this);
      panel.addComponentListener(this);
      panel.addFocusListener(this);
      panel.addKeyListener(this);
      if (parent instanceof Frame) { panel.addKeyListener(hkl); }
    }
  }

  /** Removes the default event listeners for this window.
      @see #addListeners()
   */
  protected void removeListeners () {
    if (panel != null) {
      panel.removeMouseListener(this);
      panel.removeMouseMotionListener(this);
      panel.removeComponentListener(this);
      panel.removeFocusListener(this);
      panel.removeKeyListener(this);
      if (parent instanceof Frame) { panel.removeKeyListener(hkl); }
    }
  }

  /** Removes the default help key listeners for this window.
      @see #removeListeners()
   */
  protected void removeHelpKeyListeners () {
    if (panel != null) {
      Component[] comps = panel.getComponents();
      for (Component comp : comps) {
        comp.removeKeyListener(hkl);
      }
    }
  }

  /** Set visibility of the graphics window: MAXImum FRONt BACK ICONify or DEIConify.
      @param str The string representing the visibility of this graphics window
                 (One of <code>"FRON"</code>, <code>"BACK"</code>,
                 <code>"ICON"</code>, <code>"DEIC"</code>).
      @return A boolean value representing whether the display mode was
              successfully found using the str parameter.
   */
  public boolean setDisplay (String str) {
    if (str.startsWith("DEIC")) str = "FRON";
    else if (str.length()>4) str = str.substring(0,4);
    int mode = Parser.find(stateList,str,-9,-4);
    if (mode == -9) return false;
    display (mode);
    return true;
  }

  /** Get the status of the Window.
      @return The window status
         ({@link #FRONT},{@link #BACK}, {@link #ICON}, {@link #INIT}, {@link #EXIT}).
   */
  public String getDisplay () {
    return getDisplay(status);
  }
  private String getDisplay (int status) {
    return Parser.get(stateList,status,-4);
  }

  /** Set the visibility of the graphics window.
      @param mode Integer representing a valid display mode for this window.
                  ({@link #FRONT},{@link #BACK}, {@link #ICON}, {@link #INIT}, {@link #EXIT}).
   */
  public void display (int mode) {
  // System.out.println("MWindow display. mode=" +mode+", status="+getDisplay(status) + " on="+StringUtil.substring(this.toString(), 0, 40));
    if (MidasFrame.isMidasFrame(parent)) { // Updated in 3.9.2 to use method that will check if it is inc.MidasFrame or libg.MidasFrame using reflection for jdk 11 modularity
      MidasFrame.midasFrameDisplayViaReflection(parent, mode);
    }
    else if (parent instanceof Pane) {
      Pane pane = (Pane)parent;
      if (pane.isInTab()) {
        switch(mode) {
          case FRONT: case MAXIM: pane.makeVisibleTab(); break;
          case BACK: case ICON: case INIT: case EXIT:
            Shell.warning("MWindow: Can not set display mode to: "+getDisplay(mode)+" for "+this+" on "+pane); break;
          default: throw new RuntimeException("MWindow: Unknown display mode (" + mode + ").");
        }
      }
    }
    else if (mode==status) {
      return;
    }
    if (panel != null) panel.setVisible((mode>=BACK));
    if (status!=INIT) status = mode;
    if (status==MAXIM) status = FRONT;
  }

  /** Print this window.
   *  @deprecated in 3.9.0 to fix graphical timing issues (NXM-3213)
   */
  @Deprecated
  public void print () {   ///  may need to do something so g is not useless in meantime 3.9.0 jtm
    panel.print(g);
  }

  /** Closes the graphical window but does not clear anything else out. <br>
      Only reParent() and close() should call this directly.
   */
  private void closeWindow () {
    status = EXIT;
    removeLayeredPaneComp();
    // JC4: Removing the call to removeListeners() causes exceptions when resizing the panes
    // in a PANEL after a GPrimitive has exited and called close() to see an example (1) run
    // %TEST_LIST2_EXIT, (2) tell one of them to exit, (3) resize the pane
    removeListeners();
    removeHelpKeyListeners();
    if (MW!=null && MW.panel!=null) {
      MW.panel.remove(panel);
      // Might possibly need MW.panel.repaint() here instead of the line below. Watch for errors.
      if (this instanceof GWidget) MW.panel.repaint(pos.x,pos.y,pos.w,pos.h);
    }
// this might not be ours now
//    if (g!=null)  { g.dispose(); g = null; }
    if (parent != null) {
      if (parent instanceof Container) ((Container)parent).remove(panel);
      if (parent instanceof Pane)      ((Pane)parent).remove(panel); // <-- Added for Bug 1134
      if (parent instanceof MFrame)    ((MFrame)parent).dispose();
      if (parent instanceof MJFrame)   ((MJFrame)parent).dispose();
    }
  }

  /** Close the graphics window. */
  public void close () {
    // NTN: this can be cleanup in 2.7.0
    if (Shell.isHeadless()) {
      // since 3.5.0. Break out if in Headless state. BUG 2817
    }
    else {
      // Bug 1191 - Remove panel's component's in Swing Thread to prevent Exceptions
      // during paint while MWindow/panel is closing
      invokeAndWait(() -> close0());  // invokeAndWait change made in 3.9.0 to address graphical timing issues
    }
  }

  /** This should only be called in the SWING Thread, so that it does not cause
      random exceptions in the Swing Thread using closing of this MWindow.
      See Bug 1191 for details.
      @since NeXtMidas 2.5.5.
   */
  private void close0 () {
    //if (panel != null && panel.getParent() != null)System.out.println("Closing " +this.getClass().getSimpleName()
    //    +", panel= " +panel.getClass().getSimpleName());
    closeWindow();
    if (panel instanceof MPanel ) { panel.removeAll(); ((MPanel)panel).MW  = null;}
    if (panel instanceof MJPanel) { panel.removeAll(); ((MJPanel)panel).MW = null;}
    MW         = null;
    fm         = null;
    font       = null;
    mh         = null;
    panel      = null;
    parent     = null;
    pushParent = null;
  }

  /** Remove the panel from the LayeredPane and refresh the LayeredPane
      @since NeXtMidas 2.7.1
   */
  private void removeLayeredPaneComp () {
    if (panel != null && panel.getParent() != null) {
      if (panel.getParent() instanceof JLayeredPane) {
        JLayeredPane jlp = (JLayeredPane)panel.getParent();
        if (panel!=null)
          try {
            jlp.remove(panel);
          } catch (Exception e) {}
        jlp.repaint(pos.x,pos.y,pos.w,pos.h);
      }
    } else if (parent instanceof MWindow && ((MWindow)parent).panel != null) {
      ((MWindow)parent).refresh(pos);
    }
  }

  /** Call message handler method to process a message. <br>
      This is the same as <code>process(name,info,null)</code>.
      @param name The Message name.
      @param info The Message info.
      @return The status of the message processing.
   */
  public int process (String name, int info) {
    return process(name,info,null);
  }

  /** Call message handler method to process a message.
      @param name The Message name.
      @param info The Message info.
      @param data The Message data.
      @return The status of the message processing
              (see {@link MessageHandler#processMessage}).
   */
  public int process (String name, int info, Object data) {
    return process(name, info, data, null);
  }

  /** Call message handler method to process a message.
      @param name The Message name.
      @param info The Message info.
      @param data The Message data.
      @param e    The MouseEvent
      @return The status of the message processing
              (see {@link MessageHandler#processMessage}).
   */
  public int process (String name, int info, Object data, MouseEvent e) {
    // Notify parent that we are viewable.  The first block "true && ..." comes
    // from MPlot in NeXtMidas 2.7.2 to NeXtMidas 3.8.0.  The code here in
    // NeXtMidas 3.8.0 and earlier would match "false && ...".  The code was
    // made to streamline the repaint process and since there isn't much harm
    // in using the same functionality here.
    if (status==INIT || status==BACK) {
      if (isPanelVisible()) {
        if (parent instanceof Frame) getFocus();
        status = FRONT;
      }
      else {
        status = BACK;
      }
    }
    else if (status==INIT && "PAINT".equals(name)) {
      if (parent instanceof Frame) getFocus();
      status=FRONT;
    }
    else if (pushParent!=null && "WINDOW".equals(name) && "CLOSING".equals(data)) {
      // special handling of window close button when popped
      if ((eventFilter&NOPUSHPOP)==0) {
        name = "POP"; info = -1;
      } else {
        return 1;
      }
    }
    else if ("WINDOW".equals(name)) {
      // handle mix/match between mouse and messages for ICON/DEICONIED (BUG 2489)
      if (status!=FRONT && "DEICONIFIED".equals(data)) {
        display(FRONT);
      }
      else if (status!=ICON && "ICON".equals(data)) {
        display(ICON);
      }
    }

    // give subclass preprocessor a stab at it
    int stat = (this instanceof MPlot) ? preprocess(name,info,data,e) :
      preprocess(name,info,data); // NXM-4028 allow original MouseEvent to be passed on
    // else pass on to parent handler
    if ((stat<=0) && (mh!=null)) {
      Message msg = new Message(name,info,data,mh,this); // TODO - should we pass e along?
      stat = mh.processMessage(msg);
    }
    return stat;
  }

  /** Preprocesses the message before passing it on to the MessageHandler.
      This method has a default empty implementation and will always return -1.
      Subclasses should return 1 or greater if the message should not be
      sent to the registered message handler.
      @param name the Message name.
      @param info the Message info.
      @param data the Message data.
      @return Return 1 or greater to signal that this message has been consumed
              and no further action should be done, otherwise have the registered
              message handler process the message.
   */
  public int preprocess (String name, int info, Object data) {
    return -1;
  }

  /** Preprocesses the message before passing it on to the MessageHandler.
      This method has a default empty implementation and will always return -1.
      Subclasses should return 1 or greater if the message should not be
      sent to the registered message handler.
      @param name the Message name.
      @param info the Message info.
      @param data the Message data.
      @param e    the MouseEvent (if applicable)
      @return Return 1 or greater to signal that this message has been consumed
              and no further action should be done, otherwise have the registered
              message handler process the message.
   */
  public int preprocess (String name, int info, Object data, MouseEvent e) {
    return preprocess (name, info, data);
  }

  /** Process general window messages: <code>POP</code>, <code>DISPLAY</code>.  <br>
      Any subclasses that override this method should make a call to
      <code>super.processMessage(msg)</code>.
   */
  @Override
  public int processMessage (Message msg) {
//if (!"POINTER".equals(msg.name)) System.out.println("MWindow " +name+" process a " +msg.name+" msg. ["+this+"]");
    switch (msg.name) {  // using switch statement since 3.8.1
      case "POP":
        Table devt = null;
        if (msg.quals != null && msg.quals instanceof Table) {  // added in 3.1.0 for Bug 2458
          devt = (Table)msg.quals;
        }
        if (devt !=null && devt.containsKey("DISP")) { // check to make sure this quals table has new window information since NeXtMidas 3.5.4 for Bug 2927
          int winNo = Math.max(0, devt.getL("DISP"));
          if (devt.containsKey("BOUNDS")) {
            Table b = devt.getTable("BOUNDS");
            setPopBounds(b.getL("X"), b.getL("Y"), b.getL("W"), b.getL("H"));
          }
          setPopWindow(winNo);
          pop(msg.info,popDev);
        }
        else {
          pop(msg.info);
        }
        return 1;
      case "POPMENU":
        String devstring = (String) msg.data;
        int winNo = Math.max(0, Integer.parseInt(devstring.substring(devstring.length()-1)));
        setPopWindow(winNo);
        pop(msg.info,popDev);
        return 1;
      case "DISPLAY":
        if (msg.data == null) {
          display(msg.info);
        }
        else {
          setDisplay((String)msg.data);
        }
        return 1;   // ????? is this Comandable.OPEN or consumed jtm 3.8.1
      default:
        return -1;  // Comandable.NOOP or not consumed
    }
  }

  /** Disable &amp; enable the ability to set popout location via CTRL+click
      @param val true if enabled
      @since 3.1.0
    */
  public void setEnableAdvancedPopOut (boolean val) { enableAdvancedPopOut = val; }
  /** Get if advanced CTRL+click popouts are enabled
   *  @return true if enabled
   *  @since 3.1.0
   */

  public boolean getEnableAdvancedPopOut () { return enableAdvancedPopOut; }

  /** Indicates if the window is currently popped out.
   *  @return true if the window is currently popped out */
  public boolean isPopped() {
    return (pushParent!=null);
  }

  /** Pop this window out of its parent frame and into its own frame--OR--push
      this window from its own frame back into its previous parent frame.
      @param mode the action of this method is determined by the value of this parameter as follows:
           <pre>
              1 - pop
              0 - push
             -1 - toggle (i.e. if out, push, if in pop)
           </pre>
      @return true if this window has successfully reparented according to the pop/push action.
   */
  public boolean pop (int mode) {
    return pop(mode,false);
  }

  /** Pop this window out of its parent frame and into its own frame--OR--push
      this window from its own frame back into its previous parent frame.
      @param mode the action of this method is determined by the value of this parameter as follows:
           <pre>
              1 - pop
              0 - push
             -1 - toggle (i.e. if out, push, if in pop)
           </pre>
      @param showPopMenu true to display a popup menu asking the user to select which monitor to send
                         the popped Mwindow to
      @return true if this window has successfully re-parented according to the pop/push action, OR
                   if a menu has been presented to the user to select a parent window/display
      @since NeXtMidas 3.1.0
   */
  public boolean pop (int mode, boolean showPopMenu) {
    if (!isPopped() && showPopMenu && enableAdvancedPopOut) {
      String swins = "";
      String prepend = "";
      int numScrs = GraphicsUtil.getNumScreens(false); // Since 3.9.2 changed to use same method moved from intr.Environment to lib.GraphicsUtil for jdk 11 modularity
      for (int xx=0; xx<numScrs;) {
        if (xx>0) prepend = ",";
        swins += (prepend+"Win_"+(++xx));
      }
      new GMenu (this,"POPMENU",swins,0,0,this);
      return true;
    } else if (!isPopped() && popDev != null) {
      return pop(mode,popDev);
    }
    return pop(mode,null);
  }

  /** Pop this window out of its parent frame and into its own frame--OR--push
      this window from its own frame back into its previous parent frame.
      @param mode the action of this method is determined by the value of this parameter as follows:
             <pre>
                1 - pop
                0 - push
               -1 - toggle (i.e. if out, push, if in pop)
             </pre>
     @param dev The GraphicsDevice to pop to (ignored if not pop, null to use default)
     @return true if this window has successfully reparented according to the pop/push action.
     @since NeXtMidas 2.7.2
   */
  public boolean pop (int mode, final GraphicsDevice dev) {
    if (mode==-1) mode = (pushParent==null)? 1:0;
    if (pushParent==null && mode==0) return false;
    if (pushParent!=null && mode==1) return false;

    invokeAndWait(() -> pushParent = reParent(pushParent, dev, true));  // using invokeAndWait since 3.9.0 to fix graphical timing issues
    return true;
  }

  /** Reparent this window to the given parent Object
      This will change the parent of this window to a new parent
      @param parent The new parent for this window
      @return true if this window has successfully been re-parented
   */
  public boolean reParent (Object parent) {
    reParent(parent, null, false);
    return true;
  }

  /** Reparent this window to the given parent Object
      This will change the parent of this window to a new parent
      @param newParent The new parent for this window
      @param dev The GraphicsDevice to use
      @param pushPop  TRUE for Push/Pop, false for basic reparent
      @return The new pushParent (only useful if pushPop is TRUE)
   */
  private Object reParent (Object newParent, GraphicsDevice dev, boolean pushPop) {
    Object curParent = parent;
    isPoppedOut      = (newParent == null);

    Component pp = (newParent instanceof Component)? ((Component)newParent) : null;
    Component cp = (curParent instanceof Component)? ((Component)curParent) : null;

    if (isPoppedOut) {
      if (pushPop && (popBounds != null)) pos.setBounds(popBounds);       // restore previous POP bounds
      else                                pos.setBounds(100,100,700,500); // Use default bounds
    }
    else {
      if (pushPop && (cp != null)) {
        // Save bounds to popPos before doing a PUSH (only supported with SWING)
        Component root = SwingUtilities.getRoot(cp);
        if (root instanceof JFrame) {
          JFrame    jf  = (JFrame)root;
          Point     loc = cp.getLocation();
          Dimension dim = jf.getContentPane().getSize();
          Insets    ins = jf.getInsets();
          popBounds     = new Rectangle(loc.x + ins.left, loc.y + ins.top, dim.width, dim.height);
        }
      }

    }
    // if push/pop'ing a Gpanel, adjust the panel size base don the presence of the displayed component (since 3.3.2 - Bug 42)
    GPanel gpan = (curParent instanceof GPanel) ? (GPanel)curParent : ((newParent instanceof GPanel) ? (GPanel)newParent : null);
    if (gpan != null) gpan.updateSize(isPoppedOut);

    closeWindow();
    addTo(newParent, dev);

    if (useSwing) {
      // JC4: We need to invalidate everything since we made structural changes to the layouts
      // while visible. Failure to do this can cause 'interesting' refresh issues, particularly
      // when using MidasDisplay (i.e. a full-up SWING GUI). Note:
      //   1. invalidate() everything before calling validate(). This will cause any overlapping
      //      refresh work to be done ONCE.
      //   2. When working with a SWING GUI (and in particular when the pane being pushed/popped
      //      uses OpenGL), the invalidate(), validate() is absolutely necessary on a PUSH and
      //      repaint() is absolutely necessary on a POP. In practice doing all three for both
      //      PUSH and POP seems to work well.

      // Change to revalidate as that is the out of box way to invalidate and validate. Since 3.8.1
      if (panel    != null) panel.revalidate();
      if (pp       != null) pp.revalidate();
      if (cp       != null) cp.revalidate();
      if (MW!=null && MW.panel != null) MW.panel.revalidate();

      if (panel    != null) panel.repaint();
      if (pp       != null) pp.repaint();
      if (cp       != null) cp.repaint();
      if (MW!=null && MW.panel != null) MW.panel.repaint();

    }
    return (isPoppedOut)? curParent : null;
  }

  /** Set the display window (1-based) for the Window to appear in when popped
      @param winNo the display window
      @since NeXtMidas 3.1.0
   */
  public void setPopWindow (int winNo) {
    popDev  = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[winNo-1]; // subtract one because zero-based
  }

  /** Get the display window number where the window will appear when popped
      @return The window number (1-based), or -1 if not set
      @since NeXtMidas 3.1.0
   */
  public int getPopWindow () {
    GraphicsDevice[] devs = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
    for (int xx=0; xx<devs.length; xx++) {
      if (devs[xx]==popDev) return xx+1;
    }
    return -1;
  }

  /** Get the display window graphics device for where the window will appear when popped
      @return the display windows graphics device
      @since NeXtMidas 3.1.0
   */
  public GraphicsDevice getPopDevice () {
    return popDev;
  }

  /** Set the location on the screen where a popped window will appear
      @param r The rectangle containing the x,y,w, and h. If width or height are zero,
      that value will be replaced with a default value in [w=500,h=500]
      @since NeXtMidas 3.1.0
   */
  public void setPopBounds (Rectangle r) {
    // set some defaults if needed
    if (r.getWidth()==0)  r.width  = 500;
    if (r.getHeight()==0) r.height = 500;
    popBounds = r;
  }

  /** Set the location on the screen where a popped window will appear
   *  @param x      the specified X coordinate
   *  @param y      the specified Y coordinate
   *  @param width  the width of the Rectangle
   *  @param height the height of the Rectangle
   *  @since NeXtMidas 3.1.0
   *
   */
  public void setPopBounds (int x, int y, int width, int height) {
    setPopBounds(new Rectangle(x,y,width,height));
  }

  /** get the location on the screen where a popped window will appear
      @return A rectangle with the location, or null if not set
      @since NeXtMidas 3.1.0
   */
  public Rectangle getPopBounds () {
    return popBounds;
  }

  @Override
  public void focusGained (FocusEvent e) {
    //System.out.println("Focus Gained "+name);
    hasFocus = true;
    process("FOCUS",1);
  }

  @Override
  public void focusLost (FocusEvent e) {
    //System.out.println("Focus Lost "+name);
    hasFocus = false;
    process("FOCUS",-1);
  }

  @Override
  public void mouseEntered (MouseEvent e) {
    //System.out.println("Mouse Enter "+name);
    px = e.getX();
    py = e.getY();
    process("POINTER",9);
    pactive = true;
  }

  @Override
  public void mouseExited (MouseEvent e) {
    //System.out.println("Mouse Exit "+name);
    px = e.getX();   // added in 2.9.0 (Bug 672) in order to properly update crosshairs
    py = e.getY();   // added in 2.9.0 (Bug 672) in order to properly update crosshairs
    process("POINTER",-9);
    pactive = false;
  }

  /** Converts x and y relative to a component, to a Point relative to the screen. */
  private Point convertPointToScreen (int x, int y, Component c) {
    Point p = new Point(x,y);
    SwingUtilities.convertPointToScreen(p, c);
    return p;
  }

  @Override
  public void mousePressed (MouseEvent e) {
    //System.out.println("Mouse Press "+name+" "+e);
    if ((eventFilter&NOMOUSE)!=0) return;
    dtime = System.currentTimeMillis();
    px = e.getX();
    py = e.getY();
    press = convertPointToScreen(px, py, panel);
    if (dragging==0) box.setBounds (px,py,0,0);
    if (useSwing && !hasFocus) getFocus();
  }

  @Override
  public void mouseReleased (MouseEvent e) {
    //System.out.println("Mouse Release "+name+" "+e+" dragging="+dragging);
    if ((eventFilter&NOMOUSE)!=0) return;
    if (dragging!=0) {
      // Need to use absolute coordinates rather than relative since the relative coordinates
      // change as a GDialog is dragged on the screen. If the total drag is <5 pixels, then
      // treat it as a mouseClicked(..) rather than the end of a DRAGBOX to address Bug 1247
      // on Windows. Note that the "else" branch here is rarely encountered on Linux systems.
      px = e.getX();
      py = e.getY();
      Point abs = convertPointToScreen(px, py, panel);
      if (press.distance(abs) >  5 && !isScrolling) {
        box.w = px - box.x;
        box.h = py - box.y;
        if (this instanceof MPlot) process("DRAGBOX",dragging,new MBox(box), e); // NXM-4028 pass on event for modifiers
        else process("DRAGBOX",dragging,new MBox(box));
        dragging=0;
      }
      else {
        stopScroll();
        dragging=0; // Reset dragging before calling mouseClicked(..) so message is not ignored.
        mouseClicked(e);
      }
    }
    box.setBounds (-1,-1,0,0);
  }

  /** This method will provide necessary cleanup of scrollable items.
   *  Implemented by MPlot.
   */
  protected void stopScroll () {
    // Override method to do any necessary cleanup after scrolling is finished.
    // Called in MWindow.mouseReleased()
  }

  @Override
  public void mouseClicked (MouseEvent e) {
    //System.out.println("Mouse Clicked "+name+" "+e+" dragging="+dragging);
    if ((eventFilter&NOMOUSE)!=0)
      return;
    if (dragging == 0) { // Ignore if dragging to prevent duplicate messages, see mouseReleased(..)
      px = e.getX();
      py = e.getY();
      button = event2button(e);
      htime = System.currentTimeMillis() - dtime;
      // simulate middle button <== right button held for 0.25+ second
      if (htime>250 && button==RIGHT_MOUSE_BUTTON && !Theme.getOption(Theme.MOBILE))
        button = MIDDLE_MOUSE_BUTTON;
      if (button==MIDDLE_MOUSE_BUTTON && (eventFilter&NOMIDDLEMOUSE)!=0)
        return; // since 3.3.1: ignore MMB for BUG 2712
      if (htime>500 && button==LEFT_MOUSE_BUTTON)
        process("HOVER", button,e);
      else
        process("BUTTON",button,e);  // pass MouseEvent for Bug 2458
    }
  }

  @Override
  public void mouseMoved (MouseEvent e) {
    //System.out.println("Mouse Moved "+name+" x= " +e.getX()+", xos="+e.getXOnScreen()+", mw= "+MW);
    px = e.getX();
    py = e.getY();
    if (MW!=null && MW!=this) { MW.px=pos.x+px; MW.py=pos.y+py; }
    process("POINTER",0);
  }

  @Override
  public void mouseDragged (MouseEvent e) {
    //System.out.println("Mouse Drag "+name+" "+e);
    if ((eventFilter&NOMOUSE)!=0) return;
    if (box.x==-1 || box.y==-1) return;  // drag not initiated in this window
    px = e.getX();
    py = e.getY();
    box.w = px - box.x;
    box.h = py - box.y;
    if (dragging==0) dragging = event2button(e);
    if (dragging!=0 && (Point.distance(px, py, box.x, box.y) > 5)) {
      if (this instanceof MPlot) process("DRAG",dragging,box,e); // NXM-4028
      else process("DRAG",dragging,box);
    }
  }

  @Override
  public void keyTyped (KeyEvent e) {
    // handled by keyPressed/keyReleased
  }

  @Override
  public void keyPressed (KeyEvent e) {
    if ((eventFilter&NOKEY)!=0) return;
    keycode = e.getKeyCode();
    modifiers = e.getModifiersEx(); // Updated in 3.9.0 to use extended mods
    if (keycode == KeyEvent.VK_F1) {
      helpKeyPressed();
    }
    else if ((modifiers&KeyEvent.CTRL_DOWN_MASK)!=0) { // Updated in 3.9.0 to use extended mods
      // shortcut to toggle panelized window POP/PUSH
      if (keycode==KeyEvent.VK_P) {
        if ((eventFilter&NOPUSHPOP)==0) process("POP",-1,null);
      }
    }
    else process("KEYPRESS",keycode,KeyEvent.getKeyText(keycode));
  }

  @Override
  public void keyReleased (KeyEvent e) {
    if ((eventFilter&NOKEY)!=0) return;
    keycode = e.getKeyCode();
    modifiers = e.getModifiersEx(); // Updated in 3.9.0 to use extended mods
    if ((modifiers&KeyEvent.CTRL_DOWN_MASK)!=0); // Updated in 3.9.0 to use extended mods
    else process("KEYRELEASE",keycode,KeyEvent.getKeyText(keycode));
    keycode=0; // for widgets
  }

  @Override
  public void componentMoved (ComponentEvent e) {
    if (MW==null) return;
    pos.x = panel.getLocation().x;
    pos.y = panel.getLocation().y;
    process("MOVED",0,pos);
  }

  @Override
  public void componentResized (ComponentEvent e) {
    Dimension d = panel.getSize();
// System.out.println("Comp resize "+panel+" "+d+" ComponentEvent= "+e);
// 2010-07-20 NTN: the below patch using if (..) for BUG 1866 causes a POPed PLOT
//                 to not completely fill the new Window/Frame
//    if ( (pos.w == d.width) && (pos.h == d.height) &&  // if same height AND width
//        !(this instanceof GWidget) ) {                 // AND if NOT a GWidget
//      return; // return to skip processing "RESIZE" to prevent to many PLOT refreshes for BUG 1866
//    }

    pos.w = d.width;
    pos.h = d.height;
    process("RESIZE",0);
// 2010-07-20 commented out below line for BUG 1866 to reduce PLOT refreshes
// This needs to call revalidate() - Right now, the repaint method is overridden to call revalidate, but that might be changed.
    panel.repaint(); // in case of resize to smaller; is this "extra" repaint still needed? JEFF: YES it is
  }

  @Override
  public void componentShown (ComponentEvent e) {
  // System.out.println("Comp shown "+panel+" event="+e);
    if (status!=INIT) status=FRONT;
    if (panel!=null) panel.repaint();
  }

  @Override
  public void componentHidden (ComponentEvent e) {
  // System.out.println("Comp hidden "+panel+" event="+e);
  }

  /** Does the keyboard have focus on this panel?
      @return true if this window has focus, false otherwise.
   */
  public boolean hasFocus() {
    return panel.hasFocus();
  }

  /** Get keyboard focus on this panel. Pretend we have it, if not able to request.
      @return true  if this window has focus and is able to request it.<br>
              false if this window does not have focus.
   */
  public boolean getFocus() {
    boolean gotFocus = panel.hasFocus();
    if (!gotFocus) panel.requestFocus();
    return gotFocus;
  }

  /**
   * Convert event to button number.
   * @param e The event that is being converted
   * @param isFromDrag Is this a mouse dragging event, true or false <br>
   * @since Updated in 3.9.0 - previous functionality was deprecated.
   */
  private int event2button (AWTEvent e) {
    MouseEvent me = (MouseEvent) e;
    int btn = me.getButton();

    // If it is a dragging event, the button will return as 0, need to use the modifiers there.
    int modifiers = me.getModifiersEx();
    if (btn == 0) {
      if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) btn = MouseEvent.BUTTON1;
      else if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) btn = MouseEvent.BUTTON2;
      else if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) btn = MouseEvent.BUTTON3;
      else btn = MouseEvent.BUTTON1;
    }
    else if(button>MouseEvent.BUTTON3) button = MouseEvent.BUTTON1;
    if (me.isShiftDown()) btn = btn + 100;
    //  if (me.isControlDown()) {
    //      button = button+200;
    //}
    return btn;
  }

  /** Draws the new drag box, or redraws the previous drag box in XOR mode to restore pixels. <br>
   *  <br>
   *  Note: This method should never be called by {@link MPlot}, {@link MPlot} should exclusively
   *  use <code>rubberbox(Graphics,int)</code>.
   *  @param mode The drag box mode, see <code>rubberbox(Graphics,int)</code> for details.
   *  @deprecated Since NeXtMidas 3.7.1 - This method should never have been public, and is prone
   *              to causing refresh issues if used incorrectly.
   */
  @Deprecated
  public void rubberbox (int mode) {
    // If MPlot calls this method it will cause refresh issues, see NXM-2841
    rubberbox(panel.getGraphics(), mode);
  }

  /** Draws the new drag box, or redraws the previous drag box in XOR mode to restore pixels.
   *  @param g    The graphics context to use.
   *  @param mode The drag box is drawn according to mode as defined below:<pre>
   *      {@code if (mode <  0)} redraw the previous drag box in XOR mode to restore pixels,
   *                             and reset rbox values to 0.
   *      {@code if (mode == 0)} redraw the previous drag box in XOR mode to restore pixels,
   *                             reset rbox values to 0, then draw new drag box.
   *      {@code if (mode >  0)} draw the new drag box.</pre>
   *  @since NeXtMidas 3.7.1
   */
  void rubberbox (Graphics g, int mode) { // package-private, only used by MWindow and MPlot
    int x,y,w,h;
    if (mode==0) rubberbox(g,-1);
    if (mode>=0) rbox.setBounds(box);
    if (rbox.w>=0) { x = rbox.x; w = rbox.w; }
    else { x = rbox.x + rbox.w; w = -rbox.w; }
    if (rbox.h>=0) { y = rbox.y; h = rbox.h; }
    else { y = rbox.y + rbox.h; h = -rbox.h; }
    if (mode<0) rbox.setBounds(0,0,0,0);
    if (g != null) {                     // Bug 1301 - prevent possible NPE
      g.setXORMode(panel.getBackground());
      g.drawRect(x, y, w, h);
      g.setXORMode(panel.getForeground());
      g.drawRect(x+1, y+1, w-2, h-2);
      g.setPaintMode();
      g.dispose();
    }
  }

  /** Default      shadowbox mode */  public static final int SB_DEF        =  0;
  /** Highlight    shadowbox mode */  public static final int SB_HILIT      =  1;
  /** Checkbox     shadowbox mode */  public static final int SB_CHKBOX     =  2;
  /** Alert        shadowbox mode */  public static final int SB_ALERT      =  3;
  /** Adjustment   shadowbox mode */  public static final int SB_ADJUST     =  4;
  /** Unhilight    shadowbox mode */  public static final int SB_UNHILIT    = -1;
  /** Flat         shadowbox mode */  public static final int SB_FLAT       = -2;
  /** Reverse      shadowbox mode */  public static final int SB_REV        = -3;
  /** Line         shadowbox mode */  public static final int SB_LINE       = -4;
  /** Shade        shadowbox mode */  public static final int SB_SHADE      = -5;
  /** Flat-shade   shadowbox mode */  public static final int SB_FLAT_SHADE = -6;
  /** Shaded X     shadowbox mode */  public static final int SB_SHADEX     = -7;
  /** Hairline     shadowbox mode */  public static final int SB_HAIRLINE   = -8;
  /** PushPop Icon shadowbox mode */  public static final int SB_SHADE_PUSHPOP = -9; // Since NeXtMidas 3.5.4
  /** Separator    shadowbox mode */  public static final int SB_SEPERATOR  = SB_DEF;

  /** Draws a shadow box with the given bounds in the mode specified.

      @param x    The <I>x</I>-coordinate.
      @param y    The <I>y</I>-coordinate.
      @param w    The width.
      @param h    The height.
      @param mode The mode of this shadowbox is indicated by one of following:
                  {@link #SB_DEF}, {@link #SB_HILIT}, {@link #SB_CHKBOX},
                  {@link #SB_ALERT}, {@link #SB_UNHILIT}, {@link #SB_FLAT},
                  {@link #SB_REV}, {@link #SB_LINE}, {@link #SB_SHADE},
                  {@link #SB_SEPERATOR}.
      @deprecated In 3.9.0 to fix graphical timing issues
  */
  @Deprecated
  public void shadowbox (int x, int y, int w, int h, int mode) {
    shadowbox(g,x,y,w,h,mode);
  }

  /** Draws a shadow box with the given bounds in the mode specified.

      @param g    graphics context
      @param x    The <I>x</I>-coordinate.
      @param y    The <I>y</I>-coordinate.
      @param w    The width.
      @param h    The height.
      @param mode The mode of this shadowbox is indicated by one of following:
                  {@link #SB_DEF}, {@link #SB_HILIT}, {@link #SB_CHKBOX},
                  {@link #SB_ALERT}, {@link #SB_UNHILIT}, {@link #SB_FLAT},
                  {@link #SB_REV}, {@link #SB_LINE}, {@link #SB_SHADE},
                  {@link #SB_SEPERATOR}.
   */
  @InternalUseOnly("For use by panel only)")
  public void shadowbox (Graphics g, int x, int y, int w, int h, int mode) {
    if (g==null) return;
    if (mode==SB_FLAT || mode==SB_FLAT_SHADE) {
      g.setColor (theme.cwbg);
      g.fillRect (x,y,w,h);
      g.setColor (theme.cfg);
      if (!(this instanceof GWidget)) return;
      GWidget gw = (GWidget)this;
      if (mode==SB_FLAT_SHADE);
      else if (gw.pGroup==null || gw.pDetach || !gw.is(GWidget.INLINE)) return;
      // part of inLine Group, add borders
      g.setColor (theme.cwbs);
      g.drawLine (x+1,y,x+1,y+h);
      g.drawLine (x+w-2,y,x+w-2,y+h);
      g.setColor (theme.cwms);
      g.drawLine (x+2,y,x+2,y+h);
      g.drawLine (x+w-3,y,x+w-3,y+h);
      if (mode==SB_FLAT_SHADE || this==gw.pGroup.lastWidget)
      g.drawLine (x+2,y+h-1,x+w-3,y+h-1);
      g.setColor (theme.cfg);
      return;
    }
    // Since NeXtMidas 3.5.4 Bug 2896, PUSHPOP ICON is no longer the Shaded 'X'
    if (mode==SB_SHADE || mode==SB_SHADEX || mode==SB_SHADE_PUSHPOP) {
      g.setColor (theme.cwbg);
      g.drawLine (x,y,x,y+h);
      g.drawLine (x+w-1,y,x+w-1,y+h);
      g.setColor (theme.cwms);
      g.drawLine (x+2,y,x+w-3,y);
      int ah = Math.abs(h);
      int j1=(int)(ah*.15);
      for (int i=1; i<ah; i++) {
        if (i<j1) g.setColor (MColor.getColorMix(theme.cwbg,theme.cwbs,(double)i/j1));
        else      g.setColor (MColor.getColorMix(theme.cwbs,theme.cwms,(double)(i-j1)/(ah-j1-1)));
        int ay = (h>0)? y+i : y-i;
        g.drawLine (x+1,ay,x+w-2,ay);
      }
      g.setColor (theme.cwbs);
      g.drawLine (x+1,y+j1,x+1,y+h);
      g.drawLine (x+w-2,y+j1,x+w-2,y+h);
      if (mode==SB_SHADEX) {
        int s=3, xs=x+w-10, ys=y+h/2;
        g.setColor (theme.cwts);
        g.drawLine (xs-s,ys-s,xs+s,ys+s);
        g.drawLine (xs-s,ys+s,xs+s,ys-s);
        g.setColor (theme.cwbs); xs++;
        g.drawLine (xs-s,ys-s,xs+s,ys+s);
        g.drawLine (xs-s,ys+s,xs+s,ys-s);
      }
      // Since NeXtMidas 3.5.4 Bug 2896, PUSHPOP ICON is now its own icon (not an "X")
      if (mode==SB_SHADE_PUSHPOP) {
        drawPushPopIcon(g,x,y,w,h);
      }
      g.setColor (theme.cfg);
      return;
    }
    Color cwork;
    Color cfill = theme.cwbg;
    if (mode == SB_CHKBOX) cfill = theme.cwfh;
    else if (mode==SB_ADJUST) cfill=MColor.getColorMix(theme.cwfh,theme.cwbg,0.7);

    if (mode==SB_REV) cwork=theme.cwbs;
    else if (mode==SB_LINE || mode==SB_HAIRLINE) cwork=theme.cfg;
    else if (mode>=SB_DEF) cwork=theme.cwts;
    else cwork=cfill;

    if (mode==SB_DEF||mode==SB_CHKBOX||mode==SB_ADJUST) {
      g.setColor (cfill);
      g.fillRect (x,y,w,h);
    }
    if (mode != SB_ADJUST) {
      g.setColor (cwork);
      g.drawLine (x,y,x+w-1,y);
      g.drawLine (x,y,x,y+h-1);
    }
    if (mode != SB_HAIRLINE) {
      g.setColor (MColor.getColorMix(cfill,cwork,0.7));
      g.drawLine (x+1,y+1,x+w-2,y+1);
      g.drawLine (x+1,y+1,x+1,y+h-2);
    }
    if (mode==SB_REV) cwork=theme.cwts;
    else if (mode==SB_LINE || mode==SB_HAIRLINE) cwork=theme.cfg;
    else if (mode==SB_ALERT) cwork=Color.yellow;
    else if (mode==SB_ADJUST) cwork=theme.cwfh;
    else if (mode>=SB_DEF) cwork=theme.cwbs;
    else cwork=cfill;
    if (mode != SB_ADJUST) {
      g.setColor (cwork);
      g.drawLine (x,y+h-1,x+w-1,y+h-1);
      g.drawLine (x+w-1,y,x+w-1,y+h-1);
    }
    if (mode != SB_HAIRLINE) {
      g.setColor (MColor.getColorMix(cfill,cwork,0.7));
      g.drawLine (x+1,y+h-2,x+w-2,y+h-2);
      g.drawLine (x+w-2,y+1,x+w-2,y+h-2);
    }
    g.setColor (theme.cfg);
  }

  /**
   * Draw a Shadowbox with the custom icon
   * @param x x coordinate
   * @param y y coordinate
   * @param w width
   * @param h height
   * @param mode shadowbox mode
   * @param customIconShape custom Icon shape
   * @param iconSymbolName icon symbol name
   * @since NeXtMidas 3.5.4 Bug 2895
   * @deprecated in NeXtMidas 3.9.0 to fix graphical timing issues (NXM-3213)
   */
  @Deprecated
  @InternalUseOnly("For use by panel only")
  public void shadowboxCustomIcon (int x, int y, int w, int h, int mode, int customIconShape, String iconSymbolName) {
    shadowboxCustomIcon(g, x, y, w, h, mode, customIconShape, iconSymbolName);
  }

  /** Draw a Shadowbox with the custom icon
      @param g the Graphics object used for painting
      @param x x coordinate
      @param y y coordinate
      @param w width
      @param h height
      @param mode shadowbox mode
      @param customIconShape custom Icon shape
      @param iconSymbolName icon symbol name
      @since NeXtMidas 3.9.0 (replaces version without graphics parameter)
   */
  @InternalUseOnly("For use by panel only")
  public void shadowboxCustomIcon (Graphics g, int x, int y, int w, int h, int mode,
                                   int customIconShape, String iconSymbolName) {
    shadowbox(g,x,y,w,h,mode); // draw basic shadowbox with PUSHPOP (rightmost icon)
    // draw second icon
    switch (customIconShape) {
      case nxm.sys.prim.panel.CUSTOM_ICON_SYMBOL_NONE: return;
      case nxm.sys.prim.panel.CUSTOM_ICON_SYMBOL_SHADEDX:
        drawShadedXIcon(g,x-10,y,w,h);
        return;
      default:
        drawSymbolAsIcon(g,x-10,y,w,h,iconSymbolName);
    }
  }

  private void drawSymbolAsIcon (Graphics g, int x, int y, int w, int h, String symbolName) {
    int xs=x+w-10;
    int ys=y+h/2;
    g.setColor (theme.cwbs);
    g.drawRect(xs-3,ys-5,8,11);
    g.fillRect(xs-3,ys-5,8,11);
    g.setColor (theme.cwts);
    Symbol iconSymbol = Symbol.getSymbol(symbolName, 7);
    iconSymbol.draw(xs + 1, ys + 1, g);
  }

  /** Draws an arrow at the given (<I>x</I>,<I>y</I>) coordinate, with the given
      width. Direction and type is indicated with the parameter <I>mode</I>.
      The primary use of this function is to draw the 'modifier arrows' associated
      with various widgets (see {@link GWidget#drawDiamond(Graphics,int, int, int, int)}),
      in which case it is common to draw the shadow bullet ('b') and then one of the
      shadow arrows (e.g. 'u') on top of it.

      @param x    The <I>x</I>-coordinate of the arrow.
      @param y    The <I>y</I>-coordinate of the arrow.
      @param w    The width of the arrow.
      @param mode Indicates the direction and type of this arrow as follows:
                  <pre>
                    'B' -- Bullet mode. A diamond-shaped bullet.
                    'U' -- Up arrow mode.
                    'D' -- Down arrow mode.
                    'L' -- Left arrow mode.
                    'R' -- Right arrow mode.
                    ' ' -- Clear arrow area.
                    'b' -- Bullet shadow mode.
                    'u' -- Up arrow shadow mode.
                    'd' -- Down arrow shadow mode.
                    'l' -- Left arrow shadow mode.
                    'r' -- Right arrow shadow mode.
                    'm' -- Pull Down (menu) arrow shadow mode.</pre>
      @deprecated in NeXtMidas 3.9.0 for graphical timing issues (NXM-3123)
    */
  @Deprecated
  public void shadowarrow (int x, int y, int w, char mode) {
    shadowarrow(g,x,y,w,mode);
  }

  /** Draws an arrow at the given (<I>x</I>,<I>y</I>) coordinate, with the given
      width. Direction and type is indicated with the parameter <I>mode</I>.
      The primary use of this function is to draw the 'modifier arrows' associated
      with various widgets (see {@link GWidget#drawDiamond(Graphics, int, int, int, int)}),
      in which case it is common to draw the shadow bullet ('b') and then one of the
      shadow arrows (e.g. 'u') on top of it.

      @param g    The Graphics object used for painting
      @param x    The <I>x</I>-coordinate of the arrow.
      @param y    The <I>y</I>-coordinate of the arrow.
      @param w    The width of the arrow.
      @param mode Indicates the direction and type of this arrow as follows:
                  <pre>
                    'B' -- Bullet mode. A diamond-shaped bullet.
                    'U' -- Up arrow mode.
                    'D' -- Down arrow mode.
                    'L' -- Left arrow mode.
                    'R' -- Right arrow mode.
                    ' ' -- Clear arrow area.
                    'b' -- Bullet shadow mode.
                    'u' -- Up arrow shadow mode.
                    'd' -- Down arrow shadow mode.
                    'l' -- Left arrow shadow mode.
                    'r' -- Right arrow shadow mode.
                    'm' -- Pull Down (menu) arrow shadow mode.</pre>
   */
  public void shadowarrow (Graphics g, int x, int y, int w, char mode) {

    if (g==null) return;
    int r=1,s=r+1;
    if (mode==' ') {
      g.setColor (theme.cwbg); g.fillRect (x-w-4,y-w-4,w+w+8,w+w+8);
    }
    if (mode=='B') {
      g.setColor (theme.cwbg); g.fillRect (x-w-4,y-w-4,w+w+8,w+w+8);
      g.setColor (theme.cwbs); g.drawLine (x,y+w,x-w,y); g.drawLine (x,y-w,x-w,y);
                               g.drawLine (x,y+w,x+w,y); g.drawLine (x,y-w,x+w,y);
 x--; g.setColor (theme.cwts); g.drawLine (x,y+w,x-w,y); g.drawLine (x,y-w,x-w,y);
                               g.drawLine (x,y+w,x+w,y); g.drawLine (x,y-w,x+w,y);
    }
    else if (mode=='U') {
      g.setColor (theme.cwbs); g.drawLine (x-w,y,x,y-w); g.drawLine (x+w,y,x,y-w);
 x--; g.setColor (theme.cwts); g.drawLine (x-w,y,x,y-w); g.drawLine (x+w,y,x,y-w);
    }
    else if (mode=='D') {
      g.setColor (theme.cwbs); g.drawLine (x-w,y,x,y+w); g.drawLine (x+w,y,x,y+w);
 x--; g.setColor (theme.cwts); g.drawLine (x-w,y,x,y+w); g.drawLine (x+w,y,x,y+w);
    }
    else if (mode=='L') {
      g.setColor (theme.cwbs); g.drawLine (x,y+w,x-w,y); g.drawLine (x,y-w,x-w,y);
 x--; g.setColor (theme.cwts); g.drawLine (x,y+w,x-w,y); g.drawLine (x,y-w,x-w,y);
    }
    else if (mode=='R') {
      g.setColor (theme.cwbs); g.drawLine (x,y+w,x+w,y); g.drawLine (x,y-w,x+w,y);
 x--; g.setColor (theme.cwts); g.drawLine (x,y+w,x+w,y); g.drawLine (x,y-w,x+w,y);
    }
    else if (mode=='b') {
      g.setColor (theme.cwbg); g.fillRect (x-w-s,y-w-s,w+w+s+s+1,w+w+s+s+1);
      g.setColor (theme.cwfh); g.fillRect (x-r,y-r,r+r,r+r);
      g.setColor (theme.cwbh); g.drawRect (x-r,y-r,r+r,r+r);
    }
    else if (mode=='u') {
      y-=s; fill3( x,y-w, x+w,y, x-w,y, g);
    }
    else if (mode=='d') {
      y+=s; fill3( x,y+w, x+w,y, x-w,y, g);
    }
    else if (mode=='l') {
      x-=s; fill3( x-w,y, x,y+w, x,y-w, g);
    }
    else if (mode=='r') {
      x+=s; fill3( x+w,y, x,y+w, x,y-w, g);
    }
    else if (mode=='m') {
        g.setColor(theme.cwbh);
        g.drawLine(x-w,y-w,x+w,y-w);         //top pixel of overbar
        g.setColor(theme.cwfh);
        g.drawLine(x-w,y-w+1,x+w,y-w+1);    //bottom pixel of overbar
      y+=s; fill3( x,y+w, x+w,y, x-w,y, g); //this draws the arrow head
    }
    g.setColor (theme.cfg);
  }

  /** Draw Shaded X for icon
      @param g  The Graphics object used for painting
      @param x  x coordinate
      @param y  y coordinate
      @param w  width
      @param h  height
      @since NeXtMidas 3.5.4 Bug 2895
   */
  private void drawShadedXIcon (Graphics g, int x, int y, int w, int h) {
    int s=3, xs=x+w-10, ys=y+h/2;
    g.setColor (theme.cwts);
    g.drawLine (xs-s,ys-s,xs+s,ys+s);
    g.drawLine (xs-s,ys+s,xs+s,ys-s);
    g.setColor (theme.cwbs); xs++;
    g.drawLine (xs-s,ys-s,xs+s,ys+s);
    g.drawLine (xs-s,ys+s,xs+s,ys-s);
  }

  /** @since NeXtMidas 3.5.4 Bug 2896  */
  private void drawPushPopIcon (Graphics g, int x, int y, int w, int h) {
    if (g==null) return;
    //true is with outline shading. false is with the background box shading
    drawNESWArrows(g, x, y, w, h, true);
  }

  /** Draw a PUSHPOP icon of North-East and South-West Arrows
      @param g  the Graphics object used for painting
      @param x  x coordinate
      @param y  y coordinate
      @param w  width
      @param h  height
      @param shadeOnly true = only shade the icon (do do fill a background box)
   */
  private void drawNESWArrows(Graphics g, int x, int y, int w, int h, boolean shadeOnly) {
    int s=3, xs=x+w-10, ys=y+h/2;

    g.setColor (theme.cwbs); // color for shading
    if(!shadeOnly) {
      g.drawRect(xs-3,ys-5,8,12);
      g.fillRect(xs-3,ys-5,8,12);
    }
    else{
      // NE Stem shading
      g.drawLine (xs-1,ys-1,xs+1,ys-s); // stem (left to right) towards point
      g.drawLine (xs,ys,xs+2,ys-2); // stem (left to right) towards point

      // SW Arrow Stem shading
      g.drawLine (xs,ys+3,xs+2,ys+1);
      g.drawLine (xs+1,ys+s+1,xs+s,ys+2);

      // NE Arrow shading
      g.drawLine (xs+1,ys-s-2,xs+s+1,ys-s-2);   // arrow head horizontal (left to right)
      g.drawLine (xs+s+1,ys-s-1,xs+s+1,ys-s+1);   // arrow head vertical (top to bottom)

      // SW arrow shading
      g.drawLine (xs-2,ys+s+3,xs+1,ys+s+3); // arrow head horizontal (left to right)
      g.drawLine (xs-2,ys+s,xs-2,ys+s+2); // arrow head vertical (top to bottom)
    }

    // NE arrow
    g.setColor (theme.cwts);
    g.drawLine (xs-1,ys,xs+s,ys-s-1); // stem (left to right) towards point
    g.drawLine (xs+s-2,ys-s-1,xs+s,ys-s-1); // arrow head horizontal (left to right)
    g.drawLine (xs+s,ys-s-1,xs+s,ys-s+1);       // arrow head vertical (top to bottom)

    // SW arrow
    g.drawLine (xs-1,ys+s+2,xs+s,ys+1); // stem (left to right) away from point
    g.drawLine (xs-1,ys+s+2,xs+1,ys+s+2); // arrow head horizontal (left to right)
    g.drawLine (xs-1,ys+s+2,xs-1,ys+s);
  }

  /** @since NeXtMidas 3.5.4 icon considered, but eliminated as new PUSHPOP icon*/
  private void drawBoxWithArrow (Graphics g, int x, int y, int w, int h, boolean shadeOnly) {
    int s=3, xs=x+w-10, ys=y+h/2;

    if(!shadeOnly) {
      g.setColor (theme.cwbs);
      g.drawRect(xs-3,ys-3,9,9);
      g.fillRect(xs-3,ys-3,9,9);
    }

    if(shadeOnly) {
      // Stem Shading
      g.setColor (theme.cwbs);
      g.drawLine (xs+s-1,ys,xs+3,ys+2-s);  // Top Arrow Stem
      g.drawLine (xs+s,ys+1,xs+4,ys+3-s);  // Bottom Arrow Stem

      // Arrow Point Shading
      g.drawLine (xs+3,ys+2-s,xs+1,ys+2-s); // Left Inside Horizontal Shading
      g.drawLine (xs+1,ys+2-s,xs+1,ys-s);   // Left Vertical Shading
      g.drawLine (xs+1,ys-s,xs+6,ys-s);     // Top Horizontal Shading
      g.drawLine (xs+6,ys-s,xs+6,ys-s+5);   // Right Outside Vertical Shading
      g.drawLine (xs+5,ys-s+5,xs+4,ys-s+5); // Right Horizontal Shading
      g.drawLine (xs+4,ys-s+5,xs+4,ys-s+3); // Right Inside Vertical Shading

      // Box Shading
      g.drawLine (xs+s-2,ys,xs+s-6,ys);     // Top Horizontal
      g.drawLine (xs+s-6,ys+1,xs+s-6,ys+6); // Left Vertical
      g.drawLine (xs+s-5,ys+6,xs+s,ys+6);   // Bottom Horizontal
      g.drawLine (xs+s,ys+5,xs+s,ys+1);     // Right Vertical
    }

    // NE arrow
    g.setColor (theme.cwts);
    g.drawLine (xs+s-1,ys+1,xs+5,ys+1-s);   // Arrow Stem
    g.drawLine (xs+5,ys+1-s,xs+5,ys+1);     // Right Arrow Point
    g.drawLine (xs+5,ys+1-s,xs+s-1,ys+1-s); // Left Arrow Point
    g.drawRect (xs+s-5,ys+1,4,4);           // Box connected to NE Arrow
  }

  /** @since NeXtMidas 3.5.4 icon considered, but eliminated as new PUSHPOP icon*/
  private void drawCorners (Graphics g, int x, int y, int w, int h, boolean shadeOnly) {
    int s=3, xs=x+w-10+2, ys=y+h/2-2, lts=s-1;

    if(!shadeOnly) {
      g.setColor (theme.cwbs);
      g.drawRect(xs-5,ys-3,10,10);
      g.fillRect(xs-5,ys-3,10,10);
    }

    if(shadeOnly) {
      g.setColor (theme.cwbs);
      g.drawLine (xs-4,ys-3,xs-1,ys-3); // Top Left Outside Horizontal
      g.drawLine (xs-3,ys-1,xs-2,ys-1); // Top Left Inside Horizontal
      g.drawLine (xs-5,ys-3,xs-5,ys+1); // Top Left Outside Vertical
      g.drawLine (xs-3,ys-1,xs-3,ys+1); // Top Left Inside Vertical
      g.drawLine (xs-1,ys-1,xs-1,ys-3); // Top Left, Right side, Vertical
      g.drawLine (xs-4,ys+1,xs-3,ys+1); // Top Left, Left side, Horizontal

      g.drawLine (xs+4,ys-3,xs+1,ys-3); // Top Right Outside Horizontal
      g.drawLine (xs+3,ys-1,xs+1,ys-1); // Top Right Inside Horizontal
      g.drawLine (xs+5,ys-3,xs+5,ys+1); // Top Right Outside Vertical
      g.drawLine (xs+3,ys-1,xs+3,ys+1); // Top Right Inside Vertical
      g.drawLine (xs+1,ys-3,xs+1,ys-1); // Top Right, Left side, Vertical
      g.drawLine (xs+4,ys+1,xs+5,ys+1); // Top Right, Right side, Horizontal

      g.drawLine (xs-5,ys+7,xs-1,ys+7); // Bottom Left Outside Horizontal
      g.drawLine (xs-3,ys+5,xs-2,ys+5); // Bottom Left Inside Horizontal
      g.drawLine (xs-5,ys+6,xs-5,ys+3); // Bottom Left Outside Vertical
      g.drawLine (xs-3,ys+5,xs-3,ys+3); // Bottom Left Inside Vertical
      g.drawLine (xs-1,ys+7,xs-1,ys+5); // Bottom Left, Right side, Vertical
      g.drawLine (xs-4,ys+3,xs-4,ys+3); // Bottom Left, Left side, Horizontal

      g.drawLine (xs+5,ys+7,xs+1,ys+7); // Bottom Right Outside Horizontal
      g.drawLine (xs+3,ys+5,xs+1,ys+5); // Bottom Right Inside Horizontal
      g.drawLine (xs+5,ys+6,xs+5,ys+3); // Bottom Right Outside Vertical
      g.drawLine (xs+3,ys+5,xs+3,ys+3); // Bottom Right Inside Vertical
      g.drawLine (xs+1,ys+6,xs+1,ys+6); // Bottom Right, Left side, Vertical
      g.drawLine (xs+4,ys+3,xs+4,ys+3); // Bottom Right, Right side, Horizontal
    }

    // Corners
    g.setColor (theme.cwts);
    g.drawLine (xs-4,ys-2,xs-2,ys-2); // Top Left Horizontal
    g.drawLine (xs-4,ys-2,xs-4,ys);   // Top left Vertical

    g.drawLine (xs+4,ys-2,xs+2,ys-2); // Top Right Horizontal
    g.drawLine (xs+4,ys-2,xs+4,ys);   // Top Right Vertical

    g.drawLine (xs-4,ys+6,xs-2,ys+6); // Bottom Left Horizontal
    g.drawLine (xs-4,ys+6,xs-4,ys+4); // Bottom left Vertical

    g.drawLine (xs+4,ys+6,xs+2,ys+6); // Bottom Right Horizontal
    g.drawLine (xs+4,ys+6,xs+4,ys+4); // Bottom Right Vertical
  }

  /** Draws and fills the polygon defined by the 3 x,y coordinates given. A line is drawn
      from the third point back to the first point to close the polygon.
      This should only be called from shadowarrow().
   */
  private void fill3 (int x1,int y1,int x2,int y2,int x3,int y3, Graphics g) {
    int[] xp = {x1,x2,x3,x1};
    int[] yp = {y1,y2,y3,y1};
    g.setColor (theme.cwfh);
    g.fillPolygon(xp,yp,4);
    g.setColor (theme.cwbh);
    g.drawPolygon(xp,yp,4);
  }

  /**
   * Draw a Slider Box/Bar - determine direction from height and width
   * @param x top left coordinate of slider bar
   * @param y top left coordinate of slider bar
   * @param w width of slider bar
   * @param h height of slider bar
   * @param f fraction of the way through for slider knob
   * @param flags flags indicating type of slider bar
   * @deprecated in 3.9.0 to avoid graphical timing issues use {@link #sliderbox(Graphics, int, int, int, int, double, int)}
   */
  @Deprecated
  public void sliderbox (int x, int y, int w, int h, double f, int flags) {
    sliderbox(g,x,y,w,h,f,flags);
  }

  /**
   * Draw a Slider Box/Bar - determine direction from height and width
   * @param g the Graphics object used for painting
   * @param x top left coordinate of slider bar
   * @param y top left coordinate of slider bar
   * @param w width of slider bar
   * @param h height of slider bar
   * @param f fraction of the way through for slider knob
   * @param flags flags indicating type of slider bar
   */
  public void sliderbox (Graphics g, int x, int y, int w, int h, double f, int flags) {
    if (h<w) drawHSlider (x,y,w,h, f,flags, g,theme);
    if (w<h) drawVSlider (x,y,w,h, f,flags, g,theme);
  }

  /** Slider flag SLIDER_ACTIVE */
  public static final int SLIDER_ACTIVE=0x1;
  /** Slider flag SLIDER_NOTRACK */
  public static final int SLIDER_NOTRACK=0x2;
  /** Slider flag SLIDER_INSET */
  public static final int SLIDER_INSET=0x4;

  /**
   * Draw a Horizontal Slider Box/Bar
   * @param x top left coordinate of slider bar
   * @param y top left coordinate of slider bar
   * @param w width of slider bar
   * @param h height of slider bar
   * @param f fraction of the way across for slider knob
   * @param flags flags indicating type of slider bar
   * @param g Graphics that this is a part of
   * @param theme Theme indicated color scheme, etc.
   */
  public static void drawHSlider (int x, int y, int w, int h, double f, int flags, Graphics g, Theme theme) {
    int n=w, sw=SCROLL_KNOB_SIZE, swh=sw/2;
    g.setColor (theme.cwbg);
    g.drawLine(x,y-2,x+w,y-2);
    g.drawLine(x,y+2,x+w,y+2);
    if ((flags&SLIDER_NOTRACK)==0)
    for (int i=0; i<n; i++) {
      g.setColor (MColor.getColorMix(theme.cwbs,Color.black,(double)(i)/(n)));
      g.drawLine(x+i,y-1,x+i,y-1);
      g.setColor (MColor.getColorMix(theme.cwts,theme.cwbs,(double)Math.abs(i-n+i)/(n)));
      g.drawLine(x+i,y+0,x+i,y+0);
      g.setColor (MColor.getColorMix(theme.cwms,theme.cwbs,(double)(i)/(n)));
      g.drawLine(x+i,y+1,x+i,y+1);
    }
    int aw = n-sw;
    x += swh+Math.max(0,Math.min(aw, (int)(f*aw) ));
    for (int i=0; i<swh; i++) {
      if ((flags&SLIDER_ACTIVE)!=0) g.setColor(theme.cwfg);
      g.setColor (MColor.getColorMix(theme.cwfh,theme.cwfg,(double)(i)/(swh)));
      g.drawLine(x+i,y-0,x+i,y-0);
      g.drawLine(x-i,y-0,x-i,y-0);
      g.setColor (MColor.getColorMix(theme.cwts,theme.cwms,(double)(i)/(swh)));
      g.drawLine(x+i,y-1,x+i,y-1);
      g.drawLine(x-i,y-1,x-i,y-1);
      g.drawLine(x+i,y+1,x+i,y+1);
      g.drawLine(x-i,y+1,x-i,y+1);
      g.setColor (MColor.getColorMix(theme.cwts,theme.cwms,(double)(i)/(swh)));
      g.drawLine(x+i,y-2,x+i,y-2);
      g.drawLine(x-i,y-2,x-i,y-2);
      g.drawLine(x+i,y+2,x+i,y+2);
      g.drawLine(x-i,y+2,x-i,y+2);
    }
    g.setColor(theme.cwbs);
    g.drawLine(x-swh,y+2,x-swh,y-2);
    g.drawLine(x+swh,y+2,x+swh,y-2);
  }

  /**
   * Draw a Vertical Slider Bar
   * @param x top left coordinate of slider bar
   * @param y top left coordinate of slider bar
   * @param w width of slider bar
   * @param h height of slider bar
   * @param f fraction of the way down for slider knob
   * @param flags flags indicating type of slider bar
   * @param g Graphics that this is a part of
   * @param theme Theme indicated color scheme, etc.
   */
  public static void drawVSlider (int x, int y, int w, int h, double f, int flags, Graphics g, Theme theme) {
    if ((flags&SLIDER_INSET)!=0) {
      int xi = x-w/2;
      int wi = w-1;
      g.setColor (theme.cwms);
      g.fillRect(xi,y,wi,h);
      g.setColor (theme.cwts);
      g.drawLine(xi,y,xi,y+h-1);
      g.setColor (theme.cwbs);
      g.drawLine(xi+wi,y,xi+wi,y+h-1);
      y+=4; h-=8;
      g.setColor (theme.cwbs);
      g.drawLine(x-3,y,x-3,y+h-1);
      g.setColor (theme.cwts);
      g.drawLine(x+3,y,x+3,y+h-1);
    }
    g.setColor (theme.cwbg);
    g.fillRect(x-2,y,5,h);
    int n=h, sw=SCROLL_KNOB_SIZE, swh=sw/2;
    if ((flags&SLIDER_NOTRACK)==0) {
      for (int i=0; i<n; i++) {
        g.setColor (MColor.getColorMix(theme.cwbs,Color.black,(double)(i)/(n)));
        g.drawLine(x-1,y+i,x-1,y+i);
        g.setColor (MColor.getColorMix(theme.cwts,theme.cwbs,(double)Math.abs(i-n+i)/(n)));
        g.drawLine(x+0,y+i,x+0,y+i);
        g.setColor (MColor.getColorMix(theme.cwms,theme.cwbs,(double)(i)/(n)));
        g.drawLine(x+1,y+i,x+1,y+i);
      }
    }
    int aw = n-sw;
    y += swh+Math.max(0,Math.min(aw, (int)(f*aw) ));
    for (int i=0; i<swh; i++) {
      if ((flags&SLIDER_ACTIVE)!=0) g.setColor(theme.cwfg);
      else g.setColor (MColor.getColorMix(theme.cwfh,theme.cwfg,(double)(i)/(swh)));
      g.drawLine(x-0,y+i,x-0,y+i);
      g.drawLine(x-0,y-i,x-0,y-i);
      g.setColor (MColor.getColorMix(theme.cwts,theme.cwms,(double)(i)/(swh)));
      g.drawLine(x-1,y+i,x-1,y+i);
      g.drawLine(x-1,y-i,x-1,y-i);
      g.drawLine(x+1,y+i,x+1,y+i);
      g.drawLine(x+1,y-i,x+1,y-i);
      g.setColor (MColor.getColorMix(theme.cwts,theme.cwms,(double)(i)/(swh)));
      g.drawLine(x-2,y+i,x-2,y+i);
      g.drawLine(x-2,y-i,x-2,y-i);
      g.drawLine(x+2,y+i,x+2,y+i);
      g.drawLine(x+2,y-i,x+2,y-i);
    }
    g.setColor(theme.cwbs);
    g.drawLine(x-2,y-swh,x+2,y-swh);
    g.drawLine(x-2,y+swh,x+2,y+swh);
  }

  /** Draws a string on this window's graphics panel.
      Since 3.1.1: if text is null, then nothing is done.

      @param text  The String to be drawn.
      @param x     The <I>x</I>-coordinate at which to draw the string.
      @param y     The <I>y</I>-coordinate at which to draw the string.
      @param size  The size of the Font.
      @param flags One of the drawString flags ({@link #CENTER},
                   {@link #RIGHT}, {@link #BOLD}, {@link #HILIT}).
      @deprecated in 3.9.0 to fix graphical timing issues
    */
  @Deprecated
  public void drawString (String text, int x, int y, int size, int flags) {
    drawString(g,text,x,y,size,flags);
  }

  /** Draws a string on this window's graphics panel.
      Since 3.1.1: if text is null, then nothing is done.

      @param g     The Graphics object used for painting
      @param text  The String to be drawn.
      @param x     The <I>x</I>-coordinate at which to draw the string.
      @param y     The <I>y</I>-coordinate at which to draw the string.
      @param size  The size of the Font.
      @param flags One of the drawString flags ({@link #CENTER},
                   {@link #RIGHT}, {@link #BOLD}, {@link #HILIT}).
   */
  public void drawString (Graphics g, String text, int x, int y, int size, int flags) {
    if (text == null) return; // since 3.1.1 for BUG 2500
    int style = -1;
    if ((flags&BOLD)!=0) style = Font.BOLD;
    Font f = newFont (font,"",style,size);
    if ((flags&CENTER)!=0) {
      fm = panel.getFontMetrics(f);
      x -= fm.stringWidth(text)/2;
    }
    if ((flags&HILIT)!=0) g.setColor(theme.cwfh);
    if ((flags&LOLIT)!=0) g.setColor(theme.cwfh.darker());
    if (f!=font) g.setFont(f);
    g.drawString(text,x,y);
    if (f!=font) g.setFont(font);
    if ((flags&(HILIT|LOLIT))!=0) g.setColor(theme.cfg);
  }

  /** Clears the window.
   *  @deprecated in 3.9.0 to fix graphical timing issues
   */
  @Deprecated
  public void clear () {
    clear(g);
  }

  /** Clears the window.
   *  @param g the Graphics object used for painting
   */
  public void clear (Graphics g) {
    if (g==null) return;
    Color c = g.getColor();
    g.setColor(theme.cbg);
    g.fillRect (0,0,pos.w,pos.h);
    g.setColor(c);
  }

  /** Sets the text of the title bar of the top-most container if it is a
      {@link MidasFrame} and {@link Frame}; ignored otherwise or if {@link #getNoAltTitle()}. <br>
      @param text the text to be displayed in the title bar.
   */
  public void setAltTitle (String text) {
    if (getNoAltTitle()) return; // if true, updates to title not allowed (Added in 3.3.0 for Bug 2681)
    Component p=panel;
    while (p instanceof Component) {
      MWindow mw = null;
//      if (p instanceof MidasFrame) mw = ((MidasFrame)p).getMWindow();
//      // since 3.9.2 need to check for inc.MidasFrame for backwards compatibility jdk 11
//      else if (p instanceof nxm.sys.inc.MidasFrame) mw = ((nxm.sys.inc.MidasFrame)p).getMWindow();
      if (MidasFrame.isMidasFrame(p)) { // Updated in 3.9.2 to use method that will check if it is inc.MidasFrame or libg.MidasFrame using reflection for jdk 11 modularity
        mw = MidasFrame.getMidasFrameMWindowViaReflection(p);
      }
      if (mw != null && p instanceof Frame) {
        if (mw.getNoAltTitle()) return; // if true, updates to title not allowed (Added in 3.3.0 for Bug 2681)
        if (text==null) text = mw.title;
        if (mw.mh instanceof nxm.sys.prim.panel) ((nxm.sys.prim.panel)mw.mh).setAltTitle(text);
        else ((Frame)p).setTitle(text);
        return;
      }
      p = p.getParent();
    }
  }

  /** Get whether or not to allow mouse or other readout updates on the title bar
      @return true if allowing temporary changes (e.g. mouse hovers) to the Title of the frame
      @since NeXtMidas 3.3.0
   */
  public boolean getNoAltTitle () { // added for Bug 2681
    return noAltTitle;
  }

  /** Set whether or not to allow mouse or other readout updates on the title bar
      @param val true if allowing temporary changes (e.g. mouse hovers) to the Title of the frame
      @since NeXtMidas 3.3.0
   */
  public void setNoAltTitle (boolean val) {    // added for Bug 2681
    if (noAltTitle==val) return;   // nothing to do
    noAltTitle = val;
    setTitle(title);
  }

  /** Show text in the status bar of the browser.
      @param text the text to be displayed in the status bar.
   */
  public void setStatusBar (String text) {
    Component p=panel;
    while (p instanceof Component) {
      p = p.getParent();
    }
   return;
  }

  /** Get the saved window title
      @return saved window title
      @since NeXtMidas 3.1.0
   */
  public String getTitle () { return title; }

  /** Set the window title.
      @param title the new title.
   */
  public void setTitle (String title) {
    this.title = title;
    if (parent instanceof Frame) ((Frame)parent).setTitle(title);
    else if (status==FRONT) panel.repaint();
  }

  /** Set the Font size. The graphic context's font will be set to the Font that
      correspond to the specified font size in {@link #setGraphics(Graphics)}.
      @param size the new Font size.
   */
  public void setFontSize (int size) {
    if (size<1) return;
    font = newFont(font,"",-1,size);
    // NTN for Bug 1159: Should not call panel.setFont() outside AWT/Swing thread otherwise
    //                   it can cause deadlock with the AWT/Swing thread.
    // if (panel!=null) panel.setFont(font);
    // if (g!=null) g.setFont(font); // this is always set in paint(Graphics)
    setup();
  }

  /** Returns a new derived font based on input f1 font and specified font parameters. This differs
      from AWT new font in that it checks for the window being in headless mode aborts returning f1
      (in AWT this would cause an Exception for when creating a new Font in headless mode.)
      @param f1    The default Font.
      @param name  The name of the new Font. (null or "" uses value from f1)
      @param style The new Font style. (-1 uses value from f1)
      @param size  The new Font size. (-1 uses value from f1)
      @return the new Font for this window
   */
  public Font newFont (Font f1, String name, int style, int size) {
    if (headless) return f1;
    if (f1 != null) {
      if (name == null || name.equals("")) name = f1.getName();
      if (style == -1) style = f1.getStyle();
      if (size == -1) size = f1.getSize();
    }
    Font f2 = new Font (name,style,size);
    return f2;
  }

  /** Returns the bounding rectangle of the current clipping area. This is similar to
      {@link Graphics#getClipBounds()}.
      @return clip bounds
      @since NeXtMidas 2.5.0
      @deprecated in 3.9.0 to fix graphical timing issues
   */
  @Deprecated
  public Rectangle getClipBounds() { return clip; }

  /** Set clip bounds.
      @param clip bounds
      @since NeXtMidas 2.5.0
      @deprecated in 3.9.0 to fix graphical timing issues
   */
  @Deprecated
  public void setClipBounds(Rectangle clip) { this.clip = clip; }

  /** Refresh this window.  <br>
      This method should be overridden by any subclasses of {@link MWindow} to
      perform any actions required to refresh the window.
   */
  public void refresh () {
    if (status==FRONT && panel!=null) panel.repaint();
  }

  /** Refresh area defined in box
   *  @param box MBox defining area to be repainted
   */
  public void refresh (MBox box) {
    if (status==FRONT && panel!=null) {
      if (box != null) {
        panel.repaint(box.x,box.y,box.w,box.h);
      }
      else {
        panel.repaint();
      }
    }
  }

  /** Handle resize of this window.
      @param mode the resize mode &gt;0 to call setBounds()
   */
  public void resize (int mode) {
//    if (MW!=null) place (pos,MW.pos);
//    if (mode>0) setBounds (pos);
  }

  /** Paint this component
   *  (MWindow version is not typically called, since it is not a
   *   component and the sub-classes handle most painting)
   *   @param g the Graphics object used for painting
   */
  @ProvisionalUseOnly("Treat this as protected and should only be called by subclass paintComponent methods")
  public void paintComponent (Graphics g) {
    if (g==null) return;
    g.setFont(font);
    setGraphics(g);  // keep until methods deprecated in 3.8.1 are removed
  }

  /** Set the graphics context with the specified Graphics. Also set the font
      and gets the clip bounds (see {@link #getClipBounds()}).
      @param g graphics context
      @since NeXtMidas 2.5.0
   */
  public void setGraphics (Graphics g) { // FYI: this is only used by MPlot and panel
    this.g = g;
    g.setFont(font);
    clip = g.getClipBounds();
  }

  /** Refresh this window.  <br>
      This method should be overridden by any subclasses of {@link MWindow} to
      perform any actions required to refresh the window.
      @deprecated Since NeXtMidas 3.9.0, use paintComponent(...)
   */
  @Deprecated
  public void paint () {
    // The MWindow check here gets around the circular logic between paint() and paintComponent(..).
//    refresh();
  }

  /** Place box A in box B centered at (x,y) if possible.
   *  @param a MBox A
   *  @param b MBox B
   *  @param x center x coordinate
   *  @param y center y coordinate
   */
  public void place (MBox a, MBox b, int x, int y) {
    a.x = x-a.w/2;
    a.y = y-a.h/2;
    place (a,b);
  }

  /** Place box A in box B.
   *  @param a MBox A
   *  @param b MBox B
   */
  public void place (MBox a, MBox b) {
    a.x = Math.max(0,Math.min(a.x,b.w-a.w));
    a.y = Math.max(0,Math.min(a.y,b.h-a.h));
  }

  /** The keys for this object as a {@link Keyable} object. */
  private static String[] keys = {"POS","PARENT","THEME","TITLE"};

  @Override
  public String[] getKeys() { return keys; }

  @Override
  public Object setKey (String key, Object value) {
         if (key.equals("POS")) setBounds((MBox)value);
    else if (key.equals("PARENT")) addTo(value);
    else if (key.equals("THEME")) setTheme(value);
    else if (key.equals("TITLE")) setTitle((String)value);
    else if (key.equals("REFRESH")) process("REFRESH",0);
    else return null;
    return value;
  }

  @Override
  public Object getKey (String key) {
    if (key.equals("POS")) return pos;
    if (key.equals("PARENT")) return parent;
    if (key.equals("THEME")) return theme;
    if (key.equals("TITLE")) return title;
    return null;
  }

  /** Gets the status of the window.
      @return The status ID.
   */
  public int getStatus() {
    return this.status;
  }

  /** Gets the panel that this MWindow is displayed in. Note that the return value of this method
      may change after the window has come up if the window changes display settings (e.g. it is
      moved to another monitor).
      @return The panel that this window is displayed in.
      @since NeXtMidas 1.9.4
   */
  public Container getPanel() { return panel; }

  /** Quickly checks to see if a panel is visible for the purposes of this {@link MWindow}. The
      basic implementation checks to see that the panel is not null as well as verifying that both
      {@link Container#isVisible()} and {@link Container#isShowing()} are true. Subclasses may
      override this with additional checks (such as making sure that is it is at least 1 pixel
      high and 1 pixel wide). For more detailed information, the {@link #getPanel()} can be used
      and then the panel accessed directly.
      @return true if it is visible and false if not.
   */
  public boolean isPanelVisible () {
    return (panel != null) && panel.isVisible() && panel.isShowing();
  }

  /** Sets the mouse cursor when over the panel to a given cursor type. This version will reset the
      cursor after a given number of seconds have elapsed.
      @param name The name of the cursor to set (see {@link #cursorList}), can not be "CUSTOM".
      @param sec  The number of seconds to wait.
   */
  public void setCursor (String name, final double sec) {
    final String oldCursor = getCursor();
    setCursor(name);
    cursorTimer = new javax.swing.Timer( (int)(1000*sec), new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) { setCursor(oldCursor); }
                  });
    cursorTimer.start();
  }

  /** Sets the mouse cursor when over the panel to a given cursor type.
      @param name The name of the cursor to set (see {@link #cursorList}), can not be "CUSTOM".
   */
  public void setCursor (String name) {
    if (isCursor(name)) return;   // nothing to do - added in 2.9.0 (Bug 2249); since NeXtMidas 3.7.2
    if (cursorTimer != null) {
      cursorTimer.stop();
      cursorTimer = null;
    }
    int type = Parser.find(cursorList, name, Cursor.DEFAULT_CURSOR, -2);

    if (type == Cursor.CUSTOM_CURSOR) {
      Shell.getSharedMidasContext().warning("MWindow: Can not set cursor to "+name+", using DEFAULT.");
      type = Cursor.DEFAULT_CURSOR;
    }

    // for Bug 1159: Only set panel's cursor if it changed and in the AWT/Swing thread
    final Cursor cursor = Cursor.getPredefinedCursor(type);
    if (!cursor.equals(this.cursor))  {
      this.cursor = cursor; // save the current cursor
      if (panel!=null) {
        if (EventQueue.isDispatchThread()) panel.setCursor(cursor);
        else {
          // schedule setting the panel's cursor on the AWT/Swing thread.
          // Since 3.9.0 NXM-3213
          invokeLater(() -> {
              if (panel!=null) panel.setCursor(cursor);
          });
        }
      }
    }
  }

  /** Return true if cursor is set correctly.
   *  @param name The name of the cursor (see {@link #cursorList}).
   *  @return true if cursor is set correctly
   *  @since NeXtMidas 3.7.2
  */
  public boolean isCursor (String name) {
    int type = Parser.find(cursorList, name, Cursor.DEFAULT_CURSOR, -2);
    if (type == Cursor.CUSTOM_CURSOR) {
      Shell.getSharedMidasContext().warning("MWindow: Can not set cursor to "+name+", using DEFAULT.");
      type = Cursor.DEFAULT_CURSOR;
    }

    // for Bug 1159: Only set panel's cursor if it changed and in the AWT/Swing thread
    final Cursor cursor = Cursor.getPredefinedCursor(type);
    return cursor.equals(this.cursor); //true;
  }

  /** Gets the current mouse cursor.
      @return The current mouse cursor, may be "CUSTOM".
   */
  public String getCursor() {
    Container container = panel;
    if (container == null) return "Default"; // since 3.1.1 to prevent possible NPE for BUG 2556
    return Parser.get(cursorList, container.getCursor().getType(), -2);
  }

  protected void helpKeyPressed() {
    MWindow mw = this;
    // get to the root MWindow until pointer is active
    while (!mw.pactive && mw.MW!=null && mw.MW!=mw) mw = mw.MW;
    Midas M = Convert.ref2Midas(mw.mh);
    // find the child where the pointer is active
    if (!mw.pactive) mw = findActiveChild(mw.panel);
    if (mw!=null && M!=null && M.macro!=null) {
      Table data = mw.getHelpData();
      M.macro.processMessage(new Message("HELP",0,data));
    } else {
      Shell.warning("No handler for Help key on "+mw);
    }
  }

  /** return a table of keys indicating the active item for help */
  protected Table getHelpData() {
    Table data = new Table();
    String name = this.name;
    if (mh instanceof Command) name = ((Command)mh).getID();
    data.put("NAME",name);
    return data;
  }

  /** recursively find child where the pointer is active */
  private MWindow findActiveChild (Container cont) {
    MWindow mw = null;
    Component activeComp = null; // the component under the mouse

    // First check the Layeredpane for Components, since this is the usual place
    if (cont instanceof MJPanel) {
      Component[] comps = ((MJPanel)cont).getRootPane().getLayeredPane().getComponents();
      for (int i=0; i<comps.length; i++) {
        Component c = comps[i];
        if (c.getMousePosition() != null) activeComp = c;
      }
    }

    // If not on the Layered Pane, check the container components
    if (mw == null) {
      Component[] comps = cont.getComponents();
      for (int i=0; i<comps.length; i++) {
        Component c = comps[i];
        if (c.getMousePosition() != null) activeComp = c;
      }
    }

    // get the MWindow for the active component
    if (activeComp != null) {
      if (activeComp instanceof MJPanel)        mw = ((MJPanel)activeComp).MW;
      else if (activeComp instanceof MPanel)    mw = ((MPanel)activeComp).MW;
      else if (activeComp instanceof MJText)    mw = ((MJText)activeComp).MW;
      else if (activeComp instanceof Container) mw = findActiveChild((Container)activeComp);
    }

    return mw;
  }

  /** Exports the a copy of this window to an image.
      @return An {@link ExportImage} that can be saved to disk.
      @since NeXtMidas 2.5.5
   */
  public ExportImage exportImage () {
    if (panel == null) throw new MidasException("MWindow: Can not export image, panel is null.");
    return ExportImage.export(panel);
  }

  /** set the warnings mask
      @param  warnings warnings mask (String)
      @return true if warnings mask was changed
      @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static boolean setWarningsMask (String warnings) {
    return setWarningsMask(Parser.mask(warningsList,warnings,warningsMask));
  }

  /** set the warnings mask
     @param  mask warnings mask (int)
     @return true if warnings mask was changed
     @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static boolean setWarningsMask (int mask) {
    if (warningsMask == mask) return false;
    warningsMask = mask;
    return true;
  }

  /** Return a list of the MWindow (and sub-class) warnings that are active.
   *  @return Pipe separated list of the MWindow (and sub-class) warnings that are active.
   *  @since NeXtMidas 3.8.1
   */
  @InternalUseOnly
  public static String getWarningsMask() {
    return Parser.mask2s(warningsList, warningsMask);
  }
}
