/********************************************************************
 * 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"

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "WindowMaker.h"
#include "funcs.h"
#include "text.h"
#include "actions.h"

/* X11R5 don't have this */
#ifndef IsPrivateKeypadKey
#define IsPrivateKeypadKey(keysym) \
    (((KeySym)(keysym) >= 0x11000000) && ((KeySym)(keysym) <= 0x1100FFFF))
#endif


#ifdef DEBUG
#  define  ENTER(X)  fprintf(stderr,"Entering:  %s()\n", X);
#  define  LEAVE(X)  fprintf(stderr,"Leaving:   %s()\n", X);
#  define  PDEBUG(X)  fprintf(stderr,"debug:     %s()\n", X);
#else
#  define  ENTER(X)
#  define  LEAVE(X)
#  define  PDEBUG(X)
#endif

extern Cursor wCursor[WCUR_LAST];

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

static void handleExpose( WObjDescriptor *desc, XEvent *event );

static void textInsert( WTextInput *wtext, char *txt );
#if 0
static void blink(void *data);
#endif

/********************************************************************
 * 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, XKeyEvent *event )
{
    KeySym ksym;
    char buffer[32];
    int count;

    count = XLookupString(event, buffer, 32, &ksym, NULL);

    /* 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;
    }
    else
    {
        switch(ksym)
        {
            /* Ignore these keys: */
        case XK_Escape:
            wtext->canceled = True;
        case XK_Return:
            wtext->done = True;
            break;
        case XK_Tab:
        case XK_Num_Lock:
            break;

        case XK_Delete:
            /* delete after cursor */
            if( (wtext->text.endPos == wtext->text.startPos) &&
               (wtext->text.endPos < wtext->text.length) )
                wtext->text.endPos++;
            textInsert( wtext, "" );
            break;
        case XK_BackSpace:
            /* delete before cursor */
            if(  (wtext->text.endPos == wtext->text.startPos) &&
               (wtext->text.startPos > 0) )
                wtext->text.startPos--;
            textInsert( wtext, "" );
            break;
        default:
            if (count==1 && !iscntrl(buffer[0])) {
                buffer[count] = 0;
                textInsert( wtext, buffer);
            }
        }
    }
    return True;
}



/********************************************************************
 * textXYtoPos                                                      *
 *   given X coord, return position in array                        *
 ********************************************************************/
static int
textXtoPos( WTextInput *wtext, int x )
{
    int pos;
    x -= wtext->xOffset;

    for( pos=0; wtext->text.txt[pos] != '\0'; pos++ )
    {
        if( x < 0 )
            break;
        else
            x -= WMWidthOfString( wtext->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");

    wtext = wmalloc(sizeof(WTextInput));
    wtext->core = wCoreCreate( core, x, y, width, height );
    wtext->core->descriptor.handle_anything = &textEventHandler;
    wtext->core->descriptor.handle_expose = &handleExpose;
    wtext->core->descriptor.parent_type = WCLASS_TEXT_INPUT;
    wtext->core->descriptor.parent = wtext;

    wtext->font = core->screen_ptr->menu_entry_font;

    XDefineCursor( dpy, wtext->core->window, wCursor[WCUR_TEXT] );

    /* 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;

        gcv.foreground = core->screen_ptr->black_pixel;
        gcv.background = core->screen_ptr->white_pixel;
        gcv.line_width = 1;
        gcv.function   = GXcopy;

        wtext->gc = XCreateGC( dpy, wtext->core->window,
                              (GCForeground|GCBackground|
                               GCFunction|GCLineWidth),
                              &gcv );

        /* set up the regular context */
        gcv.foreground = core->screen_ptr->black_pixel;
        gcv.background = core->screen_ptr->white_pixel;
        gcv.line_width = 1;
        gcv.function   = GXcopy;

        wtext->regGC = XCreateGC( dpy, wtext->core->window,
                                 (GCForeground|GCBackground|
                                  GCFunction|GCLineWidth),
                                 &gcv );

        /* set up the inverted context */
        gcv.function   = GXcopyInverted;

        wtext->invGC = XCreateGC( dpy, wtext->core->window,
                                 (GCForeground|GCBackground|
                                  GCFunction|GCLineWidth),
                                 &gcv );

        /* and set the background! */
        XSetWindowBackground( dpy, wtext->core->window, gcv.background );
    }

    /* Figure out the y-offset... */
    wtext->yOffset = (height - WMFontHeight(wtext->font))/2;
    wtext->xOffset = wtext->yOffset;

    wtext->canceled     = False;
    wtext->done         = False;     /* becomes True when the user    *
    * hits "Return" key             */

    XMapRaised(dpy, wtext->core->window);

    LEAVE("wTextCreate");

    return wtext;
}

/********************************************************************
 * wTextDestroy                                                     *
 *                                                                  *
 * Args:   wtext  - the text field                                  *
 * Return:                                                          *
 * Global: dpy    - the display                                     *
 ********************************************************************/
void
wTextDestroy( WTextInput *wtext )
{
    ENTER("wTextDestroy")

#if 0
        if (wtext->magic)
            wDeleteTimerHandler(wtext->magic);
    wtext->magic = NULL;
#endif
    XFreeGC( dpy, wtext->gc );
    XFreeGC( dpy, wtext->regGC );
    XFreeGC( dpy, wtext->invGC );
    wfree( wtext->text.txt );
    wCoreDestroy( wtext->core );
    wfree( wtext );

    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 */


/********************************************************************
 * textRefresh                                                     *
 *   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                                     *
 ********************************************************************/
static void
textRefresh(WTextInput *wtext)
{
    WScreen *scr = wtext->core->screen_ptr;
    char *ptr = wtext->text.txt;
    int  x1,x2,y1,y2;

    /* 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 */
    WMDrawImageString(scr->wmscreen, wtext->core->window,
                      scr->black, scr->white, wtext->font, x1, y1, 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 += WMWidthOfString(wtext->font, ptr, sp);
        /* and x2,y2 is the lower-right of the selected area */
        ptr += sp * sizeof(char);
        x2 = x1 + WMWidthOfString(wtext->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. Inverse bg and fg colors for selection */
        WMDrawImageString(scr->wmscreen, wtext->core->window,
                          scr->white, scr->black, wtext->font, x1, y1, ptr,
                          (ep - sp));
    }

    /* And draw a quick little line for the cursor position */
    x1 = WMWidthOfString(wtext->font, wtext->text.txt, wtext->text.endPos)
        + wtext->xOffset;
    XDrawLine(dpy, wtext->core->window, wtext->regGC, x1, 2, x1,
              wtext->core->height - 3);
}


/********************************************************************
 * wTextPaint                                                       *
 *                                                                  *
 * Args:   wtext  - the text field                                  *
 * Return:                                                          *
 * Global: dpy    - the display                                     *
 ********************************************************************/
void
wTextPaint( WTextInput *wtext )
{
    ENTER("wTextPaint");

    /* refresh */
    textRefresh( wtext );

    /* Draw box */
    XDrawRectangle(dpy, wtext->core->window, wtext->gc, 0, 0,
                   wtext->core->width-1, wtext->core->height-1);

    LEAVE("wTextPaint");
}


/********************************************************************
 * 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 )
{
    if (!wtext->canceled)
        return wtext->text.txt;
    else
        return NULL;
}

/********************************************************************
 * 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 *)wmalloc((length+1)*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;
}

/********************************************************************
 * textInsert                                                      *
 *   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:                                                          *
 ********************************************************************/
static void
textInsert( 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;

    wfree(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;
}

#if 0
static void
blink(void *data)
{
    int x;
    WTextInput *wtext = (WTextInput*)data;
    GC gc;

    /* And draw a quick little line for the cursor position */
    if (wtext->blink_on) {
        gc = wtext->regGC;
        wtext->blink_on = 0;
    } else {
        gc = wtext->invGC;
        wtext->blink_on = 1;
    }
    x = WMWidthOfString( wtext->font, wtext->text.txt, wtext->text.endPos )
        + wtext->xOffset;
    XDrawLine( dpy, wtext->core->window, gc, x, 2, x, wtext->core->height-3);

    if (wtext->blinking)
        wtext->magic = wAddTimerHandler(CURSOR_BLINK_RATE, blink, data);
}
#endif

/********************************************************************
 * 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
textEventHandler( WObjDescriptor *desc, XEvent *event )
{
    WTextInput *wtext  = desc->parent;
    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( event->xmotion.state & (Button1Mask|Button3Mask|Button2Mask) )
        {
            PDEBUG("MotionNotify");
            handled = True;
            wtext->text.endPos = textXtoPos( wtext, event->xmotion.x );
        }
        break;

    case ButtonPress:
        PDEBUG("ButtonPress");
        handled = True;
        wtext->text.startPos = textXtoPos( wtext, event->xbutton.x );
        wtext->text.endPos   = wtext->text.startPos;
        break;

    case ButtonRelease:
        PDEBUG("ButtonRelease");
        handled = True;
        wtext->text.endPos = textXtoPos( wtext, event->xbutton.x );
        break;

    case KeyPress:
        PDEBUG("KeyPress");
        handled = handleKeyPress( wtext, &event->xkey );
        break;

    case EnterNotify:
        PDEBUG("EnterNotify");
        handled = True;
#if 0
        if (!wtext->magic)
        {
            wtext->magic = wAddTimerHandler(CURSOR_BLINK_RATE, blink, wtext);
            wtext->blink_on = !wtext->blink_on;
            blink(wtext);
            wtext->blinking = 1;
        }
#endif
        break;

    case LeaveNotify:
        PDEBUG("LeaveNotify");
        handled = True;
#if 0
        wtext->blinking = 0;
        if (wtext->blink_on)
            blink(wtext);
        if (wtext->magic)
            wDeleteTimerHandler(wtext->magic);
        wtext->magic = NULL;
#endif
        break;

    default:
        break;
    }

    if( handled )
        textRefresh(wtext);
    else
        WMHandleEvent(event);

    return;
}


static void
handleExpose(WObjDescriptor *desc, XEvent *event)
{
    wTextPaint(desc->parent);
}



syntax highlighted by Code2HTML, v. 0.9.1