//********************************************************************************
//* File       : AnsiCmdSkf.cpp                                                  *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2022-2026 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice below                               *
//* Date       : 02-Apr-2026                                                     *
//* Version    : (see AnsiCmdVersion string in AnsiCmd.cpp)                      *
//*                                                                              *
//* Description: Auxilliary (protected, non-public) methods of the ACWin class.  *
//*              The methods in this module manage the functionality and data    *
//*              within the skForm and skField classes. The skForm and skField   *
//*              classes are defined in AnsiCmdDef.hpp.                          *
//*              Also, misc. utility methods related to the ACWin class.         *
//*                                                                              *
//* The skForm class defines a user interface consisting of an array of          *
//* text input fields and other user-interface controls. Each field within the   *
//* skForm object is defined as an skField-class object.                         *
//*                                                                              *
//* Editing of text within the fields is handled by the ACWin-class methods.     *
//*    EditForm() or                                                             *
//*    editDialog()                                                              *
//* While the skForm class is fully integrated within the ACWin class, it is     *
//* possible to implement an skForm object which _appears to be_ outside the     *
//* ACWin object. See the Test_SoftEcho() method in AnsiCmdTest.cpp for some     *
//* examples of using the skForm object within an "invisible" ACWin object.      *
//*                                                                              *
//* Certain "special" keys are defined for editing within a field, and for       *
//* moving among fields. See the isSpecialKey method for details.                *
//*                                                                              *
//* A "Hotkey" may be assigned to any control. These are in the ALT+n group or   *
//* the CTRL+A - CTRL+Z group. See the isHotkey and isFunky methods for details. *
//*                                                                              *
//* Other "special" keys and other user-interface widgets may be added in future *
//* releases.                                                                    *
//*                                                                              *
//********************************************************************************
//* Notes on use of GNOME/Wayland clipboard with skForm/skField objects:         *
//* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                *
//* To avoid tying ourselves to external libraries, we do not directly           *
//* access the Wayland (system) clipboard from inside the AnsiCmd class.         *
//* Instead, we interface with the external third-party "wl-clipboard"           *
//* utilities "wl-copy" and "wl-paste. See documentation for details.            *
//*                                                                              *
//* These utilities (if installed) may be accessed through the WaylandCB class   *
//* which will move data between the system clipboard and the AnsiCmd Local      *
//* Clipboard.                                                                   *
//* If the "wl-clipboard" utilities are not installed or are otherwise           *
//* unavailable, the CTRL+C, CTRL+V, CTRL+X clipboard commands will access only  *
//* the local (application-specific) clipboard.                                  *
//*                                                                              *
//* Note on the terminal emulator's clipboard interface:                         *
//* - - - - - - - - - - - - - - - - - - - - - - - - - -                          *
//* The terminal emulator's copy-and-paste menu items or the equivalent          *
//* shortcut keys CTRL+SHIFT+C and CTRL+SHIFT+V as well as the mouse-only        *
//* console copy-and-paste are functional, but are invisible to console          *
//* applications, and so are potentially dangerous. See documentation for        *
//* additional information on this hidden clipboard interface.                   *
//*                                                                              *
//********************************************************************************

//*****************
//* Include Files *
//*****************
#include "AnsiCmd.hpp"     //* AnsiCmd-class definition
#include "AnsiCmdWin.hpp"  //* AnsiCmd-class window-object definitions

//*----------------------------------------------------------------------------*
#if INCLUDE_API != 0 //* Build the API methods as part of the AnsiCmd library. *
//*----------------------------------------------------------------------------*

//***************
//* Definitions *
//***************
#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0
extern ofstream ofsdbg ;
extern gString  gsdbg ;
#endif   // DEBUG_ANSICMD && DEBUG_LOG

//********************
//* Local prototypes *
//********************
//* The functions which are not declared as 'static' are *
//* called from another module.                          *
//* - - - - - - - - - - - - - - - - - - - - - - - - - -  *
//* Convert the specified ASCII character to the equivalent hotkey character. *
bool char2Hotkey ( wchar_t& wkey ) ;
//* Scan a menubar control for next/previous token. *
bool menubarToken ( wchar_t wch, const wchar_t* wtxt, int& ip, int& smi ) ;

//**************
//* Local data *
//**************
//* Reference array of ANSI escape sequences in AnsiCmd.cpp.*
extern const wchar_t *ansiSeq[aesCOUNT] ;
//* Table lookup for text-modifier attributes.                     *
//* Translates acAttr value to aeSeq value which indexes the array *
//* of ANSI escape sequences for modifier ON and modifier OFF.     *
//* Used for initializing hotkey designators, "selected" text,     *
//* "highlighted" text, etc.                                       *
class modifierTable
{
   public:
   bool getSequence ( acAttr mod, const wchar_t*& modOn, const wchar_t*& modOff )
   {
      bool status = false ;               // return value
      //* Initialize caller's parameters to default values. *
      //*         (in case of invalid 'mod' value)          *
      modOn  = ansiSeq[aesUNDERLINE] ;
      modOff = ansiSeq[aesUNDERLINE_OFF] ;             

      for ( short i = ZERO ; i < MODTAB_CNT ; ++i )
      {
         if ( mod == this->acaMod[i] )
         {
            modOn = ansiSeq[this->aesMod[i]] ;
            modOff = ansiSeq[this->aesOff[i]] ;
            break ;
         }
      }
      return status ;
   }  //* getOnSequence() *

   //** Public Data Members **
   static const short MODTAB_CNT = 8 ;
   const acAttr acaMod[MODTAB_CNT] =
   { acaUNDERLINE, acaREVERSE,  acaBOLD, acaITALIC, 
     acaBLINK,     acaOVERLINE, acaXOUT, acaCONCEAL } ;
   const aeSeq  aesMod[MODTAB_CNT] =
   { aesUNDERLINE,  aesREVERSE,  aesBOLD, aesITALIC, 
     aesBLINK_FAST, aesOVERLINE, aesXOUT, aesCONCEAL } ;
   const aeSeq  aesOff[MODTAB_CNT] =
   { aesUNDERLINE_OFF, aesREVERSE_OFF,  aesBOLD_OFF, aesITALIC_OFF, 
     aesBLINK_OFF,     aesOVERLINE_OFF, aesXOUT_OFF, aesCONCEAL_OFF } ;
} modtab ;


//*************************
//*        cbCopy         *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Copy the 'selected' text of the target field to the local clipboard.         *
//* Then, if currently connected to the system clipboard, copy the local         *
//* clipboard data to the system clipboard.                                      *
//*  a) If the 'cut' flag is reset, then after copying the data to the local     *
//*     clipboard, reset the selection delimiters.                               *
//*  b) If the 'cut' flag is set, then after copying the data to the             *
//*     local clipboard, delete the selected text from source field.             *
//*     This will also reset the selection delimiters: (sela:selb)               *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          cut   : (optional, 'false' by default)                              *
//*                  if 'true', delete the selected source text from source field*
//*                  if 'false', field data are not modified                     *
//*                                                                              *
//* Returns: number of UTF-8 data bytes written to clipboard                     *
//*           or 0 if no 'selected' source text                                  *
//********************************************************************************
//* Note: It is assumed that the data referenced by 'fptr' is correct.           *
//* Specifically, that 'sela' < 'selb' and that both indices reference a         *
//* character within the field text. Garbage in, garbage out.                    *
//*                                                                              *
//* Programmer's Note: 'fi' does not _necessarily_ match 'fptr', but if          *
//* this method is called in response to copy/cut keycode, it will match.        *
//* -- Note also, that after a 'cut' operation, we would prefer to call the      *
//*    skfClearSelection() method; however, that would mean a double refresh,    *
//*    which would be a waste of time.                                           *
//* -- If wcsCUT, we must reformat the text under some circumstances.            *
//*    When reformatting should occur is a design decision based on:             *
//*     a) whether a newline was included in the text that was cut.              *
//*     b) the text-flow type specified for the field.                           *
//*     c) the amount of text removed.                                           *
//*     d) potentially other factors                                             *
//*    Currently, text is automatically reformatted for fields with flowing text *
//*    (flow != floShift) AND if a newline was deleted.                          *
//*    This decision will be revisited at intervals.                             *
//*                                                                              *
//********************************************************************************

int ACWin::cbCopy ( skForm* skfp, short fIndx, bool cut )
{
   skField *fptr = &skfp->fld[fIndx] ;    // pointer to target field
   gString gsSel ;                        // selection work buffer
   int cbBytes = ZERO ;                   // return value

   if ( (fptr->sela != fptr->selb) && (fptr->txt.gschars() > 1) )
   {
      //* Get a copy of selected text (optionally delete source). *
      this->copySelection ( skfp, fIndx, gsSel, cut ) ;

      //* Write the captured data to the clipboard. *
      cbBytes = this->cbSet ( gsSel ) ;

      if ( cut != false )
      {
         //* Determine whether field text needs to be reformatted.  *
         //* The criteria for reformat may change. (see note above) *
         bool reformat = bool((fptr->flow != floShift) &&
                              ((gsSel.find( NEWLINE )) >= ZERO)) ;

         if ( reformat )                        // perform full reformat
            this->formatFieldText ( skfp, fIndx, true ) ;
         else
            this->parseFieldText ( fptr ) ;     // re-index the data
         this->refreshField ( skfp, fIndx ) ;   // update the display
      }
      this->refreshField ( skfp, fIndx ) ;      // update the display
   }

   return cbBytes ;

}  //* End cbCopy() *

//*************************
//*     copySelection     *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* -- It is assumed that there actually is "selected" data in the target field. *
//* -- The selected text is copied to caller's buffer,                           *
//*    and if specified, the text is deleted from the target field.              *
//* -- The IP is set to the position referenced by 'sela'.                       *
//* -- The 'seli', 'sela' and 'selb' values are reset to indicate that no text   *
//*    is now selected.                                                          *
//* -- Caller is responsible for refreshing the display.                         *
//*                                                                              *
//* Called by cbCopy() to cut the selection to the clipboard, and called when    *
//* user presses the Delete key or backspace key to delete the selection         *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          gsSel : (by reference) receives the 'selected' text data which has  *
//*                  been deleted from the target field.                         *
//*          cut   : if set, delete the selected source text                     *
//*                  if reset, source text is not modified                       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* There is a design decision between copy/cut/bksp/del of "highlighted" vs.    *
//* "selected" text, (The 'state' flag is set to indicate highlighted data.)     *
//* Currently we process both selected and highlighted text in _almost_ the      *
//* same way, on the assumption that if the application allows the user to       *
//* highlight text, it must be for a reason.                                     *
//* The only real difference in the way 'highlighted' data are processed is that *
//* the highlight is persistent when user scrolls or copies the data, and        *
//* 'selections' are cleared when data are scrolled, copied or cut.              *
//********************************************************************************

void ACWin::copySelection ( skForm* skfp, short fIndx, gString& gsSel, bool cut )
{
   skField *fptr = &skfp->fld[fIndx] ;       // pointer to target field
   int selLen = fptr->selb - fptr->sela,     // number of characters in selection
       ipTrg  = fptr->sela ;                 // final IP value

   //* Note that character referenced by 'selb'  *
   //* index _is not_ included in the selection. *
   gsSel.loadChars( &fptr->txt.gstr()[fptr->sela], selLen ) ;

   if ( cut != false )
   {
      fptr->txt.erase( gsSel.gstr(), fptr->sela ) ; // delete the copied text
      fptr->ip = ipTrg ;                     // adjust IP position
      fptr->state = false ;                  // reset the 'highlight' flag
   }
   if ( ! fptr->state )                      // for 'selected' data only
   {
      fptr->sela = fptr->selb = ZERO ;       // reset the selection indices
      skfp->seli = skfp->fCnt ;
   }

}  //* End copySelection() *

//*************************
//*        cbPaste        *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Paste the contents of the clipboard to the target field.                     *
//* -- If the target field does not contain 'selected' text, the clipboard data  *
//*    are inserted at the current insertion point (IP).                         *
//* -- If the target field does contain 'selected' text, then the data from the  *
//*    clipboard will replace the 'selected' text.                               *
//*                                                                              *
//* Note that if the combinded length of the text would exceed the field's       *
//* storage capacity, the paste operation will be aborted to avoid loss of data. *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*                                                                              *
//* Returns: number of UTF-8 data bytes read from clipboard                      *
//*           or  0 if clipboard is empty                                        *
//*           or -1 if paste is aborted due to potential overrun                 *
//********************************************************************************
//* Note: The calculations for placing the IP _after_ the inserted text are a    *
//*       bit complex and fragile. Also, the calculations include some           *
//*       duplication of code, but we have not yet consolidated the duplication. *
//*       This was done to fascilitate debugging. We will revisit this later.    *
//*                                                                              *
//* Note: The potential for buffer overflow is remote because The target gString *
//*       object will automagically expand as needed; however, detection of      *
//*       potential data overflow is implemented.                                *
//********************************************************************************

int ACWin::cbPaste ( skForm* skfp, short fIndx )
{
   #define DEBUG_CBGET_FLOW (0)           // for debugging only
   #define DEBUG_CBGET_SHFT (0)           // for debugging only
   #define DEBUG_CBGET_OVER (0)           // for debugging only

   skField *fptr = &skfp->fld[fIndx] ;    // pointer to target field
   gString gsClip ;                       // contents from clipboard
   gString gsSel ;                        // copy of 'selected' text
   int   clpChars = ZERO,                 // characters from clipboard
         cbBytes  = -1,                   // return value - UTF-8 bytes from clipboard
         fldChars = fptr->txt.gschars(),  // characters in target field
         selChars = ZERO,                 // "selected" characters in target field
         row_diff,                        // number of rows the IP has moved
         clip_token,                      // index of last 'token' of clipboard data
         ip_target,                       // calculated IP value for char after inserted data
         bump = ZERO ;                    // set to 1 if clipboard contains a trailing space

   //* Read data from clipboard. (count does not include nullchar) *
   clpChars = (this->cbGet ( gsClip )) - 1 ;

   //* For single-row targets, strip newlines.  *
   if ( fptr->hgt == 1 )
   {
      gsClip.erase( NEWLINE, ZERO, true, true ) ;
      clpChars = gsClip.gschars() - 1 ;
   }

   //* If empty string, then cbBytes == ZERO. *
   //*  (exclude null terminator)             *
   if ( (cbBytes = gsClip.utfbytes() - 1) > ZERO )
   {
      //* If field contains "selected" text, capture the selection. *
      if ( fptr->sela < fptr->selb )
      {
         gsSel.loadChars( &fptr->txt.gstr()[fptr->sela], (fptr->selb - fptr->sela) ) ;
         selChars = gsSel.gschars() - 1 ; // (not incl. nullchar)
      }

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_OVER != 0
      int totChars = (fldChars + clpChars - selChars ) ;
      if ( ofsdbg.is_open() )
      {
         if ( totChars >= (gsALLOCMAX - 4) )
         {
         gsdbg.compose(
            "PASTE_OVERFLOW: fldChars:%04d clpChars:%04d selChars:%02d totChars:%04d", 
            &fldChars, &clpChars, &selChars, &totChars ) ;
         ofsdbg << gsdbg.ustr() << endl ;
         }
      }
      #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_OVER

      //* If all text will fit within target buffer,  *
      //* insert the text at the IP _or_ if selected  *
      //* text in target field, replace selection.    *
      //* (Note that total includes 1 nullchar, AND)  *
      //* (that we compare to LESS_THAN buffer max.)  *
      if ( (fldChars + clpChars - selChars) < (gsALLOCMAX - 4) )
      {
         int clip_base = fptr->ip ;    // index of insertion for clipboard data

         //* If 'selected' text in target field,  *
         //* replace it with data from clipboard. *
         if ( selChars > ZERO )
         {
            clip_base = fptr->sela ;      // remember position for start of insertion
            fptr->txt.replace( gsSel.gstr(), gsClip.gstr(), fptr->sela ) ;
         }
         //* Else insert clipboard data at Insertion Point. *
         else
            fptr->txt.insert( gsClip.gstr(), fptr->ip ) ; // insert the text

         //* Scan for the last token in clipboard data.        *
         //* If there is only one token, index will be zero.   *
         if ( (gsClip.gstr()[gsClip.gschars() - 2]) == wcsSPACE )
         { gsClip.strip() ; ++bump ; }
         clip_token = gsClip.findr( wcsSPACE ) + 1 ;

         //***********************************************************
         //* For fields with flowing text, reformat the entire field.*
         //***********************************************************
         if ( fptr->flow != floShift )
         {
            //* The new IP will be on the same row as clip_token  *
            //* and reference the character _following_ the token.*
            // Programmer's Note: This may not be true if flow == floChar 
            // which does not break on tokens, but on character columns.

            #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_FLOW != 0
            gsdbg.compose( "PASTE floToken: ip:%-3d currow:%-3d,%-3d,%-3d "
                           "rtop:%-3d rcur:%-3d rbot:%-3d rlst:%-3d",
               &fptr->ip, &fptr->mrp.bor, &fptr->mrp.eov, &fptr->mrp.eor,
               &fptr->mrp.rtop, &fptr->mrp.rcur, &fptr->mrp.rbot, &fptr->mrp.rlst ) ;
            ofsdbg << gsdbg.ustr() << endl ;
            #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_FLOW

            this->formatFieldText ( skfp, fIndx, true ) ; // reformat the text
            // (Note: This call _also_ clears "selection", if any.)

            //* The new IP references the character *
            //* FOLLOWING the inserted text.        *
            fptr->ip = ip_target = 
               fptr->txt.after( &gsClip.gstr()[clip_token], clip_base, true ) + bump ;

            //* Re-index the field data. If IP is modified by the call,   *
            //* it indicates that row containing the calculated IP is     *
            //* outside visible space.                                    *
            //* If so, adjust the offset and re-parse the data.           *
            //* (Note that if IP is invalid, 'rcur' will also be invalid.)*
            this->parseFieldText ( fptr ) ;
            if ( fptr->ip != ip_target )
            {
               fptr->ip = ip_target ;     // reset the IP to target value
               for ( int f = fptr->mrp.rCnt - 1 ; f >= ZERO ; --f )
               {
                  #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_FLOW != 0
                  gsdbg.compose( "  %02hd %03hd) ip_target:%d", 
                                 &f, &fptr->mrp.roff[f], &ip_target ) ;
                  ofsdbg << gsdbg.ustr() << endl ;
                  #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_FLOW

                  //* 'f' is index of row containing ip_target *
                  if ( ip_target >= fptr->mrp.roff[f] )
                  {
                     //* If target row is outside visible space, *
                     //* scroll the row into view.               *
                     if ( f > fptr->mrp.rbot )
                     {
                        row_diff = f - fptr->mrp.rbot ;

                        #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_FLOW != 0
                        gsdbg.compose( "  target row not visible, scroll up by %d", 
                           &row_diff ) ; ofsdbg << gsdbg.ustr() << endl ;
                        #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_FLOW

                        fptr->off = fptr->mrp.roff[fptr->mrp.rtop + row_diff] ;
                        this->parseFieldText ( fptr ) ;
                     }
                     break ;
                  }
               }
            }

            this->refreshField ( skfp, skfp->fi ) ; // make the changes visible

            #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_FLOW != 0
            gsdbg.compose( "                ip:%-3d currow:%-3d,%-3d,%-3d "
                           "rtop:%-3d rcur:%-3d rbot:%-3d rlst:%-3d",
               &fptr->ip, &fptr->mrp.bor, &fptr->mrp.eov, &fptr->mrp.eor,
               &fptr->mrp.rtop, &fptr->mrp.rcur, &fptr->mrp.rbot, &fptr->mrp.rlst ) ;
            ofsdbg << gsdbg.ustr() << endl ;
            #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_FLOW
         }     // (flowing text)

         //**************************************************************
         //* For pre-formatted text data, the row of the IP will change *
         //* only if the clipboard data (or removed 'selection' data)   *
         //* contain newline characters. Otherwise, only a horizontal   *
         //* shift may be needed. Verify that row and column containing *
         //* IP are in visible space.                                   *
         //**************************************************************
         else
         {
            //* If "selection" was replaced by clipboard data, *
            //* reset the selection indices. (no refresh)      *
            if ( fptr->sela < fptr->selb )
               this->skpClearSelection ( skfp, false ) ;

            #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_SHFT != 0
            gsdbg.compose( "PASTE floShift: shft:%-3d ip:%-3d currow:%-3hd,%-3hd,%-3hd "
                           "rtop:%-3hd rcur:%-3hd rbot:%-3hd rlst:%-3hd",
               &fptr->shft, &fptr->ip, &fptr->mrp.bor, &fptr->mrp.eov, &fptr->mrp.eor,
               &fptr->mrp.rtop, &fptr->mrp.rcur, &fptr->mrp.rbot, &fptr->mrp.rlst ) ;
            ofsdbg << gsdbg.ustr() << endl ;
            #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_SHFT

            //* The new IP references the character FOLLOWING the inserted text. *
            fptr->ip = ip_target = 
               fptr->txt.after( &gsClip.gstr()[clip_token], clip_base, true ) + bump ;

            //* Re-index the field data *
            this->parseFieldText ( fptr ) ;

            //* For multi-row fields, verify that row containing IP is  *
            //* in visible space, and if not, scroll it into view.      *
            if ( (fptr->hgt > 1) && (fptr->mrp.ovr || (fptr->ip != ip_target)) )
            {
               fptr->ip = ip_target ;     // reset the IP to target value
               for ( short f = fptr->mrp.rCnt - 1 ; f >= ZERO ; --f )
               {
                  #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_SHFT != 0
                  gsdbg.compose( "  %02hd %03hd) ip_target:%d", 
                                 &f, &fptr->mrp.roff[f], &ip_target ) ;
                  ofsdbg << gsdbg.ustr() << endl ;
                  #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_SHFT

                  //* 'f' is index of row containing ip_target *
                  if ( ip_target >= fptr->mrp.roff[f] )
                  {
                     //* If target row is outside visible space, *
                     //* scroll the row into view.               *
                     if ( f > fptr->mrp.rbot )
                     {
                        row_diff = f - fptr->mrp.rbot ;

                        #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_SHFT != 0
                        gsdbg.compose( "          row %02hd not visible, scroll up by %d", 
                           &f, &row_diff ) ; ofsdbg << gsdbg.ustr() << endl ;
                        #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_SHFT

                        fptr->off = fptr->mrp.roff[fptr->mrp.rtop + row_diff] ;
                        this->parseFieldText ( fptr ) ;
                     }
                     break ;
                  }
               }
            }

            //* If IP is outside visible space to the left or right, *
            //* adjust 'shift' to bring it into visible space.       *
            if ( (fptr->ip < (fptr->mrp.bor + fptr->shft)) ||
                 (fptr->ip > fptr->mrp.eov) )
            {
               if ( fptr->ip > fptr->mrp.eov )  // IP is to the right of visible space
                  fptr->shft += fptr->ip - fptr->mrp.eov ;
               else                             // IP is to the left of visible space
                  fptr->shft = ZERO ;     // set horizontal shift to zero
               // Programmer's Note: When IP is to the left of visible space, 
               // setting 'shft' to zero is the easy way because it (doesn't actually) 
               // guarantee that IP enters visible space; however, to set IP as the 
               // leftmost visible character: shft -= bor+shft - ip;
               // Use the adjustment that is more visually advantageous.
               this->parseFieldText ( fptr, 1 ) ; // ('shft' value updated)
            }

            this->refreshField ( skfp, skfp->fi ) ; // make the changes visible

            #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_CBGET_SHFT != 0
            gsdbg.compose( "                shft:%-3d ip:%-3d currow:%-3d,%-3d,%-3d "
                           "rtop:%-3d rcur:%-3d rbot:%-3d rlst:%-3d",
               &fptr->shft, &fptr->ip, &fptr->mrp.bor, &fptr->mrp.eov, &fptr->mrp.eor,
               &fptr->mrp.rtop, &fptr->mrp.rcur, &fptr->mrp.rbot, &fptr->mrp.rlst ) ;
            ofsdbg << gsdbg.ustr() << endl ;
            #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_CBGET_SHFT
         }     // (floShift)
      }        // (no overflow)

      //* Else, abort operation to prevent potential buffer overflow. *
      else
         cbBytes = -1 ;
//OBSOLETE         cbBytes = cbsPASTE_ABORT ;
   }  // (cbBytes > ZERO)

   return cbBytes ;

   #undef DEBUG_CBGET_OVER
   #undef DEBUG_CBGET_FLOW
   #undef DEBUG_CBGET_SHFT
}  //* End cbPaste() *

//*************************
//*      clearField       *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Erase the displayed text from the target field.                              *
//* -- Caller is responsible for setting color attributes.                       *
//* -- Caller is responsible for final cursor position.                          *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*          fIndx: index of target field                                        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::clearField ( const skForm* skfp, short fIndx )
{
   const skField *fptr = &skfp->fld[fIndx] ; // pointer to target field
   gString gs ;                              // text formatting
   gs.padCols( fptr->wid ) ;                 // pad to width of field

   //* Cursor position (window relative) *
   WinPos wp( skfp->base, fptr->orig ) ;

   //* Clear each row of the field *
   for ( short r = ZERO ; r < fptr->hgt ; ++r )
   {
      this->write ( wp, gs, false ) ;
      ++wp.row ;                          // advance to next row
   }

}  //* End clearField() *

//*************************
//*      refreshText      *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Called by RefreshField() for Text controls only.                             *
//* If the target field is formatted for 'floShift', then refreshShiftedText()   *
//* is called. A target field formatted for flowing text is processed here.      *
//*                                                                              *
//* Clear the target field and rewrite the text (if any). It is assumed that:    *
//*  1) the source text is properly formatted,                                   *
//*  2) the IP (insertion point) is at the desired position, and                 *
//*  3) the logical cursor position matches the IP.                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*          fIndx: index of target field                                        *
//*          full : (optional, 'false' by default                                *
//*                 if 'false', redraw field text only                           *
//*                 if 'true',  redraw field text AND field label (if any)       *
//*                                                                              *
//* Returns: nothing                                                             *
//*          On return, color attributes are set to field attributes on the      *
//*          assumption that the field has the input focus. If this is not       *
//           the case, it is the caller's responsibility to adjust attributes.   *
//********************************************************************************
//* Note: If a field is hidden, it does not mean that its outline and label      *
//*       (if any) also hidden, because they are outside the field.              *
//*       This may cause some confusion at the application level.                *
//*                                                                              *
//********************************************************************************

void ACWin::refreshText ( const skForm* skfp, short fIndx, bool full )
{
   skField *fptr = &skfp->fld[fIndx] ;    // pointer to target field
   gString gsOut ;                        // output buffer
   WinPos  wp( skfp->base, fptr->orig ) ; // cursor position
   const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
   acAttr  modAttr ;                      // selection/highlight text modifier
   int     sela = fptr->sela,             // local copy of selection indices
           selb = fptr->selb ;

   //* If specified, redraw the field border and label. *
   if ( full )
   {
      if ( fptr->bdr )
         this->drawFieldOutline ( skfp, fIndx ) ;
      if (fptr->lab[ZERO] != NULLCHAR)
      {
         WinPos wp( skfp->base, fptr->lpos ) ;
         this->writeSeq ( wp, fptr->lab, fptr->lattr ) ;
      }
   }

   //* If the field is set as 'hidden', don't draw it. (note the early return) *
   if( fptr->hide ) { return ; }

   //* Clear the field using specified color attributes. *
   this->setAttributes ( fptr->aca ) ;
   this->clearField ( skfp, fIndx ) ;
   this->nsleep ( 0, 1 ) ; // Experimental: avoid stale data in I/O buffers

   //* If field contains no text, don't execute remainder *
   //* of the sequence. Note the early return.            *
   if ( fptr->txt.gschars() == 1 ) { return ; }

   //* ---------------------------- *
   //* "Shifted" - Text Editor Mode *
   //* ---------------------------- *
   //* If the text has not been formatted to fit the field, *
   //* then display the data with each line shifted and if  *
   //* necessary, truncated to fit within the field.        *
   if ( fptr->flow == floShift )
      this->refreshShiftedText ( fptr, skfp->base ) ;

   //* ------------------------------------ *
   //* "Flowing" Text - Word-processor Mode *
   //* ------------------------------------ *
   else
   {
      //* If a "selection" or "highlight" segment specified, *
      //* initialize the modifier pointers.                  *
      if ( fptr->sela != fptr->selb )
      {
         modAttr = (fptr->state && (fptr->iAttr != NULL)) ? 
                    fptr->iAttr[0] : acaUNDERLINE ;
         modtab.getSequence ( modAttr, modOn, modOff ) ;
      }

      //* It is assumed that the the source text is properly formatted. *
      //* The first character to be displayed is indexed by the 'off'   *
      //* member. The last character to be displayed is indicated by    *
      //* the null terminator, or if there are more text rows than can  *
      //* be displayed, by the end of the last row which fits within    *
      //* the display area.                                             *
      gsOut = fptr->txt ;
      int oindx = fptr->off, rindx = oindx, rcnt = ZERO ;
      while ( (rindx = gsOut.find( NEWLINE, rindx )) >= ZERO )
      {
         if ( ++rcnt == fptr->hgt )       // if bottom row
         { gsOut.limitChars( rindx ) ; break ; } // truncate at the target newline
         ++rindx ;                        // step past the newline
      }

      //* Bracket the selected text (if any) using the selection attribute *
      if ( fptr->sela != fptr->selb )
      {
         //* Limit the bracketed text to text within the visible space.*
         if ( (selb >= oindx) && ((selb - sela) < (gsOut.gschars() - oindx)) )
         { 
            if ( sela < oindx )           // do not under-run the displayed text
               sela = oindx ;
            if ( (selb - sela) >= gsOut.gschars() )
               selb = gsOut.gschars() - 1 ;
            gsOut.insert( modOff, selb ) ;
            gsOut.insert( modOn, sela ) ;
         }
      }
      // Programmer's Note: There is a visual anomaly for 'selected' text which 
      // spans text rows: The newline character is a zero-width character, so 
      // no underline appears in the character cell containing the newline.
      // While it would be possible to overcome this by inserting a space before 
      // the newline, at this time it seems to be more trouble than it's worth.
      gString gs = &gsOut.gstr()[oindx] ;
      this->write ( wp, gs ) ;
   }     // flowing text

}  //* End refreshText() *

//*************************
//*      refreshCtrl      *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Called by RefreshField() for non-text controls.                              *
//* Draw non-text controls (fields). Optionaly, also draw field label.           *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          focus : 'true'   draw the control using focus color                 *
//*                  'false', draw the control using non-focus color             *
//*          full  : (optional, 'false' by default)                              *
//*                  'false' draw control only                                   *
//*                  'true'  draw control AND label text                         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) Pushbutton controls:                                                      *
//*    a) Text of control and label are a single entity.                         *
//*    b) If the label string includes a hotkey character, the character is      *
//*       bracketed by ANSI escape sequences for underlining the character.      *
//*                                                                              *
//* 2) Radiobutton controls:                                                     *
//*    a) Text of control itself is a pair of brackets enclosing the character   *
//*       signifying that the state is "set". If reset, then the brackets        *
//*       enclose a space character.                                             *
//*    b) The control text is stored in the 'txt' member, and the label text     *
//*       is stored in the 'lab' member. Label attributes are stored in 'lattr'. *
//*       Label position is stored in 'lpos'.                                    *
//*                                                                              *
//* 3) Menubox controls:                                                         *
//*    a) Some Menubox controls have labels.                                     *
//*       -- A context menu (multi-row menu, not group member) may have a label. *
//*       -- A Menubar (single-row menu) may have a label.                       *
//*       Other menu configurations do not have labels.                          *
//*    b) Menubox controls do not change color based on focus/non-focus state.   *
//*    c) Single-row menus (menubars) are written in a single pass regardless    *
//*       of whether they contain escape sequences.                              *
//*    d) Each row of a multi-row menu (except the last) is delimited by a       *
//*       newline character. Because the writeSeq() method does not handle       *
//*       column-aligned output, each row of the menu is written seperately to   *
//*       prevent writeSeq() from mis-interpreting the newlines.                 *
//*    e) If the menubox has focus, the referenced menu item (row) which contains*
//*       the IP is highlighted.                                                 *
//*                                                                              *
//* 4) Scrollbox controls:                                                       *
//*    a) Scrollbox controls may have labels.                                    *
//*    b) Scrollbox controls do not change color based on focus/non-focus state. *
//*    c) The text of a scrollbox is a single, pre-formatted paragraph.          *
//*       -- For monochrome text the data are written in a single pass.          *
//*       -- For per-row color attributes, the data are written one row          *
//*          at a time.                                                          *
//*    d) If the scrollbox has focus, the indicator character references the     *
//*       row which contains the IP.                                             *
//********************************************************************************

void ACWin::refreshCtrl ( const skForm* skfp, short fIndx, bool focus, bool full )
{
   const wchar_t indyChar   = 0x0025B6 ;  // U+25B6 '▶' (focus)
   const wchar_t indyCharNF = 0x0025B7 ;  // U+25B7 '▷' (non-focus)
   //* Indicator characters as strings. This is faster than single character.*
   const wchar_t indytxt[2]   = { indyChar, NULLCHAR } ;
   const wchar_t indytxtnf[2] = { indyCharNF, NULLCHAR} ;

   skField *fptr = &skfp->fld[fIndx] ;    // pointer to target field
   FldType type  = fptr->type ;           // control type
   WinPos  wp( skfp->base, fptr->orig ) ; // display base position
   acAttr  fldAttr ;                      // color attribute
   gString gs ;                           // text analysis

   if ( this->tset->curVis )
      this->acSetCursorStyle ( csHide ) ;

   //* If specified, redraw the field border. *
   if ( full && fptr->bdr )
      this->drawFieldOutline ( skfp, fIndx ) ;

   //* If field type has seperate field and       *
   //* label text, and if label update specified. *
   if ( full && ((type == ftRB) || (type == ftSB) ||
        ((type == ftMB) && ((fptr->grp == noGROUP) || (fptr->hgt == 1)))) )
   {
      // Note that position is form base + label offset.
      WinPos wp( skfp->base, fptr->lpos ) ;
      this->writeSeq ( wp, fptr->lab, fptr->lattr ) ;
   }

   //* Draw the control *
   if ( ! fptr->hide )                    // if the field is not set as hidden
   {
      //* Select the color attribute for field update. *
      if ( focus && ((type == ftPB) || (type == ftRB)) )
      {
         //* If a global field-focus attribute has been specified *
         if ( skfp->faca.acaVal != acaUSEDFLT )
            fldAttr = skfp->faca.acaVal ;
         else
         {
            acaExpand acaRev = fptr->aca ;// working copy of field attributes
            acaRev.swap() ;               // swap fgnd/bgnd attributes
            fldAttr = acaRev.acaVal ;
         }
      }
      //* Else use field's base (non-focus) attributes. *
      else
         fldAttr = fptr->aca.acaVal ;

      //* For most single-row controls, write the data in a single pass. *
      if ( (fptr->hgt == 1) && (fptr->type != ftSB) && (fptr->type != ftMB) )
         this->writeSeq ( wp, fptr->txt.gstr(), fldAttr ) ;

      //* For menu controls, write each display row seperately. (see note above) *
      //* Note that final cursor position is not set.                            *
      else if ( fptr->type == ftMB )
      {
         wchar_t rowtxt[gsALLOCMIN] ;        // text for each menu row
         acaExpand acaRev = fptr->aca ;      // attribute for highlighted text
         acaRev.swap() ;
         acaExpand acaSel( acAttr(fptr->aca.acaVal | acaBOLD | acaITALIC) ) ;
         this->setAttributes ( fptr->aca ) ; // set the base text attributes

         //* For menubars, draw the field, with optional focus highlighting. *
         if ( fptr->hgt == 1 )
         {
            this->writeSeq ( wp, fptr->txt.gstr() ) ; // write the field text

            //* If menubar has focus, highlight the current token. *
            if ( focus )
            {
               int si = fptr->ip, ti = ZERO ;
               const wchar_t *wptr = fptr->txt.gstr() ;
               while ( (wptr[si] != SPACE) && (wptr[si] != NULLCHAR) ) // isolate the token
                  rowtxt[ti++] = wptr[si++] ;
               rowtxt[ti] = NULLCHAR ;
               wp.col += fptr->ip ;                // adjust cursor position
               this->setAttributes ( acaRev ) ;    // set highlight attributes
               this->writeSeq ( wp, rowtxt ) ;     // write the sequence
            }
         }

         //* Multi-row menus, (group sub-menus or stand-alone menus) *
         else
         {
            const wchar_t* wptr = fptr->txt.gstr() ;  // source text
            int chCnt = fptr->txt.gschars(),          // source text chars 
            rti = ZERO,                               // row-text index
            rindx = ZERO ;                            // row index
            bool specRow,           // set for "special" i.e. multi-attribute rows
                 custColor = bool(fptr->iAttr != NULL) ; // shortcut the custom attribute test

            for ( int wi = ZERO ; wi < chCnt ; ++wi )
            {
               if ( (wptr[wi] == NEWLINE) || (wptr[wi] == NULLCHAR) ) // if end-of-row
               {
                  rowtxt[rti] = NULLCHAR ;                  // terminate the text row

                  //* Write the full row including border characters. *
                  this->writeSeq ( wp, rowtxt ) ;

                  //* For multi-attribute rows: the text WITHIN the borders   *
                  //* is drawn with special attributes.                       *
                  //* a) If control has focus and IP is within the current    *
                  //*    row, draw text with swapped fg/bg.                   *
                  //* b) ONLY For context menus which do not have focus but   *
                  //*    are locked in the visible state:                     *
                  //*    If a previous menu item selection has occurred,      *
                  //*    draw text with bold and italic mods.                 *
                  //* c) If per-row attributes in 'iAttr' array, draw the     *
                  //*    text using the per-row attributes.                   *
                  // Programmer's Note: When an item with per-row attribute gets 
                  // drawn with the highlight (reverse) attribute, the color 
                  // ideally be reverse(iAttr[item]) rather than acaRev. 
                  // The update is already slow; so is it worth it to slow it even 
                  // more by calculating this value? At this time, we say no.
                  specRow = false ;
                  if ( focus && (rindx == fptr->mrp.rcur) ) // set highlight attributes
                  { this->setAttributes ( acaRev ) ; specRow = true ; }
                  // Used ONLY for context menus, so item color applied to row_index+1.
                  else if ( ! focus &&                   // set 'selected' attribute
                            ((fptr->sitem >= ZERO) && (rindx == (fptr->sitem + 1))) )
                  {
                     if ( fptr->iAttr != NULL )
                        acaSel.decode( acAttr(fptr->iAttr[rindx] | acaBOLD | acaITALIC) ) ;
                     this->setAttributes ( acaSel ) ;
                     specRow = true ;
                  }
                  else if ( custColor && (fptr->iAttr[rindx] != fptr->aca.acaVal) )
                  { this->setAttributes ( fptr->iAttr[rindx] ) ; specRow = true ; }
                  if ( specRow )
                  {
                     rowtxt[rti - 1] = NULLCHAR ;        // remove right border
                     ++wp.col ;                          // increment output column
                     this->writeSeq ( wp, &rowtxt[1] ) ; // write the text data
                     --wp.col ;                          // restore output column
                     this->setAttributes ( fptr->aca ) ; // return to base control attributes
                  }
                  ++wp.row ;                                // update output position
                  ++rindx ;                                 // increment row index
                  rti = ZERO ;                              // reset buffer index
               }
               else                          // copy character to row buffer
                  rowtxt[rti++] = wptr[wi] ;
            }  // for(;;)
         }     // multi-row
      }     // (type==ftMB)

      else if ( fptr->type == ftSB )
      {
         //* Isolate the rows to be displayed *
         // Programmer's Note: This is the same functionality as the gString 
         // formatParagraph() but is faster because we "know" that the data 
         // are already formatted correctly.
         gs = fptr->txt ;
         int oindx = fptr->off, rindx = oindx, rcnt = ZERO ;
         while ( (rindx = gs.find( NEWLINE, rindx )) >= ZERO )
         {
            if ( ++rcnt == fptr->hgt )          // if bottom row
            { gs.limitChars( rindx ) ; break ; }// truncate at the target newline
            ++rindx ;                           // step past the newline
         }

         //* If field has focus, insert indicator character. *
         if ( focus )
            gs.replace( SPACE, indyChar, fptr->ip ) ;
         //* If field does not have focus, AND if a "selection" *
         //* has been made, insert alternate indicator.         *
         else if ( (fptr->sitem >= ZERO) && 
                   ((fptr->sitem >= fptr->mrp.rtop) && (fptr->sitem <= fptr->mrp.rbot)) )
         {
            //* Convert fptr->sitem to a character offset    *
            //* and insert the non-focus indicator character.*
            gs.replace( SPACE, indyCharNF, fptr->mrp.roff[fptr->sitem] ) ;
         }

         this->setAttributes ( fptr->aca ) ; // set the color attributes
         this->clearField ( skfp, fIndx ) ;  // clear the field

         if ( fptr->iAttr == NULL )          // monochrome output
            this->write ( wp, &gs.gstr()[oindx] ) ;
         else
         {
            gs.shiftChars( -(oindx) ) ;
            wchar_t rowtxt[gsALLOCMIN] ;     // text for each text row
            const wchar_t* wptr = gs.gstr() ;
            int chCnt = gs.gschars(),        // length of source text
                rti   = ZERO,                // rowtxt index
                aindx = fptr->mrp.rtop ;     // custom attribute index
            for ( int wi = ZERO ; wi < chCnt ; ++wi )
            {
               if ( (wptr[wi] == NEWLINE) || (wptr[wi] == NULLCHAR) ) // if end-of-row
               {
                  rowtxt[rti - 1] = NULLCHAR ;  // terminate the text row removing the trailing space.
                  if ( (rowtxt[ZERO] == indyChar) || (rowtxt[ZERO] == indyCharNF) )
                  {
                     this->setAttributes ( fptr->aca ) ;
                     this->write ( wp, (rowtxt[ZERO] == indyChar) ? indytxt : indytxtnf ) ;
                     if ( fptr->iAttr[aindx] != fldAttr )
                        this->setAttributes ( fptr->iAttr[aindx] ) ;
                  }
                  else
                     this->setAttributes ( fptr->iAttr[aindx] ) ;
                  ++wp.col ;
                  this->write ( wp, &rowtxt[1] ) ;
                  --wp.col ;
                  ++wp.row ;                    // update output position
                  rti = ZERO ;                  // reset buffer index
                  ++aindx ;                     // reference next attribute
               }
               else                             // copy character to row buffer
                  rowtxt[rti++] = wptr[wi] ;
            }
         }
      }  // (fptr->type==ftSB)
   }  // (!hide)

   //* Restore fgnd/bkgnd attributes to current terminal attributes.*
   this->restoreAttributes () ;

}  //* End refreshCtrl() *

//*************************
//*  refreshShiftedText   *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Called by refreshText() when the formatting option is floShift.              *
//* When flow==floShift, the text data have not been pre-formatted to fit within *
//* the target field. Instead, each row of data is individually shifted          *
//* horizontally and/or truncated to fit within the boundaries of the field.     *
//* This is what we might think of as text-editor mode, as opposed to            *
//* word-processor mode where data are automagically wrapped at the edge of the  *
//* display as the user types.                                                   *
//*                                                                              *
//* Each row of text begins at the same column offset and extends either to the  *
//* end of the text for that row OR to the rightmost character that will fit     *
//* within the available space.                                                  *
//*                                                                              *
//* If the field contains "selected" text, the formatting is specialized.        *
//*  a) A row which contains no selected data is formatted normally.             *
//*  b) If all selected text is within a single row, the selected data are       *
//*     bracketed by escape sequences enabling, then disabling the underline     *
//*     attribute.                                                               *
//*  c) If the selected text spans multiple lines, each line which contains      *
//*     selected data receives the pair of escape sequences.                     *
//*                                                                              *
//*                                                                              *
//* Input  : fptr    : pointer to target skField object                          *
//*          skfBase : window-relative position at which to begin output         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::refreshShiftedText ( skField* fptr, const WinPos& skfBase )
{
   const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
   acAttr  modAttr ;                      // selection/highlight text modifier
   gString gsOut ;                        // text formatting (output buffer)
   //* Convert from form-relative position to terminal-relative. *
   WinPos wp( this->txtBase, skfBase, fptr->orig ) ;
   int sela = fptr->sela,                 // local copy of selection indices
       selb = fptr->selb ;

   //* If a "selection" or "highlight" segment specified, *
   //* initialize the modifier pointers.                  *
   if ( fptr->sela != fptr->selb )
   {
      modAttr = (fptr->state && (fptr->iAttr != NULL)) ? 
                 fptr->iAttr[0] : acaUNDERLINE ;
      modtab.getSequence ( modAttr, modOn, modOff ) ;
   }

   //* ---------------- *
   //* Single-row field *
   //* ---------------- *
   if ( fptr->hgt == 1 )
   {
      gsOut = &fptr->txt.gstr()[fptr->shft] ;// load from leftmost displayed character
      gsOut.limitCols( fptr->wid ) ;         // limit text width to width of field

      //* If some or all of visible text is delimited by the   *
      //* bracketing indices insert the selection attribute(s).*
      if ( (fptr->sela != fptr->selb) &&
           (
            ((fptr->sela >= fptr->shft) && (fptr->sela < (fptr->shft + gsOut.gschars())))
             ||
            ((fptr->selb >= fptr->shft) && (fptr->selb < (fptr->shft + gsOut.gschars())))
           )
         )
      {
         int ni = gsOut.gschars() - 1 ;         // index of null-terminator
         if ( fptr->shft > ZERO )               // verify selection delimiters
         {
            if ( (selb -= fptr->shft) > ni )
               selb = ni ;
            if ( (sela -= fptr->shft) < ZERO )
               sela = ZERO ;
         }
         //* Insert escape sequence(s) to mark *
         //* the selected/highlighted text.    *
         if ( selb < ni )
            gsOut.insert( modOff, selb ) ;
         gsOut.insert( modOn, sela ) ;
      }
      this->acSetCursor ( wp ) ;
      this->ttyWrite ( gsOut ) ;
      return ;                            // note the early return
   }  // (single-row)

   //* --------------- *
   //* Multi-row field *
   //* --------------- *
   wchar_t wbuff[gsALLOCMIN + 2] ;        // work buffer for individual rows
   const wchar_t* wptr = fptr->txt.gstr() ; // pointer to source text
   wchar_t txtBefore[256] = { NULLCHAR }, // selection sub-segment buffers
           txtSelect[256] = { NULLCHAR },
           txtAfter[256]  = { NULLCHAR } ;
   int si = ZERO,                         // source index
       ti = ZERO,                         // target index
       hindx, tindx,                      // indices for head/tail of current row
       rowIndx = fptr->mrp.rtop,          // row index (initially top visible row)
       rowBot  = fptr->mrp.rbot,          // index of bottom visible row
       fwidth = fptr->wid,                // field width
       shiftOff = fptr->shft,             // left shift
       wi = ZERO,                         // selection scan index
       srci,                              // selection source index
       trgi ;                             // selection target index

   //* Load the text for each visible row. *
   for ( ; (rowIndx <= rowBot) && (wptr[si] != NULLCHAR) ; ++rowIndx )
   {
      si = fptr->mrp.roff[rowIndx] ;      // index the source row
      hindx = si + shiftOff ;             // adjust row-head index (for selections)
      ti = ZERO ;                         // reset target index

      while ( (wptr[si] != NEWLINE) &&    // copy characters of current row
              (wptr[si] != NULLCHAR) )
      { wbuff[ti++] = wptr[si++] ; }
      wbuff[ti] = NULLCHAR ;              // terminate the string
      tindx = si ;                        // index end of substring (for selections)
      gsOut = wbuff ;                     // load line text
      if ( shiftOff > ZERO )              // if specified, shift data leftward
         gsOut.shiftChars( -(shiftOff) ) ;

      //* If row contains data within the display area *
      if ( gsOut.gschars() > 1 )
      {
         if ( gsOut.gscols() > fwidth )   // truncate to width of field
            gsOut.limitCols( fwidth ) ;

         //* If no text selection, OR if no selected characters  *
         //* in current row, write data as a single unit.        *
         if ( (sela == selb) || (sela > tindx) || (selb < hindx) )
         {
            this->acSetCursor ( wp ) ;
            this->ttyWrite ( gsOut ) ;
         }

         //* If some or all of this row is "selected" text,  *
         //* bracket the text using the selection attribute. *
         else
         {
            //* If selection begins at or before head of row, AND *
            //* ends at or after end of current row text, then    *
            //* full row is written with selection attribute.     *
            if ( (sela <= hindx) && (selb > tindx) )
            {
               this->acSetMod ( aesUNDERLINE ) ;
               this->acSetCursor ( wp ) ;
               this->ttyWrite ( gsOut ) ;
               this->acSetMod ( aesUNDERLINE_OFF ) ;
            }

            //* Selection is less that full row. Break   *
            //* text into selected and non-selected      *
            //* segments.                                *
            else
            {
               srci = hindx ;             // source index is head of current row
               wi = ZERO ;                // initialize source index
               txtSelect[0] = NULLCHAR ;  // initialize the segment buffers
               txtAfter[0]  = NULLCHAR ;
               gsOut.copy( wbuff, gsALLOCMIN ) ; // copy row text to work buffer

               //* Segment: characters BEFORE selection *
               for ( trgi = ZERO ; (srci < sela) && (wbuff[wi] != NULLCHAR) ; ++srci )
                  txtBefore[trgi++] = wbuff[wi++] ;
               txtBefore[trgi] = NULLCHAR ;

               //* Segment: "selected" characters. *
               //* (exclude newline if found).     *
               if ( wbuff[wi] != NULLCHAR )
               {
                  for ( trgi = ZERO ; (srci < selb) && (wbuff[wi] != NULLCHAR) ; ++srci )
                  {
                     if ( wbuff[wi] != NEWLINE )
                        txtSelect[trgi++] = wbuff[wi++] ;
                  }
                  txtSelect[trgi] = NULLCHAR ;
               }

               //* Segment: characters AFTER selection *
               if ( wbuff[wi] != NULLCHAR )
               {
                  for ( trgi = ZERO ; wbuff[wi] != NULLCHAR ; ++srci )
                     txtAfter[trgi++] = wbuff[wi++] ;
                  txtAfter[trgi] = NULLCHAR ;
               }

               //* Construct the output sequence:                       *
               //* - One, two or three segments are to be written.      *
               //* - Any, (but not all) of the three segments may be    *
               //*   an empty string.                                   *
               //* - If an empty string is referenced by the formatting *
               //*   template, it will not affect the output. It just   *
               //*   means that we may unnecessarily write a pair of    *
               //*   escape sequences that do not enclose any text.     *
               //*             (Don't try this at home!)                *
               gsOut.compose( "%S%S%S%S%S",
                              txtBefore, modOn, txtSelect, modOff, txtAfter ) ;

               //* Write concatenated segments *
               this->acSetCursor ( wp ) ;
               this->ttyWrite ( gsOut ) ;
            }
         }  // selected text
      }     // row contains data

      ++wp.row ;                          // advance cursor position to next row

   }        // for(;;)

}  //* End refreshShiftedText() *

//*********************
//* drawFieldOutline  *
//*********************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Draw/redraw the border around the specified field.                           *
//* It is assumed that all parameters have been validated.                       *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*          fIndx: index of target field                                        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::drawFieldOutline ( const skForm* skfp, short fIndx )
{
   const skField* fptr = &skfp->fld[fIndx] ; // pointer to target field
   WinPos bpos( skfp->base, fptr->orig ) ;
   bpos.row -= 1 ;   bpos.col -= 1 ;
   short bhgt = fptr->hgt + 2,               // box height (rows)
         bwid = fptr->wid + 2 ;              // box width  (columns)

   //* Special Case: Radiobutton control with swapped field and label.*
   if ( (fptr->type == ftRB) && (fptr->lpos.col < fptr->orig.col) )
      bpos.col -= fptr->orig.col - fptr->lpos.col ;

   //* Draw the outline. *
   this->saveWrite = false ;                 // disable save to FIFO
   this->DrawBox ( bpos, bhgt, bwid, fptr->btype, fptr->battr ) ;
   this->saveWrite = true ;                  // re-enable save to FIFO

}  //* End drawFieldOutline() *

//*************************
//*     setCursor2IP      *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Set the visible cursor to the insertion point for field with focus.          *
//* This method assumes that fptr->cur (logical cursor position) is synchronized *
//* with fptr->ip (insertion point).                                             *
//*                                                                              *
//* Input  : skfp : pointer to skForm object.                                    *
//*                 skfp->fi indexes the field with focus.                       *
//*                 For window: skfp == this->skf                                *
//*                 For dialog: skfp == dlg->skf                                 *
//*                                                                              *
//* Returns: insertion point as a terminal-relative (1-based) offset             *
//********************************************************************************

void ACWin::setCursor2IP ( const skForm *skfp ) const
{
   const skField *fptr = &skfp->fld[skfp->fi] ;
   WinPos wp( this->txtBase, skfp->base, fptr->orig ) ;
   wp += fptr->cur ;
   this->acSetCursor ( wp ) ;

}  //* End setCursor2IP

//*************************
//*      init_skForm      *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Initialize the 'skForm' data members. (see notes below)                      *
//*                                                                              *
//* Input  : (optional, null pointer by default)                                 *
//*          If specified, initialize the custom skForm object.                  *
//*          Else create the default skForm object as described below.           *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes On Default Forms:                                                      *
//* -- If a custom form is specified, it is copied to the internal form object.  *
//*    the data are range checked and needed adjustments are applied.            *
//* -- If no custom form specified:                                              *
//*    -- Define a single text field using the position and dimensions           *
//*       of the window interior.                                                *
//*    -- The text for each defined field is initially an empty string.          *
//*    -- The 'ip' and 'cur' values for each field are set to zero i.e. they     *
//*       reference the field origin by default.                                 *
//*    -- The 'ins', 'insc', 'togg' and 'beep' flags and the 'icur'/'ocur'       *
//*       values of the skForm object use skForm defaults.                       *
//*                                                                              *
//* Notes On Initialization of 'skForm' object:                                  *
//* -------------------------------------------                                  *
//*  1) 'fCnt' member: must be initialized by the skForm constructor and not     *
//*            modified. This is the size of the array of skField objects in     *
//*           'fld' array.                                                       *
//*  2) 'fld' member is a pointer to an array of skField objects.                *
//*           See notes in init_skFields for more information.                   *
//*  2) 'fi' member is the 'focus index', that is, the index of the field which  *
//*          currently has the input focus.                                      *
//*          Value must be in the range: 0 <= fi < fCnt   (0 by default)         *
//*  3) 'base' member: initial value is ignored. 'base' is set to offset 0,0     *
//*            from the window's 'textBase' member (upper-left corner of window  *
//*            interior).                                                        *
//*  4) 'faca' member: Alternate color attributes for field/control with focus.  *
//*            If not specified (faca==acaUSEDFLT), then color attributes for    *
//*            field with focus are the "reverse" of non-focus attributes.       *
//*  5) 'seli' member: Index of the field containing "selected" text (if any).   *
//*            Initially there is not selected text, so 'seli' is set to 'fCnt'  *
//*            which indicate 'no selection'.                                    *
//*  6) 'pKey' member: Initial value ignored. Set to null character ('\0').      *
//*            Used internally to store the most recent keycode typed by user.   *
//*  7) 'icur' member: Cursor style when input is in 'Insert' mode.              *
//*  8) 'ocur' member: Cursor style when input is in 'Overstrike' mode.          *
//*  9) 'ins' flag: Set for 'Insert' mode, reset for 'Overstrike' mode.          *
//* 10) 'insc' flag: If set, cursor shape changes when Insert/Overstrike mode    *
//*            is toggled. If reset, cursor shape does not change.               *
//* 11) 'togg' flag: If set, Insert/Overstrike mode may be toggled by user.      *
//*            If reset, application controls Insert/Overstrike mode.            *
//* 12) 'beep; flag: If set, an audible alert will be produced when user inputs  *
//*            key data that is invalid in the current context. If reset, audible*
//*            alert is disabled.                                                *
//* 13) 'foot' flag: If set, user-friendly diagnostic messages i.e. "footnotes"  *
//*            are written in the bottom border of the window. This can be useful*
//*            during development. The application may define Footnote messages  *
//*            as desired. See the Footnote() method for details.                *
//*            If reset, footnotes are disabled, meaning that any diagnostic     *
//*            messages in the code will be ignored.                             *
//*            An example of a Footnote message is implemented for the           *
//*            "Paste data to the clipboard" command, alerting the user that     *
//*            an error has occured during a "paste" operation.                  *
//* 14) 'dlg' flag: reset. (set only by ACDlg constructor)                       *
//*                                                                              *
//* See skForm-class and skField-class documentation for more information, and   *
//* also the functional examples in the test file: AnsiCmdTest.cpp               *
//*                                                                              *
//********************************************************************************

void ACWin::init_skForm ( const skForm *formDef )
{
   #define DEBUG_skForm (0)      // for debugging only, report contents of 'skf'

   if ( this->skf != NULL )            // prevent memory leaks
      delete this->skf ;

   if ( formDef != NULL )              // custom form definition
   {
      this->skf = new skForm( formDef->fCnt ) ;
      *this->skf = formDef ;
      //* Note the odd syntax needed to make the "operator=" work *

      //* Offset from upper-left corner of window interior. *
      //* Note: This overrides the caller's value.          *
      this->skf->base = { ZERO, ZERO } ;
      this->skf->dlg = false ;         // Reset 'dlg' flag

      //* Range-check the window fields' data.*
      this->init_skFields () ;
   }
   else                                // default form definition
   {
      WinPos wp( 0, 0 ) ;              // offset into the text area
      short fieldCnt = 1,              // number of fields defined
            height   = this->txtRows ; // height (rows) for each field

      this->skf = new skForm ( fieldCnt ) ;  // create the skForm object
      this->skf->base = { ZERO, ZERO } ;     // set base position of the form

      //* Initialize the object's data members.*
      for ( short f = ZERO ; f < fieldCnt ; ++f, ++wp.row )
      {
         this->skf->fld[f].type = ftTB ;
         this->skf->fld[f].orig = wp ;
         this->skf->fld[f].hgt  = height ;
         this->skf->fld[f].wid  = this->txtCols ;
      }
      //* Note: Although we "know" that the parameters are *
      //* in range, we call the initialization method to   *
      //* initialize the color attributes.                 *
      this->init_skFields () ;
   }

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_skForm != 0
   if ( (ofsdbg.is_open()) )
   {
      if ( this->skf != NULL )
      {
         gsdbg.compose( "  skf: fCnt:%hd fi:%hd base:%hd/%hd ins:%hhd insc:%hhd "
                        "togg:%hhd icur:%hd, ocur:%hd beep:%hhd foot:%hhd ",
                        &this->skf->fCnt, &this->skf->fi,
                        &this->skf->base.row, &this->skf->base.col,
                        &this->skf->ins, &this->skf->insc, &this->skf->togg, 
                        &this->skf->icur, &this->skf->ocur, &this->skf->beep, 
                        &this->skf->foot ) ;
         ofsdbg << gsdbg.ustr() << endl ;
   
         for ( short f = ZERO ; f < this->skf->fCnt ; ++f )
         {
            gsdbg.compose( 
               "  %hd) orig:%-2hd/%-2hd hgt:%-2hd wid:%-2hd ip:%-2hd cur :%-2hd/%-2hd\n"
               "   off:%02hd shft:%02hd sela:%02hd selb:%02hd aca:%04Xh type:%02hd flow:%02hd\n"
               "   rbch:%04Xh grp:%04Xh hkey:%04Xh set:%hhd ro:%hhd\n"
               "   txt:",
                           &f, &this->skf->fld[f].orig.row, 
                           &this->skf->fld[f].orig.col,
                           &this->skf->fld[f].hgt,
                           &this->skf->fld[f].wid,
                           &this->skf->fld[f].ip,
                           &this->skf->fld[f].cur.row,
                           &this->skf->fld[f].cur.col,
                           &this->skf->fld[f].off,
                           &this->skf->fld[f].shft,
                           &this->skf->fld[f].sela,
                           &this->skf->fld[f].selb,
                           &this->skf->fld[f].aca.acaVal,
                           &this->skf->fld[f].type,
                           &this->skf->fld[f].flow,
                           &this->skf->fld[f].rbch,
                           &this->skf->fld[f].grp,
                           &this->skf->fld[f].hkey,
                           &this->skf->fld[f].set,
                           &this->skf->fld[f].ro ) ;

            if ( (this->skf->fld[f].txt.gschars()) > 1 )
               gsdbg.append( "'%S'", this->skf->fld[f].txt.gstr() ) ;
            else
               gsdbg.append( "(no text specified)" ) ;
            ofsdbg << gsdbg.ustr() << endl ;
         }
      }
   }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_skForm

   #undef DEBUG_skForm
}  //* End init_skForm() *

//*************************
//*     init_skFields     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Range-check the window fields' data, and adjust if necessary to fit          *
//* within the target fields.                                                    *
//*                                                                              *
//* Programmer's Note: It is possible that the caller COULD fool our simple      *
//* range checking with some truly stupid initialization value, but in that      *
//* case the caller would deserve the consequent overlapping of fields, field    *
//* extending beyond the text area, cursor referencing open space, etc.          *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: 'true'  if field(s) correct as specified                            *
//*          'false' if parameters modified to fit within the window             *
//********************************************************************************
//* Notes On Initialization of each 'skField' object:                            *
//* -------------------------------------------------                            *
//* For each skField object in the skForm 'fld' array, the application layer     *
//* may initialize the following members during window definition:               *
//* See documentation for further details.                                       *
//*                                                                              *
//*  1) 'type' member:                                                           *
//*         The field type (enum FldType) with a default of ftTB (text box).     *
//*         Additional field types available:                                    *
//*         - ftPB : Pushbutton                                                  *
//*         - ftRB : Radiobutton                                                 *
//*         - ftMB : Menubox                                                     *
//*         - ftSB : Scrollbox                                                   *
//*         - ftSP : Spinner  [NOT YET IMPLEMENTED, SEE BELOW]                   *
//*            Additional field types will be added as time permits.             *
//*  2) 'hgt' member:                                                            *
//*         field height in the range: 1 <= hgt <= text_area_rows                *
//*  3) 'wid' member:                                                            *
//*         field width in the range: 1 <= wid <= text_area_columns              *
//*  4) 'orig' member:                                                           *
//          zero-based vertical and horizontal OFFSET into the text area of the  *
//*         window, for the upper-left corner of the control.                    *
//*  5) 'txt' member:                                                            *
//*         Display text for the field. Any sequence of graphing (printing)      *
//*         characters plus newlines ('\n') is allowed. This is a gString text   *
//*         formatting-and-analysis object. It may contain up to gsALLOCMAX      *
//*         characters, although the number of characters actually displayed at  *
//*         any time is limited by the dimensions of the field.                  *
//*         - For most control types, this is the display text.                  *
//*         - For some control types, the text data are interpreted as labels    *
//*           or other configuration specifications.                             *
//*  6) 'ip' member:                                                             *
//*         Insertion Point: 0 <= ip <= text_characters in 'txt', excluding      *
//*         the null terminator (default vaule is zero). This member is          *
//*         referenced only for text-input fields (ftTB), and is under program   *
//*         control for other types of fields.                                   *
//*         a) 'cur' member (cursor position): Although this member is declared  *
//*            as public for technical reasons, the actual value is calculated   *
//*            directly from the 'ip' member.                                    *
//*  7) 'aca' member:                                                            *
//*         Color attributes for the field. Note that this is a complex data     *
//*         object. Only the 'acaVal' member of the object may be specified      *
//*         directly. The bitfield members are calculated internally.            *
//*         'aca.acaVal' is set to acaATTR_DFLT when the skField object is       *
//*         created which indicates that the field is to use the color attributes*
//*         specified for the interior of the parent window (win->txtAttr).      *
//*         Any valid attribute(s) (enum acAttr) may be specified. For example,  *
//*         blue-on-cyan:    aca.acaVal = acAttr(acaFG_BLUE | acaBG_CYAN);       *
//*  8) 'flow' member:                                                           *
//*         Member of enum FloType. For text fields, three automatic             *
//*         text-formatting options are available:                                               *
//*         1) floShift : Fixed line format. Field text is shifted left or right *
//*                       as necessary to bring the cursor into visible space.   *
//*         2) floChar  : Dumb word wrap: wrap each line of text at the edge of  *
//*                       the field. (break at character)                        *
//*         3) floToken : Smart word wrap: word wrap at whitespace character     *
//*            floHyphen: Same as floToken with the addition of potential break  *
//*                       at any of the hyphen-like characters.                  *
//*         In general, the application should format the text in the 'txt'      *
//*         member; however, if necessary, the text will be automatically        *
//*         reformatted to fit within the field.                                 *
//*  9) 'bdr' member: field border. If set, a border will be drawn around the    *
//*         control. This will be is the default, single-line border using the   *
//*         color attributes of the window interior. For greater control of      *
//*         border configuration, specify the border _after_ instantiation.      *
//*         -- Note that Menu controls have integrated borders, so this flag     *
//*            will be ignored if 'type' == ftMB.                                *
//* 10) 'hide' member: may be set, indicating that the field is initially        *
//*         invisible and cannot receive the input focus. This is useful if      *
//*         fields overlap or if the operation of that field is not valid in     *
//*         a given context. To make the field visible, reset the flag.          *
//*         The field can then receive the the input focus. Setting the flag     *
//*         will cause the field to become invisible again.                      *
//* 11) 'ro' member: may be set, indicating that the field is read-only and      *
//*          cannot receive input focus; or reset i.e. field may be accessed     *
//*          by the user.                                                        *
//*                                                                              *
//* Parameters specific to certain control types:                                *
//* - - - - - - - - - - - - - - - - - - - - - - -                                *
//* 12)  'rbch' member:                                                          *
//*         For Radiobutton fields (ftRB) only, specify the character which      *
//*         indicates that the control is "set".                                 *
//*          Example, reset: [ ]  a space is visible between braces              *
//*          Example, set:   [♦]  the 'rbch' char is visible between braces      *
//*         The default character is a diamond, as shown, but any graphing       *
//*         character may be specified.                                          *
//* 13)  'hkey' member:                                                          *
//*         Direct specification of "hotkey" keycodes for accessing individual   *
//*         user interface controls.                                             *
//*         -- Menubox controls (ftMB) do not have labels; therefore, if a       *
//*            hotkey is desired for a menu, it must be specified directly by    *
//*            initializing the 'hkey' member of the control's skField object.   *
//*            The hotkey may be any of the defined GUI keys (wcsALT_A, etc.) or *
//*            Function keys (wcsF02, etc.). The specified keycode should be     *
//*            unique among the defined fields. Example: Set the hotkey for a    *
//*            menu to the keycode ALT+F :                                       *
//*               skfp->fld[f].type = ftMB ;                                     *
//*               skfp->fld[f].hkey = wcsALT_F ;                                 *
//*         -- Pushbutton (ftPB) and Radiobutton (ftRB) controls are a special   *
//*            case in that the text of the control _is_ the label, and the      *
//*            hotkey (if desired) is specified by embedding it in the field     *
//*            text. For example, if the text of a Pushbutton is "Grapefruit",   *
//*            the hotkey for that control may be specified by placing a         *
//*            caret '^' before the desired hotkey as shown:                     *
//*               skfp->fld[f].type = ftPB ;                                     *
//*               skfp->fld[f].txt  = "Gra^pefruit" ;                            *
//*            The hotkey for the control will be wcsALT_P (ALT+p or ALT+P).     *
//*         -- For all other control types, labels are optional and if specified,*
//*            may contain a hotkey designation. These labels are specified      *
//*            after the window has been instantiated, but before the window is  *
//*            opened (made visible). Example:                                   *
//*               skfp->fld[f].type = ftTB ;                                     *
//*               ACWin* win = new ACWin( . . . );                               *
//*               win->SetFieldLabel ( labelIndex, "^Address Line 1" );          *
//*               win->OpenWindow ();                                            *
//*            This sets the Textbox field label to "Address Line 1", with the   *
//*            'A' underlined to indicate that the hotkey is wcsALT_A            *
//*            (ALT+a or ALT+A).                                                 *
//*            -- Note that while it is possible to use the 'hkey' member to     *
//*               directly specify a hotkey for these control types, it is not   *
//*               recommended because there will be no visual cue for indicating *
//*               to the user what that hotkey is.                               *
//* 14)  'state' member: For binary state controls, (Radiobuttons), the state    *
//*            of the control, either "set" (1) or "reset" (0).                  *
//* 15)  'grp' member: For controls which may be members of an interconnected    *
//*           "group" (Radiobuttons and Menubox controls), initialize the        *
//*           'grp' member of each control to the same ASCII character.          *
//*           Example: For three Radiobutton characters which are configured as  *
//*           an XOR group, each one would be assigned the same character        *
//*           e.g. grp = L'g';                                                   *
//* 16) 'lock' member:                                                           *
//*     For Menubox controls (ftMB) controls only. By default, menus are only    *
//*     visible when they have input focus. However, menus can optionally        *
//*     be "locked" in the visible state. See also the 'hide' flag, above.       *
//*  --------                                                                    *
//* All other data members of the skField class are "protected" and are not      *
//* available for initialization at the application level.                       *
//*                                                                              *
//* Notes On Initialization of 'skField' array:                                  *
//* -------------------------------------------                                  *
//* -- Fields must lie completely within the text area of the window.            *
//* -- Fields may overlap each other IF the application intelligently controls   *
//*    the field with focus and which fields are simultaneously visible.         *
//* -- By default, the window interior is a single ftTB field which includes     *
//*    the entire interior area of the window.                                   *
//*    Example: skForm f( 1 );                                                   *
//*             f.fld[0].hgt = ROWS_IN_TEXT_AREA ;                               *
//*             f.fld[0].wid = COLS_IN_TEXT_AREA ;                               *
//*             f.fld[0].orig = ( 0, 0 ) ;                                       *
//*             (Field text ('txt') is initially an empty string, "".)           *
//*    Example: one text-box field for each text row of the window               *
//*             (text rows == n). Optionally, initialize text for each field.    *
//*    Example: skForm f( n );                                                   *
//*             f.fld[0].type = ftTB ;                                           *
//*             f.fld[0].hgt = 1 ;                                               *
//*             f.fld[0].wid = COLS_IN_TEXT_AREA ;                               *
//*             f.fld[0].orig = { 0, 0 } ;                                       *
//*             f.fld[0].txt = "" ; (initially an empty string)                  *
//*             . . . .                                                          *
//*             f.fld[n - 1].type = ftTB ;                                       *
//*             f.fld[n - 1].hgt = 1 ;                                           *
//*             f.fld[n - 1].wid = COLS_IN_TEXT_AREA ;                           *
//*             f.fld[n - 1].orig = { 2, 2 } ;                                   *
//*             f.fld[0].txt = "" ; (initially an empty string)                  *
//* -- Define a custom number of fields and field position/dimensions for each.  *
//*    Example: skForm f( 3 );                                                   *
//*             f.fld[0].type = ftTB ;                                           *
//*             f.fld[0].hgt = 1 ;                                               *
//*             f.fld[0].wid = COLS_IN_TEXT_AREA - 2 ;                           *
//*             f.fld[0].orig = { 1, 1 } ;                                       *
//*             f.fld[0].type = ftTB ;                                           *
//*             f.fld[0].txt = "Text for field A." ;                             *
//*             f.fld[0].ip = 5 ;                                                *
//*                                                                              *
//*             f.fld[1].type = ftTB ;                                           *
//*             f.fld[1].hgt = 6 ;                                               *
//*             f.fld[1].wid = COLS_IN_TEXT_AREA - 2 ;                           *
//*             f.fld[1].orig = { 3, 4 } ;                                       *
//*             f.fld[1].txt = "Text for field B.\nAdditional text." ;           *
//*             f.fld[1].ip = 0 ;                                                *
//*             . . . .                                                          *
//*             f.fld[n].type = ftPB ;  // (Pushbutton)                          *
//*             f.fld[n].hgt = 1 ;                                               *
//*             f.fld[n].wid = 8 ;                                               *
//*             f.fld[n].orig = { win_height - 3, win_width / 2 } ;              *
//*             f.fld[n].txt = "  Exit  " ;                                      *
//*                                                                              *
//* See skForm-class and skField-class documentation for more information, and   *
//* also the functional examples in the test file: AnsiCmdTest.cpp               *
//*                                                                              *
//********************************************************************************

bool ACWin::init_skFields ( void )
{
   #define DEBUG_IMF (0)                  // for debugging only

   skField *fptr ;                        // pointer to field under test
   gString gs ;                           // text analysis
   int ni ;                               // index of null terminator
   bool status = true ;                   // return value

   //* For controls which may have hotkeys, initialize *
   //* the 'hkey' member and adjust label formatting.  *
   this->init_Hotkeys ( this->skf ) ;

   //* Range check offsets, dimensions and text width.*
   for ( short f = ZERO ; f < this->skf->fCnt ; ++f )
   {
      fptr = &this->skf->fld[f] ;         // pointer to the field definition

      //* Validate field type. *
      if ( !((fptr->type == ftTB) || (fptr->type == ftPB) || (fptr->type == ftRB) || 
             (fptr->type == ftMB) || (fptr->type == ftSB)) )
      { fptr->type = ftTB ; }

      //* These control types are always single-row fields. *
      //* Note: Pushbutton and Radiobutton width and IP are *
      //* validated by call to init_Hotkeys(), above.       *
      if ( (fptr->hgt != 1) && ((fptr->type == ftPB) || (fptr->type == ftRB)) )
      { fptr->hgt = 1 ; status = false ; }

      //* Gross range check of positioning *
      if ( (fptr->orig.row < ZERO) || (fptr->orig.row >= this->txtRows) )
      { fptr->orig.row = ZERO ; status = false ; }
      if ( (fptr->orig.col < ZERO) || (fptr->orig.col >= this->txtCols) )
      { fptr->orig.col = ZERO ; status = false ; }

      //* Gross range check of dimensions *
      if ( fptr->hgt < 1 ) { fptr->hgt = 1 ; status = false ; }
      else if ( (fptr->orig.row + fptr->hgt) > this->txtRows )
      {
         fptr->orig.row = ZERO ;
         if ( fptr->hgt > this->txtRows )
            fptr->hgt = this->txtRows ;
         status = false ;
      }
      if ( fptr->wid < 1 ) { fptr->wid = 1 ; status = false ; }
      else if ( (fptr->orig.col + fptr->wid) > this->txtCols )
      {
         fptr->orig.col = ZERO ;
         if ( fptr->wid > this->txtCols )
            fptr->wid = this->txtCols ;
         status = false ;
      }

      //* Validate IP, text offset and text shift. Special-case *
      //* initializaiton for these values is handled below.     *
      ni = fptr->txt.gschars() - 1 ;      // index of null terminator
      if ( fptr->ip < ZERO )    { fptr->ip   = ZERO ; status = false ; }
      if ( fptr->ip > ni )      { fptr->ip   = ni ;   status = false ; }
      if ( fptr->off != ZERO )  { fptr->off  = ZERO ; status = false ; }
      if ( fptr->shft != ZERO ) { fptr->shft = ZERO ; status = false ; }

      //* If default attribute specified for Fgnd or Bgnd,     *
      //* substitute the window interior attribute(s).         *
      //* Note that attributes may be adjusted below according *
      //* to field type.                                       *
      //* Programmer's Note: If user actually _wants_ terminal *
      //* default attributes for the field, they may be        *
      //* surprised that window attributes were used instead.  *
      if( fptr->aca.acaVal == acaUSEDFLT ) { fptr->aca.acaVal = acaATTR_DFLT ; }
      if ( (fptr->aca.acaVal & acaFG_DFLT) || (fptr->aca.acaVal & acaBG_DFLT) )
      {
         acAttr fgndAttr, bgndAttr ;      // separate fg/bg bitfields
         if ( fptr->aca.acaVal & acaFG_DFLT )
            fgndAttr = acAttr(this->txtAttr.acaVal & acaFG_MASK) ;
         else
            fgndAttr = acAttr(fptr->aca.acaVal & acaFG_MASK) ;
         if ( fptr->aca.acaVal & acaBG_DFLT )
            bgndAttr = acAttr(this->txtAttr.acaVal & acaBG_MASK) ;
         else
            bgndAttr = acAttr(fptr->aca.acaVal & acaBG_MASK) ;
         fptr->aca.acaVal = acAttr(fgndAttr | bgndAttr) ;
      }
      fptr->aca.decode( fptr->aca.acaVal ) ;

      //***************************************
      //* Validate data for Textbox controls. *
      //***************************************
      if ( fptr->type == ftTB )
      {
         //* Validate the auto-formatting option.        *
         //* Single-row fields are always floShift.      *
         //* Multi-row fields may be any valid format.   *
         if ( fptr->hgt == 1 )
            fptr->flow = floShift ;              
         else if ( fptr->flow == floDflt )
            fptr->flow = floChar ;

         //* Ensure that the 'state' flag is reset. *
         //* See notes in hilightFieldText().       *
         fptr->state = false ;

         //* If the text data are not properly formatted *
         //* to fit within the field, reformat the data. *
         //* Range check the index into the text data,   *
         //* and initialize the cursor offset.           *
         if ( (this->formatFieldText ( this->skf, f )) )
         {
            //* Field IP and/or the cursor position is outside visible *
            //* space. Set IP to first column of current row.          *
            this->skf->fld[f].ip = 
                     this->skf->fld[f].mrp.roff[this->skf->fld[f].mrp.rcur] ;
            this->parseFieldText ( &this->skf->fld[f], 1 ) ;
         }
      }  // (ftTB)

      //*****************************************
      //* Validate data for Scrollbox controls. *
      //*****************************************
      else if ( fptr->type == ftSB )
      {
         //* The text-flow option must be one of *
         //* the token-oriented options.         *
         if ( (fptr->flow != floToken) && (fptr->flow != floHyphen) )
            fptr->flow = floToken ;

         fptr->ip = ZERO ;                // initialize the IP

         //* Scrollbox controls can't be members of a group.*
         fptr->grp = noGROUP ;

         //* Validate formatting of field text.     *
         //* Note that if text is not pre-formatted *
         //* to fit within the control, it will be  *
         //* truncated horizontally.                *
         if ( !(this->init_Scrollbox ( fptr )) )
            status = false ;
      }  // (ftSB)

      #if DEBUG_SPINNER != 0
      //*****************************************
      //* Validate data for Spinner controls.   *
      //*****************************************
      else if ( fptr->type == ftSP )
      {
         if ( !(this->init_Spinner ( fptr )) )
            status = false ;
      }  // (ftSP)
      #endif   // DEBUG_SPINNER

      //***************************************
      //* Validate data for Menubox controls. *
      //***************************************
      else if ( fptr->type == ftMB )
      {
         //* If menu has one row, i.e. a "menubar",  *
         //* add the underline modifier.             *
         if ( fptr->hgt == 1 )
         {
            fptr->aca.decode( acAttr(fptr->aca.acaVal | acaUNDERLINE) ) ;

            //* Single-row menu (menubar) must have a group designator, *
            //* and must be part of a group. Report the error.          *
            if ( fptr->grp == noGROUP )
            { fptr->txt = " Error! Menubar has no group designator." ; }
         }
         else                             // multi-row fields
         {
            //* Group sub-menus must not be "locked" in the *
            //* visible state. Menubars and context menus   *
            //* may be locked visible if desired.           *
            if ( fptr->grp != noGROUP )
               fptr->lock = false ;
            // Programmer's Note: We would prefer to validate the group members:
            // i.e. a menubar followed by one or more sub-menus. Think about this...
         }

         //* 'flow' is set to floShift, but note that text  *
         //* shift should never happen within menus because *
         //* horizontal scrolling is disabled.              *
         fptr->flow = floShift ;

         //* Validate formatting of menu text.              *
         //* Note that if text is not pre-formatted to fit  *
         //* within the control, it will be truncated       *
         //* both horizontally and vertically. Set IP and   *
         //* cursor position to reference first menu item.  *
         if ( !(this->init_Menu ( fptr )) )
            status = false ;
      }  // (ftMB)

      //* Validate data for other control types: ftPB, ftRB.  *
      else
      {
         //* 'flow' is set to floShift, but note that text in *
         //* these fields is static, and is not modified      *
         //* under user control; therefore, 'flow' is ignored.*
         fptr->flow = floShift ;

         //* Initialize indexing and logical cursor position. *
         //* Note that for Pushbuttons and Radiobuttons, the  *
         //* parsing data will probably never be referenced,  *
         //* but this call also sets logical cursor position. *
         this->parseFieldText ( fptr ) ;
      }  // (ftPB && ftRB)

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FDUMP != 0 && DEBUG_IMF != 0
      //* Report initial field configuration *
      gString gsfdump( "skField%02hd", &f ) ;
      this->dumpField2Log ( fptr, gsfdump.gstr(), true ) ;
      #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FDUMP && DEBUG_IMF
   }  // for(;;)
   return status ;

   #undef DEBUG_IMF
}  //* End init_skFields() *

//*************************
//*        skForm         *
//*************************
//********************************************************************************
//* Initialization constructor.                                                  *
//* This constructor fully initializes the data members AND transfers the data   *
//* from caller's skField array to the internal skField array.                   *
//*                                                                              *
//* Note that range checking and error correction are not performed by this      *
//* constructor. Any needed corrections are made during instantiation of the     *
//* parent window so that the values may be compared to the window definition.   *
//*                                                                              *
//*                                                                              *
//* Input  : fldCount: number of objects in 'fldArray'                           *
//*          fldArray: an array of skField objects defining the form             *
//*          ** Optional Arguments **                                            *
//*          fldIndex: (optional, default: zero)                                 *
//*                    index of the field which receives initial input focus     *
//*          fldAttr : (optional, default: acaUSEDFLT)                           *
//*                    For fields which change color when they receive input     *
//*                    focus, this is the focus color attribute.                 *
//*                    -- acaUSEDFLT: If default attributes specified, then when *
//*                       field receives input focus, the field's embedded       *
//*                       foreground and background are swapped.                 *
//*                    -- If focus-color attributes are specified, then when     *
//*                       field receives input focus, this attribute overrides   *
//*                       the field's embedded attributes for as long as the     *
//*                       field holds focus, at which time the embedded          *
//*                       attributes are restored.                               *
//*          noisy   : (optional, default: true)                                 *
//*                    if 'true',  beep on unhandled keycode received            *
//*                    if 'false', silently discard the unhandled keycode        *
//*          ioFlag  : (optional, default: set)                                  *
//*                    initial state of Insert/Overstrike flag                   *
//*                    set  : Insert mode                                        *
//*                    reset: Overstrike mode                                    *
//*          ioToggle: (optional, default: true)                                 *
//*                    if 'true',  user may toggle between Insert mode and       *
//*                                Overstrike mode                               *
//*                    if 'false', mode is fixed, and cannot be modified by user *
//*          ioCursor: (optional, default: false)                                *
//*                    if 'true', shape of cursor changes to indicate current    *
//*                               state of Insert/Overstrike flag                *
//*                    if 'false', cursor shape is not modified for Ins/Ovr mode *
//*          insCursor: (optional, default: csBblock)                            *
//*                     member of enum CurStyle indicating the cursor shape to   *
//*                     be used when in Insert mode.                             *
//*                     (If 'ioCursor' flag is reset, then )                     *
//*                     (only this cursor style is used.   )                     *
//*          ovrCursor: (optional, default: csBuline)                            *
//*                     member of enum CurStyle indicating the cursor shape to   *
//*                     be used when in Overstrike mode.                         *
//*                                                                              *
//*                                                                              *
//* Returns: constructors _implicitly_ return a pointer to the object            *
//********************************************************************************
                                                  
skForm::skForm ( short fldCount, skField* fldArray, 
                 short fldIndex, acAttr fldAttr, bool  noisy, 
                 bool  ioFlag, bool  ioToggle, bool  ioCursor, 
                 CurStyle insCursor, CurStyle ovrCursor )
{
   if ( (fldCount < 1) || (fldCount > 99) ) { fldCount = 99 ; }
   this->reset () ;              // set data members to defaults
   this->fCnt = fldCount ;       // specify number of fields
   this->seli = this->fCnt ;     // no selected text
   if ( fldArray == NULL )       // defensive programming (should not happen)
      this->fld = new skField[this->fCnt] ;
   else                          // point to caller's array
      this->fld  = fldArray ;
   this->fi   = fldIndex ;
   this->faca.acaVal = fldAttr ; // color attributes
   this->beep = noisy ;          // audible alert
   this->ins  = ioFlag ;         // Insert/Overstrike state
   this->togg = ioToggle ;       // enabledisable user access to Insert/Overstrike
   this->insc = ioCursor ;       // cursor style change enable/disable
   if ( this->insc )
   {
      this->icur = insCursor ;   // Insert mode cursor style
      this->ocur = ovrCursor ;   // Overstrike mode cursor style
   }
}  // skForm (constructor)

//*************************
//*        skField        *
//*************************
//********************************************************************************
//* Initialization constructor.                                                  *
//* The first five(5) arguments are required. The remaining arguments are        *
//* optional and if not specified, will receive default values.                  *
//*                                                                              *
//*                                                                              *
//* Input  : fldType    : field type, member of enum FldType                     *
//*          fldHeight  : field height in rows                                   *
//*          fldWidth   : field width in columns                                 *
//*          fldOffsetY : vertical offset from parent window/dialog origin       *
//*          fldOffsetX : horizontal offset from parent window/dialog origin     *
//*          fldText    : (optional, default: an empty string "\0")              *
//*                       Initial text data. Data configuration is dependent     *
//*                       on the field type. See documentation for details.      *
//*          fldAttr    : (optional, default: acaATTR_DFLT)                      *
//*                       color attribute for field                              *
//*                       Note that for field types which change color when they *
//*                       receive input focus, this is the non-focus color.      *
//*          fldFlow    : (optional, default: floDflt) text flow                 *
//*                       Automatic text formatting option for the field.        *
//*                       Formatting option is dependent on field type, and is   *
//*                       fully configurable only for Textbox controls (ftTB).   *
//*                       See documentation for details.                         *
//*          insPoint   : (optional, default: zero) insertion point              *
//*                       The cursor offset from head of text data.              *
//*          readOnly   : (optional, default: false)                             *
//*                       if set,  field is view only, cannot receive focus      *
//*          fldBorder  : (optional, default: false)                             *
//*                       if false, field has no border.                         *
//*                          Exception: Menubox controls always have a border.   *
//*                       if true,  draw an outline around the field             *
//*                          Note: Default line type and color attribute will be *
//*                                used to draw the outline. For a non-default   *
//*                                line type or attribute, see FieldOutline().   *
//*          fldHidden  : (optional, default: false)                             *
//*                       if set, field is not displayed and cannot receive focus*
//*          fldLocked  : (optional, default: true)                              *
//*                       if set, field is always visible (unless obscured by    *
//*                               another field.)                                *
//*                       if reset, field becomes invisible when it loses focus. *
//*                               (This is primarily for menus.)                 *
//*          binState   : (optional, default: false)                             *
//*                       For binary-state controls only (Radiobuttons),         *
//*                       specifies the initial state: "set" or "reset".         *
//*          hotKey     : (optional, default: null L'\0')                        *
//*                       if specified, this is the hotkey character for setting *
//*                       focus to field. Please see documentation about         *
//*                       specification and use of hotkeys.                      *
//*          groupId    : (optional, default: noGROUP)                           *
//*                       For fields whih can be grouped(Menubox and Radiobutton)*
//*                       this is the character used to identify the members     *
//*                       of the group.                                          *
//*                                                                              *
//* Returns: constructors _implicitly_ return a pointer to the object            *
//********************************************************************************
                                                  
skField::skField ( FldType fldType, short fldHeight, short fldWidth,
                   short fldOffsetY, short fldOffsetX, const char* fldText, 
                   acAttr fldAttr, FloType fldFlow, int insPoint,
                   bool readOnly, bool fldBorder, bool fldHidden,
                   bool fldLocked, bool binState, 
                   wchar_t hotKey, wchar_t groupId )
{
   this->reset () ;           // set all data members to default values

   this->type  = fldType ;
   this->hgt   = fldHeight ;
   this->wid   = fldWidth ;
   this->orig  = { fldOffsetY, fldOffsetX } ;
   if ( fldText != NULL )
      this->txt = fldText ;
   this->aca.acaVal = fldAttr ;
   this->flow  = fldFlow ;
   this->ip    = insPoint ;
   this->ro    = readOnly ;
   this->bdr   = fldBorder ;
   this->hide  = fldHidden ;
   this->lock  = fldLocked ;
   this->state = binState ;
   this->hkey  = hotKey ;
   this->grp   = groupId ;

}  // skField()

//*************************
//*     outlineConfig     *
//*************************
//********************************************************************************
//* Set the line type and attribute for the field outline.                       *
//*                                                                              *
//* The data members which contain these values are 'protected', that is, they   *
//* are not available at the application level. This skField public method is    *
//* provided so these values can be initialized before the window or dialog is   *
//* instantiated. The configured fields will be displayed when the window(dialog)*
//* is opened.                                                                   *
//*                                                                              *
//* a) Note that this method should be called ONLY before the window (or dialog) *
//*    constructor is called. To initialize the border _after_ the window/dialog *
//*    is created, call the ACWin-class SetFieldOutline() instead.               *
//* b) Field outlines are incompatible for Menu controls (ftMB) because menus    *
//*    have integrated borders.                                                  *
//* c) Because this call does not reference the parent window(dialog), error     *
//*    checking of positioning or interference with the border and with other    *
//*    fields cannot be performed here. For this reason, use caution when        *
//*    initiating these values.                                                  *
//* d) This method is intended primarily for initializing the skForm/skField     *
//*    object to be used as a parameter when invoking a full-initialization      *
//*    dialog; however, it is possible to use this method when initializing the  *
//*    skForm/skField object to be used as a parameter when defining an ACWin    *
//*    object. In this case, use extra care in positioning the specified control.*
//*                                                                              *
//*                                                                              *
//* Input  : lType   : line type for border, member of enum LineType             *
//*                    ltSINGLE (default), ltDUAL, ltHORIZ (invisible border)    *
//*          lAttr   : (optional, default: acaUSEDFLT) border color attribute    *
//*                    if not specified, takes attribute of window interior      *
//*                    -- Note that this parameter does not apply to menu        *
//*                       controls which take their outline color from the       *
//*                       menu data.                                             *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if field type does not allow borders                        *
//********************************************************************************

bool skField::outlineConfig ( LineType lType, acAttr lAttr )
{
   bool status = false ;

   if ( (this->type != ftMB) || ((this->type == ftMB) && (this->grp == noGROUP)) )
   {
      if ( (lType != ltDUAL) && (lType != ltHORIZ) )  // validate line type
         lType = ltSINGLE ;
      this->btype = lType ;               // set the line type
      if ( this->type != ftMB )           // if not a menu control
      {
         this->battr = lAttr ;            // set the color attributes
         this->bdr = true ;               // set the border flag
      }
      status = true ;
   }
   return status ;

}  //* End outlineConfig() *

//*************************
//*      labelConfig      *
//*************************
//********************************************************************************
//* Specify label text, color and position.                                      *
//*                                                                              *
//* The data members which contain these values are 'protected', that is, they   *
//* are not available at the application level. This skField public method is    *
//* provided so these values can be initialized before the window or dialog is   *
//* instantiated. The configured fields will be displayed when the window(dialog)*
//* is opened.                                                                   *
//*                                                                              *
//* Note that this method should be called ONLY before the window (or dialog)    *
//* constructor is called. To initialize the border _after_ the window/dialog    *
//* is created, call the ACWin-class SetFieldLabel() instead.                    *
//*                                                                              *
//* The following control types will accept labels:                              *
//*  1) ftTB - Textbox                                                           *
//*  2) ftSB - Scrollbox                                                         *
//*  3) ftMB - Menu                                                              *
//*     Note that only certain menus may have labels.                            *
//*      a) Context menus i.e. multi-row menus which are not group members.      *
//*      b) Menubar menus i.e. single-row menus which anchor a group             *
//*         of menu controls.                                                    *
//*  External labels are incompatible for Pushbuttons (ftPB) and Radiobuttons    *
//*  (ftRB) because the field text _is_ the label.                               *
//*                                                                              *
//*                                                                              *
//* Input  : labText : display text, optionally including a "hotkey" character.  *
//*                    A hotkey is specified using the CARET character ('^')     *
//*                    followed by the ASCII character which indicates the       *
//*                    keycode to be used as a hotkey. (see example above)       *
//*          labAttr : (optional, default: acaUSEDFLT) color attribute for label *
//*                    If not specified label color is set to color of parent    *
//*                    window interior                                           *
//*          center  : (optional, default:'false') horizontal offset             *
//*                    if set, center the label horizontally above the field     *
//*                    if reset, label offset is in the same column as the       *
//*                            left edge of the field                            *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if field type does not allow external label                 *
//********************************************************************************

bool skField::labelConfig ( const char* labText, acAttr labAttr, bool center )
{
   bool status = false ;

   if ( (this->type == ftTB) || (this->type == ftSB) || 
        ((this->type == ftMB) && ((this->grp == noGROUP) || (this->hgt == 1))) )
   {
      if ( (labText != NULL) && (*labText != NULLCHAR) )
      {
         gString gs( labText ) ;                   // load the raw text data
         gs.erase( NEWLINE, ZERO, false, true ) ;  // remove any newlines
         gs.copy( this->lab, labelLEN ) ;          // save the text
      }
      this->lattr = labAttr ;                      // save label attributes

      //* If centered label is specified, we don't yet know where *
      //* the center is, so setting lpos.col == -1, signals that  *
      //* the label should be centered when decoding the hotkey.  *
      if ( center ) { this->lpos = { -1, -1 } ; }
      status = true ;
   }
   return status ;

}  //* End labelConfig() *

//*************************
//*      labelConfig      *
//*************************
//********************************************************************************
//* Specify label color attributes  and position.                                *
//*                                                                              *
//* This  overload is designed for Radiobutton controls (ftRB) only.             *
//* To initialize labels for other types of controls, see above.                 *
//*                                                                              *
//* By default Radiobutton labels inherit the color attributes of the parent     *
//* window/dialog _and_ the label is positioned to the right of the field.       *
//* This method allows direct specification of the label attributes and          *
//* optionally allows the label to be positioned to the left of the control.     *
//*                                                                              *
//* The data members which contain these values are 'protected', that is, they   *
//* are not available at the application level. This skField public method is    *
//* provided so these values can be initialized before the window or dialog is   *
//* instantiated. The configured fields will be displayed when the window(dialog)*
//* is opened.                                                                   *
//*                                                                              *
//* Note that this method should be called ONLY before the window (or dialog)    *
//* constructor is called.                                                       *
//*                                                                              *
//* Input  : labAttr : color attribute for Radiobutton label                     *
//*                    If acaUSEDFLT specified, then label color is set to color *
//*                    of parent window interior.                                *
//*          lableft : (optional, default:'false') horizontal offset             *
//*                    if set, label is displayed to the left of the control     *
//*                    if reset, label is displayed to the right of the control  *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if incorrect field type                                     *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* If field/label position swap specified ('lableft'), 





//* the 'lpos.col' member    *
//* is set to a negative value which signals the downstream code to make the     *
//* swap after field position have been verified. See init_Hotkeys() for details.*
//*                                                                              *
//* Minor formatting changes to the text are performed here; however,            *
//*                                                                              *
//********************************************************************************

bool skField::labelConfig ( acAttr labAttr, bool lableft )
{
   bool status = false ;

   if ( this->type == ftRB )
   {
      this->lattr = labAttr ;                      // save label attributes

      if ( lableft )
      {
         this->lpos.col = -1 ;            // signal position swap
         //* Adjust label formatting *
         gString gs( this->txt ) ;
         if ( gs.gstr()[3] == SPACE )
         { gs.erase( 3, 1 ) ; --this->wid ; }
         if ( gs.gstr()[gs.gschars() - 2] != SPACE )
         { gs.append( SPACE ) ; ++this->wid ; }
         this->txt = gs ;
      }
      status = true ;
   }
   return status ;

}  //* End labelConfig() *

//*************************
//*      colorConfig      *
//*************************
//********************************************************************************
//* Specify an array of color attributes, one for each item (row) displayed      *
//* in the control.                                                              *
//*                                                                              *
//* The data member which contains these color values is 'protected', that is,   *
//* it is not available at the application level. This skField public method is  *
//* provided so that custom color attributes may be specified, one attribute     *
//* per text row, _after_ the skField object has been instantiated, but _before_ *
//* the window (or dialog) constructor is called.                                *
//* The reonfigured field will be displayed when the window(dialog) is opened.   *
//*                                                                              *
//* The following control types support custom color attributes:                 *
//* These control types are designed to contain read-only data; the data may     *
//* may not be edited under user control, so it is possible to embed color       *
//* attributes within the displayed text.                                        *
//* Note that the specified attributes apply _only_ to the text displayed within *
//* the control, and _not_ to the borders (if any) drawn around the control.     *
//*                                                                              *
//*  1) ftMB - Menu                                                              *
//*     - All items within a menu are visible simultaneously whenever the menu   *
//*       itself is visible. Therefore one custom color must be specified        *
//*       for each item EXCLUDING the upper/lower borders.                       *
//*     - Multi-row (multi-item) menus support custom colors. A single-row menu  *
//*       (menu bar) is excluded, because it has only one row, and therefore     *
//*       only one color.                                                        *
//*                                                                              *
//*  2) ftSB - Scrollbox                                                         *
//*     - Scrollbox controls may contain more text items that are visible at     *
//*       any moment; therefore, a custom color must be specified for each       *
//*       item (row) whether it is visible or not.                               *
//*       To describe it another way, each custom color attribute is associated  *
//*       with specific text, and not with a particular row within the display   *
//*       area.                                                                  *
//*                                                                              *
//* Any valid color attribute bitfield may be specified.                         *
//* -- The acUSEDFLT value indicates that the target item will use the attribute *
//*    specified in the skField's 'aca' member. This is the attribute for the    *
//*    control as a whole.                                                       *
//*                                                                              *
//* IMPORTANT NOTE:                                                              *
//* ===============                                                              *
//* This is a dangerous initialization because it operates on arrays of variable *
//* length.                                                                      *
//* 1) It is be safer to call the SetFieldAttr() method after the ACWin object   *
//*    has been instantiated (text formatting has been validated).               *
//*    For ACDlg objects this pre-validation is not possible; therefore, care    *
//*    must be used when formatting the display text The number of rows and      *
//*    columns-per-row must align with the dimensions of the control.            *
//* 1) The 'roeAttr' array should contain at least as many elements as the number*
//*    of text rows defined for the control AND at least as many elements as     *
//*    specified by the 'rowCnt' parameter.                                      *
//* 2) If the text data of the control is incorrectly formatted, the actual      *
//*    item count may not be the number expected, potentially causing the array  *
//*    element count to be incorrect.                                            *
//* 3) The 'rowCnt' parameter specifies the MAXIMUM number of 'rowAttr' elements *
//*    to be read. If there are more items in the control than 'rowCnt'          *
//*    indicates, then the remaining items will receive the control's default    *
//*    attribute.                                                                *
//* 4) If fewer elements are allocated in 'rowAttr' than are specified by        *
//*    'rowCnt', then a "segmentation fault" (memory-access violation) is the    *
//*    likely result. Be Safe! Always allocate one or two extra elements for the *
//*    array in case your morning coffee hasn't kicked in yet.                   *
//* 5) This method does some defensive programming; however, it is not possible  *
//*    to anticipate all potential foolishness, so be careful!                   *
//*                                                                              *
//*                                                                              *
//* Input  : rowAttr: pointer to an array of acAttr color attributes, one        *
//*                   attribute for each text row of the control.                *
//*          rowCnt : number of attribute values in the 'rowAttr' array (>zero)  *
//*                   The minimum _practical_ value is two(2).                   *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if field type does not support per-row attributes           *
//*                  or if 'rowCnt' < minimum                                    *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* -- The final dimensions of the control may not have been established.        *
//*    -- If application's definition of the control dimensions will violate     *
//*       the parent window, the height and/or width may need to be adjusted.    *
//*    -- If application has not formatted the data to fit the control, it may   *
//*       need to be reformatted, potentially changing the number of text rows.  *
//*    -- See colorValidate() below for additional information on adjustments.   *
//* -- The caller's attribute data will be saved, but whether it will match      *
//*    the final configuration of the control cannot be guaranteed.              *
//* -- Note that the dimension of the array attached to the iAttr member is      *
//*    dynamic, so when the data are saved, we allocate one additional element   *
//*    and initialize it with the acaUSEDFLT bitfield value to indicate the      *
//*    end of the array. Do not be confused; if caller sends acaUSEDFLT values,  *
//*    they will be replaced with the control's primary attribute.               *
//*                                                                              *
//********************************************************************************

bool skField::colorConfig ( const acAttr* rowAttr, short rowCnt )
{
   #define DEBUG_CCFG (0)                 // for debugging only
   #define DEBUG_SB (0)
   #define DEBUG_MB (0)

   short acnt,                            // size of array
         indx = ZERO ;                    // index into array
   bool status = false ;                  // return value

   //* Verify control type and minimum height. *
   //* Also verify that 'rowCnt' > ZERO.       *
   if ( ((this->type != ftMB) && (this->type != ftSB)) || 
        ((this->type == ftMB) && (this->hgt < 2)) ||
        (rowCnt <= ZERO) )
      return status ;                     // (note the early return)

   #if DEBUG_CCFG != 0
   if ( ofsdbg.is_open() )
   {
      gsdbg.compose( "colorConfig(%02hd items for %s)", &rowCnt, 
                     (this->type == ftSB ? "ftSB" : "ftMB") ) ;
      ofsdbg << gsdbg << endl ;
   }
   #endif   // DEBUG_CCFG

   if ( this->iAttr != NULL )             // release any previous allocation
   { delete [] this->iAttr ; this->iAttr = NULL ; }


   //* Scrollbox controls (ftSB) *
   if ( this->type == ftSB )
   {
      // Programmer's Note: The number of text rows determine the length of the 
      // array.
      //* Enforce a minimum size for target array.          *
      //* -- Scrollbox controls must have a minimum of      *
      //*    two(2) display rows.                           *
      //* -- Target array must have at least as many        *
      //*    elements as text rows.                         *
      //* -- If source array has too many elements, the     *
      //*    extras will be ignored.                        *
      //* -- If the 'mrp' object has not yet been           *
      //*    initialized, count the number of data items.   *
      //     'rCnt' initialized to '1' when instantiated.   *
      if ( this->mrp.rCnt == 1 )    // count the number of text rows
      {
         const wchar_t* wptr = this->txt.gstr() ;
         for ( int chi = ZERO ; wptr[chi] != NULLCHAR ; ++chi )
         { if ( wptr[chi] == NEWLINE ) { ++this->mrp.rCnt ; } }
      }
      acnt = rowCnt ;
      if ( acnt < 2 )              { acnt = 2 ; }             // minimum value
      if ( acnt < this->mrp.rCnt ) { acnt = this->mrp.rCnt ;} // ensure full capacity

      this->iAlloc ( acnt + 1 ) ;            // allocate the array

      //* Copy the source data to target *
      for ( indx = ZERO ; (indx < rowCnt) && (indx < acnt) ; ++indx )
      {
         this->iAttr[indx] = (rowAttr[indx] == acaUSEDFLT ? 
                              this->aca.acaVal : rowAttr[indx]) ;
      }
      //* If too few specified, pad to full array length. *
      while ( indx < acnt )
         this->iAttr[indx++] = this->aca.acaVal ;
      this->iAttr[indx] = acaUSEDFLT ;       // terminate the array
      status = true ;                        // declare success

      #if DEBUG_CCFG != 0 && DEBUG_SB != 0
      if ( ofsdbg.is_open() )
      {
         indx = ZERO ;
         gString gsRow, gsSrc( this->txt ) ; short nl = ZERO ;
         for ( short r = ZERO ; r <= acnt ; ++r, ++indx )
         {
            gsRow = gsSrc ;
            nl = gsRow.find( NEWLINE ) ;
            gsRow.limitChars( nl ) ;
            gsSrc.shiftChars( -(nl + 1) ) ;
            if ( indx < rowCnt )
            {
               gsdbg.compose( "%02hd) %08X -> %08X '%S'", 
                              &r, &rowAttr[indx], &this->iAttr[r], gsRow.gstr() ) ;
            }
            else
            {
               gsdbg.compose( "%02hd) -------- -> %08X '%S'", 
                              &r, &this->iAttr[r], gsRow.gstr() ) ;
            }
            ofsdbg << gsdbg << endl ;
         }
      }
      #endif   // DEBUG_CCFG && DEBUG_SB
   }

   //* Menu controls (ftMB) *
   else
   {
      //* The application-supplied colors are assumed to apply *
      //* ONLY to the display items, NOT to the border rows;   *
      //* therefore, if too many attributes specified, ignore  *
      //* the extras.                                          *
      bool group = bool(this->grp != noGROUP) ; // set if group member
      if ( !group && (rowCnt > (hgt - 2)) )     { rowCnt = hgt - 2 ; }
      else if ( group && (rowCnt > (hgt - 1)) ) { rowCnt = hgt - 1 ; }

      //* Array height is based on the height of the control.  *
      //* (Caller has validated the 'hgt' value.)              *
      //* For efficiency of display algorithm, this method     *
      //* expands the attribute array to include color         *
      //* attributes for the border row(s).                    *
      acnt = this->hgt ;
      this->iAlloc ( acnt + 1 ) ;               // allocate the array
      indx = ZERO ;
      //* If context menu, it has a top border (default value).*
      if ( ! group ) { this->iAttr[indx++] = this->aca.acaVal ; }
      //* Copy the item attributes *
      for ( short ai = ZERO ; ai < rowCnt ; ++ai, ++indx )
      {
         this->iAttr[indx] = (rowAttr[ai] == acaUSEDFLT ? 
                              this->aca.acaVal : rowAttr[ai]) ;
      }
      //* If too few specified, pad to full array length. *
      //* This also sets default value for lower border.  *
      while ( indx < acnt )
         this->iAttr[indx++] = this->aca.acaVal ;
      this->iAttr[indx] = acaUSEDFLT ;       // terminate the array
      status = true ;                        // declare success

      #if DEBUG_CCFG != 0 && DEBUG_MB != 0
      if ( ofsdbg.is_open() )
      {
         // Note: 'this->txt' does not (yet) contain borders,
         //       so we insert them to format the debug data.
         //       Carets are also removed.
         gString gsRow, gsSrc( this->txt ) ;
         short nl = ZERO, ri = ZERO ;
         gsSrc.erase( L'^', ZERO, false, true ) ;
         if ( ! group )
         {
            gsRow.padCols( this->wid - 2, wcsHORIZ ) ;
            gsRow.insert( wcsULs ) ;
            gsRow.append( wcsURs ) ;
            gsdbg.compose( "%02hd) -------- -> %08X '%S'", 
                           &ri, &this->iAttr[ri], gsRow.gstr() ) ;
            ofsdbg << gsdbg << endl ;
            ++ri ;
         }
         for ( short ai = ZERO ; ai < (acnt - 2) ; ++ai, ++ri )
         {
            gsRow = gsSrc ;
            nl = gsRow.find( NEWLINE ) ;
            gsRow.limitChars( nl ) ;
            if ( (ri > ZERO) && (ri < (this->hgt - 1)) )
            { gsRow.insert( wcsVERT ) ; gsRow.append( wcsVERT ) ; }
            gsSrc.shiftChars( -(nl + 1) ) ;
            if ( ai < rowCnt )
            {
               gsdbg.compose( "%02hd) %08X -> %08X '%S'", 
                              &ri, &rowAttr[ai], &this->iAttr[ri], gsRow.gstr() ) ;
            }
            else
            {
               gsdbg.compose( "%02hd) -------- -> %08X '%S'", 
                              &ri, &this->iAttr[ri], gsRow.gstr() ) ;
            }
            ofsdbg << gsdbg << endl ;
         }
         while ( ri < this->hgt - 1 )
         {
            gsRow = gsSrc ;
            nl = gsRow.find( NEWLINE ) ;
            gsRow.limitChars( nl ) ;
            if ( ri < (this->hgt - 1) )
            { gsRow.insert( wcsVERT ) ; gsRow.append( wcsVERT ) ; }
            gsSrc.shiftChars( -(nl + 1) ) ;
            gsdbg.compose( "%02hd) -------- -> %08X '%S'", 
                           &ri, &this->iAttr[ri], gsRow.gstr() ) ;
            ofsdbg << gsdbg << endl ;
            ++ri ;            
         }
         gsRow.clear() ;
         gsRow.padCols( this->wid - 2, wcsHORIZ ) ;
         gsRow.insert( wcsLLs ) ;
         gsRow.append( wcsLRs ) ;
         gsdbg.compose( "%02hd) -------- -> %08X '%S'", 
                        &ri, &this->iAttr[ri], gsRow.gstr() ) ;
         ofsdbg << gsdbg << endl ;
      }
      #endif   // DEBUG_CCFG && DEBUG_MB
   }

   return status ;

   #undef DEBUG_SB
   #undef DEBUG_MB
   #undef DEBUG_CCFG
}  //* End colorConfig() *

//*************************
//*      getItem          *
//*************************
//********************************************************************************
//* For Menus (ftMB) and Scrollbox controls (ftSB), only: retrieve the "item"    *
//* i.e. the index of the item (row) containing the insertion point ('ip'),      *
//* or optionally retrieve the index of the item "selected" by the user.         *
//*                                                                              *
//*                                                                              *
//* Special Case: Context menus are drawn with a top border, so the item index   *
//*               returned will be one less than the display row index.          *
//*                 Context Menu          Group Submenu                          *
//*               ┌────────────────┐                                             *
//*               │Open a file     │    │Open a file     │ <-- iNum == zero      *
//*               │Save file       │    │Save file       │                       *
//*               │Close file      │    │Close file      │                       *
//*               │Exit Application│    │Exit Application│                       *
//*               └────────────────┘    └────────────────┘                       *
//*                                                                              *
//* Special Case: Single-row menus (Menubar): The text of the token referenced   *
//*               by 'ip' and the token index (NOT the character index) are      *
//*               returned. Example menubar:                                     *
//*               Example : | File  Edit  View  Help |                           *
//*                 'ip'  :   1     7     13    19                               *
//*                 item  :   0     1     2     3                                *
//*                                                                              *
//* Note:                                                                        *
//* -----                                                                        *
//* This method assumes that the 'ip' member and the mrp.rcur member are correct.*
//* If either of these values is incorrect, then the iNum value or iTxt data     *
//* (or both) may be incorrect.                                                  *
//*                                                                              *
//* Input  : iTxt  : (by reference) receives item text data                      *
//*          iNum  : (by reference) receives the index of the "item" referenced  *
//*                  by the 'ip' member (item referenced by the cursor)          *
//*                  (This is the 0-based item index.)                           *
//*          selnum: (optional, default: false) for multi-row controls only      *
//*                  if set AND if a previously "selected" item (sitem >= zero), *
//*                     return index of "selected" item.                         *
//*                  if reset, return index of item containing the IP.           *
//*                                                                              *
//* Returns: 'true'  if supported target control type (ftMB || ftSB), AND        *
//*                  if item identified                                          *
//*          else 'false' (iNum == -1 && iTxt == "---")                          *
//********************************************************************************
//* Note on Hotkey indicator:                                                    *
//* -------------------------                                                    *
//* The application specifies a menu hotkey using the caret ('^'); however,      *
//* the displayed hotkey character is bracketed by the underline/underline_off   *
//* escape sequences. Example: For a hotkey of 'v':                              *
//* -- The user would define the item text as: "Sa^ve File".                     *
//* -- This will be stored as: "Sa\x1B[4mv\x1B[24me File".                       *
//* These escape sequences would just confuse the application layer.             *
//* For this reason, for returned text, the escape sequences will be removed     *
//* and the string will be restored to the format created by the application.    *
//* The hotkey (if any) will then be preceeded by the caret '^' character.       *
//* If the target item contains no escape sequences, it will be returned         *
//* unchanged.                                                                   *
//*                                                                              *
//* Note on Menubar tokens:                                                      *
//* -----------------------                                                      *
//* If 'ip' does not reference the first character of a token, the calculation   *
//* for 'iNum' may be complex. This should never happen because we never make    *
//* mistakes, but just in case gremlins sneak in during the night and corrupt    *
//* our code, we check anyway. :-)                                               *
//*                                                                              *
//*     |--Scenario #1 - 'ip' references first char of a token                   *
//*     |                this is the expected data configuration                 *
//* "   File  Edit  View  Help       "                                           *
//*  |   |                     |--Scenario #2 - 'ip' is beyond the last token    *
//*  |   |                                      Return failure i.e. program error*
//*  |   |--Scenario #3 - 'ip' references a token character other than the 1st   *
//*  |                    Scan backward to first char || or until index==0       *
//*  |--Scenario #4 - 'ip' references whitespace before (or between) tokens      *
//*                   Scan to first non-whitespace character.This is common      *
//*                   for the first invocation.                                  *
//*                                                                              *
//********************************************************************************

bool skField::getItem ( gString& iTxt, short& iNum, bool selnum ) const
{
   const wchar_t* ULINE_ON  = L"\x1B[4m" ;  // aesUNDERLINE
   const wchar_t* ULINE_OFF = L"\x1B[24m" ; // aesUNDERLINE_OFF
   int  indx ;
   bool status = false ;                  // return value

   iTxt.clear() ;                         // initialize caller's parameters
   iNum = -1 ;

   if ( (this->type == ftMB) || (this->type == ftSB) )
   {
      if ( this->hgt > 1 )
      {
         //* If specified, reference the "selected" item (if any).*
         if ( selnum && (this->sitem >= ZERO) )
         {
            int offset = this->mrp.roff[this->sitem] ;
            if ( (this->type == ftMB) && (this->grp == noGROUP) ) // if a context menu
               offset = this->mrp.roff[this->sitem + 1] ;
            iTxt = &this->txt.gstr()[offset] ;
            iNum = this->sitem ;
         }
         //* Else reference highlighted item (item containing IP).*
         else
         {
            iTxt = &this->txt.gstr()[this->ip] ;
            if ( this->type == ftMB )
            {
               if ( this->grp == noGROUP )      // for context menus 
                  iNum = this->mrp.rcur - 1 ;   // item index excludes top border
               else
                  iNum = this->mrp.rcur ;       // group members have no top border
            }
            else  // ( type == ftSB )
               iNum = this->mrp.rcur ;
         }
         if ( (indx = iTxt.find( NEWLINE )) >= ZERO )
            iTxt.limitChars( indx ) ;
         if ( this->type == ftMB )
         {
            iTxt.shiftChars( -1 ) ;          // remove border characters
            iTxt.limitChars( iTxt.gschars() - 2 ) ;
         }
         iTxt.strip() ;                      // strip leading/trailing whitespace
         status = true ;
      }
      else     // Menubar (single-row menu)
      {
         // Programmer's Note 'ip' should reference the first charater of the token.
         iNum = -1 ;                      // token index(not char index)
         int thead ;                      // head (index first char) of token
         int tip = this->ip ;             // local copy of IP
         int indx = ZERO ;                // character index
         wchar_t wtxt[gsALLOCMIN] ;       // work buffer
         this->txt.copy( wtxt, gsALLOCMIN) ;

         //* If IP is in mid-token, find head of current token.*
         if ( (tip > ZERO) && (wtxt[tip] != wcsSPACE) && (wtxt[tip - 1] != wcsSPACE) )
         { while ( (tip > ZERO) && (wtxt[tip - 1] != wcsSPACE) ) { --tip ; } }

         for ( indx = ZERO ; wtxt[indx] != NULLCHAR ; ++indx )
         {
            if ( wtxt[indx] != wcsSPACE )
            {
               ++iNum ;                   // token index
               thead = indx ;             // char index of token
               if ( indx >= tip )         // current token found
               {
                  //* Isolate the token *
                  while ( (wtxt[indx] != NULLCHAR) && (wtxt[indx] != wcsSPACE) )
                     ++indx ;
                  wtxt[indx] = NULLCHAR ;
                  iTxt = &wtxt[thead] ;
                  status = true ;
                  break ;
               }
               else                       // step over current token
               {
                  while ( (wtxt[indx] != wcsSPACE) && (wtxt[indx] != NULLCHAR) )
                     ++indx ;
               }
            }
         }
      }

      //* Remove any embedded escape sequences (see note above).*
      if ( status != false )
      {
         if ( (indx = iTxt.find( ULINE_ON )) >= ZERO )
            iTxt.replace( ULINE_ON, CARET ) ;
         if ( (indx = iTxt.find( ULINE_OFF, indx )) >= ZERO )
            iTxt.erase( ULINE_OFF, indx ) ;
      }
      else     // (!status) - return placeholder text
      { iNum = -1 ; iTxt = "---" ; }
   }
   return status ;

}  //* End getItem() *

//*************************
//*     setHiliteMod      *
//*************************
//********************************************************************************
//* For Textbox (ftTB) controls, set the text-modifier attribute to be used for  *
//* highlighted substrings.                                                      *
//*                                                                              *
//* Notes:                                                                       *
//* ------                                                                       *
//*  a) The specified value will replace any previous highlight attribute.       *
//*  b) If the HilightFieldText() method is called with any attribute value      *
//*     other than the default (acaUSEDFLT), that value will replace any value   *
//*     set by this method.                                                      *
//*  c) This method does not refresh the display, so if there is currently       *
//*     highlighted text displayed in the field, the new highlight attribute     *
//*     will not be applied until the next time the field is refreshed.          *
//*                                                                              *
//* Input  : txtmod: text modifier attribute for highlighting text segments.     *
//*                  One of the following text modifier attributes:              *
//*                   acaBOLD      bold-text modifier                            *
//*                   acaITALIC    italic-text modifier                          *
//*                   acaUNDERLINE underline-text modifier (not recommended)     *
//*                   acaOVERLINE  overline-text modifier                        *
//*                   acaXOUT      x-out (strikethrough) text modifier           *
//*                   acaBLINK     blinking-text modifier                        *
//*                   acaCONCEAL   concealed (invisible) text                    *
//*                   acaREVERSE   reversed fgnd/bkgnd (default)                 *
//*                  The specified modifier flag _should not_ also be set        *
//*                  as an element of control's base color attribute.            *
//*                  Note: OR'd combinations of these modifiers are not allowed. *
//*                                                                              *
//* Returns: 'true'  if highlight attribute set successfully                     *
//*          'false' if a) invalid modifier specified and discarded.             *
//*                        (default modifier, acaREVERSE, will be used)          *
//*                     b) field type does not support text highlighting         *
//********************************************************************************

bool skField::setHiliteMod ( acAttr txtMod )
{
   const acAttr dfltMod = acaREVERSE ; // default modifier for highlights

   bool status = false ;                  // return value

   if ( this->type == ftTB )
   {
      if ( txtMod == acaUSEDFLT )
      {
         //* If storage previously allocated, retrieve value at offset zero. *
         if ( this->iAttr != NULL )
            txtMod = acAttr(this->iAttr[ZERO] & acaMOD_MASK) ;
         //* No previous value, use default value. *
         else
            txtMod = dfltMod ;
      }
      else
      {
         txtMod = acAttr(txtMod & acaMOD_MASK) ;   // strip non-modifier flags
      }

      //* Validate the modifier attribute *
      switch( txtMod )
      {
         case acaREVERSE:
         case acaBOLD:
         case acaITALIC:
         case acaUNDERLINE:
         case acaOVERLINE:
         case acaXOUT:
         case acaBLINK:
         case acaCONCEAL:
            break ;
         default:             // no flag specified, or multiple flags specified
            txtMod = dfltMod ;
            break ;
      } ;

      if ( this->iAttr == NULL )          // allocate storage
         this->iAlloc ( MIN_ACNT ) ;
      this->iAttr[ZERO] = txtMod ;        // save the modifier attribute
      this->iAttr[1]    = acaUSEDFLT ;    // signal end of array
      status = true ;
   }
   return status ;

}  //* End setHiliteMod() *

//*************************
//*       getHilite       *
//*************************
//********************************************************************************
//* Returns a copy of highlighted text segment within a Textbox (ftTB) field.    *
//* This method was developed primarily for use during development; however,     *
//* it may have other uses. (see Test_Hilite() for an example)                   *
//* Conceptually similar to copying a "selected" text substring to the           *
//* clipboard, except that the data are returned directly to caller              *
//* without accessing the clipboard.                                             *
//*                                                                              *
//* Technical Note: This method does not distinguish between "highlighted"       *
//* text and "selected" text. The text delimited by the 'sela' and 'selb'        *
//* members is returned regardless.                                              *
//*                                                                              *
//* Input  : begin : (by reference) receives index of first (leftmost) character *
//*                  of highlighted text segment                                 *
//*          len   : (by reference) receives the number of characters in the     *
//*                  highlighted segment                                         *
//*          gsTxt : (by reference) receives a copy of the highlighted text      *
//*          hmod  : (optional, default:null pointer) if specified, target       *
//*                  receives current text-modifier attribute used to highlight  *
//*                  the text segment                                            *
//*                                                                              *
//* Returns: 'true'  if highlighted text data successfully captured.             *
//*          'false' a) target field contains no highlighted/selected text data  *
//*                  b) field type does not support text highlighting            *
//*                  If 'false', then 'begin' and 'len' are set to zero, 'hmod'  *
//*                     is set to acaPLAINTEXT and 'gsTxt' is cleared (set to    *
//*                     empty string).                                           *
//********************************************************************************

bool skField::getHilite ( int& begin, int& len, gString& gsTxt, acAttr* hmod )
{
   int  cnt = this->selb - this->sela ;   // number of chars in substring
   bool status = false ;                  // return value

   //* Initialize caller's variables *
   begin = len = ZERO ;
   if ( hmod != NULL ) { *hmod = acaPLAINTXT ; }
   gsTxt.clear() ;

   if ( (this->type == ftTB) && (cnt > ZERO) )
   {
      gsTxt.loadChars( &this->txt.gstr()[this->sela], cnt ) ;
      begin = this->sela ;
      len   = cnt ;
      if ( (hmod != NULL) && (this->iAttr != NULL) )
         *hmod = this->iAttr[ZERO] ;
      status = true ;
   }
   return status ;

}  // End getHilite() *

//*************************
//*       init_Menu       *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Convert the text of menus into bordered text and initialize stat data.       *
//* The data will be truncated as necessary to make it fit the specified         *
//* dimensions.                                                                  *
//*                                                                              *
//* 1) The dimensions of the menu are provided by the 'hgt' and 'wid' menbers.   *
//*    These dimensions should include the rows and columns for the border.      *
//*    a) When 'hgt' == 1, it indicates that the menu is a "menubar", which is   *
//*       handled as a special case:                                             *
//*       -- A menubar field has no border.                                      *
//*       -- A menubar field should have no newline characters, and if it does,  *
//*          they will be discarded.                                             *
//*       -- The text of a menubar field will be padded to exactly 'wid' columns,*
//*          or if too wide, it will be truncated.                               *
//*    b) When 'hgt' > 1, the number of source text rows should be either:       *
//*       -- If text rows == hgt-1  : member of a menubar group                  *
//*       -- If text rows == hgt-2  : stand-alone 'context' menu                 *
//* 2) The source data should have been formatted as a paragraph,                *
//*    a) The width (column count) of the widest row of source text should be    *
//*       no more that 'wid' - 2, (excluding the width of the 'caret' character, *
//*       if any. See below for a discussion of caret usage.                     *
//*       The width of each row will be padded with additional spaces as         *
//*       necessary to meet the required width.                                  *
//*       Text rows which are greater than 'wid - 2' will be truncated to        *
//*       accomodate the text plus left/right borders.                           *
//*    b) Number of source text rows:                                            *
//*       -- Menubars by definition have a single row with no border.            *
//*       -- Menus which are members of a menubar group should have a number of  *
//*          text rows ONE LESS than specified field height to accomodate the    *
//*          addition of a bottom border.                                        *
//*       -- Stand-alone menus should have a number of text rows TWO LESS than   *
//*          specified field height to accomodate top and bottom borders.        *
//*       -- If too few text rows are provided, then the field height is         *
//*          adjusted to reflect the actual number of rows.                      *
//*       -- If too many text rows are provided, then the excess rows will be    *
//*          discarded.                                                          *
//*    c) The dimensions are arbitrarily limited to 40 rows by 255 columns.      *
//*       These limits will seldom be reached within the dimensions of a typical *
//*       terminal window.                                                       *
//* 3) If the source text contains caret '^' characters, they indicate the       *
//*    the hotkey for the menu item, and the width of the caret character        *
//*    IS NOT included in the width calculations.                                *
//*    a) After dimensioning and application of the border are complete, for     *
//*       each row that contains a caret character, the caret and the character  *
//*       that follows it are replaced by a pair of ANSI escape sequences which  *
//*       bracket the display character which will then be displayed with the    *
//*       underline attribute specifying the hotkey character. Example:          *
//*          "^Do Something"   becomes:   "\x1B[4mD\x1B[24mo Something"          *
//*       where the 'D' is underlined.                                           *
//* 4) IP position and cursor placement.                                         *
//*    The IP of menu controls is dynamically set when the menu becomes visible; *
//*    however, the initial IP is set here in conjunction with initializing the  *
//*    stats data.                                                               *
//* 5) Custom color attributes may be specified by the array referenced by       *
//*    the 'iAttr' member. See the skField colorValidate() method for details.   *
//*                                                                              *
//* Input  : fptr    : pointer to the field structure of type ftMB               *
//*                    structure members used include:                           *
//*             txt  : text to be converted                                      *
//*             hgt  : height of the field, including border                     *
//*             wid  : width of the field, including border                      *
//*             btype: border line type ltSINGLE or ltDUAL, and as a special     *
//*                    case, ltHORIZ will be interpreted as "no-border".         *
//*             grp  : group designator character - this cannot be relied upon   *
//*                    to indicate group membership unless caller has previously *
//*                    validated it, so it is currently ignored.                 *
//*                                                                              *
//* Returns: 'true' if data successfully formatted without truncation            *
//*          'false' if data truncated                                           *
//********************************************************************************

bool ACWin::init_Menu ( skField* fptr )
{
   #define DEBUG_IMM (0)                  // for debugging only

   const wchar_t vsCHAR  = wcsVERT,       // single-line vertical border character
                 hsCHAR  = wcsHORIZ,      // single-line horizontal border character
                 sulCHAR = wcsUL,         // single-line corner characters
                 surCHAR = wcsUR,
                 sllCHAR = wcsLL,
                 slrCHAR = wcsLR,
                 vdCHAR = wcsVERTd,       // dual-line vertical border character
                 hdCHAR = wcsHORIZd,      // dual-line horizontal border character
                 dulCHAR = wcsULd,        // dual-line corner characters
                 durCHAR = wcsURd,
                 dllCHAR = wcsLLd,
                 dlrCHAR = wcsLRd ;
   const int maxHEIGHT = 40,              // arbitrary maximum for menu rows
             maxWIDTH  = 255,             // arbitrary maximum for menu columns (MAX_FNAME)
             colFREE   = 2,               // required free columns for each row
             minWIDTH  = colFREE + 3 ;    // minimum row width
   const char* caretTemplate = "\x1B[4m%C\x1B[24m" ;

   gString gs,                            // text analysis
           gsTrg,                         // text accumulator
           gsCar ;                        // escape-sequence formatting
   wchar_t wbuff[maxWIDTH + 1] ;          // work buffer
   const wchar_t* wsrc = fptr->txt.gstr() ;  // pointer to source text
   int     chCnt = fptr->txt.gschars(),   // number of source characters (incl. nullchar)
           si = ZERO,                     // source index
           ti,                            // target index
           ci,                            // caret index
           rows = ZERO ;                  // number of rows processed
   wchar_t vbChar, hbChar, ulChar,        // border-character constants
           urChar, llChar, lrChar ;
   LineType lt = fptr->btype ;            //* border line style (see below for range check)
   short   paraHgt, paraWid ;             // dimensions of source text
   wchar_t caretPair[3] = {'^',' ','\0'}, // comparison string
           hkey ;                         // menu-item hotkey
   bool    group  = true,                 // set if menu is a member of a group
           carrot = false,                // set if menu item contains a caret character
           status = true ;                // return value (hope for the best)

   //* Range-check dimensions (caller _should be_ smart, but...) *
   if ( fptr->hgt < 1 )                { fptr->hgt = 1 ; }
   else if ( fptr->hgt > maxHEIGHT )   { fptr->hgt = maxHEIGHT ; }
   if ( fptr->wid < minWIDTH )         { fptr->wid = minWIDTH ; }
   else if ( fptr->wid > maxWIDTH )    { fptr->wid = maxWIDTH ; }

   //* Initialize border characters *
   if ( lt == ltDUAL )                    // dual-line border
   {
      vbChar = vdCHAR ;  hbChar = hdCHAR ;  ulChar = dulCHAR ; 
      urChar = durCHAR ; llChar = dllCHAR ; lrChar = dlrCHAR ;
   }
   else if ( lt == ltHORIZ )              // no border (spaces in border)
   { vbChar = hbChar = ulChar = urChar = llChar = lrChar = wcsSPACE ; }
   else                                   // single-line border (default)
   {
      vbChar = vsCHAR ;  hbChar = hsCHAR ;  ulChar = sulCHAR ; 
      urChar = surCHAR ; llChar = sllCHAR ; lrChar = slrCHAR ;
   }

   //* For menubar fields, remove all newlines and caret characters. *
   if ( fptr->hgt == 1 )
   {
      fptr->txt.erase( NEWLINE, ZERO, false, true ) ;
      fptr->txt.erase( CARET, ZERO, false, true ) ;
   }
   gs = fptr->txt ;
   if ( fptr->hgt > 1 )       // carets ('^') are not displayed characters
      gs.erase( CARET, ZERO, false, true ) ;

   //* Get the dimensions of the source text and determine  *
   //* whether menu is part of a menu group. See note above *
   //* regarding row count. Note that caret characters are  *
   //* not included in the width calculation.               *
   this->ParagraphSize ( gs, paraHgt, paraWid ) ;

   if ( fptr->hgt > 1 )                // multi-row menus
   {
      // Programmer's Note: If specified height is incorrect for 
      // intended menu type, then this test will also be wrong.
      if ( paraHgt != (fptr->hgt - 1) )   // if NOT a group member
         group = false ;

      //* If text is malformed, reset 'status' flag. *
      if ( (paraWid > (fptr->wid - colFREE)) ||
           (group && ((paraHgt != (fptr->hgt - 1)))) || 
           (!group && (paraHgt != (fptr->hgt - 2))) )
      { status = false ; }

      //* If too few source rows, reduce field *
      //* height to match actual text height.  *
      if ( group && (paraHgt < (fptr->hgt - 1)) )
         fptr->hgt = paraHgt + 1 ;
      else if ( !group && (paraHgt < (fptr->hgt - 2)) )
         fptr->hgt = paraHgt + 2 ;
   }

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_IMM != 0
   if ( ofsdbg.is_open() )
   {
      gsdbg.compose( "menuFormat( hgt:%hd wid:%hd lt:%hd, group:%hhd )",
                  &fptr->hgt, &fptr->wid, &lt, &group ) ;
      ofsdbg << gsdbg << endl ;
   }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_IMM

   //* If a menu bar, it is a special case. *
   if ( fptr->hgt == 1 )
   {
      if ( group )
      {
         //* Strip all carets from menubar controls. *
         fptr->txt.erase( CARET, ZERO, false, true ) ;
         //* Adjust text width to match specified field width.*
         fptr->txt.limitCols( fptr->wid ) ;
         fptr->txt.padCols( fptr->wid ) ;
         status = true ;
         // Programmer's Note: While a single-row, non-group-member menu is 
         // _possible_, it is illogical because there would be only one choice.

         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_IMM != 0
         if ( ofsdbg.is_open() )
         {
            gsdbg.compose( "%02d) '%S'", &rows, fptr->txt.gstr() ) ;
            gsdbg.replace( L' ', L'-', 4, false, true ) ; 
            ofsdbg << gsdbg << "\n" << endl ;
         }
         #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_IMM
      }
   }
   //* Multi-row menus *
   else
   {
      //* If a stand-alone menu, start with top border *
      if ( ! group )
      {
         gs.clear() ;
         gs.padCols( fptr->wid - 2, hbChar ) ;
         gsTrg.append( "%C%S%C\n", &ulChar, gs.gstr(), &urChar ) ;
         ++rows ;

         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_IMM != 0
         if ( ofsdbg.is_open() )
         {
            gsdbg.compose( "%02d) '%S'", &rows, gs.gstr() ) ;
            ofsdbg << gsdbg << endl ;
         }
         #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_IMM
      }

      //* Format the individual menu items *
      while ( (rows < (fptr->hgt - 1)) && (si < chCnt) )
      {
         ti = ZERO ;
         carrot = false ;
         while ( (wsrc[si] != NEWLINE) && (wsrc[si] != NULLCHAR) ) // until end of row
         {
            if ( wsrc[si] == CARET )
            { ci = ti ; carrot = true ; }
            wbuff[ti++] = wsrc[si++] ;
         }

         ++si ;                           // step over the line terminator
         wbuff[ti] = NULLCHAR ;           // terminate the substring

         gs = wbuff ;                     // pad to target width
         gs.limitCols( fptr->wid - (carrot ? 1 : 2) ) ;
         gs.padCols( fptr->wid - (carrot ? 1 : 2) ) ;

         //* If the menu items contains a caret character, and if *
         //* the hotkey character was not truncated, replace the  *
         //* caret and the character that follows it with the     *
         //* underline escape sequence. (see note above)          *
         if ( carrot && ci <= (gs.gschars() - 3) )
         {
            hkey = gs.gstr()[ci + 1] ;
            caretPair[1] = hkey ;
            gsCar.compose( caretTemplate, &hkey ) ;
            gs.replace( caretPair, gsCar.gstr() ) ;
         }

         gsTrg.append( "%C%S%C\n", &vbChar, gs.gstr(), &vbChar ) ;
         ++rows ;

         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_IMM != 0
         if ( ofsdbg.is_open() )
         {
            gsdbg.compose( "%02d) '%S'", &rows, gs.gstr() ) ;
            ofsdbg << gsdbg << endl ;
         }
         #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_IMM
      }

      //* If not invisible border, _or_ if context menu, *
      //* append the bottom border.                      *
      if ( (hbChar != wcsSPACE) || !group )
      {
         gs.clear() ;
         gs.padCols( fptr->wid - 2,  hbChar ) ;
         gsTrg.append( "%C%S%C", &llChar, gs.gstr(), &lrChar ) ;

         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_IMM != 0
         if ( ofsdbg.is_open() )
         {
            ++rows ;
            gsdbg.compose( "%02d) '%S'\n-----\n", &rows, gs.gstr() ) ;
            ofsdbg << gsdbg << gsTrg << "\n-----\n" << endl ;
         }
         #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_IMM
      }

      fptr->txt = gsTrg ;                 // copy formatted data to field structure
   }  // (multi-row menus)


   //* Analyze the text data and set initial IP.  * 
   //* For single-row menu, IP is on first token. *
   //* For other menus, IP is on first menu item. *
   fptr->ip = ZERO ;
   this->parseFieldText ( fptr ) ;
   if ( fptr->hgt == 1 )
   {
      int smi ; fptr->ip = ZERO ;
      menubarToken ( wcsTAB, fptr->txt.gstr(), fptr->ip, smi ) ;
      this->parseFieldText ( fptr, 0 ) ;
   }
   else if ( fptr->grp == noGROUP )
   {
      fptr->ip = fptr->mrp.roff[1] ; // (below top border)
      this->parseFieldText ( fptr ) ;
   }

   return status ;

   #undef DEBUG_IMM
}  //* End init_Menu() *

//*************************
//*    init_Scrollbox     *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Validate the text of Scrollbox controls and initialize stat data.            *
//* 1) The dimensions of the field are provided. The horizontal dimension        *
//*    includes one column of padding at left and right edges of field.          *
//* 2) The source data should have been formatted as a paragraph, i.e. with a    *
//*    newline character at the end of each row EXCEPT the last row.             *
//*    A trailing newline character will be automatically deleted.               *
//* 3) 'hgt' (number of rows): 1 <= hgt <= window text rows.                     *
//* 4) 'wid' (number of columns): The widest row of souce text should be no more *
//*     that 'wid' - 2. Wider rows will be truncated.                            *
//*    a) Each row of the text will be padded to exactly 'wid' columns.          *
//* 5) Hotkeys for individual rows within the control are not supported.         *
//*    If the source text contains caret '^' characters, they will be silently   *
//*    removed.                                                                  *
//* 6) Custom color attributes may be specified by the array referenced by       *
//*    the 'iAttr' member. See the skField colorValidate() method for details.   *
//*                                                                              *
//* Input  : fptr : pointer to the field structure of type ftSB                  *
//*                 structure members used include:                              *
//*                 txt : text to be converted                                   *
//*                 hgt : height of the field                                    *
//*                 wid : width of the field (including horizontal padding)      *
//*                                                                              *
//* Returns: 'true' if data successfully formatted without truncation            *
//*          'false' if data truncated                                           *
//********************************************************************************
// NOTES:                                                                        *
//*1) Be sure that all rows are the same width so highlight can be implemented.  *
//*   This results in what is basically a well-ordered textbox with no horizontal*
//*   scrolling or editing.                                                      *
//*2) Minimum height and width are arbitrary; however, code sanity is important, *
//*   so a minimum height and width are enforced.                                *
//*3) If this method is invoked by the Dialog() method, the maxHEIGHT and        *
//*   maxWIDTH limits on control dimensions defined below will be too large;     *
//*   however, it is assumed that caller has adjusted the control dimensions     *
//*   before the call.                                                           *
//*                                                                              *
//********************************************************************************

bool ACWin::init_Scrollbox ( skField* fptr )
{
   #define DEBUG_ISB (0)                  // for debugging only

   const int maxHEIGHT = this->txtRows ;  // arbitrary maximum for text rows
   const int maxWIDTH  = this->txtCols ;  // maximum field width
   const int minHEIGHT = 2 ;              // arbitrary minimum height
   const int minWIDTH  = 4 ;              // arbitrary minimum width
   gString gs,                            // text analysis
           gsTrg ;                        // text accumulator
   short   paraHgt, paraWid ;             // dimensions of source text
   bool    status = true ;                // return value (hope for the best)

   //* Range-check dimensions *
   if ( fptr->hgt < minHEIGHT )        { fptr->hgt = minHEIGHT ; }
   else if ( fptr->hgt > maxHEIGHT )   { fptr->hgt = maxHEIGHT ; }
   if ( fptr->wid < minWIDTH )         { fptr->wid = minWIDTH ; }
   else if ( fptr->wid > maxWIDTH )    { fptr->wid = maxWIDTH ; }

   //* Caret characters are not recognized as hotkey  *
   //* indicators within scrollbox controls, so to    *
   //* avoid problems, all carets are removed.        *
   fptr->txt.erase( CARET, ZERO, false, true ) ;

   //* Remove trailing newline if present. *
   if ( (fptr->txt.gstr()[fptr->txt.gschars() - 2]) == NEWLINE )
      fptr->txt.limitChars( fptr->txt.gschars() - 2 ) ;

   //* Get the dimensions of the source text.          *
   //* Verify that text formatting is within specified *
   //* dimensions for the control.                     *
   this->ParagraphSize ( fptr->txt, paraHgt, paraWid ) ;
   if ( paraWid > fptr->wid - 2 )
      status = false ;

   #if DEBUG_ANSICMD != 0 && DEBUG_ISB != 0
   if ( ofsdbg.is_open() )
   {
      gsdbg.compose( "sbFormat( field hgt:%hd wid:%hd - text rows:%hd cols:%hd )",
                  &fptr->hgt, &fptr->wid, &paraHgt, &paraWid ) ;
      ofsdbg << gsdbg << endl ;
   }
   #endif   // DEBUG_ANSICMD && DEBUG_ISB

   wchar_t wbuff[maxWIDTH + 1] ;          // work buffer
   const wchar_t* wsrc = fptr->txt.gstr() ;  // pointer to source text
   int     chCnt = fptr->txt.gschars(),   // number of source characters (incl. nullchar)
           si = ZERO,                     // source index
           ti ;                           // target index

   //* Format the individual text rows *
   for ( short rows = ZERO ; (rows < paraHgt) && (si < chCnt) ; ++rows )
   {
      ti = ZERO ;
      //* Copy text until end-of-row OR until buffer full.
      while ( (wsrc[si] != NEWLINE) && (wsrc[si] != NULLCHAR) )
      {
         wbuff[ti++] = wsrc[si++] ;
         //* If row text is too long, truncate the row.*
         if ( ti >= (maxWIDTH - 1) )
         { while ( (wsrc[si] != NEWLINE) && (wsrc[si] != NULLCHAR) ) { ++si ;} }
      }
      ++si ;                              // step over the line terminator
      wbuff[ti] = NULLCHAR ;              // terminate the substring
      gs = wbuff ;                        // pad to target width
      gs.limitCols( fptr->wid - 2 ) ;
      gs.padCols( fptr->wid - 2 ) ;
      gsTrg.append( " %S \n", gs.gstr() ) ;

      #if DEBUG_ANSICMD != 0 && DEBUG_ISB != 0
      if ( ofsdbg.is_open() )
      {
         gsdbg.compose( "%02hd) '%S'", &rows, gs.gstr() ) ;
         ofsdbg << gsdbg << endl ;
      }
      #endif   // DEBUG_ANSICMD && DEBUG_ISB
   }

   //* Remove trailing newline if present. *
   if ( (gsTrg.gstr()[gsTrg.gschars() - 2]) == NEWLINE )
      gsTrg.limitChars( gsTrg.gschars() - 2 ) ;

   //* Copy formatted data to field structure, then *
   //* analyze the text data and set cursor offset  *
   //* to match current IP.                         *
   fptr->txt = gsTrg ;
   this->parseFieldText ( fptr ) ;

   return status ;

   #undef DEBUG_ISB
}  //* End init_Scrollbox() *

//*************************
//*     init_Spinner      *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Validate the data for the Spinner controls.                                  *
//*                                                                              *
//* 1)                                                                           *
//*                                                                              *
//* 2)                                                                           *
//*                                                                              *
//* 3)                                                                           *
//*                                                                              *
//* 4)                                                                           *
//*                                                                              *
//* 5)                                                                           *
//*                                                                              *
//* 6)                                                                           *
//*                                                                              *
//*                                                                              *
//* Input  : fptr : pointer to the field structure of type ftSB                  *
//*                 structure members used include:                              *
//*                 txt  :                                                       *
//*                 hgt  :                                                       *
//*                 wid  :                                                       *
//*                 lab  :                                                       *
//*                                                                              *
//*                                                                              *
//* Returns: 'true' if data successfully validated and initialized               *
//*          'false' if unable to correct data format                            *
//********************************************************************************
// NOTES:                                                                        *
// ------                                                                        *
//*1) Similar to the spinners defined in NcDialog API, but with optional "label" *
//*   text for each value in the specified range.                                *
//*                                                                              *
//*2) To avoid adding data members to the skField class, the data are encoded    *
//*   within the 'txt' member according to the defined template.                 *
//*   a) At minimum, the template must include the data range and the default    *
//*      (initial) value: min                                                    *
//*      -- integers   low value through high value Ex: -10:10:0                 *
//*      -- decimals                                                             *
//*      -- alphabetic a:z || A:Z                                                *
//*                                                                              *
//*3) Label position may be above, below, left or right of the spinner data.     *
//*   If labels specified, there should either be a single label for the control *
//*   as a whole, OR one label for each value.                                   *
//*                                                                              *
//*4) Update the SetFieldLabel() to accept spinner label position and per-item   *
//*   label. Labels s/b integrated into the control, not written to window       *
//*   background.                                                                *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---   *
//* Examples: "2.5± label"  "label 2.5±"  "   2.5±"  label                       *
//*                                        label    "   2.5±"                    *
//* ┌────────────────────────────┐   ┌────────────────────────────┐              *
//* │ ##1.00±  Chicken Soup..... │   │ .....Chicken Soup  ##1.00± │              *
//* └────────────────────────────┘   └────────────────────────────┘              *
//*          ┌───────────────────┐   ┌───────────────────┐                       *
//*          │ Chicken Soup..... │   │ ##1.00±           │                       *
//*          │ ##1.00±           │   │ Chicken Soup..... │                       *
//*          └───────────────────┘   └───────────────────┘                       *
//* With or without outline.                                                     *
//*                                                                              *
//*                                                                              *
//* The 'lab' field would be an array of newline-delimited strings, one per      *
//* each value in the spinner range.                                             *
//* The range could be numeric (either integer or decimal).                      *
//* The range could be alphabetic (a-z || A-Z)                                   *
//* The range could also be an array of newline-delimited string values:         *
//*    一\n二\n三\n四\n五\n六\n七\n八\n九\n十                                              *
//*    cow\nhorse\npig\ngoose                                                    *
//*                                                                              *
//* Can spinners be implemented without adding skField data memebers?            *
//*                                                                              *
//* //* Field index 0: ftSP (Spinner: fldSP_DOLLAR) *                            *
//* {                                                                            *
//* ftTB,                            // field type                               *
//* 1 or 2                 // field height                                       *
//*                        // 1 : if "labels" are on left or right               *
//*                        // 2 : if "labels" are on top or bottom               *
//* spinner + label        // field width                                        *
//*                        // spinner is with of item text && ± character        *
//*                        // label on left/right width is the sum of longest    *
//*                        // label + spinner item/±                             *
//* wpul.row, wpul.col,    // Y/X offset in window                               *
//* text data              // this will probably be encoded data                 *
//*                        // min/max encoded as "minval:maxval" Ex: 0:100       *
//*                        // with identifier char:  0:100:±  (identifier const?)*
//*                        // a:z:±    A:Z:±                                     *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                        // color attribute for spinner items                  *
//* floShift,              // text formatting option (constant)                  *
//* ZERO,                  // insertion point (constant, cursor invisible)       *
//* false,                 // read-only field                                    *
//* false,                 // field outline disabled                             *
//* false,                 // field is visible (not hidden)                      *
//* true,                  // access lock reset                                  *
//* false,                 // initial binary state: 'reset' (constant)           *
//* NULLCHAR,              // no hotkey specified                                *
//* noGROUP                // not a group member (constant)                      *
//* },                                                                           *
//*                                                                              *
//*                                                                              *
//********************************************************************************

#if DEBUG_SPINNER != 0
bool ACWin::init_Spinner ( skField* fptr )
{
   #define DEBUG_ISP (0)                  // for debugging only

   bool    status = true ;                // return value (hope for the best)

   return status ;

   #undef DEBUG_ISP
}  //* End init_Spinner() *
#endif   // DEBUG_SPINNER

//*************************
//*     init_Hotkeys      *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Called by init_skFields() to extract the specified hotkey character (if any) *
//* from the field text and format the text for display.                         *
//*                                                                              *
//* Input  : skfp : pointer to skForm object containing array of skField objects *
//*                                                                              *
//* Returns: 'true'  if one or more controls has an associated hotkey            *
//*          'false' if no hotkeys identified in the skForm object               *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* Hotkeys are optional user interface keycodes for setting the input focus     *
//* on a specific field. Hotkeys may be specified in either of two ways:         *
//* 1) Embedded within the text of a field label.                                *
//*    a) For Pushbutton controls the text of the button is the control label,   *
//*       so the hotkey can be specified by placing a caret '^' before the       *
//*       character representing the hotkey.                                     *
//*       Example Label: "Hello ^World!"                                         *
//*       In the example, the caret preceeds the 'W', so the ALT+W key           *
//*       combination will set input focus to that control. Note that hotkeys    *
//*       are case-insensitive, so ALT+W == ALT+w.                               *
//*       In addition, the CTRL+W (CTRL+w) keycode (if available) will also be   *
//*       interpreted as the specified hotkey.                                   *
//*    b) Radiobutton controls are very similar in function to Pushbuttons.      *
//*       For example, a Radiobutton defined with : "[*] Hello ^World!"          *
//*       will have the same result as the Pushbutton example above.             *
//*    c) Textbox and Scrollbox controls optionally have external labels which   *
//*       are initialized _after_ field instantiation via a call to              *
//*       SetFieldLabel(). Again, the caret, if present indicates the hotkey:    *
//*       win->SetFieldLabel( fldIndex, "Hello ^World!" );                       *
//*    d) Menu controls which are configured as "context menus" may optionally   *
//*       be assigned a label, even though the menu itself may be invisible by   *
//*       default. It is also possible to specify a label for a Menubar object.  *
//*       Example: For a context menu, the label is specified as for Textbox and *
//*       Scrollbox controls.  Invisible          Visible                        *
//*                            = = = = =          = = = =                        *
//*                            Hello World!       Hello World!                   *
//*                            ..............     +------------+                 *
//*                            ..............     | Item a     |                 *
//*                            ..............     | Item b     |                 *
//*                            ..............     | Item c     |                 *
//*                            ..............     +------------+                 *
//*                                                                              *
//*    In each case, the caret character '^' if present, is removed and replaced *
//*    by a pair of ANSI escape sequences enclosing the visible character        *
//*    following the caret. This character will be displayed with the            *
//*    "underline" text modifier: "Hello ^World!                                 *
//*              is displayed as: "Hello World!" with the 'W' underlined.        *
//*                                                                              *
//*    -- If field does not contain a caret indicator, (no hotkey specified),    *
//*       then the label text is not modified (but see Radiobutton note).        *
//*    -- If the character following the caret is not a valid hotkey             *
//*       signifier, that would be an application error. The caret will be       *
//*       removed, but no hotkey is assigned to the control.                     *
//*                                                                              *
//*                                                                              *
//* 2) Specified directly in the 'hkey' member.                                  *
//*    Example:  skf->fld[n].hkey = wcsALT_W ;                                   *
//*    a) Menubox controls that are members of a menu group have no labels,      *
//*       so the hotkey may be only be specified via the 'hkey' member of the    *
//*       skField class.                                                         *
//*    b) Textbox (ftB) and Scrollbox (ftSB) controls accept direct hotkey       *
//*       initialization; however, this is discouraged because there will be     *
//*       no label to provide a visual cue to the user.                          *
//*    c) If an ASCII control keycode is specified, it is automagically converted*
//*       to the equivalent ALT+n keycode.                                       *
//*       Example: wcsCTL_U is converted to wcsALT_U.                            *
//*       Note that reserved ASCII control keycodes may not be specified as      *
//*       hotkeys; for example, wcsCTL_V is reserved as the paste text command.  *
//*                                                                              *
//* If a hotkey is specified _both_ directly and within the label text, the      *
//* the hotkey in the label takes precedence.                                    *
//*                                                                              *
//* Technical Note:                                                              *
//* ---------------                                                              *
//* For fields which contain a hotkeys in the label, the reformatted label text  *
//* of the field will contain two ANSI escape sequences to set and then reset    *
//* the "underline" text modifier. As a consequence, the calling method may      *
//* incorrectly interpret the field width as including the characters of these   *
//* sequences and truncate the text to fit within the specified field width.     *
//* This would be an error: the width of the field is the sum of the column      *
//* widths of the _displayed_ text.                                              *
//* -- Even though escape sequences are constructed from printing characters,    *
//*    they are not visible text, and therefore have zero width. For this reason,*
//*    we adjust the 'wid' member to accomodate the reformatted "width" of the   *
//*    text.                                                                     *
//*                                                                              *
//* Radiobutton (ftRB):                                                          *
//* -------------------                                                          *
//* Separate the field data from the label data.                                 *
//* a) The first three(3) characters of the 'txt' member constitute the field    *
//*    data.                                                                     *
//* b) The remaining text is the label data, and the sum of a+b is the width of  *
//*    the Radiobutton field.                                                    *
//* c) The 'state' of the control is also verified and initialized.              *
//* d) The 'ip' (insertion point) for Radiobutton controls is always at one(1),  *
//*    which is between the displayed braces.                                    *
//*                                                                              *
//* Pushbutton (ftPB):                                                           *
//* ------------------                                                           *
//* The displayed text of a Pushbutton control _IS_ the control label, so no     *
//* additional processing is necessary.                                          *
//*                                                                              *
//* Direct Label Specification (for ftTB, ftSB and some ftMB controls):          *
//* --------------------------------------------------------                     *
//* The 'lab', 'lattr' and 'lpos' members are 'protected', so labels may not     *
//* be specified by writing directly to these data members.                      *
//* -- The primary method of specifying external labels is through the           *
//*    SetFieldLabel() method. Please see documentation for that method for      *
//*    additional information.                                                   *
//* -- A secondary method is available: the public skField labelConfig() method. *
//*    Please see that method for details.                                       *
//********************************************************************************

bool ACWin::init_Hotkeys ( skForm* skfp )
{
   //* This constant represents the number of columns *
   //* for the combined underline on/off ANSI escape  *
   //* sequences. These sequences actually require no *
   //* display space. Used when calculating the width *
   //* of a label with embedded ANSI sequences.       *
   const short ULONOFF_COLS = 3 + 4 ;

   skField* fptr ;
   gString gsAttr, gs ;
   const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
   modtab.getSequence ( acaUNDERLINE, modOn, modOff ) ;
   wchar_t hchar, hkey ;
   short hki ;
   bool status = false ;

   for ( short f = ZERO ; f < skfp->fCnt ; ++f )
   {
      fptr = &skfp->fld[f] ;              // point to target field

      //* Validate specified hotkey keycode for controls whose hotkey      *
      //* IS NOT embedded within the control itself.                       *
      //* Note that if hotkey is specified directly, there will be no      *
      //* visual cue for the user.                                         *
      //* Recommended: Except for Menubox (ftMB) which (usually) do not    *
      //* have labels, specify hotkey by embedding it in the control label.*
      if ( fptr->hkey != NULLCHAR )
      {
         if ( (fptr->type == ftPB) || (fptr->type == ftRB) )
            fptr->hkey = NULLCHAR ; // do not allow direct specification

         //* If hotkey was specified directly *
         else
         {
            hkey = fptr->hkey ;
            if ( (this->isHotkey ( hkey )) || (this->isFunky( hkey )) )
            { fptr->hkey = hkey ; status = true ; }
            else
               fptr->hkey = NULLCHAR ;
         }
      }

      //* Test for a hotkey embedded in the text of the control. *
      if ( (fptr->type == ftPB) || (fptr->type == ftRB) )
      {
         if ( (hki = fptr->txt.find( CARET )) >= ZERO )
         {
            fptr->wid = fptr->txt.gscols() - 1 ; // adjust field width

            //* Get the plain-text character which specifies the       *
            //* potential hotkey and convert it to the hotkey keycode. *
            hkey = hchar = fptr->txt.gstr()[hki + 1] ;
            if ( (char2Hotkey ( hkey )) ) // convert to hotkey
            {
               fptr->hkey = hkey ;        // save the hotkey
               //* Construct the ANSI sequence to enable/disable underlining. *
               gsAttr.compose( "%S%C%S", modOn, &hchar, modOff ) ;
               //* Remove the caret and the character which follows it. *
               fptr->txt.erase( hki, 2 ) ;
               //* Insert the formatted ANSI sequence. *
               fptr->txt.insert( gsAttr.gstr(), hki ) ;
               status = true ;
            }
            //* Invalid hotkey specifier.                 *
            //* Remove the caret character. (see note #2) *
            else
               fptr->txt.erase( hki, 1 ) ;
         }
         else     // adjust field width
            fptr->wid = fptr->txt.gscols() ;

         //* For Radiobuttons, split the text into field and label segments.*
         if ( fptr->type == ftRB )
         {
            fptr->ip = 1 ;                // reference the character between the braces
            gs = &fptr->txt.gstr()[3] ;   // capture and store the label text
            gs.copy( fptr->lab, labelLEN + LABPAD ) ;
            fptr->txt.limitChars( 3 ) ;   // field data: first three characters as control text
            //* If non-default Radiobutton character, extract and store in 'rbch'.*
            //* It is possible that the application could fool us by inserting a  *
            //* whitespace or non-printing char, but we assume (provisionally)    *
            //* that application was not written by a Windoze weenie.             *
            if ( (fptr->txt.gstr()[1]!= L' ') && (fptr->txt.gstr()[1] != wcsSDIAMOND) )
               fptr->rbch = fptr->txt.gstr()[1] ;

            //* If field label should be positioned to the  *
            //* left of the field, adjust positions.        *
            //* (see also labelConfig())                    *
            if ( fptr->lpos.col < ZERO )
            {
               fptr->lpos = fptr->orig ;
               gs = fptr->lab ;
               fptr->orig.col += gs.gscols() ;
               if ( (gs.find( L'\x001B' )) >= ZERO )
                  fptr->orig.col -= ULONOFF_COLS ;
            }
            else
               fptr->lpos = { fptr->orig.row, short(fptr->orig.col + 3) } ;
            this->setFieldState ( fptr, fptr->state ) ; // initialize field state
         }
         //* For Pushbuttons, IP is always zero. *
         else if ( fptr->type == ftPB )
            fptr->ip = ZERO ;
      }     // (ftPB || ftRB)

      //* If an external label was specified for the control.  *
      //* Note that a hotkey embedded within the label will    *
      //* overwrite a directly-specified value.                *
      //* Label position ('lpos'):                             *
      //* 'row' is a constant -1.                              *
      //* 'col' may be zero(default), or                       *
      //* 'col' may be the horizontally-centered position, or  *
      //* 'col' may be the special value of -1, indicating     *
      //*       that center position needs to be calculated.   *
      else if ( (*fptr->lab != NULLCHAR) && 
                ((fptr->type == ftTB) || (fptr->type == ftSB) || 
                 ((fptr->type == ftMB) && ((fptr->grp == noGROUP) || (fptr->hgt == 1)))) )
      {
         gs = fptr->lab ;
         this->setFieldLabel ( fptr, gs.ustr(), fptr->lattr, 
                               ((fptr->lpos.col == -1) ? true : false) ) ;
         status = true ;
      }     // (external label)
   }
   return status ;

}  //* End init_Hotkeys() *

//*************************
//*      skpIsHotkey      *
//*************************
//********************************************************************************
//* Private Method                                                               *
//* --------------                                                               *
//* Compare the specified keycode with the list of valid hotkey keycodes defined *
//* for the array of controls.                                                   *
//* If the specified keycode is a valid hotkey keycode, AND is assigned to a     *
//* write-enabled control object ('ro' flag reset), return index of the control. *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          wkey  : (by reference) keycode received from user                   *
//*                  If received keycode is in the range wcsCTL_A - wcsCTL_Z,    *
//*                  on return, it will have been converted to the "equivalent"  *
//*                  wcsALT_n value. (see note in isHotkey())                    *
//*          conv  : (optional, default: true)                                   *
//*                  (but see conditional compile directive in isHotkey())       *
//*                  if 'true':                                                  *
//*                  If received keycode is in the range wcsCTL_A - wcsCTL_Z,    *
//*                  on return, it will have been converted to the "equivalent"  *
//*                  wcsALT_n value. (see note about exceptions)                 *
//*                  if 'false: conversion is not performed.                     *
//*                                                                              *
//* Returns: if successful, index of control associated with the specified       *
//*             keycode                                                          *
//*          else, returns skf->fCnt i.e. not a valid control index              *
//********************************************************************************

short ACWin::skpIsHotkey ( const skForm* skfp, wchar_t& wkey, bool conv ) const
{
   short findx = skfp->fCnt ;

   if ( (this->isHotkey ( wkey, conv )) || (this->isFunky ( wkey )) )
   {
      for ( short f = ZERO ; f < skfp->fCnt ; ++f )
      {
         if ( (wkey == skfp->fld[f].hkey) && !skfp->fld[f].ro )
         { findx = f ; break ; }
      }
   }
   return findx ;

}  //* End skpIsHotkey() *

//*************************
//*       isHotkey        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Test the specified keycode for membership in the "hotkey" group.             *
//* The hotkey group consists of keycodes in the range:                          *
//*  1) ALT+n keycodes, wcsALT_A through wcsALT_RB                               *
//*  2) CTRL+n keycodes, wcsCTL_A through wcsCTL_7, _excluding_:                 *
//*     a) wcsCTL_A   (reserved) All text "selected"                             *
//*     b) wcsCTL_C   (reserved) Copy selected text to clipboard                 *
//*     c) wcsCTL_I   same keycode as wcsTAB                                     *
//*     d) wcsCTL_J   same keycode as wcsENTER                                   *
//*     e) wcsCTL_M   same keycode as wcsENTER                                   *
//*     f) wcsCTL_V   (reserved) Paste selected text from clipboard              *
//*     g) wcsCTL_X   (reserved) Cut selected text to clipboard                  *
//*     h) wcsCTL_Z   (reserved) Undo previous action                            *
//*     i) wcsCTL_4   (reserved) System "Quit-and-Dump" signal (not seen here)   *
//*                                                                              *
//* A special test is under conditional compile directive, ENABLE_C2A_CONVERT.   *
//* This directive controls whether the 'conv' flag will be recognized.          *
//* a) When enabled, and if the 'conv' flag is set, then:                        *
//*    ASCII control key combination which are not explicity assigned to a task  *
//*    (see list above) are converted to the equivalent ALT+n key combination.   *
//*       Example: wcsCTL_B will be converted to wcsALT_B.                       *
//*    The two keycodes would then be interpreted as the same hotkey keycode.    *
//*    This makes for a cleaner hotkey test (if you like that sort of thing).    *
//* b) When disabled, the 'conv' flag is ignored:                                *
//*    ASCII control key combinations are returned unchanged; thus,              *
//*    wcsCTL_B and wcsALT_B would be seen as _different_ hotkeys.               *
//*                                                                              *
//*                                                                              *
//* Input  : wkey  : (by reference) keycode to be tested                         *
//*          conv  : (optional, default: true -- but see conditional directive)  *
//*                  if 'true':                                                  *
//*                  If received keycode is in the range wcsCTL_A - wcsCTL_Z,    *
//*                  on return, it will have been converted to the "equivalent"  *
//*                  wcsALT_n value. (see note about exceptions)                 *
//*                  if 'false: conversion is not performed.                     *
//*                                                                              *
//* Returns: 'true' if is a member of the group, else 'false'                    *
//********************************************************************************
//* Programmer's Note:                                                           *
//* This test relies upon the fact that the ALT+n and CTRL+n hotkey definitions  *
//* are defined in strict numeric sequences: wcsALT_A through wcsALT_RB and      *
//* wcsCTL_A through wcsCTL_7, respectively.                                     *
//* If these keycode definitions change, this method must be updated to match.   *
//*                                                                              *
//* Note for future development:                                                 *
//* The Ctrl+keycode combinations for select/copy/cut/paste/undo are recognized  *
//* as valid commands ONLY when the field with focus is a text field (ftTB).     *
//* Therefore, if received when focus is on a non-text field, they _could be_    *
//* reported as valid hotkeys in that scenario. Is this worth exploring?         *
//********************************************************************************

bool ACWin::isHotkey ( wchar_t& wkey, bool conv ) const
{
   //* Controls conversion of CTL+n hotkeys *
   //*  to ALT+n hotkey (see note above)    *
   #define ENABLE_C2A_CONVERT (0)

   bool isHot = false ;

   if ( !(isHot = bool((wkey >= wcsALT_A) && (wkey <= wcsALT_RB))) )
   {
      if ( (wkey >= wcsCTL_A) && (wkey <= wcsCTL_Z) )
      {
         switch ( wkey )
         {
            case wcsCTL_A: case wcsCTL_C: case wcsCTL_I: case wcsCTL_J: 
            case wcsCTL_M: case wcsCTL_V: case wcsCTL_X: case wcsCTL_Z: 
               break ;  // (excluded keycodes)
            default:
               #if ENABLE_C2A_CONVERT != 0
               if ( conv )
                  wkey = wcsALT_Z - (wcsCTL_Z - wkey) ;  //* (Note the fancy math.) *
               #endif   // ENABLE_C2A_CONVERT
               isHot = true ;
               break ;
         }
      }
   }

   return isHot ;

}  //* End isHotkey() *

//*************************
//*        isFunky        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Test whether the specified keycode is within the Function Key range.         *
//* (definitions for the full Function Key group are within a sequential range)  *
//*                                                                              *
//*  Function keycodes:                                                          *
//*  wcsF01 - fcsF12, wcsSF01 - wcsSF12, wcsCF01 - wcsCF12                       *
//*                                                                              *
//* Input  : wkey  : keycode to be tested                                        *
//*                                                                              *
//* Returns: 'true' if is a member of the group, else 'false'                    *
//********************************************************************************

bool ACWin::isFunky ( wchar_t wkey ) const
{

   return ( bool((wkey >= wcsF01) && (wkey <= wcsCF12)) ) ;

}  //* End isFunky() *

//*************************
//*     setFieldLabel     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Assign a label, with or without hotkey, to the specified field.              *
//* - Labels may be assigned only to Textbox and Scrollbox controls              *
//*   (FldType ftTB/ftSB) and 'context' menus (FldType ftMB with grp==noGROUP).  *
//*   a) Labels are incompatible with Menu controls which are defined as         *
//*      sub-menus in a menu group.                                              *
//*      See SetFieldHotkey() for setting hotkeys for menu controls.             *
//*      NOTE: Experimentally, we allow an external label for Menubars,          *
//*            i.e. single-row menus. See 'menulab' below.                       *
//*   b) For Pushbuttons and Radiobutton controls (FldType ftPB/ftRB), the label *
//*      is integrated into the control definition, so are not processed here.   *
//*                                                                              *
//* Input  : fptr    : pointer to target field                                   *
//*          labText : display text, optionally including a "hotkey" character.  *
//*                    A hotkey is specified using the CARET character ('^')     *
//*                    followed by the ASCII character which indicates the       *
//*                    keycode to be used as a hotkey. (see example above)       *
//*          labAttr : (optional, interior of container window by default)       *
//*                    color attributes for label text.                          *
//*          center  : (optional, 'false' by default) horizontal offset          *
//*                    if set, center the label horizontally abovethe field      *
//*                    if reset, label offset is in the same column as the       *
//*                            left edge of the field                            *
//*                                                                              *
//* Returns: 'true' if label initialized successfully, else 'false'              *
//********************************************************************************
//* Notes:                                                                       *
//*  1) Label may not extend into the window border.                             *
//*  2) Label may not be an empty string.                                        *
//*  3) Label may not extend beyond the right edge of field.                     *
//*     Note that calculation of available display columns may be affected by    *
//*     integer division rounding, AND that the calculation discounts the        *
//*     width of the CARET (if any) which simplifies the calculation.            *
//*  4) Label may not contain newline characters. If it does, they will be       *
//*     silently removed.                                                        *
//*  5) If label includes a hotkey, it must be one of the defined valid hotkeys. *
//*     The specified hotkey must be unique to the window. No other control may  *
//*     be assigned the same hotkey.                                             *
//*  6) It is the application's responsibility to ensure that the label does not *
//*     violate the space reserved for any other item. A programatic test for    *
//*     such a violation would just be too expensive.                            *
//*                                                                              *
//********************************************************************************

bool ACWin::setFieldLabel ( skField *fptr, const char* labText, 
                            acAttr labAttr, bool center )
{
   bool menulab = false ;                 // set if menu label allowed
   bool status  = false ;                 // return value

   if ( fptr->type == ftMB )              // menu label test (see note above)
   { if ( (fptr->grp == noGROUP) || (fptr->hgt == 1) ) { menulab = true ; } }

   if ( (labText[0] != NULLCHAR) && (fptr->orig.row > ZERO) &&
        ((fptr->type == ftTB) || (fptr->type == ftSB) || menulab)
      )
   {
      const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
      modtab.getSequence ( acaUNDERLINE, modOn, modOff ) ;
      gString gslab( labText ), gsAttr ;
      WinPos wprel( short(fptr->orig.row - 1), short(fptr->orig.col) ) ;
      wchar_t hkey, hchar ;               // hotkey and displayed character
      short availCols = fptr->wid,        // cols available for label display
            labCols = gslab.gscols(),     // raw column count
            hki ;                         // hotkey index

      gslab.erase( NEWLINE, ZERO, false, true ) ;  // remove any newline characters

      //* Do not include width of caret character in calculations *
      if ( (gslab.find( CARET )) >= ZERO ) { --labCols ; }

      //* If label width exceeds field width, centering is meaningless. *
      if ( labCols > fptr->wid ) { center = false ; }

      if ( center )                       // if label is to be centered
         wprel.col += (fptr->wid / 2) - (labCols / 2) ;

      //* Truncate label length to fit target buffer. *
      if ( (gslab.gschars()) >= labelLEN ) { gslab.limitChars( labelLEN - 1 ) ; }
      //* Truncate width to fit available display area. (see note above)*
      if ( (labCols + fptr->lpos.col) > availCols )
         gslab.limitCols( availCols - fptr->lpos.col ) ;

      //* If adjusted text is not an empty string. *
      //*   (some designers are morons, beware!)   *
      if ( (gslab.gschars()) > 1 )
      {
         //* If text contains a hotkey designation, *
         //* convert it to a hotkey keycode.        *
         if ( (hki = gslab.find( CARET )) >= ZERO )   // scan for a hotkey
         {
            hkey = hchar = gslab.gstr()[hki + 1] ;
            if ( (char2Hotkey ( hkey )) )       // convert to hotkey
            {
               fptr->hkey = hkey ;              // save the hotkey
               gsAttr.compose( "%S%C%S", modOn, &hchar, modOff ) ;
               gslab.erase( hki, 2 ) ;
               gslab.insert( gsAttr.gstr(), hki ) ;
            }
         }
         //* Save the label data. *
         gslab.copy( fptr->lab, labelLEN + LABPAD ) ;
         fptr->lattr = labAttr ;
         fptr->lpos  = wprel ;
         status = true ;
      }
   }
   return status ;

}  // setFieldLabel() *

//*********************
//*  setFieldOutline  *
//*********************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Initialize the outline around the outside of the specified field within      *
//* the window border. The display is not updated.                               *
//*                                                                              *
//* Note: This method assumes controls within an ACWin object, not ACDlg.        *
//*                                                                              *
//* Input  : fIndx   : index of target field                                     *
//*          lType   : (optional, ltSINGLE by default) line style                *
//*                     member of enum LineType: ltSINGLE or ltDUAL              *
//*          lnAttr  : (optional, acaUSEDFLT by default)                         *
//*                    If specified, color attributes to be used.                *
//*                    If not specified, stored text attributes used.            *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' incompatible field type or insufficient space around field  *
//********************************************************************************

bool ACWin::setFieldOutline ( skField* fptr, LineType lType, acAttr lnAttr )
{
   short brow = fptr->orig.row - 1,       // base row
         bcol = fptr->orig.col - 1,       // base column
         bhgt = fptr->hgt + 2,            // box height
         bwid = fptr->wid + 2 ;           // box width
   bool  status = false ;                 // return value

   //* Range check position and dimensions *
   if ( (fptr->type != ftMB) &&
        (brow >= ZERO) && (bcol >= ZERO) &&
        ((brow + bhgt) <= this->txtRows) &&
        ((bcol + bwid) <= this->txtCols) )
   {
      fptr->btype = lType ;               // save the line type
      fptr->battr = lnAttr ;              // save the line attributes
      fptr->bdr   = true ;                // set the 'valid border' flag
      status = true ;
   }
   return status ;

}  // setFieldOutline() *

//*************************
//*      char2Hotkey      *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* Convert the specified ASCII character to the equivalent hotkey character.    *
//*                                                                              *
//* Hotkeys are defined within one of two groups:                                *
//* ---------------------------------------------                                *
//* 1) As the Left ALT key plus one other key.                                   *
//*    -- Alphabetic characters are processed as case-insensitive.               *
//*    -- The number characters '0'-'9' and a range of punctuation character     *
//*       combinations are also supported. These are included primarily for      *
//*       support of languages which do not use the standard Latin alphabet.     *
//*       -- Note that the number keys and their shifted versions produce the    *
//*          same keycodes: Example: ALT+1 and ALT+! are both translated         *
//*          as wcsALT_1.                                                        *
//* 2) As the CTRL key plus one other key.                                       *
//*    -- Alphabetic characters plus are processed as case-insensitive.          *
//*    -- ASCII '4' '5' '6' '7' are members of this group, but they may          *
//*       conflict with the ALT+n punctuation definitions and are therefore      *
//*       excluded as potential hotkeys.                                         *
//*                                                                              *
//* IMPORTANT NOTE: Not all CTRL+n key combinations are available as hotkeys.    *
//*                 For this reason, the ASCII characters are always             *
//*                 converted to the equivalent ALT+n keycode.                   *
//*                                                                              *
//* Input  : wkey  : on entry, keycode to be converted                           *
//*                  if successful, receives converted keycode                   *
//*                                                                              *
//* Returns: 'true'  if keycode is converted to hotkey                           *
//*          'false' if keycode cannot be converted (wkey set to nullchar)       *
//********************************************************************************

bool char2Hotkey ( wchar_t& wkey ) 
{
   wchar_t upkey = toupper ( wkey ) ;     // uppercase version of caller's character
   bool    status = true ;                // return value (hoping for the best)

   if ( (upkey >= L'A') && (upkey <= L'Z') )
   {
      wkey = wcsALT_A + (upkey - L'A') ;
   }
   else
   {
      switch ( upkey )
      {
         case L'0':  case L')':  wkey = wcsALT_0 ; break ;  // Alt + [0 | )]  (Right Parenthesis)
         case L'1':  case L'!':  wkey = wcsALT_1 ; break ;  // Alt + [1 | !]  (EXclamation)
         case L'2':  case L'@':  wkey = wcsALT_2 ; break ;  // Alt + [2 | @]  (AT)
         case L'3':  case L'#':  wkey = wcsALT_3 ; break ;  // Alt + [3 | #]  (HAsh)
         case L'4':  case L'$':  wkey = wcsALT_4 ; break ;  // Alt + [4 | $]  (DOllar)
         case L'5':  case L'%':  wkey = wcsALT_5 ; break ;  // Alt + [5 | %]  (PErcent)
         case L'6':  case L'^':  wkey = wcsALT_6 ; break ;  // Alt + [6 | ^]  (CAret)
         case L'7':  case L'&':  wkey = wcsALT_7 ; break ;  // Alt + [7 | &]  (AMpersand)
         case L'8':  case L'*':  wkey = wcsALT_8 ; break ;  // Alt + [8 | *]  (AStrisk)
         case L'9':  case L'(':  wkey = wcsALT_9 ; break ;  // Alt + [9 | (]  (Left Parenthesis)
         case L'"':  wkey = wcsALT_DQ ;   break ;  // Alt + "  (Double Quote)
         case L'\'': wkey = wcsALT_SQ ;   break ;  // Alt + '  (Single Quote)
         case L'+':  wkey = wcsALT_PL ;   break ;  // Alt + +  (PLus)
         case L',':  wkey = wcsALT_CO ;   break ;  // Alt + ,  (COmma)
         case L'-':  wkey = wcsALT_MI ;   break ;  // Alt + -  (MInus)
         case L'.':  wkey = wcsALT_ST ;   break ;  // Alt + .  (STop)
         case L'/':  wkey = wcsALT_FS ;   break ;  // Alt + /  (Forward Stroke)
         case L':':  wkey = wcsALT_CN ;   break ;  // Alt + :  (ColoN)
         case L';':  wkey = wcsALT_SC ;   break ;  // Alt + ;  (SemiColon)
         case L'<':  wkey = wcsALT_LT ;   break ;  // Alt + <  (Less Than)
         case L'=':  wkey = wcsALT_EQ ;   break ;  // Alt + =  (EQual)
         case L'>':  wkey = wcsALT_GT ;   break ;  // Alt + >  (Greater Than)
         case L'?':  wkey = wcsALT_QU ;   break ;  // Alt + ?  (QUestion)
         case L'[':  wkey = wcsALT_LS ;   break ;  // Alt + [  (Left Square) [probably unavailable]
         case L'\\': wkey = wcsALT_BS ;   break ;  // Alt + \  (Back Stroke)
         case L']':  wkey = wcsALT_RS ;   break ;  // Alt + ]  (Right Square)
         case L'_':  wkey = wcsALT_US ;   break ;  // Alt + _  (UnderScore)
         case L'{':  wkey = wcsALT_LB ;   break ;  // Alt + {  (Left Brace)
         case L'|':  wkey = wcsALT_VB ;   break ;  // Alt + |  (Vertical Bar)
         case L'}':  wkey = wcsALT_RB ;   break ;  // Alt + }  (Right Brace)

         default:       status = false ;     break ;
      }
   }
   return status ;

}  //* End char2Hotkey()

//*************************
//*     menubarToken      *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* This method scans the text of a Menubar (single-row ftMB) control, and       *
//* returns the 'ip' character offset of the next/previous token in the text     *
//* and 'smi' receives the token number referenced by 'ip'. This indicates the   *
//* index of the sub-menu associated with that token.                            *
//* Important Note: It is the application designer's responsibility to assure    *
//*                 the sub-menus of the menubar group follow the menubar in     *
//*                 numeric sequence.                                            *
//* If the Menubar is at Index 2:  | File  Edit  View  Help |                    *
//* then sub-menus must follow at index File==3, Edit==4, View==5 and Help==6.   *
//*                                                                              *
//* Note that the value returned in 'smi' is not the actual index of the sub-menu*
//* within the skField array. It is the offset from the menubar index.           *
//*  Example: If Menubar is at index 2, then target sub-menu is 2 + smi.         *
//*                                                                              *
//* -- If target token is not found, then on return 'ip' will reference the      *
//*    first (leftmost) token of the field on the assumption that caller will    *
//*    close the menubar on return.                                              *
//*    -- Determining the field which will obtain focus when menubar closes      *
//*       is PROBABLY: For wch==wcsSTAB, then field before menubar, and for      *
//*       wch==wcsTAB, then first field after menu group; however, caller must   *
//*       make that determination.                                               *
//* -- If the specified keycode "selects" the current token, via Space or Enter  *
//*    keys, then 'ip' is not modified.                                          *
//*                                                                              *
//* Input  : wch  : keycode received from user should be one of:                 *
//*                 wcsTAB or wcsRARROW   Scan to the next token (if any).       *
//*                                       'ip' receives offset of next token     *
//*                                       (If not found, then first token.)      *
//*                 wcsSTAB or wcsLARROW  Scan to the previous token (if any).   *
//*                                       'ip' receives offset of previous token *
//*                                       (If not found, then first token.)      *
//*                 wcsSPACE or wcsENTER  calculate the token number (1-based)   *
//*                                       IP references first (leftmost) token   *
//*          wtxt : pointer to menubar text data                                 *
//*          ip   : (by reference) current IP on entry,                          *
//*                 receives the IP (offset) of the target token                 *
//*          smi  : (by reference) if token scan is successful, 'smi' receives   *
//*                 the token number (see note). If token not found, returns 0.  *
//*                                                                              *
//* Returns: 'true'  if IP references a new token, (or "selected" token)         *
//*          'false' if token not found in specified direction                   *
//********************************************************************************

bool menubarToken ( wchar_t wch, const wchar_t* wtxt, int& ip, int& smi )
{
   bool status = false ;                  // return value

   smi = ZERO ;                           // no sub-menu is referenced

   if ( (wch == wcsTAB) || (wch == wcsRARROW) )
   {
      //* Step over the current token. *
      while ( (wtxt[ip] != wcsSPACE) && (wtxt[ip] != NULLCHAR) ) { ++ip ; }
      //* If not at end-of-text, scan over whitespace. *
      if ( wtxt[ip] != NULLCHAR )
      { while ( (wtxt[ip] == wcsSPACE) && (wtxt[ip] != NULLCHAR) ) { ++ip ; } }

      //* If token found, return success. *
      if ( wtxt[ip] != NULLCHAR ) { status = true ; }
   }
   else if ( (wch == wcsSTAB) || (wch == wcsLARROW) )
   {
      //* Scan to end of previous token (if any). *
      do
      { if ( --ip < ZERO ) { ip = ZERO ; } }
      while ( (ip > ZERO) && (wtxt[ip] == wcsSPACE) ) ;

      if ( ip > ZERO )
      {
         //* Scan to beginning of new token. *
         while ( ip > ZERO )
         {
            if ( wtxt[--ip] == wcsSPACE )
            { ++ip ; break ; }
         }

         //* If token found, return success. *
         if ( wtxt[ip] != wcsSPACE ) { status = true ; }
      }
   }

   //* If token found or if "selection key", get the index   *
   //* of the sub-menu associated with the token. (see note) *
   if ( status || (wch == wcsENTER || wch == wcsSPACE) )
   {
      int indx = -1 ;
      do
      {
         //* Scan to head of first token. *
         while ( wtxt[++indx] == wcsSPACE ) ;
         //* Scan to head of 'current' token. *
         if ( wtxt[indx] != NULLCHAR )
         {
            ++smi ;
            if ( indx == ip )             // current token found
            { status = true ; break ; }
         }
         while ( (wtxt[indx] != wcsSPACE) && (wtxt[indx] != NULLCHAR) )
            ++indx ;
      }
      while ( wtxt[indx] != NULLCHAR ) ;
   }

   //* Menubar will close, so 'ip' receives offset of first  *
   //* token, and 'smi' == zero.                             *
   else
   {
      for ( int i = ZERO ; wtxt[i] != NULLCHAR ; ++i )
      { if ( wtxt[i] != wcsSPACE ) { ip = i ; break ; } }
   }
   return status ;

}  //* End menubarToken() *

//*************************
//*    EnableFootnotes    *
//*************************
//********************************************************************************
//* Enable or disable output to the window written through the 'Footnote'        *
//* group of methods.                                                            *
//*                                                                              *
//* Input  : enable: set   : enable output from the Footnote method.             *
//*                  reset : disable output from Footnote method.                *
//*                                                                              *
//* Returns: current state of the footnote flag                                  *
//********************************************************************************

bool ACWin::EnableFootnotes ( bool enable )
{

   return ( (this->skf->foot = enable) ) ;

}  //* End EnableFootnotes() *

//*************************
//*       Footnote        *
//*************************
//********************************************************************************
//* Write a diagnostic message in the lower border of the window,                *
//* Optionally, wait, a moment, then erase the message.                          *
//*                                                                              *
//* For best results the first and last characters of the message should be      *
//* spaces, but this is not enforced.                                            *
//*                                                                              *
//* Input  : msg   : text of message to be written.                              *
//*                  -- Message will be truncated if necessary to the            *
//*                     width of the window minus 4.                             *
//*                  -- If 'msg' is an empty string, then erase any              *
//*                     existing message and restore the border.                 *
//*          pause : time in tenths-of-a-second to wait before erasing message   *
//*                  a value of zero(0) indicates that message is to             *
//*                  remain visible on return.                                   *
//*          color : (optional, default: existing border attributes)             *
//*                  foreground/background color attributes for message display  *
//*          center: (optional, default: 'false')                                *
//*                  if 'true',  center the message horizontally                 *
//*                  if 'false', message is positioned at left edge of the space *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::Footnote ( const gString& msg, short pause, acAttr color, bool center )
{
   const wchar_t leftTee[]  = { wcsLTEEs, wcsLTEEb, wcsLTEEd, wcsSPACE } ;
   const wchar_t rightTee[] = { wcsRTEEs, wcsRTEEb, wcsRTEEd, wcsSPACE } ;

   //* If flag not enabled, return without writing message. *
   if ( ! this->skf->foot )
      return ;
   //* If 'msg' is an empty string, redraw the window border.*
   //* (erases any existing footer message)                  *
   if ( (msg.gschars()) == 1 )
   { this->drawBorder () ; return ; }

   short maxWidth = this->width - 4,
         teeIndex = (this->bdrStyle == ltHORIZ)      ? 3 :  // borderless
                    (this->bdrStyle == ltDUAL)       ? 2 :  // dual-line
                    (this->bdrStyle == ltSINGLEBOLD) ? 1 :  // bold single-line
                    ZERO ;                                  // single-line

   gString gsTxt = msg, gsOut ;
   gsTxt.limitCols( maxWidth ) ;
   gsOut.compose( "%C%S%C", &rightTee[teeIndex], msg.gstr(), &leftTee[teeIndex] ) ;
   WinPos wp( short(this->winBase.row + this->height - 1),
              short(this->winBase.col + 1) ) ;
   if ( center )
      wp.col = this->winBase.col + this->width / 2 - gsOut.gscols() / 2 ;

   //* Set color attributes for the message. *
   this->setAttributes ( (color == acaUSEDFLT) ? this->bdrAttr : color ) ;
   this->acSetCursor ( wp ) ;
   this->ttyWrite ( gsOut ) ;

   //* Wait the specified number of tenths-of-a-second, *
   //* then restore the lower border.                   *
   if ( pause > ZERO )
   {
      this->nsleep ( pause ) ;
      this->drawBorder () ;
   }

   //* Restore fgnd/bkgnd attributes to current terminal *
   //* attributes and return cursor to field with focus. *
   this->restoreAttributes () ;
   this->setCursor2IP ( this->skf ) ;

}  //* End Footnote() *

void ACWin::Footnote ( const wchar_t* msg, short pause, acAttr color, bool center )
{

   gString gs( msg ) ;
   this->Footnote ( gs, pause, color, center ) ;

}  //* End Footnote() *

void ACWin::Footnote ( const char* msg, short pause, acAttr color, bool center )
{

   gString gs( msg ) ;
   this->Footnote ( gs, pause, color, center ) ;

}  //* End Footnote() *

//*************************
//*        Sidebar        *
//*************************
//********************************************************************************
//* Display a message in the left or right border of the window.                 *
//* The Sidebar may have no practical value, but it's fun. :-)                   *
//*                                                                              *
//* Technical note on original cursor position.                                  *
//* -------------------------------------------                                  *
//* Ideally, the cursor position on exit should be the position on entry.        *
//* Saving the original position via a call to acGetCursor() is functional,      *
//* but UNBEARABLY slow (> 0.5 seconds).                                         *
//* Instead, before return, the field with focus is refreshed, positioning the   *
//* cursor in that field. There is no guarantee that is where the cursor         *
//* originally was; therefore, it is recommended that caller explicitly set the  *
//* cursor position on return.                                                   *
//*                                                                              *
//* Input  : msg   : text of message to be written.                              *
//*                  -- Message will be truncated if necessary to the            *
//*                     height of the window minus 4.                            *
//*                  -- If 'msg' is an empty string, then erase any              *
//*                     existing message and restore the border.                 *
//*          left  : if 'true',  message is displayed in left edge border        *
//*                  if 'false', message is displayed in right edge border       *
//*          color : (optional, existing border attributes by default)           *
//*                  foreground/background color attributes for message display  *
//*          offset: (optional, zero by default) Specify the offset              *
//*                   (number of rows) from vertically centered position.        *
//*                  if  0,  message is vertically centered                      *
//*                  if <0,  message is offset upward from centered by           *
//*                          specified number of rows.                           *
//*                  if >0,  message is offset downward from centered by         *
//*                          specified number of rows.                           *
//*          clear : (optional, 'false' by default)                              *
//*                  if 'true',  clear any existing message from the border      *
//*                              before writing the new message                  *
//*                  if 'false', existing messages are unaffected                *
//*                              (unless the new message overwrites an old one)  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Example:            ┌────────┤  Title  ├──────────┐                          *
//* --------            ┴                             │                          *
//*                     H                             │                          *
//*                     i                             ┴                          *
//*                     ┬                             M                          *
//*                     │                             o                          *
//*                     │                             m                          *
//*                     │                             !                          *
//*                     │                             ┬                          *
//*                     │                             │                          *
//*                     │                             │                          *
//*                     └─────┤ Smelly Footnote ├─────┘                          *
//*                                                                              *
//*  ACWin *win = new ACWin ( ... ) ;                                            *
//*  win->SetTitle ( "  Title  " ) ;                                             *
//*  win->Sidebar ( "Hi", true, acaATTR_DFLT, -2) ;                              *
//*  win->Footnote ( " Smelly Footnote ", 0, acaATTR_DFLT, false ) ;             *
//*  win->Sidebar ( "Mom!", false, acAttr(acaFG_GREEN | acaBG_BLUE), 0) ;        *
//*                                                                              *
//*  NOTE: A live test is under conditional compile in Test_Dialog().            *
//*                                                                              *
//********************************************************************************

void ACWin::Sidebar ( const wchar_t* msg, bool left, acAttr color, short offset, bool clear )
{
   gString gs( msg ) ;                       // working copy of message
   WinPos  wp = this->winBase ;              // display position
   short   bdrRows = this->txtRows,          // max message height
           rowModulus = this->txtRows % 2,   // modulus (0 or 1)
           rowTop  = this->txtBase.row,      // first available row (just below top border)
           rowBot  = rowTop + bdrRows - 1,   // last available row (just above bottom border)
           maxOffset,                        // maximum starting row for message
           msgRows,                          // rows required by message (incl. delimiters)
           msgModulus ;                      // modulus (0 or 1)

   //* If specified, redraw the window border to *
   //* erase any existing Sidebars or Footnotes. *
   if ( clear || (gs.gschars() == 1) )
   {
      this->drawBorder () ;
      if ( gs.gschars() == 1 )               // message is empty string
         return ;                            // note the early return
   }

   gs.erase( NEWLINE, ZERO, false, true ) ;  // erase all newlines
   gs.limitChars( bdrRows - 2 ) ;            // truncate to max message height
   msgRows = gs.gschars() - 1 + 2 ;          // rows required by message (incl. delimeters)
   msgModulus = msgRows % 2 ;                // testing for off-by-one
   maxOffset = rowBot - msgRows + 1 ;        // upper range limit
   for ( int i = gs.gschars() - 1 ; i > ZERO ; --i ) // create vertical output
      gs.insert( NEWLINE, i ) ;
   if ( ! left )                             // if right border specified
      wp.col += this->width - 1 ;

   //* Calculate position for vertically centered message. *
   wp.row = (rowTop + bdrRows / 2) - (msgRows / 2) ;
   if ( (msgModulus > ZERO) && (rowModulus == ZERO) )
      wp.row -= msgModulus ;
   if ( wp.row < rowTop )
      wp.row = rowTop ;

   if ( offset < ZERO )                      // shift upward
   {
      if ( (wp.row + offset) >= rowTop )
         wp.row += offset ;
      else
         wp.row = rowTop ;
   }
   else if ( offset > ZERO )                 // shift downward
   {
      if ( (wp.row + offset) <= maxOffset )
         wp.row += offset ;
      else
         wp.row = maxOffset ;
   }

   this->setAttributes ( this->bdrAttr ) ;   // set border attributes
   this->acSetCursor ( wp ) ;                // set cursor
   this->ttyWrite ( wcsBTEE ) ;              // write upper T character
   ++wp.row ;                                // advance to next row
   if ( color != acaUSEDFLT )                // set alternate colors (if specified)
      this->setAttributes ( color ) ;
   wp = this->acWrite ( wp, gs ) ;           // write the message
   if ( color != acaUSEDFLT )                // return to border colors
      this->setAttributes ( this->bdrAttr ) ;
   this->ttyWrite ( wcsTTEE ) ;              // write lower T character
                                             // (cursor is already in position)

   this->RefreshField () ;                   // restore cursor position. (see note above)

   //* Restore fgnd/bkgnd attributes to current terminal attributes.*
   this->restoreAttributes () ;

}  //* End Sidebar() *

void ACWin::Sidebar ( const gString& msg, bool left, acAttr color, short offset, bool clear )
{
   this->Sidebar ( msg.gstr(), left, color, offset, clear ) ;
}  //* End Sidebar() *

void ACWin::Sidebar ( const char* msg, bool left, acAttr color, short offset, bool clear )
{
   gString gs( msg ) ;
   this->Sidebar ( gs.gstr(), left, color, offset, clear ) ;
}  //* End Sidebar() *

//*************************
//*   EstablishCallback   *
//*************************
//********************************************************************************
//* Establish (or disable) a callback method which gives the application layer   *
//* access to user input so that text fields and other controls in the parent    *
//* window may be dynamically updated.                                           *
//* The specified method will be called from within the user-input loop of the   *
//* EditForm() method.                                                           *
//* The external method handles situations that the parent window cannot.        *
//* For example:                                                                 *
//*  - secondary validation of text data.                                        *
//*  - respond in real time to user keyboard input                               *
//*  - manual update of non-active controls i.e. display-only controls           *
//*    that user can view, but cannot access                                     *
//*  - signal that the window is to be closed                                    *
//*                                                                              *
//* Input  : valid pointer to caller's method of the correct type                *
//*          OR NULL pointer to disable existing callback                        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::EstablishCallback ( CUPTR methodname )
{

   this->callbkptr = methodname ;      // save the pointer

   //* Initial call to callback method initializes variables and other     *
   //* first-call stuff as specified when 'firstTime' parameter == 'true'. *
   if ( this->callbkptr != NULL )
      this->callbkptr ( ZERO, wchar_t(ZERO), true ) ;

}  //* End EstablishCallback() *

//*********************
//*  dumpConfig2Log   *
//*********************
//********************************************************************************
//* Private Method: (debugging only)                                             *
//* --------------------------------                                             *
//* Debugging method: Write key configuration data to log file.                  *
//*                                                                              *
//* Input  : title : title text for output block                                 *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_WDUMP != 0
void ACWin::dumpConfig2Log ( const wchar_t* title )
{
   if ( (ofsdbg.is_open()) )
   {
      gsdbg.compose( "%S\n  locale: %s", title, this->acGetLocale () ) ;
      ofsdbg << gsdbg.ustr() << endl ;

      gsdbg.compose( 
         "  termRows:%hd termCols:%hd fontNum:%hd fullReset:%hhd wideStream:%hhd cchAlert:%hhd\n"
         "  tset: inpBuf:%hhd inpBlk:%hhd inpEch:%s cchType:%s quitCap:%hhd suspCap:%hhd flowCap:%hhd", 
         &this->termRows, &this->termCols, &this->fontNum, &this->fullReset, 
         &this->wideStream, &this->tset->cchAlert, 
         &this->tset->inpBuf, &this->tset->inpBlk, 
         (this->tset->inpEch  == noEcho    ? "noEcho"    :
          this->tset->inpEch  == termEcho  ? "termEcho"  :
          this->tset->inpEch  == softEchoA ? "softEchoA" :
          this->tset->inpEch  == softEchoB ? "softEchoB" :
          this->tset->inpEch  == softEchoC ? "softEchoC" :
          this->tset->inpEch  == softEchoD ? "softEchoD" : "softEchoE"),
         (this->tset->cchType == cchtTerm  ? "cchtTerm"  :
          this->tset->cchType == cchtExit  ? "cchtExit"  :
          this->tset->cchType == cchtUser  ? "cchtUser"  :
          this->tset->cchType == cchtCopy  ? "cchtCopy"  : "cchtIgnore"),
         &this->tset->quitCap, &this->tset->suspCap, &this->tset->flowCap ) ;
      ofsdbg << gsdbg.ustr() << endl ;
   }
}  //* End dumpConfig2Log() *
#endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_WDUMP

//*********************
//*   dumpField2Log   *
//*********************
//********************************************************************************
//* Private Method: (debugging only)                                             *
//* --------------------------------                                             *
//* Debugging method: Write selected skField data to log file.                   *
//* See also GetFieldData().                                                     *
//*                                                                              *
//* Input  : fptr   : pointer to target field                                    *
//*          title  : title text for output line                                 *
//*          verbose: (optional, 'false' by default)                             *
//*                   if set, add individual row-head data to report             *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FDUMP != 0
void ACWin::dumpField2Log ( const skField* fptr, const wchar_t* title, bool verbose ) const
{
   if ( (ofsdbg.is_open()) )
   {
      const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
      modtab.getSequence ( acaUNDERLINE, modOn, modOff ) ;
      gString gsOut, gsLab = fptr->lab, gsTit = title ;
      int off ;
      if ( gsLab.gschars() == 1 ) { gsLab = "(none)" ; }
      else if ( (off = gsLab.find( L'\x1B' )) >= ZERO )
      {
         gsLab.insert( CARET, off ) ;
         gsLab.erase( modOn ) ;
         gsLab.erase( modOff ) ;
      }
      if ( gsTit.gschars() == 1 )
         gsTit = "none" ;
      gsOut.compose( 
         "%S\n"
         "Text:\n%S\n"
         "orig:%02hd:%02hd  cur:%02hd:%02hd  ip:%-3d    hgt:%03hd  wid:%03hd  "
            "type:%hd  flow:%hd  aca:%08X\n"
         "rbch:%04X   hkey:%04X  grp:%04X  state:%hhd  bdr :%hhd   hide:%hhd  "
            "lock:%hhd  ro :%hhd\n",
         gsTit.gstr(), fptr->txt.gstr(),
         &fptr->orig.row, &fptr->orig.col,   &fptr->cur.row,  &fptr->cur.col,
         &fptr->ip,       &fptr->hgt,        &fptr->wid,      &fptr->type, 
         &fptr->flow,     &fptr->aca.acaVal, &fptr->rbch,     &fptr->hkey,
         &fptr->grp,      &fptr->state,      &fptr->bdr,      &fptr->hide,
         &fptr->lock,     &fptr->ro ) ;
      ofsdbg << gsOut ; ofsdbg.flush() ;
      gsOut.compose(
         "off :%-3d    shft:%-3d   sela:%-3d  selb:%-3d btype:%hd  "
            "lpos:%02hd:%02hd\n"
            "lattr:%08X  battr:%08X   lab:%S\n",
         &fptr->off,      &fptr->shft,     &fptr->sela,  &fptr->selb,  &fptr->btype, 
         &fptr->lpos.row, &fptr->lpos.col, &fptr->lattr, &fptr->battr, gsLab.gstr() ) ;
      ofsdbg << gsOut ; ofsdbg.flush() ;
      this->dumpMrp2Log ( fptr ) ;
   }
}  //* End dumpField2Log() *
#endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FDUMP

//*********************
//*    dumpMrp2Log    *
//*********************
//********************************************************************************
//* Private Method: (debugging only)                                             *
//* --------------------------------                                             *
//* Debugging method: For specified field, write MRParse data to log file.       *
//*                                                                              *
//* Input  : fptr   : pointer to target field                                    *
//*          title  : (optional, default: null pointer) title text for output    *
//*          brief  : (optional, default: false)                                 *
//*                   if true,  report only indexed line items                   *
//*                   if false, report all line items                            *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && (DEBUG_MDUMP != 0 || DEBUG_FDUMP != 0)
void ACWin::dumpMrp2Log ( const skField* fptr, const wchar_t* title, bool brief ) const
{
   gString gsOut( "   " ) ;
   bool report ;                          // set if item to be reported

   if ( title != NULL ) { gsOut.insert( title ) ; }   // if title text specified
   gsOut.append( "field:%hd:%hd cursor:%hd:%hd\n", 
                 &fptr->hgt, &fptr->wid, &fptr->cur.row, &fptr->cur.col ) ;
   gsOut.padCols( (gsOut.gscols() * 2), L'-' ) ;
   gsOut.append( NEWLINE ) ;

   for ( short f = ZERO ; f < fptr->mrp.rCnt ; ++f )
   {
      report = bool( !brief || 
                     (f == fptr->mrp.rtop) || (f == fptr->mrp.rbot) ||
                     (f == fptr->mrp.rlst) || (f == fptr->mrp.rcur) ) ;
      if ( report)
      {
         if ( brief )
            gsOut.append( "%02hd) [%3d] ", &f, &fptr->mrp.roff[f] ) ;
         else
            gsOut.append( "[%3d] ", &fptr->mrp.roff[f] ) ;
      }
      if ( f == fptr->mrp.rtop )      { gsOut.append( L"rtop " ) ; }
      else if ( f == fptr->mrp.rbot ) { gsOut.append( L"rbot " ) ; }
      else if ( f == fptr->mrp.rlst ) { gsOut.append( L"rlst " ) ; }
      if ( f == fptr->mrp.rcur )
      {
         gsOut.append( "rcur: ip:%03d bov:%03d eov:%03d eor:%03d shft:%02d", 
                       &fptr->ip, &fptr->mrp.bov, &fptr->mrp.eov, &fptr->mrp.eor, &fptr->shft ) ;
         if ( fptr->mrp.ovr ) { gsOut.append( " *ovr" ) ; }
      }
      if ( report )
         gsOut.append( NEWLINE ) ;
   }
   ofsdbg << gsOut << endl ;

}  //* End dumpMrp2Log() *
#endif   // DEBUG_ANSICMD && DEBUG_LOG && (DEBUG_MDUMP != 0 || DEBUG_FDUMP != 0)

//*********************
//*   dumpFifo2Log    *
//*********************
//********************************************************************************
//* Private Method: (debugging only)                                             *
//* --------------------------------                                             *
//* Debugging method: Dump contents of specified BlockFifo object to log file.   *
//*                                                                              *
//* Input  : bptr   : pointer to target field                                    *
//*          title  : (optional, null pointer by default) title text for output  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
void ACWin::dumpFifo2Log ( const BlockFifo* bptr, const wchar_t* title ) const
{
   if ( ofsdbg.is_open() )
   {
      gString gsOut( "\nBlockFifo - " ) ;

      if ( title != NULL )
         gsOut.append( "%S\n", title ) ; // if title text specified
      else
         gsOut.append( "dump        \n" ) ;
      gsOut.padCols( (gsOut.gscols() + 40), L'-' ) ;
      //gsOut.append( NEWLINE ) ;
      ofsdbg << gsOut.ustr() << endl ;

      gsOut.compose( "base:%02hd/%02hd top:%02hd next:%02hd",
                     &bptr->base.row, &bptr->base.col, &bptr->top, &bptr->next ) ;
      ofsdbg << gsOut.ustr() << endl ;
      for ( short b = ZERO ; b < DB_MAX ; ++b )
      {
         gsOut.compose( "%02hd) wp:%02hd/%02hd  aca:%08X  active:%hhd txt: '%s", &b,
                       &bptr->block[b].wp.row, &bptr->block[b].wp.col, 
                       &bptr->block[b].aca.acaVal, &bptr->block[b].active,
                       bptr->block[b].txt ) ;
         gsOut.erase( NEWLINE, ZERO, false, true ) ;
         gsOut.limitChars( 148 ) ;
         gsOut.append( L"'" ) ;
         ofsdbg << gsOut.ustr() << endl ;
      }
   }

}  //* End dumpFifo2Log() *

//**********************
//* dumpFifoRecord2Log *
//**********************
//********************************************************************************
//* Private Method: (debugging only)                                             *
//* --------------------------------                                             *
//* Debugging method: Dump a single record of specified BlockFifo object         *
//* to log file.                                                                 *
//* Input  : bptr   : pointer to target field                                    *
//*          blkIndx: index of record in the block[] array                       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::dumpFifoRecord2Log ( const DataBlock* dbptr ) const
{
   if ( ofsdbg.is_open() && (dbptr != NULL) )
   {
      gString gsOut( 
         "DataBlock - wp:%02hd/%02hd aca:%08X active:%hhd txt:'%s", 
         &dbptr->wp.row, &dbptr->wp.col, &dbptr->aca.acaVal, &dbptr->active, dbptr->txt ) ;
      gsOut.erase( NEWLINE, ZERO, false, true ) ;
      gsOut.limitChars( 132 ) ;
      gsOut.append( L"'" ) ;
      ofsdbg << gsOut.ustr() << endl ;
   }

}  //* End dumpFifo2Log() *
#endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO

#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_PHOTO != 0
//* Initialize line-drawing characters for creating boxes. *
static void ScreenshotBoxes ( LineType style, 
                       wchar_t& ul, wchar_t& ur, wchar_t& ll, wchar_t& lr,
                       wchar_t& h, wchar_t& v, wchar_t& lt, wchar_t& rt ) ;

//*************************
//*      Screenshot       *
//*************************
//********************************************************************************
//* Capture a screenshot to the log file.                                        *
//*                                                                              *
//* Unlike the screenshot capture in the authors NcDialog library, the capture   *
//* is performed in a sectioned format. The developer is then responsible        *
//* for integrating the sections to produce the finished screenshot.             *
//*  1) Border (with optional title)                                             *
//*  2) Static text blocks                                                       *
//*  3) Array of controls, in indexed order. Control is written with border      *
//*     and label, if specified.                                                 *
//*                                                                              *
//* For HTML output, each row of each object is bracketed by a placeholder       *
//* <span>...</span>                                                             *
//*                                                                              *
//* Note that this method is _huge_ compared to its actual usefulness, so be     *
//* sure its conditional-compile is disabled when the method is not in use.      *
//*                                                                              *
//* Input  : html : (optional, default == false) output format                   *
//*                 true  : HTML script                                          *
//*                 false : plain text                                           *
//*          desc : (optional, default == null pointer) discriptive text         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* For monochrome plain-text screenshots, it is certainly more efficient to     *
//* simply copy-and-paste from the terminal window; but for HTML output it is    *
//* perhaps more efficient to use the capture method. This is especially true    *
//* for a developer who is not fluent in HTML/CSS markup.                        *
//*                                                                              *
//* The primary test window for this functionality is Test_Cookie(), which is    *
//* a simple window used in the Texinfo documentation.                           *
//*                                                                              *
//* Color attributes are not captured; however, for HTML output, the template    *
//* for CSS attribute-class specification is provided. Example:                  *
//*       <span class=\"xxxx\">Here is some text.</span>                         *
//* The "xxxx' in the template may be replaced by any of the attribute classes   *
//* defined in 'infodoc-styles.css'. For instance:                               *
//*       <span class=\"cy_r\">Here is some text.</span>                         *
//* calls out the "cy_r" attribute class cyan text with fgnd/bgnd reversed.      *
//*                                                                              *
//* Hotkey designators are displayed with the 'underline' attribute. For these   *
//* characters, a callout for the the CSS "ulin" class is embedded in the        *
//* HTML output. For example the text of a Pushbutton may be written as:         *
//*              "Buy Some ^Jaoxi Today"                                         *
//* The caret character ('^') indicates that the next character is the hotkey    *
//* character, 'J' in this example. Thus the HTML would be:                      *
//*   <span class="xxxx">Buy Some <span class="ulin">J</span>aoxi Today</span>   *
//* This will cause the 'J' to be underlined in the output.                      *
//*                                                                              *
//*                                                                              *
//********************************************************************************

void ACWin::Screenshot ( bool html, const char* desc )
{
   #define DEBUG_PHOTO2 (0)               // for debugging only

   const char* spanClass = "<span class=\"xxxx\">" ;
   const char* spanUline = "<span class=\"ulin\">" ;
   const char* spanClose = "</span>" ;
   const wchar_t *modOn, *modOff ;        // pointers to ANSI sequences
   modtab.getSequence ( acaUNDERLINE, modOn, modOff ) ;
   const int spanCols    = 26 ;   // columns in spanClass + spanClose
   const wchar_t narrowDIAMOND = L'♦' ;

   gString gsOut( "** Screenshot: format: %s    **", (html ? "HTML" : "Plain Text") ) ;
   gString gsLab, gsTxt, gs ;
   wchar_t ul, ur, ll, lr, h, v, lt, rt ; // characters for drawing border
   int labPad,
       indx ;

   //* Initialize the drawing characters for the window *
   //* border according to the line type.               *
   ScreenshotBoxes ( this->bdrStyle, ul, ur, ll, lr, h, v, lt, rt ) ;

   if ( ofsdbg.is_open() )                // if target file is open
   {
      if ( desc != NULL )
      {
         int cols = gsOut.gscols() ;
         gsOut.append( "\n** %s ", desc ) ;
         gsOut.padCols( (cols * 2) - 2 ) ;
         gsOut.append( "**" ) ;
      }
      ofsdbg << gsOut << endl ;

      //* For HTML output, open the <div> enclosure. *
      if ( html )
      {
         ofsdbg <<
            "<div class=\"tscap\" style=\"text-color:inherit; "
            "background-color:inherit; max-width:22.85em; line-height:1.165em;\">"
             << endl ;
             // Programmer's Note: The closing newline of this sequence should be 
             // manually removed during integration of the objects.
      }

      //* Draw top border *
      if ( *this->title != NULLCHAR )
      {
         gsOut.compose( "%C%S%C", &rt, this->title, &lt ) ;
         gsOut.padCols( this->width - 2, h, true ) ;
         gsOut.insert( ul ) ;
         gsOut.append( ur ) ;
         if ( html )
         {
            indx = gsOut.after( rt ) ;
            gsOut.insert( spanClass, indx ) ;
            indx = gsOut.find( lt ) ;
            gsOut.insert( spanClose, indx ) ;
         }
      }
      else
      {
         gsOut.compose( "%C", &ul ) ;
         gsOut.padCols( this->width - 1, h ) ;
         gsOut.append( "%C", &ur ) ;
      }
      if ( html ) { ofsdbg << spanClass ; }
      ofsdbg << gsOut ;
      if ( html ) { ofsdbg << spanClose ; }
      ofsdbg << endl ;

      //* Draw left and right borders *
      gsOut.clear() ;
      gsOut.padCols( this->width - 2 ) ;
      gsOut.insert( v ) ;
      gsOut.append( v ) ;
      if ( html )
      {
         gsOut.insert( spanClass, 1 ) ;
         gsOut.insert( spanClose, 1 ) ;
         gsOut.insert( spanClass, 0 ) ;

         indx = gsOut.findlast( v ) ;
         gsOut.insert( spanClose, indx ) ;
         indx = gsOut.findlast( v ) ;
         gsOut.insert( spanClass, indx ) ;
         gsOut.append( spanClose ) ;
      }
      for ( short r = this->height - 2 ; r > ZERO ; --r )
         ofsdbg << gsOut << endl ;

      //* Draw bottom border *
      gsOut.compose( "%C", &ll ) ;
      gsOut.padCols( this->width - 1, h ) ;
      gsOut.append( "%C", &lr ) ;
      if ( html )
      {
         gsOut.insert( spanClass ) ;
         gsOut.append( spanClose ) ;
      }
      ofsdbg << gsOut << endl ;

      //* For HTML output, close the <div> *
      if ( html )
         ofsdbg << "</div>" << endl ;
      ofsdbg << endl ;

      //* Draw static text *
      if ( *this->fifo->block[0].txt != NULLCHAR )
      {
         ofsdbg << "** Static Text **" << endl ;
         for ( short b = ZERO ; b < DB_MAX ; ++b )
         {
            if ( *this->fifo->block[b].txt != NULLCHAR )
            {
               gsOut.compose( "Block #%02hd at:%02hd:%02hd\n%s", 
                              &b, &this->fifo->block[b].wp.row, 
                              &this->fifo->block[b].wp.col, this->fifo->block[b].txt ) ;
               ofsdbg << gsOut ;
               ofsdbg.flush() ;
            }
         }
         ofsdbg << endl ;
      }

      //* Draw the controls *
      skField* fptr ;
      for ( short f = ZERO ; f < this->skf->fCnt ; ++f )
      {
         gs.compose( "\n** CTRL INDEX %02hd **", &f ) ; // output delimiter for field data
         ofsdbg << gs << endl ;

         fptr = &this->skf->fld[f] ;                  // point to the field

         #if DEBUG_PHOTO2 != 0   // TESTING ONLY
         //* Direct manipulation of field configuration parameters. *
         if ( (fptr->type == ftTB) || (fptr->type == ftSB) )
         {
            LineType ltype = ltSINGLE ;
            bool tbLabel   = true,
                 tbHotkey  = true,
                 tbCenter  = false,
                 tbAttr    = false,
                 tbOutline = true ;
            // Add label: with or without ANSI hotkey designator
            if ( tbLabel )
            {
               if ( tbHotkey )
                  gsLab = "This Is \x1B[4mt\x1B[24mhe Label" ;
               else
                  gsLab = "This Is the Label" ;
               gsLab.copy( fptr->lab, labelLEN ) ;
               if ( tbCenter )
               {
                  fptr->lpos = { 
                     short(fptr->orig.row - 1), 
                     short(fptr->orig.col + (fptr->wid / 2) - (gsLab.gscols() / 2)) } ;
               }
               else
                  fptr->lpos = { short(fptr->orig.row - 1), fptr->orig.col } ; // left-justified
            }
            if ( tbAttr )
               fptr->lattr = acAttr(acaFG_GREEN | this->txtAttr.acaBgnd) ; // alternate label attributes
            if ( tbOutline )
            { fptr->bdr = true ; fptr->btype = ltype ; }
            else
               fptr->bdr = false ;
         }
         else if ( fptr->type == ftPB )
         {
            LineType ltype = ltSINGLE ;
            bool pbOutline = false ;

            fptr->btype = ltype ;
            fptr->bdr   = pbOutline ;
         }
         else if ( fptr->type == ftRB )
         {
            LineType ltype = ltSINGLE ;
            bool rbOutline = false ;

            fptr->btype = ltype ;
            fptr->bdr   = rbOutline ;
         }
         #endif   // DEBUG_PHOTO2

         switch ( fptr->type )
         {
            case ftPB:
               if ( fptr->bdr )           // top of field outline
               {
                  ScreenshotBoxes ( fptr->btype, ul, ur, ll, lr, h, v, lt, rt ) ;
                  gsOut.clear() ;
                  gsOut.padCols( fptr->wid, h ) ;
                  gsOut.insert( ul ) ;
                  gsOut.append( ur ) ;
                  if ( html )
                  {
                     gsOut.insert( spanClass ) ;
                     gsOut.append( spanClose ) ;
                     gs.compose( "%s%C%s", spanClass, &v, spanClose ) ; // Left/Right outline
                  }
                  ofsdbg << gsOut << endl ;
               }
               gsOut = fptr->txt ;        // working copy of text
               //* If embedded ANSI underline sequence      *
               //* a) if HTML, replace with underline class *
               //* b) if plain text, delete the sequence    *
               if ( (indx = gsOut.find( modOn )) >= ZERO )
               {
                  if ( html )
                  { gsOut.replace( modOn, spanUline ) ; gsOut.replace( modOff, spanClose ) ; }
                  else
                  { gsOut.erase( modOn ) ; gsOut.erase( modOff ) ; }
               }

               if ( html )
               {
                  gsOut.insert( spanClass ) ;
                  gsOut.append( spanClose ) ;
                  if ( fptr->bdr )        // insert left/right of field outline
                  { gsOut.insert( gs.gstr() ) ; gsOut.append( gs.gstr() ) ; }
               }
               else if ( fptr->bdr )
               {
                  gsOut.insert( v ) ;
                  gsOut.append( v ) ;
               }
               ofsdbg << gsOut << endl ;
               if ( fptr->bdr )           // bottom of field outline
               {
                  gsOut.clear() ;
                  gsOut.padCols( fptr->wid, h ) ;
                  gsOut.insert( ll ) ;
                  gsOut.append( lr ) ;
                  if ( html )
                  { gsOut.insert( spanClass ) ; gsOut.append( spanClose ) ; }
                  ofsdbg << gsOut << endl ;
               }
               break ;

            case ftRB:
               if ( fptr->bdr )           // top of field outline
               {
                  ScreenshotBoxes ( fptr->btype, ul, ur, ll, lr, h, v, lt, rt ) ;
                  gsOut.clear() ;
                  gsOut.padCols( fptr->wid, h ) ;
                  gsOut.insert( ul ) ;
                  gsOut.append( ur ) ;
                  if ( html )
                  {
                     gsOut.insert( spanClass ) ;
                     gsOut.append( spanClose ) ;
                     gs.compose( "%s%C%s", spanClass, &v, spanClose ) ; // Left/Right outline
                  }
                  ofsdbg << gsOut << endl ;
               }

               //* The 'txt' data consist of the bracket pair and the   *
               //* character between the brackets which is either the   *
               //* 'set' character or a space.                          *
               //* Note: HTML output _incorrectly_ renders the default  *
               //* set character between the braces as a two-column     *
               //* character. If this character is present, replace     *
               //* it with a single-column character.                   *
               gsOut = fptr->txt ;
               if ( html )
               {
                  if ( (gsOut.find( wcsDIAMOND )) == 1 )
                     gsOut.replace( wcsDIAMOND, narrowDIAMOND ) ;

                  gsOut.insert( spanClass ) ;
                  gsOut.append( spanClose ) ;
                  if ( fptr->bdr )
                     gsOut.insert( gs.gstr() ) ;
               }
               else if ( fptr->bdr )
               {
                  gsOut.insert( v ) ;
               }
               ofsdbg << gsOut ; ofsdbg.flush() ;
               gsOut = fptr->lab ;
               if ( (indx = gsOut.find( modOn )) >= ZERO )
               {
                  if ( html )
                  { gsOut.replace( modOn, spanUline ) ; gsOut.replace( modOff, spanClose ) ; }
                  else
                  { gsOut.erase( modOn ) ; gsOut.erase( modOff ) ; }
               }
               if ( html )
               {
                  gsOut.insert( spanClass ) ;
                  gsOut.append( spanClose ) ;
                  if ( fptr->bdr )        // insert right of field outline
                     gsOut.append( gs.gstr() ) ;
               }
               else if ( fptr->bdr )
               {
                  gsOut.append( v ) ;
               }
               ofsdbg << gsOut << endl ;
               if ( fptr->bdr )           // bottom of field outline
               {
                  gsOut.clear() ;
                  gsOut.padCols( fptr->wid, h ) ;
                  gsOut.insert( ll ) ;
                  gsOut.append( lr ) ;
                  if ( html )
                  { gsOut.insert( spanClass ) ; gsOut.append( spanClose ) ; }
                  ofsdbg << gsOut << endl ;
               }
             break ;

            case ftTB:
            case ftSB:
               if ( *fptr->lab != NULLCHAR ) // label text
               {
                  gsLab = fptr->lab ;
                  labPad = ZERO ;
                  if ( (gsLab.find( modOn )) >= ZERO )
                  {
                     gsLab = fptr->lab ;
                     if ( html )
                     {
                        gsLab.replace( modOn, spanUline ) ;
                        gsLab.replace( modOff, spanClose ) ;
                        labPad = spanCols ;
                     }
                     else
                     {
                        gsLab.erase( modOn ) ;
                        gsLab.erase( modOff ) ;
                     }
                  }
               }

               if ( fptr->bdr )           // top of field outline
               {
                  ScreenshotBoxes ( fptr->btype, ul, ur, ll, lr, h, v, lt, rt ) ;
                  gsOut.clear() ;
                  if ( *fptr->lab != NULLCHAR )
                  {
                     gsOut = gsLab ;
                     if ( fptr->lpos.col == fptr->orig.col ) // left-justified
                        gsOut.padCols( fptr->wid + labPad, h ) ;
                     else                                   // centered
                        gsOut.padCols( fptr->wid + labPad, h, true ) ;
                     if ( fptr->lattr != fptr->battr )
                     {
                        gsOut.insert( spanClass ) ;
                        indx = gsOut.after( gsLab.gstr() ) ;
                        gsOut.insert( spanClose, indx ) ;
                     }
                  }
                  else                    // border with no label
                  {
                     gsOut.padCols( fptr->wid, h ) ;
                  }
                  gsOut.insert( ul ) ;
                  gsOut.append( ur ) ;

                  if ( html )
                  { gsOut.insert( spanClass ) ; gsOut.append( spanClose ) ; }
                  ofsdbg << gsOut << endl ;
               }
               else if ( *fptr->lab != NULLCHAR )  // label, without field outline
               {
                  gsOut = gsLab ;
                  if ( html )
                  {
                     gsOut.insert( spanClass ) ;
                     gsOut.append( spanClose ) ;
                     labPad *= 2 ;        // add width of class sequence
                  }
                  gsOut.shiftCols( fptr->lpos.col, L'.' ) ;
                  gsOut.padCols( (this->txtCols + labPad), L'.' ) ;
                  gsOut.insert( "Label:" ) ;
                  ofsdbg << gsOut << endl ;
               }

               if ( fptr->hgt == 1 )      // single-row field
               {
                  gsOut = fptr->txt ;
                  gsOut.padCols( fptr->wid ) ;
                  gsOut.limitCols( fptr->wid ) ;
                  if ( html )             // HTML border
                  {
                     gsOut.insert( spanClass ) ;
                     gsOut.append( spanClose ) ;
                     if ( fptr->bdr )     // insert left/right of field outline
                     {
                        gs.compose( "%s%C%s", spanClass, &v, spanClose ) ;
                        gsOut.insert( gs.gstr() ) ;
                        gsOut.append( gs.gstr() ) ;
                     }
                  }
                  else if ( fptr->bdr )   // plain-text border
                  {
                     gsOut.insert( v ) ;
                     gsOut.append( v ) ;
                  }
                  ofsdbg << gsOut << endl ;
               }
               else                       // multi-row field
               {
                  //* Retrieve a copy of the VISIBLE text. *
                  int rhead = fptr->mrp.roff[fptr->mrp.rtop] ; // head of current row
                  gsTxt = &fptr->txt.gstr()[rhead] ;
                  short dispRows = ZERO ;
                  for ( int i = ZERO ; i < gsTxt.gschars() ; ++i )
                  {
                     if ( (gsTxt.gstr()[i] == NEWLINE) || (gsTxt.gstr()[i] == NULLCHAR) )
                     {
                        if ( ++dispRows == fptr->hgt )
                        {
                           gsTxt.limitChars( i ) ;
                           break ;
                        }
                     }
                  }

                  //* Isolate the data for each row and apply *
                  //* CSS and/or delimeters and padding.      *
                  const wchar_t* wptr = gsTxt.gstr() ;
                  wchar_t padchar = (!html && !fptr->bdr) ? L'.' : L' ' ;
                  indx = ZERO ;
                  gsOut.clear() ;
                  for ( indx = ZERO ; indx < gsTxt.gschars() ; ++indx )
                  {
                     if ( (wptr[indx] == NEWLINE) || (wptr[indx] == NULLCHAR) )
                     {
                        gsOut.padCols( fptr->wid, padchar ) ;
                        gsOut.limitCols( fptr->wid ) ;
                        if ( html )
                        {
                           gsOut.insert( spanClass ) ;
                           gsOut.append( spanClose ) ;
                           if ( fptr->bdr )     // insert left/right of field outline
                           {
                              gs.compose( "%s%C%s", spanClass, &v, spanClose ) ;
                              gsOut.insert( gs.gstr() ) ;
                              gsOut.append( gs.gstr() ) ;
                           }
                        }
                        else if ( fptr->bdr )
                        {
                           gsOut.insert( v ) ;
                           gsOut.append( v ) ;
                        }
                        ofsdbg << gsOut << endl ;
                        gsOut.clear() ;
                     }
                     else
                        gsOut.append( wptr[indx] ) ;
                  }
               }
               if ( fptr->bdr )           // bottom of field outline
               {
                  gsOut.clear() ;
                  gsOut.padCols( fptr->wid, h ) ;
                  gsOut.insert( ll ) ;
                  gsOut.append( lr ) ;
                  if ( html )
                  { gsOut.insert( spanClass ) ; gsOut.append( spanClose ) ; }
                  ofsdbg << gsOut << endl ;
               }
               break ;

            case ftMB:
               break ;

            default:                      // unsupported control type
               break ;
         }     // switch{}
      }        // for(;;)

      ofsdbg << "** End capture **\n" << endl ;
   }

}  //* End Screenshot() *

//*************************
//*    ScreenshotBoxes    *
//*************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Called by Screenshot() to initialize line-drawing characters for creating    *
//* boxes.                                                                       *
//*                                                                              *
//* Input  : style : member of enum LineType specifying the the character group  *
//*          ul    : upper-left corner                                           *
//*          ur    : upper-right corner                                          *
//*          ll    : lower-left corner                                           *
//*          lr    : lower-right corner                                          *
//*          h     : horizontal character                                        *
//*          v     : vertical character                                          *
//*          lt    : horizontal T pointing left                                  *
//*          ul    : horizontal T pointing right                                 *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
static void ScreenshotBoxes ( LineType style, 
                       wchar_t& ul, wchar_t& ur, wchar_t& ll, wchar_t& lr,
                       wchar_t& h, wchar_t& v, wchar_t& lt, wchar_t& rt )
{
   switch ( style )
   {
      case ltDUAL:                     // dual line
         ul = wcsULd ;
         ur = wcsURd ;
         ll = wcsLLd ;
         lr = wcsLRd ;
         h  = wcsHORIZd ;
         v  = wcsVERTd ;
         lt = wcsLTEEd ;
         rt = wcsRTEEd ;
         break ;
      case ltSINGLEBOLD:               // single-line bold
      case ltDASH2BOLD:                // two-dash bold
      case ltDASH3BOLD:                // three-dash bold
      case ltDASH4BOLD:                // four-dash bold
         ul = wcsULb ;
         ur = wcsURb ;
         ll = wcsLLb ;
         lr = wcsLRb ;
         lt = wcsLTEEb ;
         rt = wcsRTEEb ;
         switch ( style )
         {
            case ltDASH2BOLD: h = wcsHDASH2b ; v = wcsVDASH2b ; break ;
            case ltDASH3BOLD: h = wcsHDASH3b ; v = wcsVDASH3b ; break ;
            case ltDASH4BOLD: h = wcsHDASH4b ; v = wcsVDASH4b ; break ;
            default:          h = wcsHORIZb ;  v = wcsVERTb ;   break ;
         } ;
         break ;
         break ;
      case ltHORIZ:                    // draw border with spaces only
      case ltVERT:                     // i.e. no border
         ul = ur = ll = lr = h = v = lt = rt = wcsSPACE ;
         break ;
      case ltSINGLE:                   // single-line normal
      case ltDASH2:                    // two-dash
      case ltDASH3:                    // three-dash
      case ltDASH4:                    // four-dash
      default:
         ul = wcsULs ;
         ur = wcsURs ;
         ll = wcsLLs ;
         lr = wcsLRs ;
         lt = wcsLTEEs ;
         rt = wcsRTEEs ;
         switch ( style )
         {
            case ltDASH2BOLD: h = wcsHDASH2s ; v = wcsVDASH2s ; break ;
            case ltDASH3BOLD: h = wcsHDASH3s ; v = wcsVDASH3s ; break ;
            case ltDASH4BOLD: h = wcsHDASH4s ; v = wcsVDASH4s ; break ;
            default:          h = wcsHORIZs ; v = wcsVERTs ;    break ;
         } ;
         break ;
   } ;

}  //* End ScreenshotBoxes() *
#endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_PHOTO


//*-----------------------*
#endif   //* INCLUDE_API  *
//*-----------------------*

//*************************
//*                       *
//*************************
//********************************************************************************
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  :                                                                     *
//*                                                                              *
//* Returns:                                                                     *
//********************************************************************************

