package nxm.sys.libg;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;

import javax.swing.JPanel;

import nxm.sys.libg.Theme;

/** A class to handle a text area using a NeXtMidas theme and scrollbar..
    This can be used to implement Area, List, or Tree functions.
    Since it is often used as a long menu, it is one-based. Line 0 is the input.
    In 3.5.1, this class was made package-private, it was deprecated in NeXtMidas 2.7.1, use {@link MJText} instead
    @author  Jeff Schoen
*/
@SuppressWarnings("serial")
class MText extends JPanel
  implements MouseListener, MouseMotionListener, KeyListener, FocusListener {

  public static final String flagsList =
    "Multiple,Fifo,Tree,Columns,NoSelect,NoHilite,BackLite,Input,Text,Wrap,Color,Banner,AddPlus";

  /** Multiple flag*/
  public static final int MULT    =    0x1;
  /** FIFO flag*/
  public static final int FIFO    =    0x2;
  /** Tree flag*/
  public static final int TREE    =    0x4;
  /** Columns flag*/
  public static final int COLS    =    0x8;
  /** No Select flag*/
  public static final int NOSEL   =   0x10;
  /** No Highlight flag*/
  public static final int NOHIL   =   0x20;
  /** Backlight flag*/
  public static final int BACKL   =   0x40;
  /** Input flag*/
  public static final int INPUT   =   0x80;
  /** Text flag*/
  public static final int TEXT    =  0x100;
  /** Wrap flag*/
  public static final int WRAP    =  0x200;
  /** Color flag*/
  public static final int COLOR   =  0x400;
  /** Banner flag*/
  public static final int BANNER  =  0x800;
  /** Add '+ at end of wrapped line flag*/
  public static final int ADDPLUS = 0x1000;  // Show the '+' ad the end of a wrapped line (BUG 1563)
  /** indicates the MJText is from MSGLOG */
  public static final int LOGGER  = 0x2000;  // indicates the MJText is from MSGLOG (BUG 1563)

  int flags;
  Font font;
  Theme theme;
  /** Number of rows that can be displayed */
  int rows;
  /** Total rows available for display on the screen */
  int screenRows;
  /** Number of columns that can be displayed */
  int cols;
  /** Contains the output data
      The first element always contains the prompt text
      The last element always contains blank  ""   */
  String[] line;
  /** Total number of output data elements in the "line" String Array.
      This number does not account for the 0 element which represents the
      prompt    */
  int items;
  /** Maximum number of characters for any element contained in lines[]*/
  int maxlen=0;
  /** Text Width */
  int tw=10;
  /** Text Height */
  int th=20;
  /** Text Ascent */
  int ta=10;
  /** Banner Height */
  int bh=0;
  int selecti1=-1, selecti2=-1, selectc1=-1, selectc2=-1;
  int selecti1_, selecti2_, selectc1_, selectc2_;
  int cw;
  /** Row number of the input prompt */
  int inrow;
  /** Number of rows of data that can be displayed on screen */
  int nr;
  /** Scroll Index. Index of data displayed on top of screen */
  int si=0;
  int sr=0;
  /** Maximum row displayed*/
  int mr=1;
  int sv=5;  // Used only for calculating sw
  /** Width of scroll bar */
  int scrollBarWidth=sv*3;
  /** Indicator if window should contain a scroll bar */
  boolean hasScroll;
  boolean dragScroll,dragSelect;
  ActionListener actionListener;
  boolean refreshI,refreshO;
  int[] row2line;
  String banner,prompt,input;  // where all the input data is stored
  int cursor=-1;
  int posi,posc;
  int wraplen;
  MWindow MW;

  /** Constructor for generic text list display
      @param lines The number of text lines to setup for.
      @param rows The number of rows to initially display.
      @param cols The number of columns to initially display.
      @param fontsize The fontsize in pixels to use
      @param theme The color theme to use
      @param flags The options to enable
   */
  public MText (int lines, int rows, int cols, int fontsize, Theme theme, int flags) {
    setFlags(flags);
    setFontSize(fontsize);
    setTheme(theme);
    setSize (cols*tw,rows*th);
    setLines(lines);
    addMouseListener(this);
    addMouseMotionListener(this);
    addKeyListener(this);
    addFocusListener(this);
  }

  public void addActionListener (ActionListener obj) {
    actionListener = obj;
  }

  private void refreshI() {
    refreshI = true;

    // JWB - The line below was changed to eliminate glitches when using double-buffering
    if (isVisible()) repaint();
  }

  public void refreshO() {
    refreshO = true;

    // JWB - The line below was changed to eliminate glitches when using double-buffering
    if (isVisible()) repaint();
  }

  public void setMWindow (MWindow mw) {
    MW = mw;
    theme = MW.theme;
  }

  public void setFontSize (int size) {
    font = new Font ("Monospaced",Font.PLAIN,size);
    FontMetrics fm = getFontMetrics(font);
    tw = fm.stringWidth("0");
    th = fm.getHeight();
    ta = fm.getAscent();
  }

  /**
     Initialize a String array resize if it exists
     The String array line contains all know lines of text
   * @param lines The size of the array
   */
  public void setLines (int lines) {
    String[] oline = line;
    line = new String[lines+1];
    if (oline != null) for (int i=0; i<=items; i++) line[i]=oline[i];
    items = 0;
    maxlen = 0;
  }

  public void setPrompt (String text) {
    prompt = text;
    refreshI();
  }

  public void setInput (String text) {
    input = text;
    cursor = text.length();
    refreshI();
  }

  public String getInput() {
    return input;
  }

  public void setBanner (String text) {
    banner = text;
    refreshO();
  }

  public String getBanner() {
    return banner;
  }

  /**
    Add a line of text to the text area
    @param text
   */
  public void addText (String text) {
    int lf = text.indexOf('\n');
    if (items==0) line[++items]="";
    while (lf>=0) {
      line[items] += text.substring(0,lf);
      add("");
      text = text.substring(lf+1);
      lf = text.indexOf('\n');
    }
    line[items] += text;
    refreshO();
  }

  public void add (String text) {
    boolean fifo = (flags&FIFO) != 0;
    boolean full = items+1 >= line.length;
    if (full && fifo) {        // shift data
      for (int i=0; i<items; i++) line[i] = line[i+1];
      setLine(items,text);
      return;
    }
    if (full) setLines(line.length*2);    // expand array

    setLine(++items,text);
  }


  // Determine number of wrapped lines. Note: This does not account for line[0]
  // which represents the prompt text
  // This is a slow way for proof of concept.
  // This could be maintained in sync with "items" and
  // recalculated if screen size is changed.
  private int getNumItemsWithWrap(int startIndex) {
    int numRowsWithWrap=0;
    for (int i=startIndex;i<items;i++) {
      // Add to total any additional wrapped lines
      numRowsWithWrap = numRowsWithWrap + (getNbrWraps(line[i]) - 1 );
    }
    return numRowsWithWrap;
  }


  /**
    Determine how many display rows would be occupied by wrapped text
    @param text
    @return int number of displayed row occupied by a line of text
   */
  public int getNbrWraps(String text) {
   if (text==null || text.length() == 0) return 1;
   return (int)Math.ceil((float)text.length() / getNumCharsWidth());
  }


  /*
    Return the number of display characters that should fit
    in width of screen
   */
  private int getNumCharsWidth() {
    int colWidth = (getWidth() - (isScroll()?scrollBarWidth:0)) / getNumSplitScreenColumns();
    return Math.max(2,colWidth/tw-1);
  }


  /*
    Determine the number of split screens.
    Controlled from the GTerminal configuration screen
    Note: DMS - I haven't fully grasped this logic, nor does the functionality
    seem to be working or useful.  We may want to obsolete this
   */
  private int getNumSplitScreenColumns() {
    int numberOfRows = Math.max(1,rows);              // number of rows
    int numSplitScreen = Math.max(1,Math.min((items-1)/numberOfRows+1,cols/(maxlen+2)));
    if ((flags&COLS)==0) numSplitScreen=1;
    return numSplitScreen;
  }

  /*
     Get the number of rows needed for the prompt
   */
  private int getPromptRows() {
     // If logger, no rows needed for prompt
     if ((flags&LOGGER)!=0 ) {
       return 0;
     } else {
       return getNbrWraps(line[0]);
     }
  }

  /** Get line by index
   *  @param i index
   *  @return selected line
   */
  public String getLine (int i) {
    if (i<0 || i>items) return null;
    return line[i];
  }

  public void replace (String text) {
    setLine(items,text);
  }

  /** Set the text of the line at given index
   *  @param i index
   *  @param text line text
   */
  public void setLine (int i, String text) {
    if (i<0 || i>items) return;
    line[i] = text;
    maxlen = Math.max(maxlen,text.length());
    resetScreenIndex();
    refreshO();
  }

  @Override
  public void removeAll() {
    for (int i=1; i<=items; i++) line[i]=null;
    items = si = sr = maxlen = 0;
    selecti1 = selecti2 = selectc1 = selectc2 = -1;
    refreshO();
  }

  /** Set the MText flags
   *  @param flags flags as int
   */
  public void setFlags (int flags) {
    this.flags = flags;
    refreshO();
  }

  /** @return MText flags */
  public int getFlags() {
    return flags;
  }

  /** Set the Theme for the MText
   *  @param theme the Theme
   */
  public void setTheme (Theme theme) {
    this.theme = theme;
    repaint();
  }

  /** Select item
   *  @param item item to select
   */
  public void select (int item) {
    select (item,-1);
  }

  /** select at item and offset
   *  @param item item for selection
   *  @param ich  offset for selection
   */
  public void select (int item, int ich) {
    if ((flags&NOSEL)!=0) return;
    if (item<0 || item>items) return;
    String text = line[item];
    int len = text.length();
    if (item==selecti1 && (ich<0 || ich>=len || (ich>=selectc1 && ich<selectc2))) {
      if (selectc1==0 && selectc2==len) { selecti1=selecti2=-1; }  // deselect
      else { selectc1=0; selectc2=len; }  // select entire line
    }
    else {
      selecti1=selecti2=item;
      selectc1=0; selectc2=len;
      // check for word select
      if ((flags&TEXT) != 0 && ich>=0 && ich<len && text.charAt(ich)>' ') {
        for (selectc1=ich; selectc1>0 && text.charAt(selectc1-1)>' '; selectc1--);
        for (selectc2=ich; selectc2<len && text.charAt(selectc2)>' '; selectc2++);
      }
    }
    if (actionListener != null) {
      ActionEvent e = new ActionEvent(this,0,"SELECT",selecti1);
      actionListener.actionPerformed(e);
    }
    copy(1);
    refreshO();
  }

  /** Copy the selected text to the system clipboard */
  public void copy() { copy(0); }

  /** Copy selected text to either selection buffer (when mode = 1) or the
   *  system clipboard buffer (when mode != 0).
   *  @param mode set to 1 to copy to selection buffer (X-Windows),
   *              otherwise copy to system clipboard.
   */
  private void copy (int mode) {
    if ((flags&TEXT)==0);
    else if (mode==1) GraphicsUtil.toSystemSelection(getSelectedItem());
    else GraphicsUtil.toSystemClipboard(getSelectedItem());
  }

  /** De-select this item
   *  @param item item to deselect
   */
  public void deselect (int item) {
    selecti1 = selecti2 = -1;
    refreshO();
  }

  /** @return selected items as String */
  public String getSelectedItem() {
    String s=null;
    if (selecti1<0);
    else if (selecti1==selecti2) s=line[selecti1].substring(selectc1,selectc2);
    else for (int i=selecti1; i<=selecti2; i++) {
           if (i==selecti1) s = line[i].substring(selectc1) + "\n";
      else if (i==selecti2) s += line[i].substring(0,selectc2);
      else s += line[i] + "\n";
    }
    return s;
  }

  /** @return selected items as String array */
  public String[] getSelectedItems() {
    return null;
  }

  /** @return the number of visible rows */
  public int getRows() { return rows; }

  /** @return the number of visible columns */
  public int getColumns() { return cols; }

  @Override
  public void update (Graphics g) {
    paint(g);
  }

  @Override
  public void paint (Graphics g) {
    if (g != null && (Theme.getOption(Theme.SWINGDB))) super.paintComponent(g); // Swing Double-buffering requires full repaint
    bh = ((flags&BANNER) != 0)? th+6 : 0;  // Banner height
    boolean isInput = (flags&INPUT) != 0;
    boolean inputOnly = isInput && refreshI && !refreshO;
    refreshI = false;
    refreshO = false;

    // Determine number of characters wide (cols) on the screen
    // Calculation factors in scroll bar width
    int w = getWidth();
    cols = Math.max(0, (w-scrollBarWidth) / tw);

    calcAvailableScreenRows();
    nr = Math.max(1,rows);              // number of rows

    int numSplitScreensCols = getNumSplitScreenColumns();
    mr = Math.max(0,(items-1)/numSplitScreensCols)+1;

    setHasScroll();
    if (!isScroll()) si=sr=0;
    if (si>0) { sr=si/numSplitScreensCols; si=sr*numSplitScreensCols; }

    cw = (w - (isScroll()?scrollBarWidth:0)) / numSplitScreensCols;
    int alen = getNumCharsWidth();   // The width of the display window
    wraplen = alen-2;                // how many chars to draw when wrapping
    if (g==null) return; // just calculating rows for setScroll
    g.setFont(font);
    if (inputOnly) {
      paintInput(g); return;
    }   // input only
    g.setColor(theme.cwbg);
    g.drawLine(0,1,w-1,1);
    g.drawLine(0,2,w-1,2);
    if (isScroll()) paintScroll(g);     // scroll bar
    if (bh>0) paintBanner(g);       // banner

    // loop through text lines
    if (row2line==null || row2line.length!=nr) row2line = new int[nr];
    int ir=0;
    int wrap=0;
    for (int lineIndex=si+1,ic=0; lineIndex<=items; lineIndex++) {
      int x = ic*cw;
      int y = ir*th + th + bh;
      paintLine (g,lineIndex,x,y,alen,wrap);
      if (ic==0) row2line[ir] = lineIndex;
      if (++ic >= numSplitScreensCols) { ic=0; ir++; }
      if (ir == nr) break;
      if ((flags&WRAP)!=0 && line[lineIndex]!=null && wrap+alen<line[lineIndex].length()) {
        wrap += wraplen; lineIndex--;
      }
      else wrap=0;
    }
    int y = ir*th +th + bh - ta;
    g.setColor(theme.cwbg);      // fill space
    g.fillRect(0,y,w-1-(isScroll()?scrollBarWidth:0),getHeight()-y);

    setInputRowIndex(ir);
    if (isInput) paintInput(g);
    // frame border
    g.setColor(theme.cwms);
    g.drawLine(1,0,w-3,0);
    g.drawLine(1,getHeight()-2,w-3,getHeight()-2);
    // column dividers
    for (int i=1; i<numSplitScreensCols; i++) g.drawLine(i*cw,bh+tw,i*cw,getHeight()-tw-1);
    // place holders
    for (;ir<row2line.length;ir++) row2line[ir]=-1;
  }

  /*
    Set the screen row index of the input prompt.
   */
  private void setInputRowIndex(int index) {
    if (isScroll()) {
      // Index of input row is the last available position accounting for
      // more then one row in the prompt
      if ((flags&LOGGER)!=0 ) {
        // If logger, no extra row needed for prompt
        inrow = screenRows;
      } else  {
        inrow = screenRows - getPromptRows() + 1 ;
      }
    } else {
      // Index of input row is one more then the number of items
      inrow = items + getNumItemsWithWrap(1) + 1;
    }
  }

  // Determine number of rows available for display on the screen
  // Calculation factors in banner.
  private void calcAvailableScreenRows() {
    rows = Math.max(0, (getHeight()-bh-th-ta) / th);
    //rows = Math.max(0, (getHeight()-bh-5) / th);
    //if ((flags&INPUT) != 0 && rows>0) rows--;    // subtract a row for the input area
    screenRows = rows + 1;
  }

  /** Paint a line of text
      @param g The Graphics context
      @param i The line number to draw
      @param x The X Position
      @param y The Y Position
      @param alen The width of the display window
      @param off Offset. 0 unless this is a wrapped line.
   */
  private void paintLine (Graphics g, int i, int x, int y, int alen, int off) {
    String text = line[i];   // get the text string for that line
    g.setColor(theme.cwbg);
    g.fillRect(x,y-ta,cw-1,th);
    if (text==null) return;
    x += tw;

    // unless disabled, color the text of certain lines
    Color c = theme.cwfg;
    if ((flags&COLOR)==0);
    else if (text.startsWith("INFO:"))  c = Color.green;
    else if (text.startsWith("WARN:"))  c = Color.yellow;
    else if (text.startsWith("ERROR:")) c = Color.red;

    if (off>0) text = text.substring(off);    // if this is the continuation of a previous line
    int tl = text.length();
    if (tl>alen) {                            // if length of line is longer than width of screen
      tl=alen-2;                              // go back 2 chars and then get the substring up to that point
      text = text.substring(0,tl);
      if ((flags&ADDPLUS)!=0) text += " +";   // add a plus sign unless it is turned off
    }

    // selected / highlighted text
    if (i>=selecti1 && i<=selecti2) {
      int c1 = Math.max(0,Math.min(tl,(i==selecti1)? selectc1-off : 0));
      int c2 = Math.max(0,Math.min(tl,(i==selecti2)? selectc2-off : tl));
      if ((flags&BACKL)!=0) {   // backlit selected text
        g.setColor(theme.cwfh);
        g.fillRect(x+c1*tw,y-ta,(c2-c1)*tw,th);
        g.setColor(c);
        g.drawString(text,x,y);
      } else {
        g.setColor(c);
        g.drawString(text,x,y);
        g.setColor(theme.cwfh);
        g.drawString(text.substring(c1,c2),x+c1*tw,y);
      }
    }
    else {
      if (i==0) { // input prompt
        int pl = Math.min(Math.min(alen,prompt.length()), text.length());
        g.setColor(theme.cwfh);
        g.drawString(text.substring(0,pl),x,y);
        text = text.substring(pl); x += tw*pl;
      }
      g.setColor(c);
      g.drawString(text,x,y);  // draw the text
    }
  }

  private void paintBanner (Graphics g) {
    int x = tw;
    int y = th;
    int w = getWidth() - (isScroll()?scrollBarWidth-2:2);
    int h = bh;
    if (banner==null) banner = "Null Banner";
    g.setColor(theme.cwms);
    g.fillRect(2,0,w-2,h);
    g.setColor(theme.cwfh);
    g.drawString(banner,x,y);
  }

  private void paintInput (Graphics g) {
    line[0] = prompt;
    if (input != null) line[0] += input;
    if (line[0]==null) return;
    int lp = prompt.length();
    int x = 0;
    int y = inrow*th + bh;
    int w = getWidth() - x - tw - (isScroll()?scrollBarWidth:4);
    int alen = (w-2)/tw - 1;
    paintLine (g,0,x,y,alen,0);
    if (!hasFocus()) return;
    x += (cursor+lp+1)*tw;
    g.setColor(theme.cwfh);
    if (x<w) g.drawLine(x,y-2*th/3,x,y);
    g.setColor(theme.cwfg);
  }

  private void paintScroll (Graphics g) {
    int x=getWidth()-scrollBarWidth/2-2, y=0, w=scrollBarWidth, h=getHeight()-2;
    double f=(double)sr/(mr-nr);
    int flags = MWindow.SLIDER_INSET | MWindow.SLIDER_NOTRACK |
                (dragScroll? MWindow.SLIDER_ACTIVE : 0);
    MWindow.drawVSlider(x,y,w,h, f,flags, g,theme);
  }

  public void setScroll (int index) {
    paint((Graphics)null); // just calculate rows
    if (index<0) index = Math.max(0,items-rows);
    else if (index>=items) index = items-1;
    si = sr = index;
    refreshO();
  }


  private boolean updateScroll (int y) {
    dragScroll = true;
    int h = getHeight()-scrollBarWidth-6;
    if (h<th) return false;
    int r = (y-scrollBarWidth/2-3)*mr/h;
    int i = r*getNumSplitScreenColumns();
    if (i<0 || i>=items) return false;
    si = i;
    sr = r;
    return true;
  }

  /**
   * Determine if the window should contain a  scroll bar
   */
  private void setHasScroll() {
    hasScroll = items+getNumItemsWithWrap(1) > getNumSplitScreenColumns()*nr;
  }

  /**
   * Sould the screen contain a scroll bar
   * @return boolean
   */
  public boolean isScroll() {
    return hasScroll;
  }


  /*
    Set si. This is one less then the start of the line[] String array that
    should be displayed on the screen.
  */
  private void resetScreenIndex()  {
    si=sr=calcBeginLineIndex(true)-1;
  }

  /*
   * Calculate the index of the line[] array that should be displayed at the top
   * of the screen
   */
  private int calcBeginLineIndex(boolean includeWraps) {

    int numDispRowsForData = screenRows-getPromptRows();
    int beginLineIdx = Math.max(0,items-numDispRowsForData)+1;
    if ( includeWraps) {
      int wrapLines = 0; // Lines including wrap
      int count=items+1;
      for (int idx=items;idx>=beginLineIdx;idx--) {
        wrapLines += getNbrWraps(line[idx]);
        if (wrapLines> numDispRowsForData) {
          break;
        }
        count--;
      }
      beginLineIdx=count;
    }

    return Math.max(1,beginLineIdx);
  }



  public void keyPressed (KeyEvent e) {
    if ((flags&INPUT)==0) return;
    int keycode = e.getKeyCode();
    char keychar = e.getKeyChar();
    boolean cc = e.isControlDown();
    String action = null;
    int modifier = 0;

    if (keycode == KeyEvent.VK_ENTER) {
      action = "ENTER";
      resetScreenIndex();
    }
    else if (keycode == KeyEvent.VK_BACK_SPACE && cursor>0) {
      input = input.substring(0,cursor-1) + input.substring(cursor);
      cursor--;
      refreshI();
    }
    else if (keycode == KeyEvent.VK_DELETE && selecti1==0) {
      int i1 = Math.max(0,selectc1-prompt.length());
      int i2 = Math.max(0,selectc2-prompt.length());
      input = input.substring(0,i1) + input.substring(i2);
      selecti1 = selecti2 = -1;
      cursor = i1;
      refreshI();
    }
    else if (keycode == KeyEvent.VK_DELETE && cursor<input.length()) {
      input = input.substring(0,cursor) + input.substring(cursor+1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_RIGHT || (cc && keycode == KeyEvent.VK_F)) {  // right arrow
      cursor = Math.min(input.length(),cursor+1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_LEFT) {  // left arrow
      cursor = Math.max(0,cursor-1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_UP) {  // up arrow
      modifier = e.isShiftDown()? -2 : -1;
      action = "POPCODE";
    }
    else if (keycode == KeyEvent.VK_DOWN) {  // down arrow
      modifier = e.isShiftDown()? 2 : 1;
      action = "POPCODE";
    }
    else if ((keycode == KeyEvent.VK_END) || (cc && keycode == KeyEvent.VK_E)) {  // end of line
      cursor = input.length();
      refreshI();
    }
    else if ((keycode == KeyEvent.VK_HOME) || (cc && (keycode == KeyEvent.VK_A || keycode == KeyEvent.VK_H))) {  // beginning of line
      cursor = 0;
      refreshI();
    }
    else if (cc && keycode == KeyEvent.VK_U) {  // delete line
      setInput("");
    }
    else if (cc && keycode == KeyEvent.VK_D) {  // interrupt
      action = "INTERRUPT";
    }
    else if (cc && keycode == KeyEvent.VK_P) {  // pop
      if (MW == null) action = "POP"; // JWB - 27 Aug 09: This is usually caught by MWindow.keyPressed
    }
    else if (cc && keycode == KeyEvent.VK_C) {  // copy
      String selectedText = getSelectedItem();
      if (selectedText==null || selectedText.length() <= 0 ) {
        // print info message to user to use CTRL-D instead of CTRL-C for BUG 1109
        addText("INFO: Use Ctrl-D=Interrupt, Ctrl-C=CopySelectedText\n"); // BUG 2218: always display message in this MJText/SHELLGUI terminal
      }
      else copy();
    }
    else if (cc && keycode == KeyEvent.VK_V) {  // paste
      paste();
    }
    else if (keychar>=' ' && keychar<='~' && !cc) {
      input = input.substring(0,cursor) + keychar + input.substring(cursor);
      cursor++;
      if (cursor==1) action = "KEYPRESS";
      refreshI();
    }
    if (actionListener != null && action != null) {
      ActionEvent ae = new ActionEvent(this,0,action,modifier);
      actionListener.actionPerformed(ae);
    }
  }

  public void addInput (String text) {
    input = input.substring(0,cursor) + text + input.substring(cursor);
    cursor += text.length();
    refreshI();
  }

  public void keyReleased (KeyEvent e) {
    // Do nothing
  }

  public void keyTyped (KeyEvent e) {
    // Do nothing
  }

  public void mouseEntered (MouseEvent e) {
  }

  public void mouseExited (MouseEvent e) {
  }

  public void focusGained (FocusEvent e) {
    refreshI();
  }
  public void focusLost (FocusEvent e) {
    refreshI();
  }


  private boolean hit (int x, int y) {
    int ic = Math.max(0,Math.min(getNumSplitScreenColumns()-1, (x-tw)/cw ));
    int ir = (y-bh)/th;
    int c  = (x-tw-ic*cw)/tw;
    int i = -1;
    posi = posc = i;
    if ((flags&INPUT)!=0 && ir==inrow && c<line[0].length()) i=0;
    else if (ir>=0 && ir<nr) i = row2line[ir] + ic;
    if (i<0 || i>items) return false;
    for (;ir>0 && row2line[ir-1]==row2line[ir]; ir--) c+=wraplen;
    posi = i;
    posc = Math.max(0,Math.min(line[i].length(), c));
    return true;
  }

  private void startSelect (int x, int y) {
    if (hit(x,y)) select(posi,posc);
  }

  private boolean updateSelect (int x, int y) {
    boolean paint = false;
    hit(x,y);
    if (posi<0 || posi>items || line[posi]==null) return paint;
    if (dragSelect) {
      paint = (posi!=selecti2_ || posc!=selectc2_);
      selecti2_ = posi;
      selectc2_ = posc;
    } else {
      selecti1_ = selecti2_ = posi;
      selectc1_ = selectc2_ = posc;
      dragSelect = true;
    }
    if (selecti2_<selecti1_ || (selecti2_==selecti1_ && selectc2_<=selectc1_)) {
      selecti1 = selecti2_; selectc1 = selectc2_;
      selecti2 = selecti1_; selectc2 = selectc1_;
    } else {
      selecti1 = selecti1_; selectc1 = selectc1_;
      selecti2 = selecti2_; selectc2 = selectc2_;
    }
    if (paint) copy(1);
    return paint;
  }

  public void mousePressed (MouseEvent e) {
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    int w = getWidth();
    if (event2button(e)!=1) return;
    if (isScroll() && x>w-scrollBarWidth) {
      if (updateScroll(y)) repaint();
    }
  }

  public void mouseReleased (MouseEvent e) {
    if (dragScroll) { dragScroll=false; repaint(); }
    dragSelect = false;
  }

  public void mouseClicked (MouseEvent e) {
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    int button = event2button(e);
    if (button==1) {
      if (y<bh) {
        ActionEvent ae = new ActionEvent(this,0,"BANNER",button);
        if (actionListener!=null) actionListener.actionPerformed(ae);
      }
      else if (x<cw*getNumSplitScreenColumns()) {
        startSelect (x,y);
      }
    }
    else if (button==2 && (flags&INPUT)!=0) {
      paste(1);
    }
    else {
      ActionEvent ae = new ActionEvent(this,0,"BUTTON",button);
      if (actionListener!=null) actionListener.actionPerformed(ae);
    }
  }

  public void paste() { paste(0); }
  private void paste (int mode) {
    if ((flags&INPUT)==0) return;
    String text = (mode==1)? GraphicsUtil.fromSystemSelection()
                           : GraphicsUtil.fromSystemClipboard();
    if (text != null) addInput(text);
  }

  public void mouseMoved (MouseEvent e) {
    if (MW==null) return;
    Rectangle pos = getBounds();
    MW.px = pos.x + e.getPoint().x;
    MW.py = pos.y + e.getPoint().y;
  }

  private void mouseMovedAction() {
  }

  public void mouseDragged (MouseEvent e) {
    if (event2button(e) != 1) return;
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    if (dragScroll) {
      if (updateScroll(y)) repaint();
    }
    else if ((flags&TEXT)!=0) {
      if (updateSelect(x,y)) repaint();
    }
  }

  /** Allow use of Mouse Scroll Wheel to scroll through lists
      @since NeXtMidas 2.7.2
   */
  public void mouseWheelMoved (MouseWheelEvent e) {
    if (e.getWheelRotation() > 0) {

     int beginLineIdx = calcBeginLineIndex(true);
     if (si  < beginLineIdx-1 ) {
        if ( (si-1) < beginLineIdx ) {
          resetScreenIndex();
        } else {
          si++;
        }
        repaint();
      }
    } else {
      if (si> 0) { si--; repaint(); } // scroll up
    }
  }

  /** Convert event to button number. */
  private int event2button (AWTEvent e) {
    MouseEvent me = (MouseEvent) e;
    int button=me.getButton(); // update in 3.9.0 to get button from event object
    int modifiers = me.getModifiersEx();
    if(button == 0) {
      if ((modifiers&MouseEvent.BUTTON1_DOWN_MASK)!=0 ) button = 1;
      else if ((modifiers&MouseEvent.BUTTON2_DOWN_MASK)!=0 ) button = 2;
      else if ((modifiers&MouseEvent.BUTTON3_DOWN_MASK)!=0 ) button = 3;
      else button = 1;
    }
    else if(button>MouseEvent.BUTTON3) button = MouseEvent.BUTTON1;
    if (me.isShiftDown()) button = button+100; // Updated in 3.9.0
    if (me.isControlDown()) button = button+200; // Updated in 3.9.0
    return button;
  }

}
