Title - Data Entry with CA-Clipper 5.2:  User Interface Issues and
Possibilities

Head 1 - Contents

  Introduction

  Custom readers, Cargo, and User-defined Commands
   - Custom readers
   - Cargo
   - User-defined Commands

  Data Entry Options
   - Picklist
   - Achoice() or TBrowse.
   - Automatic Drop-in Picklist

  Special GETs
   - Checkboxes
   - Radio button Groups
   - Password
     - Password validation
   - Increment/Decrement
   - Calculator
   - Multi-line text

  Context (field) sensitive Help
  
  GET "jumping"
  
  Multiple GET pages
  
  Using A Mouse

  More Runtime Speed?


Head 1 - Introduction

In this session we'll present a number of special data entry possibilities
in CA-Clipper 5.2 and discuss several issues that arise from their
implementation.  No "teaching" per se of Achoice() or Tbrowse or GET
objects, will be included, although related special programming solutions
will be offered and discussed.

Head 1 - Custom readers, Cargo, and UDCs (user-defined commands)

All of the solutions presented in this session are implemented 1)
internally, via a GET object's 'reader' and 'cargo' instance variables; 2)
externally via UDCs in a separate CA-Clipper header (.CH) file.  In due
course, we'll come to understand how the alliance of these two approaches
affords the programmer a particularly powerful set of tools, all available
in pure CA-Clipper, without the addition of any 3rd party libraries.

Head 2 - Custom readers

The GET system's standard reader, GetReader(), is designed to handle
traditional text-oriented data entry, the kind we all know and may or may
or not love from our days with dBASE and/or Summer '87.  The special GET
behaviors demonstrated in this session, however, require a bit more in the
way of imagination, and therefore rely heavily on the use of several
"custom" readers.  Gratefully, the GET system has been written with an eye
toward taking special requirements into account.

If we assign a GET object our own 'reader' (a CA-Clipper codeblock pointing
to a routine of our own composition), CA-Clipper will use our reader and
forego usage of GetReader().  Our custom readers all begin with a copy of
GetReader(), which is then modified to accommodate our purposes.  For
reference during the rest of the session, here's a copy of GetReader():

 PROCEDURE GetReader( oGet )

   // Read the GET if the WHEN condition is satisfied
   IF ( GetPreValidate( oGet ) )

      // Activate the GET for reading
      oGet:setFocus()

      WHILE ( oGet:exitState == GE_NOEXIT )

         // Check for initial typeout (no editable positions)
         IF ( oGet:typeOut )
            oGet:exitState := GE_ENTER
         ENDIF

         // Apply keystrokes until exit
         WHILE ( oGet:exitState == GE_NOEXIT )
            GetApplyKey( oGet, inkey( 0 ) )
         ENDDO

         // Disallow exit if the VALID condition is not satisfied
         IF ( !GetPostValidate( oGet ) )
            oGet:exitState := GE_NOEXIT
         ENDIF

      ENDDO

      // De-activate the GET
      oGet:killFocus()

   ENDIF

   RETURN

Assigning a custom reader is no more involved than assigning a simple
variable.  Here's how we assign a custom reader to handle our checkbox GETs
in the CHECKBOX UDC:

Atail( getlist ):reader:= {|e| CheckBoxReader( e )}

The following also assign a custom reader:

// you have a GET object reference in variable form.
oGet:reader:= {|e| CheckBoxReader(e)}

// you have a numeric offset into the getlist.
// this assigns the same custom reader to each GET.
FOR nX:= 1 TO Len( getlist )
   getlist[nX]:reader:= {|e| CustomReader( e )}
NEXT

In describing our special GETs, only code that strays from that contained
in GetReader() will be presented.  In summary, then, substitution of a
custom reader enables a widely expanded range of programming possibilities.

Head 2 - Cargo

Throughout the session, we'll rely heavily on the GET object's 'cargo'
instance variable to hold a number of values crucial to making each GET
object work together harmoniously in one getlist.  Think of these as
"pseudo-instance variables".  :cargo is initialized to an array of 10
elements, each of which is referenced through a mnemonically significant
preprocessor directive.  For example, element 3 of :cargo is a reference
to the getlist of which a (any) GET object is a member.  In the source
code, however, you will not see:

oGet:cargo[3]

when we want to reference the getlist.  Instead, using a PP directive, we
use:

oGet:GETLIST

where the directive 'GETLIST' has been defined this way:

#define GETLIST  cargo[3]

These are the 10 elements, followed by the assignment as it appears in each
UDC:

        Directive Element   Type/Explanation
        --------- -------   ----------------
#define TITLEDATA cargo[1]  A: array of row,col,text,color for SAY.
#define ORDPOS    cargo[2]  N: ordinal position of GET in getlist.
#define GETLIST   cargo[3]  A: getlist of which the GET is a member.
#define RADIODATA cargo[4]  A: array of data for RADIO GROUP
#define PCKLIST   cargo[5]  A: array of options for AUTO PICKLIST GET.
#define PASSCHAR  cargo[6]  C: ASCII char. to display for PASSWORD GET.
#define INCREVAL  cargo[7]  N: numeric by which to inc/dec INCREMENT GET.
#define JUMP2POS  cargo[8]  N: target position of a getlist jump.
#define GETDATA   cargo[9]  A: array holding GET identification data.
#define TWRAPGET  cargo[10] A: array of data enabling multi-line GET.

#define TITLEROW   cargo[1][1] N: screen row for title.
#define TITLECOL   cargo[1][2] N: screen column for title.
#define TITLETEXT  cargo[1][3] C: GET title (SAY).
#define TITLECOLOR cargo[1][4] C: screen color for title.

#define ISRADIO    cargo[4][1] L: is GET a RADIO GROUP GET?
#define RADIOID    cargo[4][2] N: unique integer identifying the RADIO GROUP.
#define RBUTTON    cargo[4][3] C: ASCII char. to be used for radio button.

#define GET_ID     cargo[9][1] C: string ID for GET, "INCREMENT", e.g.
#define GET_ALIAS  cargo[9][2] C: Alias() referenced by GET.

// assign cargo.
ATail(GetList):cargo:= { IIF(<.say.>,{<row>,<col>,<text>,<sclr>},{}) ,;
                         Len(getlist),getlist,{.F.,0,""}, ,"","",0, ;
                         {"INCREMENT",Alias()} }

A more, or purer, object-oriented approach would transform each of these
values into true instance variables, but one of our intentions here is to
demonstrate that all we need to implement a very powerful, attractive, non-
kludgy data entry user interface is native CA-Clipper.

Head 2 - User-defined Commands

The third key element of our implementation is the use of several user-
defined commands (UDCs) of almost exactly the same type as the standard '@
<row>, <col> GET' command.  Using these reduces to a no-brainer the task of
adding our special GETs to your code.  Writing a password GET, for example,
becomes no more difficult than:

@ 5,5 SAY "PassWord" GET cPassWord PASSWORD CHARACTER "*"

The 'SAY' portion of each command, or what I refer to as a TITLE, is
optional and is executed separately from the initialization of the GET.  The
<row>,<col>,<text>,<color> for the TITLE is stored in each GET's TITLEDATA
(cargo[1]).  In our Titles() routine, we look for a non-empty array, and if
found, we use SetPos() and DispOut() against the elements in TITLEDATA.  We
include an optional second parameter to indicate whether the GET itself
should also be displayed.

 FUNCTION Titles( getlist, lGetdisp )
 LOCAL nX,cColor,oGet,nLen:= Len(getlist)
 DEFAULT lGetdisp TO .F.
 DispBegin()
 FOR nX:= 1 TO nLen
    oGet:= getlist[nX]
    IF !Empty( oGet:TITLEDATA )
       cColor:= IIF( oGet:TITLECOLOR==NIL,SetColor(),oGet:TITLECOLOR )
       SetPos( oGet:TITLEROW, oGet:TITLECOL )
       DispOut( oGet:TITLETEXT, cColor )
    ENDIF
    IF lGetDisp
       // special for our multi-line GET.
       IF oGet:GET_ID=="TEXTWRAP"
          oGet:SAREA:= SaveScreen( oGet:row, oGet:col, maxrow(), maxcol() )
          _display( oGet )
       ELSE ; oGet:display()
       ENDIF
    ENDIF
    // reset each exitstate.
    oGet:exitstate:= GE_NOEXIT
 NEXT
 DispEnd()
 RETURN NIL

Head 1 - Data Entry Options

Head 2 - Picklist

With regard to data entry, the picklist alternative is always an appealing
option because it does away with the need for multiple keystrokes while
adding an element of "fun" to data entry.  It should be used whenever entry
of a particular datum lends itself to this approach, that is, whenever
entry of the datum is limited to a specific, but numerous set of choices.

Head 3  - AChoice() or Tbrowse

If not for the color-restrictive nature of AChoice(), there would be little
reason not to use AChoice() (with the user function) most of the time. 
With AChoice(), you are restricted to one color for unselected items and
one color for the current item.  To underscore the availability of
selecting an item by pressing its first letter, programmers may want to
paint the first character of an item or all items in a different color/s.

Aside from this restriction, AChoice() with the user function can meet most
programmer demands.  But let's not forget AChoice()'s other main
limitation: it's a canned, closed routine, limiting you to what *it* can
do.  On the other hand, the TBrowse option is a much more open environment.
With Tbrowse, each keystroke or mouse click is easily configurable, and
picklist items can be painted in any fashion whatsoever.

One example of using TBrowse as an AChoice() replacement can be found in
FT_ACH2T.PRG on the conference source code disk.  With the addition of the
Nanforum Toolkit (obtainable free for the download from Compuserve's
Clipper forum, library 5), it also allows for using a mouse.

Head 3 - Automatic drop-in picklist (AUTO PICKLIST)

We've all seen and written the picklist that pops up when the user presses
K_ENTER.  But if a GET's valid data entry is limited to a specific set of
choices which are already known, why should the user be told to press
K_ENTER first?  S/he should be presented with the list as soon as the GET
receives focus.

This feature is easily implemented in either a 'preBlock' or in a custom
reader, but since it can be convincingly argued that a 'preBlock' should be
used only as a test for "admission" to a particular GET, we implement the
picklist in a custom reader.  Here's the main loop therein:

      WHILE ( oGet:exitState == GE_NOEXIT )

          GetPickList( oGet )

          // Disallow exit if the VALID condition is not satisfied
          IF ( !GetPostValidate( oGet ) )
             oGet:exitState := GE_NOEXIT
          ENDIF

      ENDDO

First, let's note what's been eliminated (from the code in GetReader()) for
lack of relevance to the task at hand.  There's no text entry in a picklist
GET :buffer, therefore no need to test for :typeOut and no need to call
GetApplyKey().  Instead, keystrokes are handled separately by the
A_CHOICE() routine (shown below) which also creates and displays a TBrowse
of the options contained in 'aOptions' below.

Here's the essential core of the GetPicklist() routine, which should appear
familiar to users of AChoice():

IF (nChoice := A_CHOICE( nTop,nLeft,, ;
                         ARRAY:aOptions         ;
                         BOXCOLOR:"+w/b"        ;
                         USELCOLOR:"+gr/b"      ;
                         HOTKEYCOLOR:"+w/b"     ;
                         SHADOW:"FT"            ;
                         MES_COLOR:"+w/"+cBkgnd     )) > 0
   oGet:varPut( Padr( aOptions[nChoice][1], nLongest )  )
   oGet:upDatebuffer()
   oGet:exitstate:= IIF( nKey==K_UP,GE_UP,GE_ENTER )
ELSE
   oGet:exitstate:= GE_ESCAPE
ENDIF

The array to browse, 'aOptions', is RETURNed by 'oGet:PCKLIST' which calls
PL( <nOrdPos> ).  <nOrdPos> represents the ordinal position of the GET.

 FUNCTION PL( nPos )
 LOCAL aRet
 DO CASE
 CASE nPos==2 .or. nPos==9 .or. nPos==6
   aRet:= { {"WASHINGTON","Great White Father"},  ;
            {"ADAMS","First Veep"},               ;
            {"JEFFERSON","Sage of Monticello"},   ;
            {"MADISON","Little Jimmy"},           ;
            {"MONROE","Era of Good Feeling",2},   ;
            {"ADAMS, J.Q.","Son o' First Veep",2},;
            {"JACKSON","Old Hickory",3},          ;
            {"VAN BUREN","Little Magician"}       ;
          }
 OTHER
 ENDCASE
 RETURN aRet

The structure of the array (nested arrays) is specific to our A_CHOICE()
routine.  To use CA-Clipper's AChoice(), you'd, of course, RETURN an array
of character strings.  If you require an additional reference to the array
in your source, it's represented by 'oGet:PCKLIST'.  In fact, in our
GetPickList() routine, you'll see:

aOptions:= oGet:PCKLIST

Our demo uses three getlists with the AUTO PICKLIST in three respectively
different getlist positions.  That is why PL() checks for three 'nPos'
values.  More typically, each CASE would check for only one position and
return a different array for each position.  In any event, PL()'s purpose
is to return an array compatible with whatever picklist routine you decide
to use.

Our element selection logic here in no way differs from that used with
Achoice().  We check for a return value greater than 0, and if found, we
use the GET object's :varPut() method to assign the selected element to the
underlying variable.

Remember that the controlling loop in our custom reader examines the
current GET's 'exitstate', so by re-assigning it in GetPicklist(), we tell
the GET system to move us either to another GET or to leave the READ
completely (GE_WRITE/GE_ESCAPE).

Head 2 - Special GETs

Head 3 - Checkboxes (CHECKBOX)

Checkbox GETs are conceptually quite simple, but their design may not be
immediately obvious.  One keypress (and perhaps an alternate), usually
K_SPACE, toggles the dis/appearance of a checkmark (CHR(251)) in the
current GET's :buffer.  The key to implementing this behavior is proper
composition of the GET's 'block'.

Checkboxes can be implemented with either logical or character variables as
the GET's underlying data source.  The latter is less complicated because
only one data type (character) is involved, while the former requires a
character display based on a logical value.  Since most checkboxes will be
used in conjunction with logical values we'll spend time on these only.

The standard GET 'block' takes the following GET/SET form (where 'xVar' is
the underlying variable):

{|e| IIF( Pcount()==0,xVar,xVar:= e )}

Now, it should be obvious that this is inadequate for purposes of
implementing a checkbox on an underlying logical value.  What we need is a
block that, in the event of a 0 PCount(), will examine the underlying
variable and display a checkmark if .T., and a space (CHR(32)) if .F.  If
we are operating in SET mode (PCount() > 0), we need to see if our
codeblock parameter is a checkmark.  If it is, we can assign .T. to the
underlying variable, otherwise, it becomes .F.  This block handles the
situation concisely:

{|e| IIF(Pcount()==0, IIF(xVar,CHR(251)," "),xVar:= (e==CHR(251))  ) }

Here's the controllling loop of our custom reader:

      WHILE ( oGet:exitState == GE_NOEXIT )

          // act only if the keystroke is a space.
          IF (nKey:= SKInkey( oGet, 0 )) == K_SPACE
             oGet:varput( IIF(oGet:varget==CHR(251)," ",CHR(251)) )
             oGet:upDateBuffer()
             oGet:exitstate:= GE_ENTER
          ELSE
             ApplyKey( oGet, nKey )
          ENDIF

      ENDDO

SKInkey() is a replacement Inkey(0) routine which enables use of a mouse
and also turns Inkey(0) into a "true" CA-Clipper wait state.  More about
SKInkey() in the section, "Using A Mouse", below.

ApplyKey() is a keystroke filtering routine which disallows character entry
(ASCII 32-255) because it's not pertinent to (most of) our special GETs and
also avoids any SET KEYs because we handle those in SKInkey().
Incidentally, you'll see the same basic logic in most of our custom
readers.  That is, if the keystroke is one pertinent to the current GET, we
handle it in the reader, otherwise, we pass it on to ApplyKey() for default
processing:

For reference during the session, here's ApplyKey():

 FUNCTION ApplyKey( oGet, nKey )
 IF !(nKey >= K_SPACE .and. nKey <= 255) .and. ;
    (valtype( SetKey(nKey) ) <> "B")
    GetApplyKey( oGet, nKey )
 ENDIF
 RETURN NIL

Head 3 - Radio Button groups (RADIO GROUP)

The concept underlying a radio button group is much the same as that
underlying an automatic picklist.  A selection of one GET precludes the
selection of all others in the group or list.  The interface in "hot" mode
is a character approximating the appearance of a radio button while the
"cold" display would be either any other visible character or an empty
space.  In our demo, we use CHR(9) as "hot", and an empty space for "cold".

We use the same 'block' structure as that for a checkbox, substituting only
CHR(9) for the checkmark.

{|e| IIF(Pcount()==0, IIF(xVar,CHR(9)," "),xVar:= (e==CHR(9))  ) }

We scan the getlist for RADIO BUTTON GETs that belong to same group as that
of the current GET.  IF found, any and all are set to "cold", and our
current GET becomes "hot".

Our reader's controlling loop:

      WHILE ( oGet:exitState == GE_NOEXIT )

          IF (nKey:= Inkey(0)) == K_SPACE
             // if we have a radio button GET.
             IF oGet:ISRADIO
                // set other group members to empty space.
                Aeval( oGet:GETLIST, {|e| IIF(e:ISRADIO .and. ;
                                          e:RADIOID==oGet:RADIOID,;
                                          RGroup(e), ) } )
                // depress radio button for current GET.
                oGet:varput( oGet:RBUTTON )
                oGet:upDateBuffer()
                oGet:exitstate:= GE_ENTER
             ENDIF
          ELSE
             ApplyKey( oGet, nKey )
          ENDIF

      ENDDO

 FUNCTION RGroup( oGet )
   oGet:varput(" ")
   oGet:display()
 RETURN NIL

Head 3 - Passwords (PASSWORD)

Password GETs can be implemented in a number of ways, but the central
concept remains the same.  The user's keystrokes are not echoed to the
screen but instead, are captured in a temporary variable for later relay to
the underlying variable.  In place of the typed characters, a string of
password characters is sent to the screen.  Our reader:

 PROCEDURE PassWordReader( oGet )
 LOCAL nKey:= 0,cBuffer:= "",nLenBuffer

   // Read the GET if the WHEN condition is satisfied
   IF ( GetPreValidate( oGet ) )

      nLenBuffer:= Len( oGet:varget )
      oGet:varput( Space( nLenBuffer ) )

      // Activate the GET for reading
      oGet:setFocus()

      WHILE ( oGet:exitState == GE_NOEXIT )

         // Apply keystrokes until exit
         WHILE ( oGet:exitState == GE_NOEXIT )
            cBuffer:= PWord( oGet, Inkey(0), cBuffer, nLenBuffer )
         ENDDO

         // validation here.
         IF oGet:exitstate <> GE_ESCAPE
            IF !Eval( oGet:postBlock, oGet, cBuffer )
               oGet:exitstate:= GE_NOEXIT
               cBuffer:= ""
               oGet:varput( Space( nLenBuffer ) )
            ENDIF
         ELSE; oGet:undo()
         ENDIF

      ENDDO

      // De-activate the GET
      oGet:killFocus()

      // varput stored password.
      IF oGet:exitstate <> GE_ESCAPE
         oGet:varPut( Trim(cBuffer) )
      ENDIF

   ENDIF

 RETURN

We first need to transform the GET's :buffer prior to the send of
:setFocus() because our intent is for the user to always begin with an
empty :buffer.  Then, following completion of the user's entry and the send
of :killFocus(), finally, we :varput() the temporary variable holding the
user's password to the underlying variable.  :killFocus() displays :varget
in the GET's unselected color, therefore we take pains to :varput() *after*
:killFocus().

Our reader is interested in handling a range of ASCII characters (32-126)
and just 4 other keys.  K_DEL will wipeout the :buffer, K_BS will trim the
rightmost password character, and we've trapped K_LEFT and K_RIGHT to do
nothing.  This handling takes place in PWord():

 FUNCTION PWord( oGet, nKey, cBuffer, nLenBuffer )
 LOCAL nCol,cPw:= oGet:PASSCHAR

 DO CASE
 CASE nKey==K_DEL
   cBuffer:= ""
   oGet:varput( Space( nLenBuffer ) )
 CASE nKey==K_BS
   cBuffer:= Left( cBuffer,Len(cBuffer)-1  )
   oGet:varPut( Padr( Replicate( cPw, Len(cBuffer) ), nLenBuffer ) )
 CASE nKey >= K_SPACE .and. nKey <= 255
    cBuffer+= CHR( nKey )
    oGet:varPut( Padr( Replicate( cPw, Len(cBuffer) ), nLenBuffer ) )
 OTHER
    GetApplyKey( oGet, nKey )
 ENDCASE

 oGet:updateBuffer()

 IF Len(cBuffer) < nLenBuffer
    nCol:= oGet:col+Len(cBuffer)
    SetPos( oGet:row, nCol )
 ENDIF

 RETURN cBuffer


Head 3 - Password validation

We can't use GetPostValidate() for validating our password because it will
not see the actual characters entered, only the string of password
characters.  Instead, functionally equivalent validation code has been
substituted in the reader.

Head 3 - Increment/decrement (INCREMENT)

It's something of a neat trick to allow the user to use K_PLUS or K_MINUS
to add or subtract from a date or numeric GET.  This comes in particularly
handy in the case of dates when the date to be entered just happens to be a
few days from a default date.

Essentially, all that's required is to check for K_PLUS or K_MINUS, adding
:INCREVAL to :varget for the former, subtracting :INCREVAL for the latter.

IF (nKey:= Inkey(0)) == K_PLUS .or. nKey==K_MINUS
   nVal:= IIF( nKey==K_PLUS,oGet:INCREVAL,-oGet:INCREVAL )
   oGet:varPut( oGet:varget+nVal )
   oGet:updateBuffer()
ENDIF

:INCREVAL, to remind you, is our :cargo element holding the numeric value
by which to increment or decrement the variable.  The programmer sets it in
the INCREMENT UDC.

Head 3 - Calculator (CALCULATOR)

Calculator-style entry for numerics introduces a different way of working
with the GET object.  Instead of using :varput() to update the underlying
variable, we directly assign :buffer and update the underlying variable
with the :assign() method.  Reason being that :buffer, a character string
reflection of :varget() (a numeric), is immediately ready for string
concatenation which is what calculator style entry demands.  We use a
temporary variable ('cBuffer' below) because we'll potentially make several
changes while the GET :hasFocus.  Our reader first assures that the cursor
is always positioned to the last character in :buffer with:

nLenBuf:= Len(oGet:buffer)
SetPos( oGet:row, oGet:col+nLenbuf-1 )

It then calls Calc():

 FUNCTION Calc( oGet, nKey, nDecPos, nLenBuf )
 LOCAL cBuffer

 // 'nDecPos' is the decimal point position passed from the reader.

 IF nKey==K_DEL
    cBuffer:= "0"+IIF(nDecPos>0,"."+Replicate( "0",nLenBuf-nDecPos ),"")
 ELSE
    IF nKey==ASC(".")
       cBuffer:= oGet:buffer+ IIF( "." $oGet:buffer,"",CHR(nKey) )
    ELSE
       cBuffer:= SUBSTR( oGet:buffer+ CHR(nKey), 2 )
    ENDIF
    // if we've just added a ".", add a couple of decimal places
    IF Right(cBuffer,1)=="."
       cBuffer+= "00"
       cBuffer:= SUBSTR( cBuffer, 4 )
       nDecPos:= AT( ".", cBuffer )
    ENDIF
     // if we have a decimal point, remove, and reposition.
    IF nDecPos > 0
       cBuffer:= Stuff( Strtran( cBuffer, "." ), nDecPos, 0, "." )
    ENDIF
 ENDIF

 oGet:buffer:= PadL( cBuffer, nLenbuf )
 oGet:assign()
 oGet:upDatebuffer()

 RETURN NIL

We check for K_DEL, a keystroke between ASC("0") and ASC("9"), or ASC(".").
In the case of 0-9, we simply concatenate.  If we have ".", we concatenate
only if "." is not already contained in :buffer.  If K_DEL, we set :buffer
to "0.00".  The decision to use 2 decimal places here is strictly
arbitrary.

Because we just concatenated :buffer, it now is longer than the length of
the variable originally supplied by the programmer, and therefore, we
SUBSTR() the front end by the number of keystrokes added.  Also, because
we've concatenated, we've misplaced any decimal point present, requiring us
to remove and replace it.  Our 'cBuffer', or rather a version padded left
to the original length of the GET's :buffer, is now ready for assignment to
the underlying variable.

Head 4 - Multi-line text (TEXTWRAPGET)

Since the advent of CA-Clipper 5.0, some programmers have wished that it
still featured the automatic wrapping of text at maxcol() seen in Summer
87.  Toward that end, I set out to write a multi-line text GET to emulate
this behavior, optionally allowing selection of the column at which you
want wrapping to occur.

It turned out to be a much more involved project than first imagined, as I
soon discovered that keystroke handling had to be almost completely
redefined.  Something as trivial as the press of K_RIGHT, e.g., had to be
redefined to allow for the possibility of the cursor currently positioned
at the right-most column of the GET's :buffer.  In effect, you end up
writing a mini text editor.

I wrote both pure CA-Clipper and Classy 2.0x versions, the only significant
coding difference being that the former made heavy use of cargo, while
the latter didn't use 'cargo' at all.  As with all classes that are created
from scratch, a cargo instance variable is almost always unnecessary.

A full explanation of all that's going on inside TEXTWRAPGET is beyond the
scope of this session.  However, it's worth spending some time on the GET's
_display(<oGet>) routine.  We have to display several lines from a variable
length character string and handle both :hasFocus .and. !:hasFocus.  So how
best to accomplish this?  I settled on the following:

 FUNCTION _display( oGet )
 LOCAL cBuffer,nX,nPos,nWidth,nMod
 LOCAL aCurpos:= {row(),col()},cColor

 cBuffer:= IIF( oGet:hasFocus,oGet:buffer,Transf(oGet:varget,oGet:picture))
 cColor:= ColorPart( IIF(oGet:hasFocus,2,1),oGet )
 nPos:= 1

 nWidth:= oGet:MAXCOL -oGet:col +1
 nMod:= ( Len(cBuffer) % nWidth )
 oGet:NUMROWS:= Int( Len(cBuffer)/nWidth ) + IIF(nMod >0,1,0)
 oGet:LENFIRSTROW:= Len( SUBS(cBuffer,1,nWidth) )

 DispBegin()
 rScreen(oGet)

 FOR nX:= 1 TO oGet:NUMROWS
    SetPos( oGet:row+nX-1, oGet:col )
    Dispout( SUBSTR( cBuffer, nPos, nWidth ), cColor )
    nPos+= nWidth
 NEXT

 oGet:LENLASTROW:= Len(SUBS(cBuffer,nPos-nWidth))
 SetPos( aCurpos[1], aCurpos[2] )
 DispEnd()

 RETURN oGet

What to display is determined by the GET's ':hasFocus' instance variable. 
If the GET :hasFocus, we know to display :buffer, else we know to use the
Transform()ed :varget.  The same goes for what color to use.  The routine
Colorpart() returns either the first or second half of the GET's
:colorSpec, depending on :hasFocus.

Before we can move on to the actual display, we need to restore the GET's
original underlying screen area (RScreen(oGet)), which is initialized at
run-time.  This serves two purposes:  1) to erase the result of :setFocus()
which displays only the first line of :buffer; 2) to accommodate an added
feature of TEXTWRAPGET, the ability to dynamically resize :buffer during text
entry or via special keystroke.  This feature is implemented via the
DYNAMIC_MODE keyword in the UDC.  I have to confess it was added more for
show (to demonstrate that it could be done relatively easily) than out of
necessity.

Moving on to the actual display, we next determine the width of the :buffer
by subtracting the GET's :col from the right-most display column as written
via the MAXCOLUMN keyword in the UDC.  The default is Maxcolumn().  The
number of rows to display is obtained by dividing the length of the string
by the :buffer's width.  We can then execute a simple FOR/NEXT loop to
display 'nWidth' number of characters, move ahead in the string by 'nPos'
plus 'nWidth', and re-display until the entire string has been displayed. 
Note that both the screen area restoration and the actual display occur
inside DispBegin()/DispEnd() to give the effect of a single screen write.

Also worth noting here are some of the special key assignments enabled in
TEXTWRAPGET:

Keypress      Behavior
--------      --------
K_CTRL_PGUP   top line of buffer.
K_CTRL_PGDN   bottom line of buffer.
K_CTRL_U      restores the entire buffer.
K_ALT_U       restores the current line, provided changes occurred.
K_ALT_W       restores the current word, provided changes occurred.
K_TAB         next word (rather than next line).
K_SH_TAB      previous word (rather than previous line).
K_CTRL_Y      delete from cursor to end of buffer.
K_CTRL_L      delete from cursor to end of line.
K_ENTER       -INSERT on: push text from cursor to end of line to next row,
              inserting spaces.  Carriage return/line feed is NOT inserted.
              -INSERT off: advance cursor row and column positions.
              -when on last buffer row: exit GET.

More complete documentation can be found in TEXTWRAP.DOC on the conference
source code disk.

Head 2 - Context (field) sensitive help

Because we've loaded the GET's ordinal position in :cargo (oGet:ORDPOS),
if we're using an array that keeps a 1-1 correspondence between getlist
elements and ordinal field positions, we can implement field-sensitive help
in a snap.

The most efficient way to load table field values for inclusion in a
getlist is via an array.  Typically, we'd do something like this:

aArray:= Array( FCount() )
Aeval( aArray,{|e,n| aArray[n]:= FieldGet(n)} )

And so, if we build our getlist, in the same order, i.e., GET 1 refers to
aArray[1], GET 2 refers to aArray[2], etc., we can use oGet:ORDPOS to
inform us of the field name and value.  Inside our Help() routine:

// identify the field name.
Field( oGet:ORDPOS )

// retrieve the field value.
FieldGet( oGet:ORDPOS )


Head 2 - GET "jumping"

Moving to any other position in a getlist (what we'll call "jumping")
without GETSYS.PRG modifications is a great deal easier in CA-Clipper 5.2. 
In case you hadn't noticed, ReadModal() now accepts a second parameter, the
starting ordinal position in the getlist.  As we're about to see, this is
used to great advantage.

First, we need an external (to the ReadModal() call) loop.  We'll continue
to re-enter ReadModal() as long as we keep RETURNing a getlist ordinal
position greater than 0 and while our getlist contains no 'exitstate's of
either GE_WRITE or GE_ESCAPE (see StillReading()).

 FUNCTION SKReadModal( getlist, nReadPos )
 LOCAL lUpdated:= .F.,nGetPos
 DEFAULT nReadPos TO 1

 WHILE nReadPos > 0 .and. StillReading(getlist)
     lUpdated:= ReadModal( getlist, nReadPos )
     // if we had a jump, do it.
     IF (nGetPos:= Ascan( getlist,{|e| e:JUMP2POS > 0} )) > 0
        // reset for another entry.
        nReadPos:= getlist[nGetPos]:JUMP2POS
        getlist[nGetPos]:JUMP2POS:= 0
        getlist[nGetPos]:exitstate:= GE_NOEXIT
     ENDIF
 ENDDO
 RETURN lUpdated

 FUNCTION StillReading( getlist )
 RETURN Ascan( getlist, {|e| e:exitstate==GE_WRITE .or. ;
                             e:exitstate==GE_ESCAPE } ) == 0

Most likely, we will want to jump to another GET from within the current
GET's 'postblock', where our coding requirement is simply:

// assign to the current GET, the ordinal position of the destination GET,
// 5, e.g..
oGet:JUMP2POS:= 5

// leave the READ.
oGet:exitstate:= GE_WRITE

Following the READ, we Ascan() the getlist for a JUMP2POS greater than 0. 
If found, we now have the position of the GET holding the ordinal position
of our destination GET, 'nGetPos' above.  From that, we capture JUMP2POS in
a variable, 'nReadPos' above, and reset JUMP2POS to 0.  Remember, though,
that we also re-assigned the current GET's 'exitstate' so now, we re-assign
to GE_NOEXIT.

With the new getlist starting position in 'nReadPos', we supply that as a
second argument to ReadModal() which dutifully begins a new READ of the
same getlist at position, 'nReadPos'.

In our demo, we implement "user-defined jumping", if you will.  A press of
K_F5 pops up a GET for entry of either the ordinal position to which to
jump or the GET's title.

Since "jumping" is a GET system-wide capability, it more properly belongs
as part of GETSYS.PRG.  To accomplish this, we would compose a get/set
routine, ReadPos(), and add a new 'exitstate', GE_JUMP. Then, to jump from
a 'postblock' to GET 10, for example, your code would be:

#define GE_JUMP  10

oGet:exitstate:= GE_JUMP
ReadPos( 10 )

with ReadPos() looking like this:

 FUNCTION ReadPos( nPos )
    LOCAL nSavPos := snReadPos
    IIF( valtype(nPos)=="N", snReadPos:= nPos, )
    RETURN ( nSavPos )

'snReadPos' is an additional external STATIC variable in the modified
GETSYS.  See GETSYSSK.PRG on the conference source code disk.

Also, a new CASE is added to Settle() in order to detect GE_JUMP:

CASE ( nExitState== GE_JUMP )
      nPos:= ReadPos()

Settle(), in GETSYS.PRG, RETURNs 'nPos' to ReadModal() where the new GET is
assigned and posted to GetActive().


Head 2 - Multiple GET pages

A problem of sorts arises when our underlying data, usually the number of
table fields for which we're building our getlist, contains more GETs than
can be conveniently or attractively displayed on maxrow() rows.  In this
case, we'd like the capability of "paging" through any number of screens
built from the same underlying source.

Our strategy here is simply to compose as many getlists as is required, and
switch to another list when the user exits the current READ.  Here's how we
achieve this in our demo:

aGets:= { Gets1(),Gets2(),Gets3() }
nGetList:= 1
WHILE nGetlist > 0
    Titles( aGets[nGetlist], DISPLAY_GETS )
    SKReadModal( aGets[nGetlist] )
    IF (nKey:= Lastkey()) == K_ESC
       nGetlist:= 0
    ELSE
       nGetlist+= IIF( nKey==K_PGDN,1,-1 )
       IF nGetlist < 1 .or. nGetlist > Len(aGets)
          nGetlist:= IIF( nGetlist < 1,Len(aGets),1 )
       ENDIF
    ENDIF
ENDDO

Our demo presupposes a invididual routines composing each getlist, but it
might be more common to see a long getlist RETURNed from a routine which
creates a GET for each field in a table.  In this case, we would break
apart the RETURNed getlist in a sequence such as this:

// routine returns long getlist.
getlist:= BuildGets()

aGets:= Array(3)
aGets[1]:= Getlist( getlist, 1, 20 )
aGets[2]:= Getlist( getlist, 21, 40 )
aGets[3]:= Getlist( getlist, 41, 60 )

 FUNCTION Getlist( getlist, nStart, nLimit )
 LOCAL nX,aGetlist:= {}
 FOR nX:= nStart TO nLimit
    Aadd( aGetlist, getlist[nX] )
 NEXT
 RETURN aGetlist

Head 2 - Using a mouse

Mouse-ready applications are always in demand, and the good news for
programmers is...they aren't difficult to write.  A set of sturdy mouse
routines is availble free of charge in the Nanforum Toolkit (library 5,
Clipper forum, CompuServe).  We make almost exclusive use of them for our
mouseable READs.  The actual mouse routines referenced below are pseudo-
functions implemented via the preprocessor and contained in MOUSE.CH on the
source code disk.

First, we need to replace our calls to Inkey(0) with a routine that can
terminate with *either* a keystroke or a mouse click.  The demo uses
SKInkey( <oGet>, 0 ):

 FUNCTION SKInkey( oGet, nKey )
 LOCAL lLooping:= .T.,bBlock,aKeys

 WHILE lLooping

     nKey:= MouseHandler( oGet )
     lLooping:= ( nKey > 0 )
     IF nKey==0 .and. Nextkey() <> 0
        nKey:= Inkey(0)
        IF valtype( bBlock := SetKey(nKey) ) == "B"
           Eval( bBlock, Procname(1), Procline(1), Readvar(), oGet )
        ELSE; lLooping:= .F.
        ENDIF
     ENDIF

 ENDDO

 RETURN (nKey)

Within our loop we first call our mouse polling routine, MouseHandler(),
which RETURNs 0 if no mouse click has occurred and a key has been pressed. 
In this case, we know to obtain Inkey(0)'s return value and check for a SET
KEY block which we execute if found.  If we've had a mouse click,
MouseHandler() RETURNs 'nKey' which will be either 1) 27, corresponding to
a right button click; or 2) 999 (K_MOUSE) to indicate a left button click
outside the current GET's :buffer.  Our loop terminates if either a mouse
click in another GET's buffer has occurred, or a non-SET KEY has been
pressed.

It's also possible, of course, to left click inside the GET's :buffer in
order to reposition the text cursor.  In this case, we SetPos() within
MouseHandler() and remain in its loop:

WHILE Nextkey() == 0 .and. nKey==0
 
    DO CASE
    CASE MRClick()     // right button click.
       nKey:= K_ESC
       oGet:exitstate:= GE_ESCAPE
       WHILE MRClick()  ; ENDDO
    CASE MLClick()     // left button click.
       // mouse row and col.
       nR:= MRow() ; nC:= MCol()
       // have we a GET there?
       IF (nGet:= GetAtMouse( oGet, nR, nC )) > 0
          IF oGet==oGet:GETLIST[nGet]
             SetPos( nR, nC )
          ELSE   // jump.
             oGet:JUMP2POS:= nGet
             oGet:exitstate:= GE_WRITE
             nKey:= K_MOUSE
          ENDIF
       ENDIF

       // clear mouse click.
       WHILE MLClick() ; ENDDO

    ENDCASE

 ENDDO

We begin with a loop that continually polls Nextkey() and a LOCAL 'nKey'.
On a right button click, we "get outta Dodge" quickly.  'nKey' is assigned
K_ESC, the GET's 'exitstate' is assigned GE_ESCAPE, and we clear the mouse.
With "fast" mouse buttons, failure to perform this step can lead to
spurious display results.

On a left button click, we assume the user wants to take some action which
will either be a text cursor movement with the current GET's :buffer or a
jump to another GET.  For the former, we simply SetPos() at the mouse
coordinates.  For the latter, we use GetAtMouse() to Ascan() the getlist,
determining if the click has occurred within another GET's buffer.

 FUNCTION GetAtMouse( oGet, nR, nC )
 RETURN Ascan( oGet:GETLIST,{|e| MInregion( e:row,e:col,e:row, ;
               e:col+Len(TRANSFORM(e:varget,e:picture))-1 ) } )

MInregion() is an NF Toolkit routine that detects if the mouse cursor is
positioned within coordinates represented by its 4 parameters.  If
GetAtMouse() returns greater than 0, we *have* clicked on another GET, and
set up a jump precisely as explained in the section, "GET jumping", above.
Assigning 'nKey' K_MOUSE, we instruct our loop to terminate, RETURNing
'nKey' to SKInkey(), which in turn, RETURNs to the current reader.

Using a mouse with Tbrowse is only a slightly different matter.  Instead of
checking for a GET at the mouse coordinates, we check for a click within
the TBrowse routine's array with code such as this:

IF MInregion( oBr:nTop, oBr:nLeft, oBr:nTop+Len(aArray), oBr:nBottom )
   nR:= MRow()
   // what row does current elem occupy?
   nCurrow:= oBr:nTop+oBr:rowPos-1
   // difference between this and nR is number of positions to move.
   nNumpos:= IIF( nR==nCurrow,0,ABS(nR-nCurrow) )
   IF nNumpos > 0
      // move TBrowse to new position.
      WHILE nR > nCurrow ; oBr:down(); nCurrow++ ; ENDDO
      WHILE nR < nCurrow ; oBr:up()  ; nCurrow-- ; ENDDO
      nKey:= K_MOUSE
   ENDIF
   WHILE MLClick() ; ENDDO
ENDIF

Our TBrowse routine also checks for left clicks on any of the four corners
of the displayed box:

DO CASE
CASE MInregion( oBr:nTop, oBr:nLeft, oBr:nTop, oBr:nLeft ) .or. ;
     MInregion( oBr:nBottom, oBr:nLeft, oBr:nBottom, oBr:nLeft )
     // upper left or bottom left.
     nKey:= IIF( MInregion( oBr:nTop, oBr:nLeft, oBr:nTop, oBr:nLeft ),;
                 K_CTRL_PGUP,K_CTRL_PGDN )
CASE MInregion( oBr:nTop, oBr:nRight, oBr:nTop, oBr:nRight ) .or. ;
     MInregion( oBr:nBottom, oBr:nRight, oBr:nBottom, oBr:nRight )
     // upper right or bottom right.
     nKey:= IIF( MInregion( oBr:nTop, oBr:nRight, oBr:nTop, oBr:nRight ),;
                 K_PGUP,K_PGDN)
ENDCASE
RETURN nKey

In these cases, the actual TBrowse movement is performed by the main
routine based upon the value of 'nKey'.

Head 2 - More Runtime Speed?

Load size permitting, try moving the following CLIPPER.LIB and/or EXTEND.LIB
modules (a non-exhaustive list) into your application's root:

Head 3 - For General display:

ACHOICE, BOX, COLOR, GT, GTAPI, PICT, SCROLL, SAVEREST, SETCURS, TERM

Head 3 - For GETs:

GETS0, GETS1, GETS2

Head 3 - For TBrowse:

TBROWSE,TBROWSE0
