package nxm.sys.libg;

import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;

import javax.swing.JTextField;

import nxm.sys.inc.IDable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Convert;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;


/** A class to open and maintain a graphical widget.

  @author Jeff Schoen
*/
public class GWidget extends GWidgetModSwap implements BeanWidget, MessageHandler, IDable {

  /** Indicates change due to a user action like click */
  public static final int CHANGED_BY_USER_INPUT = 0;
  /** @deprecated Since NeXtMidas 2.7.2, use {@link #CHANGED_BY_USER_INPUT} instead */
  @Deprecated public static final int CHANGED_BY_CLICK = CHANGED_BY_USER_INPUT;

  /** Indicates change due to monitoring (polling) of result value */
  public static final int CHANGED_BY_POLL = -1;

  /** Indicates changed by message from the macro */
  public static final int CHANGED_BY_MSG  = -2;

  /** The default value for {@link #actionType} */
  public static final int DEF_ACTION_TYPE = CHANGED_BY_MSG;  // default to changed by msg

  /** Indicates the type of action that changed the Widget */
  public int actionType = DEF_ACTION_TYPE;

  /** Indicates the mouse has just been clicked or key pressed (for keyboard input).
      This is different from {@link #CHANGED_BY_USER_INPUT} in that this can be
      immediately reset while {@link #CHANGED_BY_USER_INPUT} cannot be
      reset until after {@link #sendWidgetMessage(String, int, Object)} */
  public boolean userAction = false;

  /** Indicates the widget is to be double buffered to reduce flicker
      TODO - remove in 4.1.0, as this is unused
      Unused with switch to Full Swing Graphics - added by Jeff for flicker on monitored widgets
   */
  @Deprecated
  public boolean doubleBuffer = false;
  /** global flags mask */
  public static String flagsList = "Permanent,Sticky,Fixed,NoEdit,NoMsg,InLine,VBox,VBoxMO";

  /** flags mask: this widget is permanent; it will persist no matter what action is taken.  */
  public static final int PERM = 0x01;
  /** flags mask: this widget is sticky; it will persist until a right mouse click removes it */
  public static final int STICKY = 0x02;
  /** flags mask: this widget is fixed; it cannot be moved by the user.  */
  public static final int FIX = 0x04;
  /** flags mask: this widget is not editable. */
  public static final int NOEDIT = 0x08;
  /** flags mask: suppresses messages to the macro when this widget changes. */
  public static final int NOMSG = 0x10;
  /** flags mask: this widget will not have any borders to allow embedding in another display.  */
  public static final int INLINE = 0x20;
  /** flags mask: draw Box around current value */
  public static final int VBOX=0x40;
  /** flags mask: Value Box on MouseOver . */
  public static final int VBOXMO = 0x80;
  /** flags mask: indicates this widget has a title to display */
  public static final int TITLE = 0x10000000;
  /** flags mask: poll a results name for changes. */
  public static final int MONITOR = 0x20000000;
  /** flags mask: add the next widget on the same line. */
  public static final int CONCAT = 0x40000000;
  /** flags mask: indicates the headless state of this widget */
  public static final int HEADLESS=0x80000000;

  /** flags mask: indicates the MASK values that dont require setup()/refresh to be called */
  public static final int flagsRequiringRefresh = ~(PERM|STICKY|FIX|NOEDIT|NOMSG|MONITOR);

  /** default border width */                       public static final int DEFBW=4;
  /** panel zone for parent panel */                public int zone;
  /** width of panel zone for parent panel */       public int fixWidth;
  /** height of panel zone for parent panel */      public int fixHeight;
  /** line height of panel zone for parent panel */ public int fixLineHeight;
  /** amount of compression currently enabled */    public int compression;
  /** text width */                                 public int tw;
  /** text height */                                public int th;
  /** text ascent */                                public int ta;
  /** border width */                               public int bw;
  /** widget panel relative position <I>x</I>-coordinate */     public double   rx;
  /** widget panel relative position <I>y</I>-coordinate */     public double   ry;
  /** indicates whether the relative position has been moved */ public boolean  rMoved = false;
  /** indicates this widget has been detached from its panel */ public boolean  pDetach=false;
  /** the optional widget group label */                        public GLabel   pGroup;
  /** panel width */                   protected int    pw;
  /** panel height */                  protected int    ph;
  /** title width */                   protected int    titlew;
  /** title height */                  protected int    titleh;
  /** label for action message name */ public String    label;
  /** group name for panels */         public String    group;
  /** optional units string */         public String    units;
  /** object to monitor */             protected String monitorName;
  /** the Midas context */             protected Midas  M;
  /** the display state */             private boolean  show=true;
  /** other state flags */             public int       flags=0;
  /** optional border box */           public MBox      bbox;
  /** index of callback routine */     public int       callback;
  /** who to send Messages from */     private Object   from;
  /** Tool Tip Text @since 2.9.0 */    private String   tipText;
  /** widget requires legacy paint */  public boolean   isWidgetThatNeedsLegacyPaint; // since 3.9.0 for backwards compatibility
  /** current pane */                  public Object    pane;

  /** disabled (enabled==false) includes grayed out  */
  protected boolean enabled = true;  // since 2.9.1 (Bug 2323)

  /** Border size of shadowbox to use when calculating panel sizes and such */
  protected final int SB_BORDER = 2;

  /** Default color of disabled widgets */       public static final Color DEF_DISABLED_COLOR = Color.gray;
  /** the color for a disabled widget */         protected Color   disabledColor = DEF_DISABLED_COLOR;

  /** List of types of Widgets types that can be disabled */
  public static final String DISABLE_LIST = "gvalue, glabel, gmenu, gprompt";  // Bug 2323

  /** If there is a better way to detect that a copy of a global theme has been made this can be removed.

      TBD -- As per Jeff Schoen conversation, make this a flag to indicate that widget theme is "StickY
      so as not to reset it if theme is set to a global theme later.  This would be one of many flags
      to indicate a "sticky" attribute.  For example, this is much like we need for PLOT when a user
      sets a Color Map they may or may not want a new layer to be opened with the same color map.
  */
  private boolean haveLocalTheme = false;

  /** Currently sliding knob in scroll bar, so do not move based on drag boc */
  private boolean scrollNoMove = false; // default to NOT scrolling - Since 3.7.0 for Bug 2825

  /** If this is true, then it tells the paint() that is is being called from the new paintComponent(Graphics g) code
   * Since 3.9.0 for backwards compatibility NXM-3213
   */
  private volatile boolean inPaintComponent = false;

  /**
    Empty constructor required for widget lookup via reflection.
   */
  public GWidget () {}

  /** Instantiates a new Widget.  The constructor was modified to call
     doSetup() in order to set various attributes on the widget.  This makes
     GWidget more compatible with widgets that are opened via the BEAN approach.
     @param name the name of this widget
     @param parent this widget's parent
     @param title the title of this widget
     @param flags the flags for this widget
     @param mh the MessageHandler to be registered for this widget
  */
  public GWidget (String name, Object parent, String title, int flags, MessageHandler mh) {
    super (name,mh,parent);
    doSetup(parent, title, flags);
    isWidgetThatNeedsLegacyPaint = GraphicsUtil.checkIfNeedLegacyPaint(this);  // Since 3.9.0 for backwards compatibility (NXM_3213)
  }

  /** Sets up the Gwidget similar to what the constructor does. Meant to be used with
      Bean Gwidgets that use an empty constructor.
      @param name the name of this widget
      @param parent this widget's parent
      @param title the title of this widget
      @param flags the flags for this widget
      @param mh the MessageHandler to be registered for this widget
   */
  @Override
  public void setupBean (String name, Object parent, String title,
          int flags, MessageHandler mh, nxm.sys.lib.Table argTable) {

    isPopup = (parent instanceof MWindow);
    useSwing = Theme.getOption(Theme.SWING);
    useSwing &= Theme.getOption(isPopup?Theme.SWINGPOPUPS:Theme.SWINGWIDGETS);
    if (Theme.getOption(Theme.MOBILE)) useSwing = true;

    status = INIT;
    hkl = new HelpKeyListener(this);
    if (useSwing) {
      MJPanel mpanel = new MJPanel();
      mpanel.setMWindow(this);
      panel = mpanel;
    } else {
      MPanel mpanel = new MPanel();
      mpanel.setMWindow(this);
      panel = mpanel;
    }

    this.title = name;
    this.name = name;
    this.mh = mh;
    doSetup(parent, title, flags);
    isWidgetThatNeedsLegacyPaint = GraphicsUtil.checkIfNeedLegacyPaint(this); // Since 3.9.0 for backwards compatibility (NXM_3213)
  }

  /**
    Sets attributes on this parent GWidget in lieu of of performing these
    operations inside of the GWidget constructor.
    @param parent The parent object
    @param title The title of the widget
    @param flags Any flags that were set when the widget was created
   */
  private void doSetup (Object parent, String title, int flags) {
    this.parent = parent;
    this.title = title;
    this.label = title.toUpperCase().trim();    // default label to title
    if (parent instanceof MWindow) MW=(MWindow)parent;
    if (parent instanceof GDialog && !label.endsWith(".CFG")) flags |= (FIX|INLINE|PERM);  // exclude CFG popups from this since 3.3.2
    this.flags = flags;

    //    this.widgetTheme = (Theme)theme.copy();
    setHeadless(is(HEADLESS));
    tipText = "";
}

  /** Returns this widget's current Theme.
      @return The current Theme.
   */
  public Theme getWidgetTheme() { return this.theme; }

  /** Sets this widget's theme without affecting the global theme.
      @param themeObj The String or Table or Object defining the theme
  */
  public void setWidgetTheme (Object themeObj) {
    if (themeObj == null) return;
    boolean doRefresh = true;
    if (!haveLocalTheme) {
      //System.out.println("GW: input theme="+themeObj+" class "+themeObj.getClass());
      //System.out.println("GW: "+label+" ["+title+"] Making a copy of global theme... ");
      this.theme = (Theme) this.theme.copy();
      haveLocalTheme = true;
    } else {
      //System.out.println("GW: "+label+" a copy of global theme has already been made........");
    }
    if (themeObj instanceof Theme) {
      this.theme = (Theme)themeObj;
    }
    else if (themeObj instanceof Table) {
      setWidgetThemeFromTable ((Table)themeObj);   // moved to separate method for Bug 2398
      doRefresh = false;
    }
    else if (themeObj instanceof String) {
      String themeName = (String)themeObj;
      if (!themeName.isEmpty()) {
        this.theme.setByName(themeName);
      } else { Shell.warning("Invalid widget theme name given "+themeObj); return; }
    }

    // Make any color adjustments for GUI components that may not be
    // visible due to poor color combinations.  Bug 2797
    this.theme.adjustThemeColorVisibilityIssues(M,true);

    if (doRefresh) refresh();
  }

  /** Set the theme for a widget based on a given table. The table should be in the
      form {&lt;THEME_COMP&gt;=&lt;COLOR&gt;,...}. The color can be specified as a current
      Theme component color, for example {CWTI=_THEME_CWFG}. The color can also be
      the int (RGB) value for a color. The Theme component ids must match {@link Theme#COMPONENT_LIST}
      @param themeTable The table with theme components and colors
      @since NeXtMidas 3.0.0
   */
  public void setWidgetThemeFromTable (Table themeTable) {
    if (themeTable!=null && themeTable.getSize()>0 ) {
      Table.Iterator ti = themeTable.iterator();
      while (ti.getNext()) {
        Object val = ti.value;
        if (val instanceof String) {
          String sval = (String)val;
          if (sval.startsWith("_THEME_")) {   // get other colors from the current Theme (Bug 2398)
            String comp = sval.substring(7);  // 7 is the number of chars in '_THEME_'
            try {
              int rgb = theme.getColor(comp).getRGB();
              themeTable.put(ti.key, rgb);
            } catch (Exception e) {
              Shell.warning("Invalid Theme component name. Name must be in: "+Theme.getComponentList());
            }
          }
        }
      }
      this.theme.createTheme(themeTable);
    } else { Shell.warning("Invalid widget theme table given "+themeTable);}
    refresh();
  }

  @Override
  public void open () {
    super.open();
    if (fm==null) {
      tw=10; th=20; ta=10;
    } else {
      tw = fm.stringWidth("0");
      th = fm.getHeight();
      ta = fm.getAscent();
    }
    setTitle(title);

    th = Math.min(th,tw*2+2);    // be reasonable 1.4
    ta = Math.min(ta,tw*2-2);    // be reasonable 1.4
    if (is(INLINE)) bw=0; else bw=2;
    addListeners();
  }

  /** Open the graphics window for this widget with the given name. This widget's
      panel will be sized according to the given width and height.
     @param name the name of this window.
     @param pw the width of this widget's panel.
     @param ph the height of this widget's panel.
  */
  public void open (String name, int pw, int ph) {
    open();
    this.name = name;
    this.pw = pw;
    this.ph = ph;
    setup();
  }

  /** Returns the width of the given string. Calculated with FontMetrics if
      defined for this widget. Otherwise determined with default text width
      and string length.
     @param str the string for which the width is calculated.
     @return the width of the string.
  */
  protected int stringWidth (String str) {
    if (fm==null) return tw*str.length();
    else return fm.stringWidth(str);
  }

  @Override
  public void setup () {
    if (is(INLINE)) bw=0; else bw=DEFBW;
    if (is(TITLE)) { titleh = th+ta; bw=DEFBW; } else titleh=0;
    pos.w = bw+pw+bw;
    pos.h = bw+titleh+ph+bw;
    layout();
  }

  /** Is the widget enabled
   *  @return Is the widget enabled. See {@link #DISABLE_LIST} for list of widgets that can be disabled.
   *  @since NeXtMidas 2.9.1
   */
  public boolean isEnabled () {
    return enabled;
  }

  /** Disable or enable the widget. See {@link #DISABLE_LIST} for list of widgets
      that can be disabled. This method sets the flags +{@link #NOMSG}|+{@link #NOEDIT}
      and indicates the widget text should be 'grayed out'.
      @param enabled true if it is enabled, false otherwise
      @since NeXtMidas 2.9.1
  */
  public void setEnabled (boolean enabled) {
    String name = this.getClass().getSimpleName().toLowerCase();
    if (DISABLE_LIST.indexOf(name)<0) {
      M.warning("Cannot disable widget of type "+this.getClass().getSimpleName()+". " +
                "Can only disable widgets in list ["+DISABLE_LIST+"]");
      return;
    }
    if (this.enabled != enabled) {
      this.enabled = enabled;
      refresh();
    }
  }

  /** Calls {@link #setEnabled(boolean)} to disable or enable the widget.
      @since NeXtMidas 2.9.1
   */
  public void toggleEnabled () {
    setEnabled(!enabled);
  }

  /**
   * Set the color of a disabled widget. Default is {@link #DEF_DISABLED_COLOR}
   * @param c the color to be disabled
   * @since NeXtMidas 2.9.1
   */
  public void setDisabledColor (Color c) {
    if (c==null) return;
    disabledColor = c;
  }

  /**
   * Set the color of a disabled widget. Default is {@link #DEF_DISABLED_COLOR}
   * @param color the string value of the color to be disabled
   * @since NeXtMidas 2.9.1
   */
  public void setDisabledColor (String color) {
    if (color==null) return;
    setDisabledColor(MColor.getColor(color));
  }

  @Override
  public void resize (int mode) {
    if (MW!=null) place (pos,MW.pos);
    if (mode>0) setBounds (pos);
    pw = pos.w-bw-bw;
    ph = pos.h-bw-bw-titleh;
  }

  /** Method to adjust the graphics clip of a container to ensure that any text drawn
      on the container stays within the border created by {@link #shadowbox(int, int, int, int, int)}
      @param gc The Graphics context to clip
      @param panel The Container that is getting text drawn on
      @param text The text itself
      @param startX The start position for drawing the text
      @since NeXtMidas 3.1.0
   */
  protected void setTextClip (Graphics gc, Container panel, String text, int startX) {
    // This method added for Bug 2473
    int totalTextWidth = stringWidth(text)+startX;
    int maxTextPos = panel.getLocation().x + panel.getWidth();
    if (totalTextWidth>maxTextPos) gc.setClip(0, 0, maxTextPos-6, panel.getHeight());
  }

  /**
   *  @deprecated Since NeXtMidas 3.9.0 to avoid graphical timing issues
   *  use {@link #paintComponent(Graphics)} (for NXM-3213)
   */
  @Deprecated
  public void paint () {
    if(!inPaintComponent) refresh();
  }

  /**
   * Set Widget Color
   * @param color color
   * @deprecated Since NeXtMidas 3.9.0
   */
  @Deprecated
  protected void setColor (Color color) {
    setColor(g, color);
  }

  /** Set GWidget color
   *  @param g     the Graphics object used for painting
   *  @param color color
   *  Note: added parameter g in 3.9.0 for graphical timing issues (NXM-3213)
   */
  protected void setColor (Graphics g, Color color) {
    g.setColor( enabled? color : disabledColor );
  }

  /** Get whether or not the theme has been set using /wtheme=
   *  @return Whether or not the theme has been set using /wtheme= .
      @since NeXtMidas 3.3.0
   */
  public boolean hasLocalTheme () { return haveLocalTheme; }

  /** This method performs 'home-made double buffering' to prevent flickering and
      blinking.
      @since neXtMidas 2.9.0  (Bug 1065 and Bug 1208)
   */
  @InternalUseOnly
  @Override  //since neXtMidas 2.9.0  (Bug 1065 and Bug 1208) with major update in 3.9.0 for NXM_3213
  public void paintComponent (Graphics g) {
    // The switch from paint() to paintComponent(Graphics) was made in 3.9.0 to fix graphical timing issues
    // To support backwards compatibility in custom GWidgets, GWidgets without a paintComponent(Graphics) method
    // that have a paint() method, will have their paint() method called.
    if (isWidgetThatNeedsLegacyPaint) {
      inPaintComponent=true;
      paint();
      inPaintComponent=false;
    }
    Rectangle clipBounds = g.getClipBounds();
    g.setColor(panel.getBackground());
    g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
    setGraphics(g);
  }

  protected Graphics getOffscreen (Image offscreen, Rectangle clipBounds) {
    Graphics offgc = offscreen.getGraphics();
    offgc.setColor(panel.getBackground());
    offgc.fillRect(0, 0, clipBounds.width, clipBounds.height);
    offgc.setColor(panel.getForeground());
    // do normal redraw
    offgc.translate(-clipBounds.x, -clipBounds.y);
    return offgc;
  }
  /** Draws a shadow box with the given bounds in the mode specified
   *  @param g          The graphics component
   *  @param x          The <I>x</I>-coordinate
   *  @param y          The <I>y</I>-coordinate
   *  @param w          The width
   *  @param h          The height
   *  @param inlineMode Mode when in-line
   *  @param normalMode Normal mode
   *  @since NeXtMidas 3.9.0 for NXM-3213
   */
  protected void shadowbox (Graphics g, int x, int y, int w, int h, int inlineMode, int normalMode) {
    int bf = (is(INLINE))? inlineMode : normalMode;
    shadowbox(g,x,y,w,h,bf);
  }

  /**
   * Draws a diamond with the given dimensions; draws shadows as defined by the given mask.
   * @param ax the <I>x</I>-coordinate of the diamond
   * @param ay the <I>y</I>-coordinate of the diamond
   * @param aw the width of the diamond
   * @param am this mask contains bits that are set to indicate the direction in which the diamond should be shadowed:
   *  <PRE>1: shadow in up mode</PRE>
   *  <PRE>2: shadow in down mode</PRE>
   *  <PRE>4: shadow in left mode</PRE>
   *  <PRE>8: shadow in right mode</PRE>
   * @deprecated Since 3.9.0
   */
  @Deprecated
  protected void drawDiamond (int ax, int ay, int aw, int am) {
    drawDiamond(g, ax, ay, aw, am);
  }

  /** Draws a diamond with the given dimensions; draws shadows as defined
      by the given mask.
     @param g  the Graphics object used for painting (added in 3.9.0)
     @param ax the <I>x</I>-coordinate of the diamond
     @param ay the <I>y</I>-coordinate of the diamond
     @param aw the width of the diamond
     @param am this mask contains bits that are set to indicate the
     direction in which the diamond should be shadowed:
     <PRE>1: shadow in up mode</PRE>
     <PRE>2: shadow in down mode</PRE>
     <PRE>4: shadow in left mode</PRE>
     <PRE>8: shadow in right mode</PRE>
  */
  protected void drawDiamond (Graphics g, int ax, int ay, int aw, int am) {
    if (aw<=0) aw = tw*2/3;
    shadowarrow (g, ax, ay, aw, 'b');
    if ((am&1)!=0) shadowarrow(g, ax, ay, aw, 'u');
    if ((am&2)!=0) shadowarrow(g, ax, ay, aw, 'd');
    if ((am&4)!=0) shadowarrow(g, ax, ay, aw, 'l');
    if ((am&8)!=0) shadowarrow(g, ax, ay, aw, (this instanceof GMenu)? 'm':'r');
  }

  /** Override of MWindow MouseEntered to disable hover highlighting if
      the GWidget is {@link #NOEDIT}
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseEntered (MouseEvent e) {
    //System.out.println("Mouse Enter "+name);
    super.mouseEntered(e);  // 4.1.2 hover readouts allowed with NOEDIT
  }

  /** Override of MWindow MouseMoved to disable hover highlighting if
      the GWidget is {@link #NOEDIT}
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseMoved (MouseEvent e) {
    //System.out.println("Mouse Moved "+name+" x= " +e.getX()+", xos="+e.getXOnScreen()+", mw= "+MW);
    super.mouseMoved(e);  // 4.1.2 hover readouts allowed with NOEDIT
  }

  /** Override of <code>MWindow</code> method to capture mouse click events and disable them
      if the GWidget is {@link #NOEDIT}
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseClicked (MouseEvent e) {
    if ((eventFilter&NOMOUSE)!=0) return;
    // Do not respond to LMB else userAction = true;
    // If NOEDIT, let RMB exit if control without panel (4.1.2)
    if (is(NOEDIT) && e.getButton() == 1) return;
    userAction = true;
    super.mouseClicked(e);
  }

  /** Override of MWindow method so that Key Events are stopped if NOEDIT and
      allows for setting of {@link #userAction} to TRUE.
      @since NeXtMidas 2.7.2
   */
  @Override
  public void keyPressed (KeyEvent e) {
    if ((eventFilter&NOKEY)!=0) return;
    if (is(NOEDIT))             return;   // do not respond to KeyEvents if NOEDIT
    else userAction = true;
    super.keyPressed(e);
  }

  /** Override of MWindow method so that Key Events are stopped if NOEDIT and
      allows for setting of {@link #userAction} to TRUE.
      @since NeXtMidas 2.7.2
   */
  @Override
  public void keyReleased (KeyEvent e) {
    if ((eventFilter&NOKEY)!=0) return;
    if (is(NOEDIT))             return;   // do not respond to KeyEvents if NOEDIT
    else userAction = true;
    super.keyReleased(e);
  }

  /** Present a configuration menu. */
  public void configure () {
    if (title.equals("GMenu.cfg")) { System.out.println("Configure of configure menu not allowed"); return; }
    String mtitle = (name==null)? "GWidget.cfg" : name+".cfg";
    new GMenu (parent,mtitle,flagsList(),flags,GMenu.TOGGLE,this);
  }

  @Override
  public void addTo (Object entry) { addTo(entry,true); }

/** Adds this to the named object
 *  @param entry The object to which this MWindow should be added to
 *  @param top   ? used to decide this is added to a GDialog
 */
  public void addTo (Object entry, boolean top) {
    // exclude config popups from being added to Dialog widgets
    if (top && entry instanceof GDialog && !label.endsWith(".CFG")) ((GDialog)entry).addWidget(this);
    //else if (entry instanceof GWidget) addTo(((GWidget)entry).MW,false);  // commented out for BUG 2108
    else if (entry!=null) super.addTo(entry);
  }

  /** Set the label used for message and results names.
     @param label the new label
  */
  public void setLabel (String label) {
    this.label = label;
  }

  /** Get the label used for message and results names.
     @return the label
   */
  public String getLabel () { return label; }

  /** Sets or clears the specified flag bits using +-~ syntax.
      @param mask the flag bits to set.
   */
  public void setFlags (String mask) {
    setFlags(Parser.mask (flagsList(),mask,flags));
  }

  /** Sets or clears the specified flag bit.
      @param mask the flag bit to set/clear.
      @param value if true, set the bit; if false, clear the bit.
  */
  public void setFlag (int mask, boolean value) {
    setFlags( value? (flags|mask) : (flags&~mask) );
  }

  /** Set the flags for the widget
   *  @param flags The flags to be set
   */
  public void setFlags (int flags) {
    int mask = flags ^ this.flags;
    this.flags = flags;
    if ((mask&flagsRequiringRefresh) != 0) {
      setup();
      refresh();  // Bug 1617- needed to draw VBOX
    }
  }

  /** Tests whether the bit represented in the mask is set in the flag.
      @param mask One of the valid flags masks in {@link #flagsList}
      @return true if the bit represented in the mask is set in the flag.
      @since NeXtMidas 2.7.0
  */
  public boolean getFlag (int mask) { return is(mask); }

  /** Gets the current flags
     @return the current flags
  */
  public String getFlags () {
    return Parser.mask2s (flagsList(),flags);
  }

  String flagslist=null;
  protected String flagsList () {
    // this finds the flagsList of the subclass for use in the super class functions
    if (flagslist==null) flagslist = KeyObject.getKey(this,"flagsList").toString();
    if (flagslist==null) flagslist = flagsList;
    return flagslist;
  }

  /** Set the labeled group for this widget.
     @param group the new group label
   */
  public void setGroup (String group) {
    if (group==null || group.length()==0) this.group=null;
    else this.group = group.toUpperCase();
  }

  /** Get the labeled group for this widget.
      @return the labeled group for this widget.
  */
  public String getGroup () { return group; }

  /** Set the optional units string.
      @param units the new optional units string
  */
  public void setUnits (String units) { this.units = units; }

  /** Get the optional units string.
      @return the optional units string, if there is one; null otherwise.
   */
  public String getUnits () { return units; }

  /** Sets the value of this widget.
      @param value the new value for this widget.
  */
  public void setValue (String value) { }

  /** Gets the current value of this widget in  the form of an Object.
      @return the value of this widget.
  */
  public Object getValue () { return null; }

  /** Sets the value of this widget.
      @param value the new value for this widget.
  */
  public void setAction (String value) {
    setActionType();
    setValue(value);
  }

  /** Set whether the action was instigated by a user action (mouse/key) and resets {@link #userAction} */
  public void setActionType () {
    if (userAction) {
      actionType = CHANGED_BY_USER_INPUT;
      userAction = false;
    } else {
      actionType = DEF_ACTION_TYPE;
    }
  }

  /** Set whether the action was instigated by a user action
      @param type  One of
  */
  public void setActionType (int type) {
    if ( (type == CHANGED_BY_USER_INPUT) || (type == CHANGED_BY_POLL) || (type == CHANGED_BY_MSG)) {
      actionType = type;
    } else {
      Shell.warning("Invalid action type = "+actionType);
    }
  }

  /** Get whether the action was instigated by a user action
   *  @return whether the action was instigated by a user action */
  public int getActionType () { return actionType; }

  /** Get the string represention of the action type
   *  @return the string represention of the action type */
  public String getActionTypeString () { return getActionTypeString (actionType); }

  /** Get the Action Type as a String
   *  @param type the int representation of the action type
   *  @return the string representation of the action type
   */
  public String getActionTypeString (int type) {
    switch (type) {
      case (CHANGED_BY_USER_INPUT) : return "CHANGED_BY_USER_INPUT";
      case (CHANGED_BY_MSG)        : return "CHANGED_BY_MSG";
      case (CHANGED_BY_POLL)       : return "CHANGED_BY_POLL";
      default                      : return null;
    }
  }

  /** Returns a string representation of this widget.
      @return Returns the string "GWidget".
   */
  @Override
  public String toString () { return "GWidget"; }

  /** Present the value of this widget in a list format.
     @param value the current value of this widget.
     @param list the list of possible values for this widget.
     @return A string representation of this widget's current value
     in list format.
  */
  public String toList (String value, String list) {
    String ucvalue = ","+value.toUpperCase()+",";
    String uclist  = ","+list.toUpperCase()+",";
    int i = uclist.indexOf(ucvalue);
    int lv = value.length();
    String s;
    if (title!=null && title.length()>0) s = title+"=[";
    else if (label!=null && label.length()>0) s = label+"=[";
    else s = "[";
    if (list.length()==0) s += value;
    else if (i<0) s += "<"+value+">,"+list;
    else if (i==0) s += "<"+value+">"+list.substring(lv);
    else s += list.substring(0,i)+"<"+value+">"+list.substring(i+lv);
    if (units!=null) s += "] "+units;
    else s += "]";
    return s;
  }

  /** Returns a readout string containing information about this widget
      that is useful for the end user.
     @return a string representation of this widget's class name,
     its current value, and its legend of mouse instructions.
  */
  public String getReadOut () {
    String s = getClass().getName();
    int i = s.lastIndexOf('.');
    s = s.substring(i+1);
    s += " " + label + "=" + getValueString();
    if (units!=null) s += " "+units;
    String l = getLegend();
    if (l!=null && !is(NOEDIT)) return s+" "+l;
    return s;
  }

  private String setReadOut (String text) {
    return getReadOut() + " " +text;
  }

  /** Get the string value of the given widget
   *  @return The string value of the given widget (custom per widget type) */
  public String getValueString() {
    if (getValue() == null) return "";
    return getValue().toString();
  }

  /** Returns a string containing instructions on mouse actions for this widget.
     @return the string of mouse action instructions
  */
  public String getLegend () {
    return null;
  }

  /** Set the tool tip text on a Gwidget. Overrides method in MWindow.java.
      This method will call setToolTipText(String) in MWindow.java, and if the
      Widget is not a Swing component, the tool tip text will be displayed in the
      title bar.
      @param text The text to display
      @return True if the tool tip was added to a Swing component, False if added to the title bar
      @since NeXtMidas 2.9.0 (Bug 2240)
   */
  @Override
  public boolean setToolTipText (String text) {
    tipText = text;
    boolean swingComp = super.setToolTipText(text); // attempt to set on Swing panel
    if (!swingComp) formatReadOut(1);               // or else set on the title bar
    return swingComp;
  }

  /** Get the ToolTipText, if any.
      @return The Tool Tip text, if any, or an empty String, if none.
   */
  @Override
  public String getToolTipText () { return tipText; }

  /** Displays this widget's readout in the title bar, if possible. See
      {@link MWindow#setAltTitle }
     @param mode Sets the display mode of the readout:<br>
     <PRE>mode&gt;0: displays this widget's readout in title bar</PRE>
     <PRE>mode&lt;0: displays the top-most MWindow's title, if there is one,
     in the title bar</PRE>
  */
  public void formatReadOut (int mode) {
    if (mode>0) setAltTitle(setReadOut(tipText));
    if (mode<0) setAltTitle(null);
  }

  /**
   * Sets the display state of this widget.
     @param show the new display state.
     @deprecated Since NeXtMidas 2.9.2: use setShow(boolean)
   */
  @Deprecated public void setShow (int show) {
    setShow(show > 0);
  }

  /**
   * Sets the display state of this widget.
     @param show the new display state.
   */
  public void setShow (boolean show) {
    if (show != this.show) {
      this.show = show;
      if (wp != null)
        wp.layoutWidget(this);
    }
  }

  /** Is the widget being shown
   *  @return Is the widget being shown. */
  public boolean isShow () {
    return show;
  }

  /** Sets the height of the panel zone for parent panel. If the value of
      fixHeight has changed, calls setup() to redisplay widget with new height.
     @param value new the fixHeight
  */
  public void fixHeight (int value) {
    if (fixHeight==value) return;
    fixHeight = value;
    setup();
  }

  /** Sets the line height of the panel zone for parent panel. If the value of
      fixLineHeight has changed, calls setup() to redisplay widget with new fixLineHeight.
     @param value the new fixLineHeight
  */
  public void fixLineHeight (int value) {
    if (fixLineHeight==value) return;
    fixLineHeight = value;
    setup();
  }

  /** Sets the width of the panel zone for parent panel. If the value of
      fixWidth has changed, calls setup() to redisplay widget with new fixWidth.
     @param value the new fixWidth
  */
  public void fixWidth (int value) {
    if (fixWidth==value) return;
    fixWidth = value;
    setup();
  }

  /** Sets the compression of the panel zone for parent panel. If the value of
      compression has changed, calls setup() to redisplay widget with new compression.
     @param value the new compression
  */
  public void setCompression (int value) {
    if (compression==value) return;
    compression = value;
    setup();
  }

  /** Sets the optional monitor name for this widget.
      @param name The name of the object to monitor (must have length &gt; 0). If the name is null
                  monitoring is turned off. <i>Note: When calling this from a macro it is
                  important to quote the name and put it in uppercase if monitoring a result
                  (e.g. use <code>setMonitor("CFREQ")</code> not <code>setMonitor(CFREQ)</code> and
                  not <code>setMonitor("cfreq")</code>).</i>
  */
  public void setMonitor (String name) {
    if (name!=null && name.length()>0) {
      monitorName = name;
      flags |= MONITOR;     // turn on monitor flag
//      doubleBuffer = true; // no longer used as of 3.8.1 & the switch to full Swing - hopefully no flicker on monitored widgets (test_rpofiler looks good)
    }
    else {
      flags &= (~MONITOR); // turn off monitor flag
      monitorName = null;  // null out monitorName after turning off monitor flag for BUG 2118
    }
  }
  // 2010-03-25 NTN TODO: considering adding the getMonitor() method:
  // pro: user can see what result name is monitored
  // con: it hides the MONITOR constant
  //      if we can update KeyObject to be smarter, this shouldn't be an issue
  // public String getMonitor () { return monitorName; }

  /** Get the widget panel
   *  @return the widget panel
   *  @since NeXtMidas 3.9.2 to JDK modularity to support move of inc.WidgetPanel to libg.WidgetPanel
   */
  public WidgetPanel getGWidgetPanel() {
    if (this.wp instanceof WidgetPanel) return (WidgetPanel) this.wp;
    return null;
  }

  /** Graphically lay out this widget. */
  public void layout () {
    if (status==FRONT) setBounds(pos);
    if (wp!=null) wp.layoutWidget(this);

    refresh(); // needed to re-draw the Widget after changes
  }

  /** Set the Midas context for this widget.
     @param midas the Midas context
  */
  public void setMidas (Midas midas) {
    M = midas;
  }

  /** Poll for change in monitor. Currently unimplemented.
    @return always false.
  */
  public boolean poll () {
    return false;
  }

  /** Tests whether the bit represented in the mask is set in the flag.
     @param mask One of the valid flags masks:
     <PRE>PERM</PRE>
     <PRE>STICKY</PRE>
     <PRE>FIX</PRE>
     <PRE>NOEDIT</PRE>
     <PRE>INLINE</PRE>
     <PRE>INBAND</PRE>
     <PRE>NOMSG</PRE>
     <PRE>MONITOR</PRE>
     @return true if the bit represented in the mask is set in the flag.
   */
  protected boolean is (int mask) {
    if (!enabled && (mask==NOMSG || mask==NOEDIT)) return true;
    return (flags&mask)!=0;
  }

  @Override
  public int process (String name, int info, Object data) {
    //System.out.println("GW process. Name=" +name+" info= " +info+", class="
    //    +getClass().getSimpleName()+" thread="+Thread.currentThread());
    if ("PAINT".equals(name)) {
      if (status==INIT) { // notify parent that we are viewable
        if (parent instanceof MFrame) panel.requestFocus();
        status=FRONT;
      }
      paintComponent((Graphics)data);
    }
    if (status==EXIT) return 1;
    // handle specific widget messages locally
    if (processEventMessage(name,info,data)>0) return 1;

    // handle general widget messages locally (3.8.1 switched to case statements)
    switch (name) {
      default:
        return -1;
      case "REFRESH":
        if ((eventFilter & NOREFRESH) == 0) refresh();
        return 1;
      case "RESIZE":
        if ((eventFilter & NOREFRESH) == 0) resize(0);
        return 1;
      case "POINTER":
        return 1;
      case "BUTTON":
        return 1;
      case "DRAG":
        return 1;
      case "FOCUS":
        return 1;
      case "DRAGBOX":
        if (!is(FIX) && (eventFilter&NOMOVE)==0 && !scrollNoMove) {  // allow temporary disabling of drag box moves due to scrolling - since 3.7.0 for Bug 2825
          pos.x+=box.w; pos.y+=box.h;
          setBounds(pos);
          rMoved=true;
          layout();
        }
        else {
          scrollNoMove = false; // end any temporary disabling of moves due to scrolling - since 3.7.0 for Bug 2825
        }
        button=0;
        return 1;
    }
  }

  /** Send a message from this widget to the registered MessageHandler.
     @param name the Message name
     @param info the Message info
     @param data the Message data
     @return the status returned by the MessageHandler's processMessage
     method. See {@link MessageHandler#processMessage}
  */
  protected int sendWidgetMessage (String name, int info, Object data) {
    return sendWidgetMessage(name, info, data, null);
  }

  /** Send a message from this widget to the registered MessageHandler.
      Same as {@link #sendWidgetMessage(String, int, Object)} with the addition
      @param name the Message name
      @param info the Message info
      @param data the Message data
      @param addToQuals merge the key/values into the Message quals (Table) (when not null)
      @return the status returned by the MessageHandler's processMessage method.
              See {@link MessageHandler#processMessage}
      @since NeXtMidas 3.1.0
   */
  protected int sendWidgetMessage (String name, int info, Object data, Table addToQuals) {
    if (is(NOMSG)) return 0;
    if (userAction) setActionType();
    MessageHandler wmh=this;
    if (from!=null) wmh=(MessageHandler)from;
    else if (MW!=null) wmh = MW.mh;
    Table quals = new Table();
    if (addToQuals != null) { quals.putAll(addToQuals); } // since 3.1.0
    quals.put("ACTIONTYPE", actionType);
    quals.put("ACTIONTYPESTRING", getActionTypeString(actionType));
    actionType = DEF_ACTION_TYPE;
    return (mh==null) ? 0 : mh.processMessage (new Message (name,info,data,mh,wmh,quals));
  }

  /** Process event messages for this widget locally.
      Note: this method is unimplemented; subclasses should override.
     @param name the Message name
     @param info the Message info
     @param data the Message data
     @return value greater than 0 to mean that message have been process by subclass so that
                   {@link #process(String, int, Object)} can skip subsequent general
                   GWidget event message processing [DEF=-1].
  */
  protected int processEventMessage (String name, int info, Object data) {
    return -1;
  }

  /**
   * @deprecated since NeXtMidas 3.7.1
   */
  @Deprecated // Since NeXtMidas 3.7.1, same as MWindow.rubberbox(..)
  @Override
  public void rubberbox (int mode) {
    gwRubberbox(g,mode);
  }

  /** A GWidget-specific version of rubberbox(..) see MWindow.rubberbox(..) for details.
   *  @param g    The graphics component
   *  @param mode The drag box is drawn according to mode as defined as defined in MWindow.rubberbox(..)
   *  @since NeXtMidas 3.7.2
   */
  private void gwRubberbox (Graphics g, int mode) {
    if (mode==0) gwRubberbox(g, -1);
    if (mode>=0) rbox.setBounds(box);
    int x=rbox.w, y=rbox.h, w=pos.w-1, h=pos.h-1;

    if (MW!=null ) { x+=pos.x; y+=pos.y; }
    g.setXORMode(theme.getBackground());
    g.drawRect(x,y,w,h);
    g.setXORMode(theme.getForeground());
    g.drawRect(x+1,y+1,w-2,h-2);
    g.setPaintMode();
    if (mode<0) rbox.setBounds(0,0,0,0);
  }

  @Override
  public Object setKey (String key, Object value) {
    switch (key) {
      case "V":       setValue(Convert.o2s(value));  break;
      case "VALUE":   setValue(Convert.o2s(value));  break;
      case "ACTION":  setAction(Convert.o2s(value)); break;
      case "POS":     setBounds((MBox)value);        break;
      case "PARENT":  addTo(value);                  break;
      case "TITLE":   setTitle(Convert.o2s(value));  break;
      case "SHOW":    setShow(Convert.o2z(value));   break;
      case "REFRESH": process("REFRESH",0);          break;
      default:        return null;
    }
    return value;
  }

  @Override
  public Object getKey (String key) {
    switch (key) {
      case "V":      return getValue();
      case "VALUE":  return getValue();
      case "POS":    return pos;
      case "PARENT": return parent;
      case "THEME":  return theme;
      case "TITLE":  return title;
      default:       return null;
    }
  }

  /** This method does nothing except replace the inappropriate handler in MWindow.
      It should be overridden by any class that needs to process messages from
      child windows.
   */
  @Override
  public int processMessage (Message msg) {
    return 0;
  }

  @Override
  public String getID () {
    return "MAIN";
  }

  @Override
  public void close () {
    formatReadOut(-1); // clear any readout that may have been set (Bug 1332)
    super.close();
  }

  @Override
  public void setTitle (String title) {
    this.title = title;
    titlew = stringWidth(title);
    refresh();
  }

  /** override MWindow.setBounds to handle compression */
  @Override
  public void setBounds (int x, int y, int w, int h) {
    if (compression!=0) {
      pos.w=w; pos.h=h;
      setLocation(x,y);
    }
    else  super.setBounds(x,y,w,h);
  }

  protected void setValueBox (int x, int y, int w, int h) {
    if (bbox==null) bbox = new MBox();
    bbox.setBounds(x,y,w+1,h+1);
  }

  /** Draws the input border box
   *  @param mode mode=0 is for initial refresh
   *              mode!=0 is for state change (mouse enter|exit)
   *  @deprecated Since NeXtMidas 3.9.0 use {@link GWidget#drawDiamond(Graphics, int, int, int, int)}
  */
  @Deprecated
  protected void drawValueBox (int mode) {
    drawValueBox(g, mode);
  }

  /** Draws the input border box
   *  @param g    The graphics component
   *  @param mode mode=0 is for initial refresh
   *              mode!=0 is for state change (mouse enter|exit)
  */
  protected void drawValueBox (Graphics g, int mode) {
    int draw=0;
    if (bbox==null || is(NOEDIT)) return;            // no borders if not set or editable
    if (is(VBOX)) {
      if (mode==1) draw=1;
    }
    else if (is(VBOXMO)) {
      if (mode==1 && pactive) draw=1;
      if (Math.abs(mode)==9) draw=-1;
    }
    if (draw<0) g.setXORMode(theme.getWidgetBackground());
    if (draw!=0) {
      g.setColor(theme.getWidgetTopShade());
      g.drawRect(bbox.x,bbox.y,bbox.w-1,bbox.h-1);
    }
    if (draw<0) g.setPaintMode();
    if (draw!=0) g.setColor(theme.getWidgetForeground());
  }

  /** function to refresh the area in the given box */
  public void refreshValue() { refresh(bbox); }


  /** Place the text input
   *  @param bc The box to place the text
   *  @param texti the text field to be sized according to the MBox
   */
  public void placeTextInput (MBox bc, JTextField texti) {
    if (bc == null || texti == null) {
      return; // Since NeXtMidas 3.5.0. Break out if in Headless state. BUG 2817
    }
    boolean inline = is(INLINE);
    int x = bc.x         + (inline?2:3);
    int y =              + (inline?2:0);
    int w = pos.w - bc.x - (inline?6:6);
    int h = pos.h +      - (inline?4:0);
    texti.setBounds (x, y, w, h);
  }

  /** Get if it makes sense to stretch this widget vertically
   *  @return whether it makes sense to stretch this widget vertically */
  public boolean isStretchable () {
    return false;
  }

  @Override
  protected Table getHelpData () {
    Table data = new Table();
    data.put("NAME",label);
    data.put("VALUE",getValue());
    return data;
  }

  /** Sets the default position for the widget.
      @param corner The corner the legend should be in. Should be one of:
                    <pre>
                      TL or TOPLEFT
                      TR or TOPRIGHT
                      BL or BOTTOMLEFT
                      BR or BOTTOMRIGHT
                    </pre>
   */
  public void setCorner (String corner) {
    if (StringUtil.isNull(corner) || MW==null) return;
    setCorner(MW, corner);
  }

  /** Sets the default position for the widget.
      @param corner   The corner the menu should be in.
      @param winBound The bounding window (usually MW).
   */
  protected void setCorner (MWindow winBound, String corner) {
    MBox newPos = new MBox(winBound.pos.x, winBound.pos.y, pos.w, pos.h);
    //for standalone commands, MW is null--make position (0,0)
    if (winBound.MW == null) { //LISA--fix x,y start values
      newPos.x=0;
      newPos.y=0;
    }
    corner = corner.trim().toUpperCase();
    switch (corner) {
      case "TL":
      case "TOPLEFT":
      //default position, do nothing
        break;
      case "TR":
      case "TOPRIGHT":
        newPos.x = newPos.x + winBound.pos.w - pos.w;
        break;
      case "BL":
      case "BOTTOMLEFT":
        newPos.y = newPos.y + winBound.pos.h - pos.h;
        break;
      case "BR":
      case "BOTTOMRIGHT":
        newPos.x = newPos.x + winBound.pos.w - pos.w;
        newPos.y = newPos.y + winBound.pos.h - pos.h;
        break;
      default:
        Shell.warning("GWidget: Unknown corner (" + corner + ")");
        break;
    }
    setBounds(newPos);
  }

  /** Set who to send Messages from
   *  @param from who to send Messages from
   */
  public void setFrom (Object from) {
    this.from = from;
  }

  /**
   * Set If Drag Box moves are disabled because a Scroll Bar knob is sliding
   * @param scrollNoMove temporarily disable drag box moves because scrolling
   * @since NeXtMidas 3.7.0 for Bug 2825
   */
  void setScrollNoMove(boolean scrollNoMove) {
    this.scrollNoMove = scrollNoMove;
  }
}
