/********************************************************************\
 * text.c -- a basic text field                                     *
 * Copyright (C) 1997 Robin D. Clark                                *
 *                                                                  *
 * This program is free software; you can redistribute it and/or    *
 * modify it under the terms of the GNU General Public License as   *
 * published by the Free Software Foundation; either version 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program is distributed in the hope that it will be useful,  *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
 * GNU General Public License for more details.                     *
 *                                                                  *
 * You should have received a copy of the GNU General Public License*
 * along with this program; if not, write to the Free Software      *
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.        *
 *                                                                  *
 *   Author: Rob Clark                                              *
 * Internet: rclark@cs.hmc.edu                                      *
 *  Address: 609 8th Street                                         *
 *           Huntington Beach, CA 92648-4632                        *
\********************************************************************/

#include "wconfig.h"

#ifndef USE_TEXT_FIELD
static int dummy;
#else
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <stdlib.h>

#include "generic/wwmlib.h"
#include "WindowMaker.h"
#include "text.h"
#include "funcs.h"
#include "keys.h"

#define TEXT_SHIFT -4  /* Move the text up relative to the bottom of
                      * the text-box */
#if 0
#  define  ENTER(X)  fprintf(stderr,"Entering:  %s()\n", X);
#  define  LEAVE(X)  fprintf(stderr,"Leaving:   %s()\n", X);
#  define  DEBUG(X)  fprintf(stderr,"debug:     %s()\n", X);
#else
#  define  ENTER(X)
#  define  LEAVE(X)
#  define  DEBUG(X)
#endif

extern Cursor wCursor[WCUR_LAST];

/********************************************************************\
 * The event handler for the text-field:                            *
\********************************************************************/
static void wTextEventHandler( WObjDescriptor *desc, XEvent *event );

/********************************************************************\
 * How to respond to events:                                        *
 *   All the events are handled in wTextEventHandler(), which       *
 *   returns TRUE if the event is handled, else FALSE.  Call        *
 *   wTextEventHandler() before DispachEvent() in the dialog's      *
 *   event loop.  Only call DispachEvent() if wTextEventHandler()   *
 *   returns FALSE.                                                 *
\********************************************************************/
WMethodTable wTextMethods = {
  (WEventHandler)NULL,                  /* mouse_down event */
  (WEventHandler)NULL,                  /* click event      */
  (WEventHandler)NULL,                  /* dbl_click event  */ 
  (WEventHandler)wTextEventHandler      /* any_event event  */
};


/********************************************************************\
 * keyboard translation stuff:                                      *
 *   Basic idea: keep a table of all the characters we accept as    *
 *               valid input, and us that to figure out the key     *
 *               that was pressed.  Keys that result in an action,  *
 *               such as delete, or the cursor keys, are handled    *
 *               seperately.                                        *
 *   But.... since we won't have text fields all the time, we can   *
 *   keep track of when there are text fields, so we can destroy    *
 *   the table when it is not needed, and re-initialize it when it  *
 *   is needed.  To do this, keep track of the number of instances  *
 *   of the text field, and when instances == 0, we can free the    *
 *   table.                                                         *
\********************************************************************/

/* The various keyboard modifiers: */
#define MK_NONE        0x00
#define MK_SHIFT       0x01
#define MK_SHIFT_LOCK  0x02
#define MK_CONTROL     0x04

static u_int instance   = 0;	       /* The number of text-fields...  */
static u_int modifier   = MK_NONE;     /* The modifier key bitfield     */
static char  **keyTable = NULL;	       /* Table of key strings          */

/********************************************************************\
 * initKeyTable                                                     *
 *   fills in the keyTable                                          *
\********************************************************************/
static void 
initKeyTable( void )
  {
  if( keyTable == NULL )
    {
    int i;
    
    keyTable = (char **)malloc(TABLE_SIZE*sizeof(char*));
    
    /* Initialize the table */
    for( i=0; i<TABLE_SIZE; i++ )
      keyTable[i] = XKeysymToString(i+TABLE_BASE);
    
    /* Override specific keys: */
    for( i=0; i<OVERRIDE_TABLE_SIZE; i++ )
      keyTable[override[i].index-TABLE_BASE] = override[i].value;
    }
  }

/********************************************************************\
 * freeKeyTable                                                     *
 *   deallocates the keyTable                                       *
\********************************************************************/
static void
freeKeyTable( void )
  {
  free(keyTable);
  keyTable = NULL;
  }

/********************************************************************\
 * getKey                                                           *
 *   takes a KeySym, and returns a string.  inits the key-table     *
 *   if necessary                                                   *
\********************************************************************/
static char *
getKey( KeySym ksym )
  {
  /* Figure out upper/lower */
  int    index;
  KeySym upper,lower;
  XConvertCase( ksym, &lower, &upper );
  
  if( keyTable == NULL )
    initKeyTable();
  
  if( modifier & (MK_SHIFT | MK_SHIFT_LOCK) )
    index = upper - TABLE_BASE;
  else
    index = lower - TABLE_BASE;
  
  if( (index < 0) || (index >= TABLE_SIZE) )
    index = 0;

  return keyTable[index];
  }

/********************************************************************\
 * handleKeyPress                                                   *
 *   handle cursor keys, regular keys, etc.  Inserts characters in  *
 *   text field, at cursor position, if it is a "Normal" key.  If   *
 *   ksym is the delete key, backspace key, etc., the appropriate   *
 *   action is performed on the text in the text field.  Does not   *
 *   refresh the text field                                         *
 *                                                                  *
 * Args:   wText - the text field                                   *
 *         ksym  - the key that was pressed                         *
 * Return: TRUE, unless the ksym is ignored                         *
 * Global: modifier - the bitfield that keeps track of the modifier *
 *                    keys that are down                            *
\********************************************************************/
static int
handleKeyPress( WTextInput *wText, KeySym ksym )
  {
  /* Ignore these keys: */
  if( IsFunctionKey(ksym)      || IsKeypadKey(ksym) ||
      IsMiscFunctionKey(ksym)  || IsPFKey(ksym)     ||
      IsPrivateKeypadKey(ksym) )
    /* If we don't handle it, make sure it isn't a key that
     * the window manager needs to see */
    return FALSE;
  
  /* Take care of the cursor keys.. ignore up and down
   * cursor keys */
  else if( IsCursorKey(ksym) )
    {
    int length = wText->text.length;
    switch(ksym)
      {
      case XK_Home:
      case XK_Begin:
      wText->text.endPos = 0;
      break;
      case XK_Left:
      wText->text.endPos--;
      break;
      case XK_Right:
      wText->text.endPos++;
      break;
      case XK_End:
      wText->text.endPos = length;
      break;
      default:
      return FALSE;
      }
    /* make sure that the startPos and endPos have values
     * that make sense  (ie the are in [0..length] ) */
    if( wText->text.endPos < 0 )
      wText->text.endPos = 0;
    if( wText->text.endPos > length )
      wText->text.endPos = length;
    wText->text.startPos = wText->text.endPos;
    }
  
  /* Handle the modifier keys... we need to keep track of
   * some of the modifier keys, such as the shift key, so
   * we know if the user is trying to get an upper-case
   * key, etc. */
  else if( IsModifierKey(ksym) )
    {
    switch(ksym)
      {
      case XK_Shift_L:
      case XK_Shift_R:
      modifier |= MK_SHIFT;
      break;
      case XK_Control_L:
      case XK_Control_R:
      modifier |= MK_CONTROL;
      break;
      case XK_Shift_Lock:
      modifier |= MK_SHIFT_LOCK;
      break;
      default:
      return FALSE;
      }
    }
  
  /* Handle all the other keys... do a lookup from a table to 
   * find the normal keys ??? */
  else
    {
    switch(ksym)
      {
      /* Ignore these keys: */
      case XK_Return:
      wText->done = TRUE;
      case XK_Tab:
      case XK_Num_Lock:
      case XK_Escape:
      break;
      
      case XK_Delete:
      /* delete after cursor */
      if( (wText->text.endPos == wText->text.startPos) && 
          (wText->text.endPos < wText->text.length) )
        wText->text.endPos++;
      wTextInsert( wText, "" );
      break;
      case XK_BackSpace:
      /* delete before cursor */
      if(  (wText->text.endPos == wText->text.startPos) && 
           (wText->text.startPos > 0) )
        wText->text.startPos--;
      wTextInsert( wText, "" );
      break;
      default:
      wTextInsert( wText, getKey(ksym) );
      }
    }  
  return TRUE;
  }

/********************************************************************\
 * handleKeyRelease                                                 *
 *   keep track of when the modifier keys are released              *
 *   returns TRUE if KeyPress is handled, else false                *
 *                                                                  *
 * Args:   wText - the text field                                   *
 *         ksym  - the key that was pressed                         *
 * Return: TRUE, unless the ksym is ignored                         *
 * Global: modifier - the bitfield that keeps track of the modifier *
 *                    keys that are down                            *
\********************************************************************/
static int
handleKeyRelease( WTextInput *wText, KeySym ksym )
  {
  if( IsModifierKey(ksym) )
    {
    switch(ksym)
      {
      case XK_Shift_L:
      case XK_Shift_R:
      modifier &= ~MK_SHIFT;
      break;
      case XK_Control_L:
      case XK_Control_R:
      modifier = (modifier & ~(MK_CONTROL));
      break;
      case XK_Shift_Lock:
      modifier = (modifier & ~(MK_SHIFT_LOCK));
      break;
      default:
      return FALSE;
      }
    }
  return TRUE;
  }

/********************************************************************\
 * wTextXYtoPos                                                     *
 *   given X coord, return position in array                        *
\********************************************************************/
static int
wTextXtoPos( WTextInput *wText, int x )
  {
  int pos;
  x -= wText->xOffset;
  
  for( pos=0; wText->text.txt[pos] != '\0'; pos++ )
    {
    if( x < 0 )
      break;
    else
      x -= MyTextWidth( wText->font->font, &(wText->text.txt[pos]), 1 );
    }
  
  return pos;
  }

/********************************************************************\
 * wTextCreate                                                      *
 *   create an instance of a text class                             *
 *                                                                  *
 * Args:                                                            *
 * Return:                                                          *
 * Global: dpy - the display                                        *
\********************************************************************/
WTextInput *
wTextCreate( WCoreWindow *core, int x, int y, int width, int height )
  {
  WTextInput *wText;
  
  ENTER("wTextCreate");
  
  instance++;                      /* Keep track of the number of instances */
  
  wText = wmalloc(sizeof(WTextInput));
  wText->core = wCoreCreate( core, x, y, width, height );
  wText->core->descriptor.self    = wText;
  wText->core->descriptor.methods = &wTextMethods;
  
  /* maybe we should have our own font?? how do we handle mapping
   * the text class onto a menu?  */
  wText->font = &(core->screen_ptr->panel_text_font);
  
  /* setup the text: */
  wText->text.txt      = (char *)wmalloc(sizeof(char));
  wText->text.txt[0]   = '\0';
  wText->text.length   = 0;
  wText->text.startPos = 0;
  wText->text.endPos   = 0;
  
    {
    XGCValues gcv;
    
    /* set up the regular context */
    gcv.foreground = core->screen_ptr->black_pixel;
    gcv.background = core->screen_ptr->white_pixel;
    gcv.font       = wText->font->font->fid;
    gcv.line_width = 1;
    gcv.function   = GXcopy;
    
    wText->regGC = XCreateGC( dpy, wText->core->window,
                            (GCForeground|GCBackground|
                             GCFunction|GCFont|GCLineWidth),
                            &gcv );
    
    /* set up the inverted context */
    gcv.function   = GXcopyInverted;
    
    wText->invGC = XCreateGC( dpy, wText->core->window,
                            (GCForeground|GCBackground|
                             GCFunction|GCFont|GCLineWidth),
                            &gcv );
    
    /* and set the background! */
    XSetWindowBackground( dpy, wText->core->window, gcv.background );
    }
  
  /* Figure out the y-offset... */
  wText->yOffset = (height - wText->font->height)/2;
  wText->xOffset = wText->yOffset;
  
  wText->buttonDown   = FALSE;     /* The mouse button is not down  */
  wText->done         = FALSE;     /* becomes true when the user    *
                                  * hits "Return" key             */
  
  LEAVE("wTextCreate");
  
  return wText;
  }

/********************************************************************\
 * wTextDestroy                                                     *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 * Return:                                                          *
 * Global: dpy    - the display                                     *
\********************************************************************/
void
wTextDestroy( WTextInput *wText )
  {
  ENTER("wTextDestroy")
  
  XFreeGC( dpy, wText->regGC );
  XFreeGC( dpy, wText->invGC );
  free( wText->text.txt );
  wCoreDestroy( wText->core );
  wfree( wText );
  
  instance--;
  if( instance == 0 )
    freeKeyTable();
  
  LEAVE("wTextDestroy");
  }

/* The text-field consists of a frame drawn around the outside,
 * and a textbox inside.  The space between the frame and the
 * text-box is the xOffset,yOffset.  When the text needs refreshing,
 * we only have to redraw the part inside the text-box, and we can
 * leave the frame.  If we get an expose event, or for some reason
 * need to redraw the frame, wTextPaint will redraw the frame, and
 * then call wTextRefresh to redraw the text-box */

/********************************************************************\
 * wTextPaint                                                       *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 * Return:                                                          *
 * Global: dpy    - the display                                     *
\********************************************************************/
void
wTextPaint( WTextInput *wText )
  {
  WScreen *scr = wText->core->screen_ptr;
  Window   win = wText->core->window;
  int      x   = wText->core->width-1;
  int      y   = wText->core->height-1;
  
  ENTER("wTextPaint");
  
  /* Draw box */
  XDrawLine( dpy, win, scr->normal_button_texture->solid.dim_gc,
           0, 0, 0, y );
  XDrawLine( dpy, win, scr->normal_button_texture->solid.dim_gc, 
           0, 0, x, 0 );
      
  XDrawLine( dpy, win, scr->normal_button_texture->solid.dark_gc,
           1, 1, 1, y-2 );
  XDrawLine( dpy, win, scr->normal_button_texture->solid.dark_gc,
           1, 1, x-2, 1 );

  XDrawLine( dpy, win, scr->normal_button_texture->solid.normal_gc, 
           x-1, y-1, 1, y-1 );
  XDrawLine( dpy, win, scr->normal_button_texture->solid.normal_gc, 
           x-1, y-1, x-1, 1 );
  
  /* refresh */
  wTextRefresh( wText );
  
  LEAVE("wTextPaint");
  }

/********************************************************************\
 * wTextRefresh                                                     *
 *   Redraw the text field.  Call this after messing with the text  *
 *   field.  wTextRefresh re-draws the inside of the text field. If *
 *   the frame-area of the text-field needs redrawing, call         *
 *   wTextPaint()                                                   *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 * Return: none                                                     *
 * Global: dpy    - the display                                     *
\********************************************************************/
void
wTextRefresh( WTextInput *wText )
  {
  int  x1,x2,y1,y2;
  char *ptr = wText->text.txt;
  
  /* x1,y1 is the upper left corner of the text box */
  x1 = wText->xOffset;
  y1 = wText->yOffset;
  /* x2,y2 is the lower right corner of the text box */
  x2 = wText->core->width - wText->xOffset;
  y2 = wText->core->height - wText->yOffset;
  
  /* Fill in the text field.  Use the invGC to draw the rectangle, 
   * becuase then it will be the background color */
  XFillRectangle( dpy, wText->core->window, wText->invGC,
                x1, y1, x2-x1, y2-y1 );

  /* Draw the text normally */
  XDrawString( dpy, wText->core->window, wText->regGC, x1, y2+TEXT_SHIFT,
             ptr, wText->text.length );
  
  /* Draw the selected text */
  if( wText->text.startPos != wText->text.endPos )
    {
    int sp,ep;
    /* we need sp < ep */
    if( wText->text.startPos > wText->text.endPos )
      {
      sp = wText->text.endPos;
      ep = wText->text.startPos;
      }
    else
      {
      sp = wText->text.startPos;
      ep = wText->text.endPos;
      }
    
    /* x1,y1 is now the upper-left of the selected area */
    x1 += MyTextWidth( wText->font->font, ptr, sp );
    /* and x2,y2 is the lower-right of the selected area */
    ptr += sp * sizeof(char);
    x2 = x1 +MyTextWidth( wText->font->font, ptr, (ep - sp) );
    /* Fill in the area  where the selected text will go:     *
     * use the regGC to draw the rectangle, becuase then it   *
     * will be the color of the non-selected text             */
    XFillRectangle( dpy, wText->core->window, wText->regGC,
                  x1, y1, x2-x1, y2-y1 );
    
    /* Draw the selected text... use invGC so it will be the
     * opposite color as the filled rectangle */
    XDrawString( dpy, wText->core->window, wText->invGC, x1, y2+TEXT_SHIFT,
               ptr, (ep - sp) );
    }
  
  /* And draw a quick little line for the cursor position */
  x1 = MyTextWidth( wText->font->font, wText->text.txt, wText->text.endPos )
       + wText->xOffset;
  XDrawLine( dpy, wText->core->window, wText->regGC, x1,y1+1,x1,y2-1 );
  }

/********************************************************************\
 * wTextGetText                                                     *
 *   return the string in the text field wText.  DO NOT FREE THE    *
 *   RETURNED STRING!                                               *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 * Return: the text in the text field (NULL terminated)             *
 * Global:                                                          *
\********************************************************************/
char *
wTextGetText( WTextInput *wText )
  {
  return wText->text.txt;
  }

/********************************************************************\
 * wTextPutText                                                     *
 *   Put the string txt in the text field wText.  The text field    *
 *   needs to be explicitly refreshed after wTextPutText by calling *
 *   wTextRefresh().                                                *
 *   The string txt is copied                                       *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 *         txt    - the new text string... freed by the text field! *
 * Return: none                                                     *
 * Global:                                                          *
\********************************************************************/
void
wTextPutText( WTextInput *wText, char *txt )
  {
  int length = strlen(txt);
  
  /* no memory leaks!  free the old txt */
  if( wText->text.txt != NULL )
    wfree( wText->text.txt );
  
  wText->text.txt = (char *)malloc(length*sizeof(char));
  strcpy( wText->text.txt, txt );
  wText->text.length   = length;
  /* By default No text is selected, and the cursor is at the end */
  wText->text.startPos = length;
  wText->text.endPos   = length;
  }

/********************************************************************\
 * wTextInsert                                                      *
 *   Insert some text at the cursor.  (if startPos != endPos,       *
 *   replace the selected text, otherwise insert)                   *
 *   The string txt is copied.                                      *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 *         txt    - the new text string... freed by the text field! *
 * Return: none                                                     *
 * Global:                                                          *
\********************************************************************/
void
wTextInsert( WTextInput *wText, char *txt )
  {
  char *newTxt;
  int  newLen, txtLen, i,j;
  int  sp,ep;
  
  /* we need sp < ep */
  if( wText->text.startPos > wText->text.endPos )
    {
    sp = wText->text.endPos;
    ep = wText->text.startPos;
    }
  else
    {
    sp = wText->text.startPos;
    ep = wText->text.endPos;
    }
  
  txtLen = strlen(txt);
  newLen = wText->text.length + txtLen - (ep - sp) + 1;
  
  newTxt = (char *)malloc(newLen*sizeof(char *));
  
  /* copy the old text up to sp */
  for( i=0; i<sp; i++ )
    newTxt[i] = wText->text.txt[i];
  
  /* insert new text */
  for( j=0; j<txtLen; j++,i++ )
    newTxt[i] = txt[j];
  
  /* copy old text after ep */
  for( j=ep; j<wText->text.length; j++,i++ )
    newTxt[i] = wText->text.txt[j];
  
  newTxt[i] = '\0';
  
  /* By default No text is selected, and the cursor is at the end
   * of inserted text */
  wText->text.startPos = sp+txtLen;
  wText->text.endPos   = sp+txtLen;
  
  free(wText->text.txt);
  wText->text.txt    = newTxt;
  wText->text.length = newLen-1;
  }

/********************************************************************\
 * wTextSelect                                                      *
 *   Select some text.  If start == end, then the cursor is moved   *
 *   to that position.  If end == -1, then the text from start to   *
 *   the end of the text entered in the text field is selected.     *
 *   The text field is not automatically re-drawn!  You must call   *
 *   wTextRefresh to re-draw the text field.                        *
 *                                                                  *
 * Args:   wText  - the text field                                  *
 *         start  - the beginning of the selected text              *
 *         end    - the end of the selected text                    *
 * Return: none                                                     *
 * Global:                                                          *
\********************************************************************/
void
wTextSelect( WTextInput *wText, int start, int end )
  {
  if( end == -1 )
    wText->text.endPos = wText->text.length;
  else
    wText->text.endPos = end;
  wText->text.startPos = start;
  }

/********************************************************************\
 * textEventHandler -- handles and dispatches all the events that   *
 *   the text field class supports                                  *
 *                                                                  *
 * Args:   desc - all we need to know about this object             *
 * Return: none                                                     *
 * Global:                                                          *
\********************************************************************/
static void
wTextEventHandler( WObjDescriptor *desc, XEvent *event )
  {
  WTextInput *wText  = desc->self;
  int   handled = FALSE;      /* has the event been handled */
  
  switch( event->type )
    {
    case MotionNotify:
      /* If the button isn't down, we don't care about the
       * event, but otherwise we want to adjust the selected
       * text so we can wTextRefresh() */
      if( wText->buttonDown )
      {
      DEBUG("MotionNotify");
      handled = TRUE;
      wText->text.endPos = wTextXtoPos( wText, event->xmotion.x );
      }
      break;
    case ButtonPress:
      DEBUG("ButtonPress");
      handled = TRUE;
      wText->text.startPos = wTextXtoPos( wText, event->xbutton.x );
      wText->text.endPos   = wText->text.startPos;
      wText->buttonDown = TRUE;
      break;
    case ButtonRelease:
      DEBUG("ButtonRelease");
      handled = TRUE;
      wText->text.endPos = wTextXtoPos( wText, event->xbutton.x );
      wText->buttonDown = FALSE;
      break;
    case KeyPress:
      DEBUG("KeyPress");
      handled = handleKeyPress( wText, 
                              XKeycodeToKeysym(dpy,event->xkey.keycode,0) );
      break;
    case KeyRelease:
      DEBUG("KeyRelease");
      handled =handleKeyRelease( wText, 
                               XKeycodeToKeysym(dpy,event->xkey.keycode,0) );
    case EnterNotify:
      DEBUG("EnterNotify");
      handled = TRUE;
      XDefineCursor( dpy, event->xcrossing.window, wCursor[WCUR_TEXT] );
      break;
    case LeaveNotify:
      DEBUG("LeaveNotify");
      handled = TRUE;
      XDefineCursor( dpy, event->xcrossing.window, wCursor[WCUR_NORMAL] );
      break;
    default:
      break;
    }
  
  if( handled )
    wTextRefresh(wText);
  else
    DispatchEvent(event);
  
  return;
  }

#endif
