/*
 * tscroll.c
 *
 * standard table class.
 *
 * scrolling and selection routines
 *
 * see table.h for interface description
 *
 * This implementation currently only supports TM_SINGLE, not TM_MANY
 * modes of selection.
 */

#include "windows.h"
#include "commdlg.h"
#include "gutils.h"

#include "table.h"
#include "tpriv.h"


VOID
gtab_extendsel(
    HWND hwnd,
    lpTable ptab,
    long startrow,
    long startcell,
    BOOL bNotify
);


/* handle a vscroll message */
void
gtab_msg_vscroll(HWND hwnd, lpTable ptab, int opcode, int pos)
{
    long change;

    switch(opcode) {
    case SB_THUMBPOSITION:
    case SB_THUMBTRACK:
        change = (pos * ptab->scrollscale) - ptab->toprow;
        break;

    case SB_LINEUP:
        change = -1;
        break;

    case SB_LINEDOWN:
        change = 1;
        break;

    case SB_PAGEUP:
        change = - (ptab->nlines - 3);
        if (change>=0)
            change = -1;    // consider nlines <=3!
        break;

    case SB_PAGEDOWN:
        change = (ptab->nlines - 3);
        if (change<=0)
            change = 1;     // consider nlines <=3!
        break;

    default:
        return;
    }
    gtab_dovscroll(hwnd, ptab, change);
}

/* handle a hscroll message */
void
gtab_msg_hscroll(HWND hwnd, lpTable ptab, int opcode, int pos)
{
    int change;

    switch(opcode) {
    case SB_THUMBPOSITION:
    case SB_THUMBTRACK:
        change = pos - ptab->scroll_dx;
        break;

    case SB_LINEUP:
        change = -(ptab->avewidth);
        break;

    case SB_LINEDOWN:
        change = ptab->avewidth;
        break;

    case SB_PAGEUP:
        change = - (ptab->winwidth * 2 / 3);
        break;

    case SB_PAGEDOWN:
        change = (ptab->winwidth * 2 / 3);
        break;

    default:
        return;
    }
    gtab_dohscroll(hwnd, ptab, change);
}



/*
 * set new vertical scroll pos,
 * adjust linedata array
 * set line win-relative start posns & clip top/bottom posns
 * revise display.
 */
void
gtab_dovscroll(HWND hwnd, lpTable ptab, long change)
{
    int cury, i;
    long ncopy;
    lpCellPos cp;
    LineData ldtemp;
    RECT rc, rcpaint;
    long range;
    long newtop;
    int newpos;
    BOOL fWasVisible = FALSE;

    if (ptab->selvisible)
    {
        fWasVisible = TRUE;
        ptab->selvisible = FALSE;
        gtab_invertsel(hwnd, ptab, NULL);
    }

    range = ptab->hdr.nrows - (ptab->nlines - 1);
    newtop = ptab->toprow + change;
    if (range < 0) {
        range = 0;
    }
    if (newtop > range) {
        change = range - ptab->toprow;
    } else if (newtop < 0) {
        change = -(ptab->toprow);
    }
    ptab->toprow += change;

    newpos = (int) (newtop / ptab->scrollscale);
    SetScrollPos(hwnd, SB_VERT, newpos, TRUE);

    if (ptab->hdr.sendscroll) {
        gtab_sendtq(hwnd, TQ_SCROLL, ptab->toprow);
    }

    /* adjust data ptrs rather than invalidate, to retain the
     * data we know is still valid
     */
    if (abs(change) >= ptab->nlines) {
        gtab_invallines(hwnd, ptab, ptab->hdr.fixedrows,
            ptab->nlines - ptab->hdr.fixedrows);
        InvalidateRect(hwnd, NULL, FALSE);
        change = 0;
    } else if (change < 0) {
        /* copy data down */
        ncopy = (ptab->nlines - ptab->hdr.fixedrows) - abs(change);
        for (i =  ptab->nlines - 1;
                i >= (ptab->hdr.fixedrows + abs(change)); i--) {
            ldtemp = ptab->pdata[i - abs(change)];
            ptab->pdata[i - abs(change)] = ptab->pdata[i];
            ptab->pdata[i] = ldtemp;
        }
        gtab_invallines(hwnd, ptab,
                ptab->hdr.fixedrows, (int) abs(change));
    } else if (change > 0) {
        ncopy = (ptab->nlines - ptab->hdr.fixedrows) - change;
        for (i = ptab->hdr.fixedrows;
                i < (ncopy + ptab->hdr.fixedrows); i++) {
            ldtemp = ptab->pdata[i + change];
            ptab->pdata[i + change] = ptab->pdata[i];
            ptab->pdata[i] = ldtemp;
        }
        gtab_invallines(hwnd, ptab,
            (int) ncopy + ptab->hdr.fixedrows, (int) change);
    }

    /* scroll window */
    GetClientRect(hwnd, &rc);
    rcpaint = rc;
    if (change > 0) {
        rc.top += (int) (change + ptab->hdr.fixedrows) * ptab->rowheight;
        rcpaint.top = (ptab->hdr.fixedrows * ptab->rowheight);
        rcpaint.top += rc.bottom - rc.top;
    } else if (change < 0) {
        rc.top += (ptab->hdr.fixedrows * ptab->rowheight);
        rc.bottom += (int) (change * ptab->rowheight);
        rcpaint.bottom -= rc.bottom - rc.top;
    }

    /* loop through each line setting relative posn and clipping */

    /* set up all rows  - the fixed/moveable difference for
     * rows is made at fetch-time during painting, when we remember
     * which absolute row nr to ask for, for a given screen line
     */
    cury = 0;
    for (i = 0; i < ptab->nlines; i++) {
        cp = &ptab->pdata[i].linepos;
        cp->start = cury;
        cp->clipstart = cury;
        cp->clipend = cury + cp->size;
        cury += cp->size;
    }

    /* now move and repaint the window */
    if (change != 0) {
        if (rc.top < rc.bottom) {
            ScrollWindow(hwnd, 0, (int) -(change * ptab->rowheight),
                &rc, NULL);
        }

        // don't repaint the fixed rows
        rc.top = 0;
        rc.bottom = ptab->hdr.fixedrows * ptab->rowheight;
        ValidateRect(hwnd, &rc);

        /* force repaint now, not just post message for later,
         * since we want to repaint that line before the next
         * scroll down occurs
         */
        ValidateRect(hwnd, &rcpaint);
        RedrawWindow(hwnd, &rcpaint, NULL,
                RDW_NOERASE | RDW_INVALIDATE | RDW_INTERNALPAINT);

    }

    if (fWasVisible)
    {
        gtab_invertsel(hwnd, ptab, NULL);
        ptab->selvisible = TRUE;
    }
}

/*
 * set new horizontal scroll pos,
 * set col win-relative start posns & clip left/right posns
 * revise display.
 */
void
gtab_dohscroll(HWND hwnd, lpTable ptab, long change)
{
    int curx, i;
    int moveable;
    lpCellPos cp;
    int newdx, range;


    /* check that the new scroll pos is still within the valid range */
    range = ptab->rowwidth - ptab->winwidth;
    newdx = ptab->scroll_dx + (int) change;
    if (range < 0) {
        range = 0;
    }
    if (newdx > range) {
        change = range - ptab->scroll_dx;
    } else if (newdx < 0) {
        change = -(ptab->scroll_dx);
    }
    ptab->scroll_dx += (int) change;

    SetScrollPos(hwnd, SB_HORZ, ptab->scroll_dx, TRUE);
    if (ptab->hdr.fixedcols > 0) {
        RECT rc;
        GetClientRect(hwnd, &rc);
        rc.left = ptab->pcellpos[ptab->hdr.fixedcols - 1].clipend;
        InvalidateRect(hwnd, &rc, FALSE);
    } else {
        InvalidateRect(hwnd, NULL, FALSE);
    }

    /* loop through each col setting relative posn and clipping */
    /* clip off 1 pixel left and right (we added 2 on to size for this) */

    /* first set up fixed columns */
    curx = 0;
    for (i = 0; i < ptab->hdr.fixedcols; i++) {
        cp = &ptab->pcellpos[i];
        cp->start = curx + 1;
        cp->clipstart = cp->start;
        cp->clipend = cp->start + cp->size - 2;
        curx += cp->size;
    }

    /* now moveable columns. remember start of moveable cols */
    moveable = curx;
    curx = - ptab->scroll_dx;       /* rel. pos of col */
    for (i = ptab->hdr.fixedcols; i < ptab->hdr.ncols; i++) {
        cp = &ptab->pcellpos[i];
        cp->start = curx + moveable + 1;
        cp->clipstart = max(moveable+1, cp->start);
        cp->clipend = cp->start + cp->size - 2;
        curx += cp->size;
    }
}

/*
 * convert screen line nr to table row nr
 */
long
gtab_linetorow(HWND hwnd, lpTable ptab, int line)
{
    if (line < ptab->hdr.fixedrows) {
        return(line);
    }

    return (line + ptab->toprow);
}

/*
 * convert table row nr to screen line nr or -1 if not on screen
 */
int
gtab_rowtoline(HWND hwnd, lpTable ptab, long row)
{
    if (row < ptab->hdr.fixedrows) {
        return( (int) row);
    }

    row -= ptab->toprow;
    if ((row >= ptab->hdr.fixedrows) && (row < ptab->nlines)) {
        return ( (int) row);
    }
    return(-1);
}


/*
 * check if a given location is within the current selection.
 * Returns true if it is inside the current selection, or false if
 * either there is no selection, or the row, cell passed is outside it.
 */
BOOL
gtab_insideselection(
    lpTable ptab,
    long row,
    long cell)
{
    long startrow, endrow;
    long startcell, endcell;

    if (0 == ptab->select.nrows) {
        // no selection
        return FALSE;
    }

    // selection maintains anchor point as startrow,
    // so the selection can extend forwards or backwards from there.
    // need to convert to forward only for comparison
    startrow = ptab->select.startrow;
    if (ptab->select.nrows < 0) {
        endrow = startrow;
        startrow += ptab->select.nrows + 1;
    } else {
        endrow = startrow + ptab->select.nrows - 1;
    }
    if ((row < startrow) || (row > endrow)) {
        return FALSE;
    }

    // if we are in row-select mode, then that's it - its inside
    if (ptab->hdr.selectmode & TM_ROW) {
        return TRUE;
    }

    // same calculation for cells
    startcell = ptab->select.startcell;
    if (ptab->select.ncells < 0) {
        endcell = startcell;
        startcell += ptab->select.ncells + 1;
    } else {
        endcell = startcell + ptab->select.ncells - 1;
    }
    if ((cell < startcell) || (cell > endcell)) {
        return FALSE;
    }

    return TRUE;
}



/*
 * replace old selection with new. Notify owner if bNotify. Change
 * display to reflect new display.
 */
void
gtab_select(
        HWND hwnd,
        lpTable ptab,
        long row,
        long col,
        long nrows,
        long ncells,
        BOOL bNotify)
{

    /* if in ROW mode, force col and ncells to reflect the entire row. */
    if (ptab->hdr.selectmode & TM_ROW) {
        col = 0;
        ncells = ptab->hdr.ncols;
    }

    /* clear existing sel if valid and visible */
    if ((ptab->select.nrows != 0) && (ptab->selvisible == TRUE)) {

        /* only clear sel if it is different from the new one */
        if ((ptab->select.startrow != row) ||
                (ptab->select.startcell != col) ||
                (ptab->select.nrows != nrows) ||
                (ptab->select.ncells != ncells)) {

            gtab_invertsel(hwnd, ptab, NULL);
            ptab->selvisible = FALSE;
        }
    }

    /* set select fields and send TQ_SELECT */
    if (row < ptab->hdr.nrows) {
        ptab->select.startrow = row;
        ptab->select.startcell = col;
        ptab->select.nrows = nrows;
        ptab->select.ncells = ncells;
    } else {
        ptab->select.nrows = 0;
        ptab->select.startrow = 0;
        ptab->select.startcell = 0;
        ptab->select.ncells = 0;
    }

    if (bNotify) {
        gtab_sendtq(hwnd, TQ_SELECT, (LPARAM) &ptab->select);
    }

    /* paint in selection */
    if (nrows != 0) {
        if (!ptab->selvisible) {
            gtab_invertsel(hwnd, ptab, NULL);
            ptab->selvisible = TRUE;
        }
    } else {
        if (ptab->selvisible) {
            gtab_invertsel(hwnd, ptab, NULL);
            ptab->selvisible = FALSE;
        }
        ptab->selvisible = FALSE;
    }
}

/*
 * convert window y co-ord to a line nr
 */
int
gtab_ytoline(HWND hwnd, lpTable ptab, int y)
{
    return(y / ptab->rowheight);
}

/*
 * convert window x co-ord to a cell nr
 */
int
gtab_xtocol(HWND hwnd, lpTable ptab, int x)
{
    int i;
    lpCellPos ppos;

    for (i = 0; i < ptab->hdr.ncols; i++) {
        ppos = &ptab->pcellpos[i];
        if (ppos->clipstart < ppos->clipend) {
            if ( (x >= ppos->clipstart) && (x < ppos->clipend)) {
                return(i);
            }
        }
    }
    return(-1);
}


/*
 * check if x co-ord is 'near' (+- 2 pixels) the right border of given cell
 */
BOOL
gtab_isborder(HWND hwnd, lpTable ptab, long x, long col)
{

    if (abs(ptab->pcellpos[col].clipend - x) < 2) {
        return(TRUE);
    } else {
        return(FALSE);
    }
}


/*
 * set selection and send 'TQ_ENTER' event to owner
 */
void
gtab_enter(HWND hwnd, lpTable ptab, long row, long col, long nrows,
        long ncells)
{
    /* clear existing sel if valid and visible */
    if ((ptab->select.nrows != 0) && (ptab->selvisible == TRUE)) {

        /* only clear sel if it is different from the new one */
        if ((ptab->select.startrow != row) ||
                (ptab->select.startcell != col) ||
                (ptab->select.nrows != nrows) ||
                (ptab->select.ncells != ncells)) {
            gtab_invertsel(hwnd, ptab, NULL);
            ptab->selvisible = FALSE;
        }
    }

    /* set select fields and send TQ_ENTER */
    if (row < ptab->hdr.nrows) {
        ptab->select.startrow = row;
        ptab->select.startcell = col;
        ptab->select.nrows = nrows;
        ptab->select.ncells = ncells;
    } else {
        ptab->select.nrows = 0;
        ptab->select.startrow = 0;
        ptab->select.startcell = 0;
        ptab->select.ncells = 0;
    }

    /* paint in selection */
    if (nrows != 0) {
        if (!ptab->selvisible) {
            gtab_invertsel(hwnd, ptab, NULL);
            ptab->selvisible = TRUE;
        }
        /* do this at end because it could cause a layout-change */
        gtab_sendtq(hwnd, TQ_ENTER, (LPARAM) &ptab->select);
    } else {
        if (ptab->selvisible) {
            gtab_invertsel(hwnd, ptab, NULL);
        }
        ptab->selvisible = FALSE;
    }
}


/*
 * start re-sizing a column
 */
void
gtab_trackcol(HWND hwnd, lpTable ptab, long col, long x)
{

    /* ensure we see the mouse-up */
    SetCapture(hwnd);
    ptab->trackmode = TRACK_COLUMN;
#ifdef WIN32
    ptab->tracknr = col;
    ptab->trackline1 = x;
#else
    // maximum 32767 columns is a reasonable limit!
    ptab->tracknr = (int) (col & 0x7fff);
    ptab->trackline1 = (int) (x & 0x7fff);
#endif

    /* if line at other side of cell is visible, draw that too */
    if (ptab->pcellpos[col].start >= ptab->pcellpos[col].clipstart) {
        ptab->trackline2 = ptab->pcellpos[col].start;
    } else {
        ptab->trackline2 = -1;
    }
    gtab_drawvertline(hwnd, ptab);
}


/*
 * called on right-click events. Select the cell clicked on, and if
 * valid, send on to owner for any context-menu type operation
 */
void
gtab_rightclick(HWND hwnd, lpTable ptab, int x, int y)
{
    long cell, ncells;
    long row;
    HWND hOwner;

    /* find which col, row he selected */
    cell = gtab_xtocol(hwnd, ptab, x);
    if (cell == -1) {
        return;
    }
    row = gtab_linetorow(hwnd, ptab, gtab_ytoline(hwnd, ptab, y));

    /* is he selecting a disabled fixed area ? */
    if ( (row < ptab->hdr.fixedrows) || (cell < ptab->hdr.fixedcols)) {
        if (ptab->hdr.fixedselectable == FALSE) {
            return;
        }
    }

    // ignore if beyond data
    if ((row >= ptab->hdr.nrows) ||
            (cell >= ptab->hdr.ncols)) {
        return;
    }

    /* is this within the already-selected area? */
    if (!gtab_insideselection(ptab, row, cell)) {
        // no selection, or clicked outside the selection - make new selection
        // before sending the right-click

        // if shift is down, extend selection
        if (GetKeyState(VK_SHIFT) & 0x8000) {
            gtab_extendsel(hwnd, ptab, row, cell, TRUE);
        } else {
            /* record and paint new selection */

            if (ptab->hdr.selectmode & TM_ROW) {
                cell = 0;
                ncells = ptab->hdr.ncols;
            } else {
                ncells = 1;
            }
            gtab_select(hwnd, ptab, row, cell, 1, ncells, TRUE);
        }
    }

    // now we have sent the selection, pass the message onto him
    hOwner = (HANDLE) GetWindowLongPtr(hwnd, WW_OWNER);
    SendMessage(hOwner, WM_RBUTTONDOWN, 0, MAKELONG( (short)x, (short)y));
}


/*
 * called on mouse-down events. decide what to start tracking.
 */
void
gtab_press(HWND hwnd, lpTable ptab, int x, int y)
{
    long cell, ncells;
    long row;

    if (ptab->trackmode != TRACK_NONE) {
        return;
    }

    /* has he grabbed a cell-edge to resize ? */
    cell = gtab_xtocol(hwnd, ptab, x);
    if (cell == -1) {
        return;
    }
    if (gtab_isborder(hwnd, ptab, x, cell)) {
        gtab_trackcol(hwnd, ptab, cell, x);
        return;
    }
    if ( (cell > 0) && gtab_isborder(hwnd, ptab, x, cell-1)) {
        gtab_trackcol(hwnd, ptab, cell, x);
        return;
    }

    /* find which line he selected */
    row = gtab_linetorow(hwnd, ptab, gtab_ytoline(hwnd, ptab, y));

    /* is he selecting a disabled fixed area ? */
    if ( (row < ptab->hdr.fixedrows) || (cell < ptab->hdr.fixedcols)) {
        if (ptab->hdr.fixedselectable == FALSE) {
            return;
        }
    }

    // ignore if beyond data
    if ((row >= ptab->hdr.nrows) ||
            (cell >= ptab->hdr.ncols)) {
        return;
    }


    /* ok, start cell selection */
    ptab->trackmode = TRACK_CELL;
    SetCapture(hwnd);

    /* record and paint new selection */

    if (ptab->hdr.selectmode & TM_ROW) {
        cell = 0;
        ncells = ptab->hdr.ncols;
    } else {
        ncells = 1;
    }

    /*
     * if the shift key is down, then extend the selection to this
     * new anchor point, rather than create a new selection
     */
    if (GetKeyState(VK_SHIFT) & 0x8000) {
        gtab_extendsel(hwnd, ptab, row, cell, FALSE);
    } else {
        gtab_select(hwnd, ptab, row, cell, 1, ncells, FALSE);
    }
    return;
}

/*
 * called on mouse-up. complete any tracking that was happening
 */
void
gtab_release(HWND hwnd, lpTable ptab, int x, int y)
{
    lpCellPos ppos;
    lpProps pprop;
    long row, cell;
    int cx;

    switch(ptab->trackmode) {

    case TRACK_NONE:
        return;

    case TRACK_COLUMN:
        /* erase marker lines */
        gtab_drawvertline(hwnd, ptab);
        ReleaseCapture();
        ptab->trackmode = TRACK_NONE;

        /* adjust cell width */
        ppos = &ptab->pcellpos[ptab->tracknr];
        cx = ptab->trackline1 - ppos->start;
        pprop = &ptab->pcolhdr[ptab->tracknr].props;
        pprop->valid |= P_WIDTH;
        pprop->width = cx;
        gtab_calcwidths(hwnd, ptab);
        gtab_setsize(hwnd, ptab);
        InvalidateRect(hwnd, NULL, FALSE);
        return;

    case TRACK_CELL:
        row = gtab_linetorow(hwnd, ptab, gtab_ytoline(hwnd, ptab, y));
        cell = gtab_xtocol(hwnd, ptab, x);

        ReleaseCapture();
        ptab->trackmode = TRACK_NONE;

        // ignore if before or beyond data
        if ( (row < ptab->hdr.fixedrows) ||
             (cell < ptab->hdr.fixedcols)) {
            if (ptab->hdr.fixedselectable == FALSE) {
                gtab_select(
                    hwnd,
                    ptab,
                    ptab->select.startrow,
                    ptab->select.startcell,
                    ptab->select.nrows,
                    ptab->select.ncells,
                    TRUE);

                return;
            }
        }

        if ((row >= ptab->hdr.nrows) ||
                (cell >= ptab->hdr.ncols)) {
            gtab_select(
                    hwnd,
                    ptab,
                    ptab->select.startrow,
                    ptab->select.startcell,
                    ptab->select.nrows,
                    ptab->select.ncells,
                    TRUE);
            return;
        }

        /*
         * Extend to this new selection end point
         * we used to only do this if shift key pressed, but that
         * is not a good UI.
         */
        gtab_extendsel(hwnd, ptab, row, cell, TRUE);
        return;
    }
}


/* called on mouse-move. if tracking - adjust position, if not,
 * set correct cursor
 */
void
gtab_move(HWND hwnd, lpTable ptab, int x, int y)
{
    BOOL fOK;
    int line;
    long row;
    int col;
    lpCellPos ppos;

    switch(ptab->trackmode) {

    case TRACK_NONE:
        col = gtab_xtocol(hwnd, ptab, x);
        if (col == -1) {
            SetCursor(hNormCurs);
            return;
        }
        if (gtab_isborder(hwnd, ptab, x, col)) {
            SetCursor(hVertCurs);
            return;
        }
        if ( (col > 0) && gtab_isborder(hwnd, ptab, x, col-1)) {
            SetCursor(hVertCurs);
            return;
        }
        SetCursor(hNormCurs);
        return;

    case TRACK_CELL:
        line = gtab_ytoline(hwnd, ptab, y);

        // we used to only allow drag to extend
        // the selection if the shift key was down.
        // this doesn't seem to work as a UI - you expect
        // to drag and extend.

        /* if extending selection then
         * allow scrolling by dragging off window
         */
        if (line < 0) {
            gtab_dovscroll(hwnd, ptab, -1);
            line = gtab_ytoline(hwnd, ptab, y);
        } else if (line >=  ptab->nlines) {
            gtab_dovscroll(hwnd, ptab, 1);
            line = gtab_ytoline(hwnd, ptab, y);
        }


        row = gtab_linetorow(hwnd, ptab, line);
        col = gtab_xtocol(hwnd, ptab, x);

        // ignore if before or beyond data
        if ( (row < ptab->hdr.fixedrows) || (col < ptab->hdr.fixedcols)) {
            if (ptab->hdr.fixedselectable == FALSE) {
                return;
            }
        }

        if ((row >= ptab->hdr.nrows) ||
            (col >= ptab->hdr.ncols)) {
            return;
        }

        /*
         * extend to this new selection end point
         */
        gtab_extendsel(hwnd, ptab, row, col, FALSE);
        return;

    case TRACK_COLUMN:
        /* check that new x is still visible/valid */
        ppos = &ptab->pcellpos[ptab->tracknr];
        fOK = FALSE;

        if (ptab->tracknr < ptab->hdr.fixedcols)  {
            if ((x > ppos->start) && (x < ptab->winwidth)) {
                fOK = TRUE;
            }
        } else {
            if ((x > ppos->clipstart) && (x < ptab->winwidth)) {
                fOK = TRUE;
            }
        }
        if (fOK == TRUE) {
            gtab_drawvertline(hwnd, ptab);
            ptab->trackline1 = x;
            gtab_drawvertline(hwnd, ptab);
        }
        return;
    }
}

/* dbl-click - send an TQ_ENTER event to the owner (if valid) */
void
gtab_dblclick(HWND hwnd, lpTable ptab, int x, int y)
{
    int cell, line;
    long row;

    line = gtab_ytoline(hwnd, ptab, y);
    cell = gtab_xtocol(hwnd, ptab, x);
    if ( (line < ptab->hdr.fixedrows) || (cell < ptab->hdr.fixedcols) ) {
        if (!ptab->hdr.fixedselectable) {
            return;
        }
    }
    row = gtab_linetorow(hwnd, ptab, line);

    if (ptab->hdr.selectmode & TM_ROW) {
        gtab_enter(hwnd, ptab, row, 0, 1, ptab->hdr.ncols);
    } else {
        gtab_enter(hwnd, ptab, row, cell, 1, 1);
    }
}

/*
 * move selection area to visible part of window. argument bToBottom
 * indicates whether to move the line onto the bottom or the top of the
 * window if not visible - this affects the smoothness of scrolling
 * line-by-line.
 */
void
gtab_showsel(HWND hwnd, lpTable ptab, BOOL bToBottom)
{
    int line;
    long change;

    line = gtab_rowtoline(hwnd, ptab, ptab->select.startrow);

    /* move up if last line or not at all visible */
    if ( (line < 0) || line == (ptab->nlines - 1)) {
        change = ptab->select.startrow - ptab->toprow;
        if (bToBottom) {
            /* change to bottom of window. subtract 2 not 1
             * since nlines includes one line that is only
             * partly visible
             */
            change -= (ptab->nlines - 2);
        }
        change -= ptab->hdr.fixedrows;
        gtab_dovscroll(hwnd, ptab, change);
    }
    /* add support for TM_CELL here! */
}

/*
 * scroll the window so that if possible, the selected row is in the
 * middle 60% of the screen so that context around it is visible.
 */
void
gtab_showsel_middle(HWND hwnd, lpTable ptab, long dyRowsFromTop)
{
    int line = ptab->select.startrow;
    long change = 0;
    int mid_top, mid_end;
    BOOL fScroll = FALSE;

    if (dyRowsFromTop >= 0)
    {
        fScroll = TRUE;
        change = (ptab->select.startrow - dyRowsFromTop) - ptab->toprow;
        change -= ptab->hdr.fixedrows;
    }

    /* is this within the middle 60 % ?  */
    mid_top = ptab->toprow + (ptab->nlines * 20 / 100);
    mid_end = ptab->toprow + (ptab->nlines * 80 / 100);
    if ((line < mid_top + change) || (line > mid_end + change))
    {
        /* no - scroll so that selected line is at
         * the 20% mark
         */
        fScroll = TRUE;
        change = (ptab->select.startrow - mid_top);
        change -= ptab->hdr.fixedrows;
    }

    if (fScroll)
    {
        gtab_dovscroll(hwnd, ptab, change);
    }

    /* again - need code here for TM_CELL mode to ensure that
     * active cell is horizontally scrolled correctly
     */
}


/*
 * extend the selection to set the new anchor point as startrow, startcell.
 *
 * nrows and ncells will then be set to include the end row of the previous
 * selection. nrows, ncells < 0 indicate left and up. -1 and +1 both indicate
 * just one cell or row selected.
 */
VOID
gtab_extendsel(
    HWND hwnd,
    lpTable ptab,
    long startrow,
    long startcell,
    BOOL bNotify
)
{
    long endrow, endcell, nrows, ncells;

    /*
     * if no current selection, then just select the new anchor point
     */
    if (ptab->select.nrows == 0) {
        gtab_select(hwnd, ptab, startrow, startcell, 1,
            (ptab->hdr.selectmode & TM_ROW) ? ptab->hdr.ncols:1,
            bNotify);
        return;
    }

    if (startrow >= ptab->hdr.nrows) {
        startrow = ptab->hdr.nrows -1;
    } else if (startrow < 0) {
        startrow = 0;
    }
    if (startcell >= ptab->hdr.ncols) {
        startcell = ptab->hdr.ncols-1;
    } else if (startcell < 0) {
        startcell = 0;
    }



    /* calculate the row just beyond the selection
     * this is one above for upwards sels, and one below for
     * downard-extending sels. Then adjust down or up one
     * to be the actual (inclusive) last row.
     */
    endrow = ptab->select.startrow + ptab->select.nrows;
    if (ptab->select.nrows < 0) {
        endrow++;
    } else {
        endrow--;
    }

    if (endrow >= ptab->hdr.nrows) {
        endrow = ptab->hdr.nrows-1;
    }
    nrows = endrow - startrow;

    if (nrows >= 0) {
        // convert from exclusive to inclusive
        nrows++;
    } else {
        // convert from exclusive to inclusive
        nrows--;
    }

    /* same calculation for cells */
    endcell = ptab->select.startcell + ptab->select.ncells;
    if (ptab->select.ncells < 0) {
        endcell++;
    } else {
        endcell--;
    }
    ncells = endcell - startcell;
    if (ncells >= 0) {
        ncells++;
    } else {
        ncells--;
    }
    gtab_select(hwnd, ptab, startrow, startcell, nrows, ncells, bNotify);
}



/* move the selection a specified nr of rows or cells
 * if no selection, select first visible unit
 *
 * if bExtend is true and there is a current selection, then extend it rather than
 * replace it. Note that (startrow, startcell) will always be set to the newly
 * selected position - this is the anchor point. nrows or ncells may be negative
 * if the selection extends upwards above the anchor. nrows == -1 is the same
 * as nrows == 1, meaning only the current row is visible. Similarly
 * (in TM_CELL mode), ncells may be negative.
 *
 * Move the selection (ie anchor point) to make it visible. bToBottom
 * indicates whether it should be moved to the bottom or the top
 * of the window.
 */
VOID
gtab_changesel(
    HWND hwnd,
    lpTable ptab,
    long rowincr,
    int cellincr,
    BOOL bToBottom,
    BOOL bExtend
)
{
    long row, col, ncols;

    /* is there a selection ? */
    if (ptab->select.nrows == 0) {

        /* no selection - force a selection
         * at the first visible unit
         */
        if (ptab->hdr.fixedselectable) {
            row = 0;
            col = 0;
        } else {
            row = gtab_linetorow(hwnd, ptab, ptab->hdr.fixedrows);
            /* should really check for first visible cell */
            col = ptab->hdr.fixedcols;
        }
        ncols = 1;
        if (ptab->hdr.selectmode & TM_ROW) {
            col = 0;
            ncols = ptab->hdr.ncols;
        }
        gtab_select(hwnd, ptab, row, col, 1, ncols, TRUE);

    } else {
        /* move the anchor point by rowincr, cellincr */
        row = ptab->select.startrow + rowincr;
        col = ptab->select.startcell + cellincr;


        /*
         * ensure that new anchor point is in a valid position
         */

        while (col >= ptab->hdr.ncols) {
            col -= ptab->hdr.ncols;
            row++;
        }
        while (col < 0) {
            col += ptab->hdr.ncols;
            row--;
        }
        if (row < 0) {
            row = 0;
        }
        if (row >= ptab->hdr.nrows) {
            row = ptab->hdr.nrows-1;
        }
        /* check we haven't moved into non-selectable region */
        if ((row < ptab->hdr.fixedrows) &&
            (!ptab->hdr.fixedselectable)) {
                    row = ptab->hdr.fixedrows;
        }

        if (bExtend) {
            gtab_extendsel(hwnd, ptab, row, col, TRUE);
        } else {
            gtab_select(
                hwnd,
                ptab,
                row,
                col,
                1,
                (ptab->hdr.selectmode & TM_ROW) ? ptab->hdr.ncols : 1,
                TRUE);
        }
    }

    /* ensure selection visible */
    gtab_showsel(hwnd, ptab, bToBottom);
}

/*
 * set the topmost selectable unit in window as the selection
 *
 * if bExtend is TRUE, then extend the selection to include this, rather
 * than replacing the existing selection. Note that (startrow, startcell)
 * is always the anchor point - ie most recently selected end, and the
 * (nrows, ncells) can be + or - to extend the selection downwards or upwards.
 */
void
gtab_selhome(HWND hwnd, lpTable ptab, BOOL bExtend)
{
    long startrow, startcell, ncells;

    if (ptab->hdr.selectmode & TM_ROW) {
        ncells = ptab->hdr.ncols;
    } else {
        ncells = 1;
    }
    startcell = 0;


    if (ptab->hdr.fixedselectable) {
        startrow = gtab_linetorow(hwnd, ptab, 0);
    } else {
        startrow = gtab_linetorow(hwnd, ptab, ptab->hdr.fixedrows);
        if (!(ptab->hdr.selectmode & TM_ROW)) {
            startcell = ptab->hdr.fixedcols;
        }
    }

    if (bExtend) {
        gtab_extendsel(hwnd, ptab, startrow, startcell, TRUE);
    } else {
        gtab_select(hwnd, ptab, startrow, startcell, 1, ncells, TRUE);
    }
}


/* handle key-down events - scroll windows and/or move selection */
int
gtab_key(HWND hwnd, lpTable ptab, int vkey)
{
    long startrow, ncells, startcell;
    BOOL bControl = FALSE;
    BOOL bShift = FALSE;

    if (GetKeyState(VK_CONTROL) & 0x8000) {
        bControl = TRUE;
    }
    if (GetKeyState(VK_SHIFT) & 0x8000) {
        /* ignore shift key here if TM_MANY -multiple selection flag- is
         * not selected
         */
        if (ptab->hdr.selectmode & TM_MANY) {
            bShift = TRUE;
        }
    }

    switch(vkey) {

    case VK_UP:
        if (bControl) {
            /* control-uparrow scrolls window without selection.
             * the selection is de-selected (to avoid surprises
             * moving back to it).
             */
            gtab_select(hwnd, ptab, 0, 0, 0, 0, TRUE);
            gtab_dovscroll(hwnd, ptab, -1);
        } else {
            /* uparrow moves selection up one line */
            gtab_changesel(hwnd, ptab, -1, 0, FALSE, bShift);
        }
        return(0);

    case VK_DOWN:
        if (bControl) {
            /* control downarrow scrolls window without
             * a selection.
             */
            gtab_select(hwnd, ptab, 0, 0, 0, 0, TRUE);
            gtab_dovscroll(hwnd, ptab, 1);
        } else {
            /* the normal gtab_changesel behaviour is
             * that if the selected line is not visible now,
             * we scroll it to the top of the window. This is fine
             * in most cases but causes unacceptable jumps when
             * repeatedly scrolling down with the down key.
             *
             * Thus we now have an argument to changesel to say
             * that in this case, if you need to move the line onto
             * the window, move it to the bottom and not the top
             */
            gtab_changesel(hwnd, ptab, 1, 0, TRUE, bShift);
        }
        return(0);

    case VK_LEFT:
        /* if cell-selection mode, move left one cell.
         * otherwise the whole row is selected - scroll
         * the line left a little
         */

        if (ptab->hdr.selectmode & TM_ROW) {
            if (bControl) {
                /* ctrl-left moves to start of line */
                gtab_dohscroll(hwnd, ptab, -(ptab->scroll_dx));
            } else {
                gtab_dohscroll(hwnd, ptab, -(ptab->avewidth));
            }
        } else {
            gtab_changesel(hwnd, ptab, 0, -1, FALSE, bShift);
        }
        return(0);

    case VK_RIGHT:
        /* if cell-selection mode, move right one cell.
         * otherwise the whole row is selected - scroll
         * the line right a little
         */
        if (ptab->hdr.selectmode & TM_ROW) {
            if (bControl) {
                /* control-right moves to right end of line */
                gtab_dohscroll(hwnd, ptab, ptab->rowwidth -
                                ptab->winwidth);
            } else {
                gtab_dohscroll(hwnd, ptab, ptab->avewidth);
            }
        } else {
            gtab_changesel(hwnd, ptab, 0, 1, TRUE, bShift);
        }
        return(0);

    case VK_HOME:
        if (bControl) {
            /* control-home == top of file */
            gtab_dovscroll(hwnd, ptab, -(ptab->toprow));
        }
        /* top of window */
        gtab_selhome(hwnd, ptab, bShift);
        gtab_showsel(hwnd, ptab, FALSE);

        return(0);

    case VK_END:
        if (bControl) {
            /* control-end -> end of file */
            startrow = ptab->hdr.nrows-1;
        } else {
            startrow = gtab_linetorow(hwnd, ptab, ptab->nlines - 1);
            if (startrow >= ptab->hdr.nrows) {
                startrow = ptab->hdr.nrows-1;
            }
        }

        startcell = 0;
        ncells = ptab->hdr.ncols;
        if (!(ptab->hdr.selectmode & TM_ROW)) {
            startcell = ptab->hdr.ncols-1;
            ncells = 1;
        }

        if (bShift) {
            gtab_extendsel(hwnd, ptab, startrow, startcell, TRUE);
        } else {
            gtab_select(hwnd, ptab, startrow, startcell, 1, ncells, TRUE);
        }

        /* we have selected the bottom line. We don't want to
         * move it up into the window, since the intended
         * effect is to select the lowest line. This doesn't
         * apply to the ctrl-end behaviour (move to bottom of
         * buffer.
         */
        if (bControl) {
            /* move the selection to make it visible - but move it
             * to the bottom and not to the top of the window
             */
            gtab_showsel(hwnd, ptab, TRUE);
        }
        return(0);

    case VK_RETURN:
        if (ptab->select.nrows != 0) {
            gtab_showsel(hwnd, ptab, FALSE);
            gtab_enter(hwnd, ptab, ptab->select.startrow,
                    ptab->select.startcell,
                    ptab->select.nrows, ptab->select.ncells);
        }
        return(0);

    case VK_SPACE:
        /* toggle the selection */
        if (ptab->select.nrows == 0) {
                /* no selection - make one */
                gtab_changesel(hwnd, ptab, 0, 0, TRUE, FALSE);
        } else {
                /* there is a selection - deselect it */
                gtab_select(hwnd, ptab, 0, 0, 0, 0, TRUE);
        }
        return(0);

    case VK_PRIOR:          /* page up */

        if (ptab->nlines > 3) {
            gtab_dovscroll(hwnd, ptab, -(ptab->nlines - 3));
        }
        gtab_selhome(hwnd, ptab, bShift);
        return(0);

    case VK_NEXT:           /* page down */

        /* scroll down one page */
        if (ptab->nlines > 3) {
            gtab_dovscroll(hwnd, ptab, (ptab->nlines - 3));
        }

        /* select new bottom line */
        startrow = gtab_linetorow(hwnd, ptab, ptab->nlines - 1);
        if (startrow >= ptab->hdr.nrows) {
            startrow = ptab->hdr.nrows-1;
        }
        startcell = 0;
        ncells = ptab->hdr.ncols;
        if (!(ptab->hdr.selectmode & TM_ROW)) {
            startcell = ptab->hdr.ncols-1;
            ncells = 1;
        }

        /* select bottom line, but don't call showsel
         * since we don't want to adjust it's position - we
         * want it to remain at the bottom of the window
         */
        if (bShift) {
            gtab_extendsel(hwnd, ptab, startrow, startcell, TRUE);
        } else {
            gtab_select(hwnd, ptab, startrow, startcell, 1, ncells, TRUE);
        }
        return(0);

    default:
        return(1);
    }
}

int gtab_mousewheel(HWND hwnd, lpTable ptab, DWORD fwKeys, int zDelta)
{
    static ULONG uScrollLines = 0;

    if (fwKeys & MK_MBUTTON) {
        return 1;
    }

    if (uScrollLines == 0) {
        SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &uScrollLines, FALSE);
        if (uScrollLines == 0) {
            uScrollLines = 3;
        }
    }

    zDelta /= -WHEEL_DELTA;

    if (fwKeys & MK_CONTROL) {
        //
        // Left-Right scroll
        //
        if (ptab->hdr.selectmode & TM_ROW) {
            if (fwKeys & MK_SHIFT) {
                zDelta = (zDelta > 0) ? ptab->rowwidth : -ptab->rowwidth;
            }
            gtab_dohscroll(hwnd, ptab, ptab->avewidth * zDelta);
            return 0;
        }
        return 1;
    }

    if (fwKeys & MK_SHIFT) {
        //
        // Page scroll
        //
        if (ptab->nlines > 3) {
            zDelta *= ptab->nlines - 3;
        }
    }
    else {
        if (uScrollLines) {
            zDelta *= uScrollLines;
            zDelta = min(zDelta, ptab->nlines - 3);
        }
    }

    gtab_dovscroll(hwnd, ptab, zDelta);

    return 0;
}