package nxm.sys.libg;

import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.HashMap;

import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Message;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;

/**
  A class to implement a graphical menu.
  <pre>
  Left click to select or toggle an entry and exit.
  Middle click to select an entry without exiting.
  Right click to exit selecting the default if present.

  Keypress selects the first entry beginning with that key.

  Middle click over the title enters the configuration menu.

    Life =  Temporary - exit when selected
            Sticky - stay until right mouse click
            Permanent - never exit
    Items = Auto - reorganize to fit window
            Option - only show current selection item until clicked
            OneRow - all items appear in a single horizontal row
            NColumn - arrange items into N columns
    Title = Auto - choose title position to fit window
            Top - place title at top
            Left - place title at left
            Right - place title at right
            Bottom - place title at bottom
    NColumns = number of columns (when applicable)

    Item = 0 - this is an action menu, buttons are always raised
           N - this is a state menu, current selection is depressed
          -1 - this is a state menu, without a current selection

    If NOSTATE, don't act as state menu, even if set to non-zero value

    If NODEPRESS, don't depress current selection

    If INPUT, allow keyboard entry of non-listed item in CHOICE menu

    If TOGGLELIST, return current list in message not just the item that changed
  </pre>

  @author Jeff Schoen
*/
public class GMenu extends GWidget implements ActionListener {

  /** Allow setting of toggle with mask values ("+FOO"). Since NeXtMidas 2.1.0. */
  private static final boolean SET_TOGGLE_WITH_MASK = true;

  /** flags mask. */
  public static String flagsList = GWidget.flagsList
          + ",Choice,Button,Toggle,ChkBox,TTop,TLeft,TRight,TBottom,TOff"
          + ",OneRow,NColumn,setColumns>,ToggleList,Input,CS,NoState"
          + ",NoDepress,Rollup,Flat,AssumeState,Shade";

  /** Flags: CHOICE -- Is a Choice GMenu. */
  public static final int CHOICE      =0x100;
  /** Flags: BUTTON -- Is a Button Group GMenu. */
  public static final int BUTTON      =0x200;
  /** Flags: TOGGLE -- Can it be toggled. */
  public static final int TOGGLE      =0x400;
  /** Flags: CHKBOX -- check box group. */
  public static final int CHKBOX      =0x800;
  /** Flags: TTOP -- Title on top */
  public static final int TTOP        =0x1000;
  /** Flags: TLEFT -- Title on left */
  public static final int TLEFT       =0x2000;
  /** Flags: TRIGHT -- Title on right */
  public static final int TRIGHT      =0x4000;
  /** Flags: TBOTTOM -- Title on bottom */
  public static final int TBOTTOM     =0x8000;
  /** Mask for title placement bits (TTTOP,TLEFT,TBOTTOM,TRIGHT) */
  public static final int TMASK       =0xF000;
  /** Flags: TOFF -- Title off */
  public static final int TOFF        =0x10000;
  /** Flags: ONEROW -- display items in a single row */
  public static final int ONEROW      =0x20000;
  /** Flags: number of columns directly set */
  public static final int NCOL        =0x40000;
  /** unused flag */
  public static final int SETCOL      =0x80000;
  /** Flags: TOGGLELIST --can the items be toggled */
  public static final int TOGGLELIST  =0x100000;
  /** Flags: allow keyboard entry of non-listed item in CHOICE menu */
  public static final int INPUT       =0x200000;
  /** Flags: CS -- Return values Case Sensitive (do NOT upper case). */
  public static final int CS          =0x400000;
  /** Flags: don't act as state menu, even if set to non-zero value */
  public static final int NOSTATE     =0x800000; // see ReleaseNotes_1_8_X for more details
  /** Flags: dont depress current selection */
  public static final int NODEPRESS   =0x1000000;
  /** Flags: meant to be an internal state toggle for popup menus when collapsed (rolled up) or expanded (opened).*/
  public static final int ROLLUP      =0x2000000;
  /** Flags: flat color - no extra shading ?*/
  public static final int FLAT        =0x4000000;
  /** Flags: used with items in state menu that should not hold state - ex: push buttons */
  public static final int ASSUMESTATE =0x8000000;
  /** Flags: translucent - do not clear background */
  public static final int SHADE       =0x10000000;

  private static final int pixPerIndent = 16; // Number of pixels per indent level

  /** default color for disabled items. */
  public static final Color DEF_DISABLED_COLOR = Color.GRAY;

  private String[] itemv;  /** */

  private int rows, columns, items, titlex1, titley1, titlex2, titley2;
  private int itemwidth=0, maxwidth=0, titlewidth=0, altwidth=0, altheight=0, maxheight=0;
  private int curitem=0, defitem=0, nc=2, noStateMask=0, disableMask=0;
  protected MBox posi = new MBox();
  private MBox bp,bc,bt; // prompt,diamond,text selection bounds
  private Font fontb;
  private GMenu gchoice;
  private MJTextField jtexti;
  /** The comma separated list of items */
  public String list;
  /** color table */
  public Table ctab = null;
  private Table savedColors = null;
  private int mouseMode = 0;
  private boolean altzone = false;
  private HashMap<Integer,String> disabledItems;  // list of items (by index) that are displayed but not selected
  private Color disabledItemColor = DEF_DISABLED_COLOR;
  private boolean isSetup = false;

  // fields supporting scrolling - generally added for Bug 2825 (added or at least first used in 3.7.0)
  private boolean shouldScroll = false;
  private static final int SCROLLBAR_WIDTH = 10;   // width of scroll bar
  private static double scrollY = 0; // Current relative y for top of scroll bar
  private double scrollYAtStartOfSlide = 0; // location of scroll bar when knob/slider was first grabbed for sliding
  private boolean slidingKnob = false; // is the knob/slider of the scroll bar being dragged / sliding
  private int startDisplayAt = 0;  // relative y to start display if Menu Items
  private int maxMenuHeight = 0;   // Maximum Height of this GMenu - based on parent height or fixed height of GMenu
  private boolean isPopup = false; // True if this menu is a popup

  /** Constructor
   *  @param parent The parent of the gmenu
   *  @param name The name of the gmenu
   *  @param list The list of items to be on the gmenu
   *  @param item The default item selected
   *  @param flags The flags for the GMenu
   *  @param mh The message handler for the GMenu
   */
  public GMenu (Object parent, String name, String list, int item, int flags, MessageHandler mh) {
    super("GMenu",parent,name,flags,mh);
    open(name, list, item);
    addTo(parent);
    // JWB: Bug 220. The line below is needed to be able to adjust popup menu via arrow keys
    if (parent != null) panel.requestFocusInWindow();   // parent != null mean popup
  }

  /** Constructor
   *  @param label The label for the menu
   *  @param parent The parent of the gmenu
   *  @param name The name of the gmenu
   *  @param flags The flags for the GMenu
   *  @param mh The message handler for the GMenu
   */
  public GMenu (String label, Object parent, String name, int flags, MessageHandler mh) {
    super(label,parent,name,flags,mh);
  }

  void open (String name) { // package-private, only used in GMenu and GLegend
    super.open();
    if (name.equals("GCHOICE")) { label=name; title=""; }
    fontb = newFont (font,"",Font.BOLD,-1);
    if (fontb==null) titlewidth = tw*title.length();
    else titlewidth = panel.getFontMetrics(fontb).stringWidth(title);
    if (is(TOGGLELIST)) flags |= TOGGLE;
    if (!is(PERM)&&is(TOGGLE)) flags ^= CHKBOX;
    if ((parent != null) && (parent instanceof MPlot)) { // Since NXM 3.8.1 This is used to increment the amount of popups contained inside of MPLot.
      ((MPlot)parent).popupCount++;
    }
    if (parent != null && parent instanceof GWidget) {
      // This makes sure that the choice or menu has correct theme when popped
      setWidgetTheme(((GWidget)parent).getWidgetTheme());
    }
  }

  public void setTitle (String title) {
    this.title = title;
    titlew = stringWidth(title);
    if (fontb==null) titlewidth = tw*title.length();
    else titlewidth = panel.getFontMetrics(fontb).stringWidth(title);
    if (isSetup) { setup(); refresh(); }
  }

  /** Open the GMenu
   *  @param name name of GMenu
   *  @param list Is this prompt a file chooser prompt type (GFile)
   *  @param item default item
   */
  public void open (String name, String list, int item) {
    open(name);
    setItems(list);  // Since 3.7.0 this method will wipe out existing defitem value since this is a new GMenu (ex: popup menu)
    defitem = item;  // Set default after setting items - Since 3.7.0 for Bug 2847
    resize(1);  // calculate size
  }

  @Override
  public void close () {
    // tell the GCHOICE widget that its popup is down
    if (mh instanceof GMenu) ((GMenu)mh).gchoice = null;
    if ((parent != null) && (parent instanceof MPlot)) { // Since NXM 3.8.1 This is used to decrement the amount of popups contained inside of MPLot.
      ((MPlot)parent).popupCount--;
    }

    super.close();
  }

  /** Sets the list of pickable items in the menu.
      @param list A comma-separated list of choices (e.g. "Bird,Dog,Cat").
   */
  public void setItems (String list) {
    Parser parser = new Parser(list, false); parser.setUseSpaceDelimiter(false);
    itemwidth = 0;
    boolean redo = (this.list!=null);
    String item0 = redo? getValue (defitem,false) : ""; // Since 3.7.0 for Bug 2847
    this.list = list;
    items = 0;
    if (list.length()>0) items = parser.elements();
    if (is(TOGGLE) && items > 31) { // Limit due to use of int to hold mask value (use 31 not 32 since <0 is special)
      Shell.warning("GMenu: TOGGLE list has max length of 31, ignoring entries 32 to "+items+" in "+list);
      items = 31;
    }
    itemv = new String[items+1];
    // itemv[0] may store default item as of 3.7.0 for Bug 2847 - fixed in 3.9.3 to make sure not null
    itemv[0] = (item0!=null) ? item0 : "";
    int ndefitem=0;   // Since 3.7.0 for Bug 2847
    for (int i = 1; i <= items; i++) {
      String tmpstr = parser.get(i);
      itemv[i] = tmpstr;
      int w = stringWidth(tmpstr);
      if (w > itemwidth) itemwidth = w;
      if (redo && defitem!=0 && tmpstr.equals(item0)) ndefitem=i;
    }
    if (!is(TOGGLE)) defitem = ndefitem;  // Since 3.7.0 for Bug 2847
    itemwidth += tw*2;
    setup();               // adjust layout based on current set of items
    if (redo) refresh();
  }

  /** Sets the list of pickable items in the menu.
   *  (Converts the array of items to a comma separated String list of items)
   *  @param array array containing items
   *  @param len   number of items
   */
  void setItems (Object[] array, int len) { // package-private only used by GLegend
    itemwidth = 0;
    boolean redo = this.list!=null;
    String item0 = redo? itemv[0] : "";
    list  = "";
    items = len;
    itemv = new String[items+1];
    itemv[0] = item0;
    for (int i = 1; i <= items; i++) {
      String tmpstr = array[i-1].toString();
      itemv[i] = tmpstr;
      if (i > 1) list += ","+tmpstr;
      else       list += tmpstr;
      int w = stringWidth(tmpstr) + getIndent(i)*pixPerIndent;
      if (w > itemwidth) itemwidth = w;
    }
    itemwidth += tw*2;
    setup();
    if (redo) refresh();
  }

  /** Get the list of menu items
   *  @return the list of menu items. */
  public String getItems () {
    return list;
  }

  /** Get the number of items in the menu
   *  @return the number of items in the menu. */
  public int getItemCount () {
    // itemv[0] holds user input
    if (itemv == null) return 0; // Since 3.9.3 for NXM-3545 - one constructor does not immediately create this
    int nitems = itemv.length;
    // Though for GMenu, the # or items is always 1 less than the length,
    // a subclass could work differently. As of 3.7.0, itemv[0] could hold the default item.
    // 3.9.4 has an improved check (over 3.9.3) to see if a non-empty itemv[0] occurs a second time
    // If so, it is specifying the default and should not be counted twice - NXM-3545
    if ((itemv[0].length() <= 0) || (Arrays.asList(itemv).lastIndexOf(itemv[0])) > 0) nitems--;
    return nitems;
  }

  @Override
  public void resize (int mode) {
    super.resize(mode);
    if (is(CHOICE)) {
      bt.w = pos.w-bt.x-bw-titlex2;
    }
  }

  @Override
  public void setup () {
    if (is(INLINE)) bw=0; else bw=2;
    if (is(CHOICE)) setupChoice();
    else setupMenu();
    layout();
    isSetup=true;
  }

  /** Method to set up the choice widget.
      LSN: BUG 2015 - Improve the GCHOICE to accept title positioning similar to GMENU
   */
  protected void setupChoice () {
    maxheight = th+ta-compression;
    maxwidth = itemwidth+altwidth+tw;
    if (units!=null) maxwidth += tw+stringWidth(units);
    if (fixLineHeight>maxheight) maxheight=fixLineHeight;
    titlex1 = titley1 = titlex2 = titley2 = 0;
    if (!is(TOFF)) {
      if (is(TTOP)) titley1 = maxheight+2;
      if (is(TLEFT)) titlex1 = titlewidth+tw+tw;
      if (is(TBOTTOM)) titley2 = maxheight+2;
      if (is(TRIGHT)) titlex2 = titlewidth+tw+tw;
      if (title.length() > 0 && !is(TMASK)) titlex1 = titlewidth+tw+tw;
    }
    pos.w = maxwidth + bw*2 + titlex1 + titlex2;
    pos.h = maxheight + bw*2 + titley1 + titley2;
    if (fixWidth>pos.w) pos.w=fixWidth;
         if (titlex1>0) bp = new MBox (bw+tw, bw+titley1, titlewidth, maxheight);
    else if (titley1>0) bp = new MBox (pos.w/2-titlewidth/2, bw, titlewidth, maxheight);
    else if (titlex2>0) bp = new MBox (pos.w-bw-titlewidth-tw, bw+titley1, titlewidth, maxheight);
    else if (titley2>0) bp = new MBox (pos.w/2-titlewidth/2, pos.h-bw-maxheight, titlewidth, maxheight);
    else                bp = new MBox (bw, bw, 0, 0);
    if (titlex1>0) bc = new MBox (bw+titlex1-tw, bw+titley1, th+tw, maxheight);
    else           bc = new MBox (bw, bw+titley1, th+tw, maxheight);
    bt = new MBox (bc.x+bc.w, bc.y+bc.h/2-th/2, maxwidth, th);
  }

  protected void setupMenu () {
    maxheight = th+5-compression;
    if (is(BUTTON)) maxheight+=5;
    maxwidth = itemwidth+altwidth+tw;
    if (is(CHKBOX)) maxwidth += tw*2;
    if (fixLineHeight>maxheight) maxheight=fixLineHeight;
    isPopup = (fixWidth == 0);
    int parentHeight; // height available for GMenu (either parent, grandparent, or fixed)
    int parentWidth;  // width available for GMenu (either parent, grandparent, or fixed)
    if (parent instanceof MWindow) {
      // Get immediate parent height and width
      parentHeight = MW.pos.h;
      parentWidth  = MW.pos.w;
      // Get parent's Container Height and Width (if applicable)
      Container outerParent = getOuterContainerOfParent(1); // get parent's parent (1 level beyond parent)
      if (outerParent != null && isPopup) { // adjust to parent's parent's dimensions
        if (outerParent.getWidth() > parentWidth) {
          parentWidth = outerParent.getWidth();
        }
        if (outerParent.getHeight() > parentHeight) {
          parentHeight = outerParent.getHeight();
        }
      }
    }
    else if (parent instanceof Container) {
      // Get immediate Container's height and width
      parentHeight = pos.h;
      parentWidth  = pos.w;
    }
    else {
      // use defaults
      parentHeight = 1000;
      parentWidth  = 1000;
    }

    // default to overall width and height of parent, but then adjust width and height
    // if this GMenu has a fixed width or height within the parent - Since 3.7.0 for Bug 2825
    int maxMenuWidth = parentWidth;  // default width to overall width of parent
    maxMenuHeight    = parentHeight; //TODO: get relative y of this GMenu in parent to calculate how much height is left
    // check if fixed area within the parent (not a popuup)
    if (fixWidth > 0) {
      maxMenuWidth = fixWidth;
    }
    // DO NOT Adjust back to fixedHeight if specified
    // if (fixHeight > 0) maxMenuHeight = fixHeight; // TODO: figure out why this is automatically set (when not user specified) see Bug 2825

    titlex1 = titley1 = titlex2 = titley2 = 0;
    if (!is(TOFF)) {
      if (is(TTOP)) titley1 = maxheight+2;
      if (is(TLEFT)) titlex1 = titlewidth+tw+tw;
      if (is(TBOTTOM)) titley2 = maxheight+2;
      if (is(TRIGHT)) titlex2 = titlewidth+tw+tw;
      if (title.length() > 0 && !is(TMASK)) {
        if (maxMenuHeight > maxheight*Math.min(3,items+1)) titley1 = maxheight+2;
        else titlex1 = titlewidth+10;  // switch title on side if not enough vertical space
      }
    }
    int maxrows = Math.max( 1, (maxMenuHeight-titley1-titley2-2*bw)/maxheight ); // since 3.7.0 for bug 2825, also take into account fixed width with parent cases
    int maxcolumns = Math.max( 1, (maxMenuWidth-titlex1-titlex2-2*bw)/maxwidth ); // since 3.7.0 for bug 2825, also take into account fixed height with parent cases
    // Add scrollbar if items can not all be displayed at once - Since 3.7.0 for Bug 2825
    // If needed, adjust width to allow space for scrollbar
    shouldScroll = (items > maxrows*maxcolumns && isPopup) ? true : false;  // only handling scrolls when menu is popup (Box 0 GMenus scrolling is handled by panel)
    int widthForScrollBar = 0;
    if (shouldScroll) {
      widthForScrollBar = SCROLLBAR_WIDTH;
      maxcolumns = Math.max( 1, (maxMenuWidth-titlex1-titlex2-SCROLLBAR_WIDTH-2*bw)/maxwidth );
    }
    scrollY = 0; // move any scroll knob back to the beginning of the menu

    int ilw = (is(INLINE) && pGroup!=null)? 4: 0;
    columns = (items-1)/Math.min(10,maxrows) + 1;
    columns = Math.min(columns,maxcolumns);
    if (is(NCOL)&&nc>0) columns = nc;
    if (is(NCOL)&&nc<0) columns = items;
    if (is(ONEROW)) columns = items;
    rows = (items+columns-1) / columns;
    pos.w = maxwidth*columns + bw*2 + ilw*2 + titlex1 + titlex2+widthForScrollBar; // account for scrollbar since 3.7.0 for Bug 2825
    if (titley1>0||titley2>0) pos.w = Math.max(titlewidth+tw+bw*2,pos.w);
    if (fixWidth>pos.w) pos.w=fixWidth;
    maxwidth = (pos.w - bw*2 - ilw*2 - titlex1 - titlex2-widthForScrollBar)/columns;
    //System.out.println("GMenu.setUpMenu rows:"+rows+"maxwidth:"+maxwidth+" pos.w:"+pos.w+" columns:"+ columns+" widthForScrollBar:"+widthForScrollBar+" titley1:"+titley1+" titley2:"+titley2+" titlex1:"+titlex1+" titlex2:"+titlex2+" fixWidth:"+fixWidth);
    pos.h = bw*2 + titley1 + titley2;
    if (!is(ROLLUP)) pos.h += maxheight*rows;
    if (fixLineHeight>pos.h) pos.h=fixLineHeight;
  }

  /** Get Outer Container of parent
   *  @return outer Container of parent
   *  @param levelsOut number of levels out to go on parents
   *  @since NeXtMidas 3.7.0 for Bug 2825
   */
  private Container getOuterContainerOfParent (int levelsOut) {
    Container outerParent = null;
    if (levelsOut == 0) return outerParent;
    int currentLevelOut = 1;
    if (parent instanceof MWindow) {
      outerParent = MW.panel.getParent();
      Container nextParent = outerParent;
      while (nextParent != null && (levelsOut < 0 || currentLevelOut < levelsOut)) {
        currentLevelOut += 1;
        outerParent = nextParent;
        nextParent = outerParent.getParent();
      }
      //if (outerParent != null) System.out.println("MW.panel.outer_parent.height:"+ outerParent.getHeight());
    }
    else if (parent instanceof Container) {
      outerParent = ((Container)parent).getParent();
      Container nextParent = outerParent;
      while (nextParent != null && (levelsOut < 0 || currentLevelOut < levelsOut)) {
        currentLevelOut += 1;
        outerParent = nextParent;
        nextParent = outerParent.getParent();
      }
      //if (outerParent != null) System.out.println("parent.outer_parent.height:"+ outerParent.getHeight());
    }
    return outerParent;
  }

  /** @deprecated Since NeXtMidas 3.9.0 to avoid graphical timing issues
   *  use {@link #paintComponent(Graphics)}
   */
  @Deprecated
  public void paint () {
    refresh();
  }

  @ProvisionalUseOnly("Treat this as protected and should only be called by subclass paintComponent methods")
  @Override
  public void paintComponent (Graphics g) {
    super.paintComponent(g);
    if (is(CHOICE)) paintChoice(g); // Updated to use g in 3.9.0 for NXM-3213
    else paintMenu(g); // Updated to use g in 3.9.0 for NXM-3213
  }

  protected final void paintChoice () { }
  protected final void paintMenu () { }

  protected void paintChoice (Graphics g) { // Updated with parameter g in 3.8.1 for NXM-3213
    // LSN: BUG 2015 - Since 3.1.1 to support title placement
    int x,y;
    int b2 = 2*bw;
    int bf = is(SHADE)? SB_SHADE : is(INLINE)? SB_FLAT : SB_DEF;
    Rectangle r = g.getClipBounds();
    if (hasLocalTheme() && !is(SHADE)) {
      Color origColor = g.getColor();
      g.setColor(theme.cwbg);
      g.fillRect(r.x,r.y,r.width, r.height);
      g.setColor(origColor);
    }
    if (bc.equals(r)) {
      if (is(SHADE)) shadowbox(g,0,0,pos.w,pos.h,bf);
      drawArrows(g); // Updated to use g in 3.9.0 for NXM-3213
      return;
    }
    shadowbox(g,0,0,pos.w,pos.h,bf); // Updated to use g in 3.9.0 for NXM-3213
    setValueBox(bt.x-3,bt.y,bt.w-2,bt.h);
    drawValueBox(g,1); // Updated to use g in 3.9.0 for NXM-3213
    g.setFont(fontb); // LSN: Added to bold the title for when top/bottom
    int ty = bw + titley1 + maxheight/2 + ta/2;
    x = pos.w/2 - titlewidth/2;
    if (titley1>0) {
      int dh=titley1, j1=(int)(dh*.15);
      y = bw+maxheight/2+ta/2-1;
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      g.drawString(title, x, y);
      y = bw+maxheight-2;
      if (!is(INLINE)) {
        setColor(g,theme.cwbs); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y); y += 2;
        setColor(g,theme.cwms); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y);
      }
    }
    if (titley2>0) {
      y = pos.h-bw-maxheight/2+ta/2;
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      g.drawString(title, x, y); // LSN: y being used to print the title based on pos.h
      y = pos.h-bw-maxheight-1;
      if (!is(INLINE)) {
        setColor(g,theme.cwbs);
        g.drawLine(b2,y,pos.w-b2-1,y); y += 2;
        setColor(g,theme.cwms); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y);
      }
    }
    g.setFont(font); // no bold
    y = (titley1+pos.h-titley2)/2 + maxheight/4; // LSN
    if (titlex1>0) {
      x = bw+tw;
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      g.drawString(title, x, y);
    }
    if (titlex2>0) {
      x = pos.w-titlewidth-bw-tw;
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      g.drawString(title, x, y);
    }
    setTextClip(g, panel, title, bp.x);  // keep text within shadowbox (Bug 2473)
    g.setColor(theme.cwfg); // Reset color back since it is used later
    String text = getValue(defitem,false);
    if (is(TOGGLE)) {
      text = Parser.mask2s(list,defitem);
      if (text.length()<=1) text = "";
      else text = text.substring(1,text.length()-1);
    }

    Color c = null;
    if (ctab!=null) c=(Color)ctab.get(text.toUpperCase());
    if (c==null) c=theme.cwfg;
    setColor(g,c); // Updated to use g in 3.9.0 for NXM-3213
    if (units!=null) text += " "+units;
    setTextClip(g, panel, text, bt.x);  // keep text within shadowbox (Bug 2473)
    g.drawString(text,bt.x,ty);
    g.setColor(theme.cwfg);
    drawArrows(g); // Updated to use g in 3.9.0 for NXM-3213
  }

  private void drawArrows (Graphics g) { // Updated with parameter g in 3.9.0 for NXM-3213
    drawDiamond(g, bc.x+bc.w/2, bc.y+bc.h/2, tw*2/3, mouseMode); // Updated to use g in 3.9.0 for NXM-3213
  }

  protected void paintMenu (Graphics g) { // Updated with parameter g in 3.9.0 for NXM-3213
    int x,y;
    int b2 = 2*bw;  // width of top + bottom or left + right borders
    int bf = is(SHADE)? SB_SHADE : is(INLINE)? SB_FLAT : SB_DEF;
    g.setFont(font);
    setColor(g,theme.cwfg); // Updated to use g in 3.9.0 for NXM-3213
    shadowbox(g,0,0,pos.w,pos.h,bf); // Updated to use g in 3.9.0 for NXM-3213
    setColor(g,theme.cwfg); // Updated to use g in 3.9.0 for NXM-3213
    x = pos.w/2 - titlewidth/2;
    boolean rollup = is(ROLLUP);
    if (titley1>0) {
      // draw titley1  (title at top of menu)
      int dh=titley1, j1=(int)(dh*.15);
      if (!is(FLAT)) {
        for (int i=1; i<=dh; i++) {
          if (i<j1) g.setColor(MColor.getColorMix(theme.cwbg,theme.cwbs,(double)i/j1));
          else      g.setColor(MColor.getColorMix(theme.cwbs,theme.cwms,(double)(i-j1)/(dh-j1-1)));
          g.drawLine(1,i,pos.w-2,i);
        }
        g.drawLine(0,0,pos.w,0);
      }
      if (is(FLAT)) {
        setColor(g,theme.cwti);
        g.setFont(fontb);
      }
      else if (rollup) {
        setColor(g,theme.cwti.darker()); // Updated to use g in 3.9.0 for NXM-3213
      } else {
        setColor(g,theme.cwbs); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(bw+1,dh,pos.w-bw-1,dh);
        setColor(g,theme.cwti);
      }
      y = bw+maxheight/2+ta/2-1;
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      g.drawString(title, x, y);
      // If this is not INLINE (INLINE removes borders to allow embedding in another display),
      // draw parallel lines just below the top border starting at 2 border widths in from the left
      // across to just before 2 border widths from the left
      y = bw+maxheight-2;
      if (is(FLAT) && !is(INLINE)) {
        setColor(g,theme.cwbs); // Updated to use g in 3.8.1 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y); y += 2;
        setColor(g,theme.cwms);
        g.drawLine(b2,y,pos.w-b2-1,y);
      }
    }
    if (rollup) return;
    g.setFont(fontb);
    int totalHeight = (isPopup) ? maxMenuHeight : pos.h;
    if (titley2>0) {
      // draw titley1  (title at bottom of menu)
      y = totalHeight-bw-maxheight/2+ta/2; // take into account maximum GMenu height - Since 3.7.0 for Bug 2825
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      g.drawString(title, x, y);
      // If this is not INLINE (INLINE removes borders to allow embedding in another display),
      // draw parallel lines just above the bottom border starting at 2 border widths in from the left
      // across to just before 2 border widths from the left
      // (allow for only part of the menu being shown when there is a scrollbar,
      // the titley2 being displayed right below last displayed menu item, and
      // this line ending right above that - Since 3.7.0 for Bug 2825)
      y = totalHeight-bw-maxheight-1;
      if (!is(INLINE)) {
        setColor(g,theme.cwbs); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y); y += 2;
        setColor(g,theme.cwms); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(b2,y,pos.w-b2-1,y);
      }
    }
    int heightForTitleX = (pos.h < maxMenuHeight) ? pos.h : maxMenuHeight;
    y = (titley1+heightForTitleX-titley2)/2 + maxheight/4;
    if (titlex1>0) {
      // draw titlex1 (title on left of menu)
      x = bw+tw; // at right of border by text width
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      g.drawString(title, x, y);
      // If this is not INLINE (INLINE removes borders to allow embedding in another display),
      // draw parallel lines just to the right of the left title and border? (TODO: why -4?) starting at 1 border
      // width below top border and titley1 down to 1 border width above the bottom title
      // (allow for only part of the menu being shown when there is a scrollbar,
      // the titley2 being displayed right below last displayed menu item, and
      // this line ending right above that - Since 3.7.0 for Bug 2825)
      x = bw+titlex1-4;
      if (!is(INLINE)) {
        setColor(g,theme.cwbs); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(x,titley1+b2,x,heightForTitleX-titley2-b2-1); x += 2;
        setColor(g,theme.cwms); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(x,titley1+b2,x,heightForTitleX-titley2-b2-1);
      }
    }
    if (titlex2>0) {
      // draw titlex2 (title on right of menu)
      x = pos.w-bw-titlex2+4+tw; // at left of border and titlex2
      setColor(g,theme.cwti); // Updated to use g in 3.9.0 for NXM-3213
      setTextClip(g, panel, title, x);  // keep text within shadowbox (Bug 2473)
      g.drawString(title, x, y);
      // If this is not INLINE (INLINE removes borders to allow embedding in another display),
      // draw parallel lines just to the left of titlex2 starting at 1 border width below
      // top border and titley1 down to 1 border width above the bottom title
      // (allow for only part of the menu being shown when there is a scrollbar,
      // the titley2 being displayed right below last displayed menu item, and
      // this line ending right above that - Since 3.7.0 for Bug 2825)
      x = pos.w-bw-titlex2+1;
      if (!is(INLINE)) {
        setColor(g,theme.cwbs); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(x,titley1+b2,x,heightForTitleX-titley2-b2-1); x += 2;  // below top border and titley1
        // draw a line parallel to the first
        setColor(g,theme.cwms); // Updated to use g in 3.9.0 for NXM-3213
        g.drawLine(x,titley1+b2,x,heightForTitleX-titley2-b2-1);
      }
    }
    g.setFont(font);
    g.setColor(theme.cwfg);
    for (int i=1; i<=items; i++) refreshItem(g,i,false); // Updated to use g in 3.9.0 for NXM-3213
    if (shouldScroll) {      // Since NXM 3.5.1 BUG 2825 if shouldScroll, add scrollbar
      paintScroll(g);
    }
  }

  /**
   * Paint a Vertical Scroll Bar
   *
   * @param g Graphics for paint
   * @since NeXtMidas 3.7.0 for bug 2825
   */
  private void paintScroll (Graphics g) {     // Since NXM 3.5.1 BUG 2825
    int x=pos.w-SCROLLBAR_WIDTH-bw-titlex2+1; // paint inside border and inside any right hand side title
    int y=titley1;  // paint on right hand side below title
    int flags = MWindow.SLIDER_INSET | MWindow.SLIDER_NOTRACK | (shouldScroll? MWindow.SLIDER_ACTIVE : 0);
    int maxHeight = maxMenuHeight - titley1 - titley2 - 2*bw;
    MWindow.drawVSlider(x,y,SCROLLBAR_WIDTH,maxHeight, scrollY/maxHeight,flags, g,theme);
  }

  /** Is TOGGLE item selected (i=1..N). */
  boolean isToggled (int i) { //package-private used in GMenu and GLegend
    return ((1<<(i-1))&defitem) != 0;
  }

  /** Sets the Toggle mask of items.
      @param mask The bit mask value to set to
      @since NeXtMidas 3.5.1
   */
  public void setNoStateMask (int mask) {
    noStateMask = mask;
    flags |= ASSUMESTATE;
  }
  private boolean isNoState (int i) {
    return ((0x1<<(i-1)) & noStateMask) != 0;
  }

  /** Sets the Disable mask of items.
      @param mask The bit mask value to set to
      @since NeXtMidas 3.5.1
   */
  public void setDisableMask (int mask) {
    disableMask = mask;
  }
  private boolean isDisable (int i) {
    return ((0x1<<i) & disableMask) != 0;
  }

  /** Sets the color of the selected text in the GMenu.
      @param c The color to set to
      @since NeXtMidas 2.9.0
   */
  public void setSelectedTextColor (Color c) {
    jtexti.setSelectedTextColor(c);
  }

  /** Sets the color of the selected text.
      @param color The color to set to
      @since NeXtMidas 2.9.0
   */
  public void setSelectedTextColor (String color) {
    setSelectedTextColor(MColor.getColor(color));
  }

  /** Gets the color of the selected text.
      @return The color
      @since neXtMidas 2.9.0
   */
  public String getSelectedTextColorString () {
    return MColor.toString(getSelectedTextColor());
  }
  /** Gets the color of the selected text in the GMenu.
      @return The color
      @since neXtMidas 2.9.0
   */
  public Color getSelectedTextColor () {
    return jtexti.getSelectedTextColor();
  }

  /** Sets the background selection color of the textfield.
      @param c The Color to set
      @since NeXtMidas 2.9.0
   */
  public void setSelectionColor (Color c) {
    jtexti.setSelectionColor(c);
  }

  /** Sets the background selection color of the selection.
      @param c The Color to set
      @since NeXtMidas 2.9.0
   */
  public void setSelectionColor (String c) {
    setSelectionColor(MColor.getColor(c));
  }

  /** Gets the background color of the selection.
      @return The color
      @since NeXtMidas 2.9.0
   */
  public String getSelectionColorString () {
    return MColor.toString(getSelectionColor());
  }

  /** Gets the background color of the text field selection.
      @return The color
      @since NeXtMidas 2.9.0
   */
  public Color getSelectionColor () {
    return jtexti.getSelectionColor();
  }

  /** Is TOGGLE item selected (i=1..N). */
  int getIndent (int i) { //package-private used in GMenu and GLegend
    return 0;
  }

  /**
   * @param i
   * @param overwrite
   * @deprecated since NeXtMidas 3.9.0
   */
  @Deprecated
  protected void refreshItem (int i, boolean overwrite) {
    refreshItem(g, i, overwrite);
  }

  /**
   * Refresh / redraw an item by index
   * @param g the graphics context
   * @param i index of item
   * @param overwrite
   */
  protected void refreshItem (Graphics g, int i, boolean overwrite) {
    if (is(ROLLUP)) return;
    if (g==null) return;  // Not ready to paint yet

    int totalHeight;
    int maxHeight;
    // If popup, y location to display (has been scrolled down to this point) - Since 3.7.0 for bug 2825
    if (isPopup) {
      totalHeight = maxMenuHeight;
      maxHeight = totalHeight - titley1 - titley2 - 2*bw;
      startDisplayAt = (int)(pos.h * (scrollY/maxHeight)); // overall height of full list of items * percent down list
      startDisplayAt = Math.min(pos.h - maxHeight , startDisplayAt); // do not scroll down below last item
      startDisplayAt = Math.max(0, startDisplayAt); // do not back up before first item
    }
    else {
      totalHeight = pos.h;
      maxHeight = totalHeight - titley1 - titley2 - 2*bw;
    }

    boolean standout,butt=is(BUTTON),togg=is(TOGGLE),chk=is(CHKBOX);
    String tmpstr = getItem(i);
    if(!is(CHOICE)) { // Since NeXtMidas 3.5.4 for BUG 2892
      getitemloc (i,posi);
    }
    if (togg) standout = isToggled(i);
    else standout = (i==defitem) && !is(NODEPRESS);
    if (overwrite) shadowbox(g, posi.x, posi.y, posi.w, posi.h, SB_DEF);
    if (butt) shadowbox(g, posi.x+2, posi.y+2, posi.w-4, posi.h-4, standout?SB_REV:SB_HILIT);
    int x=posi.x+tw, y=posi.y+maxheight/2;
    if (chk) {
      x+=(tw/2); int w = tw*3/2;
      shadowbox(g, x-w/2, y-w/2, w, w, standout?SB_CHKBOX:SB_HILIT);
      x+=(3*tw/2);
    }
    if (butt) x += (posi.x+posi.w-tw-x-stringWidth(tmpstr))/2; // center the text
    Color c= theme.cwfg;

    // If the color table exists, assign color based on the value in the table
    if (ctab!=null) c=(Color)ctab.get(tmpstr.toUpperCase());

    // If this item is the selected/default item and is not a checkbox or a button
    // then show it in the highlight color.  (Note: disabled items can not be selected)
    // (fixed logic to display selected item when there are disabled items in 3.7.1 for NXM-2871)
    if (standout && !butt && !chk) {
      c=theme.cwfh; // if not disabled, set item to foreground highlight color
    }

    if (c==null) c=theme.cwfg; // default is foreground color

    setColor(g,c);
    setTextClip(g, panel, tmpstr, x); // BUG 2473 CHANGE (keep text from exceeding shadowbox width)
    if (y > titley1 + bw && y+ta/2-1 < totalHeight - titley2 - bw) {  // preserve area for title - Since 3.7.0 for bug 2825
      g.drawString(tmpstr, x, y+ta/2-1);
    }
    if (i==curitem) {
      shadowbox(g,posi.x, posi.y, posi.w, posi.h, butt?SB_LINE:SB_HILIT);
    }
    if (altwidth>0) {
      posi.x += posi.w-tw-altwidth; posi.w=altwidth;
      posi.y += 4; posi.h-=8;
      refreshItemAlt(g,i,posi);
    }
  }

  /**
   * @param i    The index in the entries (only used by {@link GLegend})
   * @param posi The MBox that defines the location of the shadow arrow by the {@link MBox#x}
   * @deprecated since NeXtMidas 3.9.0 use {@link GMenu#refreshItemAlt(Graphics, int, MBox)}
   */
  @Deprecated
  protected void refreshItemAlt (int i, MBox posi) {
    shadowarrow(posi.x+pos.w/2, pos.y+pos.h/2, tw*2/3, 'r');
  }

  /**
   * @pram g     The graphics object to draw on
   * @param i    The index in the entries (only used by {@link GLegend})
   * @param posi The MBox that defines the location of the shadow arrow by the {@link MBox#x}
   */
  protected void refreshItemAlt (Graphics g, int i, MBox posi) {
    shadowarrow(g, posi.x+pos.w/2, pos.y+pos.h/2, tw*2/3, 'r');
  }

  /**
   * Highlight an item by index
   * @param i index of the item to highlight
   */
  protected void highlight (int i) {
    if (i==curitem) return;
    if (curitem!=0) {
      getitemloc(curitem,posi);
      refresh(posi);
    }
    if (i==0) curitem=0;
    else if (i>0) {
      curitem = i;   // set curitem to this valid index, so it will be highlighted
      getitemloc(curitem,posi);
      refresh(posi);
    } else if (i<0) {
      getitemloc(-i,posi);
      refresh(posi);
    }
  }

  /**
   * Get the location of an item by index
   * @param index index of item
   * @param posj  MBox giving position, height, and width of the item
   */
  private void getitemloc (int index, MBox posj) {
    int indt = getIndent(index) * pixPerIndent; // indentation in pixels
    int ir, ic;
    ic = (index-1) / rows;
    ir = (index-1) - ic*rows;
    posj.x = ic*maxwidth  + bw + titlex1 + indt;
    posj.y = ir*maxheight + bw + titley1 - startDisplayAt; // Adjust for scroll position - Since 3.7.0 for Bug 2825
    posj.w = maxwidth - indt;
    posj.h = maxheight;
    if (is(INLINE) && pGroup!=null) posj.x += 4;
  }

  /**
   * Get Index of Menu Item based on current relative coordinated
   * @param x relative x to this GMenu
   * @param y relative y (not to display, but taking into account scrolling in Menus)
   * @return index of Menu item selected
   */
  private int getindex (int x, int y) {
    int ir, ic, i;
    if (y < titley1 || y > pos.h-titley2) return 0;
    if (x < titlex1 || x > pos.w-titlex2) return 0;
    ic = (x-titlex1-bw) / maxwidth;
    if (shouldScroll) {
      ir = (y-titley1-bw+startDisplayAt) / maxheight;  //todo 3.7.0
      //System.out.println("GMenu.getIndex ir:"+ir+" StartDisplayAt:"+startDisplayAt+" y:"+y+" bw:"+bw+" maxheight:"+maxheight);
    }
    else {
      ir = (y-titley1-bw) / maxheight;
    }
    ic = Math.max(0,Math.min(ic,columns-1));
    ir = Math.max(0,Math.min(ir,rows-1));
    i  = Math.max(0,Math.min(ic*rows+ir+1,items));
    altzone = (altwidth>0) && (x>((ic+1)*maxwidth-tw-altwidth));
    return i;
  }

  @Override
  public String getLegend () {
    String rm = "";
    if (!is(CHOICE)) return "(LM=Select)";
    if (!is(PERM)) rm = ",RM=Cancel";
    if (mouseMode==0) {
      if (!is(PERM)) return "(MM=Configure, RM=Cancel)";
      else return "(MM=Configure)";
    }
    else if (mouseMode==1) return "(LM=UP" + rm + ")";
    else if (mouseMode==2) return "(LM=Down" + rm + ")";
    else if (mouseMode==3) return "(MM=Configure" + rm + ")";
    else if (is(INPUT)) return "(LM=PopupMenu,RM=EditItem)";
    else return "(LM=PopupMenu" + rm + ")";
  }

  @Override
  protected int processEventMessage (String name, int info, Object data) {
    if (is(CHOICE)) processChoiceEventMessage (name,info,data);
    else processMenuEventMessage (name,info,data);
    return -1;
  }

  protected int processChoiceEventMessage (String name, int info, Object data) {

    if (name.equals("KEYPRESS")) {
      handleKeyPress(info, (String)data);
    } else if (name.equals("POINTER")) {
      int mm=0;
      if (info<0) mm=0;  // 4.1.2 hover readouts allowed with NOEDIT
      else if (px>bc.x+bc.w) mm=8;
      else if (px<=bc.x) mm=0;
      else if (is(TOGGLE)) { }
      else if (py>bc.h/2+titley1) mm=2; // LSN: BUG 2015
      else mm=1;
      if (mm!=mouseMode) {
        mouseMode=mm;
        refresh(bc);
        if (info==0) info=1;
      }
      if (is(VBOXMO) && Math.abs(info)==9) refreshValue();
      formatReadOut(info);
    }
    if (!name.equals("BUTTON")) return -1;
    if (button==1 && px > bt.x && items>0) {
      if (gchoice==null) {
        int pflags = flags & (NCOL|TOGGLE);
//        if (is(TOGGLE)) pflags |= STICKY;
        gchoice = new GMenu (this,"GCHOICE",list,defitem,pflags,this);
        if (is(NCOL)) gchoice.setColumns(nc);
      }
      gchoice.ctab = ctab;
    }
    // LSN: BUG 2015 - Bring up the configuration when middle clicking on the
    // title similar to menu
    else if (button==2 && curitem<=0) configure();
    //move to here so rt. mouse click for close is captured before next code block
    //else if (button==3 && !is(PERM)) {  // Changed to line below for 2.7.4 (BUG 2081)
    else if (button==3 && !is(PERM) && !is(INPUT)) {
      selectChoice(defitem);
      close();
    }
    // REMOVED for DR541292-1: Gcontrol popups are ridiculously hard to use.
    //This code block incremented/decremented the widget's value with left/right mouse button.
    else if (button>0 && mouseMode!=0 && mouseMode!=8) {
      int item = defitem;
      int mitem = (is(INPUT) && itemv[0].length()>0)? 0 : 1;
      if (button==1 && mouseMode==1) item = Math.max(defitem-1,mitem);
      else if (button==1 && mouseMode==2) item = Math.min(defitem+1,items);
      if (item!=defitem) selectChoice (item);
    }

    else if (button==3 && bt.within(px,py) && is(INPUT)) {
      setupTextField();
    }

    formatReadOut(1);
    return 1;
  }

  /** This method created to move the KEYPRESS message handling code out of {@link #processMenuEventMessage(String, int, Object)}
      so that it can be used by {@link #processChoiceEventMessage(String, int, Object)} as well. See Bug 220.
      @param keyCode The KeyEvent code value for the key
      @param keyText String representation of the key pressed
      @since NeXtMidas 2.9.0
   */
  private void handleKeyPress (int keyCode, String keyText) {
    int diff=0;
    if (rows==0) rows=1;    // avoid divide by zero later on
    switch (keyCode) {
    case KeyEvent.VK_DOWN:  diff=1;     break;
    case KeyEvent.VK_UP:    diff=-1;    break;
    case KeyEvent.VK_RIGHT: diff=rows;  break;
    case KeyEvent.VK_LEFT:  diff=-rows; break;
    case KeyEvent.VK_ENTER: select(curitem,1); formatReadOut(1); break;
    default:
      for (int i=1; i<=items; i++) {
        String tmpstr = getValue(i,!is(CS));
        if (tmpstr.startsWith(keyText)) { select(i,1); break; }
      }
    }
    if (diff!=0) highlight( ((curitem+diff-1+items)%items)+1 );
  }

  protected void setupTextField () {
    jtexti = new MJTextField ("",20);
    jtexti.setFont(font);
    jtexti.setForeground(theme.cwfg);
    jtexti.setBackground(theme.cwbg);
    jtexti.addActionListener(this);
    jtexti.setText(getValue(defitem,true));
    panel.add(jtexti);
    jtexti.requestFocus();
    int hd = 2;
    jtexti.setBounds(bc.x+hd, bc.y+hd, bc.w+bt.w-hd-hd, bc.h-hd-hd);
  }

  protected int processMenuEventMessage (String name, int info, Object data) {
    if (name.equals("POINTER")) {
      int extraMarginX = 2; // allow small extra margin for pointing to scrollbar
      if ((shouldScroll && ((px > pos.w - titlex2 - bw - SCROLLBAR_WIDTH - extraMarginX) ||
          (py+ta/2 > maxMenuHeight - titley2- bw -1)))) {
        // Scrollbar is being pointed to or area below displayed portion of menu, so un-highlight any buttons - since 3.7.0 for bug 2825
        // (Note: The extra -1 is to try to catch menu items that are just out of range, since the top might have
        // been within range, but not the bottom)
        highlight (0);
      }
      else {
        int i = getindex (px,py);
        //System.out.println("GMenu.processEventMessage highlight i:"+i+" px:"+px+" py:"+py+" startDisplayAt:"+startDisplayAt+" titley1"+titley1+" dragging:"+dragging+" info:"+info);
        if (dragging!=0) { }
        else if (info<0) highlight(0);
        else highlight (i);
        formatReadOut(info);
      }
    }
    else if (name.equals("BUTTON")) {
      // If we have a scrollbar, check to see if this click is in the area covered
      // by the scrollbar, and therefore NOT a selection of an Item (button)
      // (Since 3.7.0 for Bug 2825)
      // Note: Clicking above scrollbar slider/knob pages up
      //       Clicking below scrollbar slider/knob pages down
      int leftOfScrollBar = pos.w - titlex2 - bw - SCROLLBAR_WIDTH; // x just to left of scrollbar
      int extraMarginX = 2; // allow small ectra margin on click of scrollbar
      if (shouldScroll && px > leftOfScrollBar - extraMarginX &&
          px < leftOfScrollBar + SCROLLBAR_WIDTH + 2*extraMarginX) {
        int maxHeight = maxMenuHeight - titley1 - titley2;
        int yAdjustedForTitle = py - titley1;
        if (yAdjustedForTitle > scrollY + MWindow.SCROLL_KNOB_SIZE) {
          // page down
          scrollY += maxHeight *((float)maxHeight/pos.h); // 14OCT
          scrollY = Math.min(scrollY,maxHeight);
          layout();
        }
        else if (yAdjustedForTitle < scrollY){
          // page up
          scrollY -= maxHeight *((float)maxHeight/pos.h);  // 14 oct
          scrollY = Math.max(scrollY,0);
          layout();
        }
      }
      else {
        // Find the item (button) selected
        int i = getindex (px,py);
        if (info==0) highlight(-i);
        else if (info==1 && py<titley1) setFlags(flags^ROLLUP);
        else if (info==2 && curitem<=0) configure();
        else if (info==1 && altzone)    selectAlt(curitem,info);
        else                            select(curitem,info); //NOTE: curitem set when pointed to (in handling "POINTER" message)
        formatReadOut(1);
      }
    }
    else if (name.equals("KEYPRESS")) {
      handleKeyPress(info, (String)data);
    }
    else if (name.equals("DRAG")) {
      // Scroll down based on user dragging of slider/knob in scrollbar - Since 3.7.0 for Bug 2825
      MBox dragBox = (MBox)data;
      int x = dragBox.x;
      int extraMarginX = 2;  // allow small extra margin on drag
      int leftOfScrollBar = pos.w - titlex2 - bw - SCROLLBAR_WIDTH; // x just to left of scrollbar
      if (shouldScroll && x > leftOfScrollBar - extraMarginX  &&
          x < leftOfScrollBar + SCROLLBAR_WIDTH + 2*extraMarginX) {
        int y = dragBox.y;
        int h = dragBox.h;
        int yAdjustedForTitle = y - titley1;
        //System.out.println("GMenu DRAG y:"+y+" h:"+h+" slidingKnob:"+slidingKnob+" yAdjustedForTitle:"+yAdjustedForTitle+" scrollY:"+scrollY);
        // Check if the knob/slider of the scrollbar is just starting to slide
        // 1) should not already be sliding
        // 2) the y value of the dragbox should be over the knob/slider
        int extraMarginY = 15;  // allow a small extra margin on grabbing knob
        if (!slidingKnob && yAdjustedForTitle >= scrollY - extraMarginY &&
            yAdjustedForTitle <= scrollY + MWindow.SCROLL_KNOB_SIZE + extraMarginY) {
          scrollYAtStartOfSlide = scrollY;
          slidingKnob = true;
          setScrollNoMove(true); // disable movement of GMenu based on drag box (which is being used for scrolling)
        }
        if (slidingKnob ) {
          scrollY = scrollYAtStartOfSlide + h;
          scrollY = Math.max(0, scrollY);
          int maxHeight = maxMenuHeight - titley1 - titley2;  //14OCT
          scrollY = Math.min(maxHeight, scrollY);
          layout();
        }
        //System.out.println("GMenu.processMenuEventMessage name"+name+" info:"+info+" data x:"+x+" data y:"+y+"data h:"+h+" width text:"+pos.w+" scrollY:"+scrollY+" panelY:"+panel.getAlignmentY()+" px:"+px+" py:"+py);
      }
    }
    else if (name.equals("DRAGBOX")) {
      if (slidingKnob) {  // Since 3.7.0 for Bug 2825
        // mouse has been release and so no longer sliding knob
        slidingKnob = false;
      }
      //System.out.println("GMenu.processMenuEventMessage name"+name+" info:"+info+" data:"+data+" px:"+px+" py:"+py);
    }
    else return -1;
    return 1;
  }

  /** Set the flags, remove any that are invalid for specified type.
      @since NeXtMidas 2.7.0
   */
  @Override
  public void setFlags (int flags) {
    if (is(INLINE)) flags &= ~ROLLUP;
    if (flags==this.flags) return;
    this.flags = flags;
    setup();
    resize(1);
    refresh();
  }

  /** process message. */
  @Override
  public int processMessage (Message msg) {
    if (status==EXIT) { }
    else if (msg.name.equals("GCHOICE")) {
      selectChoice(msg.info);
      if (!is(PERM)) close();
    }
    else if (msg.name.equals("GMENU.CFG")) {
      String text = (String)msg.data;
      if (text.equals("SETCOLUMNS>")) {
        new GValue (this,"GMenu.columns",nc,-1,8,1,0,this);
      } else {
        setFlags(msg.info);
      }
    }
    else if (msg.name.equals("GMENU.COLUMNS"))
      setColumns(Convert.o2l(msg.data));
    else
      return super.processMessage(msg);
    return -1;
  }

  /**
   * Select the item in the menu
   * @param i The index of the item
   * @param button The int value of the button clicked
   */
  public void select (int i, int button) {
    String sitem = "NULL";
    if (disabledItems != null && disabledItems.get(i)!=null) return; // do not allow selection of disabled items
    if (button==3) { if (is(TOGGLE)) i=0; else i=defitem; }
    if (i>=0 && i<=items) sitem = getValue(i,!is(CS));
    boolean stateChange = (defitem!=0 || is(ASSUMESTATE)) && !isNoState(i);
    //if (is(NOEDIT));   // JWB - removed this to allow programmatic editing (seee BUG 1900)
    //else               // JWB - removed this to allow programmatic editing (seee BUG 1900)
    if (is(TOGGLE) && !sitem.endsWith(">")) {
      if (button!=3) defitem = defitem ^ (1<<(i-1));
      if (is(TOGGLELIST)) sitem = Parser.mask2s(list,defitem);
      sendMessage (label,defitem,sitem);
    }
    else if (is(TOGGLE) && button==2 && sitem.endsWith(">")) {  // BUG 910: in case of middle mouse button
      if (is(TOGGLELIST)) sitem = Parser.mask2s(list,defitem);
      sendMessage (label,defitem,sitem);
    }
    else if (button==3 && (defitem==0||label.equals("GCHOICE"))) {
      // dont present message
    }
    else {
      if (i!=0 && stateChange) defitem=i; // for Dialog handlers that getValue()
      sendMessage (label,i,sitem);
    }
    if ( is(PERM) || (is(STICKY) && button!=3) || (!is(PERM|STICKY) && button==2) ) {
      if (i<0 || is(TOGGLE)) { } // do nothing
      else if (is(NOSTATE)) defitem=0;
      else if (stateChange) defitem=i;
      refresh();
    }
    else close ();
  }

  /**
   * Select the item in the menu with Alt ??
   * @param i The index of the item
   * @param button The int value of the button clicked
   */
  public void selectAlt (int i, int button) {
    String sitem = "NULL";
    if (button==3) { if (is(TOGGLE)) i=0; else i=defitem; }
    if (i>0 && i<=items) sitem = getValue(i,!is(CS));
    sendMessage (label+".ALT",i,sitem);
  }

  @Override
  public boolean poll () {
    if (monitorName == null || !is(MONITOR)) return false;
//DR 1900: JPH -- mods to handle string values as well as legacy item numbers
//    double ditem = M.results.getD(monitorName);
//    int item = (int) ditem;
    String sval = M.results.getS(monitorName);
    int item = 0;
    if (StringUtil.isNumber(sval)) {
      item = (int)M.results.getD(monitorName);
    } else {
      item = getItemIndex(sval);
    }
    if ((item == defitem) || (item<0 || item>items)) return false;
    defitem = item;
    sendMessage(label,item,getValue(item,!is(CS)));
    refresh();
    return true;
  }

  private void selectChoice (int item) {
    //if (is(NOEDIT) || item<0) return; // JWB - removed this to allow programmatic editing (see BUG 1900)
    if (item<0) return;
    if (item>items && !is(TOGGLE)) return;
    if (disabledItems != null && disabledItems.get(item)!=null)  return;  // Since 2.9.1 (Bug 2326)
    defitem = item;
    sendMessage (label,item,getValue(item,!is(CS)));
    refresh();
  }

  //wrapper to allow for monitor put at the same time a message is sent (DR 1900)
  private void sendMessage(String name, int info, Object data) {
    if (monitorName != null && is(MONITOR)) M.results.put(monitorName,data);
    sendWidgetMessage(name,info,data);
  }

  /** Determine if mask is set when in TOGGLE mode.
      @param  maskName Mask name
      @return TRUE if mask is set, FALSE otherwise
   */
  public boolean isSet (String maskName) {
    boolean maskIsSet = false;
    if (is(TOGGLE)) {
      int i = Parser.mask(list, maskName, Parser.MASK_NONE);
      if ((i & defitem) !=0) maskIsSet = true;
    } else {
      M.warning("GMenu.isSet method only intended for /TOGGLE menus");
    }
    return maskIsSet;
  }

  @Override
  public Object getValue () {
    if (is(TOGGLE)) {
      return Parser.mask2s(list,defitem);
    } else {
      return getValue(defitem,!is(CS));
    }
  }

  /** Get the item at the given index
   *
   *  @param i The index of the item
   *  @return The item at the given index
   */
  public String getItem (int i) {
    if (i<0 || i>items) return null;
    return itemv[i];
  }

  /** Get the value of the item at the given index
   *
   *  @param item The index of the item
   *  @param uc true for upper case result, false for the raw value
   *  @return The value of the item at the given index
   */
  public String getValue (int item, boolean uc) {
    if (item<0 || item>items) return "";
    String sitem = getItem(item);
    if (uc) sitem = sitem.toUpperCase();
    return sitem;
  }

  @Override
  public void setAction (String text) {
    setActionType();  // check if mouse click triggered action
    int indx = getItemIndex(text);
    if (indx<0 && is(INPUT)) { itemv[0]=text; indx=0; }
    if (indx<0) { }
    else if (is(CHOICE)) selectChoice (indx);
    else select(indx,1);
  }

  /** Find the index of the item with the given value.
      @param text the value of the item
      @return The index of the item with the given text
  */
  public int getItemIndex (String text) {
    if (text==null || text.length()==0) return -1;
    boolean ofNumbers = false;
    boolean isNumber = StringUtil.isNumber(text);
    String uctext = text.toUpperCase();
    // first check for exact match
    for (int i=1; i<=items; i++) {
      String value = getItem(i).toUpperCase();
      if (value.equals(uctext) && !is(TOGGLE)) { return i; }  // isToggle check added for Bug 2320
      if (!ofNumbers && StringUtil.isNumber(value)) ofNumbers=true;
    }
    // now check for other number matches
    if (isNumber && ofNumbers && text.endsWith(".0")) {
      // OK remove extraneous fp decimal - will still match abbreviation
      String tntext = text.substring(0,text.length()-2);
      for (int i=1; i<=items; i++) {
        String value = getItem(i).toUpperCase();
        if (value.equals(tntext)) { return i; }
      }
    }
    // check for it being an index
    if (isNumber && text.length()==1 && !ofNumbers && !is(INPUT)) {
      return Convert.s2l(text);
    }
    // check for it being a MASK
    if (is(TOGGLE)) {
      if (SET_TOGGLE_WITH_MASK) {
        int i = Parser.mask(list, text, defitem);
        if (i > 0) return i;
      } else {
        int i = Parser.mask(list, text, Parser.MASK_NONE);
        if (i > 0) return i;
      }
    }
    // then check for abbreviation, but not on numbers
    if (!isNumber && !ofNumbers) {
      for (int i = 1; i <= items; i++) {
        String value = getItem(i).toUpperCase();
        if (value.startsWith(uctext)) { return i; }
      }
    }
    return -1;
  }

  @Override
  public void setValue (String text) {
    if (text==null || text.length()==0) return;
    // OK remove extraneous fp decimal - will still match abbreviation
    boolean strippedDecimal = false;
    if (text.endsWith(".0") && StringUtil.isFloat(text)) {
      text = text.substring(0,text.length()-2);
      strippedDecimal = true;
    }
    boolean ofNumbers = false; // GMenu has items that start with a digit
    boolean isNumber = StringUtil.isNumber(text); // this value starts with a number
    String uctext = text.toUpperCase();
    // first check for exact match
    for (int i=1; i<=items; i++) {
      String value = getItem(i).toUpperCase();
      if ((value.equals(uctext) || (strippedDecimal && value.equals(uctext+".0"))) && !is(TOGGLE)) { setValue(i); return; }  // isToggle check added for Bug 2320
      if (!ofNumbers && StringUtil.isNumber(value)) ofNumbers=true;
    }
    // check for it being an index (i.e. text is single digit and none of the GMenu values start with a number
    if (isNumber && text.length()==1 && !ofNumbers && !is(INPUT)) {
      if (is(TOGGLE)) {
        M.deprecate("GMENU: setting a toggle mask with the number ["+text+"] has been deprecated since NeXtMidas 2.9.0. "
                   +"Use text String from list: ["+list+"]");
      }
      setValue(Convert.s2l(text));
      return;
    }
    // check for it being a MASK
    if (is(TOGGLE)) {
      if (SET_TOGGLE_WITH_MASK) {
        int i = Parser.mask(list, text, defitem);
        if (i > 0) { setValue(i); return; }
      } else {
        int i = Parser.mask(list, text, Parser.MASK_NONE);
        if (i > 0) { setValue(i); return; }
      }
    }
    // then check for abbreviation, but not on values that start with a number
    if (!isNumber && !ofNumbers) {
      for (int i=1; i<=items; i++) {
        String value = getItem(i).toUpperCase();
        if (value.startsWith(uctext)) {
          setValue(i);
          return;
        }
      }
    }
    // else set current to actual text (only CHOICE will use it)
    itemv[0] = text;
    setValue(0);
  }

  /**
   * Set the value of the item at the given index
   * @param item the index of the item
   */
  public void setValue (int item) {
    if (item==-1 && is(TOGGLE)) item = (1 << items)-1;
    defitem = item;
    refresh();
  }

  /**
   * Set the number of columns for the menu
   * @param nc the number of columns
   */
  public void setColumns (int nc) {
    this.nc = nc;
    if (nc==0) this.flags &= ~NCOL;
    else this.flags |= NCOL;
    setup();
    resize(1);
    refresh();
  }

  /**
   * Add item to the list
   * @param item the value of the item to be added
   */
  public void addItem (String item) {
    if (items>0) list += ',';
    list += item;
    setItems(list);
  }

  /**
   * Add item to the list
   * @param item The value of the item
   * @param toggleOn is the item going to have the flag for Toggle
   */
  public void addItem (String item, boolean toggleOn) {
    addItem(item);
    if (toggleOn && is(TOGGLE)) {
      int mask = 1 << (items-1);
      defitem = defitem | mask;
      setValue(defitem);
    }
  }

  /**
   * remove item from the menu
   * @param item The string value of the item
   */
  public void remItem (String item) {
    int removeIndex = 0;
    int newdi = 0;
    int comparemask = 0;
    int addmask = 0;
    int addmaskindex = 0;

    //remove the item from the item vector
    for (int i=1; i<=items; i++) {
      if (item.equalsIgnoreCase(itemv[i])) {
        removeIndex = i;
        itemv[i]=null;
        if (disabledItems!=null && disabledItems.get(i)!=null) {
          disabledItems.remove(i);
          for (int index=i+1; index<disabledItems.size(); index++) {   // keep the disabled items index up to date
            if (disabledItems.get(index)!=null) {
              disabledItems.put(index-1, this.getValue(index, false));
            }
          }
        }
      }
    }

    if (removeIndex<1) return;

    //create and set new item list
    itemv2list();
    setItems(list);

    //If not TOGGLE, update defitem if removed Item is defitem
    //If TOGGLE, update defitem by creating new mask.
    //After removing item, "remove" corresponding
    //bit. "Move" remaining bits right.
    if (!is(TOGGLE) && removeIndex == defitem) {
      defitem--;
    }
    else if (is(TOGGLE)) {
      for (int i=1; i<=(items+1);i++ ) {
        comparemask = 1 << (i-1);
        if (i == removeIndex) continue;
        addmaskindex++;
        addmask = 1 << (addmaskindex-1);
        if ((defitem & comparemask) == comparemask) {
          newdi += addmask;
        }
      }
      defitem = newdi;
    }
    setValue(defitem);
  }

  /**
   * Set the value at a given index
   * @param index The index of the item
   * @param item the string value of the item
   */
  public void setItem (int index, String item) {
    if (index<1 || index>items) return;
    itemv[index] = item;
    itemv2list();
    setItems(list);
  }

  private void itemv2list () {
    list = null;
    for (int i=1; i<=items; i++) {
      if (itemv[i]==null) { }
      else if (list==null) list = itemv[i];
      else list += ","+itemv[i];
    }
    if (list==null) list = "";
  }

  /** Get the index of the selected item
   *  @return the index of the selected item. */
  public int getItemIndex () {
    return defitem;
  }

  /**
   * set the color table
   * @param t the color table
   */
  public void setItemColorTable (Table t) {
    ctab = t;
  }

  /** Set the colors of the MENU items.
      @param t Table of MENUITEM=COLOR key values
  */
  public void setItemColors (Table t) {
    if (ctab==null) ctab=new Table();
    for (Table.Iterator ti=t.iterator(); ti.getNext(); ) {
      ctab.put(ti.key,MColor.getColor((String)ti.value));
    }
    refresh();
  }
  /** Set the color of an individual item.
      @param item Menu item name
      @param color Color name
  */
  public void setItemColor (String item, String color) {
    setItemColor(item, MColor.getColor(color));
  }
  /** Set the color of an individual item.
      @param item Menu item name
      @param color Color object
  */
  public void setItemColor (String item, Color color) {
    if (color == null && ctab != null) {  // remove item from color table if set Color is null - since 3.7.1 for NXM-2871
      ctab.remove(item);
    }
    else {
      if (ctab==null) ctab=new Table();
      ctab.put(item,color);
    }
    refresh();
  }

  /** Disable one or more items on the menu, but leave them visible.
      @param items The text of the items, comma separated, to disable
      @see #disableItem(String)
      @since NeXtMidas 2.9.1 (Bug 2326)
  */
  public void setDisabledItems (String items) {
    if (items==null || items.equals("")) return;
    String list = items;
    int i = list.indexOf(',');
    if (i<0) { disableItem(list); return; }  // single item only

    while (i>0) {
      disableItem(list.substring(0,i));
      list = list.substring(i+1);
      i = list.indexOf(',');
      if (i<0) { disableItem(list); return; }  // single item only
    }
  }

  /** Disable an item on the menu, but leave it visible. If the item to disable
      is the default item, set the default item to none (0).
      @param item The text of the item to disable
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void disableItem (String item) {
    int iItem = getItemIndex(item);
    if(iItem < 0) { // Since NeXtMidas 3.5.4 for BUG 2892
      M.warning("Attempt to disable an item that does not exist: '" + item + "'.");
      return; //nothing to do - item being disabled does not exist!
    }
//    if ((defitem & iItem)==iItem) defitem &= ~iItem;        // Replaced by line below
    if (defitem == iItem) defitem &= ~iItem;                // Since NeXtMidas 3.5.1 for BUG 2839
    item = item.toUpperCase();                              // Since NeXtMidas 3.5.4 for BUG 2892
    saveItemColor(item);
    setItemColor(item, disabledItemColor);
    if (disabledItems==null) disabledItems = new HashMap<>();
    disabledItems.put(iItem, item);
//    refreshItem(iItem, true);
    refresh(); // Changed to refresh as refreshItem because of the lack of graphics object. Since 3.9.0
  }

  /** Re-enable an item that has been disabled.
      @param item The text of the item to re-enable
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void enableItem (String item) {
    int iItem = getItemIndex(item);
    if(iItem < 0) { // Since NeXtMidas 3.5.4 for BUG 2892
      M.warning("Attempt to enable an item that does not exist: '" + item + "'.");
      return; //nothing to do - item being enabled does not exist!
    }
    if (disabledItems != null && disabledItems.get(iItem)!=null) disabledItems.remove(iItem);
    restoreColor(item.toUpperCase());
//    refreshItem(iItem, true);
    refresh(); // Changed to refresh as refreshItem because of the lack of graphics object. Since 3.9.0
  }

  /** Check to see if an item has been disabled.
      @param item The text of the item to check
      @return True if enabled, false if disabled
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public boolean isItemEnabled (String item) {
    boolean enabled = true;
    int iItem = getItemIndex(item);
    if (disabledItems != null && disabledItems.get(iItem)!=null) enabled = false;
    return enabled;
  }

  /** Enable all the items in the menu.
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void enableAllItems () {
    for (int index=1; index <= items; index++) {
      String item = getItem(index);
      if (!isItemEnabled(item)) enableItem(item);
    }
  }

  /** Disable all the items in the menu.
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void disableAllItems () {
    for (int index=1; index <= items; index++) {
      String item = getItem(index);
      if (!isItemEnabled(item)) disableItem(item);
    }
  }

  /** Save the original color of an item so that it can be retrieved later.
      @param item The text of the item to save the color
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  private void saveItemColor (String item) {
    if (savedColors==null) savedColors = new Table();
    Color color;
    if (ctab != null && ctab.get(item)!=null) {
      color = (Color)ctab.get(item);
      savedColors.put(item, color);
    }
  }

  /** Restore the previously saved color of an item.
      @param item The text of the item to restore the color
      @since NeXtMidas 2.9.1 (Bug 2326)
   */
  private void restoreColor (String item) {
    Color color;
    if (savedColors != null && savedColors.get(item)!=null) color = (Color)savedColors.get(item);
    else color = null;
    setItemColor(item, color);
  }

  /**
   * Set the color to use for disabled items.
   * @param color the color to be disabled
   * @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void setDisabledItemColor (Color color) {
    disabledItemColor = color;
  }

  /**
   * Set the color to use for disabled items. Uses {@link MColor#getColor(Object)} to determine the Color of the input
   * text.
   * @param color The string value of the color to be disabled
   * @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public void setDisabledItemColor (String color) {
    setDisabledItemColor(MColor.getColor(color));
  }

  /**
   * Get the color to use for disabled items.
   * @return the disable color
   * @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public Color getDisabledItemColor () {
    return disabledItemColor;
  }

  /**
   * Get the color to use for disabled items in String form. Uses {@link MColor#toString(Color)} to determine the String
   * form of the color
   * @return the string value of the disabled color
   * @since NeXtMidas 2.9.1 (Bug 2326)
   */
  public String getDisabledItemColorString () {
    return MColor.toString(disabledItemColor);
  }

  @Override
  public void actionPerformed (ActionEvent e) {
    String cval = getValue(defitem,false);
    String nval = getText();
    panel.remove(jtexti);
    //if (nval.equals(cval)) return; // no change
    if (nval.equals(cval)) { refresh(bbox); return; }  // added refresh call to remove border artifacts
    itemv[0] = nval;
    setValue(0);
    selectChoice(0);
    if (!is(PERM)) close();
  }

  /**
   * For documentation please see {@link MJTextField#getText()}
   * @return the text
   */
  public String getText() {
    return  jtexti.getText();
  }

  @Override
  public Object setKey (String name, Object value) {
         if (name.equals("ACTION")) setAction((String)value);
    else if (name.equals("VALUE")) setValue((String)value);
    else if (name.equals("ITEM")) setValue(Convert.o2l(value));
    else value = super.setKey(name,value);
    return value;
  }

  @Override
  public Object getKey (String name) {
    Object value;
    if (name.equals("ITEM")) value = Convert.l2o(defitem);
    else value = super.getKey(name);
    return value;
  }

  @Override
  public String toString () {
    return toList((String)getValue(),getItems());
  }

  /**
   * Set the width and height of the menu
   * @param width Width of the menu
   * @param height the height of the menu
   */
  public void setAltSize (int width, int height) {
    altwidth = width;
    altheight = height;
  }

}
