package nxm.sys.prim;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Window;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;
import java.awt.Image;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.swing.AbstractButton;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.JRootPane;

import nxm.sys.inc.Filter;
import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.Widget;
import nxm.sys.lib.Args;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Macro;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.Util;
import nxm.sys.libg.GAlert;
import nxm.sys.libg.GLabel;
import nxm.sys.libg.GMenu;
import nxm.sys.libg.GPanel;
import nxm.sys.libg.GPanelSetup;
import nxm.sys.libg.GPrimitive;
import nxm.sys.libg.GPrompt;
import nxm.sys.libg.GQuery;
import nxm.sys.libg.GText;
import nxm.sys.libg.GWidget;
import nxm.sys.libg.MBox;
import nxm.sys.libg.MPanel;
import nxm.sys.libg.MWindow;
import nxm.sys.libg.MidasControls;
import nxm.sys.libg.MidasDisplay;
import nxm.sys.libg.PushPopPanel;
import nxm.sys.libg.Theme;
import nxm.sys.libg.WidgetPanel;
import nxm.sys.libg.WidgetWrapper;

/**
  Container for other plots and/or widgets.

  @author Jeff Schoen
*/
public class panel extends GPrimitive implements WidgetPanel, Filter {

  private static final int sbw=4;  // Size of the border slider line (in pixels)
  private static final int bw=3;   // Border width (in pixels)
  private        final int maxLine=69;
  private        final int maxPane=65;
  private        final int maxWidget=256;

  private int exit=-1, filterMode=0;
  private int nline=0, npane=0, nwidget=0;
  private int iwz0=0, iwz1=0, iwz2=0;   // left/right widget group current indices
  private int dlh=50;                   // default logger height
  private int cfgx,cfgy,configure,options,eventFilter,dragline;
  private int pushPopIconSymbol;  // Since NeXtMidas 3.5.4 for Bug 2895
  private int pushPopIconActions;  // Since NeXtMidas 3.5.4 for Bug 2895
  private int customIconSymbol; // Since NeXtMidas 3.5.4 for Bug 2895
  private int customIconActions; // Since NeXtMidas 3.5.4 for Bug 2895
  private Line[] line = new Line[maxLine];
  private Pane[] pane = new Pane[maxPane];
  private GWidget[] widget = new GWidget[maxWidget];
  private String cfgfile,regname,titlebar;
  private double lastPollTime;
  private int nTimers = 0;
  private double[] timer = null;
  private double[] timerinit = null;
  private Table cpos = new Table();
  private boolean inline,inlinx,init=true,needLayout,headless;
  private int gnx,gny;
  private Object wmsgid;
  private GLabel curGLabel,lastCollapsableGLabel,lastOpenedGLabel;
  /**
   *  Panel / bbox areas
   *  ----------------------------------------------------
   *  |                      |       bbox[3]         |    |
   *  |                      |-----------------------|    |
   *  |                      |                       |    |
   *  |                      |                       |    |
   *  |                      |                       | b  |
   *  |                      |                       | b  |
   *  |                      |                       | o  |
   *  |      bbox[1]         |       bbox[0]         | x  |
   *  |                      |                       | [  |
   *  |                      |                       | 2  |
   *  |                      |                       | ]  |
   *  |                      |                       |    |
   *  |                      |                       |    |
   *  |                      |-----------------------|    |
   *  |                      |       bbox[4]         |    |
   *  ----------------------------------------------------
   */
  private MBox[] bbox = new MBox[5];
  private boolean logger;
  private boolean addNewLoggerPane;
  //Menu Bar fields: hbar (menu bar height), xbar (x coordinate for next GLabel menu), and ytop (adjusted top of panel area) are for NXM-3313
  private int nleft,yleft,hbar=0,xbar,ytop,ybott,iScroll,oScroll,vScroll,cursorDir;
  private int needCompress, prevCompress, curCompress, maxCompress=10;
  private String  userCursor  = null; // The cursor set by the user, restored after line drag.
  private Table   xmCanvasTbl = null; // Table of XmCanvas elements that need to be configured.
  private String  altTitle;
  private String  currentTitle;
  private int     mobile,mpad=640;
  private Robot   robot;
  private boolean tsetup         = false;  //  /tsetup is present (with or without a Table)
  private Table   setupTable     = null;   //  setup Table
  private boolean parentResult   = false;  //  the setup table is a parent result variable
  private boolean localResult    = false;  //  the setup table is a local result variable
  private boolean holdLayout     = false;  //  hold layout refreshes (Bug 2214)
  private boolean hideWidget     = false;  //  hidden widgets (unsupported on layer) are kept by default
  private boolean menuBarEnabled = false;  //  create menu bar when Pane 0 is collapsed - since 3.9.0 for NXM-3321
  private boolean fixByOffset    = false;  //  use X/Y offset instead of fractional widget positions
  private int     widthLabelInMenuBar = 0; //  width saved off from Pane 0 width is used for width of labels in menu bar

  /** use {@link #getPanes()}. */
  private Table panes = new Table();
  /** use {@link #getControls()}. */
  private Table cntrls;

  private static final int EXIT_RETURN=0x1, EXIT_MENU=0x2,
          EXIT_MESSAGE=0x4, EXIT_WINDOW=0x8, EXIT_ALL=0xF;
  private static final String exitList = "RETURN,MENU,MESSAGE,WINDOW";

  private static final int ROTATETABS   = 0x01; // Option: Tabs Rotate.
  private static final int PUSHPOPICON  = 0x02; // Option: Show icon for Push/Pop.
  private static final int COMPRESS     = 0x04; // Option: Compress widgets.
  private static final int COLLAPSE     = 0x08; // Option: Collapse roll-downs.
  private static final int LOCKSETUP    = 0x10; // Option: Lock setup (can't drag lines).
  private static final int LOCKCONTROLS = 0x20; // Option: Lock controls (can't drag widgets).
  private static final int MINIDRAGZONE = 0x40; // Option: Tiny drag bars.
  private static final int ADVPUSHPOP   = 0x80; // Option: Ctrl+click on 'X' to bring up pop menu (Since 3.1.0)
  private static final int HIDECONTROLS = 0x100; // Option: Hide controls (can't see or open controls border).

  private int optionsDefault = PUSHPOPICON|COMPRESS|ADVPUSHPOP; // The default options. ADVPUSHPOP added for Bug 2458 in 3.1.0
  private int optionsDefaultMnM = LOCKSETUP|LOCKCONTROLS;     // The default options for mobile.

  /** The list of options. */
  public static String optionsList = "RotateTabs,PushPopX,CompressControls,CollapseControls,"
                                   + "LockSetup,LockControls,MiniDragZone,AdvPushPopX,HideControls";

  // Add ability to revert back to the old Shaded X for a PUSHPOP icon Since NeXtMidas 3.5.4 Bug 2895
  /** Old Shaded-X PUSHPOP icon (No CUSTOM_ICON)
   *  @since NeXtMidas 3.5.4
   */
  @InternalUseOnly("Since NeXtMidas 3.5.4, this has always been meant for internal use only")
  public static final int PUSHPOP_ICON_SYMBOL_SHADEDX = 1;
  /** New PUSHPOP icon (CUSTOM_ICON is "close" X symbol (a X with shading))
   *  @since NeXtMidas 3.5.4
   */
  @InternalUseOnly("Since NeXtMidas 3.5.4, this has always been meant for internal use only")
  public static final int PUSHPOP_ICON_SYMBOL_PUSHPOP = 2;
  /** List of both available PUSHPOP icon options
   *  @since NeXtMidas 3.5.4
   */
  @InternalUseOnly("Since NeXtMidas 3.5.4, this has always been meant for internal use only")
  public static final String pushpopIconSymbolList = "ShadedX,PushPop";

  // Since NeXtMidas 3.5.4 for Bug 2895 added PUSHPOPICON Actions
  private static final int PUSHPOP_ICON_ACTION_PUSHPOP = 0x01; // PUSHPOPICON pops/pushes command to new window [DEFAULT]
  private static final int PUSHPOP_ICON_ACTION_MESSAGE = 0x02; // PUSHPOPICON sends message with command id and PANE

  /** The list of PUSHPOPICON Actions. */
  public static String pushPopIconActionList = "PushPop,Message";

  // Add ability to have second icon (in addition to push pop icon) Since NeXtMidas 3.5.4 Bug 2895
  // Symbols - only one can be chosen - Since NeXtMidas 3.5.4  Bug 2895
  /** No CUSTOM_ICON Symbol */
  public static final int CUSTOM_ICON_SYMBOL_NONE = 1;
  /** CUSTOM_ICON is "close" X symbol (a X with shading) */
  public static final int CUSTOM_ICON_SYMBOL_SHADEDX = 2;
  /** CUSTOM_ICON is whatever Symbol was specified */
  public static final int CUSTOM_ICON_SYMBOL_MIDASSYMBOL = 3;

  // Actions - can choose multiple
  private static final int CUSTOM_ICON_ACTION_CLOSE = 0x01; // CUSTOM_ICON exits command and deletes PANE
  private static final int CUSTOM_ICON_ACTION_MESSAGE = 0x02; // ICON sends message with command id and PANE

  // Custom Icon symbol and action defaults
  private int customIconSymbolDefault = CUSTOM_ICON_SYMBOL_NONE;
  private int customIconActionDefault = 0x0; // Default to no action

  private String customIconSymbolName = "NONE";

  /** The list of Custom Icon symbols where Symbol means the icon is the Symbol specified. */
  public static final String customIconSymbolList = "None,ShadedX,Symbol";
  /** The list of PUSHPOPICON Actions. */
  public static String customIconActionList = "Close,Message";

  private static final int BUTTON_SELECTED_NONE = 0;
  private static final int BUTTON_SELECTED_PUSHPOP = -1;
  private static final int BUTTON_SELECTED_CUSTOM = -2;
  private static final int BUTTON_SELECTED_OOR = -3;

  private static final int TABS_ROTATE = 0x1;            // Tab Option: Rotate tabs.
  private static final String tabOptionsList = "ROTATE"; // The list of tab options.

  private int tabOptions = TABS_ROTATE; // The tab options.

  /** Panel border options string list which corresponds with the BORDER Options. */
  public static final String bordersList = "Left,Right,Top,Bottom,TopLeft,TopRight,BottomLeft,BottomRight";
  /** Mnemonics for BORDER flag options.  Users should use names from the border list */
  private static final int BORD_LEFT=0x1, BORD_RIGHT=0x2, BORD_TOP=0x4, BORD_BOTTOM=0x8,
                           BORD_TOPLEFT=0x10, BORD_TOPRIGHT=0x20,
                           BORD_BOTTOMLEFT=0x40, BORD_BOTTOMRIGHT=0x80;

  private static final int BORDERS_DEFAULT = BORD_LEFT|BORD_RIGHT|BORD_TOP|BORD_BOTTOM;
  private int borders=BORDERS_DEFAULT;
  private static final int MENUBAR_BORDERS_DEFAULT = BORDERS_DEFAULT&~BORD_LEFT;

  private static final int VLINE=1, HLINE=2, VLINEX=-1, HLINEX=-2;

  private static final int SETUP  = 1;
  private static final int TSETUP = 2;

  /** The pane/parent to place any controls on. If this is null, then MW is used.  */
  private MidasControls ctlPane = null;

  /** The top-level frame/panel being used if /JSETUP is used and the panel is a Container.  */
  private Container container = null;

  /** panel(s) of Macro that may be added to parent Macro */
  private Table  subPanels;
  /** name of the subPanel. This must match what is in the parent setup file */
  private String subPanelName;

  private boolean shareTheme;  // whether or not to use the Global theme
  private Theme theme;         // the theme to use  - since 3.3.0 Panel gets it's own theme reference

  /** default constructor */
  public panel () {}

  @Override
  public int open () {
    String panelClass = MA.getCS("/JSETUP", null, "");  // null if not present, "" for state switch
    titlebar = "Panel";
    cfgfile = MA.getS("FILE");
    tsetup = MA.find("/TSETUP");
    if (tsetup)  setupTable = configTSetup();
    if (MA.find("/SETUP") ) setConfigFileName(SETUP);
    setTimers (MA.getL("/NTIMERS",3));
    regname = MA.getU("/PANES","WIN");
    String rname = MA.getU("/WMSGID");
    if (rname.length()>0) wmsgid = M.registry.getCommand(rname,5);
    if (M.macro==null) cntrls = new Table();
    else cntrls = M.macro.getControls();
    if (MA.find("/CONTROLS")) MR.put(MA.getS("/CONTROLS","GC"),cntrls);
    if (M.pipeMode==Midas.PINIT) exit=EXIT_WINDOW; else exit=EXIT_ALL;
    exit  = MA.getOptionMask("/EXIT",exitList,exit);
    if (MA.find("/TABS")) M.deprecate("The /TABS= list is now in the /OPTIONS= list");
    tabOptions = MA.getOptionMask("/TABS",tabOptionsList,tabOptions);
    if ((tabOptions&TABS_ROTATE)==0) optionsDefault &= ~ROTATETABS;
    headless = MA.getState("/HEADLESS", Shell.isHeadless());
    fixByOffset = MA.getState("/FIXBYOFF");
    // BEGIN BUG 2817 CHANGES since 3.5.0
//    boolean headlessF = MA.getState("/HEADLESS", false); // This allows us to determine if the headless tag was specified.
//    if (headless && !headlessF && !Theme.getOption(Theme.HEADLESS)) {
//        String warningText = "/HEADLESS not set on a headless system. System may appear to hang due to no display.";
//        M.warning(warningText); // Send warning to the console.
//        HeadlessException headlessEx = new HeadlessException(warningText);
//        if (M.macro != null) { // Send message to the macro so it can act on this warning.
//          M.macro.processMessage(new Message("ERROR", 0, headlessEx, M.macro, this));
//        }
//    }                           This code moved to MWindow to better catch other graphical primitives
    // END BUG 2817 CHANGES
    if (!headless && Shell.isHeadless()) // error when /HEADLESS=FALSE if the shell is headless
      M.error("PANEL: Can not set '/HEADLESS=" + MA.getS("/HEADLESS") + "' on a headless system.");
    inline = MA.getState("/INLINE",true);
    inlinx = MA.getState("/INLINX",false);
    logger = MA.getState("/LOGGER");
    String themeName = MR.getS("ENV.THEME");
    Object themeObj = MA.getO("/THEME",themeName);
     // BEGIN BUG 2618  OPEN() CHANGES since 3.3.0
    shareTheme = MA.getState("/SHARETHEME");  // for backwards compatibility
    this.theme = shareTheme ? (nxm.sys.libg.Theme) nxm.sys.lib.Theme.global() : (nxm.sys.libg.Theme) nxm.sys.lib.Theme.global().copy(); // Casting is fine here because if libg.Theme is present, then the global instance is libg.theme since 3.9.2
    if (themeObj!=null && themeObj instanceof Theme) this.theme = (Theme) themeObj;
    // END BUG 2618 OPEN() CHANGES
    boolean mnm = (themeObj != null) && themeObj.toString().equals("MNM");
    options = MA.getOptionMask("/OPTIONS",optionsList,mnm?optionsDefaultMnM:optionsDefault);
    MW = new GPanelSetup(titlebar,this); // GPanelSetup is the wrapper class that will fix the graphics timing issue since 3.9.0 NXM-3213
    hideWidget = MA.getState("/HIDEWIDGET",false);
    menuBarEnabled = MA.getState("/MENUBAR",false); // switch to put it into Menu Bar mode - since 3.9.0 for NXM-3321

    // Allow ability to revert back to old Shaded X for PUSHPOP icon with System Property
    if (Installation.ApplyBugFix.PANEL_REVERT_TO_SHADEDX_PUSHPOP_ICON) {
      pushPopIconSymbol = PUSHPOP_ICON_SYMBOL_SHADEDX;
    }
    else {
      // Allow ability to revert back to old Shaded X for PUSHPOP icon with SWITCH
      pushPopIconSymbol = MA.getChoice("/PUSHPOPICONSYMBOL", pushpopIconSymbolList, PUSHPOP_ICON_SYMBOL_PUSHPOP);
    }

    // Allow different Actions for the PUSHPOP Icon - Since NeXtMidas 3.5.4 Bug 2895
    pushPopIconActions = MA.getOptionMask("/PUSHPOPICONACTION",pushPopIconActionList,PUSHPOP_ICON_ACTION_PUSHPOP);

    // Allow a second Icon (ex: Close Icon)  Since NeXtMidas 3.5.4 Bug 2895
    String customIconString = MA.getCS("/CUSTOMICONSYMBOL", "None", "ShadedX");
    // Check to see if the symbol is in the custom icon symbol list
    customIconSymbol = Parser.find (customIconSymbolList,customIconString,CUSTOM_ICON_SYMBOL_NONE,0);
    // If the icon is not on list, assume this is the name of a NeXtMidas Symbol which we will use as an icon
    if (customIconSymbol == -1) {
      customIconSymbol = CUSTOM_ICON_SYMBOL_MIDASSYMBOL;
      customIconSymbolName = customIconString;
    }
    if (customIconSymbol == CUSTOM_ICON_SYMBOL_NONE) {
      customIconActions = customIconActionDefault;  // default to no action
    }
    // default action is close if symbol of Shaded "X" is chosen
    else if (customIconSymbol == CUSTOM_ICON_SYMBOL_SHADEDX) {
      customIconActions = MA.getOptionMask("/CUSTOMICONACTION",customIconActionList,CUSTOM_ICON_ACTION_CLOSE);
    }
    else {
      customIconActions = MA.getOptionMask("/CUSTOMICONACTION",customIconActionList,customIconActionDefault);
    }

    panes.put("PANE0",MW);

    if (panelClass == null) {
      if (!headless) MW.open();
      useDefaultSetup();
      addNewLoggerPane = logger;
      if (mnm) {
        Theme.setOptions("+MOBILE");
        mobile = -1;
        load("nxm.sys.mcr.mnm.mmp");
        mobile = 1;
        try { robot = new Robot(); }
        catch (Exception e) { M.printStackTrace("Unable to instantiate Robot class for event generation ",e); }
      }
      if (setupTable != null ) {
        try { setSetupTable(setupTable); }
        catch (Exception e) { M.printStackTrace("Error loading setup from table "+setupTable,e); useDefaultSetup(); }
      }
      else if (cfgfile.length()>0 && !tsetup) {
        try { load(cfgfile); }
        catch (Exception e) { M.printStackTrace("Error loading setup from mmp file "+cfgfile,e); useDefaultSetup(); }
      }
      // only add logger panel if not defined in setup file and /logger is present  (since 3.1.3 - Bug 2623)
      addNewLoggerPane = logger && addNewLoggerPane;
    }
    else {
      int width = 150;
      loadPanelClass(panelClass);
      if (ctlPane != null) {
        width = ctlPane.getWidth();
        if (!headless) MW.open();
      }
      for (int i=0; i<=4; i++) bbox[i] = new MBox();
      nline=4;
      for (int i=0; i<=4; i++) line[i]=new Line();
      setLine(0,HLINE,0,0.0F,0.0F,0,0);     // dummy
      setLine(1,VLINE,width,0.0F,1.0F,0,0); // left edge
      pane[0] = new Pane(this, "PANE0");
    }
    if (addNewLoggerPane && logger) addPane (-1, "LOGGER", "Message Logger", "LOGGER", 12, 1,2,4,0, 0);

    Table t = MA.getTable("/GRID");
    if (t!=null) autoGrid(t);

    if (headless) {
      init=false;
    } else {
      if (container == null) {                // No /JSETUP=
        MW.addTo(this);
      }
      else if (container instanceof Frame) {  // /JSETUP= with a Frame
        if (ctlPane != null) {
          MW.addTo(this); // <-- needed for the pane0 init
        }
      }
      else {                                  // /JSETUP= with a Panel
        MW.addTo(this); // <-- may change MW.parent

        if (MW.parent instanceof Container) {
          // JC4: If we don't do this, we get into situations where the JPanel
          //      specified via /JSETUP= does not show up. This is because it
          //      is nominally given a size of [0,0]
          //      even if the preferred size of the JPanel is set. This in
          //      its self doesn't do the resizing, the .pack() call (below)
          //      does the rest
          Container _parent = (Container)MW.parent;
          _parent.remove(container);
          _parent.setLayout(new BorderLayout());
          _parent.add(container, BorderLayout.CENTER);

          if (_parent instanceof Window) {
            // JC4: Added the frame.pack() call in Sept 2012 -- Without this, JOGL
            //      components on a /JSETUP= panel can get an exception when trying
            //      to access the "packer" in com.sun.opengl.util.j2d.TextRenderer's
            //      getBackingStore(..) following a user resizing the window. The
            //      best example of this is when using WorldWind with an
            //      AbstractTacticalSymbol visible.
            ((Window)_parent).pack();
          }
        }
      }
      MW.setTheme(themeObj,inline);
      this.theme = MW.theme;  // make sure our Theme reference is correct - added in 3.3.0
      resize();
      refresh();
    }
    M.registry.put(regname,panes);

    if (logger) Shell.runCommand(M,"MSGLOG/ID=LOGGER/BANNER=PANE/WRAP="+mnm);

    return NORMAL;
  }

  @Override
  public int process () {
    try {
      if (xmCanvasTbl != null) {
        // Any X-Midas displays need to be pulled into the panel after M$SYNC has been called (i.e.
        // at the top of process()). If not running from X-Midas, xmCanvasTbl will be null.
        String keys[] = xmCanvasTbl.getKeys();
        for (int i = 0; i < keys.length; i++) {
          KeyObject.setKey(xmCanvasTbl.get(keys[i]), "XS", keys[i].substring(2));
        }
        xmCanvasTbl = null;
      }

      if (init && M.pipeMode!=Midas.PINIT) {
        if (curGLabel!=null)  maxCompress=curGLabel.pos.h/4;
        curGLabel=null;
        updateLastWidget();
        init=false;
        if (menuBarEnabled) setBorders(borders&~BORD_LEFT); // expand left over Pane 0 if Menu Bar enabled - since 3.9.0
      }

      // wait for pipe system to be running
      if (init) return NOOP;

      double time = Time.current();
      // If timers set, send timer message with timer number in the info field
      for (int i=0; i<nTimers; i++) {
        double nthTimer = timer[i];
        if (nthTimer != 0) {
          if ((time-timerinit[i]) > Math.abs(nthTimer)) {
            MQ.put("TIMER", i, Double.valueOf(time), "MAIN", this);
            if (nthTimer>0) timerinit[i]=time;
            else timer[i]=0;
          }
        }
      }

      for (int i=1; i<=nwidget; i++) {
        GWidget gWidget = widget[i];
        if (gWidget != null) gWidget.poll();  // added check for null in 3.3.1 for Bug 2715
      }

      if (needLayout) layout();
      if (needCompress!=0 || prevCompress!=0) {
        curCompress += needCompress;
        layout();
      }
      else if (is(COLLAPSE) && lastOpenedGLabel!=null) {
        lastOpenedGLabel.setActive(GLabel.aCOLLAPSABLE);
        lastOpenedGLabel = null;
      }
      if (time-lastPollTime>0.3) {
        lastPollTime = time;
        refreshTitle();
      }
    } catch (Exception e) {
      // Bug 1366 - PANEL should never crash
      M.printStackTrace(Thread.currentThread().toString(), e);
    }
    return NOOP;
  }

  @Override
  public int close () {
    int to=10; // wait for primitives writing to panes to go away
    for (Table.Iterator ti=panes.iterator(); ti.getNext(); ) {
      while (to>0 && M.registry.get(ti.key)!=null) { to--; Time.sleep(0.1); }
    }
    if (mobile!=0) Theme.setOptions("-MOBILE");
    if (MW != null) { // Since 3.5.0. Skip if in Headless state. BUG 2817
      MW.theme.setInline(false);
      MW.close();
    }
    M.registry.remove(regname);

    if (container != null) {
      container.setVisible(false);
      if (Shell.needRemoveNotifyToAvoidJVMHang()) { // NXM-3986
        container.removeNotify();
      }
    }

    return NORMAL;
  }

  /** Loads the default setup for the panel. */
  private void useDefaultSetup () {
    for (int i=0; i<=4; i++) bbox[i]=new MBox();
    nline=4; for (int i=0; i<=4; i++) line[i]=new Line();
    setLine(0,HLINE,0,0.0F,0.0F,0,0);   // dummy
    setLine(1,VLINE,150,0.0F,0.5F,0,0); // left edge
    setLine(2,VLINE,50,1.0F,0.5F,0,0);  // right edge
    setLine(3,HLINE,50,0.5F,0.0F,1,2);  // top edge
    setLine(4,HLINE,50,0.5F,1.0F,1,2);  // bottom edge
    pane[0] = new Pane(this, "PANE0");
  }

  @Override
  public Container getGraphicsContainer () {
    // override to return the /JSETUP frame as needed
    return (container != null)? container: super.getGraphicsContainer();
  }

  @Override
  public void checkout () {
    for (int i=0; i < widget.length; i++) {
      GWidget gWidget = widget[i];
      if (gWidget != null) gWidget.close();
    }
    super.checkout();
  }

  private boolean is (int mask) {
    return (options&mask) != 0;
  }

  /** Midas Panel is repainted */
  public void refresh () {
    if (MQ != null) MQ.remove("REFRESH");
    if ((MW != null) && (MW.panel != null)) MW.panel.repaint();
  }

  /** A more comprehensive refresh than {@link #refresh()}. This method will call
      repaint on each widget and pane's graphics panel and then call {@link #refresh()}
      @since NeXtMidas 3.3.0
   */
  public void refreshAll () {
    if (!isHideWidgets()) {
      for (int i=0; i<widget.length; i++) {
        GWidget gWidget = widget[i];
        if (gWidget != null && gWidget.panel != null) gWidget.panel.repaint();
        if (gWidget instanceof GText) {
          ((GText)gWidget).getMJTextArea().setAttributesGW((GText)gWidget);   // make sure Gtext text area colors get updated
        }
      }
      for (int i=0; i<pane.length; i++) {
        if (pane[i]!=null && pane[i].panel!=null) pane[i].panel.repaint();
      }
    }
    refresh();
  }

  /** Paint this component based on both panel graphics and current graphics object ???
   *  @param gp the GPanelSetup containing information on the panel
   *  @param g  the current Graphics object used for painting
   */
  @InternalUseOnly("Only GPanelSetup should use this")
  public void paintComponent (GPanelSetup gp, Graphics g) {
    if (headless) return;
    MW.setGraphics(g);// kept only for semblance of backwards compatibility - to be removed
    // paneRefresh added in 3.3.1 to allow for ALL panes to be repainted, not just the first one (Bug 2707)
    boolean paneRefresh = false;
    for (int i=1; i<=npane; i++) {
      Pane p = pane[i];  // just a pane refresh ?
      if (p!=null && p.pos.equals(g.getClipBounds())) { p.paint(gp, g); paneRefresh=true; }
    }
    if (paneRefresh) return;
    MW.clear(g); // Not in J. Czechowski change, 3.9.0
    for (int i=1; i<=nline; i++) {
      Line l=line[i];
      if (i<=4 && l.fp==0 && (borders&(1<<(i-1)))!=0) continue;
      gp.shadowbox(g,l.x1, l.y1, l.x2-l.x1, l.y2-l.y1, MWindow.SB_SEPERATOR);
      int xm = (l.x1+l.x2)/2;
      int ym = (l.y1+l.y2)/2;
      g.setColor(MW.theme.cwts);
      if (i==1 || i==2) {
        g.drawLine (l.x1+bw,l.y1+1*sbw,l.x2-bw,l.y1+2*sbw);
        g.drawLine (l.x2-bw,l.y1+2*sbw,l.x1+bw,l.y1+3*sbw);
        g.drawLine (l.x1+bw,l.y1+3*sbw,l.x2-bw,l.y1+4*sbw);
      }
      else if (i==3 || i==4) {
        g.drawLine (l.x1+1*sbw,l.y1+bw,l.x1+2*sbw,l.y2-bw);
        g.drawLine (l.x1+2*sbw,l.y2-bw,l.x1+3*sbw,l.y1+bw);
        g.drawLine (l.x1+3*sbw,l.y1+bw,l.x1+4*sbw,l.y2-bw);
      }
      else if (is(MINIDRAGZONE) && !is(LOCKSETUP)) {
        if (l.dir==HLINE) g.drawLine (xm-sbw,ym,xm+sbw,ym);
        if (l.dir==VLINE) g.drawLine (xm,ym-sbw,xm,ym+sbw);
      }
      if (configure>0) {
        gp.shadowbox(g,l.x1, l.y1, l.x2-l.x1, l.y2-l.y1, MWindow.SB_ALERT);
      }
    }
    for (int i=1; i<=npane; i++) {
      if (pane[i]!=null) pane[i].paint(gp, g);
    }
    paintScroll(g);
  }

  private void paintScroll (Graphics g) {
    Line l = line[1];
    if (l != null) {
      vScroll = Math.max(10,Math.min(200,l.y2-l.y1-sbw*8));
      int xm = (l.x1+l.x2)/2;
      int ym = l.y2 - sbw*2 - vScroll;
      MWindow.drawVSlider(xm,ym,0,vScroll, (double)iScroll/nwidget,0, g,MW.theme);
    }
  }

  private void resize () {
    if (headless || isHideWidgets()) return;
    ytop = (menuBarEnabled && (borders&BORD_LEFT)==0)? hbar : 0; // top of panel is adjusted if there is an active menu bar - since 3.9.0 for NXM-3321
    for (int i=1; i<=4; i++) fixLine(i); // borders need 2 passes of fixLine
    for (int i=1; i<=nline; i++) fixLine(i);
    for (int i=1; i<=npane; i++) fixPane(i);
    setBorderBoxes();
    layout();
    if(MW!=null) MW.panel.revalidate(); // Added to resize items as needed. 3.8.1 NXM-3213 - This fixes the cutoff text with Centos 7
  }

  /** Hold application of future layout changes (e.g. widget updates).
      Typically set to true before a large batch of gcontrol updates and then
      set back to false at the conclusion of those updates.
      @param hold True to holdLayout refreshes, false to allow them (normal operations)
      @since NeXtMidas 2.8.2
   */
  public void setHoldLayout (boolean hold) { holdLayout = hold; }

  /** Hold application of future layout changes (e.g. widget updates)?
   *  @return true if hold application of widget updates
   */
  public boolean getHoldLayout () { return holdLayout; }

  private void layout () {
    if (init || holdLayout) return;  // holdLayout check added in 2.8.2 for Bug 2214
    if (isHideWidgets()) return;
    Graphics g = MW.panel.getGraphics();
    if (g == null) return;

    nleft=yleft=0; ybott=bbox[1].y+bbox[1].h;
    xbar=line[1].x2;//  initial GLabel placement along menu bar is just to right of line 1 - for NXM-3321
    if (!is(COMPRESS)) curCompress=0;
    iwz0=iwz1=iwz2=0;
    if (headless) return;
    int needCompressO = needCompress;
    int prevCompressO = prevCompress;
    lastOpenedGLabel = null;
    lastCollapsableGLabel = null;
    for (int i=1; i<=nwidget; i++) fixWidget(i,1);
    needLayout=false;
    prevCompress = needCompressO;
    needCompress = 0;
    if (needCompressO==0 && prevCompressO!=0) return;
    if (yleft>ybott && curCompress<maxCompress) needCompress=1;
    if (curCompress>0 && ybott-yleft>nleft && prevCompress<=0) needCompress=-1;
    if (is(COLLAPSE) && needCompress>0 && lastCollapsableGLabel!=null) {
      lastCollapsableGLabel.setActive(GLabel.aCOLLAPSED);
      needCompress=0;
    }
    if (!is(COMPRESS)) needCompress=0;
    if (yleft>ybott && iScroll<0) iScroll=0;
    if (yleft<ybott && iScroll==0) iScroll=-1;
    if (iScroll != oScroll) {
      paintScroll(g);
      oScroll = iScroll;
    }
  }

  @Override
  public int processMessage (Message msg) {
    String text;

    // convert a few special sequences to specifically named messages
    if (msg.name.equals("KEYPRESS")) {
      text = msg.getS();
      if (text.equalsIgnoreCase("M")) msg.name = "MENU";
      if (text.equalsIgnoreCase("Enter") && (exit&EXIT_RETURN)!=0) msg.name = "EXIT";
    }
    if (msg.name.equals("BUTTON") && msg.info==2) {
      if (cfgClick (MW.px,MW.py)) return 1;
      msg.name = "MENU";
    }

    // all drawing is done via the PAINT message, which only the drawing thread sends.
    if (msg.name.equals("PAINT")) {
      paintComponent((GPanelSetup)MW, (Graphics)msg.data);
    }
    else if (msg.name.equals("RESIZE")) {
      resize();
    }
    /* if initializing queue for me */
    else if (msg.name.equals("POINTER")) {
      int i = selLine (MW.px, MW.py);
      int cdir = (i>0 && msg.info!=-9)? line[i].dir : 0;
      if (cdir!=cursorDir) setResizeCursor(cdir);
      cursorDir = cdir;
      hovPane(MW.px,MW.py);
    }
    else if (msg.name.equals("DRAG")) {
      MBox box = (MBox)msg.data;
      if (dragline==0) dragline = selLine (box.x,box.y);
      if (dragline>0) movLine(dragline,box.x+box.w,box.y+box.h);
      if (dragline==-1) movScroll(box.y+box.h);
    }
    else if (msg.name.equals("DRAGBOX")) {
      MBox box = (MBox)msg.data;
      if (msg.info==1 && dragline>0) movLine(dragline,box.x+box.w,box.y+box.h);
      dragline=0;
    }
    else if (msg.name.equals("BUTTON")) {
      int i = selLine (MW.px,MW.py);
      if (i==0) {
        int[] paneButton = selPane (MW.px,MW.py); // get both pane & button selected, since NeXtMidas 3.5.4 Bug 2895
        if (paneButton[0]!=0) {
          boolean isCtrl = false;
          // since 3.1.0 for Bug 2458: Check to see if CTRL key was held while clicking
          if (msg.data instanceof MouseEvent){
            MouseEvent me = (MouseEvent)msg.data;
            isCtrl = (me.getModifiersEx()&InputEvent.CTRL_DOWN_MASK) != 0;
          }
          hitPane (paneButton[0], isCtrl, paneButton[1]);
        }
      }
      else if (i==-1) movScroll(MW.py);
      else if (((i<=2 && MW.py>line[i].y1 && MW.py<line[i].y1+sbw*5) ||
          (i>2 && i<=4 && MW.px>line[i].x1 && MW.px<line[i].x1+sbw*5)) ) {
        // hit the auto collapse/expand border zones
        setToggleBorders(1<<(i-1)); // Moved logic to new method for Menu Bar since 3.9.0
      }
    }
    else if (msg.name.equals("SHOW")) {
      MW.display(msg.info);
    }
    else if (msg.name.equals("SHOWN")) {
      if (MW.status!=MWindow.INIT) MW.status=msg.info;
      sendMessage (msg);
    }
    else if (msg.name.equals("REFRESH")) {
      refresh(); // doesn't actually draw
    }
    else if (msg.name.equals("RESIZE")) {
      resize();
    }
    else if (msg.name.equals("MENU")) {
      new GMenu (MW,"Panel","Configure,Controls,Borders,Options,Theme,Query,Macro,Debug,Exit",0,0,this);
    }
    else if (msg.name.equals("PANEL")) {
      text = msg.getS();
      if (text.equals("CONFIGURE")) {
      if (configure>0) text="NoModify"; else text="Modify";
        new GMenu (MW,"Panel.cfg","Setup,Clear,Load,SaveResult,Save,"+text,0,0,this);
      }
      else if (text.equals("CONTROLS")) {
        new GMenu (MW,"Controls","Display,Events",0,0,this);
      }
      else if (text.equals("BORDERS")) {
        new GMenu (MW,"Borders",bordersList,borders,GMenu.TOGGLE,this);
      }
      else if (text.equals("OPTIONS")) {
        new GMenu (MW,"Options",optionsList, options,GMenu.TOGGLE,this);
      }
      else if (text.equals("THEME")) {
        new GMenu (MW,"Theme",MWindow.themeList,0,0,this);
      }
      else if (text.equals("QUERY")) {
        new GQuery (MW,"Query Results",MR,0,this);
      }
      else if (text.equals("MACRO")) {
        new GMenu (MW,"Macro","Run,Pause,End,Registry,Results,Pipes,Commands,Remotes",0,0,this);
      }
      else if (text.equals("DEBUG")) {
        new GMenu (MW,"Debug","Results,PipeMon,MsgMon,Terminal",0,0,this);
      }
      else if (text.equals("EXIT")) {
        msg.name = "EXIT";
        if ((exit&EXIT_MENU)!=0) MQ.put(msg);
      }
    }
    else if (msg.name.equals("MACRO")) {
      text = msg.getS();
           if (M.macro==null);
      else if (text.equals("RUN")) M.macro.setPipeMode(Midas.PWAIT);
      else if (text.equals("PAUSE")) M.macro.setPipeMode(Midas.PPAUSE);
      else if (text.equals("END")) M.macro.setPipeMode(Midas.PSTOP);
      else if (text.equals("REGISTRY")) new GQuery (MW,"Query Registry",M.registry.table,0,this);
      else if (text.equals("RESULTS")) new GQuery (MW,"Query Results",MR,0,this);
      else if (text.equals("PIPES")) {
        Table t = Convert.o2t(MR.get("RAM"));
        new GMenu (MW,"Pipe",t.getKeyList(),0,GMenu.STICKY,this);
      }
      else if (text.equals("COMMANDS")) {
        Table t = Convert.o2t(MR.get("REG")); filterMode=1;
        new GMenu (MW,"Command",t.getKeyList(this),0,GMenu.STICKY,this);
      }
      else if (text.equals("REMOTES")) {
        Table t = Convert.o2t(MR.get("REG")); filterMode=2;
        new GMenu (MW,"Remote",t.getKeyList(this),0,GMenu.STICKY,this);
      }
    }
    else if (msg.name.equals("DEBUG")) {
      text = msg.getS();
           if (text.equals("RESULTS")) new GQuery (MW,"Query Results",MR,0,this);
      else if (text.equals("PIPEMON")) Shell.runCommand (M,"GCONTROL,PIPEMON");
      else if (text.equals("MSGMON")) Shell.runCommand (M,"GCONTROL,MSGMON");
      else if (text.equals("TERMINAL")) Shell.runCommand (M,"SHELLGUI/ATTACH/BG/DISPLAY=FRONT");
    }
    else if (msg.name.equals("PIPE")) {
      text = msg.getS();
      new GQuery (MW,"Query "+text,M.pipes.get(text),GQuery.NOEDIT,this);
    }
    else if (msg.name.equals("COMMAND") || msg.name.equals("REMOTE")) {
      text = msg.getS();
      new GQuery (MW,"Query "+text,M.registry.get(text),0,this);
    }
    else if (msg.name.equals("PANEL.CFG")) {
      text = msg.getS();
      if (text.equals("MODIFY")) {
        if (is(LOCKSETUP)) {
          new GAlert (MW,"Setup","Setup is locked, can not modify.","OK",1,0,this,4);
        } else {
          configure=1; refresh();
        }
      }
      else if (text.equals("NOMODIFY")) {
        configure=0; refresh();
      }
      else if (text.equals("CLEAR")) {
        for (int i=npane; i>0; i--) delPane(i);
        npane=0; nline=4; refresh();
      }
      else if (text.equals("SETUP")) {
        int nChars = getPromptNumChars(80); // Desire an 80 width input prompt
        if (M.macro!=null) new GPrompt (MW,"LoadSetup","",nChars,0,this);
        else new GAlert (MW,"Setup","No macro available for setup","OK",1,0,this);
      }
      else if (text.equals("LOAD")) {
        int nChars = getPromptNumChars(cfgfile.length());
        new GPrompt (MW,"LoadFile",cfgfile,nChars,0,this);
      }
      else if (text.equals("SAVERESULT")) {
        if (cfgfile.contains(".mmp") || cfgfile.contains(".tbl")) cfgfile = "setupTable";
        int nChars = Math.max(getPromptNumChars(cfgfile.length()), 25);
        new GPrompt (MW,"SaveTableResult",cfgfile,nChars,0,this);
      }
      else if (text.equals("SAVE")) {
        if (tsetup) {
          if (!cfgfile.endsWith(".tbl")) setConfigFileName(TSETUP);
        } else setConfigFileName(SETUP);
        int nChars = Math.max(getPromptNumChars(cfgfile.length()), 25);
        new GPrompt (MW,"SaveFile",cfgfile,nChars,0,this);
      }
    }
    else if (msg.name.equals("CFG.ADD")) {
      text = msg.getS();
           if (text.equals("HLINE")) addLine (HLINE,cfgx,cfgy);
      else if (text.equals("VLINE")) addLine (VLINE,cfgx,cfgy);
      else if (text.equals("HLINEX")) addLine (HLINEX,cfgx,cfgy);
      else if (text.equals("VLINEX")) addLine (VLINEX,cfgx,cfgy);
      else if (text.equals("PANE")) addPane (0,cfgx,cfgy);
      configure=1;
    }
    else if (msg.name.equals("THEME")) {
      String themeName = msg.getS();
      MW.setTheme(themeName,inline);
      this.theme = MW.theme;
      for (int i=1; i<=nwidget; i++) {
        if (!widget[i].hasLocalTheme()) widget[i].setTheme(MW.theme);
      }
      for (int i=1; i<=npane; i++) pane[i].setTheme(MW.theme);
      resize();
      refreshAll(); // make sure everything refreshes
    }
    else if (msg.name.equals("CONTROLS")) {
      text = msg.getS();
      if (text.equals("DISPLAY")) { borders ^= BORD_LEFT; resize(); refresh(); }
      else if (text.equals("EVENTS"))
        new GMenu (MW,"Events",MWindow.eventFilterList,eventFilter,GMenu.TOGGLE,this);
    }
    else if (msg.name.equals("EVENTS")) {
      eventFilter = msg.info;
      for (int i=1; i<=nwidget; i++) widget[i].setEventFilter(eventFilter);
    }
    else if (msg.name.equals("BORDERS")) {
      //jph -- Use set method here instead, it does the same thing
      //TBD -- Use setBorders(String) preferred
      //borders = msg.info; resize(); refresh();
      setBorders(msg.info);
    }
    else if (msg.name.equals("OPTIONS")) {
      options = msg.info; refresh();
    }
    else if (msg.name.equals("LOADSETUP")) {
      String cfgfile = M.macro.getFileName()+"p"+msg.getS();
      load(cfgfile); resize(); refresh();
    }
    else if (msg.name.equals("LOADSETUPTABLE")) {
      setSetupTable(Convert.o2t(msg.data)); resize(); refresh();
    }
    else if (msg.name.equals("LOADFILE")) {
      load(msg.getS()); resize(); refresh();
    }
    else if (msg.name.equals("SAVEFILE")) {
      save(msg.getS());
    }
    else if (msg.name.equals("SAVETABLERESULT")) {
      saveResult(msg.getS());
    }
    else if (msg.name.equals("SAVETABLEFILE")) {
      saveSetupTable(msg.getS());
    }
    else if (msg.name.equals("WINDOW")) {
      // since 3.3.0: handle mouse and messages for ICON/DEICONIED (BUG 2690) on PANEL
      if (MW.status!=MWindow.FRONT && "DEICONIFIED".equals(msg.data)) {
        MW.display(MWindow.FRONT);
      } else if (MW.status!=MWindow.ICON && "ICON".equals(msg.data)) {
        MW.display(MWindow.ICON);
      }
      // TODO: The above ~5 lines of code should move to a common place in GPrimitive (or MWindow)
      //       It also matches code in MWindow.process(String name, int info, Object data)

      if (wmsgid==null) {
        if ((exit&EXIT_WINDOW)!=0 && "CLOSING".equals(msg.data) && M.macro!=null) {
          M.macro.setPipeMode(Midas.PSTOP);
        }
      } else sendWMessage (msg);
    }
    else if (msg.name.equals("EXIT")) {
      if ((exit&EXIT_MESSAGE)!=0) return FINISH;
    }
    else if (msg.name.equals("CURSOR")) {
      setCursor(msg.data.toString());
    }

    return NORMAL;
  }

  /* Compute the number of characters for the prompt for SAVE or LOAD configuration file */
  private int getPromptNumChars (int desiredNumChars) {
    int panelWidth = MW.panel.getWidth();
    int charWidth  = MW.fm.charWidth('W'); // Find width of widest character
    int maxChars   = Math.max(10, (panelWidth/charWidth)-17);
    // This is, reserve some space for SAVEFILE or LOAD title and border
    int numChars = Math.min(desiredNumChars,maxChars);
    return numChars;
  }

  private boolean cfgClick (int x, int y) {
    int i;
    if (configure!=1) return false;
    i = selLine (x,y);
    if (i>4) {
      Line l = line[i];
      new GMenu (MW,"Cfg.Line","Cancel,Extend,Shrink,Fix,UnFix,Delete",1,0,l);
      return true;
    }
    // [0] contains Pane number and [1] contains button selected - Since NeXtMidas 3.5.4 Bug 2895
    int[] paneButton = selPane (x,y);
    i = paneButton[0];
    int buttonSelected = paneButton[1];
    if (buttonSelected == BUTTON_SELECTED_NONE && i > 0) {
      Pane p = pane[i];
      cfgx=x; cfgy=y;
      new GMenu (MW,"Cfg.Pane","Cancel,TitleText,TitleColor,TitleSize,Attach,Delete,AddTab",1,0,p);
      return true;
    }
    if (x<line[1].x2) return false;
    cfgx=x; cfgy=y;
    new GMenu (MW,"Cfg.Add","Cancel,Vline,HLine,VLineX,HLineX,Pane",1,0,this);
    return true;
  }

  /** Refers to {@link MWindow#eventFilterList} for a comma-delimited string of valid event filter flags for the window */
  public static String eventFilterList = MWindow.eventFilterList;

  /** set or modify event filter
   *  @param mask event filter flags in mask form
   */
  public void setEventFilter (String mask) {
    int ef = 0;
    if (mask.equals("ENABLE")) ef=0;
    else if (mask.equals("DISABLE")) ef=-1;
    else ef = Parser.mask(eventFilterList,mask,eventFilter);
    for (int i=1; i<=nwidget; i++) widget[i].setEventFilter(ef);
    eventFilter = ef;
  }
  /** Get event filter
   *  @return the event filter flags in mask form
   */
  public String getEventFilter () {
    return Parser.mask2s(eventFilterList,eventFilter);
  }

  /** set or modify options
   * @param mask options in mask form
   */
  public void setOptions (String mask) {
    options = Parser.mask(optionsList,mask,options);
    resize();
    refresh();
  }

  private void sendWMessage (Message msg) {
    if (wmsgid instanceof MessageHandler) {
      if (wmsgid == msg.from) return;    // don't send it back
      msg.to = wmsgid; msg.from = this;
      ((MessageHandler)wmsgid).processMessage(msg);
    }
  }

  private int getix (int il) {
    Line l = line[il];
    float x = l.rx;
    int i1 = line[1].x2-bw;
    int i2 = line[2].x1+bw;
    int wx1=0, wx2=MW.pos.w; 
    // check for outside fixed box borders
   if (il>4) {
    if (l.i1==0 && ((borders&BORD_TOPLEFT)!=0)) i1=wx1;
    if (l.i1==0 && ((borders&BORD_TOPRIGHT)!=0)) i2=wx2;
    if (l.i2==0 && ((borders&BORD_BOTTOMLEFT)!=0)) i1=wx1;
    if (l.i2==0 && ((borders&BORD_BOTTOMRIGHT)!=0)) i2=wx2;
   }
    return (int)(x*(i2-i1))+i1;
  }

  private int getiy (int il) {
    Line l = line[il];
    float y = l.ry;
    int i1 = line[3].y2-bw;
    int i2 = line[4].y1+bw;
    int wy1=ytop, wy2=MW.pos.h; 
    // check for outside fixed box borders
   if (il>4) {
    if (l.i1==0 && ((borders&BORD_TOPLEFT)==0)) i1=wy1;
    if (l.i1==0 && ((borders&BORD_BOTTOMLEFT)==0)) i2=wy2;
    if (l.i2==0 && ((borders&BORD_TOPRIGHT)==0)) i1=wy1;
    if (l.i2==0 && ((borders&BORD_BOTTOMRIGHT)==0)) i2=wy2;
   }
    return (int)(y*(i2-i1))+i1;
  }

  private float getrx (int x, int y) {
    int i1 = line[1].x2-bw;
    int i2 = line[2].x1+bw;
    int wx1=0, wx2=MW.pos.w; 
    // check for outside fixed box borders
    if ((borders&BORD_TOPLEFT)!=0     && y<line[3].y1) i1=wx1;
    if ((borders&BORD_TOPRIGHT)!=0    && y<line[3].y1) i2=wx2;
    if ((borders&BORD_BOTTOMLEFT)!=0  && y>line[4].y2) i1=wx1;
    if ((borders&BORD_BOTTOMRIGHT)!=0 && y>line[4].y2) i2=wx2;
    return (float)(x-i1)/(float)Math.max(1,i2-i1);
  }

  private float getry (int x, int y) {
    int i1 = line[3].y2-bw;
    int i2 = line[4].y1+bw;
    int wy1=ytop, wy2=MW.pos.h; 
    // check for outside fixed box borders
    if ((borders&BORD_TOPLEFT)==0     && x<line[1].x1) i1=wy1;
    if ((borders&BORD_BOTTOMLEFT)==0  && x<line[1].x1) i2=wy2;
    if ((borders&BORD_TOPRIGHT)==0    && x>line[2].x2) i1=wy1;
    if ((borders&BORD_BOTTOMRIGHT)==0 && x>line[2].x2) i2=wy2;
    return (float)(y-i1)/(float)Math.max(1,i2-i1);
  }

  private void addLine (int dir, int x, int y) {
    float rx,ry; int i,i1,i2,ii1,ii2, n=nline+1;
    boolean extend = (dir<0);
    if (extend) dir = -dir;
    Line l = new Line();
    line[n] = l;
    l.dir=dir;
    l.rx=rx=getrx(x,y);
    l.ry=ry=getry(x,y);
    if (dir==VLINE) {
      if (rx<0 || rx>1) { new GAlert (MW,"AddLine","Must be inside the vertical fixable walls","OK",1,0,this); return; }
      l.x1 = x-bw; l.x2 = x+bw;
      if (extend) { i1=3; i2=4; }
      else if (ry<0) { i1=0; i2=3; }
      else if (ry>1) { i1=4; i2=0; }
      else for (i1=3,i2=4,i=5; i<=nline; i++) {
        ii1 = line[i].i1; ii2 = line[i].i2;
        if (line[i].dir==HLINE && line[ii1].rx<rx && line[ii2].rx>rx
          && line[i].ry<ry && line[i].ry>line[i1].ry) i1=i;
        if (line[i].dir==HLINE && line[ii1].rx<rx && line[ii2].rx>rx
          && line[i].ry>ry && line[i].ry<line[i2].ry) i2=i;
      }
      l.i1 = i1;
      l.i2 = i2;
    }
    if (dir==HLINE) {
      if (ry<0 || ry>1) { new GAlert (MW,"AddLine","Must be inside the horizontal fixable walls","OK",1,0,this); return; }
      l.y1 = y-bw; l.y2 = y+bw;
      if (extend) { i1=1; i2=2; }
      else if (rx<0) { i1=0; i2=1; }
      else if (rx>1) { i1=2; i2=0; }
      else for (i1=1,i2=2,i=5; i<=nline; i++) {
        ii1 = line[i].i1; ii2 = line[i].i2;
        if (line[i].dir==VLINE && line[ii1].ry<ry && line[ii2].ry>ry
          && line[i].rx<rx && line[i].rx>line[i1].rx) i1=i;
        if (line[i].dir==VLINE && line[ii1].ry<ry && line[ii2].ry>ry
          && line[i].rx>rx && line[i].rx<line[i2].rx) i2=i;
      }
      l.i1 = i1;
      l.i2 = i2;
    }
//    System.out.println("Add line d="+dir+" x="+x+" y="+y+" rx="+rx+" ry="+ry+" i1="+l.i1+" i2="+l.i2);
    fixLine(n);
    nline=n;
    refresh();
  }

  private void addPane (int ip, int x, int y) {
    Pane p = new Pane(this);
    if (ip==0) for (ip=1; ip<=npane && pane[ip]!=null; ip++);
    pane[ip]=p;
    p.ix1 = p.ix2 = p.iy1 = p.iy2 = 0;
    for (int i=1; i<=nline; i++) {
      if (line[i].dir == VLINE) {
        if (line[i].y1<y && line[i].y2>y) {
          if (line[i].x2<x && (p.ix1==0 || line[i].x2>line[p.ix1].x2)) p.ix1=i;
          if (line[i].x1>x && (p.ix2==0 || line[i].x1<line[p.ix2].x1)) p.ix2=i;
        }
      } else {
        if (line[i].x1<x && line[i].x2>x) {
          if (line[i].y2<y && (p.iy1==0 || line[i].y2>line[p.iy1].y2)) p.iy1=i;
          if (line[i].y1>y && (p.iy2==0 || line[i].y1<line[p.iy2].y1)) p.iy2=i;
        }
      }
    }
    p.name = "PANE"+ip;
    p.title = "Pane "+ip;
    p.ts = 12;
    fixPane(ip);
    panes.put(p.name,p);
    npane = Math.max(ip,npane);
    refresh();
  }

  private void delPane (Pane p) {
    for (int i=1; i<=npane; i++) if (p==pane[i]) delPane(i);
  }

  private void delPane (int i) {
    if (i==0) return;
    Pane p = pane[i];
    if (p==null) return; // NXM-4074 handle case where pane indicies are not consecutive
    // since 3.8.1 to manage closing windows in JLayeredpane for NXM-3213
    JRootPane rootPane = null;
    if((MW.panel instanceof JPanel) && (p.panel !=null)) {
      rootPane = ((JPanel)MW.panel).getRootPane();
      if(rootPane!=null && rootPane.getLayeredPane()!=null && p.panel!=null) rootPane.getLayeredPane().remove(p.panel);
    }

    if (p.panel != null) MW.panel.remove(p.panel);
    for (--npane; i<=npane; i++) pane[i] = pane[i+1];
    pane[npane+1] = null;      //bug 1881
    getPanes().remove(p.name); //bug 1881
    refresh();
  }

  private void fixPane (int i) {
    Pane p = pane[i];
    if (p==null) return;
    int wx1=0, wx2=MW.pos.w, wy1=ytop, wy2=MW.pos.h; // new ytop top boundary is > 0 when menu bar active - NXM-3321
    int x1 = (p.ix1==0)? wx1 : line[p.ix1].x2;
    int x2 = (p.ix2==0)? wx2 : line[p.ix2].x1;
    int y1 = (p.iy1==0)? wy1 : line[p.iy1].y2;
    int y2 = (p.iy2==0)? wy2 : line[p.iy2].y1;
    if      (p.ix2==1) {      x1 = wx1;
      if (p.iy1==3) y1 = ((borders&BORD_TOPLEFT)!=0)? line[3].y2 : wy1;
      if (p.iy2==4) y2 = ((borders&BORD_BOTTOMLEFT)!=0)? line[4].y1 : wy2;
    }
    else if (p.ix1==2) {      x2 = wx2;
      if (p.iy1==3) y1 = ((borders&BORD_TOPRIGHT)!=0)? line[3].y2 : wy1;
      if (p.iy2==4) y2 = ((borders&BORD_BOTTOMRIGHT)!=0)? line[4].y1 : wy2;
    }
    else if (p.iy2==3) {      y1 = wy1;
      if (p.ix1==1) x1 = ((borders&BORD_TOPLEFT)==0)? line[1].x2 : wx1;
      if (p.ix2==2) x2 = ((borders&BORD_TOPRIGHT)==0)? line[2].x1 : wx2;
    }
    else if (p.iy1==4) {      y2 = wy2;
      if (p.ix1==1) x1 = ((borders&BORD_BOTTOMLEFT)==0)? line[1].x2 : wx1;
      if (p.ix2==2) x2 = ((borders&BORD_BOTTOMRIGHT)==0)? line[2].x1 : wx2;
    }
    p.x=x1; p.y=y1+3*p.ts/2; p.w=x2-p.x; p.h=y2-p.y;
    if (i>0 && mobile!=0) { p.w=mpad; p.h=mpad; }
    p.hidden=false;
    if (p.ix2==1) p.hidden = ((borders&BORD_LEFT)==0);
    if (p.ix1==2) p.hidden = ((borders&BORD_RIGHT)==0);
    if (p.iy2==3) p.hidden = ((borders&BORD_TOP)==0);
    if (p.iy1==4) p.hidden = ((borders&BORD_BOTTOM)==0);

    for (int ip=1; ip<i; ip++) {
      Pane pp = pane[ip];
      if (pp!=null && pp.ix1==p.ix1 && pp.iy1==p.iy1) {
        if (p.getTabManager() == null) {
          if (pp.getTabManager() == null) {
            pp.setTabManager(new TabManager());
          }
          p.setTabManager(pp.getTabManager());
        }
      }
    }
    if (p.panel==null) return;
    if (p.mw!=null && p.mw.pushParent!=null) return;
    boolean vis = (!p.hidden) && ((p.getTabManager() == null)
                || p.getTabManager().isVisibleTab(p));
    p.panel.setVisible(vis);
    if (vis) p.panel.setBounds(p.x,p.y,p.w,p.h);
  }

  /** This is called when a tab is clicked, and other times.
      @param i The pane number (negative if the X button was clicked (e.g. for Push/Pop of that pane)
      @param isCtrl Whether or not the CTRL key is held down
      @param buttonSelected - added to accommodate second custom icon/button - Since NeXtMidas 3.5.4 Bug 2895
      @since NeXtMidas 3.1.0 (Bug 2458)
   */
  private void hitPane (int i, boolean isCtrl, int buttonSelected) {
    if (buttonSelected != BUTTON_SELECTED_NONE && i != 0) {
      Pane p = pane[i];  // Since 3.5.4 Bug 2895 no negative
      if (p!=null && p.panel!=null) p.panel.setVisible(true);
      if (buttonSelected == BUTTON_SELECTED_PUSHPOP) {
        // Since 3.5.4 Bug 2895 - Allow custom behavior for the PUSHPOP Icon
        if ((pushPopIconActions & PUSHPOP_ICON_ACTION_PUSHPOP) != 0) {
          // PUSH / POP on icon click
          if (p!=null && p.mw!=null && p.mw.status!=MWindow.EXIT) p.mw.pop(-1,isCtrl&&((options&ADVPUSHPOP)!=0));
        }
        if ((pushPopIconActions & PUSHPOP_ICON_ACTION_MESSAGE) != 0) {
          /// Send PUSHPOPCLICKED message
          Table paneData= new Table();  // Create Table with Data for message
          paneData.put("EMBEDDED_COMMAND_ID", p.mw.getEmbeddedCommandID());
          paneData.put("PANE", p);
          Message msg = new Message("PUSHPOPCLICKED",i,paneData,null,this);// (name info data, to, from)
          sendMessage(msg);
        }
      }
      else if (buttonSelected>0) {
	String icon = p.iconKeys[buttonSelected-1];
        Table iconData= new Table("{PANE="+p.name+",ICON="+icon+",X="+p.x+",Y="+p.y+"}");  // Create Table with Data for message
	Message msg = new Message("ICONCLICKED",1,iconData,null,this);// (name info data, to, from)
	sendMessage(msg);
      }
      else {
        // Check for the Custom Icon Actions - Since NeXtMidas 3.5.4 Bug 2895
	if ((customIconActions & CUSTOM_ICON_ACTION_CLOSE) != 0) {
	  // Send EXIT message to Command in Pane
	  Message msg = new Message("EXIT",i,"EXIT",p.mw.getEmbeddedCommandID(),this);// (name info data, to, from)
	  MQ.put(msg);
	  // Delete the PANE
	  delPane(p);
	  // Note: this reference to the Pane still remains and can be used for line deletion and message information
	  // Delete excess boundary line, if applicable
	  int lineToDelete = deleteAppropriateLine(p);
	}
	if ((customIconActions & CUSTOM_ICON_ACTION_MESSAGE) != 0) {
	  /// Send CUSTOMICONCLICKED message
	  Table paneData= new Table();  // Create Table with Data for message
	  paneData.put("EMBEDDED_COMMAND_ID", p.mw.getEmbeddedCommandID());
	  paneData.put("PANE", p);
	  Message msg = new Message("CUSTOMICONCLICKED",i,paneData,null,this);// (name info data, to, from)
	  sendMessage(msg);
	}
      }
    } else {
      Pane p = pane[i];
      if (p.getTabManager() != null) {
	p.makeVisibleTab(); /* calls updateTabs() */
      } else {
	resize();
	refresh();
      }
    }
  }

  /** Determine of this former Panes lines can be deleted, and delete it
      @param p pane that has been deleted
      @return index of line that was deleted (-1 if no line found)
      @since NeXtMidas 3.5.4 Bug 2895
   */
  private int deleteAppropriateLine(Pane p) {
     int formerPaneIX1 = p.ix1;
     int formerPaneIX2 = p.ix2;
     int formerPaneIY1 = p.iy1;
     int formerPaneIY2 = p.iy2;
     int lineToDelete = formerPaneIY2;
     int substituteLine = -1;
     boolean delSuccessful = false; // default to unsuccessful
     // check to see if line below can be deleted
     if (formerPaneIY2 > 4) {
       substituteLine= lineBelow(formerPaneIY2);
       if (substituteLine > 0) {
         delSuccessful = delLine(lineToDelete);
       }
     }
     // check to see if line above can be deleted
     if ((substituteLine < 0  || !delSuccessful) && formerPaneIY1 > 4) {
       lineToDelete = formerPaneIY1;
       substituteLine = lineAbove(lineToDelete);
       if (substituteLine > 0) {
         delSuccessful = delLine(lineToDelete);
       }
     }
     // check to see if line to the right can be deleted
     if ((substituteLine < 0 || !delSuccessful) && formerPaneIX2 > 4) {
       lineToDelete = formerPaneIX2;
       substituteLine = lineToRight(lineToDelete);
       if (substituteLine > 0) {
         delSuccessful = delLine(lineToDelete);
       }
     }
     // check to see if line to the left can be deleted
     if ((substituteLine < 0 || !delSuccessful) && formerPaneIX1 > 4) {
       lineToDelete = formerPaneIX1;
       substituteLine = lineToLeft(lineToDelete);
       if (substituteLine > 0) {
         delLine(lineToDelete);
       }
     }
     if (substituteLine < 0 || !delSuccessful) {
       lineToDelete = -1;
     }
     return lineToDelete;
  }

  /** Check if the PANEs that reference the line all reference it on the same side
      If so, the line can still be deleted even though multiple PANEs reference it
      @param paneReferences pane index, border facing line
      @return true if all PANEs have the line on the same border (top, bottom, left, or right)
      @since NeXtMidas 3.5.4 Bug 2909 Bug 2895
   */
  private boolean doAllPanesReferenceLineOnSameSide(int[][] paneReferences) {
    boolean sameSide = true;
    // Only need to check if PANEs reference line on same side if there are multiple PANEs
    if (paneReferences.length > 1) {
      int side = paneReferences[0][1];
      for (int i = 1; i < paneReferences.length; i++) {
        // If this PANE has the line against a different side than the first, they are not all on the same side
        if (paneReferences[i][1] != side) {
          return false;
        }
      }
    }
    return sameSide;
  }

  /** This calls a resize() and a refresh() to update the tabs. */
  private void updateTabs () {
    resize();
    refresh();
  }

  private void movScroll (int y) {
    int y2 = line[1].y2 - sbw*2;
    int y1 = y2 - vScroll;
    double f = (double)(y-y1)/(y2-y1);
    int i = (int)(f*nwidget);
    if (setScroll(i)) layout();
  }

  private boolean setScroll (int i) {
    if (i==iScroll) return false;
    iScroll = i;
    return true;
  }

  private void setLine (int i, int dir, int fp, float rx, float ry, int i1, int i2) {
    line[i].dir=dir;
    line[i].fp=fp;
    line[i].rx=rx;
    line[i].ry=ry;
    line[i].i1=i1;
    line[i].i2=i2;
    if (dir==VLINE) {
      line[i].y1=ytop; // new ytop top boundary is > 0 when menu bar active - NXM-3321
      line[i].y2=(MW != null) ? MW.pos.h : 0;
      line[i].x1=line[i].x2=getix(i);
    } else {
      line[i].x1=0;
      line[i].x2=(MW != null) ? MW.pos.w : 0;
      line[i].y1=line[i].y2=getiy(i); 
    }
  }

  private void delLine (Line l) {
    for (int i=1; i<=nline; i++) {
      if (l==line[i]) {
        boolean lineDeleted = delLine(i);
        if (!lineDeleted) M.warning("Line can not be deleted because it is inbetween two active Panes");
      }
    }
  }

  /** Only delete Line when only remaining Pane references to it are all on one side
         (Since NeXtMidas 3.5.4 Bug 2909)
      @param i index of line to delete
      @return true if line deletion was successful
   */
  private boolean delLine (int i) {
    int[][] panesThatRef = findPaneReferencesToLine(i);
    // if a pane has a reference to the line being deleted, switch it to
    // reference the next line above, below, to the left, or to the right
    // (depending on the situation)   Since NeXtMidas 3.5.4  Bug 2909
    int replacementLine;
    if (panesThatRef.length == 1) {
      replacementLine = replacePaneLine(i,pane[panesThatRef[0][0]], panesThatRef[0][1]);
    }
    else if (panesThatRef.length > 1) {
      // If all PANEs reference on the same border, the line can still be deleted, all the
      // panes and lines affected need updating
      if (doAllPanesReferenceLineOnSameSide(panesThatRef)) {
        replacementLine = replacePaneLine(i,pane[panesThatRef[0][0]], panesThatRef[0][1]);
        for (int j = 1; j < panesThatRef.length; j++) {
          replacePaneLine(i,pane[panesThatRef[j][0]], panesThatRef[j][1]);
        }
        // Check all lines for a reference to the line being deleted, and replace any references with replacement line
        for (int j = 1; j <= nline; j++) {
          replaceLineReference(i,replacementLine,j);
        }

      }
      else {
        return false;
      }
    }
    // shift down pane line references for deleted line NeXtMidas Since 3.5.4 Bug 2909
    for (int j=1; j<=npane; j++) {
      if (pane[j].ix1>=i) pane[j].ix1--;
      if (pane[j].ix2>=i) pane[j].ix2--;
      if (pane[j].iy1>=i) pane[j].iy1--;
      if (pane[j].iy2>=i) pane[j].iy2--;
    }
    // shift down line line references for deleted line
    for (int j=5; j<=nline; j++) {
      if (line[j].i1>=i) line[j].i1--;
      if (line[j].i2>=i) line[j].i2--;
    }
    for (int j=i; j<nline; j++) line[j]=line[j+1];
    nline--;
    resize();
    refresh();
    return true;
  }

  /** Find the PANEs that reference this line
      @param line number
      @return array with {PANE number, BORDER type number} pairings
      @since NeXtMidas 3.5.4  Bug 2909
   */
  private int[][] findPaneReferencesToLine (int i) {
    int[][] paneRefs = new int[npane][2];
    int numRefs = 0;
    // If this is a horizontal line, PANES will have this as either the top or bottom border
    if (line[i].dir == HLINE) {
      for (int p=0; p <= npane; p++) {
        if (pane[p].iy1 == i) {
          paneRefs[numRefs][0] = p;
          paneRefs[numRefs++][1] = BORD_TOP;
        }
        else if (pane[p].iy2 == i) {
          paneRefs[numRefs][0] = p;
          paneRefs[numRefs++][1] = BORD_BOTTOM;
        }
      }
    }
    // If this is a vertical line, PANES will have this as either the left or right border
    else if (line[i].dir == VLINE) {
      for (int p=0; p <= npane; p++) {
        if (pane[p].ix1 == i ) {
          paneRefs[numRefs][0] = p;
          paneRefs[numRefs++][1] = BORD_LEFT;
        }
        else if (pane[p].ix2 == i) {
          paneRefs[numRefs][0] = p;
          paneRefs[numRefs++][1] = BORD_RIGHT;
        }

      }
    }
    // create array of exact size to fit these PANE references
    int[][] actualPaneRefs = new int[numRefs][];
    System.arraycopy(paneRefs, 0, actualPaneRefs, 0, numRefs);
    return actualPaneRefs;
  }

  /** Find replacement line for PANE with line reference to line that is being deleted
      @param i line being deleted
      @param paneWithReference PANE containing reference to line being deleted
      @param howBordersPane how the line borders this pane (BORD_TOP, BORD_BOTTOM,...)
      @return replacement line that the PANE is referencing  (-1 if no replacement found)
      @since NeXtMidas 3.5.4 Bug 2909
   */
  private int replacePaneLine (int i, Pane paneWithReference, int howBordersPane) {
    int replacementLine = -1;
    if (howBordersPane == BORD_TOP) {
       int lineClosestAbove = lineAbove(i);
       if (lineClosestAbove >= 0) {
         replacementLine = lineClosestAbove;
         paneWithReference.iy1 = replacementLine;
       }
    }
    else if (howBordersPane == BORD_BOTTOM) {
       int lineClosestBelow = lineBelow(i);
       if (replacementLine < 0 ) {
         replacementLine = lineClosestBelow;
         paneWithReference.iy2 = replacementLine;
       }
    }
    else if (howBordersPane == BORD_LEFT) {
      int lineClosestOnLeft = lineToLeft(i);
      if (lineClosestOnLeft >= 0) {
        replacementLine = lineClosestOnLeft;
        paneWithReference.ix1 = replacementLine;
      }
    }
    else if (howBordersPane == BORD_RIGHT) {
      int lineClosestOnRight = lineToRight(i);
      if (replacementLine < 0 && lineClosestOnRight >= 0) {
        replacementLine = lineClosestOnRight;
        paneWithReference.ix2 = replacementLine;
      }
    }
    return replacementLine;
  }

  /** Check the Line that may have a reference to the line being replaced
      If this line has a reference to that Line, replace that reference with a
      reference to the designated replacement line.

      Note: X and Y values for the Line being updated can remain the same, as the
      the X on a Vertical line remains the same and the boundaries on it are determined
      by its bounding lines (Y not important).  Similarly the Y on a horizontal
      line will remain the same and its length will just be extended by changing
      one of its bounding lines (so the X is not important)

     @param lineBeingReplaced the line that is being deleted, and needs all references to it changed
     @param replacementLine   the line that is taking the place of the line being deleted
     @param lineThatMayNeedReferencesChanged the line to check if it has references to the line being
                                             deleted, and if so have that line reference updated to
                                             refer to the replacement line
     @return line index that was replace or -1 if no replacement was made
     @since NeXtMidas 3.5.4  Bug 2909, Bug 2895
   */
  private int replaceLineReference(int lineBeingReplaced,int replacementLine, int lineThatMayNeedReferencesChanged) {
    int indexReplaced = -1;
    if (line[lineThatMayNeedReferencesChanged].i1 == lineBeingReplaced){
      line[lineThatMayNeedReferencesChanged].i1 = replacementLine;
      indexReplaced = 1;
    }
    else if (line[lineThatMayNeedReferencesChanged].i2 == lineBeingReplaced){
      line[lineThatMayNeedReferencesChanged].i2 = replacementLine;
      indexReplaced = 2;
    }
    return indexReplaced;
  }

  /** Find line that is just above the specified horizontal line
      @param indexHorizontalLine
      @return line number of line above or -1 if no line above found or -2 if this line is not horizontal
      @since NeXtMidas 3.5.4  Bug 2909
   */
  private int lineAbove (int indexHorizontalLine) {
    if (indexHorizontalLine > nline || (line[indexHorizontalLine].dir != HLINE)) {
      return -2; // invalid horizontal line
    }
    int lineX1 = line[indexHorizontalLine].x1;
    int lineX2 = line[indexHorizontalLine].x2;
    int lineY = line[indexHorizontalLine].y1;
    int[][] linesAbove = new int[nline][2];
    int nLinesAbove = 0;
    for (int j = 0; j <= nline; j++) {
      // Check if we have found a horizontal line that is above the supplied line
      // for the entire width of the supplied line
      if ((line[j].dir == HLINE) && (line[j].y1 < lineY) && (line[j].x1 <= lineX1) && (line[j].x2 >= lineX2)) {
        linesAbove[nLinesAbove][0] = j;
        linesAbove[nLinesAbove][1] = line[j].y1;
        nLinesAbove++;
      }
    }
    if (nLinesAbove == 0) {
      return -1;
    }
    int closestAbove = 0;
    for (int c = 1; c < nLinesAbove; c++ ) {
      if (linesAbove[c][1] > linesAbove[closestAbove][1]) {
        closestAbove = c;
      }
    }
    return linesAbove[closestAbove][0];
  }

  /** Find line that is just below the specified horizontal line
      @param indexHorizontalLine
      @return line number of line below or -1 if no line above found or -2 if this line is not horizontal
      @since NeXtMidas 3.5.4  Bug 2909
   */
  private int lineBelow (int indexHorizontalLine) {
    if (indexHorizontalLine > nline || (line[indexHorizontalLine].dir != HLINE)) {
      return -2; // invalid horizontal line
    }
    int lineX1 = line[indexHorizontalLine].x1;
    int lineX2 = line[indexHorizontalLine].x2;
    int lineY = line[indexHorizontalLine].y1;
    int[][] linesBelow = new int[nline][2];
    int nLinesBelow = 0;
    for (int j = 0; j <= nline; j++) {
      // Check if we have found a horizontal line that is above the supplied line
      // for the entire width of the supplied line
      if ((line[j].dir == HLINE) && (line[j].y1 > lineY) && (line[j].x1 <= lineX1) && (line[j].x2 >= lineX2)) {
        linesBelow[nLinesBelow][0] = j;
        linesBelow[nLinesBelow][1] = line[j].y1;
        nLinesBelow++;
      }
    }
    if (nLinesBelow == 0) {
      return -1;
    }
    int closestAbove = 0;
    for (int c = 1; c < nLinesBelow; c++ ) {
      if (linesBelow[c][1] < linesBelow[closestAbove][1]) {
        closestAbove = c;
      }
    }
    return linesBelow[closestAbove][0];
  }

  /** Find line that is just to the left of the specified vertical line
      @param indexVerticalLine
      @return line number of line to the left or -1 if no line to the left found or -2 if this line is not vertical
      @since NeXtMidas 3.5.4  Bug 2909
   */
  private int lineToLeft (int indexVerticalLine) {
    if (indexVerticalLine > nline || (line[indexVerticalLine].dir != VLINE)) {
      return -2; // invalid horizontal line
    }
    int lineY1 = line[indexVerticalLine].y1;
    int lineY2 = line[indexVerticalLine].y2;
    int lineX = line[indexVerticalLine].x1;
    int[][] linesOnLeft = new int[nline][2];
    int nLinesOnLeft = 0;
    for (int j = 0; j <= nline; j++) {
      // Check if we have found a horizontal line that is above the supplied line
      // for the entire width of the supplied line
      if ((line[j].dir == VLINE) && (line[j].x1 < lineX) && (line[j].y1 <= lineY1) && (line[j].y2 >= lineY2)) {
        linesOnLeft[nLinesOnLeft][0] = j;
        linesOnLeft[nLinesOnLeft][1] = line[j].x1;
        nLinesOnLeft++;
      }
    }
    if (nLinesOnLeft == 0) {
      return -1;
    }
    int closestOnLeft = 0;
    for (int c = 1; c < nLinesOnLeft; c++ ) {
      if (linesOnLeft[c][1] > linesOnLeft[closestOnLeft][1]) {
        closestOnLeft = c;
      }
    }
    return linesOnLeft[closestOnLeft][0];
  }

  /** Find line that is just to the right of the specified vertical line
      @param indexVerticalLine
      @return line number of line to the right or -1 if no line to the right found or -2 if this line is not vertical
      @since NeXtMidas 3.5.4  Bug 2909
   */
  private int lineToRight (int indexVerticalLine) {
    if (indexVerticalLine > nline || (line[indexVerticalLine].dir != VLINE)) {
      return -2; // invalid horizontal line
    }
    int lineY1 = line[indexVerticalLine].y1;
    int lineY2 = line[indexVerticalLine].y2;
    int lineX = line[indexVerticalLine].x1;
    int[][] linesOnRight = new int[nline][2];
    int nLinesToRight = 0;
    for (int j = 0; j <= nline; j++) {
      // Check if we have found a horizontal line that is above the supplied line
      // for the entire width of the supplied line
      if ((line[j].dir == VLINE) && (line[j].x1 > lineX) && (line[j].y1 <= lineY1) && (line[j].y2 >= lineY2)) {
        linesOnRight[nLinesToRight][0] = j;
        linesOnRight[nLinesToRight][1] = line[j].x1;
        nLinesToRight++;
      }
    }
    if (nLinesToRight == 0) {
      return -1;
    }
    int closestOnRight = 0;
    for (int c = 1; c < nLinesToRight; c++ ) {
      if (linesOnRight[c][1] < linesOnRight[closestOnRight][1]) {
        closestOnRight = c;
      }
    }
    return linesOnRight[closestOnRight][0];
  }

  private void fixLine (int i) {
    Line l = line[i]; int i1,i2;
    int wx1=0, wx2=MW.pos.w, wy1=ytop, wy2=MW.pos.h; 
    if (i<=4) {
      if ((borders&(1<<(i-1)))==0) { i1=0; i2=sbw+bw+bw; }
      else if (l.fp==0) i1=i2=0;
      else { i1=l.fp-sbw-bw; i2=l.fp+bw; }
      if (i==1) {
        if (is(HIDECONTROLS)) i1=i2=0;
        l.x1=i1; l.x2=i2;
        if ((borders&BORD_TOPLEFT)!=0) l.y1=line[3].y2; else l.y1=0;
        if ((borders&BORD_BOTTOMLEFT)!=0) l.y2=line[4].y1; else l.y2=MW.pos.h;
      }
      if (i==2) {
        l.x1=MW.pos.w-i2; l.x2=MW.pos.w-i1;
        if ((borders&BORD_TOPRIGHT)!=0) l.y1=line[3].y2; else l.y1=ytop; // new ytop top boundary is > 0 when menu bar active - NXM-3321
        if ((borders&BORD_BOTTOMRIGHT)!=0) l.y2=line[4].y1; else l.y2=MW.pos.h;
      }
      if (i==3) {
        l.y1=ytop+i1; l.y2=ytop+i2; // new ytop top boundary is > 0 when menu bar active - NXM-3321 - 3.9.1 correction
        if ((borders&BORD_TOPLEFT)!=0) l.x1=0; else l.x1=line[1].x2;
        if ((borders&BORD_TOPRIGHT)!=0) l.x2=MW.pos.w; else l.x2=line[2].x1;
      }
      if (i==4) {
        l.y1=MW.pos.h-i2; l.y2=MW.pos.h-i1;
        if ((borders&BORD_BOTTOMLEFT)!=0) l.x1=0; else l.x1=line[1].x2;
        if ((borders&BORD_BOTTOMRIGHT)!=0) l.x2=MW.pos.w; else l.x2=line[2].x1;
      }
    }
    else if (l.dir==HLINE) {
      l.x1 = (l.i1==0)? wx1 : getix(l.i1)+bw;
      l.x2 = (l.i2==0)? wx2 : getix(l.i2)-bw;
      l.y1 = getiy(i)-bw; 
      l.y2 = getiy(i)+bw;
      if (l.i1==2) l.x1+=bw;
      if (l.i2==1) l.x2-=bw;
    }
    else if (l.dir==VLINE) {
      l.y1 = (l.i1==0)? wy1 : getiy(l.i1)+bw;
      l.y2 = (l.i2==0)? wy2 : getiy(l.i2)-bw;
      l.x1 = getix(i)-bw;
      l.x2 = getix(i)+bw;
      if (l.i1==4) l.y1+=bw;
      if (l.i2==3) l.y2-=bw;
    }
  }

  /** Moves a line.
      @param i  The line index.
      @param x  The x coordinate (in pixels).
      @param y  The y coordinate (in pixels).
   */
  private void movLine (int i, int x, int y) {
    if (is(LOCKSETUP)) return;
    Line l = line[i];
    x = Math.max(0,Math.min(MW.pos.w,x));
    y = Math.max(0,Math.min(MW.pos.h,y));
         if (i==1) l.fp=Math.min(MW.pos.w-1,x); // 3.9.1 do not allow borders to go out of panel
    else if (i==2) l.fp=Math.max(1,MW.pos.w-x); // bounds or they can not be recovered
    else if (i==3) l.fp=Math.min(MW.pos.h-1,y);
    else if (i==4) l.fp=Math.max(1,MW.pos.h-y);
    else if (l.dir==HLINE) { l.ry = getry(x,y); if (l.fp!=0) l.fp=y; }
    else if (l.dir==VLINE) { l.rx = getrx(x,y); if (l.fp!=0) l.fp=x; }
    if (i>=1 && i<=4) borders |= (0x1<<(i-1));
    if (i==1 && curCompress!=0) {
      options &= ~COMPRESS;
      resize();
      refresh();
      resize();
      refresh();
      options |= COMPRESS;
    }
    if (menuBarEnabled && i==1 && (x <=0) ) { // since 3.9.0
      // This allows the Menu Bar to be restored when Pane 0 is re-collapsed
      setBorders(borders&~BORD_LEFT);
    }
    resize();
    refresh();
  }

  /** Indicates the line that is at a given point.
      @param x The x-coord.
      @param y The y-coord.
      @return The line at (x,y).
   */
  private int selLine (int x, int y) {
    for (int i=1; i<=nline; i++) {
      Line l=line[i]; if (l==null) continue;
      if (x>=l.x1 && x<=l.x2 && y>=l.y1 && y<=l.y2) {   // on the separator
        // don't allow select of the controls scroll bar
        if (i==1 && y>l.y2-sbw*4-vScroll) return -1;
        // setup is locked, no resize, but collapse is ok
        if (configure<=0 && i>4 && is(LOCKSETUP)) continue;
        if (configure<=0 && is(MINIDRAGZONE)) {
          if ((i==1 || i==2) && y>l.y1+4*sbw) continue;
          if ((i==3 || i==4) && x>l.x1+4*sbw) continue;
          if (i>4 && l.dir==HLINE && Math.abs(x-(l.x1+l.x2)/2)>sbw*2) continue;
          if (i>4 && l.dir==VLINE && Math.abs(y-(l.y1+l.y2)/2)>sbw*2) continue;
        }
        return i;
      }
    }
    return 0;
  }

  /** Indicates the pane that is at a given point.
      @param x The x-coordinate
      @param y The y-coordinate
      @return {The pane at (x,y) or 0 for none, button selected} - {pane,button} Since NeXtMidas 3.5.4 Bug 2895
   */
  private int[] selPane (int x, int y) {
    int[] paneButton = {0,BUTTON_SELECTED_OOR};  // default to no pane no button
    for (int i=1; i<=npane; i++) {
      Pane p=pane[i]; if (p==null) continue;
      int hit = p.clickHit(x,y);		// try this pane
      if (hit==BUTTON_SELECTED_OOR) continue;	// not in pane range
      paneButton[0] = i;
      paneButton[1] = hit;
      break;
    }
    return paneButton;
  }

  private void hovPane (int x, int y) {
    for (int i=1; i<=npane; i++) {
      Pane p=pane[i]; if (p==null) continue;
      if (p.hoverHit(x,y)>=0) break;		// try this pane
    }
  }

  /** Arrange the widget on the panel
      @param i The index of the widget in the {@link #widget} array
      @param mode The display mode. <br>
        -1 = set bounds even if widget is not shown. Used by initial call in {@link #addWidget(GWidget)}
         1 = do not set bounds if widget is not shown. Used by calls from {@link #layout()}
   */
  private void fixWidget (int i, int mode) {
    GWidget gw=widget[i];
    GLabel gwg=gw.pGroup;
    boolean isLabel=(gw instanceof GLabel);
    boolean show=(gw.isShow())&&(gwg==null || gwg.isShow()); // A hidden group means a hidden control
    boolean bar = menuBarEnabled && (borders&BORD_LEFT)==0; // for Menu Bar 3.9.0

    if (headless) { gw.display(-1); return; }

    // recalc rx,ry if this was moved by the user
    if (gw.rMoved && !is(LOCKCONTROLS)) {
      if (isLabel && bar); // for Menu Bar 3.9.0
      else setPos2RXY(gw);
      if (isLabel || gwg==null || gwg.rx>0);
      else if (gw.rx>0) { gw.pDetach=true; gw.setFlag(GWidget.INLINE,inlinx); }
      else if (gw.rx<0) { gw.pDetach=false; gw.setFlag(GWidget.INLINE,true); }
      gw.rMoved=false;
    }

    // no show by label activity ?
    if (gwg==null);
    else if (isLabel) show &= (gwg.getActive()!=GLabel.aHIDDEN);
    else if (!gw.pDetach) {
      show &= (gwg.getActive()>0);
      gw.rx=gwg.rx;
    }
    if (gw.rx<0 && !bar && gw.pos.w!=line[1].x1) { // for Menu Bar 3.9.0
      gw.setCompression(0);
      gw.setBounds(0,gw.pos.y,line[1].x1,gw.pos.h);
    }
    // Jeff's fix 3.9.0 menu bar scroll
    if (i<=iScroll && gw.rx<0) show = false; // if gw is out of range on the scroll position and in the left border (not detached), do not show
    // unless gw is detached, even if gW is "in range", should not show if using menu bar and GLabel is out-of-range
    if (bar && !isLabel && show && gwg!=null && gwg.getStatus()<0 && gw.rx<0) show=false;
    //if (!show) { if (mode>0) gw.display(-1); return; } // changed to block below for Bug 2217
    if (!show && mode>0 ) {
      gw.display(GWidget.ICON);
      return;
    }

    // no show by border activity ?
         if (gw.rx<0) show = (borders&BORD_LEFT)!=0 || bar; // add "|| bar" so active menus in menu bar show even when Pane 0 collapsed - NXM-3321
    else if (gw.rx>1) show = (borders&BORD_RIGHT)!=0;
    else if (gw.ry<0) show = (borders&BORD_TOP)!=0;
    else if (gw.ry>1) show = (borders&BORD_BOTTOM)!=0;
    if (!show && mode>0 ) {
      gw.display(GWidget.ICON);
      return;
    }

    // find the next row for an outside group entry
    if (gwg!=null && gwg!=gw && ((gwg.rx>0 && gwg.rx<1) || bar) && !gw.pDetach) { // adding "|| bar" for Menu Bar mode since 3.9.0
      int x = widget[iwz0].pos.x;
      int y = widget[iwz0].pos.y + widget[iwz0].pos.h;
      int w = widget[iwz0].pos.w;
      int h = gw.pos.h;
      gw.fixWidth(w);
      gw.setBounds(x,y,w,h);
      iwz0=i;
    }
    else if (isLabel && bar) { // gw is GLabel in menu bar - since 3.9.0 for NXM-3321
      gw.pos.w = Math.max(widthLabelInMenuBar,gw.pos.w); // go back to initial width if Pane 0 collapsed
      gw.pos.x=xbar; gw.pos.y=0; xbar+=gw.pos.w;
      gw.setBounds(gw.pos);
      iwz0=i; show=true;
    }
    else if (gw.rx<0) {    // find next row in left column
      int x = 0;
      int y = bbox[1].y;
      int w = line[1].x1;
      gw.fixWidth(w);
      // don't refresh during compression move to reduce flicker
      if (needCompress!=0) gw.eventFilter |= GWidget.NOREFRESH;
      else gw.eventFilter &= ~GWidget.NOREFRESH;
      gw.setCompression(curCompress);
      int h = gw.pos.h;
      if (iwz1>0) y = widget[iwz1].pos.y+widget[iwz1].pos.h;
      gw.setBounds (x,y,w,h);
      if ((borders&BORD_BOTTOMLEFT)!=0) show = y+h < ybott; // scrolled off-screen bottom
      else show = y+5 < ybott; // scrolled off-screen bottom - allow partial widgets
      if (show && needCompress==0 && prevCompress!=0) gw.refresh();
      yleft = y+h;
      nleft++;
      iwz1=i;
      if (isLabel && gwg.getActive()==GLabel.aOPEN) lastOpenedGLabel=gwg;
      if (isLabel && gwg.getActive()==GLabel.aCOLLAPSABLE) lastCollapsableGLabel=gwg;

    }
    else if (gw.rx>1) {    // find next row in right column
      int x = line[2].x2;
      int y = bbox[2].y;
      int w = MW.pos.w-line[2].x2;
      int h = gw.pos.h;
      if (iwz2>0) y = widget[iwz2].pos.y+widget[iwz2].pos.h;
      gw.fixWidth(w);
      gw.setBounds(x,y,w,h);
      show = y+h < bbox[2].y+bbox[2].h; // scrolled off-screen bottom
      iwz2=i;
    }
    else {     // if not in the left border, find scaled location
      setRXY2Pos(gw);
      gw.setBounds(gw.pos);
      if (isLabel) iwz0=i;
    }
    if (mode<0)    ;                // skip (show/hide)
    else if (show) gw.display(1);   // show widget
    else           gw.display(-1);  // hide widget
  }

  private void setBorderBoxes () {
    int bx1, bx2, by1, by2;
    // calc middle region
    bx1=line[1].x2;
    bx2=line[2].x1;
    by1=line[3].y2;
    by2=line[4].y1;
    bbox[0].setBounds(bx1,by1,bx2-bx1,by2-by1);
    // calc the left border
    bx1=0;
    bx2=line[1].x2;
    by1=ytop; if ((borders&BORD_TOPLEFT)!=0) by1=line[3].y2; // new ytop top boundary is > 0 when menu bar active - since 3.9.0 for NXM-3321
    by2=MW.pos.h; if ((borders&BORD_BOTTOMLEFT)!=0) by2=line[4].y1;
    bbox[1].setBounds(bx1,by1,bx2-bx1,by2-by1);
    // calc the right border
    bx1=line[2].x1;
    bx2=MW.pos.w;
    by1=ytop; if ((borders&BORD_TOPRIGHT)!=0) by1=line[3].y2; // new ytop top boundary is > 0 when menu bar active - since 3.9.0 for NXM-3321
    by2=MW.pos.h; if ((borders&BORD_BOTTOMRIGHT)!=0) by2=line[4].y1;
    bbox[2].setBounds(bx1,by1,bx2-bx1,by2-by1);
    // calc the top border
    bx1=0; if ((borders&BORD_TOPLEFT)==0) bx1=line[1].x2;
    bx2=MW.pos.w; if ((borders&BORD_TOPRIGHT)==0) bx2=line[2].x1;
    by1=ytop; // new ytop top boundary is > 0 when menu bar active - since 3.9.0 for NXM-3321
    by2=line[3].y2;
    bbox[3].setBounds(bx1,by1,bx2-bx1,by2-by1);
    // calc the bottom border
    bx1=0; if ((borders&BORD_BOTTOMLEFT)==0) bx1=line[1].x2;
    bx2=MW.pos.w; if ((borders&BORD_BOTTOMRIGHT)==0) bx2=line[2].x1;
    by1=line[4].y1;
    by2=MW.pos.h;
    bbox[4].setBounds(bx1,by1,bx2-bx1,by2-by1);
  }

  private void setPos2RXY (GWidget gw) {
    int x = gw.pos.x + gw.pos.w/2;
    int y = gw.pos.y + gw.pos.h/2;
    int i;
    for (i=0; i<5; i++) { if (bbox[i].within(x,y)) break; }
    if (i==5) i=0; // malformed - assume in middle region
    gw.rx = (x-bbox[i].x)/(double)bbox[i].w;
    gw.ry = (y-bbox[i].y)/(double)bbox[i].h;
    gw.pane = null;
    if (i==0) for (int j=1; j<=npane; j++) { 
      Pane p = pane[j]; 
      if (p.pos.within(x,y)) {
	gw.pane=p; 
        int w = fixByOffset? 10000 : p.pos.w, h = fixByOffset? 10000 : p.pos.h;
        gw.rx = (x-p.pos.x)/(double)w;
        gw.ry = (y-p.pos.y)/(double)h;
	break;
      }
    }
    if (i==1) gw.rx-=1;
    if (i==2) gw.rx+=1;
    if (i==3) gw.ry-=1;
    if (i==4) gw.ry+=1;
    //System.out.println("SetPos2RXY "+gw+" "+i+" "+x+" "+y+" "+gw.rx+" "+gw.ry+" "+gw.pane);
  }

  private void setRXY2Pos (GWidget gw) {
    int i=0; double rx=gw.rx, ry=gw.ry;
         if (rx<0) { i=1; rx+=1; }
    else if (rx>1) { i=2; rx-=1; }
    else if (ry<0) { i=3; ry+=1; }
    else if (ry>1) { i=4; ry-=1; }
    MBox pos = (gw.pane!=null)? ((Pane)gw.pane).pos : bbox[i];
    int w=pos.w, h=pos.h;
    if (i==0 && fixByOffset) { w=h=10000; }
    gw.pos.x = (int)(rx*w) + pos.x - gw.pos.w/2;
    gw.pos.y = (int)(ry*h) + pos.y - gw.pos.h/2;
  }

  /** Setup config when using /TSETUP */
  private Table configTSetup () {
    Table setupTable =  MA.getTable("/TSETUP",null,Args.NO_ERROR);   // get from /tsetup switch, if given

    if (M.results.getTable(MA.getS("/TSETUP")) != null) localResult = true;   // check for it in local results table
    if (setupTable == null) {    // check to see if parent result
      try {
        setupTable = M.results.getPrevious().getTable(MA.getS("/TSETUP") );
        if (setupTable != null) parentResult = true;
      } catch (Exception e) { e.printStackTrace(); }
    }
    String setupTableFileName = MA.getS("/TSETUP", null);
    if (setupTableFileName == null || setupTableFileName.equals("")) { setConfigFileName(TSETUP); } // default name
    else {
      cfgfile = setupTableFileName.toLowerCase();
      if (M.macro != null) titlebar = M.macro.name; // since 3.5.3 for BUG 2891
    }
    if (setupTable == null) {
      setupTable = Convert.o2t(cfgfile,M);   // look for setupTable as a file
    }
    return setupTable;
  }

  private void setConfigFileName (int setupType) {
    if (M.macro!=null) {
      cfgfile = M.macro.getFileName();
      titlebar = M.macro.name;
    } else {
      Args targs = Shell.parseCommand(M,cfgfile);
      String option = targs.option.toLowerCase();
      cfgfile = Macro.getFileName(M,cfgfile,option);
    }
    if (setupType==TSETUP) cfgfile = cfgfile.substring(0,cfgfile.lastIndexOf('.'))+".tbl";  // make a .tbl file name
    if (setupType==SETUP ) cfgfile = cfgfile+"p"+MA.getS("/SETUP").toLowerCase();           // make a .mmp file name

  }

  /** Saves an MMP or a TBL file. */
  private void save (String fname) {
    String text;
    String base = fname.toLowerCase();
    if (base.endsWith(".tbl")) { saveSetupTable(fname); return; }  // save as a TBL file
    TextFile tf = new TextFile(M,fname);
    if (!tf.open(TextFile.OUTPUT|TextFile.NOABORT)) {
      M.info("Could not open setup file: "+tf.getURL());
      return;
    }
    if (MW != null) {
      text = "{TYPE=POS,X="+MW.pos.x+",Y="+MW.pos.y+",W="+MW.pos.w+",H="+MW.pos.h+"}";
      tf.writeln(text);
    }
    boolean menubarMode = menuBarEnabled && (borders&BORD_LEFT)==0;
    boolean bordersNotDefault = (menubarMode) ?
        borders!=MENUBAR_BORDERS_DEFAULT : borders!=BORDERS_DEFAULT;
    if (bordersNotDefault) {
      text = "{TYPE=BORDERS,MASK="+Convert.l2x(borders)+"}";
      tf.writeln(text);
    }
    for (int il=1; il<=nline; il++) {
      Line l = line[il];
      text = "{TYPE=LINE,INDEX="+il+",DIR="+l.dir+",X="+l.rx+",Y="+l.ry+
          ",I1="+l.i1+",I2="+l.i2+",FP="+l.fp+"}";
      tf.writeln(text);
    }
    for (int ip=1; ip<=npane; ip++) {
      Pane p = pane[ip];
      if (p==null) continue;
      text = "{TYPE=PANE,INDEX="+ip+",NAME="+p.name+",TITLE="+p.title+",TH="+p.ts+
          ",IX1="+p.ix1+",IX2="+p.ix2+ ",IY1="+p.iy1+ ",IY2="+p.iy2+"}";
      tf.writeln(text);
    }
    for (int iw=1; iw<=nwidget; iw++) {
      GWidget w = widget[iw];
      if (w==null) continue;
      if (w.pos.x==0) continue;
      if (w.pGroup!=null && w.pGroup!=w && w.pDetach==false) continue;
      if (menubarMode && (w instanceof GLabel)) continue;
      text = "{TYPE=CPOS,NAME="+w.label+",X="+w.pos.x+",Y="+w.pos.y+
          ",W="+w.pos.w+",H="+w.pos.h+"}";
      tf.writeln(text);
    }
    M.info("Saved file to: "+tf.getURL());
    tf.close();
  }

  /** Loads an MMP file. */
  private void load (String fname) {
    String text;
    TextFile tf = new TextFile(M,fname);
    tf.open(TextFile.INPUT|TextFile.NOABORT);
    if (!tf.isOpen) { M.info("Could not open setup file: "+tf.getURL()); return; }
    while ( (text=tf.readProper()) != null) {
      Table t = new Table(text);
      loadSetupLine(t);
    }
    tf.close();
    // Width of labels in menu bar is based on width of Pane 0 in mmp - since 3.9.0
    widthLabelInMenuBar = line[1].fp - sbw - bw;
    // initial state of logger if not defined is opened
    if (logger && line[4].fp==0) line[4].fp=dlh;
  }

  /** Loads a single line from the setup file/table.
      This should only be called from load(..) and loadTableSetup(..).
   */
  private void loadSetupLine(Table t) {
      String type = t.getS("TYPE");
      String name = t.getS("NAME");
      // if the logger pane is already defined, do not add it again in open() (since 3.1.3 - Bug 2623)
      if (name!=null && name.equals("LOGGER")) addNewLoggerPane = false;
      int i = t.getL("INDEX",0);
      if (mobile>0) {
        if (type.equals("PANE")) addPane (i,t.getS("NAME"),t.getS("TITLE"),t.getS("ID"),10,1,2,3,4,0);
      }
      else if (type.equals("PANE")) {
        addPane (i,t.getS("NAME"),t.getS("TITLE"),t.getS("ID"),t.getL("TH"),
          t.getL("IX1"),t.getL("IX2"),t.getL("IY1"),t.getL("IY2"),t.getL("FLAG",0));
      }
      else if (type.equals("GRID")) {
        autoGrid(t);
      }
      else if (type.equals("LINE")) {
        addLine (i,t.getL("DIR"),t.getF("X"),t.getF("Y"),
          t.getL("I1"),t.getL("I2"),t.getL("FP"),t.getL("FLAG",0));
      }
      else if (type.equals("POS")) {
        // Check setup file Position to ensure it can be displayed on the screen. (Bug 1398)
        int xpos   = t.getL("X");
        int ypos   = t.getL("Y");
        int width  = t.getL("W");
        int height = t.getL("H");
        if (!headless) { // Since 3.5.0. Skip if in Headless state. BUG 2817
          Rectangle screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().
                                   getDefaultScreenDevice().getDefaultConfiguration().getBounds();
          int maxX = screenBounds.x + screenBounds.width;         // right edge of screen. adds in Xinerama values
          int maxY = screenBounds.y + screenBounds.height;        // bottom edge of screen.
          if (width  > maxX) width  = maxX;               // keep width no larger than screen
          if (height > maxY) height = maxY;               // keep height no larger than screen
          if ((xpos+width)  > maxX) xpos = maxX - width;  // if x off screen, move back on
          if ((ypos+height) > maxY) ypos = maxY - height; // if y off screen, move back on
          MW.setBounds(xpos,ypos,width,height);
        }
      }
      else if (type.equals("CPOS")) {
        MBox pos = new MBox(t.getL("X"),t.getL("Y"),t.getL("W"),t.getL("H"));
        cpos.put(name,pos);
      }
      else if (type.equals("BORDERS")) {
        borders = t.getL("MASK");
      }
      else M.warning("Unknown table type "+type+" in setup file");
  }

  /** Loads the setup from a table.
      @param t The table containing the setup.
   */
  public void setSetupTable (Table t) {
    Table.Iterator iter = t.iterator();
    while (iter.hasNext()) {
      loadSetupLine(t.getTable(iter.next().toString(),true));
    }
    // Width of labels in menu bar is based on width of Pane 0 in tsetup - since 3.9.0
    widthLabelInMenuBar = line[1].fp - sbw - bw;

    // initial state of logger if not defined is opened
    if (logger && line[4].fp==0) line[4].fp=dlh;
  }

  /** Gets the current setup as a table.
   *  @return current setup in table form
   */
  public Table getSetupTable () {
    Table tbl = new Table();
    if (MW != null) {
      tbl.put("POS", "{TYPE=POS,X="+MW.pos.x+",Y="+MW.pos.y+",W="+MW.pos.w+",H="+MW.pos.h+"}");
    }
    boolean menubarMode = (menuBarEnabled && (borders&BORD_LEFT)==0);
    boolean bordersNotDefault = (menubarMode) ?
        borders!=MENUBAR_BORDERS_DEFAULT : borders!=BORDERS_DEFAULT;
    if (bordersNotDefault) {
      tbl.put("BORDERS", "{TYPE=BORDERS,MASK="+Convert.l2x(borders)+"}");
    }
    for (int il=1; il<=nline; il++) {
      Line l = line[il];
      tbl.put("LINE_"+il, "{TYPE=LINE,INDEX="+il+",DIR="+l.dir+",X="+l.rx+",Y="+l.ry+
                          ",I1="+l.i1+",I2="+l.i2+",FP="+l.fp+"}");
    }
    for (int ip=0; ip<=npane; ip++) {
      Pane p = pane[ip];
      if (p!=null) {
        tbl.put("PANE_"+ip, "{TYPE=PANE,INDEX="+ip+",NAME="+p.name+",TITLE="+p.title+",TH="+p.ts+
                            ",IX1="+p.ix1+",IX2="+p.ix2+ ",IY1="+p.iy1+ ",IY2="+p.iy2+"}");
      }
    }
    for (int iw=1; iw<=nwidget; iw++) {
      GWidget w = widget[iw];
      if ((w!=null) && (w.pos.x!=0) && w.pGroup==null && w.pGroup==w && w.pDetach!=false &&
           !(menubarMode && (w instanceof GLabel))) {
        tbl.put("WIDGET_"+iw, "{TYPE=CPOS,NAME="+w.label+",X="+w.pos.x+",Y="+w.pos.y+
                              ",W="+w.pos.w+",H="+w.pos.h+"}");
      }
    }

    return tbl;
  }

  /** Save a setup table that is a result variable
      @param resName The name of the result
      @since NeXtMidas 2.9.0
   */
  public void saveResult (String resName) {
    String loc = "";
    if (parentResult || (!parentResult && !localResult)) {
      M.results.getPrevious().put(resName.toUpperCase(), getSetupTable());  // save result to Parent Midas context
      loc = "parent";
    }
    if (localResult) {
      M.results.put(resName.toUpperCase(), getSetupTable());           // save result in local Midas context
      loc = "local";
    }
    M.info("Saved result "+resName+" to "+loc+" results table.");
  }

  /** Saves a TBL file.
   *  @param fname file name for table file
   */
  public void saveSetupTable (String fname) {
    TextFile tf = new TextFile(M,fname);
    tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
    Table.toTextFile(tf,getSetupTable(),"");
    tf.close();
    //getSetupTable().toTextFile(tf);  // changed to lines above per BUG 2189 in 2.9.0
    M.info("Saved setup table to: "+tf.getURL());
  }

  private void autoGrid (Table t) {
    int nx = t.getL("NX",4);
    int ny = t.getL("NY",4);
    int nx2 = t.getL("NX2",nx); // allow autogrid of 2 rows with different # of columns NXM-2755
    String name = t.getS("NAME","GP");
    String title = t.getS("TITLE","Pane");
    int ts = t.getL("TH",10);
    String fill = t.getS("FILL","LRTB");

    // Jeff contributed PX (percentage based layout) NXM-4029
    Data dpx = t.getData("PX"); if (dpx!=null) dpx = Data.fromString(dpx.toString().replace("|",","),'D',M);
    Data dpx2 = t.getData("PX2"); if (dpx2!=null) dpx2 = Data.fromString(dpx2.toString().replace("|",","),'D',M);
    float[] px = new float[nx]; px[0]=0.0F;
    float[] px2 = new float[nx2]; px2[0]=0.0F;
    for (int ix=0; ix<nx; ix++) px[ix] = (float)ix/nx;
    for (int ix=0; ix<nx2; ix++) px2[ix] = (float)ix/nx2;
    if (dpx!=null) for (int ix=1; ix<nx; ix++) px[ix] = px[ix-1]+(float)dpx.getD(ix-1);
    if (dpx2!=null) for (int ix=1; ix<nx2; ix++) px2[ix] = px2[ix-1]+(float)dpx2.getD(ix-1);

    nline=4; // erase other lines first
    if (nx2==nx) {
      for (int ix=1; ix<nx; ix++) addLine(-1,VLINE,px[ix],0.5F,3,4,0,0);
      for (int iy=1; iy<ny; iy++) addLine(-1,HLINE,0.5F,(float)iy/ny,1,2,0,0);
    } else {
      for (int iy=1; iy<ny; iy++) addLine(-1,HLINE,0.5F,(float)iy/ny,1,2,0,0);
      for (int ix=1; ix<nx; ix++) addLine(-1,VLINE,px[ix],0.5F,3,3+ny,0,0);
      for (int ix=1; ix<nx2; ix++) addLine(-1,VLINE,px2[ix],0.5F,3+ny,4,0,0);
    }

    int ip=0; // add panes
    if (mobile!=0);
    else if (nx2!=nx) {
      for (int iy=1; iy<=ny; iy++) {
        int iy1=3; if (iy>1) iy1=4+iy-1;
        int iy2=4; if (iy<ny) iy2=4+iy;
        int nxx = (iy==ny)? nx2:nx;
        for (int ix=1; ix<=nxx; ix++) {
          int ix1=1; if (ix>1) ix1=4+ny-1+ix-1 + ((iy==ny)?nx-1:0);
          int ix2=2; if (ix<nxx) ix2=4+ny-1+ix  + ((iy==ny)?nx-1:0);
          ip++; addPane (ip,name+ip,title+ip,id+ip,ts,ix1,ix2,iy1,iy2,0);
        }
      }
    }
    else if (fill.equals("TBLR")) {
      for (int ix=1; ix<=nx; ix++) {
        int ix1=1; if (ix>1) ix1=4+ix-1;
        int ix2=2; if (ix<nx) ix2=4+ix;
        for (int iy=1; iy<=ny; iy++) {
          int iy1=3; if (iy>1) iy1=4+nx-1+iy-1;
          int iy2=4; if (iy<ny) iy2=4+nx-1+iy;
          ip++; addPane (ip,name+ip,title+ip,id+ip,ts,ix1,ix2,iy1,iy2,0);
        }
      }
    }
    else if (fill.equals("LRTB")) {
      for (int iy=1; iy<=ny; iy++) {
        int iy1=3; if (iy>1) iy1=4+nx-1+iy-1;
        int iy2=4; if (iy<ny) iy2=4+nx-1+iy;
        for (int ix=1; ix<=nx; ix++) {
          int ix1=1; if (ix>1) ix1=4+ix-1;
          int ix2=2; if (ix<nx) ix2=4+ix;
          ip++; addPane (ip,name+ip,title+ip,id+ip,ts,ix1,ix2,iy1,iy2,0);
        }
      }
    }
    gnx = nx;
    gny = ny;
  }

  private void updateLastWidget() {
    for (int i=1; i<=nwidget; i++) {
      GWidget gw = widget[i];
      if (gw==null || gw.pGroup==null) continue;
      if (i==nwidget || widget[i+1] instanceof GLabel) {
        gw.pGroup.lastWidget = gw;
        gw.refresh();
      }
    }
  }

  @Override
  public Object addWidget (GWidget gw) {
    if (gw==null) return null;
    if (isHideWidgets()) return null;
    if (!cntrls.containsKey(gw.label)) {
      cntrls.put(gw.label,gw);
    }
    if (gw instanceof GLabel) curGLabel=(GLabel)gw;
    GLabel gwg = curGLabel;
    int i = ++nwidget;
    // insert this GWidget gw to the end the matching GLabel group & shift follow-on GWidgets
    if (gw.group != null) {
      for (i=1; i<nwidget && (widget[i].group==null || !widget[i].group.equals(gw.group)); i++);
      gwg = (GLabel)widget[i]; // not adding to curGLabel
      for (;i<nwidget && widget[i].group.equals(gw.group); i++);
      for (int j=nwidget; j>i; j--) widget[j]=widget[j-1];
    }
    if (gwg!=null) { gw.pGroup=gwg; gw.setGroup(gwg.label); } //gwg.lastWidget=gw; }
    widget[i] = gw;
    if (!widget[i].hasLocalTheme()) gw.setTheme(theme);  // enabled this in 3.3.0
    gw.setFlag(GWidget.INLINE,inline);
    gw.setWidgetPanel(this);
    MBox pos = (MBox)cpos.get(gw.label);
    if (pos!=null) {
      gw.pos.setBounds(pos); setPos2RXY(gw);
      if (gwg!=null && gwg.rx<0) gw.pDetach=true;
    } else if (gwg!=null && gw!=gwg) { // i.e. in a label group, but not the GLabel
      gw.rx=gwg.rx;
    } else {
      gw.rx=-1;
    }
    gw.parent=gw.MW=MW;
    if (headless) return gw;
    gw.resize(1);
    fixWidget(i,-1); // default placement in left border.
    MW.addOn(gw.panel);
    if (gw instanceof GPanel) panes.put(gw.label,gw);
    if (gw instanceof GLabel && hbar==0) hbar = gw.pos.h; // horizontal menu bar width is set by height of GLabel - since 3.9.0 for NXM-3321
    return gw;
  }

  @Override
  public void layoutWidget (GWidget gw) {
    needLayout = true;
  }

  @Override
  public boolean accept (Object entry) {
    boolean rmif = entry instanceof nxm.sys.prim.rmif;
    boolean remote = entry instanceof nxm.sys.net.Rmif.Remote;
    boolean command = entry instanceof nxm.sys.lib.Command;
    if (filterMode==1 && command) return true;
    if (filterMode==2 && rmif) return true;
    if (filterMode==2 && remote) return true;
    return false;
  }

  /** get a timer value by index
   *  @param i index of timer
   *  @return timer value
   */
  public double getTimer (int i) {
    if (i < 0 || i >= nTimers) {
      throw new MidasException("Invalid timer index = "+i);
    }
    return timer[i];
  }

  /** Set the number of timers.  Initializes (RESETS) timer arrays
   *  @param in_nTimers number of timers
   */
  public void setTimers (int in_nTimers) {
    if (in_nTimers < 0 ) {
      M.error ("Invalid number of timers = "+in_nTimers);
    } else {
      nTimers = in_nTimers;
      if (nTimers == 0) {
        timer = null;
        timerinit = null;
      } else {
        timer = new double[nTimers];
        timerinit = new double[nTimers];
      }
    }
  }

  /** 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 MWindow#cursorList}), cant be "CUSTOM".
      @param sec  The number of seconds to wait.
   */
  public void setCursor (String name, double sec) {
    if (MW != null) {
      MW.setCursor(name, sec);
      if (userCursor != null) userCursor = getCursor();
    } else {
      M.warning("PANEL: Can not set cursor, MW is null");
    }
  }

  /**  Set alternate title
   *  @param title alternate title */
  public void setAltTitle (String title) {
    altTitle = title;
  }

  private void refreshTitle () {
    if (altTitle==null) {
      if (currentTitle==null) return;
    }
    if (altTitle!=null) {
      if (currentTitle==null);
      else if (altTitle.equals(currentTitle)) return;
    }
    currentTitle = altTitle;
    ((Frame)MW.parent).setTitle(currentTitle);
  }


  /** Sets the mouse cursor when over the panel to a given cursor type.
      @param name The name of the cursor to set (see {@link MWindow#cursorList}), cant be "CUSTOM".
   */
  public void setCursor (String name) {
    if (MW != null) {
      MW.setCursor(name);
      if (userCursor != null) userCursor = getCursor();
    } else {
      M.warning("PANEL: Can not set cursor, MW is null");
    }
  }

  /** Sets the cursor when in the proper location to resize.
      @param dir The direction from lines[i].dir or -1 to reset to normal.
   */
  private void setResizeCursor (int dir) {
    if (dir > 0) {
      if (userCursor == null) userCursor = getCursor();
      MW.setCursor( (dir==VLINE)? "WResize" : "SResize");
    }
    else if (userCursor != null) {
      MW.setCursor(userCursor);
      userCursor = null;
    }
  }

  /** Gets the current mouse cursor.
      @return The current mouse cursor, may be "CUSTOM".
   */
  public String getCursor () {
    return (MW != null)? MW.getCursor() : "Default";
  }

  /** set a timer value by index
   *  @param i     timer index
   *  @param value value to set
   */
  public void setTimer (int i, double value) {
    if (i < 0 ) {
      M.error ("Invalid timer index = "+i);
    } else {
      timer[i]=value;
      timerinit[i]=Time.current();
    }
  }

  /** Add MWindow
   *  @param mw MWindow to add */
  public void addWindow (MWindow mw) {
    int n = ++npane;
    mw.MW = MW;
    if (pane[n]==null) addPane(n,""+n,""+n,""+n, 10, 1,2,3,4, 0);
    pane[n].add(mw);
  }

  /**Helper method to prepare the menubar Only use in menubar mode
   * @since NeXtMidas 3.9.0
   */
  private void prepBar() {
    for (int i=0; i<nwidget; i++) {
      if (!(widget[i] instanceof GLabel)) continue;
      GLabel gwg = (GLabel)widget[i];
      if (gwg.getActive()==GLabel.aOPEN) gwg.setActive(GLabel.aCLOSED);
    }
  }

  /** Control borders on the panel (see {@link #bordersList})
   *  @param str String description of borders
   */
  public void setBorders (String str) {
    setBorders(Parser.mask(bordersList,str,borders));
  }
  /** Get the panel borders mask as a string (see {@link #bordersList})
   *  @return borders mask as a String
   */
  public String getBordersString () {
    return Parser.mask2s(bordersList,borders);
  }

  /** Users should use (see {@link #getBordersString()})
   *  @return mask describing the borders
   */
  public double getBorders () { return borders; }
  /** Users should not use this method.  Users should use (see {@link #setBorders(String)})
      @param value borders setting
    */
  public void setBorders (int value) {
    if (menuBarEnabled && (value&BORD_LEFT)==0 && (borders&BORD_LEFT)!=0) prepBar(); // prep Menu Bar if active - since 3.9.0 for NXM-3321
    borders=value; resize(); refresh();
  }

  /** Users should not use this method.  Users can toggle borders options with
    a <code>~</code> before the borders option to toggle.  (see {@link #setBorders(String)})
    @param value borders to toggle (XOR)
  */
  public void setToggleBorders (int value) { setBorders(borders^value); }


  /** Get controls in Table form
   *  @return controls in Table form */
  public Table getControls () { return cntrls; }
  /** Get panes in Table form
   *  @return panes in Table form */
  public Table getPanes () { return panes; }

  /** Gets a new array of Panes
   *  @return the new array of Panes
   */
  public Pane[] getPanesArray() {
    Pane[] pa = new Pane[npane+1];
    for (int i=0; i<=npane; i++) pa[i]=pane[i];
    return pa;
  }

  private int lbu=0, lbm=0, ldp=-1;
  /** Handles mobile service requests
   *  @param name name of request
   *  @return byte-array output stream
   */
  public java.io.ByteArrayOutputStream handleMobileRequest (String name) {
    //System.out.println("HandleMobileRequest: "+name);
    int iarg = name.indexOf('?');
    if (iarg>0) {
      Table t = new Table("{"+name.substring(iarg+1)+"}");
      String type=t.getS("TYPE");
      int x=t.getL("X"), y=t.getL("Y"), ip=t.getL("IP");
      Container c = (ip==0)? MW.panel : pane[ip].panel;
      while (c!=null) {
        x += c.getLocation().x;
        y += c.getLocation().y;
        c = c.getParent();
      }
      if (type.equals("MOVE")) {
        robot.mouseMove(x,y);
      }
      else if (type.equals("PRESS")) {
        robot.mouseMove(x,y);
        int bu = t.getL("B");
        int ab = Math.abs(bu);
        int bm = (ab==3)? InputEvent.BUTTON3_DOWN_MASK : (ab==2)? InputEvent.BUTTON2_DOWN_MASK : InputEvent.BUTTON1_DOWN_MASK; // Updated in 3.9.0 to use extended mods
        if (bm!=lbm && lbu>0)  robot.mouseRelease(lbm); // auto finish last button
        if (bu>0 || lbu<=0) robot.mousePress(bm);
        if (bu<0) robot.mouseRelease(bm);
        lbu = bu; lbm = bm;     // last Button
      }
      else if (type.equals("KEY")) {
        if (ldp != ip) { // get focus
          robot.mouseMove(x,y);
          robot.mousePress(InputEvent.BUTTON3_DOWN_MASK); // Updated in 3.9.0 to use extended mods
          robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK); // Updated in 3.9.0 to use extended mods
          ldp = ip;
        }
        String keys = t.getS("K");
        while (keys.length()>0) {
          char ch = keys.charAt(0);
          int key = (int)ch;
          if (ch>='a' && ch<='z') key -= 32;
          robot.keyPress(key);
          robot.keyRelease(key);
          keys = keys.substring(1);
        }
        int key = KeyEvent.VK_ENTER;
        robot.keyPress(key);
        robot.keyRelease(key);
      }
      name = name.substring(0,iarg);
    }
    if (name.startsWith("DisplayPane")) {
      int ip = (name.charAt(11)-'0');
      Container p = (ip==0)? MW.panel : pane[ip].panel;
      Rectangle rect = p.getBounds();
      int x=p.getX(), y=p.getY(), w=p.getWidth(), h=p.getHeight();
      try {
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        BufferedImage image = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
        Graphics gc = image.getGraphics();
        p.paint(gc);
        for (int i=0; i<MW.panel.getComponentCount(); i++) {
          Object o = MW.panel.getComponent(i);
          if (o instanceof MPanel) {
            MPanel mp = (MPanel)o;
            Rectangle r=mp.getBounds();
            if (mp.MW instanceof GWidget && rect.contains(r.getX(),r.getY())) {
              gc.translate((int)r.getX()-x,(int)r.getY()-y);
              mp.paint(gc);
            }
          }
        }
        ImageIO.write(image, "png", baos);
        //System.out.println("Image pane="+ip+" size="+baos.size());
        return baos;
      }
      catch (Exception e) { M.printStackTrace("Unable to export image: ", e); }
    }
    return null;
  }

  /** Add a new Pane to the current list of Panes
   *  @param i     the index
   *  @param name  the name of the Pane
   *  @param title the title of the Pane
   *  @param id    the id of the Pane
   *  @param ts    the text size
   *  @param ix1   the first x-coordinate
   *  @param ix2   the second x-coordinate
   *  @param iy1   the first y-coordinate
   *  @param iy2   the second y-coordinate
   *  @param flags the flags of the Pane
   */
  public void addPane (int i, String name, String title, String id, int ts,
                       int ix1,int ix2, int iy1,int iy2, int flags) {
    if (i<0) i=npane+1;
    Pane p = pane[i];
    if (p==null) p = pane[i] = new Pane(this);
    p.name = name;
    p.title= title;
    // shorten names for mobile tabbed screens
    if (mobile!=0 && ix1!=0 && ix2!=0 && iy1!=0 && iy2!=0) p.tag = ""+i;
    p.ts   = ts;
    p.ix1  = ix1;
    p.ix2  = ix2;
    p.iy1  = iy1;
    p.iy2  = iy2;
    p.flags = flags;
    panes.put(p.name,p);
    npane = Math.max(npane,i);
  }

  /** Add a new Panel Line
   *  @param i     the index
   *  @param dir   the Line direction
   *  @param rx    the x-ratio relative to surrounding vertical Lines (depending on dir)
   *  @param ry    the y-ratio relative to surrounding horizontal Lines (depending on dir)
   *  @param i1    the first Line this Line is between
   *  @param i2    the second Line this Line is between
   *  @param fp    the margin between Panel Line and matching Line/Panel boundary
   *  @param flags the Line flags
   */
  public void addLine (int i, int dir, float rx, float ry, int i1, int i2, int fp, int flags) {
    if (i<0) i=nline+1;
    Line l = line[i];
    if (l==null) l=new Line();
    l.dir = dir;
    l.rx  = rx;
    l.ry  = ry;
    l.i1  = i1;
    l.i2  = i2;
    l.fp  = fp;
    l.flags = flags;
    line[i] = l;
    nline = Math.max(nline,i);
  }

  /** An inner class for Panel Lines */
  public class Line implements MessageHandler {
    float rx,ry;
    /** lines this line is between */
    int i1,i2;
    int x1,x2;
    int y1,y2;
    int w;
    /** line direction 1-vertical, 2-horizontal */
    int dir;
    /** margin between panel line and matching line/panel boundary */
    int fp=0;
    int flags=0;

    /** default constructor */
    public Line () {}

    @Override
    public int processMessage (Message msg) {
      if (msg.name.equals("CFG.LINE")) {
        String text = msg.getS();
        if (text.equals("DELETE")) delLine(this);
        else if (text.equals("FIX")) { if (dir==VLINE) fp=(x1+x2)/2; else fp=(y1+y2)/2; }
        else if (text.equals("UNFIX")) fp=0;
      }
      return 1;
    }

    @Override
    public String toString() {
      return "Line rx:"+rx+" ry:"+ry+" i1:"+i1+" i2:"+i2+" x1:"+x1+" x2:"+x2+" y1:"+y1+" y2:"+y2+" w:"+w+" dir:"+dir+" fp:"+fp+" flags:"+flags;
    }
  } // end inner class Line

  String iconDir = "";
  public void setIconDir (String s) {
    if (!s.endsWith("/")) s += '/';
    iconDir = s;
  }
  private Icon getIcon (String fname, int size) {
    try {
      ImageIcon ii = new ImageIcon(iconDir+fname);
      Image img = ii.getImage().getScaledInstance(size,size,Image.SCALE_SMOOTH);
      Icon icon = new ImageIcon(img);
      return icon;
    } 
    catch (Exception e) {
      throw new MidasException("Symbol: Unable to read '"+fname+"'", e);
    }
  }

  /** A pane in a panel. */
  public class Pane implements MessageHandler {
    private final panel pan;

    /** name of Pane */
    public String name;
    /** title of Pane */
    public String title;
    /** tag of Pane */
    public String tag;
    /** the top-level Panel's Midas Window */
    public MWindow MW;
    /** the Pane's Midas Window */
    public MWindow mw;
    /** the Pane's Java graphics panel */
    public Container panel;
    /** left vertical Panel line */
    public int ix1=1;
    /** right vertical Panel line */
    public int ix2=2;
    /** bottom horizontal Panel line */
    public int iy1=3;
    /** top horizontal Panel line */
    public int iy2=4;
    /** x-coordinate of the Pane */
    public int x;
    /** y-coordinate of the Pane */
    public int y;
    /** width of Pane */
    public int w;
    /** height of Pane */
    public int h;
    /** text size */
    public int ts=0;
    /** flags of the Pane */
    public int flags=0; // seem to be unused and always 0
    /** Is the Pane hidden? */
    public boolean hidden=false;
    /** the Pane's tab manager */
    public TabManager tabManager = null;
    /** Midas Box representing the Pane area */
    public MBox pos = new MBox();
    /** iconbar of Pane */
    private String[] iconKeys;	// Icon message names
    private Icon[] icons;	// Actual icons
    private int[] iconOffs;	// pixel offsets
    private int nicons=0,iconCur=0;// total icons and current hilite

    /** @deprecated Since NeXtMidas 2.5.4: Only PANEL should be creating instances of Pane.
     *  @param MW Panel's MW
     */
    @Deprecated public Pane (MWindow MW) {
      this.MW  = MW;
      this.pan = null;
    }

    /** Creates a new instance.
     *  @param p     panel associated with this pane
     *  @param title title for this pane
     */
    Pane (panel p, String title) {
      this(p);
      this.title = title;
    }

    /** Creates a new instance.
     *  @param p panel associated with this pane
     */
    Pane (panel p) {
      this.pan = p;
      this.MW  = p.MW;
    }

    /** Sets the pane's IconBar.
        @param s The icon bar params
     */
    public void setIconBar (String s) {
      Parser p = new Parser(s);
      nicons = p.elements();
      iconKeys = new String[nicons];
      iconOffs = new int[nicons+1];
      icons = (s.indexOf(';')>0)? new Icon[nicons] : null;	// Icon or text based
      int tw = (MW.fm!=null)? MW.fm.charWidth('0') : 8;
      int ioff=ts; iconOffs[0]=ioff;
      for (int i=0; i<nicons; i++) {
	String key = p.get(i+1);
	int ic = key.indexOf(';');
	if (ic>0) {
	  icons[i] = getIcon(key.substring(ic+1),ts);
	  key = key.substring(0,ic);
	  ioff += 6*ts/4;
	} else {
	  ioff += key.length()*tw + ts;
	}
	iconKeys[i]=key;
	iconOffs[i+1]=ioff;
      }
      iconCur=0;
      refresh();
    }

    /** Sets the pane's TabManager.
        @param mgr The tab manager for this tab.
     */
    private void setTabManager (TabManager mgr) {
      tabManager = mgr;
      mgr.addTab(this);
    }

    /** Gets the pane's TabManager.
     *  @return pane's TabManager
     */
    private TabManager getTabManager () {
      return tabManager;
    }

    /** Indicates if the pane is in a tab.
        @return tue if the pane is in a tab.
     */
    public boolean isInTab () {
      return getTabManager() != null;
    }

    /** Indicates if the pane is the visible tab.
     *  @return true if pane is visible
     */
    public boolean isVisibleTab () {
      return isInTab() && getTabManager().isVisibleTab(this);
    }

    /** Makes this pane the visible tab. */
    public void makeVisibleTab () {
      if (isInTab() && !isVisibleTab()) {
        getTabManager().setVisibleTab(this);
        updateTabs();
      }
    }

    /** Makes this pane a non-visible tab (if other tabs exist). */
    public void makeNonVisibleTab () {
      Shell.warning("Pane.makeNonVisibleTab() does not work for all pane types.");
      if (isInTab() && isVisibleTab()) {
        getTabManager().setNonVisibleTab(this);
        updateTabs();
      }
    }

    /** Sets the title of the pane
     *  @param value the title of the pane
     */
    public void setTitle (String value) { title=value; refresh(); }

    /** Sets the title size of the pane in pixels
     *  @param value title size
     */
    public void setTS (int value) { if (mobile==0) ts=value; refresh(); }

    /** Set the theme via String form
     *  @param value the theme in String form */
    public void setTheme (String value) { if (mw!=null) mw.setTheme(value); }

    /** Set the theme
     *  @param value the theme */
    public void setTheme (Object value) { if (mw!=null) mw.setTheme(value); }

    /** Add Midas Window for a new Pane, as well as
     *  calls {@link #add(Container)} to add its Java graphics panel
     *  @param mw the Midas Window
     */
    public void add (MWindow mw) { this.mw=mw; add(mw.panel); }

    /** Add Java graphics panel for current Pane
     *  @param panel the Java graphics panel
     */
    public void add (Container panel) {
      this.panel = panel;
      if (panel != null) { // Since 3.5.0. Skip if in Headless state. BUG 2817
        panel.setBounds(x,y,w,h);
        MW.addOn(panel,-1);
        panel.setVisible(!hidden);
      }
    }

    /** Removes a Pane's Java graphics panel from the parent Panel's Midas Window.
     *  Also refreshes the parent Panel.
     *  @param panel the Java graphics panel
     */
    public void remove (Component panel) {
      if ((MW != null) && (MW.panel != null)) MW.panel.remove(panel);
      pan.refresh();
    }

    /** Refresh the parent Panel's Midas Window at the position of the Pane */
    public void refresh () {
      MW.refresh(pos);
    }

    /** paint this Pane
     *  @deprecated in 3.9.0 for graphical timing issue use {@link #paint(GPanelSetup, Graphics)}
     */
    @Deprecated
    public void paint () {
      int xi=x; // top-left X Pos (pixel)
      int wi=w; // width of the pane
      int yi=y; // top left Y Pos (pixel)
      int hi=h; // height
      int tsp=3*ts/2;   // area around text??
      if (tsp>0) { yi-=tsp; hi+=tsp; }  // add text "zone" to borders
      pos.setBounds(xi,yi,wi,hi);
      if (tabManager != null) {
        wi/=tabManager.getTabCount();
        xi+=tabManager.getTabIndex(this) * wi;
      }
      if (ts>0 && !hidden) {
        int color = ((tabManager==null) || tabManager.isVisibleTab(this))? MWindow.HILIT: MWindow.LOLIT;
        int modeForPushPop = (pushPopIconSymbol == PUSHPOP_ICON_SYMBOL_SHADEDX)?MWindow.SB_SHADEX:MWindow.SB_SHADE_PUSHPOP;
        if (customIconSymbol == CUSTOM_ICON_SYMBOL_NONE){
          MW.shadowbox(xi,yi,wi,tsp,is(PUSHPOPICON)?modeForPushPop:MWindow.SB_SHADE);
        }
        // Since NeXtMidas 3.5.4 Bug 2895, allow for a second custom icon to the left of the PUSHPOP icon
        else {
          MW.shadowboxCustomIcon(xi,yi,wi,tsp,is(PUSHPOPICON)?modeForPushPop:MWindow.SB_SHADE,customIconSymbol,customIconSymbolName);
        }
        MW.drawString((tag!=null)?tag:title,xi+wi/2,y-4,ts,MWindow.CENTER|color);
      }
      if (panel==null) {  // if no Graphics panel in the Pane, draw a rectangle border and the pane name in the center
        MW.g.drawRect(xi+2,y+2,wi-4,h-4);  // rectangle size reduced slightly in 3.1.1 to remove artifacts  (Bug 2511)
        MW.drawString(name,xi+wi/2,y+h/2,12,0);
      }
    }

    /** paint this Pane based on both panel graphics and current graphics object ???
     *  @param gp the GPanelSetup containing information on the panel
     *  @param g  the current Graphics object used for painting
     */
    public void paint (GPanelSetup gp, Graphics g) {
      int xi=x; // top-left X Pos (pixel)
      int wi=w; // width of the pane
      int yi=y; // top left Y Pos (pixel)
      int hi=h; // height
      int tsp=3*ts/2;   // area around text??
      if (tsp>0) { yi-=tsp; hi+=tsp; }  // add text "zone" to borders
      pos.setBounds(xi,yi,wi,hi);
      if (tabManager != null) {
        wi/=tabManager.getTabCount();
        xi+=tabManager.getTabIndex(this) * wi;
      }
      if (ts>0 && !hidden) {
        int color = ((tabManager==null) || tabManager.isVisibleTab(this))? MWindow.HILIT: MWindow.LOLIT;
        int modeForPushPop = (pushPopIconSymbol == PUSHPOP_ICON_SYMBOL_SHADEDX)?MWindow.SB_SHADEX:MWindow.SB_SHADE_PUSHPOP;
        if (customIconSymbol == CUSTOM_ICON_SYMBOL_NONE){
          gp.shadowbox(g,xi,yi,wi,tsp,is(PUSHPOPICON)?modeForPushPop:MWindow.SB_SHADE);
        }
        // Since NeXtMidas 3.5.4 Bug 2895, allow for a second custom icon to the left of the PUSHPOP icon
        else {
          gp.shadowboxCustomIcon(g,xi,yi,wi,tsp,is(PUSHPOPICON)?modeForPushPop:MWindow.SB_SHADE,customIconSymbol,customIconSymbolName);
        }
	for (int i=0; i<nicons; i++) {
	  int xip = xi+iconOffs[i], yip = yi+(ts/3);
	  if (icons!=null && icons[i]!=null) icons[i].paintIcon(panel,g,xip,yip);
	  else gp.drawString(g,iconKeys[i],xip,yip+ts,ts,(iconCur==i+1)?MWindow.HILIT:0);
	}
        gp.drawString(g,(tag!=null)?tag:title,xi+wi/2,y-4,ts,MWindow.CENTER|color);
      }
      if (panel==null) {  // if no Graphics panel in the Pane, draw a rectangle border and the pane name in the center
        g.drawRect(xi+2,y+2,wi-4,h-4);  // rectangle size reduced slightly in 3.1.1 to remove artifacts  (Bug 2511)
        gp.drawString(g,name,xi+wi/2,y+h/2,12,0);
      }
    }

    /** Check if Either the PUSHPOP icon or Custom Icon were clicked
        @param xp x coordinate of click
        @param yp y coordinate of click
        @return -1 if clicked on top right Push/Pop Icon, -2 if clicked on custom icon, -3 title, 1+ icon, 0 none
        @since NeXtMidas 3.5.4 Bug 2895 allow for selection of second custom icon, as well as selection for the PUSHPOP icon
     */
    private int clickHit (int xp, int yp) {
      int xi=pos.x, wi=pos.w, tsp=3*ts/2;
      int iconW = 10;  // width of an icon
      int rightPadding = 5;
      if (tabManager != null) {
        wi/=tabManager.getTabCount();
        xi+=tabManager.getTabIndex(this) * wi;
      }
      int leftXOfPushPopIcon = xi+wi-iconW-rightPadding; // left boundary of PUSHPOP icon
      if (xp<xi || xp>xi+wi || yp<y-tsp || yp>y+h) return BUTTON_SELECTED_OOR;
      if (is(PUSHPOPICON) && xp>leftXOfPushPopIcon) return BUTTON_SELECTED_PUSHPOP; // Bug 2895
      if ((customIconSymbol != CUSTOM_ICON_SYMBOL_NONE) && ((xp>leftXOfPushPopIcon-iconW) &&
          (xp<=leftXOfPushPopIcon))) return BUTTON_SELECTED_CUSTOM; //Since NeXtMidas 3.5.4 Bug 2895
      if (nicons>0 && hoverHit(xp,yp)>0) return iconCur; // insure that we hit an icon
      return BUTTON_SELECTED_NONE;
    }
    private int hoverHit (int xp, int yp) {
      if (ts==0) return -1;
      int te=ts/4, tsp=3*ts/2;
      int xi=pos.x, xw=pos.w, yi=pos.y;
      int iconLast=iconCur;
      if (xp<xi || xp>xi+xw) return -1;
      if (yp<yi || yp>yi+tsp) return -1;
      if (nicons<=0 || xp>xi+iconOffs[nicons]) return 0;
      int icon=nicons; for (; icon>0 && xp<xi+iconOffs[icon-1]; icon--);
      if (yp<yi+te || yp>yi+tsp-te) icon=0;
      iconCur=icon;
      if (icon!=iconLast) refresh();
      return icon;
    }

    /** Set Panel display
     *  @param str the Panel display
     */
    public void setDisplay (String str) {
      if (str.equals("FRONT")) hitPane(myIndex(), false, BUTTON_SELECTED_NONE);
      else if (str.equals("POP")) { panel.setVisible(true); mw.pop(1); }
      else if (str.equals("PUSH")) mw.pop(0);
    }

    private int myIndex () {
      for (int i=1; i<=npane; i++) if (pane[i]==this) return i;
      return 0;
    }

    @Override
    public int processMessage (Message msg) {
      if (msg.name.equals("CFG.PANE")) {
        String text = msg.getS();
             if (text.equals("TITLETEXT"))  new GPrompt (MW,"TitleText",title,title.length(),0,this);
        else if (text.equals("TITLESIZE"))  new GMenu (MW,"TitleSize","0,8,10,12,14,16,20,24",0,0,this);
        else if (text.equals("TITLECOLOR")) new GPrompt (MW,"TitleColor","ForeGround",0,this);
        else if (text.equals("ATTACH"))     new GMenu (MW,"Attach",M.registry.table.getKeyList(),0,0,this);
        else if (text.equals("ADDTAB"))     addPane(0,cfgx,cfgy);
        else if (text.equals("DELETE"))     delPane(this);
        else M.warning("Unrecognized message "+msg.name+" text "+text);
        configure=1;
      }
      else if (msg.name.equals("TITLETEXT"))  { setTitle(msg.getS()); }
      else if (msg.name.equals("TITLESIZE"))  { setTS(Convert.o2l(msg.data)); resize(); }
      else if (msg.name.equals("TITLECOLOR")) { M.warning("Setting color is TBD"); }
      else if (msg.name.equals("ATTACH"))     { attach(msg.getS()); }
      else if (msg.name.equals("SUBPANELS"))  { attachSubPanel(msg.getS()); }
      return NORMAL;
    }

    /** Attach an object (either a primitive or a Macro) to the panel. If the
        object is a Macro, the macro panel is added
        @param name The name of the object as it is in the registry
     */
    private void attach (String name) {
      Object entry = M.registry.get(name);
      if (entry instanceof Macro) getSubPanels((Macro)entry, name);
      else attach(entry,name);
    }

    /** Attaches an object to the panel<br>
        Note: made public InternalUseOnly, to allow plots created outside a pipe to be attached to a panel
        @param entry A GPrimitive Object
        @param name  pane name
        @since NeXtMidas 2.7.3
     */
    @InternalUseOnly
    public void attach (Object entry, String name) {
      if (GPrimitive.isInstance(entry)) {
        mw = GPrimitive.getMWindowViaReflection(entry);
        if(mw==null) return; // Should we log an error?
        mw.pushParent = this;
        mw.pop(0);
        this.name = name;
      }
      else M.info ("Attach Object must be an instanceof GPrimitive: "+name);
    }

    /** Attach a panel to this panel
        @param name The name of the panel to attach
        @since NeXtMidas 2.7.3
     */
    public void attachSubPanel (String name) {
      attach(subPanels.get(name), subPanelName);
    }

    /** Get any and all panels from a Macro
        @param m The Macro
        @param name The id or name given to the sub-panel in the parent macro
        @since NeXtMidas 2.7.3
     */
    private void getSubPanels (Macro m, String name) {
      panel p = (panel) m.M.registry.get("PANEL");
      if (p != null) attach(p,name);      // if default, go ahead and attach it
      else {
        subPanels  = new Table();         // clear out the subPanels table
        subPanelName = name;              // keep the name in sync with what the parent expects
        Table t    = m.M.registry.table;  // get the list of objects
        String[] s = t.getKeys();         // get the keys of the objects
        Object tmp = null;
        for (int i=0; i<s.length; i++) {
          tmp =  t.getO(s[i]);            // get the object
          if (tmp instanceof panel) subPanels.put(s[i], tmp);  // if the obj is a panel, add it to the sub-panels table
        }
        // If there is one panel, add it. If there are many, present a menu to the user
        if (subPanels.getSize() > 0) {
          if (subPanels.getSize() == 1) {
            String id = subPanels.getKeys()[0];
            attach(subPanels.getO(id), subPanelName);
            // Since 3.8.1 to support full Swing (NXM-3213), check the
            // panel to see if is has any panes associated with it.
            // If so, add each pane to its panel
            // TODO: may need to add more logic for when not full Swing & needs more testing
            if (subPanels.getO(id) instanceof panel) {
              panel myPanel = (panel)subPanels.getO(id);
              for (Pane myPane : myPanel.getPanesArray()) {
                //System.out.println("My pane:"+myPane);
                if (myPane != null) {
                  if (myPane.mw != null) {
                    //System.out.println("My pane mw:"+mw);
                    myPane.mw.addTo(myPane);
                  }
                }
              }
            }
          }
          else new GMenu (MW,"SubPanels",subPanels.getKeyList(),0,0,this);
        }
      }
    }

    @Override
    public String toString() {
      return "Pane "+name+" x="+x+" y="+y+" w="+w+" h="+h;
    }
  } // end inner class Pane

  /** Loads the configuration of a panel from a class.
      @param className The fully-qualified name of the class to load the config from.
   */
  private void loadPanelClass (String className) {
    if ((className.trim().length() == 0) && (M.macro != null)) {
      className = M.macro.name.toLowerCase();
    }

    // The default package the class will be in
    String defPkg = "";
    if (M.macro != null) { defPkg = "nxm." + M.macro.option.toLowerCase() + ".mcr."; }

    String nameLC = className.toLowerCase();
    String nameTC = className.substring(0,1).toUpperCase() + nameLC.substring(1);

    // Possible class names
    String[] names = new String[] { defPkg+className, defPkg+nameLC, defPkg+nameTC, className, nameLC, nameTC };

    Class<?> configClass = null;

    // Try to figure out what the name of the class is
    for (int i = 0; (i < names.length) && (configClass == null); i++) {
      try {
        configClass = Class.forName(names[i]);
      } catch (Exception e) {
        /* ignore */
      }
    }

    if (configClass == null) {
      M.warning ("Unable to load panel class '" + className + "': Class not found.");
    }
    else {
      try {
        final MessageHandler mh          = M.macro;
        final Object         config      = configClass.getConstructor().newInstance(); // Updated in 3.9.0 to use default constructor
        final Message        closeMsg    = new Message("WINDOW", 0, "CLOSING", config, this);
        final WindowListener listener    = new WindowAdapter() {
          @Override
          public void windowClosing (WindowEvent e) {
            mh.processMessage(closeMsg);
          }
        };
        //loadDisplays(configClass.getFields(), config, false);
        loadDisplays(configClass.getDeclaredFields(), config, true);

        if (config instanceof Frame) {
          Frame frame = (Frame)config;
          container = frame;
          frame.addWindowListener(listener);
          frame.setVisible(true);
          MW.parent = frame;                         // <-- PARENT = java.awt.Frame
        }
        else if (config instanceof Container) {
          container = (Container)config;
          container.setVisible(true);
          MW.panel = container;                      // <-- PANEL = java.awt.Container
        }
        else {
          M.warning("Class loaded with /JSETUP is a "+Util.getClassName(config)+", "
                   +"expected a AWT/Swing container");
          if (config instanceof Component) {
            // JC4: This next line is backwards-compatible with NeXtMidas 3.1 and earlier but
            //      doesn't make much sense... I suspect this was part of my initial attempt to
            //      support non-Frame components that didn't work out.
            ((Component)config).setVisible(true);
          }
        }
      } catch (Exception e) {
        M.printStackTrace("Unable to load panel class '"+className+"'", e);
      }
    }
  }

  /** Loads the fields for a class. (Used by loadPanelClass)
      @param fields  The fields to load.
      @param config  The configuration instance.
      @throws Exception If an error occurs.
   */
  private void loadDisplays (Field[] fields, Object config, boolean noPublic) throws Exception {
    AccessibleObject.setAccessible(fields, true);
    MessageHandler mh   = M.macro;
    Object         to   = null;
    Object         from = this;

    Map<Object,String> controls = new HashMap<>();
    Map<String,Object> groups   = new HashMap<>();

    // Find the controls and displays then add the displays and put the controls in the appropriate table.
    for (int i = 0; i < fields.length; i++) {
      String name  = fields[i].getName().toUpperCase();
      Object value = fields[i].get(config);

      if (name.equals("PANE0")) {                     // The pane for gcontrol
        if (value instanceof MidasControls) {
          ctlPane = (MidasControls)value;
          ctlPane.setMWindow(MW);
        } else {
          M.error("PANE0 must be an instance of MidasControls.");
        }
      }
      else if (value instanceof PushPopPanel) {
        PushPopPanel ppp = (PushPopPanel)value;
        getPanes().put(name, ppp.getMidasDisplay());
      }
      else if (value instanceof MidasDisplay) {     // A NeXtMidas display
        ((MidasDisplay)value).setMWindow(MW);
        getPanes().put(name, value);
      }
      else if ((value != null) && value.getClass().getName().equals("nxm.sys.libxm.XmDisplay")) {
        // An X-Midas display
        if (xmCanvasTbl == null) { xmCanvasTbl = new Table(); }
        if (name.toUpperCase().startsWith("XS") && StringUtil.isInteger(name.substring(2))) {
          xmCanvasTbl.put(name, value);
        } else {
          M.error("Illegal name for an XmCanvas, given '"+name+"'. Must use name in the form xsN "
                 +"where N is the value of /XS=. For example use the name xs3 for /XS=3.");
        }
      }
      else if (value instanceof ButtonGroup) {
        groups.put(name, value);
      }
      else if (value instanceof JPanel) {
        final JPanel jpanel = (JPanel)value;
        MouseAdapter mouseListener = new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent evt) {
            if (evt.getButton() == MouseEvent.BUTTON2) {
              processMessage("BUTTON", 2, evt);
            }
          }
        };
        jpanel.addMouseListener(mouseListener);
      }
      else if (value instanceof Widget) {           // A nxm.sys.inc.Widget
        controls.put(value, name);
      }
      else if (value instanceof Component) {        // A Swing/AWT component
        controls.put(value, name);
      }
    }

    // Take care of any groups.
    for (Map.Entry<String,Object> e : groups.entrySet()) {
      String name  = e.getKey();
      Object value = e.getValue();

      if (value instanceof ButtonGroup) {
        Enumeration<?> buttons = ((ButtonGroup)value).getElements();

        while (buttons.hasMoreElements()) {
          AbstractButton button  = (AbstractButton)buttons.nextElement();
          String         btnName = controls.remove(button);

          cntrls.put(name, new WidgetWrapper(button, mh, name, btnName, to, from));
        }
      }
    }

    // Take care of the rest of the controls.
    for (Map.Entry<Object,String> e : controls.entrySet()) {
      Object value = e.getKey();
      String name  = e.getValue();

      WidgetWrapper w;
      if (value instanceof Widget) w = new WidgetWrapper((Widget)value,    mh, name, null, to, from);
      else                         w = new WidgetWrapper((Component)value, mh, name, null, to, from);

      cntrls.put(name, w);
    }
  }

  /** Gets the panel to use for the controls.
      @return Returns the panel to use for the controls or <code>null</code>
              if the default should be used.
   */
  public MidasControls getMidasControls () {
    return ctlPane;
  }

  /** Hide normal gcontrols? (/JSETUP= with no PANE0) */
  private boolean isHideWidgets () {
    return (container != null) && (ctlPane == null) && hideWidget ;
  }

  /** Indicates if the panel is running in headless mode.
      @return <code>true</code> if in headless mode, <code>false</code> otherwise.
      @since NeXtMidas 1.9.5
   */
  public boolean isHeadless () { return headless; }


  /** Manages a set of tabbed panes. */
  public class TabManager {
    private java.util.List<Object> panes = new LinkedList<>();  // The panes in this tabbed area.
    private int      visiblePane = 0;                 // The index of the visible pane.

    /** Creates a new instance.  */
    private TabManager () {
    }

    /** Adds a pane.
        @param pane The pane to add.
     */
    public synchronized void addTab (Pane pane) {
      panes.add(pane);
    }

    /** Sets the active tab.
        @param index The index of the tab to show.
     */
    public synchronized void setVisibleTab (int index) {
      if (is(ROTATETABS)) {
        Object pane = panes.get(index);
        panes.remove(index);
        panes.add(0, pane);
        visiblePane = 0;
      } else {
        visiblePane = index;
      }
    }

    /** Sets the active tab.
        @param pane The tab to show.
     */
    public synchronized void setVisibleTab (Pane pane) {
      setVisibleTab(getTabIndex(pane));
    }

    /** Sets the active tab to be a tab other than the given tab (unless it is the only tab).
        @param pane The tab to not show.
     */
    public synchronized void setNonVisibleTab (Pane pane) {
      if (isVisibleTab(pane)) {
        if (is(ROTATETABS)) {
          setVisibleTab(1);
        } else {
          int paneIndex = getTabIndex(pane) + 1;
          if (paneIndex < getTabCount()) {
            setVisibleTab(paneIndex);
          } else {
            setVisibleTab(0);
          }
        }
      }
    }

    /** Gets the active tab.
     *  @return the active tab
     */
    public synchronized Pane getVisibleTab () {
      return (Pane)panes.get(visiblePane);
    }

    /** Indicates if the given pane is visible.
        @param pane The pane to check for being visible.
        @return Indication of whether or not it is visible.
     */
    public synchronized boolean isVisibleTab (Pane pane) {
      return pane == getVisibleTab();
    }

    /** Gets the index number of a given pane.
     *  @param pane the tab (pane) for which the index is needed
     *  @return index number of the given pane
     */
    public synchronized int getTabIndex (Pane pane) {
      return panes.indexOf(pane);
    }

    /** Gets the number of tabs.
     *  @return the number of tabs
     */
    public synchronized int getTabCount () {
      return panes.size();
    }
  } // end inner class TabManager
}
