//********************************************************************************
//* File       : AnsiCmdWin.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: ACWin class implementation. ACWin is derived from the           *
//* AnsiCmd class and defines a high-level "window" object with user-interaction *
//* implemented via the skForm and skField user-interface objects.               *
//*                                                                              *
//*               ┌───────┤ ACWin-class Window ├───────┐                         *
//*               │                                    │                         *
//*               │            Hello World!            │                         *
//*               │                                    │                         *
//*               │                                    │                         *
//*               └────────────────────────────────────┘                         *
//*                                                                              *
//* This window is not as flexible as an ncurses window, but it is implemented   *
//* in about 10,000 lines of code, while ncurses is over 200,000 lines of code.  *
//* See the NcDialog API library for the the author's full, robust, all          *
//* bells-and-whistles implementation of ncurses support.                        *
//*                                                                              *
//********************************************************************************
//* Copyright Notice:                                                            *
//* This program is free software: you can redistribute it and/or modify it      *
//* under the terms of the GNU General Public License as published by the Free   *
//* Software Foundation, either version 3 of the License, or (at your option)    *
//* any later version.                                                           *
//*                                                                              *
//* This program is distributed in the hope that it will be useful, but          *
//* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY   *
//* or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License     *
//* for more details.                                                            *
//*                                                                              *
//* You should have received a copy of the GNU General Public License along      *
//* with this program.  If not, see <http://www.gnu.org/licenses/>.              *
//*                                                                              *
//********************************************************************************
//* Version History: ACWin updates only                                          *
//* ===================================                                          *
//* v: 0.00.07 20-Dec-2025                                                       *
//* -- Several methods related to user interface control have been modified      *
//*    to accept an skForm pointer or skField pointer instead of a single        *
//*    index value. This means that they are available from both ACWin and       *
//*    ACDlg windows. The existing index-oriented methods are still available    *
//*    (for ACWin windows only).                                                 *
//* -- The Scrollbox (control type ftSB) has been updated from a simple          *
//*    read-only Textbox. It can now optionally function as a scrolling          *
//*    menu from which the user can select an item. For an example, see          *
//*    how the AcEdit app interacts with the user in traversing the directory    *
//*    tree and selecting a file from the list.                                  *
//* -- For Scrollbox and Menu controls, a new skField public method: getIem()    *
//*    can be used to report the 'selected' (or current-highlighted) item.       *
//* -- Both Scrollbox and Menu controls may now be configured for multi-color    *
//*    text, one color per item (row).                                           *
//* -- All control types are now supported within dialog window with the         *
//*    following exceptions:                                                     *
//*    -- Textbox controls (ftTB) are limited to a single row, which in          *
//*       turn limits the formatting option to floShift.                         *
//*       -- Technical Note: During development the use of multi-row             *
//*          text fields was validated; however, at this time, dialogs text      *
//*          fields are limited to single-row controls. This simplifies the      
//*          range check on field dimensions.                                    *
//*    -- Menus: Context menus are supported, but must be locked in the          *
//*       visible state. Menu groups are not supported because it would          *
//*       make dialog refresh unnecessarily complex.                             *
//* -- Control outlines and external labels for control types that support       *
//*    them are not directly available for controls within a dialog window.      *
//*    The data members that define outlines and labels are 'protected'          *
//*    members; that is, they may not be directly modified at the application    *
//*    level. Instead, two specialized public methods of the skField class       *
//*    are available to configure these options: labelConfig() and               *
//*    outlineConfig(). See documentation for details.                           *
//* -- Implement support for callback functions within the EditDialog() method.  *
//*    -- Implement "Dialog_nnn" group of methods for use by dialog callbacks.   *
//*       See documentation for details on use of these methods.                 *
//*       This group of methods may be expanded as needed.                       *
//* -- Hotkey validation: The ALT+n group of keycodes are the primary keycodes   *
//*    used as hotkeys; however, many of the ASCII control codes                 *
//*    (CTRL+n keycode group) are not assigned to any particular function and    *
//*    may be designated as hotkeys.                                             *
//*    The logic needed to test for both groups is complex and error prone, so   *
//*    if a keycode in the range: wcsCTL_A - wcsCTL_Z is received, _AND_ if      *
//*    it is not assigned to a specific functionality, (ex:wcsCTL_C==Copy),      *
//*    then the isHotkey() method can optionally convert to the equivalent       *
//*    keycode in the range: wcsALT_A - wcsALT_Z.                                *
//*      Example: wcsCTL_U becomes wcsALT_U.                                     *
//*    NOTE: This option is fully implemented; however, the code is currently    *
//*          disabled under a conditional compile directive in isHotkey().       *
//*          If future, we hope to implement a configuration option to           *
//*          dynamically enable or disable the conversion.                       *
//*    See the 'isHotkey() method for details.                                   *
//* -- Redesign of skField class to include 'mrptr' which is a const pointer     *
//*    to the protected 'mrp' member.                                            *
//*    This was done because real-world code needs access to the MRParse-class   *
//*    data considerably more often than we had anticipated.                     *
//*    As a result of this modification, the GetFieldData method was simplified. *
//* -- Add optional parameters to SetFieldIP().                                  *
//*    -- Add an optional 'centered' parameter.                                  *
//*       Vertically centers the target (IP) row within the field.               *
//*    -- Add an optional 'refresh' parameter.                                   *
//*       Avoids potential double refresh during complex configurations.         *
//* -- Add an optional 'refresh' parameter to SetFieldData().                    *
//*    Avoids potential double refresh during complex configurations.            *
//* -- Add an experimental input stream pushback queue. This is useful within    *
//*    application callback methods, allowing greater control over program       *
//*    flow without compromising library integrity.                              *
//* -- Bug Fix: In formatFieldText(), if specified text flow option was          *
//*    'floChar', then the text was under some circumstances, incorrectly        *
//*    formatted as 'floToken'.                                                  *
//*    This fix required implementation for the previously unused 'force'        *
//*    parameter of the private fftReformat() method.                            *
//* -- Add an overload to the pre-instantiation method 'labelConfig'. This       *
//*    overload is designed for Radiobutton controls (ftRB) only.                *
//*    By default Radiobutton labels inherit the color attributes of the parent  *
//*    window/dialog _and_ the label is positioned to the right of the field.    *
//*    The new method allows direct specification of the label attributes and    *
//*    allows the label to optionally be positioned to the left of the control.  *
//* -- Add a 'brief' parameter to the dumpMrp2Log() debugging method.            *
//*    If the flag is set, then only the indexed records are reported.           *
//*    If the flag is reset, then all records are reported.                      *
//* -- Implement ShiftFieldText() method. This complements the ScrollFieldText() *
//*    method, but instead of moving the cursor row, it performs a vertical      *
//*    block shift of the visible text (Textbox (ftTB) fields only).             *
//* -- Replace the hard-coded underline used to indicate "selected" text.        *
//*    The hidden 'modifierTable' class is now used to retrieve pointers to      *
//*    ANSI escape sequences for the text-modifier group of attributes:          *
//*    underline, bold, italic, reversed, blink, etc.                            *
//*    See HilightFieldText() for notes on this feature.                         *
//* -- In SetFieldFlow(), redefine the 'force' parameter for call to             *
//*    formatFieldText(). Now, if the text is already formatted to fit within    *
//*    the field, then it will not be reformatted.                               *
//* -- Bug Fix: In EditForm(), if an exit field was specifield                   *
//*    ('exitField' parameter), the validation test was incorrect. This caused   *
//*    the parameter to be unrecognized under some circumstances.                *
//* -- Bug Fix: If using the Delete key to delete "selected" text the command    *
//*    was being ignored if IP referenced the null terminator.                   *
//* -- Bug Fix: In parseFieldText(), the 'ovr' flag was not being set reliably   *
//*    if IP was outside the left margin of the visible area.                    *
//*    Applies only to Textbox fields configured for 'floShift'.                 *
//* -- update documentation.                                                     *
//*                                                                              *
//* v: 0.00.06 27-Aug-2024                                                       *
//* -- Begin implementation of the "dialog" object.                              *
//*    This is a child window of the parent ACWin object, which temporarily      *
//*    obscures the parent window for specific tasks.                            *
//*    The dialog is in two formats:                                             *
//*    -- The "quick" dialog that gives information and optionally returns a     *
//*       yes or no response.                                                    *
//*    -- The custom dialog which can be configured as a less complex version    *
//*       of the ACWin object.                                                   *
//*    The experimental dialog code was moved to its own module to provide room  *
//*    for expansion.                                                            *
//* -- Add support for "hidden" controls. Any control may be hidden, that is,    *
//*    invisible and inactive. This is useful for controls which overlap.        *
//*    This required additional support methods for display and erasure of the   *
//*    controls.                                                                 *
//* -- Implement new control objects:                                            *
//*    -- Scrollbox, a view-only text field                                      *
//*    -- Menubox, a menu-oriented field which can also be configured as a       *
//*       Menubar group.                                                         *
//* -- Redefine the dumpField2Log method. It now reports _all_ data members,     *
//*    including 'protected' members.                                            *
//* -- Rename 'FieldOutline' method to 'SetFieldOutline' for uniformity among    *
//*    field configuration methods. Expand functionality to optionally specify   *
//*    both outline and label in a single call.                                  *
//* -- Redefine and/or enhance definition of the skForm and skField classes      *
//*    to support new functionality.                                             *
//* -- Add an _optional_ parameter to GetFieldData method for public access to   *
//*    the MRParse class members.                                                *
//* -- Bug Fix: In QuickDialog(), if 'timeout' > ZERO, always return 'false'.    *
//* -- Major update to documentation.                                            *
//*                                                                              *
//* v: 0.00.05 21-Aug-2023                                                       *
//*    -- Add an optional parameter to ACWin initialization constructor:         *
//*       'termDef' specifies optional terminal configuration data.              *
//*    -- Expand the definition of the skForm and skField classes to define      *
//*       additional types of user-interface controls. (see enum FldType)        *
//*       -- Update data members of the skForm and skField classes to support    *
//*          the new functionality.                                              *
//*    -- Add the ACWin-class WinInfo method which reports the essential         *
//*       configuration data for the window                                      *
//*    -- Add overload to ACWin Write method to write a single wide character.   *
//*    -- Improved automatic text formatting for the ACWin Write method group.   *
//*    -- Add an optional parameter 'clear' to the ACWin-class method DrawBox,   *
//*       which will clear the interior of the box using the attributes of the   *
//*       parent window.                                                         *
//*    -- Update the FieldOutline method to refresh the contents of the field    *
//*       after the outline has been drawn.                                      *
//*    -- Expand the definition of the WinInfo class to include AnsiCmd-class    *
//*       information. Existing members are unchanged. See WinInfo method.       *
//*    -- Redefine the SetFieldData() method to include a parameter for setting  *
//*       the word-wrap option.                                                  *
//*    -- Add a field to the "Text Selection and Clipboard Access" test,         *
//*       (--test=Tea) to exercise the functionality of the 'floShift' text      *
//*       formatting option.                                                     *
//*    -- Implement support for an optional callback method to be called inside  *
//*       the user-interface loop for EditForm() method.                         *
//*       This allows the application layer to process any keycodes which are    *
//*       not automatically handled within the called edit methods.              *
//*       See EstablishCallback() and the test methods which invoke it.          *
//*                                                                              *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* To Do:                                                                       *
//* ------                                                                       *
//* -- 'selection' within a Scrollbox S/B optional because ftSB controls may     *
//*    or may not contain 'selectable' items.                                    *
//*    -- If plain paragraph text, 'selection' would not apply.                  *
//*    -- If itemized text, 'selection' would apply.                             *
//*    It is probably not worth creating another member flag for this; however,  *
//*    Idea: Create a ResetFieldSelection() method for use by callbacks.         *
//*          bool SetFieldSelection ( short fIndx, bool set = false,             *
//*                                   short item_index = -1 ) ;                  *
//*          bool setFieldSelect ( skField* fptr, bool set,                      *
//*                                short item_index = -1 ) ;                     *
//* -- Right-justified numeric input Textbox field.                              *
//*    This could also be a generic right-justified field.                       *
//* -- Create OpenContextMenu()                                                  *
//*    a) reset the 'hide' and 'ro' flags                                        *
//*    b) specify position                                                       *
//*    c) set focus with refresh                                                 *
//*    d) interact (either return to main loop or call specialized method)       *
//*    e) return selection (item number) if any                                  *
//*                                                                              *
//* -- Implement an alternate-attribute bracket for displaying a segment of      *
//*    Textbox text using reverse attribute or specific color attribute.         *
//*    The 'sela' and 'selb' members could indicate the segment; however, the    *
//*    segment would not be referenced by copy/cut/paste operations.             *
//*    This could be done by allowing highlighted segments only within read-only *
//*    Textbox controls or by creating a flag to indicate when selection is      *
//*    active.                                                                   *
//*    -- Escape sequences cannot just be integrated into the text unless the    *
//*       refresh method subtracts the "width" of the characters within the      *
//*       escape sequence when calculating the horizontal width of the output.   *
//*       That would create a significant performance hit for very little benefit*
//*       -- The 'iAttr' member could hold the attribute pair, or we could go    *
//*          totally wild and attach a different class to that pointer indicatiog*
//*          color and position of highlighted segment.                          *
//*    -- Alternatively, a pair of highlight colors could be stored (somewhere)  *
//*       to be referenced by the refresh routine for the highlighted segment.   *
//*    -- The 'state' flage could be repurposed for Textbox controls to indicate *
//*       whether 'selection' or highlight is active.                            *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* --                                                                           *
//*                                                                              *
//*                                                                              *
//********************************************************************************


//*****************
//* Include Files *
//*****************
#include "AnsiCmd.hpp"     //* AnsiCmd-class definition and miscellaneous
#include "AnsiCmdWin.hpp"  //* ACWin-class definition

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

//***************
//* Definitions *
//***************

//* Argument for call to private method, skpIp2Cur() *
//* indicating the target of the calculation.        *
enum ipoTarget : short
{
   ipoCur,        // calculate offset to current insertion point
   ipoToT,        // calculate offset to Top-Of-Text
   ipoEoT,        // calculate offset to End-Of-Text
   ipoEoV,        // calculate offset to end of _visible_ text
   ipoToR,        // calculate offset to top of current text row
   ipoEoR,        // calculate offset to end of current text row
} ;


//**************
//* Local data *
//**************
extern AnsiCmd *acObjPtr ;    // non-member variable (see AnsiCmd.cpp)

//* Informational messages displayed using the Footnote() method. *
const char* pasteAborted = " Paste operation aborted. Insufficient space. " ;
const char* pasteTruncate = " Field data truncated to accomodate clipboard data. " ;
const char* pasteNothing = " Nothing to paste. Clipboard is empty. " ;
const char* const dbFifoTemplate = "B$o$X:%hd:%hd:%hd:%hhd" ;

#if INCLUDE_WAYLANDCB != 0
extern bool wcbConnected ;    // clipboard-connected flag
#endif   // INCLUDE_WAYLANDCB


//********************
//* Local prototypes *
//********************
static int  horizShift ( const short* colPtr, short rowBase, short currIp, short maxWidth ) ;
//* If a keycode signals a text "selection", then reset the 'state' flag.*
static bool staleHilightSegment ( skField* fptr, wchar_t wkey ) ;
extern bool char2Hotkey ( wchar_t& wkey ) ;
extern bool menubarToken ( wchar_t wch, const wchar_t* wtxt, int& ip, int& smi ) ;

//*************************
//*        ~ACWin         *
//*************************
//********************************************************************************
//* Destructor: Return all allocated resources to the system before exit.        *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

ACWin::~ACWin ( void )
{
   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0
   if ( (ofsdbg.is_open()) )
   { ofsdbg << "~ACWin()" << endl ; }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG

   //* Make cursor visible. *
   if ( ! this->tset->curVis )
      this->acSetCursorStyle ( csShow ) ;
   //* If 'insc' is set, return to default cursor style.*
   if ( this->skf->insc )
      this->acSetCursorStyle ( csDflt ) ;

   //* If skForm dynamic allocation, release it. *
   if ( this->skf != NULL )
   { delete ( this->skf ) ; this->skf = NULL ; }

   //* If BlockFifo dynamic allocation, release it. *
   if ( this->fifo != NULL )
   { delete ( this->fifo ) ; this->fifo = NULL ; }

   acObjPtr = this->origaoptr ;  // restore pointer to base object

}  //* End ~ACWin *

//*************************
//*        ACWin          *
//*************************
//********************************************************************************
//* Default Constructor:                                                         *
//* Create a default window:                                                     *
//*    posRow  : offset one row from top of terminal window                      *
//*    posCol  : centered in the terminal window                                 *
//*    winHgt  : height == eight(8) rows                                         *
//*    winWid  : width  == thirty-six(36) columns                                *
//*    All other parameters use the initialization defaults.                     *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: implicitly returns a pointer to object                              *
//********************************************************************************
//* Note on visibility of inherited elements for a derived class.                *
//* This is related to the methods and data of the parent class (AnsiCmd)        *
//* which are accessible to the ACWin derived class.                             *
//*                                                                              *
//* Derived Classes:                                                             *
//* Inheritance Type  Base Class    Derived Class                                *
//* ----------------  -----------   --------------                               *
//*   public           public        public                                      *
//*                    protected     protected                                   *
//*                    private       no direct access                            *
//*   protected        public        protected                                   *
//*                    protected     protected                                   *
//*                    private       no direct access                            *
//*   private          public        private                                     *
//*                    protected     private                                     *
//*                    private       no direct access                            *
//*                                                                              *
//********************************************************************************

ACWin::ACWin ( void )
{
   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0
   if ( (ofsdbg.is_open()) )
   { ofsdbg << "ACWin(dflt)" << endl ; }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG

   const short posRow = 2 ;
   const short posCol = 1 ;
   const short winHgt = 8 ;
   const short winWid = 36 ;

   //* Initialize the terminal setup data and allocate text storage.*
   this->initialize ( posRow, posCol, winHgt, winWid ) ;

   //* Adjust horizontal position of window.*
   short hOffset = this->termCols / 2 - winWid / 2 ;
   this->winBase.col = hOffset ;
   this->txtBase.col = this->winBase.col + 1 ;

   // Programmer's Note: The acaExpand default constructor has decoded
   // color attributes to terminal defaults.

   //* Create the default set of input fields.*
   this->init_skForm () ;

}  //* End ACWin() *

//*************************
//*        ACWin          *
//*************************
//********************************************************************************
//* Initialization Constructor:                                                  *
//* Four parameters are required: upper-left row, upper-left column window       *
//* height (rows) and window width (columns).                                    *
//* Other parameters, if not specified, are set to default values.               *
//*                                                                              *
//* Input  : posRow    : Row    - position of window within the terminal window  *
//*          posCol    : Column - position of window within the terminal window  *
//*          winHeight : Height of window in rows                                *
//*          winWidth  : Width of window in columns                              *
//*          bdrLine   : (optional, member enum LineType, ltSINGLE by default)   *
//*                      border style i.e. type of line for border               *
//*                      Note: specifying either ltHORIZ OR ltVERT will cause    *
//*                      the border to be drawn with spaces only (no line).      *
//*          bdrAttrib : (optional, acaATTR_DFLT by default, terminal defaults)  *
//*                      border color attributes OR'd value of acAttr bitfields  *
//*          txtAttrib : (optional, acaATTR_DFLT by default, terminal defaults)  *
//*                      interior color attributes and text modifiers            *
//*                      OR'd value of acAttr bitfields                          *
//*          winTitle  : (optional, null pointer by default)                     *
//*                      if specified, text for window title displayed in        *
//*                      top border                                              *
//*          formDef   : (optional, null pointer by default)                     *
//*                      pointer to an initialized 'skForm' object which         *
//*                      specifies the position and dimensions of input fields   *
//*                      (see example below)                                     *
//*          termDef   : (optional, null pointer by default)                     *
//*                      pointer to an initialized TermConfig object which       *
//*                      specifies terminal setup options. (see notes below)     *
//*                                                                              *
//* Returns: implicitly returns a pointer to object                              *
//********************************************************************************
//* Sample initialization of an skForm object:                                   *
//* ------------------------------------------                                   *
//* Some developers will want to write directly into the window, while others    *
//* will want to design an array of text-input fields for user interactions      *
//* which are defined by some-kind-of-form i.e. the skForm class.                *
//*                                                                              *
//* If the ACWin constructor is called without specifying an skForm object, a    *
//* default skForm object is created based on the size of the window. If the     *
//* window is small (less than 1,000 character cells), then the skForm object    *
//* is created with one(1) skField. Else, the skForm will generate an array of   *
//* skField objects, one per row of the window interior.                         *
//*                                                                              *
//* To achieve control over the way the skForm object is configured, a pointer   *
//* to an skForm template object ('formDef')is passed to the ACWin constructor.  *
//* The position and dimensions of each field must fall entirely within the      *
//* text area of the window, and the fields may not overlap. Although range      *
//* checking and automatic adjustments of these fields is performed, an          *
//* over-caffinated developer could still cause chaos, so it is important to     *
//* carefully verify the parameters provided.                                    *
//*                                                                              *
//* There are several ACWin objects created in the test code included with this  *
//* library. Some tests write text directly into the window, while others write  *
//* into the defined fields (or some combination of the two). Please refer to    *
//* those examples, as well as the discussion of skForm and skField objects in   *
//* the documentation for this package.                                          *
//*                                                                              *
//* To create an skForm template, allocate a new object specifying the number    *
//* of fields it contains. Here we define an skForm object with two(2) fields.   *
//*                                                                              *
//*   skForm *skfPtr = new skForm( 2 ) ;                                         *
//*                                                                              *
//*               ┌───────┤ ACWin-class Window ├───────┐                         *
//*               │                                    │                         *
//*               │ ██████████████████████████████████ │                         *
//*               │                                    │                         *
//*               │ ██████████████████████████████████ │                         *
//*               │                                    │                         *
//*               └────────────────────────────────────┘                         *
//*                                                                              *
//* Initialize the FORM-LEVEL members. Note that 'fCnt' is initialized during    *
//* instantiation, and must not be changed. Also, the 'base' value (offset) is   *
//* overridden by the ACWin initialization and is set to the coordinates         *
//* corresponding to the terminal-window offset for the upper-left corner of the *
//* window interior.                                                             *
//*                                                                              *
//*   skfPtr->ins  = false ;      // insert/overstrike flag                      *
//*   skfPtr->togg = true ;       // insert/overstrike lock                      *
//*   skfPtr->beep = true ;       // make noise if invalid user input            *
//*   skfPtr->fi   = ZERO ;       // index of field with initial focus           *
//*                                                                              *
//* Remaining members of skForm will use the default values.                     *
//*                                                                              *
//* Initialize the FIELD-LEVEL members:                                          *
//* WinPos wpul(1,1) ;                                                           *
//* short f = ZERO ;                                                             *
//* skfPtr->fld[f].type = ftTB ;  // field type (textbox)                        *
//* skfPtr->fld[f].orig = wpul ;  // origin (row/column offset) for field        *
//* skfPtr->fld[f].hgt = 1 ;      // field height                                *
//* skfPtr->fld[f].wid = winWIDTH - 4 ; // width of field                        *
//* skfPtr->fld[f].ip = ZERO ;    // text insertion point (offset into field)    *
//* skfPtr->fld[f].txt = "Some text for field 0." ;                              *
//*                                                                              *
//* wpul.row += 2 ;               // position of next field==two rows below      *
//* ++f ;                                                                        *
//*                                                                              *
//* skfPtr->fld[f].type = ftTB ;                                                 *
//* skfPtr->fld[f].orig = wpul ;                                                 *
//* skfPtr->fld[f].hgt = 1 ;                                                     *
//* skfPtr->fld[f].wid = winWIDTH - 4 ;                                          *
//* skfPtr->fld[f].ip = 5 ;                                                      *
//* skfPtr->fld[f].txt = "Some text for field 1." ;                              *
//*                                                                              *
//* -- Note that the 'cur' (physical cursor position) is dynamically calculated  *
//*    from the 'ip' (insertion point).                                          *
//* -- The 'aca' (color attributes) member is the logical OR of enum acAttr      *
//*    values. If not specified, the terminal default attributes are used.       *
//* -- The 'ro' (read-only) flag retains its default value (false).              *
//* -- The 'off' (text positioning) member is defined for future development     *
//*    and is currently ignored.                                                 *
//*                                                                              *
//* The initialized skForm template is then passed as a parameter for the        *
//* ACWin constructor.                                                           *
//*                                                                              *
//* Notes on Terminal Configuration:                                             *
//* --------------------------------                                             *
//* This parameter may be used to configure the terminal settings if it has not  *
//* already been configured by a previous instantiation of an AnsiCmd-class      *
//* object, or to replace the previous configuration.                            *
//* Important Note: The ACWin constructor will override some of the specified    *
//* terminal configuration parameters in order to maintain operational ingegrity.*
//*                                                                              *
//*                                                                              *
//* - - - - -                                                                    *
//* Programmer's Note: The AnsiCmd constructor is called as part of the ACWin    *
//* constructor. It is important to monitor the interaction between the two      *
//* constructors.                                                                *
//*                                                                              *
//********************************************************************************

ACWin::ACWin ( short posRow, short posCol, short winHeight, short winWidth,
               LineType bdrLine, acAttr bdrAttrib, acAttr txtAttrib, 
               const char* winTitle, 
               const skForm *formDef, const TermConfig *termDef )
{
   //* Initialize the terminal setup data and window parameters.*
   this->initialize ( posRow, posCol, winHeight, winWidth, termDef ) ;

   //* Note: ltHORIZ or ltVERT specifies no line drawn in border.*
   if ( bdrLine == ltVERT )
      bdrLine = ltHORIZ ;
   this->bdrStyle = bdrLine ;

   //* For borders, some text-modifier attributes     *
   //* are meaningless or just plain ugly. Clear them.*
   bdrAttrib = acAttr(bdrAttrib & 
                      ~(acaITALIC | acaUNDERLINE | acaOVERLINE | 
                        acaXOUT | acaCONCEAL)) ;

   //* If placeholder value specified, substitute default attributes.*
   if ( bdrAttrib == acaUSEDFLT ) { bdrAttrib = acaATTR_DFLT ; }
   if ( txtAttrib == acaUSEDFLT ) { txtAttrib = acaATTR_DFLT ; }
   //* Decode the border and interior attributes.*
   this->bdrAttr.decode( bdrAttrib ) ;
   this->txtAttr.decode( txtAttrib ) ;

   //* If window title specified, save title text.*
   if ( winTitle != NULL )
   {
      gString gs( winTitle ) ;
      gs.limitCols( this->width - 4 ) ;      // if necessary, truncate to fit
      gs.copy( this->title, gsALLOCMIN ) ;
   }

   //* If an skForm object was specified, use it as a template.  *
   //* Else, (formDef==NULL), create default set of input fields.*
   this->init_skForm ( formDef ) ;

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_WDUMP != 0
   this->dumpConfig2Log ( L"ACWin(init)" ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_WDUMP
   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
   this->dumpFifo2Log ( this->fifo, L"ACWin Constructor:" ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO

}  //* End ACWin() *

//*************************
//*      initialize       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called during instantiation to initialize the data members, terminal         *
//* environment and text-management structure.                                   *
//*                                                                              *
//* Instead of setting the terminal environment, directly, request a copy of     *
//* the settings from the existing AnsiCmd, parent-class object and use them     *
//* to initialize our member variables.                                          *
//*                                                                              *
//* Input  : posRow   : Row    - position of window within the terminal window   *
//*          posCol   : Column - position of window within the terminal window   *
//*          winHeight: Height of window in rows                                 *
//*          winWidth : Width of window in columns                               *
//*          termDef  : (optional, NULL pointer by default)                      *
//*                     if specified, pointer to a TermConfig object containing  *
//*                     terminal setup information.                              *
//*                     Otherwise, defaults will be used for all options.        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: The AnsiCmd class is always be instantiated before the    *
//* ACWin class, either explicitly by the application (default or initialization *
//* constructor), or implicitly due to the fact that AnsiCmd is the parent class *
//* of the ACWin class, meaning that the AnsiCmd default constructor will be     *
//* invoked _before_ the ACWin constructor.                                      *
//*                                                                              *
//* Note also that the non-member signal handler methods access the member       *
//* method for the signal through the static pointer, "acObjPtr" which is        *
//* initialized by the underlying AnsiCmd object. In order to gain control over  *
//* the non-member methods, it is necessary to point "acObjPtr" at the ACWin     *
//* object.                                                                      *
//* In a future release, we may create local non-member signal handlers so this  *
//* kludgy assignment is not necessary.                                          *
//********************************************************************************

void ACWin::initialize ( short posRow, short posCol, short winHeight, short winWidth,
                         const TermConfig *termDef )
{
   const short minDIM = 3 ;   // a window requires at least three rows/columns

   this->winBase  = { 1, 1 } ;         // set all data members to defaults
   this->txtBase  = { 2, 2 } ;
   this->txtOff   = { 0, 0 } ;
   this->height   = 3 ;
   this->width    = 3 ;
   this->txtRows  = 1 ;
   this->txtCols  = 1 ;
   this->ioLocale = NULL ;
   this->bdrStyle = ltSINGLE ;
   *this->title   = NULLCHAR ;
   this->skf      = NULL ;
   this->fifo     = NULL ;
   this->origaoptr = NULL ;
   this->callbkptr = NULL ;
   this->saveWrite = true ;
   this->bdrAttr.reset() ;
   this->txtAttr.reset() ;
   this->termAttr.reset() ;

   this->tset = new TermSet ;          // allocate and initialize TermSet object

   this->ioLocale = new locale("") ;   // get the terminal default locale

   //* Get a copy of the data members from the parent *
   //* AnsiCmd object. See note above.                *
   if ( acObjPtr != NULL )
   {
      acObjPtr->getDataMembers ( this->tset,
                                 &this->termAttr,
                                 this->ioLocale,
                                 this->termRows,
                                 this->termCols,
                                 this->fontNum,
                                 this->fullReset,
                                 this->wideStream
                               ) ;
   }
   //* Save the base object's pointer, then set pointer *
   //* for non-member access to the ACWin object.       *
   //* See the signal handler methods for details.      *
   //**------------------------------------------------**
   //* NOTE: This is dangerous. Don't try this at home. *
   //**------------------------------------------------**
   this->origaoptr = acObjPtr ;
   acObjPtr = (AnsiCmd*)(this) ;

   //* If non-default terminal configuration data provided, *
   //* initialize _only_ selected parameters.               *
   if ( termDef != NULL )
      this->init_Term ( termDef ) ;

   //* Set the flag indicating that terminal configuration *
   //* is under the control of an ACWin object.            *
   //* This disables certain terminal configuration        *
   //* options that may be requested by the user.          *
   this->tset->isWin = true ;

   //* Range check: window dimensions >= minimum *
   if ( winHeight < minDIM )           winHeight = minDIM ;
   if ( winWidth < minDIM )            winWidth = minDIM ;
   //* Range check: window dimensions <= terminal dimensions.*
   if ( winHeight > this->termRows )   winHeight = this->termRows ;
   if ( winWidth > this->termCols )    winWidth = this->termCols ;
   //* Range check: window origin is within terminal window.*
   if ( posRow <= ZERO )               posRow = 1 ;
   if ( posCol <= ZERO )               posCol = 1 ;
   //* Range check: window does not extend beyond terminal window.*
   if ( (posRow + winHeight - 1) > this->termRows )
      posRow = this->termRows - winHeight + 1 ;
   if ( (posCol + winWidth - 1) > this->termCols )
      posCol = this->termCols - winWidth + 1 ;

   //* Window position and dimensions now within the terminal window. (1-based) *
   this->winBase = { posRow, posCol } ;   // window position
   this->height = winHeight ;             // window height (incl. border)
   this->width  = winWidth ;              // window width (incl. border)
   this->txtRows = this->height - 2 ;     // text (interior) rows
   this->txtCols = this->width - 2 ;      // text (interior) columns

   //* Upper left corner of window interior i.e. text area (1-based) *
   this->txtBase = { short(this->winBase.row + 1), short(this->winBase.col + 1) } ;
   this->txtOff = { 0, 0 } ;     // OFFSET from upper-left corner of interior

   //* Allocate storage for the window text area: one _wide_ character     *
   //* for each character cell PLUS space for newlines and null terminator.*
   this->fifo = new BlockFifo( this->txtBase, this->txtAttr.acaVal ) ;

}  //* End initialize() *

//*************************
//*       init_Term       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Initialize terminal configuration. Called only by the initialize() method,   *
//* and only if the constructor is invoked with a pointer to a TermConfig object.*
//*                                                                              *
//* See notes below about which parameters will be referenced and which ignored. *
//*                                                                              *
//* Input  : termDef  : pointer to a TermConfig object containing terminal       *
//*                     setup information.                                       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* TermConfig Members which are processed (or verified) here:                   *
//* ----------------------------------------------------------                   *
//* -- termStream: set specified stream for terminal control                     *
//* -- localeName: set specified locale                                          *
//* -- foreground/background: terminal attributes (if not defaults)              *
//* -- nonStandard: if specified, set 'fullReset' flag                           *
//* -- cursorStyle: set specified cursor style                                   *
//*                                                                              *
//* TermConfig Members which are not handled here:                               *
//* ----------------------------------------------                               *
//* -- configFile    : not yet implemented                                       *
//* -- wideStream    : ignored, default wide I/O streams already initialized     *
//* -- bufferInput   : ignored, set by OpenWindow()                              *
//* -- echoOption    : ignored, set by OpenWindow()                              *
//* -- suspendSignal : ignored, set by OpenWindow()                              *
//* -- quitSignal    : ignored, set by OpenWindow()                              *
//* -- bcType        : ignored, set by OpenWindow()                              *
//* -- bcAlert       : ignored, set by OpenWindow()                              *
//*                                                                              *
//********************************************************************************

void ACWin::init_Term ( const TermConfig *termDef )
{

   aeSeq tmpFg = termDef->foreground,     // temporary fgnd/bgnd
         tmpBg = termDef->background ;

   //* If I/O stream != stdIn *
   if ( (termDef->termStream == stdOut) || (termDef->termStream == stdErr) )
      this->tset->aStr = termDef->termStream ;

   //* If specified locale != locale from environment *
   if ( *termDef->localeName != NULLCHAR )
      this->acSetLocale ( termDef->localeName ) ;

   //* If terminal window foreground/background attributes != default *
   //* New values will be encoded and stored in 'termAttr' member.    *
   if ( (tmpFg < aesFG_BLACK) || (tmpFg > aesFG_DFLT) )  // range check
      tmpFg = aesFG_DFLT ;
   if ( (tmpBg < aesBG_BLACK) || (tmpBg > aesBG_DFLT) )  // range check
      tmpBg = aesBG_DFLT ;
   if ( (tmpFg != aesFG_DFLT) || (tmpBg != aesBG_DFLT) )
   {
      bool setFlag ;
      acAttr fg = this->termAttr.aeseq2bits ( tmpFg, setFlag ),
             bg = this->termAttr.aeseq2bits ( tmpBg, setFlag ) ;
      this->termAttr.decode( acAttr( fg | bg ) ) ;
   }

   //* If ANSI-non-compliant-terminal flag set *
   if ( termDef->nonStandard )
      this->fullReset = true ;

   //* If a cursor style specified.                 *
   //* Style is stored in TermSet 'curStyle' member.* 
   if ( termDef->cursorStyle != csDflt )
      this->tset->cursorStyle ( termDef->cursorStyle ) ;

}  //* End init_Term() *

//*************************
//*    formatFieldText    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Scan the text data, and if necessary, reformat it to fit the field.          *
//*                                                                              *
//* The cursor offset within the field is updated to match the insertion point   *
//* (IP). If the IP is not in the desired position, the cursor will also be      *
//* incorrect. Beware!                                                           *
//*                                                                              *
//* Note that the data are not truncated; however, newline characters are        *
//* inserted if necessary to be sure the data do not overrun the edge of         *
//* the field. (If flow option is floShift, then text is not formatted.)         *
//*                                                                              *
//* Important Note: Cursor movement and scrolling rely on properly-formatted     *
//*                 source text (see parseFieldText method). Therefore,          *
//*                 incorrectly formatted text _must not_ be stored              *
//*                 in the 'txt' member.                                         *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*          fIndx   : index of target field                                     *
//*          force: (optional, 'false' by default)                               *
//*                 if set, then force reformatting of fields with flowing text, *
//*                 Applies only to multi-row fields and only when flow!=floShift*
//*                                                                              *
//* Returns: current state of the fptr->mrp.ovr flag                             *
//*          'false' if no errors                                                *
//*          'true'  if IP and/or logical cursor is outside visible space.       *
//********************************************************************************
//* Parse the text for the field into one sub-string for each row of the field.  *
//* The parsed data are then concatenated into a single string with each row     *
//* (except the last) defined by a newline character ('\n').                     *
//*                                                                              *
//* 1) If current formatting of each line is within the field, then do not       *
//*    change the formatting. (but see 'force' parameter)                        *
//* 2) If current formatting violates the field boundaries, apply current        *
//*    formatting option: enum FloType in fptr->flow.                            *
//*    a) floChar:                                                               *
//*       Insert line breaks after the last CHARACTER which fits on the line.    *
//*    b) floToken || floHyphen:                                                 *
//*       Insert line breaks after the last TOKEN which fits on the line.        *
//*    c) floShift:                                                              *
//*       This formatting option is handled directly by the output routine;      *
//*       therefore, the text is not formatted here. The only action taken here  *
//*       is to calculate the cursor position ('cur').                           *
//* 3) If the field currently contains 'selected' text, the selection indices,   *
//*    are reset                                                                 *
//*                                                                              *
//********************************************************************************

bool ACWin::formatFieldText ( skForm* skfp, short fIndx, bool force )
{
   skField* fptr = &skfp->fld[fIndx] ;    // pointer to target field
   gString gsParse ;                      // parse buffer

   //* If field contains 'selected' text, de-select it. *
   //* (display is not updated)                         *
   if ( fptr->sela < fptr->selb )
      this->skpClearSelection ( skfp, false ) ;

   //* For fields configured for text-editor mode, *
   //* manual formatting is not required.          *
   //* (This includes all single-row fields.)      *
   else if ( fptr->flow == floShift )
   {
      //* Newlines are incompatible with single-row *
      //* fields. Silently delete them.             *
      if ( fptr->hgt == 1 )
         fptr->txt.erase( NEWLINE, ZERO, false, true ) ;

      //* Analyze the text data and set      * 
      //* cursor offset to match current IP. *
      this->parseFieldText ( fptr ) ;     // (format text, floShift)
   }

   else     // (fptr->hgt > 1) && (fptr->flow != floShift)
   {
      //* If 'force' flag is set, and floToken || floHyphen specified, *
      //* strip all newlines and reformat to fit field dimensions.     *
      if ( force && (fptr->flow != floChar) )
      {
         this->FormatParagraph ( fptr->txt, fptr->hgt, fptr->wid, false,
                                 (fptr->flow == floHyphen ? true : false) ) ;
      }

      //* floChar formatting specified. Scan the data *
      //* and if not pre-formatted, (or 'force'       *
      //* parse the data into seperate display rows   *
      //* delimited by newline characters.            *
      else
      {
         if ( (this->fftReformat ( fptr, gsParse, force )) )
         {
            //* Strip the trailing newline (if any).*
            int eoti = (gsParse.gschars()) - 2,
                ti   = gsParse.findlast( NEWLINE ) ;
            if ( ti == eoti )
               gsParse.limitChars( eoti ) ;
            fptr->txt = gsParse ;            // save the reformatted text
         }
      }
      this->parseFieldText ( fptr ) ;        // (re-format text, not floShift)
   }
   return ( fptr->mrp.ovr ) ;

}  //* End formatFieldText() *

//*************************
//*      fftReformat      *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Called by formatFieldText().                                                 *
//* Scan the field text, and if needed, format the text to fit the field.        *
//*                                                                              *
//*    NOTE: Do not call this method if 'flow' == floShift.                      *
//*                                                                              *
//* 1) If current formatting of each line is within the field, then formatting   *
//*    is not modified, (but see 'force' flag).                                  *
//* 2) If display of the source data as currently formatted would violate the    *
//*    boundaries of the target field, then:                                     *
//*    a) If 'flow' == floChar, reformat the data for the row which overflowed,  *
//*       and subsequent rows, breaking each text row after the last character   *
//*       that will fit within the width of the field.                           *
//*    b) If 'flow' == floToken || floHyphen, use the gString class to perform   *
//*       intelligent formatting on the text of the overflowed row and all       *
//*       subsequent rows.                                                       *
//*                                                                              *
//* Input  : fptr    : pointer to field containing the text to be parsed         *
//*          gsParse : (by reference) receives the reformatted data or if        *
//*                    no reformatting required, then a copy of the source data. *
//*          force   : (optional, 'false' by default)                            *
//*                    if set, then force reformatting of fields text,           *
//*                                                                              *
//* Returns: 'true'  if data have been reformatted                               *
//*          'false' if no reformatting necessary (or if flow==floShift)         *
//********************************************************************************
//* Programmer's Note: For flow==floChar, the parsing is inefficient because we  *
//* may read the same unformatted (or incorrectly formatted) text sequences      *
//* multiple times. The floChar option should seldom, if ever be used by any     *
//* self-respecing developer, so this should not be a problem, but the           *
//* algorithm will be revisited in a future release.                             *
//*                                                                              *
//* For flow==floToken || flow==floHyphen:                                       *
//* When calculating whether the text of a row "fits" within the field's visible *
//* area, we reference 'eor' (end-of-row) rather than 'eov' (end-of-visible)     *
//* text. Because newline and nullchar characters have zero width, they do not   *
//* affect the column count. Therefore, the character referenced by 'eor' may    *
//* lie outside the visible space. This is a design decision which fascilitates  *
//* editing of the text. This decision also affects the parameters for calling   *
//* the gString formatParagraph() method, specifically specifying the width as   *
//* 'fptr->wid', rather that '(fptr->wid - 1), which would force the newlines    *
//* to stay within visible space. We may revisit this decision in a future       *
//* release.                                                                     *
//*                                                                              *
//********************************************************************************

bool ACWin::fftReformat ( const skField* fptr, gString& gsParse, bool force ) const
{
   const short CHUNK = 300 ;           // max characters per iteration

   //* If an empty field OR if 'floShift', return error. *
   if ( (fptr->txt.gschars() == 1) || (fptr->flow == floShift) )
   { gsParse = fptr->txt ; return false ; } // Note the early return

   gString gsFmt ;                        // text formatting
   gString gsSrc( fptr->txt ) ;           // working copy of source data
   if ( force )                           // if specified, remove initial formatting
      gsSrc.erase( L'\n', ZERO, false, true ) ;
   const wchar_t* wptr = gsSrc.gstr() ;   // pointer to source data
   wchar_t* wbuff = new wchar_t[fptr->txt.wAlloc()] ; // work buffer
   int  srcChars = gsSrc.gschars(),       // number of source characters
        si = ZERO,                        // source index
        ti = ZERO ;                       // target index
   bool reformatted = false ;             // return value

   gsParse.clear() ;                   // initialize caller's buffer

   while ( si < srcChars )
   {
      //* Copy characters to work buffer.* 
      if ( (wptr[si] != NEWLINE) && (wptr[si] != NULLCHAR) && (ti < CHUNK) )
         wbuff[ti++] = wptr[si++] ;

      //* Newline characters indicate end-of-row.  *
      //* Null character indicates end-of-text.    *
      //* If 'ti' >= CHUNK, source is unformatted. *
      else
      {
         wbuff[ti] = NULLCHAR ;           // terminate the sub-string
         gsFmt = wbuff ;                  // test column count for this row

         //* If isolated row text fits within the field, *
         //* copy it to caller's buffer.                 *
         if ( (gsFmt.gscols()) <= fptr->wid )
         {
            gsParse.append( gsFmt.gstr() ) ;
            if ( wptr[si] == NEWLINE )    // copy the newline
               gsParse.append( NEWLINE ) ;
            ++si ;                        // step over end-of-row marker
         }

         //* Text in gsFmt is wider than the field.                *
         //* Apply formatting to current row, and subsequent rows. *
         else
         {
            //* If intelligent formatting option specified, *
            //* parse the current row AND all subsequent    *
            //* rows using the specified formatting option. *
            if ( (fptr->flow == floToken) || (fptr->flow == floHyphen) )
            {
               //* Copy the remaining unprocessed text to the formatting buffer. *
               // Programmer's Note The specified height for formatting is actually 
               // wrong here (if some data rows have already been processed), but it 
               // SHOULDN'T matter because the data are not truncated to fit the height.
               if ( wptr[si] != NULLCHAR )
                  gsFmt.append( &wptr[si] ) ;
               gsFmt.erase( NEWLINE, ZERO, false, true ) ; // remove all line breaks
               gsFmt.formatParagraph( fptr->hgt, fptr->wid, false, 
                                      bool(fptr->flow == floHyphen) ) ;
               gsParse.append( gsFmt.gstr() ) ;
            }

            //* Formatting option is 'floChar'. Insert newline(s) *
            //* to break each full row of remaining remaining     *
            //* text at edge of field.                            *
            //* Note that newline will be within visible space.   *
            else  // (flow == floChar)
            {
               //* Copy the remaining unprocessed text to the temp buffer. *
               while ( wptr[si] != NULLCHAR )
                  wbuff[ti++] = wptr[si++] ;
               wbuff[ti] = NULLCHAR ;     // terminate the data

               for ( si = ZERO ; si < ti ; )
               {
                  gsFmt.loadChars( &wbuff[si], CHUNK ) ; // load a "chunk" of text
                  if ( gsFmt.gscols() > fptr->wid )      // if wider than target field
                  {
                     gsFmt.limitCols( fptr->wid - 1 ) ;  // isolate text for row
                     gsParse.append( "%S\n", gsFmt.gstr() ) ; // append to output buffer
                     si += gsFmt.gschars() - 1 ;         // set index beyond processed text
                  }
                  else  // partial row, do not append a newline
                  { gsParse.append( gsFmt.gstr() ) ; break ; }
               }
            }
            reformatted = true ;          // text has been modified
            break ;                       // all text processed
         }
         ti = ZERO ;                      // reset target index
      }
   }     // while()

   delete [] wbuff ;                      // release the dynamic allocation

   return reformatted ;

}  //* fftReformat() *

//*************************
//*      OpenWindow       *
//*************************
//********************************************************************************
//* Complete window setup:                                                       *
//*  1) Complete terminal configuration, then                                    *
//*  2) Draw window and its contents in the terminal window.                     *
//*                                                                              *
//* Input  : initText : (optional, null pointer by default)                      *
//*                     initial text to be written to window                     *
//*                      -- Note that if initial text overlaps the defined       *
//*                         controls, then the text will be obscured.            *
//*          initField: (optional, -1 by default) [CURRENTLY UNUSED]             *
//*                     index of target field for storing 'initText' data        *
//*                     Ignored if 'initText' not specified.                     *
//*                                                                              *
//* Returns: cursor position (offset) i.e. text insertion point                  *
//********************************************************************************
//* Programmer's Note: It costs a few extra CPU cycles to call the high-level    *
//* configuration methods, but it allows debugging info (if enabled) to be       *
//* written to the log file.                                                     *
//********************************************************************************

WinPos ACWin::OpenWindow ( const char* initText, short initField )
{
   //** Terminal Configuration: **
   //* Disable input buffering, set non-blocking read  *
   //* of the input stream, and set "soft-echo" option.*
   this->acBufferedInput ( false, softEchoA, false ) ;

   //* Capture break signal and convert to Ctrl+C keycode *
   if ( this->tset->cchType != cchtCopy )
      this->tset->captureBreakSignal ( true, cchtCopy ) ;

   //* Capture 'suspend' signal and convert to Ctrl+Z keycode *
   if ( ! this->tset->suspCap )
      this->tset->captureSuspendSignal ( true ) ;

   //* Capture 'quit' signal, enabling an orderly shutdown when signal received.*
   if ( ! this->tset->quitCap )        // capture Ctrl+4 (and Ctrl+\)
      this->tset->captureQuitSignal ( true ) ;

   if ( ! this->tset->flowCap )        // disable Ctrl+S and Ctrl+Q signals
      ;  // (this flag should always be set when buffering is disabled)

   //* Set cursor to invisible while drawing window.     *
   //* This make for a less-visible write to the display.*
   if ( this->tset->curVis )
      this->acSetCursorStyle ( csHide ) ;

//   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_PHOTO != 0
//   //* Take a screenshot of the window and save to debug file.*
//   this->Screenshot ( true, "Called from OpenWindow" ) ;
//   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_PHOTO

   //* Clear the area of the terminal window *
   //* where the window will be drawn.       *
   this->clearWin ( this->txtAttr, true ) ;

   //* Draw the window border *
   this->drawBorder () ;

   //* Draw the initial window text, if provided. *
   //* Cursor position will be at end-of-text.    *
   //* Else, set cursor to window origin.         *
   if ( initText != NULL )
   {
      this->setAttributes ( this->txtAttr ) ;
      this->txtOff = this->Write ( this->txtOff, initText ) ;
   }
   else
      this->acSetCursor ( this->txtBase ) ;

   //* If cursor style change when Insert key is pressed   *
   //* is enabled, set the initial cursor style.           *
   //* Otherwise, current cursor style is not modified.    *
   if ( this->skf->insc != false )
      this->acSetCursorStyle ( (this->skf->ins ? this->skf->icur : this->skf->ocur) ) ;

   //* Draw all field objects. *
   this->RefreshField ( this->skf->fCnt ) ;

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

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_WDUMP != 0
   this->dumpConfig2Log ( L"OpenWindow()" ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_WDUMP

   return ( this->txtOff ) ;     // return window insertion point (relative offset)

}  //* End OpenWindow() *

//*************************
//*      CloseWindow      *
//*************************
//********************************************************************************
//* Close the window: Erase the window display data.                             *
//* As a side-effect, input buffering is re-enabled.                             *
//*                                                                              *
//* Note that the window object still exists in memory. To release the window's  *
//* resources, invoke the destructor i.e. delete the ACWin object.               *
//* Example: ACWin *winPtr = new ACWin ;  ...  delete winPtr ;                   *
//*                                                                              *
//* Input  : clear    : (optional, 'true' by default)                            *
//*                     if 'true',  clear (erase) the display area               *
//*                     if 'false', do not clear the display area                *
//*          fillColor: (optional, terminal dflt attr by default)                *
//*                     color attribute used to erase data from window.          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::CloseWindow ( bool clear, acAttr fillColor )
{
   //* Erase the window object from the terminal display. *
   if ( clear )
   {
      if ( fillColor == acaUSEDFLT )   // use saved terminal display attributes
         this->clearWin ( this->termAttr, true ) ;
      else                             // decode caller's background attribute
      {
         acaExpand acatmp( fillColor ) ;
         this->clearWin ( acatmp, true ) ;
      }
   }
   this->restoreAttributes () ;        // Return background to terminal default

   this->acBufferedInput ( true, termEcho ) ;   // Re-enable buffered input

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_WDUMP != 0
   this->dumpConfig2Log ( L"CloseWindow()" ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_WDUMP

}  //* End CloseWindow() *

//*************************
//*      RefreshWin       *
//*************************
//********************************************************************************
//* Redraw the window to display any changes that have been made, or to recover  *
//* when other data have obscured the the window.                                *
//* Examples:                                                                    *
//*  -- Call RefreshWin() after a previoius call to HideWin()                    *
//*  -- Refresh data which may have been obscured by a control which has just    *
//*     been set to "hidden".                                                    *
//*                                                                              *
//* Input  : all  : (optional, 'true' by default)                                *
//*                 if 'true',  refresh border and interior                      *
//*                 if 'false', refresh interior only                            *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::RefreshWin ( bool all )
{
   DataBlock* dbptr ;               // pointer to stored data from direct writes
   bool  cvState ;                  // current state of cursor 'curVis' flag

   //* Set cursor to invisible while drawing window.     *
   //* This make for a less-visible write to the display.*
   if ( (cvState = this->tset->curVis) )
      this->acSetCursorStyle ( csHide ) ;

   //* Clear the area of the terminal window *
   //* where the window will be drawn.       *
   this->clearWin ( this->txtAttr, all ) ;

   //* Draw the window border *
   if ( all )
      this->drawBorder () ;

   //* If data records were written directly to the window *
   //* interior, retrieve and display them.                *
   if ( (dbptr = this->fifo->pullRecord ( true )) != NULL )
   {
      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
      if ( ofsdbg.is_open() )
      {  gsdbg.compose( "RefreshWin: top:%02hd next:%02hd", 
                        &this->fifo->top, &this->fifo->next ) ;
         ofsdbg << gsdbg.ustr() << endl ; }
      #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO

      gString gsBox ;
      LineType boxLine ;
      short boxHgt, boxWid, boxConv ;
      bool  boxClr ;

      this->saveWrite = false ;        // disable save to FIFO
      do
      {
         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
         if ( ofsdbg.is_open() ) { this->dumpFifoRecord2Log ( dbptr ) ; }
         #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO

         //* Display the retrieved record.      *
         //* The record may be an encoded box   *
         //* object or plain text data.         *
         boxConv = ZERO ;
         if ( (dbptr->txt[1] == dbFifoTemplate[1]) && 
              (dbptr->txt[3] == dbFifoTemplate[3]) )
         {
            gsBox = dbptr->txt ;
            boxConv = gsBox.gscanf( dbFifoTemplate, &boxHgt, &boxWid, &boxLine, &boxClr ) ;
         }
         if ( boxConv == 4 )
            this->DrawBox ( dbptr->wp, boxHgt, boxWid, boxLine, dbptr->aca.acaVal, boxClr ) ;
         else
            this->Write ( dbptr->wp, dbptr->txt, dbptr->aca.acaVal ) ;
      }
      while ( (dbptr = this->fifo->pullRecord ( false )) != NULL ) ;
      this->saveWrite = true ;         // re-enable save to FIFO
   }

   //* For all defined fields, display field objects. *
   this->RefreshField ( this->skf->fCnt ) ;

   if ( cvState )
      this->acSetCursorStyle ( csShow ) ; // make the cursor visible

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

}  //* End RefreshWin() *

//*************************
//*        HideWin        *
//*************************
//********************************************************************************
//* Temporarily erase the window from the terminal display.                      *
//*                                                                              *
//* All window data are retained; but will not be visible to the user.           *
//*                                                                              *
//* Input  : fillAttr : (optional, default: terminal default attributes)         *
//*                     color attribute used to erase data from window.          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::HideWin ( acAttr fillAttr )
{
   if ( fillAttr == acaUSEDFLT )    // use saved terminal display attributes
      this->clearWin ( this->termAttr, true ) ;
   else                             // decode caller's background attribute
   {
      acaExpand acatmp( fillAttr ) ;
      this->clearWin ( acatmp, true ) ;
      this->setAttributes ( this->termAttr ) ;  // restore terminal attributes
   }

}  //* End HideWin() *

//*************************
//*        MoveWin        *
//*************************
//********************************************************************************
//* Move the window to another position within the terminal window.              *
//* This is simply erasing the window, updating the base position and redrawing  *
//* the window at the new position.                                              *
//*                                                                              *
//*                                                                              *
//* Input  : newPos   : (by reference) new row/column position for window origin *
//*                     (upper-left corner of window)                            *
//*          newText  : (optional, null pointer by default)                      *
//*                     text to be written to interior of window, beginning at   *
//*                     the upper left corner of the window interior.            *
//*                                                                              *
//* Returns: 'true'  if success                                                  *
//*          'false' window position not updated if specified position           *
//*                  would place any part of the window beyond the edges         *
//*                  of the terminal window.                                     *
//********************************************************************************

bool ACWin::MoveWin ( const WinPos& newPos, const char *newText )
{
   WinPos wpnew = newPos ;                // working copy of new position
   bool iLikeToMoveIt = false ;           // return value

   //* Range check the new position *
   if ( wpnew.row < 1 )    wpnew.row = 1 ; // silently convert zero to 1-based.
   if ( wpnew.col < 1 )    wpnew.col = 1 ;
   if ( ((wpnew.row + this->height) < this->termRows) &&
        ((wpnew.col + this->width) < this->termCols) )
   {
      this->HideWin () ;
      this->winBase = wpnew ;
      this->txtBase = { short(this->winBase.row + 1), 
                        short(this->winBase.col + 1) } ;
      this->RefreshWin () ;
      iLikeToMoveIt = true ;
   }

   return iLikeToMoveIt ;

}  //* End MoveWin() *

//*************************
//*       ClearWin        *
//*************************
//********************************************************************************
//* Erase all text from the interior (text area) of the window.                  *
//*                                                                              *
//* The text in the skForm object (if any) is not redrawn. See also RefreshWin().*
//*                                                                              *
//* Note on cursor visibility:                                                   *
//* -- On entry, cursor is set to invisible, and remains invisible on return,    *
//*    on the assumption that caller will write some additional data to the      *
//*    window, OR will move the input focus to one of the skField objects.       *
//*                                                                              *
//* Input  : newAttr : (optional, acaUSEDFLT by default)                         *
//*                    new color attributes for text area of window              *
//*                    (logical OR of forground and background color)            *
//*                    (attributes and attribute modifiers          )            *
//*                                                                              *
//* Returns: Cursor position (text insertion point)                              *
//*          This is the upper-left corner of the text area (0/0).               *
//********************************************************************************

WinPos ACWin::ClearWin ( acAttr newAttr )
{
   //* If cursor is visible, set to invisible. *
   if ( this->tset->curVis )
      this->acSetCursorStyle ( csHide ) ;

   //* Discard any saved data prevously written *
   //* directly to window interior.             *
   this->fifo->reset ( this->txtAttr.acaVal ) ;

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
   this->dumpFifo2Log ( this->fifo, L"ACWin ClearWin:" ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO

   if ( newAttr != acaUSEDFLT )     // if new interior attribute specified
      this->txtAttr.decode( newAttr ) ;

   //* Set the interior background color attribute *
   this->setAttributes ( this->txtAttr, false, true ) ;

   //* Clear the display area *
   this->acClearArea ( this->txtBase.row, txtBase.col, 
                       this->txtRows, this->txtCols ) ;

   //* Set cursor offset to upper-left corner, *
   //* and set physical cursor to txtBase.     *
   this->txtOff = { 0, 0 } ;
   this->acSetCursor ( this->txtBase ) ;

   //* Restore current terminal attributes *
   this->restoreAttributes () ;

   return ( this->txtOff ) ;     // return new cursor position

}  //* End ClearWin() *

//*************************
//*        WinInfo        *
//*************************
//********************************************************************************
//* Returns a copy of the window's current size, position, attributes and other  *
//* basic information.                                                           *
//*                                                                              *
//* Note that the current window dimensions and position are not guaranteed to   *
//* be those specified during instantiation. This is because the constructor     *
//* range checks and automatically adjusts these values as necessary to fit      *
//* within the terminal window.                                                  *
//*                                                                              *
//* Input  : wInfo  : (by reference) a winInfo-class object to receive the data  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::WinInfo ( winInfo& wInfo )
{
   wInfo.winBase      = this->winBase ;
   wInfo.height       = this->height ;
   wInfo.width        = this->width ;
   wInfo.txtRows      = this->txtRows ;
   wInfo.txtCols      = this->txtCols ;
   wInfo.bdrAttr      = this->bdrAttr.acaVal ;
   wInfo.txtAttr      = this->txtAttr.acaVal ;
   wInfo.bdrStyle     = this->bdrStyle ;
   wInfo.skf          = this->skf ;
   #if INCLUDE_WAYLANDCB != 0
   wInfo.wcbConnected = wcbConnected ;
   #else    // WaylandCB not compiled
   wInfo.wcbConnected = false ;
   #endif   // INCLUDE_WAYLANDCB
   this->getConfig ( wInfo.tcfg ) ;

}  //* End WinInfo() *

//*************************
//*       ClearRow        *
//*************************
//********************************************************************************
//* Erase text from the specified row of the text area.                          *
//*                                                                              *
//* Note: ClearRow() does not clear the stored text-field data associated with   *
//*       the target row. To clear a single text field within the window, use    *
//*       the ClearField() method instead.                                       *
//*                                                                              *
//* Input  : trgRow  : target row (0-based offset into text area)                *
//*          txtAttr : (optional, acaUSEDFLT by default)                         *
//*                    new color attributes for text area of window              *
//*                    (logical OR of forground and background color)            *
//*                    (attributes and attribute modifiers          )            *
//*                                                                              *
//* Returns: Cursor position (text insertion point)                              *
//*          This is the left edge of the specified row (trgRow/0).              *
//*          Note: If 'trgRow' is out-of-range, window will not be modified.     *
//********************************************************************************

WinPos ACWin::ClearRow ( short trgRow, acAttr txtAttr )
{
   if ( (trgRow >= ZERO) && (trgRow < this->txtRows) )
   {
      if ( txtAttr != acaUSEDFLT )     // if new interior attribute specified
         this->txtAttr.decode( txtAttr ) ;

      //* Set the background color attribute *
      this->acSetBg ( this->txtAttr.aesBgnd ) ;

      //* Clear the target row *
      gString gs ;
      gs.padCols( this->txtCols ) ;
      this->acSetCursor ( this->txtBase.row + trgRow, this->txtBase.col ) ;
      this->ttyWrite ( gs ) ;

      //* Set cursor position to left edge of target row *
      this->txtOff = { trgRow, 0 } ;

      //* Restore terminal-window background attribute *
      this->acSetBg ( this->termAttr.aesBgnd ) ;

      //* Restore terminal-window background attribute *
      this->acSetBg ( this->termAttr.aesBgnd ) ;
   }
   return ( this->txtOff ) ;

}  //* End ClearRow() *

//*************************
//*     SetFieldData      *
//*************************
//********************************************************************************
//* Set or modify the setup data for the specified field.                        *
//* The setup data parameters include:                                           *
//*   1) field text                                                              *
//*   2) field color attributes                                                  *
//*   3) insertion point (cursor position)                                       *
//* The effect is the same as seperately calling three individual methods:       *
//* SetFieldText, SetFieldAttr and SetFieldIP except that this method is more    *
//* efficient if modifying multiple parameters.                                  *
//*                                                                              *
//* Not all parameters apply equally to all field types. Some combinations of    *
//* field-type and configuration settings are fixed on instantiation, and some   *
//* configuration settings are managed internally by the library methods.        *
//*   1) "modify"   may be modified under application control.                   *
//*      (Note that timing may be an issue, so make modification only )          *
//*      (when target field does not have focus.                      )          *
//*   2) "static"   indicates set during instantiation, cannot be modified.      *
//*   3) "program"  value is programatically managed by the library.             *
//*                                                                              *
//* Field  Text    Color   Ins.                                                  *
//* Type   Data    Attr    Point                                                 *
//* -----  ------  -----   ------                                                *
//* ftTB   modify  modify  modify                                                *
//* ftSB   modify  modify  modify                                                *
//* ftPB   modify  modify  static                                                *
//* ftRB   static  modify  static                                                *
//* ftMB   static  modify  program                                               *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          srcTxt  : text to be written to target field                        *
//*          txtAttr : (optional, default: acaUSEDFLT, indicating that current)  *
//*                    (attributes should not be modified)                       *
//*                    acAttr bitfield which is a logical OR of forground and    *
//*                    background color attributes and text modifiers            *
//*          newIp   : (optional, zero(0) by default)                            *
//*                    character offset into the text of the field               *
//*                    ZERO <= newIp <= text characters                          *
//*                    where ZERO == field origin and any value at or beyond     *
//*                    the number of characters in the array signals that IP     *
//*                    should be set to end-of-text.                             *
//*          refresh : (optional, default:true) refresh display                  *
//*                    if 'true'  make changes visible                           *
//*                    if 'false' do not update display                          *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if invalid field index specified                            *
//********************************************************************************

bool ACWin::SetFieldData ( short fIndx, const gString& srcTxt, acAttr txtAttr, 
                           int newIp, bool refresh )
{
   bool fref    = false,                  // refresh display
        status  = false ;                 // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr  = &this->skf->fld[fIndx] ; // pointer to field object
      FldType  ftype = fptr->type ;             // control type
      int      maxIp = fptr->txt.gschars() - 1 ;// range-check IP

      //* If negative IP value, set to zero,  or if control *
      //* defines a static IP, do not modify the IP.        *
      if ( (newIp < ZERO) || (ftype == ftPB) || (ftype == ftRB) )
         newIp = ZERO ;

      //* If a non-default color attribute is specified, *
      //* decode the new attribute. (all control types)  *
      if ( txtAttr != acaUSEDFLT )
      { fptr->aca.decode( txtAttr ) ; fref = true ; }

      //* If new field text is specified (specific control types only) *
      if ( srcTxt.gschars() > 1 )
      {
         if ( (ftype == ftTB) || (ftype == ftSB) || (ftype == ftPB) )
         {
            this->SetFieldText ( fIndx, srcTxt, false ) ; // set text with no refresh
            if ( ftype == ftTB )          // new max IP
               maxIp = fptr->txt.gschars() - 1 ;
            fref = true ;
         }
      }

      //* If a non-default IP is specified *
      if ( newIp != ZERO )
      {
         if ( newIp > maxIp )  newIp = maxIp ;
         this->parseFieldText ( fptr ) ;  // initialize field stats
         this->SetFieldIP ( fIndx, newIp, false, false ) ; // set IP with no refresh
         fref = true ;
      }

      //* If field data modified and refresh not disabled *
      if ( refresh && fref )
      {
         this->RefreshField ( fIndx ) ;   // make mods visible

         //* If target field is not the field with focus, *
         //* set visible cursor to the field with focus.  *
         if ( fIndx != this->skf->fi )
            this->RefreshField ( this->skf->fi ) ;

         this->restoreAttributes () ;     // Restore current terminal attributes
      }
      status = true ;
   }
   return status ;

}  //* End SetFieldData() *

//********************************************************************************
//* Overload referencing a null-terminated wchar_t text string.                  *
//********************************************************************************
bool ACWin::SetFieldData ( short fIndx, const wchar_t* srcTxt, acAttr txtAttr, 
                           int newIp, bool refresh )
{
   gString gsTxt( srcTxt ) ;
   bool status = false ;               // return value

   status = this->SetFieldData ( fIndx, gsTxt, txtAttr, newIp, refresh ) ;

   return status ;

}  //* End SetFieldData() *

//********************************************************************************
//* Overload referencing a null-terminated char text string.                     *
//********************************************************************************
bool ACWin::SetFieldData ( short fIndx, const char* srcTxt, acAttr txtAttr, 
                           int newIp, bool refresh )
{
   gString gsTxt( srcTxt ) ;
   bool status = false ;               // return value

   status = this->SetFieldData ( fIndx, gsTxt, txtAttr, newIp, refresh ) ;

   return status ;

}  //* End SetFieldData() *

//*************************
//*     GetFieldData      *
//*************************
//********************************************************************************
//* Get a copy of the current settings for the specified field.                  *
//* This method is intended primarily for reporting data during development;     *
//* however, the data may also be useful for reporting field data after the      *
//* EditForm method has returned control to the application.                     *
//*                                                                              *
//* NOTE: All data members _including_ protected members are reported.           *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          skfld   : (by reference) receives a copy of data members            *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if invalid field index specified                            *
//********************************************************************************

bool ACWin::GetFieldData ( short fIndx, skField& skfld ) const
{
   #define DEBUG_GFD (0)                  // for debugging only

   bool  status = false ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skfld = this->skf->fld[fIndx] ;
      status = true ;

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FDUMP != 0 && DEBUG_GFD != 0
      //* Compare source data with copied data. (see Test_Form()) *
      this->dumpField2Log ( &this->skf->fld[fIndx], L"GetFieldData(Live Data):" ) ;
      this->dumpField2Log ( &skfld, L"GetFieldData(Copied Data):" ) ;
      if ( fMrp != NULL )
      {
         gsdbg.compose( "fMrp: rCnt:%03d rtop:%03d rcur:%03d rbot:%03d rlst:%03d",
            &fMrp->rCnt, &fMrp->rtop, &fMrp->rcur, &fMrp->rbot, &fMrp->rlst ) ;
         ofsdbg << gsdbg << endl ;
      }
      #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FDUMP && DEBUG_GFD
   }
   return status ;

   #undef DEBUG_GFD
}  //* End GetFieldData() *

//*************************
//*     SetFieldText      *
//*************************
//********************************************************************************
//* Replace the current contents of the field with new data.                     *
//* The 'off' (vertical offset), 'shft' (horizontal offset) and 'ip'             *
//* (insertion point) members of the field are reset.                            *
//*                                                                              *
//* Field types for which text data may be modified:                             *
//*  1) ftTB (Textbox):                                                          *
//*     Source text replaces the existing text.                                  *
//*  2) ftSB Scrollbox                                                           *
//*     Source text is reformatted to fit the field width, and to accomodate     *
//*     the row indicator character, and then replaces existing text.            *
//*  3) ftPB (Pushbutton):                                                       *
//*     Although the text of a pushbutton control may be changed, the existing   *
//*     hotkey designation (if any) is not modified.                             *
//*                                                                              *
//* Text for other field types is set during instantiation and remains static.   *
//*                                                                              *
//* Input  : fIndx : index of target field within the skForm object              *
//*          srcTxt: text to be written to target field                          *
//*                  Text data may be in the form:                               *
//*                  1) gString object (by reference)                            *
//*                  2) pointer to wide (wchar_t) text data                      *
//*                  3) pointer to UTF-8 (byte-oriented) text data               *
//*          refresh : (optional, default: true)                                 *
//*                    if 'true',  refresh the field display                     *
//*                    if 'false', do not refresh the display                    *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if invalid field index specified, or if operation           *
//*                  is not supported for target field type                      *
//********************************************************************************
//* Note: For Pushbuttons, a caret ('^') indicating a hotkey will be silently    *
//*       removed. Pushbutton text is also padded to the width of the field.     *
//********************************************************************************

bool ACWin::SetFieldText ( short fIndx, const gString& srcTxt, bool refresh )
{
   bool status = false ;               // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( (fptr->type == ftPB) || (fptr->type == ftTB) || (fptr->type == ftSB) )
      {
         fptr->ip = fptr->off = ZERO ; fptr->shft = ZERO ;
         fptr->cur = { 0, 0 } ;

         if ( fptr->type == ftPB )
         {  // (see note above)
            gString gs = srcTxt ;
            if ( (gs.find( CARET )) >= ZERO )
               gs.erase( CARET, ZERO, false, true ) ;
            gs.padCols( fptr->wid ) ;
            gs.limitCols( fptr->wid ) ;
            fptr->txt = gs ;
         }
         else if ( fptr->type == ftTB )
         {
            // Programmer's Note: Selections and Highlightes are cleared by the call.
            fptr->txt = srcTxt ;
            this->formatFieldText ( this->skf, fIndx ) ;
         }
         else     // (type == ftSB)
         {
            fptr->txt = srcTxt ;
            this->init_Scrollbox ( fptr ) ;
         }
         if ( refresh )
            this->RefreshField ( fIndx ) ;
         status = true ;
      }
   }
   return status ;

}  //* End SetFieldText() *

//********************************************************************************
//* Overload referencing a null-terminated wchar_t text string.                  *
//********************************************************************************
bool ACWin::SetFieldText ( short fIndx, const wchar_t* srcTxt, bool refresh )
{

   gString gs( srcTxt ) ;
   return ( (this->SetFieldText ( fIndx, gs, refresh )) ) ;

}  //* End SetFieldText() *

//********************************************************************************
//* Overload referencing a null-terminated char text string.                     *
//********************************************************************************
bool ACWin::SetFieldText ( short fIndx, const char* srcTxt, bool refresh )
{

   gString gs( srcTxt ) ;
   return ( (this->SetFieldText ( fIndx, gs, refresh )) ) ;

}  //* End SetFieldText() *

//*************************
//*     GetFieldText      *
//*************************
//********************************************************************************
//* Get a copy of the current contents of the target Textbox (ftTB) field.       *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          txtBuf  : (by reference) receives a copy of field text              *
//*                 OR pointer to wchar_t buffer                                 *
//*                 OR pointer to char (UTF-8) buffer                            *
//*          maxChars: for wchar_t target buffer, maximum number of wchar_t      *
//*                    characters to write                                       *
//*                 OR                                                           *
//*          maxBytes: for char target buffer, maximum number of UTF-8           *
//*                    bytes to write                                            *
//*                                                                              *
//* IMPORTANT NOTE: Target buffer must be large enough to hold _at least_        *
//*                 two(2) wchar_t characters OR five(5) UTF-8 bytes.            *
//*                                                                              *
//* Returns: number of characters written to target buffer including null        *
//*          terminator (if target is UTF-8 buffer, byte count is returned)      *
//*          -- If invalid field index specified, or if operation is not         *
//*             supported for target field type, the target buffer is set to     *
//*             an empty string, and a zero value is returned.                   *
//*          -- If the text is larger than the target buffer, then text will be  *
//*             truncated.                                                       *
//********************************************************************************

int ACWin::GetFieldText ( short fIndx, gString& txtBuf ) const
{
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) && 
        (this->skf->fld[fIndx].type == ftTB) )
   {
      txtBuf = this->skf->fld[fIndx].txt.gstr() ;
      return ( txtBuf.gschars() ) ;
   }
   else
   {
      txtBuf.clear() ;
      return ZERO ;
   }

}  //* End GetFieldText() *
//********************************************************************************
//* Overload referencing a wchar_t target buffer.                                *
//********************************************************************************
int ACWin::GetFieldText ( short fIndx, wchar_t* txtBuf, int maxChars )
{
   gString gs( gsALLOCMAX ) ;             // temporary buffer
   int copiedChars = ZERO ;               // return value
   txtBuf[0] = NULLCHAR ;                 // initialize target buffer

   //* Prevent stupidity: min == 1 character plus null terminator *
   if ( maxChars < 2 )     { maxChars = 2 ; }

   copiedChars = this->GetFieldText ( fIndx, gs ) ;
   if ( copiedChars > ZERO )
   {
      gs.copy( txtBuf, maxChars ) ;
      if ( maxChars < gs.gschars() )
         copiedChars = maxChars ;
   }
   return copiedChars ;

}  //* End GetFieldText() *
//********************************************************************************
//* Overload referencing a 'char' target buffer.                                 *
//********************************************************************************
int ACWin::GetFieldText ( short fIndx, char* txtBuf, int maxBytes )
{
   gString gs( gsALLOCMAX ) ;             // temporary buffer
   int copiedBytes = ZERO ;               // return value
   txtBuf[0] = NULLCHAR ;                 // initialize target buffer

   //* Prevent stupidity: min == 1 UTF-8 character plus null terminator *
   if ( maxBytes < 5 )     { maxBytes = 5 ; }

   if ( (this->GetFieldText ( fIndx, gs )) > ZERO )
   {
      gs.copy( txtBuf, maxBytes ) ;
      if ( maxBytes < gs.utfbytes() )
         copiedBytes = maxBytes ;
   }
   return copiedBytes ;

}  //* End GetFieldText() *

//*************************
//*   ReformatFieldText   *
//*************************
//********************************************************************************
//* Reformat text of the target field using the specified formatting type.       *
//* This operation applies ONLY to multi-row textbox fields (ftTB).              *
//* All existing newline characters are removed and the data are reformatted     *
//* according to the specified formatting option.                                *
//*  1) Set new formatting option if specified.                                  *
//*  2) Remove existing formatting (newline characters).                         *
//*  3) Reformat the data.                                                       *
//*  4) Set IP and logical cursor to field origin (upper-left corner).           *
//*  5) Refresh the display.                                                     *
//*                                                                              *
//* See also: SetFieldFlow() which sets the formatting option, and reformats the *
//* data ONLY if necessary to keep the text within the field boundaries.         *
//*                                                                              *
//* Input  : fIndx  : index of target field                                      *
//*          floType: (optional, 'floDflt' by default) member of enum FloType    *
//*                   specify the type of text formatting to be used             *
//*                   If 'floDflt', use the existing formatting option;          *
//*                   otherwise, set the formatting type to the specified value  *
//*                   before reformatting the data.                              *
//*                                                                              *
//* Returns: 'true'  if operation is successful                                  *
//*          'false' if a) invalid field index specified                         *
//*                     b) target field is not a multi-row textbox field (ftTB)  *
//********************************************************************************

bool ACWin::ReformatFieldText ( short fIndx, FloType floType )
{
   bool status = false ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( (fptr->hgt > 1) && (fptr->type == ftTB) )
      {
         if ( floType != floDflt )                    // set formatting option
            fptr->flow = floType ;
         fptr->off = fptr->shft = fptr->ip = ZERO ;   // set IP to origin
         fptr->txt.erase( NEWLINE, ZERO, false, true ) ; // remove existing formatting
         this->formatFieldText ( this->skf, fIndx ) ; // reformat text data and update indexing
         this->refreshField ( this->skf, fIndx ) ;    // refresh the display
         status = true ;                              // success
      }
   }
   return status ;

}  //* End ReformatFieldText() *

//*************************
//*    parseFieldText     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Scan the text of the field and calculate index values for each text row      *
//* (MRParse object), as well as the logical cursor position.                    *
//*                                                                              *
//* This scan is primarily for multi-row text fields; however, it also handles   *
//* initialization and logical cursor positioning for other fields.              *
//*                                                                              *
//* Members of the MRParase object are initialized:                              *
//* 1) 'rCnt' == total rows of text                                              *
//* 2) 'roff' == array of character offsets (index leftmost char of each row)    *
//* 3) 'rtop' == row index (0-based) of top _visible_ row                        *
//* 4) 'rcur' == row index (0-based) of row containing the IP                    *
//* 5) 'rbot' == row index (0-based) of bottom _visible_ row                     *
//* 6) 'rlst' == row index (0-based) of last text row (rCnt - 1)                 *
//* 7) 'bor'  == character index of first character on current row               *
//* 8) 'bov'  == character index of first _visible_ character on current row     *
//* 9) 'eov'  == character index of last _visible_ char on current row (floShift)*
//*10) 'eor'  == character index of last character on current row                *
//*11) 'ni'   == character index of null terminator                              *
//*12) 'cur'  == logical cursor position (zero-based offset from field origin)   *
//*13) 'ovr'  == flag is set if _calculated_ logical cursor position is outside  *
//*              visible space. (cursor position will be moved inside boundaries)*
//*                                                                              *
//* For speed, the calculations are broken into groups as defined by the 'scope' *
//* parameter described below. Caller is responsible for using some intelligence *
//* when deciding whether to call with 'scope' set to the default value.         *
//* -- For the default value of 'scope', two things must be true:                *
//*     1) The MRParse object was initialized by a previous call to this method. *
//*     2) The field text has not been modified or moved since the previous call.*
//*    In practice, this means that only cursor movement has occurred.           *
//* -- For all options, movement of the logical cursor may cause the 'ovr' flag  *
//*    to be set, indicating that the calculated logical cursor is outside the   *
//*    visible space. (In this case, cursor position will be adjusted to lie     *
//*    within visible space.)                                                    *
//*                                                                              *
//* Input  : fptr   : pointer to skField object to be scanned                    *
//*                   'mrp' member and logical cursor position will be updated   *
//*          scope  : (optional, default:3)                                      *
//*                   if 0, Update only the logical cursor position, in response *
//*                         to horizontal/vertical cursor movement within the    *
//*                         visible space. Text content and position within the  *
//*                         window have not been modified since previous call.   *
//*                         The 'cur' (cursor position) is updated to match IP.  *
//*                   if 1, Update the values which describe the row containing  *
//*                         the IP. 'bor', 'bov', 'eov', 'eor', 'cur'.           *
//*                         IP is on the same row as for previous call, but      *
//*                         horizontal shift may have occurred.                  *
//*                   if 2, Number of text columns on the current row may have   *
//*                         changed i.e. character(s) added or deleted, BUT      *
//*                         number of rows has not changed. Update current row   *
//*                         (see scope==1), and recalculate indices of the       *
//*                         following rows.                                      *
//*                         (reserved) not currently defined, processed as '3'   *
//*                   if 3, Calculate/recalculate all values of mrp structure.   *
//*                         Use this option if:                                  *
//*                         a) field text has been reformatted                   *
//*                         b) if vertical scrolling has occurred                *
//*                         c) newlines added or removed                         *
//*                           (This is the default: slower, but more reliable.)  *
//*                                                                              *
//* Returns: current state of 'ovr' member (see above)                           *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) 'fptr->ip'                                                                *
//*    The IP value _should_ index a character within the visible space because  *
//*    the logical cursor position is calculated from IP.                        *
//*    However, if IP is outside the visible space, the logical cursor will be   *
//*    adjusted to bring it within the boundaries of the field.                  *
//*    The IP value itself will not be adjusted UNLESS there is a program error: *
//*     a) IP references a character beyond the null terminator                  *
//*     b) IP < ZERO                                                             *
//*                                                                              *
//* 2) 'fptr->shft'                                                              *
//*    This value applies only to fields which are configured for text-editor    *
//*    formatting: fptr->flow == floShift                                        *
//*    'shft' specifies how many characters the display data are shifted toward  *
//*    the left. A zero indicates no left shift. A positive value indicates the  *
//*    shift as the number of characters (NOT number of columns) the data are    *
//*    shifted.                                                                  *
//*    Note that by design, the text may not be shifted left so far as to leave  *
//*    more that one blank space to the right of the displayed text.             *
//*    Only one blank character cell (contains newline or null terminator)       *
//*    is visible on the row containing IP.                                      *
//*    - Although the application could specify an initial value for 'shft'      *
//*      which violates this design criterion, it will be adjusted during field  *
//*      configuration (see init_skFields()).                                    *
//*    - Within the field editing algorithm, the value of 'shft' is not          *
//*      permitted to violate the design criterion.                              *
//*    Therefore, only valid 'shft' values should be seen here.                  *
//*                                                                              *
//* 3) 'fptr->off'                                                               *
//*    Used to calculate where the top row of displayed text begins.             *
//*    Caller is responsible for setting this value to index the first           *
//*    character of the first displayed row; however, if 'shft' > ZERO, then     *
//*    this character will not be in visible space.                              *
//*    Caller will need to adjust 'off' whenever vertical scrolling has occurred *
//*    causing the text to scroll upward or downward.                            *
//*    a) For "flowing" text (fptr->flow==[floChar || floToken || floHyphen),    *
//*       the 'shft' member is always zero; therefore the the first visible      *
//*       character of the field will always be the first character of the       *
//*       first row, i.e. the character referenced by 'off'.                     *
//*       Typically, caller will set the the top row as:                         *
//*         fptr->off == ZERO  if IP is in the top row, or                       *
//*         fptr->off == fptr->mrp.roff[x]  where 'x' is the roff[] index of the *
//*         desired top row.                                                     *
//*    b) If fptr->flow==floShift, the first visible character will be the       *
//*       first character of the specified row PLUS 'shft'.                      *
//*                                                                              *
//********************************************************************************

bool ACWin::parseFieldText ( skField* fptr, short scope )
{
   #define DEBUG_PFT (0)                  // for debugging only

   const wchar_t *wptr = fptr->txt.gstr() ;
   int chi,                   // character index
       chCnt,                 // text characters
       roi = ZERO,            // row index
       ip = fptr->ip,         // insertion point
       colAcc = ZERO ;        // column-width accumulator
   bool curcolSet = false ;   // set when cursor column has been identified
   const short *colwid = fptr->txt.gscols( chCnt ) ;  // array of column widths
   WinPos wp ;             // calculate logical cursor position
   if ( scope < ZERO || scope > 3 ) { scope = 3 ; } // prevent stupidity
   if ( scope == 2 ) { scope = 3 ; }   // '2' is currently unused (see note above)

   fptr->mrp.ni = chCnt - 1 ; // index the null terminator
   fptr->mrp.ovr = false ;    // reset the overrun flag

   //* Do a gross range-check of IP. (Caller should never be so dumb.) *
   if ( fptr->ip < ZERO ) { ip = fptr->ip = ZERO ; }
   else if ( fptr->ip > fptr->mrp.ni ) { ip = fptr->ip = fptr->mrp.ni ; }

   //********************************************
   //* If target field has only one display row *
   //********************************************
   if ( fptr->hgt == 1 )
   {
      if ( scope > ZERO )
      {
         //* If necessary allocate/reallocate the index array. Note that   *
         //* this should never happen for single-row fields because the    *
         //* constructor handles the initialization, but be safe.          *
         if ( (fptr->mrp.roff == NULL) || (fptr->mrp.rCnt != 1) )
         {
            if ( fptr->mrp.roff != NULL ) { delete [] fptr->mrp.roff ; }
            fptr->mrp.roff = new int[fptr->mrp.rCnt = 1] ;
            fptr->mrp.roff[ZERO] = ZERO ;
         }
         //* The only text row is row zero(0). Set the row indices. *
         fptr->mrp.rtop = fptr->mrp.rcur = fptr->mrp.rbot = fptr->mrp.rlst = ZERO ;
         fptr->mrp.bor = ZERO ;     // beginning-of-row index is always zero
      }

      //* End-of-row index == index of null terminator. *
      fptr->mrp.eor = fptr->mrp.ni ;

      //* Set beginning-of-visible-text *
      fptr->mrp.bov = fptr->shft ;

      //* Set end-of-visible-text == end-of-row, but this will*
      //* be adjusted below if text extends beyond visible    *
      //* space, or if row text is not as wide as field.      *
      fptr->mrp.eov = fptr->mrp.eor ;

      //* Calculate the logical cursor position as the 0-based offset *
      //* from field origin. 'col' is calculated as the number of     *
      //* columns between 'shft' and 'ip'. 'eov' indexes the last     *
      //* visible character on the line.                              *
      fptr->cur.row = ZERO ;              // for single-row fields, row is always zero
      colAcc = ZERO ;                     // column-width accumulator
      curcolSet = false ;                 // cursor offset not yet identified

      if ( fptr->ip < fptr->shft )        // if IP is outside left edge of field
      {
         fptr->cur.col = ZERO ;
         fptr->mrp.ovr = true ;
         for ( chi = fptr->shft ; chi <= fptr->mrp.ni ; ++chi )
         {
            if ( ((colAcc += colwid[chi]) >= fptr->wid) || (chi == fptr->mrp.ni))
            {
               if ( colAcc > fptr->wid ) { --chi ; } // if overrun, subtract one
               fptr->mrp.eov = chi ;   // index last visible character
               break ;
            }
         }
      }
      else                                // IP >= fptr->shft
      {
         for ( chi = fptr->shft ; chi <= fptr->mrp.ni ; ++chi )
         {
            //* If IP reached (or end-of-text), set cursor column. *
            if ( (chi == ip) || (!curcolSet && chi == fptr->mrp.ni) )
            {
               if ( colAcc >= fptr->wid )    // if field overrun
               {  //* Set cursor position back inside the field boundaries. *
                  colAcc = fptr->wid - ((colwid[ip] > ZERO) ? colwid[ip] : 1) ;
                  if ( chi == ip ) { fptr->mrp.eov = ip - 1 ; }
                  else             { fptr->mrp.eov = chi - 1 ; }
                  fptr->mrp.ovr = true ;     // note the overrun
               }
               fptr->cur.col = colAcc ;      // save the column offset
               curcolSet = true ;            // cursor position has been set
               if ( fptr->mrp.ovr )          // if overrun, we're done
                  break ;
            }
            if ( colAcc >= fptr->wid )       // if field overrun
            {  //* Set cursor position back inside the field boundaries. *
               colAcc = fptr->wid - ((colwid[chi] > ZERO) ? colwid[chi] : 1) ;
               --chi ;                       // step back by one character
               fptr->mrp.eov = chi ;         // save index of last visible character
               if ( ! curcolSet )            // if 'cur.col' not already set
               {
                  fptr->cur.col = colAcc ;   // save the column offset
                  fptr->mrp.ovr = true ;     // note the overrun
               }
               break ;                       // non-displayed text is ignored
            }
            colAcc += colwid[chi] ;          // increment the index
         }
      }
   }  // (hgt == 1)

   //********************
   //* Multi-row fields *
   //********************
   else
   {
      //* Calculate/recalculate all members of the object.*
      if ( scope == 3 )
      {
         //* Count the number of text rows *
         int rcnt = 1 ;             // init. # of text rows (last row has no newline)
         for ( chi = ZERO ; wptr[chi] != NULLCHAR ; ++chi )
         { if ( wptr[chi] == NEWLINE ) { ++rcnt ; } }

         //* Allocate/reallocate index array *
         if ( (rcnt != fptr->mrp.rCnt) || (fptr->mrp.roff == NULL) )
         {
            if ( fptr->mrp.roff != NULL ) { delete [] fptr->mrp.roff ; }
            fptr->mrp.roff = new int[fptr->mrp.rCnt = rcnt] ;
         }

         //* Scan for character index at top of each row. *
         fptr->mrp.roff[roi++] = ZERO ;         // character index for top physical row

         for ( chi = ZERO ; wptr[chi] != NULLCHAR ; ++chi )
         { if ( wptr[chi] == NEWLINE ) { fptr->mrp.roff[roi++] = chi + 1 ; } }
         //* If no newline terminates last text row, use index of null terminator *
         if ( roi < fptr->mrp.rCnt ) { fptr->mrp.roff[roi] = fptr->mrp.ni ; }

         //* Last physical text row *
         fptr->mrp.rlst = fptr->mrp.rCnt - 1 ;

         //* Calculate top visible row (0-based) *
         for ( roi = fptr->mrp.rlst ; roi >= ZERO  ; --roi )
         {
            if ( fptr->off >= fptr->mrp.roff[roi] )
            { fptr->mrp.rtop = roi ; break ; }
         }

         //* Calculate bottom visible row (0-based). *
         if ( (fptr->mrp.rbot = fptr->mrp.rtop + fptr->hgt - 1) >= fptr->mrp.rCnt )
            fptr->mrp.rbot = fptr->mrp.rCnt - 1 ;

      }  // (scope == 3)

      //* Calculate the current row i.e. the row containing IP (0-based) *
      for ( roi = (fptr->mrp.rCnt - 1) ; roi >= ZERO ; --roi )
      {
         if ( ip >= fptr->mrp.roff[roi] )
         {
            fptr->mrp.rcur = roi ;        // row index for current
            //* Copy of character index for beginning of current row. *
            fptr->mrp.bor = fptr->mrp.roff[fptr->mrp.rcur] ;
            //* Index 1st visible character on the row.          *
            //* For floShift, this is beginning of row + 'shft'. *
            //* (For flowing text, 'shft' is always zero.)       *
            fptr->mrp.bov = fptr->mrp.bor + fptr->shft ;
            break ;
         }
      }

      //* Test whether IP references a character in visible space.*
      //* If character referenced by IP is outside visible space, *
      //* set the 'ovr' (overrun) flag.                           *
      if ( (ip < fptr->mrp.bov) || 
           ((fptr->mrp.rbot < fptr->mrp.rlst) &&
            (ip >= fptr->mrp.roff[fptr->mrp.rbot + 1])) )
      { fptr->mrp.ovr = true ; }

      //* Scan to end of current row *
      fptr->mrp.eor = fptr->mrp.ni ;         // assume end-of-text
      for ( chi = ip ; chi < fptr->mrp.ni ; ++chi )
      { if ( wptr[chi] == NEWLINE ) { fptr->mrp.eor = chi ; break ; } }

      //* Calculate the logical cursor position as       *
      //* the 0-based offset from field origin:          *
      //* -- 'cur.row' == difference between 'rcur' and  *
      //*    top visible row 'rtop'.                     *
      //* -- 'cur.col' == number of columns between      *
      //*    mrp.roff[rcur]+'shft' and 'ip'.             *
      //*    Note that 'shft' > 0 only if flow==floShift.*
      //* -- 'eov' indexes the last visible character on *
      //*    the current row.                            *
      fptr->cur.row = fptr->mrp.rcur - fptr->mrp.rtop ;
      colAcc = ZERO ;
      curcolSet = false ;

      //* Index last visible character of IP row.  *
      fptr->mrp.eov = fptr->mrp.eor ;        // assume end-of-row is in visible space
      for ( chi = fptr->mrp.bov ; chi <= fptr->mrp.eor ; ++chi )
      {
         if ( chi == ip )                    // if IP reached (logical cursor offset)
         {
            if ( colAcc >= fptr->wid )       // if field overrun (IP is outside visible space)
            {  //* Step back inside the field boundaries. *
               while ( colAcc >= fptr->wid )
                  colAcc -= colwid[chi--] ;
               fptr->mrp.eov = ip ;          // IP references the last visible char on the row
               fptr->mrp.ovr = true ;        // note the overrun
            }
            fptr->cur.col = colAcc ;         // save the column offset
            curcolSet = true ;               // cursor position has been set
            if ( fptr->mrp.ovr )             // if overrun, we're done
               break ;
         }

         if ( colAcc >= fptr->wid )          // if field overrun (eor is outside visible space)
         {  //* Step back inside the field boundaries. *
            while ( colAcc >= fptr->wid )
               colAcc -= colwid[--chi] ;
            fptr->mrp.eov = chi ;            // save index of last visible character
            if ( ! curcolSet )               // if 'cur.col' not already set
            {
               fptr->cur.col = colAcc ;      // save the column offset
               fptr->mrp.ovr = true ;        // note the overrun
            }
            break ;                          // we're done
         }
         colAcc += colwid[chi] ;             // add width of indexed character
      }

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_PFT != 0
      if ( (fptr->hgt > 1) && (fptr->flow == floShift) )
      {
         gString gsrow ; int dsi = ZERO, dip ; wchar_t ipchar ;
         for ( short dr = ZERO ; dr < fptr->mrp.rCnt ; ++dr )
         {
            gsdbg.compose( "%02hd @ %03d) ", &dr, &fptr->mrp.roff[dr] ) ;
            gsrow.clear() ;
            while ( (wptr[dsi] != NEWLINE) && (wptr[dsi] != NULLCHAR) )
               gsrow.append( wptr[dsi++] ) ;
            if ( wptr[dsi] == NEWLINE ) ++dsi ;
            if ( dr == fptr->mrp.rcur )
            {
               dip = fptr->ip - fptr->mrp.roff[dr] ;
               ipchar = gsrow.gstr()[dip] ;
               if ( ipchar == NULLCHAR )
                  gsrow.append( wcsFISHEYE ) ;
               else
                  gsrow.replace( ipchar, wcsFISHEYE, dip ) ;
               gsdbg.append( "'%S' [[ip:%d eov:%d eor:%d cur:%hd,%hd]]\n", 
                             gsrow.gstr(), &fptr->ip, &fptr->mrp.eov, &fptr->mrp.eor,
                             &fptr->cur.row, &fptr->cur.col ) ;
            }
            else
               gsdbg.append( "'%S'\n", gsrow.gstr() ) ;
            ofsdbg << gsdbg.ustr() ; ofsdbg.flush() ;
         }
         ofsdbg << "    -----" << endl ;
      }
      #endif   // DEBUG_PFT
   }  // (hgt > 1)

   return ( fptr->mrp.ovr ) ;

   #undef DEBUG_PFT
}  //* End parseFieldText() *

//*************************
//*      ClearField       *
//*************************
//********************************************************************************
//* Erase the displayed text in the specified text field.                        *
//* Optionally, the stored text is also deleted.                                 *
//*                                                                              *
//* This operation applies ONLY to fields with editable text, specifically,      *
//* Textbox fields (field type: ftTB).                                           *
//*                                                                              *
//* Input  : fIndx   : index of target field                                     *
//*          delText : (optional, 'false' by default)                            *
//*                    if 'true',  delete the stored text of the field           *
//*                    if 'false', stored text is not modified                   *
//*          txtAttr : (optional, acaUSEDFLT by default)                         *
//*                    color attributes for the clear operation                  *
//*                    (logical OR of forground and background color)            *
//*                    (attributes and attribute modifiers          )            *
//*                    -- If not specified, then current field attributes        *
//*                       will be used.                                          *
//*                    if 'delText' is set: field attributes will be updated     *
//*                    if 'delText' is reset: specified attributes will be used  *
//*                                 for the clear operation, but won't be saved  *
//*                                                                              *
//* Returns: 'true'  if target field cleared                                     *
//*          'false' if fIndx out-of-range or if field type of target is not     *
//*                  supported by this operation (data not modified)             *
//********************************************************************************

bool ACWin::ClearField ( short fIndx, bool delText, acAttr txtAttr )
{
   bool status = false ;            // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) ) 
   {
      skField *fptr = &this->skf->fld[fIndx] ; // point to the target field

      if ( fptr->type == ftTB )
      {
         //* If specified, delete the stored text data. *
         if ( delText )
         {
            //* Clear the stored text (both standard and extended text). *
            fptr->txt.clear() ;

            //* Reinitialize field parsing data. (sets IP and cursor to origin) *
            fptr->ip = fptr->off = fptr->shft = ZERO ;
            this->parseFieldText ( fptr ) ;        // (clear field, text deleted)

            if ( txtAttr != acaUSEDFLT )           // if specified, set new attributes
               fptr->aca.decode( txtAttr ) ;

            //* Refresh the target field display, and if it's not  *
            //* the field with focus, refresh the focus field also.*
            this->RefreshField ( fIndx ) ;
            if ( fIndx != this->skf->fi )
               this->RefreshField ( this->skf->fi ) ;
         }

         //* Erase the displayed text, but do not delete the source text. *
         //* (IP and cursor position are not modified.)                   *
         else
         {
            //* Set the color attributes to use for the clear.*
            //* If specified, calculate alternate attributes. *
            acAttr clearColor = this->skf->fld[fIndx].aca.acaVal ; // current attributes
            if ( txtAttr != acaUSEDFLT )
            { acaExpand aca ; aca.decode( txtAttr ) ; clearColor = aca.acaVal ; }
            this->acSetAttributes ( clearColor, true ) ;

            this->clearField ( this->skf, fIndx ) ;  // erase displayed text

            //* If target field is not the field with focus, *
            //* redraw the field with focus.                 *
            if ( fIndx != this->skf->fi )
               this->RefreshField ( this->skf->fi ) ; // update the display
         }
         this->restoreAttributes () ;           // restore terminal attributes

         status = true ;                        // success
      }
   }
   return status ;

}  //* End ClearField() *

//*************************
//*      SetFieldIP       *
//*************************
//********************************************************************************
//* Set the insertion point (IP) for the field. This will set the cursor         *
//* position over the character at the specified offset.                         *
//*                                                                              *
//* Field types supported by this method are:                                    *
//*  1) ftTB (Textbox) 2) ftSB (Scrollbox).                                      *
//* Insertion point for other field types is managed programatically.            *
//*                                                                              *
//* If 'fIndx' references the field which has input focus, the display will be   *
//* updated.                                                                     *
//*                                                                              *
//* Special Case: For Scrollbox controls, IP is normalized to first character    *
//*               of target row.                                                 *
//*                                                                              *
//* Scrolling to ensure that new IP position is within visible space:            *
//* 1) If target IP is already within visible space, no vertical scrolling       *
//*    will occur unless the 'centered' flag is set.                             *
//* 2) If target IP is above visible space, the row containing the cursor        *
//*    will be positioned at the top row of the display.                         *
//* 3) If target IP is below visible space, the row containing the cursor        *
//*    will be positioned at the bottom row of the display.                      *
//* 4) Optionally, the position of row containing the target IP may be adjusted  *
//*    to vertically center it within the target window.                         *
//*                                                                              *
//* Input  : fIndx   : index of target field                                     *
//*          newIp   : character offset into the text of the field               *
//*                    ZERO <= newIp < text characters                           *
//*                    where ZERO == field origin and any value at or beyond     *
//*                    the number of characters in the array signals that IP     *
//*                    should be set to end-of-text (referencing null terminator)*
//*          centered: (optional, default:false) center the row and column of    *
//*                    the IP within the visible area of the field.              *
//*          refresh : (optional, default:true) refresh display                  *
//*                    if 'true'  make changes visible                           *
//*                    if 'false' update the display _only_ if target field      *
//*                               has the input focus.                           *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if invalid field index specified                            *
//********************************************************************************
//* Notes:                                                                       *
//* -- If newIp == current IP, then the calculation could be performed anyway,   *
//*    which might, or might not cause scrolling.                                *
//* -- Design Question: Not yet sure whether 'centered' should apply only to     *
//*    vertical shift, or to both vertical and horizontal shift.                 *
//*                                                                              *
//********************************************************************************

bool ACWin::SetFieldIP ( short fIndx, int newIp, bool centered, bool refresh )
{
   bool status = false ;               // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( ((fptr->type == ftTB) || (fptr->type == ftSB)) && (newIp != fptr->ip) )
      {
         //* Range limit test */
         int maxIp = fptr->txt.gschars() - 1 ;
         if ( newIp < ZERO )        newIp = ZERO ;
         else if ( newIp > maxIp )  newIp = maxIp ;
         fptr->ip = newIp ;

         bool horiz = false ;             // horizontal centering
         bool verti = false ;             // vertical centering
         if ( fptr->hgt >= 3 )            // pre-tests
            verti = centered ;
         if ( (fptr->flow == floShift) && (fptr->type != ftSB) )
            horiz = centered ;
         this->centerFieldIP ( fptr, horiz, verti ) ;

         //* If target field currently has the input focus, *
         //* refresh the display.                           *
         if ( refresh || (fIndx == this->skf->fi) )
            this->refreshField ( this->skf, fIndx ) ;

         status = true ;
      }
   }
   return status ;

}  //* End SetFieldIP() *

//*************************
//*     centerFieldIP     *
//*************************
//********************************************************************************
//* Protected Method:                                                            *
//* -----------------                                                            *
//* Adjust Textbox (or Scrollbox) field text positioning to bring the field IP   *
//* within visible range. This method updates the positioning data, but          *
//* _does not_ update the display.                                               *
//*                                                                              *
//* A) The row containing IP, if not already within visible space, is shifted    *
//*    vertically to bring it within visible space (top or bottom visible row).  *
//*    a) Then, if vertical centering specified ('verti' parameter), shift the   *
//*       IP row toward the vertical center of the target field.                 *
//*       (without leaving empty rows above or below).                           *
//* B) If the character referenced by IP is not already in visible space, the    *
//*    text data are shifted horizontally to bring IP within visible space       *
//*    (leftmost or rightmost visible character).                                *
//*    a) If horizontal centering is specified ('horiz' parameter), the text     *
//*       data are shifted to move the character referenced by IP toward the     *
//*       horizontal center of the field.                                        *
//*                                                                              *
//* Notes:                                                                       *
//* ------                                                                       *
//* Note that these are complex calculations which rely on accurate MRParse data.*
//* 1) Textbox fields configured for "flowing" text require only vertical        *
//*    positioning because the text is formatted to lie fully within the         *
//*    left and right margins of the field.                                      *
//*    The vertical shift is done in two steps:                                  *
//*    First, if the target row is not currently visible:                        *
//*     a) If the text row containing the character referenced by IP is above    *
//*        visible space, the text data are scrolled downward to place the target*
//*        row at the top row of visible space.                                  *
//*     b) If the text row containing the character referenced by IP is below    *
//*        visible space, the text data are scrolled upward to place the target  *
//*        row at the bottom row of visible space.                               *
//*     c) If the text row containing the IP character is already visible, then  *
//*        this step is unnecessary.                                             *
//*    Next, if centering is specified, the row containing the character         *
//*    referenced by IP is scrolled toward the vertical center of the visible    *
//*    space. Note that if there is an EVEN number of rows in visible space,     *
//*    then the target row will be placed as near the center as possible.        *
//*    -- Note that if there are less than three(3) visible rows, then vertical  *
//*       centering does not apply                                               *
//* 2) Textbox fields configured for 'floShift', may require both vertical and   *
//*    horizontal positioning to bring IP into visible space.                    *
//*    The horizontal shift is done in two steps:                                *
//*    a) First, if the character referenced by IP is not already in visible     *
//*       space, the 'shft' value is adjusted to bring the IP character into     *
//*       view positioned at the edge of visible space nearest to the original   *
//*       position. If the IP character is already visible, then this step       *
//*       is unnecessary.                                                        *
//*    b) If centering is specified, the character referenced by IP is shifted   *
//*       toward the center of the visible space by counting the number of       *
//*       columns (not characters) between the current position an the visual    *
//*       center. While this is a relatively slow calculation, it is quite       *
//*       useful in certain circumstances. If centering is not specified         *
//*       (horiz==false), then this step is skipped.                             *
//* 3) Scrollbox data are only scrolled vertically _not_ horizontally.           *
//*    a) Note also that IP for Scrollbox always references the first character  *
//*       of the target row.                                                     *
//*                                                                              *
//* Input  : fptr    : pointer to Textbox field                                  *
//*          horiz   : (optional, default: true)                                 *
//*                    if 'true',  IP is shifted horizontally as needed to ensure*
//*                                that it is within visible space _and_ places  *
//*                                it at the (approximate) center of the field   *
//*                    if 'false', IP character is brought into visible space,   *
//*                                but no horizontal centering occurs.           *
//*          verti   : (optional, default: false)                                *
//*                    if 'true',  row containing IP is shifted vertically as    *
//*                                needed to bring it into visible space; THEN   *
//*                                IP row is shifted to the (approximate)        *
//*                                vertical center of the field.                 *
//*                    if 'false', IP row is brought into visible space if       *
//*                                necessary, but no vertical centering occurs.  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::centerFieldIP ( skField* fptr, bool horiz, bool verti )
{
   //* Re-parse to ensure valid data *
   this->parseFieldText ( fptr ) ;
   int old_offset = fptr->off,            // initial vert/horiz offsets
       old_shift  = fptr->shft ;

   //* Horizontal centering does not apply to Scrollbox controls  *
   //* or to Textbox controls configured for "flowing" text data. *
   if ( (fptr->flow != floShift) || (fptr->type == ftSB) ) { horiz = false ; }
   //* Vertical centering does not apply to single-row (or 2-row) fields. *
   if ( fptr->hgt < 3) { verti = false ; }

   //* ------------------------ *
   //* Calculate vertical shift *
   //* ------------------------ *
   //* If target row is _above_ visible space *
   if ( fptr->ip < fptr->mrp.roff[fptr->mrp.rtop] )
   {
      for ( short r = fptr->mrp.rtop ; r >= ZERO ; --r )
      {
         if ( fptr->ip >= fptr->mrp.roff[r] )   // if target row found
         {
            fptr->off = fptr->mrp.roff[r] ;     // set top visible row

            //* For Scrollbox controls, normalize IP to head of target row.*
            if ( fptr->type == ftSB ) { fptr->ip = fptr->off ; }
            break ;
         }
      }
   }
   //* If target row is _below_ visible space *
   else if ( (fptr->mrp.rbot < fptr->mrp.rlst) && 
             (fptr->ip >= fptr->mrp.roff[fptr->mrp.rbot + 1]) )
   {
      //* Maximum row offset - prevents empty space below text *
      short rti = fptr->mrp.rtop,
            rbi = fptr->mrp.rbot ;
      while ( rbi <= fptr->mrp.rlst )
      {
         if ( (rbi == fptr->mrp.rlst) || (fptr->ip < fptr->mrp.roff[rbi+1]) )
         {
            fptr->off = fptr->mrp.roff[rti] ; // set top visible row

            //* For Scrollbox controls, normalize IP to head of target row.*
            if ( fptr->type == ftSB ) { fptr->ip = fptr->mrp.roff[rbi] ; }
            break ;
         }
         ++rti ; ++rbi ;
      }
   }

   //* Update parsing data *
   if ( fptr->off != old_offset )
      this->parseFieldText ( fptr ) ;

   //*-------------------------------- *
   //* If vertical centering specified *
   //*-------------------------------- *
   if ( verti )
   {
      int rcent = fptr->mrp.rtop + ((fptr->mrp.rbot - fptr->mrp.rtop) / 2) ; // center row
      int rtrg ;                          // target row
      int rmax ;                          // limit row

      if ( fptr->mrp.rcur > rcent )
      {
         rtrg = fptr->mrp.rtop - (rcent - fptr->mrp.rcur ) ;
         rmax = fptr->mrp.rlst - fptr->hgt + 1 ;
         if ( rtrg > rmax ) { rtrg = rmax ; }
         fptr->off = fptr->mrp.roff[rtrg] ;
      }
      else if ( fptr->mrp.rcur < rcent )
      {
         rtrg = fptr->mrp.rtop + (fptr->mrp.rcur - rcent) ;
         if ( rtrg < ZERO ) { rtrg = ZERO ; }
         fptr->off = fptr->mrp.roff[rtrg] ;
      }

      //* Update parsing data *
      if ( fptr->off != old_offset )
         this->parseFieldText ( fptr ) ;
   }

   //* -------------------------- *
   //* Calculate horizontal shift *
   //* -------------------------- *
   //* If IP is outside the left or right margin, adjust 'shft' *
   //* as necessary to bring it to the first visible position   *
   //* in that direction. Then, if 'horiz' flag is set continue *
   //* the horizontal shift to center IP in field.              *
   gString gsSeg ;
   const short* segWid = NULL ;
   int row_head = fptr->mrp.bor, 
       vis_shft, max_shft, cen_shft,
       trg_cols = fptr->wid / 2,
       txt_cols = ZERO, segChars ;
   if ( fptr->ip > fptr->mrp.eov )  // IP is outside right edge
   {
      vis_shft = fptr->ip - fptr->mrp.eov ;
      max_shft = fptr->mrp.eor - (fptr->mrp.eov + vis_shft) ;
      fptr->shft += vis_shft ;            // moves IP to right edge of visible space
      if ( horiz )                        // if horizontal centering specified
      {
         gsSeg = &fptr->txt.gstr()[fptr->ip] ;
         segWid = gsSeg.gscols( segChars ) ;
         for ( cen_shft = ZERO ; (cen_shft < max_shft) && (cen_shft < (segChars - 1)) ; ++cen_shft )
         { if ( (txt_cols += segWid[cen_shft]) >= trg_cols ) { break ; } }
         fptr->shft += ((cen_shft < max_shft) ? cen_shft : max_shft) ;
      }
   }
   else if ( fptr->ip < fptr->mrp.bov )   // IP is outside left edge
   {
      max_shft = row_head + fptr->shft ;
      vis_shft = max_shft - fptr->ip ;
      fptr->shft -= vis_shft ;            // moves IP to left edge of visible space
      if ( horiz )                        // if horizontal centering specified
      {
         segWid = fptr->txt.gscols( segChars ) ;
         cen_shft = ZERO ;
         for ( int ci = fptr->ip ; ci >= ZERO ; --ci )
         {
            if ( (txt_cols += segWid[ci]) < trg_cols ) { ++cen_shft ; }
            else  { break ; }
         }
         if ( (fptr->shft -= (cen_shft < max_shft ? cen_shft : max_shft)) < ZERO )
            fptr->shft = ZERO ;  // (this _should_ never happen)
      }
   }
   else if ( horiz ) // IP is within visible space, center if specified
   {
      segWid = fptr->txt.gscols( segChars ) ;
      int cbef = ZERO,  // columns before IP
          caft = ZERO,  // columns after IP
          chi ;         // character index
      for ( chi = fptr->mrp.bov ; chi < fptr->ip ; ++chi )
         cbef += segWid[chi] ;
      for ( ++chi ; chi <= fptr->mrp.eov ; ++chi )
         caft += segWid[chi] ;
      if ( cbef != caft )                 // if not already centered
      {
         cen_shft = ZERO ;
         if ( cbef < caft )               // shift data to the right
         {
            max_shft = fptr->shft ;
            for ( chi = fptr->mrp.eov ; chi > fptr->ip ; --chi )
               txt_cols += segWid[chi] ;
            while ( txt_cols > trg_cols )
            { txt_cols -= segWid[chi++] ; ++cen_shft ; }

            fptr->shft -= (cen_shft < max_shft ? cen_shft : max_shft) ;
         }
         else                             // shift data to the left
         {
            max_shft = fptr->mrp.eor - fptr->mrp.eov ;
            cen_shft = ZERO ;
            for ( chi = fptr->mrp.bov ; chi < fptr->ip ; ++chi )
               txt_cols += segWid[chi] ;
            while ( txt_cols > trg_cols )
            { txt_cols -= segWid[chi--] ; ++cen_shft ; }
            fptr->shft += (cen_shft < max_shft ? cen_shft : max_shft) ;
         }
      }
   }

   //* Update parsing data *
   if ( fptr->shft != old_shift )
      this->parseFieldText ( fptr ) ;

}  //* End centerFieldIP() *

//*************************
//*      GetFieldIP       *
//*************************
//********************************************************************************
//* Get the current insertion point (IP) for the specified field.                *
//*                                                                              *
//* Note that although this method returns the IP value for all control types,   *
//* this value will be meaningful _only_ for ftTB Textbox fields (and possibly   *
//* ftSB Scrollbox fields).                                                      *
//* For other types of controls IP and cursor position are not meaningful.       *
//*                                                                              *
//* Input  : fIndx   : index of target field                                     *
//*                                                                              *
//* Returns: current character offset into the text of the field                 *
//*          (if invalid 'fIndx', zero will be returned)                         *
//********************************************************************************

int ACWin::GetFieldIP ( short fIndx )
{
   short fldIp = ZERO ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      fldIp = this->skf->fld[fIndx].ip ;
   }
   return fldIp ;

}  //* End GetFieldIP() *

//*************************
//*    ScrollFieldText    *
//*************************
//********************************************************************************
//* Scroll upward or downward through multi-row fields (ftTB, ftSB).             *
//*                                                                              *
//* This method applies only to multi-row text fields (ftTB and ftSB)            *
//* and is ignored for single-row fields and other field types.                  *
//*                                                                              *
//* Examples: (note how the sign of the shift determines direction of scroll)    *
//*    steps ==   1   scroll one row toward bottom of text                       *
//*    steps ==  -1   scroll one row toward top text                             *
//*    steps ==  hgt  scroll one page toward bottom of text                      *
//*    steps == -hgt  scroll one page toward top of text                         *
//*    steps ==   0   scroll to the first row of text (special case)             *
//*    steps ==  999  scroll to the last row of text. Note that any value        *
//*                   at or beyond the last text row will signal a move          *
//*                   to end-of-text.                                            *
//*                                                                              *
//* Input  : fIndx   : index of target field                                     *
//*          steps   : number of rows to scroll                                  *
//*                                                                              *
//* Returns: 'true'  if successful scroll                                        *
//*          'false' if already at limit in specified direction                  *
//*                  (or index out-of-range)                                     *
//********************************************************************************
//* Notes:                                                                       *
//* -- 'off' determines the top row of text. It references the first character   *
//*    of the topmost _visible_ text row.                                        *
//* -- This is somewhat different from the user repeatedly pressing the up-arrow *
//*    or down-arrow keys, in that the cursor does not move directly down or     *
//*    up, but rather to the beginning of the target row.                        *
//* -- Note that for text fields that support horizontal shift, the 'shft'       *
//*    member will be set to zero i.e. no horizontal shift.                      *
//* -- This is a public method and may be called independently, but it is also   *
//*    called in response to receipt of the UpArrow, DownArrow, PageUp,          *
//*    PageDown, Home and End keys within multi-row fields.                      *
//********************************************************************************

bool ACWin::ScrollFieldText ( short fIndx, short steps )
{
   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
      status = scrollFieldText ( this->skf, fIndx, steps ) ;
   return ( status ) ;

}  //* End ScrollFieldText() *

//*************************
//*    scrollFieldText    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Scroll upward or downward through multi-row fields ftTB, ftSB, and the       *
//* special case of context menus (defined as a multi-row menu which is not part *
//* of a menubar group).                                                         *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          steps : number of rows to scroll                                    *
//*                                                                              *
//* Returns: 'true'  if successful scroll                                        *
//*          'false' if already at limit in specified direction                  *
//********************************************************************************

bool ACWin::scrollFieldText ( skForm* skfp, short fIndx, short steps )
{
   skField* fptr = &skfp->fld[fIndx] ;    // pointer to target field
   FldType  ftype = fptr->type ;          // field type
   bool status = false ;                  // return value

   //* Validate the type of target control.          *
   //* Note the early return if not a supported type.*
   if ( !((fptr->hgt > 1) && ((ftype == ftTB) || (ftype == ftSB) || 
          ((ftype == ftMB) && (fptr->grp == noGROUP)))) )
      return status ;

   //* Update the 'shft' (horizontal shift) value.       *
   //* Horizontal shift applies only to Textbox controls.*
   //* Cursor position will move, so set the status flag.*
   if ( (ftype == ftTB) && (fptr->shft > ZERO) )
   { fptr->shft = ZERO ; status = true ; }

   //* For Textbox and Scrollbox controls, determine *
   //* whether IP is already on the top/bottom row.  *
   if ( (ftype == ftTB) || (ftype == ftSB) )
   {
      int  oldIp = fptr->ip ;             // current IP value
      bool rowLimit = false ;
      this->parseFieldText ( fptr ) ;     // refresh field indexing (just in case)

      if ( ((steps < ZERO) && (fptr->mrp.rcur == ZERO)) ||
           ((steps > ZERO) && (fptr->mrp.rcur == fptr->mrp.rlst)) )
      { rowLimit = true ; }

      //* For Textbox only, Cursor is already on the top *
      //* or bottom row, but may not be at head of row.  *
      //* Verify that IP is at leftmost position of row. *
      if ( rowLimit && (ftype == ftTB) )
      {
         int trgRow = steps < ZERO ? ZERO : fptr->mrp.rlst ;
         fptr->ip = fptr->mrp.roff[trgRow] ; // reference leftmost char of target row
         this->parseFieldText ( fptr, 1 ) ;  // update parsing reflecting new cursor position
         if ( fptr->ip != oldIp )            // if IP updated, refresh display
         { this->refreshField ( skfp, fIndx ) ; status = true ; }
         return ( status ) ;                 // note the early return
      }

      //* Range check the step count. *
      if ( (steps < ZERO) && ((fptr->mrp.rcur + steps) < ZERO) )
         steps = -(fptr->mrp.rcur) ;
      else if ( (steps > ZERO) && ((fptr->mrp.rcur + steps) > fptr->mrp.rlst) )
         steps = fptr->mrp.rlst - fptr->mrp.rcur ;
      int trgRow = fptr->mrp.rcur + steps ;

      //* Bring target row into visible space, placing   *
      //* placing IP on first character of target row.   *
      fptr->ip = fptr->mrp.roff[trgRow] ;
      this->centerFieldIP ( fptr, false, false ) ;
      this->refreshField ( skfp, fIndx ) ;   // update the display
      if ( fptr->ip != oldIp )
         status = true ;                     // report success
   }

   //* For Context menus determine whether IP is already on *
   //* the top/bottom item. (excludes top/bottom borders)   *
   else
   {
      int availSteps ;                    // max steps available in target direction
      if ( steps > ZERO )                 // move downward
      {
         availSteps = (fptr->mrp.rlst - 1) - fptr->mrp.rcur ;
         if ( steps > availSteps )
            steps = availSteps ;
         fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + steps] ;
      }
      else if ( steps < ZERO )            // move upward
      {
         availSteps = -(fptr->mrp.rcur - 1) ;
         if ( steps < availSteps )
            steps = availSteps ;
         fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + steps] ;
      }
      if ( steps != ZERO )                // scroll to the target position
      {
         this->parseFieldText ( fptr, 1 ) ;  // update field stats
         this->refreshCtrl ( skfp, fIndx, false ) ; // refresh the control
         status = true ;
      }
   }

   return ( status ) ;

}  //* End scrollFieldText() *

//*************************
//*    ShiftFieldText     *
//*************************
//********************************************************************************
//* Perform a vertical block shift of Textbox data, either upward or downward.   *
//* This method applies ONLY to multi-row Textbox controls.                      *
//*                                                                              *
//* This is similar to the ScrollFieldText() method which scrolls the cursor     *
//* through the rows of text data, EXCEPT that the cursor position continues to  *
//* reference the same character UNLESS the row containing the cursor is shifted *
//* out of visible space.                                                        *
//*                                                                              *
//* Input  : fIndx : index of target field                                       *
//*          steps : direction of shift                                          *
//*                  positive value: shift text upward by one row                *
//*                     (toward bottom of text)                                  *
//*                  negative value: shift text downward by one row              *
//*                     (toward top of text)                                     *
//*                                                                              *
//* Returns: 'true'  if successful shift and/or cursor movement                  *
//*          'false' if already at limit in specified direction                  *
//*                  or target control does not support text shift               *
//*                  or parameterout-of-range                                    *
//********************************************************************************

bool ACWin::ShiftFieldText ( short fIndx, short steps )
{

   return ( (this->shiftFieldText ( this->skf, fIndx, steps )) ) ;

}  //* End ShiftFieldText()

//*************************
//*    shiftFieldText     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Perform a vertical block shift of Textbox data, either upward or downward.   *
//* This method applies ONLY to multi-row Textbox controls.                      *
//*                                                                              *
//* This is similar to the ScrollFieldText() method which scrolls the cursor     *
//* through the rows of text data, EXCEPT that the cursor position continues to  *
//* reference the same character UNLESS the row containing the cursor is shifted *
//* out of visible space.                                                        *
//*                                                                              *
//* Input  : fIndx : index of target field                                       *
//*          steps : direction of shift                                          *
//*                  positive value: shift text upward by one row                *
//*                     (toward bottom of text)                                  *
//*                  negative value: shift text downward by one row              *
//*                     (toward top of text)                                     *
//*                                                                              *
//* Returns: 'true'  if successful shift and/or cursor movement                  *
//*          'false' if already at limit in specified direction                  *
//*                  or target control does not support text shift               *
//*                  or parameterout-of-range                                    *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 'steps' is currently limited to a single-row shift: -1 <= steps <= 1         *
//* It is possible to implement a multi-row shift; however, the usefulness of a  *
//* multi-row shift is not currently apparent.                                   *
//*                                                                              *
//********************************************************************************

bool ACWin::shiftFieldText ( skForm* skfp, short fIndx, short steps )
{
   #define DEBUG_SHIFT_TEXT (0)           // for debugging only

   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < skfp->fCnt) && (steps != ZERO) &&
        (skfp->fld[fIndx].type == ftTB) && (skfp->fld[fIndx].hgt > 1) )
   {
      skField* fptr = &skfp->fld[fIndx] ; // pointer to target field
      gString gs ;                        // text analysis
      int trgRTop = fptr->mrp.rtop,       // index of target top visible row
          trgRCur = fptr->mrp.rcur,       // index of target cursor row
          hOff = ZERO, nli ;              // calculate horizontal offset
      bool shift  = false ;
      bool cursor = false ;

       //* Implementation currently limited to single step. (see note) *
      if ( steps < ZERO )      { steps = -1 ; }
      else if ( steps > ZERO ) { steps = 1 ; }

      //***********************
      //* Shift text upward.  *
      //***********************
      if ( steps > ZERO )
      {
         if ( fptr->mrp.rbot < fptr->mrp.rlst ) // last row is below visible space
         {
            trgRTop = fptr->mrp.rtop + 1 ;      // shift data upward
            shift = true ;
            if ( fptr->mrp.rcur == fptr->mrp.rtop ) // cursor is on top visible row
            { ++trgRCur ; cursor = true ; }     // move cursor downward
         }
         else if ( fptr->mrp.rcur < fptr->mrp.rlst ) // last row is visible
         { ++trgRCur ; cursor = true ; }        // move cursor downward
         else                                   // cursor on last row
            ;
      }

      //***********************
      //* Shift text downward *
      //***********************
      else
      {
         if ( fptr->mrp.rtop > ZERO )
         {
            trgRTop = fptr->mrp.rtop - 1 ;      // shift data downward
            shift = true ;
            if ( fptr->mrp.rcur == fptr->mrp.rbot )
            { --trgRCur ; cursor = true ; }
         }
         else if ( fptr->mrp.rcur > ZERO )      // first row is visible
         { --trgRCur ; cursor = true ; }
         else                                   // cursor on first row
            ;
      }

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_SHIFT_TEXT != 0
      bool dbVerbose = false ;                  // verbose debugging report
      if ( ofsdbg.is_open() )
      {
         #if DEBUG_MDUMP != 0
         if ( dbVerbose ) { this->dumpMrp2Log ( fptr, L"ShiftFieldText(A)", true ) ; }
         #endif   // DEBUG_MDUMP
         gsdbg.compose( "ShiftFieldText( steps:%2hd rtop:%d->%d rcur:%d->%d )", 
                        &steps, &fptr->mrp.rtop, &trgRTop, &fptr->mrp.rcur, &trgRCur ) ;
         ofsdbg << gsdbg << endl ;
      }
      #endif   // DEBUG_ACE && DEBUG_LOG && DEBUG_SHIFT_TEXT

      //* If text is to be shifted OR if cursor will move *
      if ( shift || cursor )
      {
         //* If cursor will move to a new row,  *
         //* calculate horizontal offset and    *
         //* update the IP.                     *
         if ( cursor )
         {
            hOff = fptr->ip - fptr->mrp.bor ;
            gs = &fptr->txt.gstr()[fptr->mrp.roff[trgRCur]] ;
            if ( (nli = gs.find( NEWLINE )) < ZERO )
               nli = gs.gschars() - 1 ; // reference end-of-text (null terminator)
            //* If target row is shorter than current horizontal offset *
            if ( nli < hOff )
            {
               hOff = nli ;            // set cursor at end of target row
               fptr->shft = ZERO ;     // reset horizontal shift
            }
            fptr->ip = fptr->mrp.roff[trgRCur] + hOff ;
         }

         if ( shift )
         {
            fptr->off = fptr->mrp.roff[trgRTop] ;
         }

         this->parseFieldText ( fptr ) ;
         this->refreshField ( skfp, fIndx ) ;
         status = true ;

         #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_SHIFT_TEXT != 0
         #if DEBUG_MDUMP != 0
         if ( dbVerbose && (ofsdbg.is_open()) )
            this->dumpMrp2Log ( fptr, L"ShiftFieldText(B)", true ) ;
         #endif   // DEBUG_MDUMP
         #endif   // DEBUG_ACE && DEBUG_LOG && DEBUG_SHIFT_TEXT
      }
   }

   //* Warn if:                                          *
   //* a) already at limit in specified direction,       *
   //* b) operation not supported for target field type  *
   //* c) parameter value out-of-range                   *
   if ( ! status && (this->skf->beep) )
      this->acBeep () ;

   return status ;

   #undef DEBUG_SHIFT_TEXT
}  //* End shiftFieldText()

//*************************
//*    hshiftFieldText    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Perform HORIZONTAL shift for Textbox fields configured for 'floShift'.       *
//* For Textbox controls only, and only if set for 'floShift'.                   *
//*                                                                              *
//* Important Note: The 'steps' parameter is the number of characters to shift,  *
//*                 NOT the number of columns. If the field contains multi-column*
//*                 characters, then caller should calculate the character       *
//*                 shift needed to move by the desired number of columns.       *
//*                 See the gString gscols(int) method which can perform this    *
//*                 calculation.                                                 *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          steps : number of characters to shift, positive values move toward  *
//*                  tail of text, negative values move toward head of text      *
//*                                                                              *
//* Returns: 'true'  if successful shift                                         *
//*          'false' if already at limit in specified direction                  *
//********************************************************************************

bool ACWin::hshiftFieldText ( skForm* skfp, short fIndx, short steps )
{
   skField* fptr = &skfp->fld[fIndx] ;    // pointer to target field
   bool status = false ;                  // return value

   if ( (fptr->type == ftTB) && (fptr->flow == floShift) )
   {
      fptr->ip += steps;                  // apply the offset
      int ni = fptr->txt.gschars() - 1 ;  // index of null terminator

      //* Range check IP and reset horizontal shift. *
      if ( fptr->ip < ZERO )  { fptr->ip = ZERO ; }
      if ( fptr->ip > ni )    { fptr->ip = ni ; }
      fptr->shft =  ZERO ;                // reset horizontal shift
      this->parseFieldText ( fptr ) ;     // re-parse the text (just in case)

      //* 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 -= fptr->mrp.bor - fptr->ip ;

         this->parseFieldText ( fptr, 1 ) ; // 'shft' value updated, so refresh parsing data
      }
      this->refreshField ( skfp, fIndx ) ;// update the display
      status = true ;                     // declare success
   }
   return ( status ) ;

}  //* End hshiftFieldText()

//*************************
//*   HilightFieldText    *
//*************************
//********************************************************************************
//* For Textbox controls only (ftTB), highlight a segment of text                *
//* using one of the text-modifier attributes.                                   *
//* If the target field is currently visible, it will be refreshed before return.*
//*                                                                              *
//*                                                                              *
//* Input  : fIndx : index of target field                                       *
//*          set   : if set, highlighted the text segment                        *
//*                  if reset, clear highlight                                   *
//*          begin : index of first (leftmost) character to highlight            *
//*                  (ignored if !set)                                           *
//*          len   : number of characters in highlighted segment                 *
//*                  Note: If segment spans rows, be sure to include             *
//*                        the newlines in the count.                            *
//*                  (ignored if !set)                                           *
//*          txtmod: (optional, default:acaUSEDFLT)                              *
//*                  text modifier attribute for highlighted text segment        *
//*                  - If specified, text modifier attribute for highlighted     *
//*                    text segment. One of the following:                       *
//*                     acaBOLD      bold-text modifier                          *
//*                     acaITALIC    italic-text modifier                        *
//*                     acaUNDERLINE underline-text modifier                     *
//*                     acaOVERLINE  overline-text modifier                      *
//*                     acaXOUT      x-out (strikethrough) text modifier         *
//*                     acaBLINK     blinking-text modifier                      *
//*                     acaCONCEAL   concealed (invisible) text                  *
//*                     acaREVERSE   reversed fgnd/bkgnd (default)               *
//*                     Note: OR'd combinations of these modifiers not allowed.  *
//*                  - If not specified, then use previously-specified modifier  *
//*                    (if any), else acaREVERSE (fg/bg attributes reversed)     *
//*          center: (optional, default: true)                                   *
//*                  if set, assure that highlight is within visible space by    *
//*                          adjusting the IP if necessary.                      *
//*                  if reset, do not adjust IP (highlighted text may or may not *
//*                          be visible).                                        *
//*                                                                              *
//* Returns: 'true'  if highlight set/cleared                                    *
//*          'false' a) 'fIndx' references an upsupported control type           *
//*                  b) parameter out-of-range                                   *
//********************************************************************************

bool ACWin::HilightFieldText ( short fIndx, bool set, int begin, int len, 
                               acAttr txtmod, bool center )
{
   bool status = false ;
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
      status = this->hilightFieldText ( this->skf, fIndx, set, begin, len, 
                                        txtmod, center ) ;

   return status ;

}  //* End HilightFieldText() *

//*************************
//*   hilightFieldText    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* For Textbox (ftTB) and Scrollbox(ftSB) controls only, highlight a segment    *
//* of text using one of the text-modifier values.                               *
//*                                                                              *
//* The text-selection modifier is acaUNDERLINE, so this modifier is not         *
//* recommended as a highlight modifier because it would tend to confuse the     *
//* poor user.                                                                   *
//*                                                                              *
//* If the target field is not set as hidden, it will be refreshed before return.*
//*                                                                              *
//* Called for both ACWin and ACDlg controls.                                    *
//*                                                                              *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object                                    *
//*          fIndx : index of target field                                       *
//*          set   : if set, highlight the text segment                          *
//*                  if reset, clear highlight                                   *
//*          begin : index of first (leftmost) character to highlight            *
//*                  (ignored if !set)                                           *
//*          len   : number of characters in highlighted segment                 *
//*                  Note: If segment spans rows, be sure to include             *
//*                        the newlines in the count.                            *
//*                  (ignored if !set)                                           *
//*          txtmod: (optional, default:acaREVERSE fg/bg reversed)               *
//*                  if specified, the text modifier attribute for the           *
//*                  highlighted text segment. One of the following text         *
//*                  modifiers:                                                  *
//*                   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. *
//*          center: (optional, default: true)                                   *
//*                  if set, assure that highlight is within visible space by    *
//*                          adjusting the IP if necessary.                      *
//*                  if reset, do not adjust IP (some portion of the highlighted *
//*                          text may not be visible).                           *
//*                                                                              *
//* Returns: 'true'  if highlight set/cleared                                    *
//*          'false' a) 'fIndx' references an upsupported control type           *
//*                  b) parameter out-of-range                                   *
//********************************************************************************
//* Note on interaction between text "selection" and "highlighted" text.         *
//* --------------------------------------------------------------------         *
//* A) Target control may not have both highlighted and 'selected' text          *
//*    simultaneously. Setting a highlight will release any selection, and       *
//*    setting a selection will reset any highlight.                             *
//*    a) Data members shared between selections and highlights:                 *
//*       skForm:                                                                *
//*        'seli' - used only for selections, and is not referenced by highlight *
//*                 segments. See next item for details.                         *
//*       skField:                                                               *
//*        'sela' and                                                            *
//*        'selb' - track the head and tail of selected segment or               *
//*                 highlighted segment (but not both at the same time)          *
//*        'state' - when set, it indicate that the field contains a highlighted *
//*                  segment, and when reset no highlighted segment.             *
//*                  Text selections do not reference the 'state' flag           *
//*        'iAttr' - When a highlight is active, the first element of the array  *
//*                  contains the text-modifier attribute.                       *
//*                  This array is otherwise unused within Textbox control.      *
//*                                                                              *
//* B) Calling this method will release any selected segment in the target       *
//*    field. Any selection in a _different_ field will not be affected.         *
//*    The field containing a selected segment is referenced by the skForm       *
//*    'seli' member. This value will be modified here only if 'seli' references *
//*    the field targeted for the highlighted segment.                           *
//*                                                                              *
//* C) The five(5) selection keys: wcsSLEFT/wcsSRIGHT/wcsSUP/wcsSDOWN/wcsALT_A   *
//*    when seen in a field with a highlighted segment, will remove the          *
//*    highlight from the segment. before initializing the selection.            *
//     Practically speaking, the highlighted segment is un-highlighted by        *
//*    resetting the 'state' flag of the target field.                           *
//*      state == reset  : text selection mode (default)                         *
//*      state == set    : text highlighting                                     *
//*    Note that resetting the 'state' flag is needed ONLY when a new selection  *
//*    is started. Subsequent keycodes that update an existing selection sequence*
//*    would not need to be aware of the flag. This operation is handled in      *
//*    either skpMultirow() or skpShiftedText() methods, depending on the        *
//*    current formatting option for the field text.                             *
//*                                                                              *
//* D) The skField 'iAttr' array is used by Scrollbox and Menu control to produce*
//*    multi-color (per-row) attributes. In Textbox controls, the first element  *
//*    of the 'iAttr' array is used to store the specified text-modifier         *
//*    attribute used for the highlight segment.                                 *
//*      Example: skfp->fld[fIndx].iAttr[0] = modifier attribute                 *
//*               skfp->fld[fIndx].iAttr[1] is reserved, and is currently unused.*
//*    a) Note that the specified text-modifier SHOULD NOT also be present in    *
//*       the control's base text color, because it would cause a conflict with  *
//*       the text data which is not highlighted.                                *
//*    b) Note that the actual ANSI escape sequences for the highlighted segment *
//*       must be mapped to the specified text modifier. This is done through    *
//*       the special-purpose 'modifierTable' class located at the top of the    *
//*       AnsiCmdSkf module.                                                     *
//*                                                                              *
//********************************************************************************

bool ACWin::hilightFieldText ( skForm* skfp, short fIndx, bool set, 
                               int begin, int len, acAttr txtmod, bool center )
{
   acAttr basemods ;
   bool status = false ;
   if ( (fIndx >= ZERO) && (fIndx < skfp->fCnt) && 
        ((skfp->fld[fIndx].type == ftTB) /*|| (skfp->fld[fIndx].type == ftSB)*/) )
   {
      skField* fptr = &skfp->fld[fIndx] ;

      //* ------------------------------- *
      //* If set highlighted segment      *
      //* ------------------------------- *
      if ( set )
      {
         //* Validate and set the modifier attribute.*
         //* Stored at fptr->iAttr[ZERO].            *
         fptr->setHiliteMod ( txtmod ) ;

         //* If validated modifier is in conflict with a modifier included *
         //* in the base color of the field,try to resolve the conflict.   *
         basemods = acAttr(fptr->aca.acaVal & acaMOD_MASK) ;
         if ( (fptr->iAttr[ZERO] & basemods) != ZERO )
         {
            if      ( !(basemods & acaBOLD) )      { fptr->iAttr[ZERO] = acaBOLD ; }
            else if ( !(basemods & acaITALIC) )    { fptr->iAttr[ZERO] = acaITALIC ; }
            else if ( !(basemods & acaREVERSE) )   { fptr->iAttr[ZERO] = acaREVERSE ; }
            else if ( !(basemods & acaUNDERLINE) ) { fptr->iAttr[ZERO] = acaUNDERLINE ; }
            else if ( !(basemods & acaBLINK) )     { fptr->iAttr[ZERO] = acaBLINK ; }
            else if ( !(basemods & acaOVERLINE) )  { fptr->iAttr[ZERO] = acaOVERLINE ; }
            else if ( !(basemods & acaXOUT) )      { fptr->iAttr[ZERO] = acaXOUT ; }
            else if ( !(basemods & acaCONCEAL) )   { fptr->iAttr[ZERO] = acaCONCEAL ; }
            // else, "My give up!" -- Jarjar Binks
         }

         //* If existing "selection" in target field, *
         //* release the current selection.           *
         if ( skfp->seli == fIndx )
            skfp->seli = skfp->fCnt ;

         //* Range check and initialize begin position and run length.*
         int chcnt = fptr->txt.gschars() ;   // character count
         int ni = fptr->mrp.ni ;             // reference null terminator
         int end ;                           // index char after end of segment
         if ( len <= ZERO )         { len = 1 ; }
         else if ( len >= chcnt )   { len = chcnt - 1 ; }
         if ( begin < ZERO )        { begin = ZERO ; }
         else if ( begin >= ni )    { begin = ni - 1 ; len = 1 ; }
         if ( (end = begin + len) >= chcnt ) { end = ni ; }
         fptr->sela = begin ;
         fptr->selb = end ;

         //* If centering specified, ensure that (at least) the first     *
         //* character of the highlighted segment is within visible space.*
         if ( center )
         {
            fptr->ip = begin ;
            this->centerFieldIP ( fptr, center, center ) ;
         }

         fptr->state = true ;                // signal that highlight is active
      }  // ( set )

      //* ------------------------------- *
      //* If reset of highlighted segment *
      //* ------------------------------- *
      else
      {
         fptr->sela = fptr->selb = ZERO ;    // reset the bracketing indicators
         fptr->state = false ;               // reset the flag
         status = true ;
      }

      if ( ! fptr->hide )
         this->refreshField ( skfp, fIndx ) ;
   }
   return status ;

}  //* End hilightFieldText() *

//*************************
//*  staleHilightSegment  *
//*************************
//********************************************************************************
//* Non-Member Method:                                                           *
//* ------------------                                                           *
//* The 'state' flag of the Textbox field indicates whether a "highlighted" text *
//* segment is currently displayed. If the keycode signals a text "selection",   *
//* then reset the 'state' flag, which removes the highlight.                    *
//*                                                                              *
//* Called ONLY when field with focus is a Textbox (ftTB).                       *
//*                                                                              *
//* Input  : fptr : pointer to Textbox field with focus                          *
//*          wkey : keycode received from user                                   *
//*                                                                              *
//* Returns: current state of the 'state' flag                                   *
//********************************************************************************

static bool staleHilightSegment ( skField* fptr, wchar_t wkey )
{
   if ( (fptr->state) && 
        ((wkey == wcsSRIGHT) || (wkey == wcsSLEFT) || (wkey == wcsSELECT) ||
         (wkey == wcsSUP)    || (wkey == wcsSDOWN)) )
   { fptr->state = false ; }

   return ( fptr->state ) ;

}  //* End staleHilightSegment() *

//*************************
//*     SetHilightMod     *
//*************************
//********************************************************************************
//* For Textbox controls only (ftTB), set the text-modifier attribute to be      *
//* used for highlighting text segments within the field.                        *
//*                                                                              *
//* Note that no text is actually highlighted by this method; only the attribute *
//* is initialized. Please see the HighlightFieldText() method for additional    *
//* information.                                                                 *
//*                                                                              *
//* Input  : fIndx : index of target field                                       *
//*          txtmod: text modifier attribute for highlighting text segments      *
//*                  One of the following:                                       *
//*                    acaBOLD      bold-text modifier                           *
//*                    acaITALIC    italic-text modifier                         *
//*                    acaUNDERLINE underline-text modifier                      *
//*                    acaOVERLINE  overline-text modifier                       *
//*                    acaXOUT      x-out (strikethrough) text modifier          *
//*                    acaBLINK     blinking-text modifier                       *
//*                    acaCONCEAL   concealed (invisible) text                   *
//*                    acaREVERSE   reversed fgnd/bkgnd (default)                *
//*                    Note: OR'd combinations of these modifiers not allowed.   *
//*                                                                              *
//* Returns: 'true'  if highlight attribute initialized                          *
//*          'false' a) 'fIndx' references an upsupported control type           *
//*                  b) parameter out-of-range (default parameter set)           *
//********************************************************************************
//* Note that there is currently no dialog-oriented equivalent for this method.  *
//* The Dialog_Hilite() method should be sufficient. Wait for some feedback.     *
//********************************************************************************

bool ACWin::SetHilightMod ( short fIndx, acAttr txtmod )
{
   bool status = false ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
      status = this->skf->fld[fIndx].setHiliteMod ( txtmod ) ;

   return status ;

}  //* End SetHilightMod() *

//*************************
//*     RefreshField      *
//*************************
//********************************************************************************
//* Refresh (erase and redraw) the contents of the field which has input focus.  *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::RefreshField ( void )
{

   this->refreshField ( this->skf, this->skf->fi ) ;

}  //* End RefreshField() *

//*************************
//*     RefreshField      *
//*************************
//********************************************************************************
//* Refresh (erase and redraw) the contents of the specified field, or           *
//* optionally, refresh all fields defined by the skForm object.                 *
//* Input focus remains on the currently active field, and the cursor (whether   *
//* visible or invisible) is returned to insertion point within that field.      *
//*                                                                              *
//* Input  : fIndx: index of target field                                        *
//*                 If fIndx < ZERO || fIndx >= total of fields defined (fCnt),  *
//*                 then all fields will be refreshed.                           *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::RefreshField ( short fIndx )
{
   bool all_fields ;
   if ( (all_fields = (((fIndx < ZERO) || (fIndx >= this->skf->fCnt)) ? true : false)) )
      fIndx = this->skf->fi ;    // field with focus is refreshed last

   this->refreshField ( this->skf, fIndx, all_fields ) ;

}  //* End RefreshField() *

//*************************
//*     refreshField      *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Refresh (erase and redraw) the contents of the specified field, or           *
//* optionally, refresh all fields defined by the skForm object.                 *
//* Input focus remains on the currently active field, and the cursor (whether   *
//* visible or invisible) is returned to insertion point within that field.      *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*          fIndx: index of target field                                        *
//*          all  : (optional, 'false' by default)                               *
//*                 if 'true',  refresh all fields                               *
//*                             (field with focus is refreshed last)             *
//*                 if 'false', refresh only target field                        *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Note: There is a design decision that if the 'all' (all fields) flag is set, *
//*       then caller is trying to refresh the entire window. Thus, the          *
//*       lower-level method to refresh the individual field is always called    *
//*       with the optional flag for label/outline set. This ensures that the    *
//*       field label and outline (if present) will be drawn/redrawn.            *
//********************************************************************************

void ACWin::refreshField ( skForm* skfp, short fIndx, bool all )
{
   //* If cursor is visible, set to invisible. *
   if ( this->tset->curVis )
      this->acSetCursorStyle ( csHide ) ;

   if ( all )                                // if updating all fields
   {
      for ( short f = ZERO ; f < skfp->fCnt ; ++f )
      {
         //* If NOT the target field and field is not hidden *
         if ( (f != fIndx) && ! skfp->fld[f].hide )
         {
            if ( skfp->fld[f].type == ftTB ) // refresh text fields
               this->refreshText ( skfp, f, all ) ;
            else                             // refresh other types of controls
               this->refreshCtrl ( skfp, f, bool(f == skfp->fi), all ) ;
         }
      }
   }

   //* Draw the target field last.                                   *
   //* a) For Textbox fields (and optionally the field title/outline)*
   //* b) For other field types (and optionally their title/outline) *
   if ( skfp->fld[fIndx].type == ftTB ) // refresh text fields
      this->refreshText ( skfp, fIndx, all ) ;
   else                             // refresh other types of controls
      this->refreshCtrl ( skfp, fIndx, bool(fIndx == skfp->fi), all ) ;

   //* Set cursor to insertion point of field with focus.*
   this->setCursor2IP ( skfp ) ;

   //* Make cursor visible for certain fields:  *
   //* a) Textbox fields                        *
   if ( skfp->fld[skfp->fi].type == ftTB )
      this->acSetCursorStyle ( csShow ) ;

}  //* End refreshField() *

//*************************
//*     SetFieldAttr      *
//*************************
//********************************************************************************
//* Set the color attributes and modifiers for the specified field.              *
//* Refresh the field display using the modified attributes.                     *
//*                                                                              *
//* On return, the cursor is positioned in the field with input focus.           *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          txtAttr : (acAttr bitfield) a logical OR of forground and           *
//*                    background color attributes and attribute modifiers       *
//*                    (note: use of 'underline' modifier not recommended)       *
//*                                                                              *
//* Returns: 'true'  if attribute set successfully                               *
//*          'false' if invalid field index specified                            *
//********************************************************************************
//* Notes: 'underline' modifier is not recommended because it would conflict     *
//*        with text selection and 'hotkey' indicators                           *
//********************************************************************************

bool ACWin::SetFieldAttr ( short fIndx, acAttr txtAttr )
{
   bool status = false ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      //* If multi-color attributes, release the dynamic *
      //* allocation and disable multi-colored data.     *
      if ( this->skf->fld[fIndx].iAttr != NULL )
      {
         delete [] this->skf->fld[fIndx].iAttr ;
         this->skf->fld[fIndx].iAttr = NULL ;
      }

      //* Decode the field attribute *
      this->skf->fld[fIndx].aca.decode( txtAttr ) ;

      //* Redraw the text in the field *
      this->RefreshField ( fIndx ) ;

      //* Set the cursor to its position in the field with focus.*
      if ( fIndx != this->skf->fi )
         this->RefreshField ( this->skf->fi ) ;

      //* Restore current terminal attributes *
      this->restoreAttributes () ;

      status = true ;
   }
   return status ;

}  //* End SetFieldAttr() *

//*************************
//*     SetFieldAttr      *
//*************************
//********************************************************************************
//* Set the color attributes and modifiers for the specified field.              *
//* The array of attributes will be applied one per text row within the field.   *
//* Multi-color attributes apply _only_ to Menus (ftMB) and Scrollbox (ftSB)     *
//* controls.                                                                    *
//*                                                                              *
//* It is recommended that this method be called AFTER instantiation of          *
//* the ACWin object, but BEFORE the call to OpenWindow(). If called after the   *
//* window has been opened, then on return the display should be refreshed       *
//* explicitly to ensure that the changes are visible.                           *
//*                                                                              *
//* Input  : fIndx  : index of target field (ftMB or ftSB only)                  *
//*          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 invalid field index specified, or                        *
//*                  if field type does not support per-row                      *
//*                  attributes, or if 'rowCnt' out-of-range                     *
//********************************************************************************

bool ACWin::SetFieldAttr ( short fIndx, const acAttr* rowAttr, short rowCnt )
{
   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
      status = this->skf->fld[fIndx].colorConfig ( rowAttr, rowCnt ) ;
   return status ;

}  //* End SetFieldAttr() *

//*************************
//*     SetFieldFlow      *
//*************************
//********************************************************************************
//* Set the text-flow type, reformatting the text if necessary, and optionally   *
//* refresh the display.                                                         *
//*                                                                              *
//* Important Note: Do not modify formatting option for a field which is         *
//*                 currently under edit. The edit routine would crap its pants. *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          fType   : member of FloType                                         *
//*          refresh : (optional, default: 'false')                              *
//*                    if 'true',  immediately reformat field text               *
//*                                and update the display                        *
//*                    if 'false', text is reformatted as needed, but            *
//*                                display is not updated                        *
//*                                                                              *
//* Returns: 'true'  if formatting optione set successfully                      *
//*          'false' if invalid field index specified or if target field         *
//*                  is a single-row field, or invalid 'fType'                   *
//********************************************************************************

bool ACWin::SetFieldFlow ( short fIndx, FloType fType, bool refresh )
{
   bool force  = false ;      // for call to formatFieldText(), always reset
   bool status = false ;      // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) &&
        (this->skf->fld[fIndx].hgt > 1) && (fType != floDflt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( fType != fptr->flow )          // if an actual change of type
      {
         fptr->flow = fType ;             // set the new flow type
         fptr->off = fptr->shft = fptr->ip = ZERO ;         // set IP to origin
         this->formatFieldText ( this->skf, fIndx, force ) ;// reformat the text data
      }

      if ( refresh )
         this->RefreshField ( fIndx ) ;

      status = true ;
   }
   return status ;

}  //* End SetFieldFlow() *

//*************************
//*     SetFieldState     *
//*************************
//********************************************************************************
//* Set the "state" of a binary field.                                           *
//* Currently, this applies only to Radiobuttons (type==ftRB).                   *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          state   : if 'true',  set field state                               *
//*                    if 'false', reset field state                             *
//*          refresh : (optional, default: 'false')                              *
//*                    if 'true',  refresh the field display                     *
//*                    if 'false', do not refresh the field                      *
//*                                                                              *
//* Returns: current state of the field                                          *
//********************************************************************************
//* Programmer's Note: There is some chance that the text and the flag will      *
//* get out-of-synch. For this reason, we set/reset explicitly, rather that      *
//* relying on synchronized data.                                                *
//********************************************************************************

bool ACWin::SetFieldState ( short fIndx, bool state, bool refresh )
{
   bool newstate = false ;

   //* Verify that index is valid and that *
   //* 'fIndx' references a radiobutton.   *
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( fptr->type == ftRB )
      {
         newstate = this->setFieldState ( fptr, state ) ;
         if ( refresh )
            this->refreshCtrl ( this->skf, fIndx, false ) ;
      }
   }
   return newstate ;

}  //* End SetFieldState() *

//*************************
//*     setFieldState     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Set the "state" of a binary field.                                           *
//* Currently, this applies only to Radiobuttons (type==ftRB).                   *
//*                                                                              *
//* Input  : fptr    : pointer to target field                                   *
//*          state   : if 'true',  set field state                               *
//*                    if 'false', reset field state                             *
//*                                                                              *
//* Returns: current state of the field                                          *
//********************************************************************************

bool ACWin::setFieldState ( skField *fptr, bool state )
{
   fptr->txt.erase( 1, 1 ) ;
   fptr->txt.insert( wchar_t(state ? fptr->rbch : SPACE), 1 ) ;
   fptr->state = state ;
   return ( fptr->state ) ;

}  //* End setFieldState() *

//*************************
//*     GetFieldState     *
//*************************
//********************************************************************************
//* Get the current "state" of a binary field (or XOR field group).              *
//* Currently, this applies only to Radiobuttons (type==ftRB).                   *
//*                                                                              *
//* XOR Groups:                                                                  *
//* -----------                                                                  *
//* For radiobutton XOR groups, the binary state for only one member of the      *
//* group can be "set" i.e. 'true'. If 'fIndx' references a member of an XOR     *
//* group, _and_ if the 'xorMember' pointer is non-null, then the variable       *
//* referenced by 'xorMember' receives the index of the group member whose flag  *
//* is "set".                                                                    *
//* - If the control referenced by 'fIndx' is not a Radiobutton or is not a      *
//*   member of an XOR group, then the 'xorMember' parameter is ignored.         *
//* - Note that in the (unlikely) case where no member of the XOR group has      *
//*   its flag set, *xorMember' will receive the 'fIndx' value.                  *
//*                                                                              *
//* Input  : fIndx    : index of target field within the skForm object           *
//*          xorMember: (optional, default: null pointer) for XOR groups only    *
//*                     if specifed, the referenced variable receives the index  *
//*                     of the control whose 'set' flag is 'true.                *
//*                                                                              *
//* Returns: current state of 'set' flag for field referenced by 'fIndx'         *
//*          ('false' if fIndx is out of range)                                  *
//********************************************************************************

bool ACWin::GetFieldState ( short fIndx, short* xorMember )
{
   bool state = false ;

   //* Range-check the index. *
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      state = this->getFieldState ( this->skf, fIndx, xorMember ) ;
   }
   return ( state ) ;

}  //* End GetFieldState() *

//*************************
//*     getFieldState     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Get the current "state" of a binary field (or XOR field group).              *
//* Currently, this applies only to Radiobuttons (type==ftRB).                   *
//*                                                                              *
//* (See notes in the public method, above.)                                     *
//*                                                                              *
//* Input  : skfp     : pointer to skForm object containing the target field     *
//*        : fIndx    : index of target field within the skForm object           *
//*          xorMember: XOR groups only: If non-null, the referenced variable    *
//*                     receives the index of the control whose 'set' flag value *
//*                     is 'true.                                                *
//*                                                                              *
//* Returns: current state of 'set' flag for field referenced by 'fIndx'         *
//*          ('false' if fIndx is out of range)                                  *
//********************************************************************************

bool ACWin::getFieldState ( const skForm *skfp, short fIndx, short* xorMember )
{
   bool curstate = false ;                // return value

   //* Range-check the index. *
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      const skField* fptr = &skfp->fld[fIndx] ;
      curstate = fptr->state ;         // state of target control is returned

      //* If target control is a Radiobutton AND is a member of a group *
      //* AND if the group-member index was requested, scan for the     *
      //* member whose binary flag is set.                              
      if ( (fptr->type == ftRB) && (fptr->grp != noGROUP) && (xorMember != NULL) )
      {
         *xorMember = fIndx ;    // default (in case no group member is 'set')
         for ( short f = ZERO ; f < skfp->fCnt ; ++f )
         {
            if ( (skfp->fld[f].grp == fptr->grp) && skfp->fld[f].state )
            { *xorMember = f ; break ; }
         }
      }
   }
   return ( curstate ) ;

}  //* End getFieldState() *

//*************************
//*     SetFieldLabel     *
//*************************
//********************************************************************************
//* Assign a label, with or without hotkey, to the specified field.              *
//* - Labels may be assigned only to Textbox and Scrollbox fields                *
//*   (FldType ftTB/ftSB).                                                       *
//*   -- A label may also be assigned to a Menu (FldType ftMB)                   *
//*      under specific circumstances.                                           *
//*      1) Context menus (multiple-row menus which are not members              *
//*         of any group.                                                        *
//*      2) Menubars (single-row menus which anchor a menu group.                *
//*      -- Labels are incompatible with other Menubox controls.                 *
//*         See SetFieldHotkey() for setting hotkeys for these                   *
//*         controls.                                                            *
//* - For Pushbuttons and Radiobutton controls (FldType ftPB/ftRB),              *
//*   the label is integrated into the control definition, and                   *
//*   therefore are not handled by this method.                                  *
//*                                                                              *
//* -- Label length: 1 <= length <= labelLEN.                                    *
//*    (will be truncated if necessary)                                          *
//* -- Label width (columns): truncated if necessary to avoid                    *
//*    violation of window border.                                               *
//* -- Range of valid hotkeys: wcsALT_A <= hotkey <= wcsALT_RB.                  *
//*    This corresponds to ASCII A-Z, a-z, and most punctuation.                 *
//*    The specified hotkey should be unique within the window.                  *
//* -- The label is positioned one row above the target field with               *
//*    the horizontal offset determined by the 'center' parameter.               *
//*                                                                              *
//* Input  : fIndx   : index of 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.                                         *
//*          labAttr : (optional, interior of container window by                *
//*                    default) color attributes for label text.                 *
//*          center  : (optional, default: false) horiz. offset                  *
//*                    if set, center the label horizontally in the              *
//*                            row above the field                               *
//*                    if reset, label offset is in the same column              *
//*                            as the left edge of the field                     *
//*          outline : (optional, default: false) include outline                *
//*                    if set, include a field outline for control.              *
//*                    if reset, the 'lType' parameter is ignored.               *
//*          lType   : (optional, ltSINGLE by default) line style                *
//*                     member of enum LineType: ltSINGLE or ltDUAL              *
//*                    This parameter is referenced _only_ when                  *
//*                    the 'outline' flag is set.                                *
//*                    Outline takes the attribute of the labe.                  *
//*                                                                              *
//* Returns: 'true' if label successfully written                                *
//*          'false' if invalid field index or incompatible field type.          *
//*          Note that if 'outline' is specified, but insufficient space is      *
//*          available, the outline will not be drawn, but the returned status   *
//*          will not reflect the error.                                         *
//********************************************************************************
//* Notes: See private method for additional info.                               *
//********************************************************************************

bool ACWin::SetFieldLabel ( short fIndx, const char* labText, acAttr labAttr, bool center,
                            bool outline, LineType lType )
{
   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField* fptr = &this->skf->fld[fIndx] ;

      //* If specified, initialize the field outline *
      //* (display is not updated)                   *
      if ( outline )
         this->setFieldOutline ( fptr, lType, labAttr ) ; 

      //* If label successfully initialized, redraw the *
      //* field with label (and optional outline).      *
      if ( (status = this->setFieldLabel ( fptr, labText, labAttr, center )) )
         this->refreshField ( this->skf, fIndx, true ) ;
   }
   return status ;

}  //* End SetFieldLabel() *

//*********************
//*  SetFieldOutline  *
//*********************
//********************************************************************************
//* Draw an outline around the outside of the specified field within the window  *
//* border. The contents of the field will be refreshed after border is drawn.   *
//*                                                                              *
//* This method should not be used for Menu fields (ftMB) because menus have     *
//* embedded borders,                                                            *
//*                                                                              *
//* The target field must have at least one space open on all sides. That is,    *
//* it must not be positioned against the window border, and must not be         *
//* adjacent to any other field.                                                 *
//* Programmer's Note: Potential incursion into the border is range checked;     *
//* however, potential incursion into other fields _is not_ tested. Beware!      *
//*                                                                              *
//* 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' if invalid field index, incompatible field type or          *
//*                  insufficient space around the field                                            *
//********************************************************************************

bool ACWin::SetFieldOutline ( short fIndx, LineType lType, acAttr lnAttr )
{
   bool status = false ;                     // return value

   //* If a valid target field index was specified *
   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      if ( (status = this->setFieldOutline ( &this->skf->fld[fIndx], lType, lnAttr )) )
         this->refreshField ( this->skf, fIndx, true ) ;
   }
   return status ;

}  //* End SetFieldOutline() *

//*************************
//*    SetFieldHotkey     *
//*************************
//********************************************************************************
//* Set the "hotkey" for the specified control. A hotkey is a character used to  *
//* set the input focus to the associated control.                               *
//* -- The range of valid hotkeys is: wcsALT_A <= hotkey <= wcsALT_RB.           *
//*    This corresponds to ASCII A-Z, a-z, and most punctuation characters.      *
//* -- The function keys may also be used as hotkeys: wcsF01 <= hotkey <= wcsCF12*
//* -- The specified hotkey should be unique within the window.                  *
//*                                                                              *
//* Note that the standard method for hotkey specification is to embed the       *
//* hotkey character in the control label so that the user has a visual cue,     *
//* (see SetFieldLabel().                                                        *
//*                                                                              *
//* Menubox controls (ftMB) do not have labels. For this reason, if a hotkey     *
//* is desired for a menu control, it must be set either during skField          *
//* instantiation ('hkey' member of skField), OR via this method after ACWin     *
//* instantiation.                                                               *
//*                                                                              *
//* This method may also be used to set the hotkey for some other types of       *
//* controls (ftTB, ftSB), but this is not recommended. Note that for Pushbutton *
//* and Radiobutton controls, the label (and therefore the hotkey) are           *
//* embedded in the control itself; and therefore this method may not be used    *
//* to set the hotkey for those control types. Note also that if a hotkey        *
//* has been previously specified for the target control, it will not be         *
//* overwritten by this method.                                                  *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          hotKey  : hotkey character                                          *
//*                                                                              *
//* Returns: 'true' if hotkey set successfully                                   *
//*          'false' if invalid control index, unsupported control type,         *
//*                  specified keycode is not one of the valid hotkey characters,*
//*                  target control's hotkey was previously set.                 *
//********************************************************************************

bool ACWin::SetFieldHotkey ( short fIndx, wchar_t hotKey )
{
   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      skField *fptr = &this->skf->fld[fIndx] ;
      if ( ((fptr->type == ftMB) || (fptr->type == ftTB) || (fptr->type == ftSB))
           && (fptr->hkey == NULLCHAR)
           && ((this->isHotkey ( hotKey )) || (this->isFunky( hotKey )))
         )
      { fptr->hkey = hotKey ; status = true ; }
   }
   return status ;

}  //* End SetFieldHotkey() *

//*************************
//*      SetReadOnly      *
//*************************
//********************************************************************************
//* Enable or disable read-only state for the specified field.                   *
//*                                                                              *
//* Input  : fIndx   : index of target field within the skForm object            *
//*          set    : if 'true',  set field as read-only                         *
//*                   if 'false', set field as user-accessible                   *
//*                                                                              *
//* Returns: current state of the skForm 'ro' flag                               *
//********************************************************************************

bool ACWin::SetReadOnly ( short fIndx, bool set )
{

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
      this->skf->fld[fIndx].ro = set ;
   return ( this->skf->fld[fIndx].ro ) ;

}  //* End SetReadOnly() *

//*************************
//*    SetFieldHidden     *
//*************************
//********************************************************************************
//* Set the visible/hidden state of the target control and optionally refresh    *
//* the display.                                                                 *
//* 1) The 'hidden' flag specifies whether the control is visible.               *
//* 2) The 'read-only' flag specifies whether control can receive input focus.   *
//*                                                                              *
//* If target control is set as 'hidden', AND if 'refresh' is specified,         *
//* then the entire window interior will be refreshed to restore display         *
//* of any controls or static text that had been obscured by that control.       *
//*                                                                              *
//* If target control is set as visible (hidden==false), AND if 'refresh'        *
//* is specified, then only the target control will be refreshed.                *
//*                                                                              *
//*                                                                              *
//* Input  : fIndx    : index of target field within the skForm object           *
//*          hidden   ; if 'true',  field will become invisible when the window  *
//*                                 is refreshed                                 *
//*                     if 'false', field will become visible when refreshed     *
//*          readOnly : if 'true',  field cannot receive input focus             *
//*                     if 'false', field can receive input focus                *
//*          refresh  : if 'true',  if 'hidden' is set, refresh the entire       *
//*                                    window interior                           *
//*                                 if 'hidden' is reset, refresh only the       *
//*                                    target field                              *
//*                     if 'false', display is not updated                       *
//*                                                                              *
//* Returns: 'true'  if operation successful                                     *
//*          'false' if invalid field index                                      *
//********************************************************************************

bool ACWin::SetFieldHidden ( short fIndx, bool hidden, bool readOnly, bool refresh )
{
   bool status = false ;                  // return value

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) )
   {
      status = this->setHidden ( this->skf, fIndx, hidden, readOnly ) ;
      if ( refresh )
      {
         if ( hidden )                    // if 'hidden' refresh full window interior
            this->RefreshWin ( false ) ;
         else                             // else refresh target only
            this->refreshField ( this->skf, fIndx ) ;
      }
   }
   return status ;

}  //* End SetFieldHidden() *

//*************************
//*       setHidden       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called by the public SetFieldHidden() to enable/disable menus (and other     *
//* controls).                                                                   *
//* This method updates the configuration of the specified control, but DOES NOT *
//* update the display.                                                          *
//*                                                                              *
//* IMPORTANT NOTE: If the target field currently has the input focus when it    *
//*                 is set as hidden, it is caller's responsibility to           *
//*                 immediately shift focus to another field.                    *
//*                                                                              *
//* If field referenced by fIndx is locked visible, then trying to hide it will  *
//* fail. This boneheaded attempt is not currently reported.                     *
//*                                                                              *
//* Input  : skfp     : pointer to skForm object                                 *
//*        : fIndx    : index of target field within the skForm object           *
//*          hidden   ; if 'true',  field will become invisible when refreshed   *
//*                     if 'false', field will become visible when refreshed     *
//*          readOnly : (optional, false by default)                             *
//*                     if 'true',  field cannot receive input focus             *
//*                     if 'false', field can receive input focus                *
//*                                                                              *
//* Returns: 'true'  if operation successful, else 'false'                       *
//*          'false' if operation fails: [CURRENTLY UNUSED]                      *
//********************************************************************************

bool ACWin::setHidden ( skForm *skfp, short fIndx, bool hidden, bool readOnly )
{
   bool status = true ;                   // return value (hope for the best)

   skField* fptr = &skfp->fld[fIndx] ;    // pointer to target field
   if ( ! fptr->lock || ! hidden )        // if not locked in visible state
      fptr->hide = hidden ;               // update the flags
   fptr->ro = readOnly ;

   //* For menu controls which are becomming visible, *
   //* reinitialize the IP and field parsing.         *
   if ( fptr->type == ftMB )
   {
      if ( ! fptr->hide )
         this->setVisible ( fptr ) ;
   }
   return status ;

}  //* End setHidden() *

//*************************
//*      setVisible       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Configure the specified control as visible. This method currently supports   *
//* only menu controls (ftMB).                                                   *
//* This method updates the configuration of the specified control, but DOES NOT *
//* update the display.                                                          *
//*                                                                              *
//* For ftMB and ftSB controls, reinitialize IP to reference top of data and     *
//* re-parse the field stats.                                                    *
//*  a) For single-row (menubar) menus, the IP references the first (leftmost)   *
//*     character of the target token and the cursor is visible to show the      *
//*     position                                                                 *
//*  b) For multi-row menus the IP is at the leftmost column of the current      *
//*     row i.e. in the left menu border. The cursor is invisible, and the       *
//*     IP position is indicated by the "indicator character".                   *
//*                                                                              *
//********************************************************************************

bool ACWin::setVisible ( skField *fptr )
{
   bool status = true ;                   // return value (hope for the best)

   //* For a target menu that is already visible, do nothing.  *
   //* Note: Current design is that IP of an already-open menu *
   //* does not change when it receives focus. If design       *
   //* changes, this test will neede to be updated.            *
   if ( !fptr->hide && (fptr->type == ftMB) && (fptr->hgt > 1) && (fptr->grp == noGROUP) )
      return true ;

   fptr->hide = false ;                   // reset the 'hidden' flag

   if ( fptr->type == ftMB )
   {
      int smi ;                           // receives token number (ignored)
      fptr->ip = ZERO ;                   // set IP to head of text (group sub-menu)
      if ( fptr->hgt == 1 )               // for menubar, set IP to first token
         menubarToken ( wcsTAB, fptr->txt.gstr(), fptr->ip, smi ) ;

      //* For multi-row context (independent) menus only, *
      //* set IP to first column of second row,           *
      //* i.e. below the top border.                      *
      else if ( ! fptr->hide && (fptr->grp == noGROUP) )
         fptr->ip = fptr->mrp.roff[1] ;

      //* Update the parsing stats *
      this->parseFieldText ( fptr, ((fptr->hgt == 1) ? 0 : 1) ) ;
   }
   return status ;

}  //* End setVisible() *

//*************************
//*       menuExit        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Set menu or menu group with focus as hidden, set new 'fi' and refresh the    *
//* window (ACWin) or field(s) (ACDlg).                                          *
//* -- If the source control's 'lock' flag is set, it will not be hidden.        *
//* -- If the target control is hidden, it will be set visible.                  *
//* -- If the target control is a sub-menu within a menu group, then the         *
//*    associated menubar will also be set as visible.                           *
//* -- If the controls are located inside a dialog window and not the ACWin      *
//*    object, only the source and target controls will be refreshed.            *
//*    This is necessary because we do not have access to the dialog object at   *
//*    this level. If the closed menu had obscured other data then the dialog    *
//*    code will be responsible for restoring the obscured data.                 *
//*                                                                              *
//* Input  : skfp     : pointer to skForm object                                 *
//*        : fIndx    : index of field with focus                                *
//*          fTrg     : index of field which will receive focus                  *
//*                     (target's 'ro' and 'hide' flags must have been reset)    *
//*          save     : (optional, default:false) save selection                 *
//*                     Used only for multi-row menus which are locked in the    *
//*                     visible state, this allows the "selected" item to be     *
//*                     displayed using an alternate attribute.                  *
//*                     if set, save the selected item number to 'sitem' member  *
//*                     if reset, 'sitem' is not modified                        *
//*                                                                              *
//* Returns: 'true'  if successful                                               *
//*          'false' if invalid field index or if target is set as read-only.    *
//********************************************************************************

bool ACWin::menuExit ( skForm *skfp, short fIndx, short fTrg, bool save )
{
   skField *fptr  = &skfp->fld[fIndx] ;      // pointer to field
   wchar_t trgGrp = fptr->grp ;              // group Id
   bool status = false ;                     // return value

   if ( (fIndx >= ZERO) && (fIndx < skfp->fCnt) &&
        (fTrg >= ZERO)  && (fTrg < skfp->fCnt) && !skfp->fld[fTrg].ro )
   {
      if ( trgGrp == noGROUP)             // stand-alone menu
      {
         if ( fptr->lock )                // if locked visible, just refresh
         {
            //* If specified, save the (selected) menu item number.*
            if ( save )
            {
               gString gs ;
               short menu_item ;
               if ( (fptr->getItem ( gs, menu_item )) )
                  fptr->sitem = menu_item ;
            }
            this->refreshCtrl( skfp, fIndx, false ) ;
         }
         else                             // else set as hidden
            this->setHidden ( skfp, fIndx, true ) ;
      }
      else if ( trgGrp != noGROUP )       // member of menu group
      {
         for ( short f = ZERO ; f < skfp->fCnt ; ++f )
         {
            fptr = &skfp->fld[f] ;
            if ( (fptr->grp == trgGrp) && !fptr->lock )
               this->setHidden ( skfp, f, true ) ;
         }
      }

      fptr = &skfp->fld[fTrg] ;
      if ( fptr->hide )                      // if target is hidden, make it visible
         this->setVisible ( fptr ) ;
      //* If target is a sub-menu within a group, *
      //* menubar must also be set as visible.    *
      if ( (fptr->grp != noGROUP) && (fptr->hgt > 1) )
      {
         for ( short f = fTrg ; f >= ZERO ; --f )
         { if ( skfp->fld[f].hgt == 1 ) { this->setVisible ( &skfp->fld[f] ) ; break ; } }
      }
      skfp->fi = fTrg ;                      // set the target field
      //* If the menu is located in an ACWin object, refresh the *
      //* window interior including all controls.                *
      //* If the menu is located in an ACDlg object, refresh     *
      //* only the control which now has focus. (see note above).*
      if ( ! skfp->dlg ) 
         this->RefreshWin ( false ) ;        // refresh window interior
      else
         this->refreshField ( skfp, skfp->fi, false ) ;
      status = true ;
   }
   return status ;

}  //* End menuExit() *

//*************************
//*     GetFieldItem      *
//*************************
//********************************************************************************
//* Reports the menu (or scrollbox) line item which is referenced by the IP      *
//* (Insertion Point) of the specified control.                                  *
//*                                                                              *
//* This method will typically be called from the application's callback         *
//* function after the item-selection signal is received; however, a prior       *
//* item selection is not necessary.                                             *
//*                                                                              *
//* Input  : fIndx     : index of target control                                 *
//*          itemIndex : (by reference) receives (0-based) index of item which   *
//*                      is referenced by the IP                                 *
//*          itemHotkey: (by reference) receives the "hotkey" character (if any) *
//*                      for that item                                           *
//*          itemText  : (optional, default: null pointer)                       *
//*                      If specified, this is a pointer to a gString object     *
//*                      which will receive the text of the target item.         *
//*                                                                              *
//* Returns: 'true'  if successful,                                              *
//*          'false' if invalid field index or target is of unsupported          *
//*                     control type                                             *
//*                     'itemIndex' will be set to -1 and                        *
//*                     'itemHotkey' will be set to '?', and                     *
//*                     'itemText' if specified, will be set to an empty string. *
//********************************************************************************
//* Notes:                                                                       *
//* a) For ftMB menu controls (and also ftSB Scrollbox controls) the IP          *
//*    always references the first (leftmost) character of the row.              *
//* b) Caller's 'itemIndex' receives the index of the menu item which contains   *
//*    the IP in the range: 0 <= itemIndex <= items-1                            *
//*    Example: ┌────────────────┐                                               *
//*             │Open a file     │    │Open a file     │ <-- itemIndex == zero   *
//*             │Save file       │    │Save file       │                         *
//*             │Close file      │    │Close file      │                         *
//*             │Exit Application│    │Exit Application│                         *
//*             └────────────────┘    └────────────────┘                         *
//*    -- In the first example, the top and bottom borders will never contain    *
//*       the IP. The first menu item, "Open a file" is item #0 and the last     *
//*       menu item, "Exit Application" is item #3.                              *
//*       Note that sub-menus within a menu group have only bottom borders, NOT  *
//*       top borders, but the returned item number is the same in either case.  *
//* c) Each menu item MAY include a "hotkey" indicator character.                *
//*    -- If found, this character is reported to caller in 'itemHotkey'.        *
//*    -- If the item does not include a hotkey character, 'itemHotkey' receives *
//*       a placeholder '?' character.                                           *
//*    When a menu item is created, the hotkey character is identified by the    *
//*    caret character. Example: "E^xit Application" indicates that the item     *
//*    may be selected by typing the 'x' character (or via Enter or Space keys). *
//*    -- Internally, the caret is replaced by the ANSI escape sequence which    *
//*       causes the character to be displayed as _underlined_; however, to      *
//*       avoid confusion, when the item text is returned by this method,        *
//*       (optional 'itemText' parameter) the escape sequence is replaced by     *
//*       the original caret character.                                          *
//*                                                                              *
//* d) Note that the single-row (Menubar) menu is a special case in that the     *
//*    "itemIndex" indicates a TOKEN in the text data rather than a menu item.   *
//*    -- For Menubars, 'itemHotkey' will receive the first character of the     *
//*       target token. Example: A Menubar: "|  File  Edit  Help  |"             *
//*       If the "Edit" token is referenced by IP, then                          *
//*         'itemIndex'  receives 1, and                                         *
//*         'itemHotkey' receives 'E'  (even though Menubars technicall do not   *
//*                      contain hotkey characters.                              *
//*         'itemText' (if specified), will receive the token text, "Edit".      *
//* e) Scrollbox controls (ftSB) typically contain plain, paragraph-oriented     *
//*    text data, not itemized data records; however, if desired, the application*
//*    may define each display row of the Scrollbox control as a data item.      *
//*    This method supports reporting of Scrollbox data items, with the exception*
//*    that Scrollbox items never contain selection hotkeys.                     *
//*    -- Therefore, 'itemHotkey' will receive the first (leftmost)              *
//*       non-whitespace character of the item.                                  *
//*    -- 'itemText' (if specified), will receive the full text of the target    *
//*       item (row).                                                            *
//*                                                                              *
//*               See also the skField-class method: getItem().                  *
//********************************************************************************

bool ACWin::GetFieldItem ( short fIndx, short& itemIndex, wchar_t& itemHotkey,
                           gString* itemText ) const
{
   gString iTxt ;                         // text from called method
   int     c ;                            // caret index
   short   iNum ;                         // item index from called method
   bool status = false ;                  // return value

   itemIndex  = -1 ;                      // initialize caller's variables
   itemHotkey = L'?' ;
   if ( itemText != NULL )
      itemText->clear() ;

   if ( (fIndx >= ZERO) && (fIndx < this->skf->fCnt) && 
         ((this->skf->fld[fIndx].type == ftMB) ||
          (this->skf->fld[fIndx].type == ftSB)) )
   {
      if ( (this->skf->fld[fIndx].getItem ( iTxt, iNum )) )
      {
         itemIndex = iNum ;
         if ( (c = iTxt.after( CARET )) >= ZERO )
            itemHotkey = iTxt.gstr()[c] ;
         if ( itemText != NULL )
            *itemText = iTxt ;
         status = true ;
      }
   }
   return status ;

}  //* End GetFieldItem() *

//*************************
//*     GetInputFocus     *
//*************************
//********************************************************************************
//* Get the index of the field which currently has the input focus.              *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************

short ACWin::GetInputFocus ( void )
{
   return this->skf->fi ;

}  //* End GetInputFocus() *

//*************************
//*     SetInputFocus     *
//*************************
//********************************************************************************
//* Set input focus to the specified field, and refresh field contents.          *
//* The cursor is placed on the text insertion point.                            *
//* Cursor Visibility:                                                           *
//*  -- Cursor is visible when field with focus is a text field (ftTB).          *
//*  -- Cursor is invisible when focus is on a non-text field, or when the       *
//*     cursor referencs a position outside any field (window interior space).   *
//* Note: Read-only fields cannot receive the input focus.                       *
//*                                                                              *
//* Input  : fldIndex: index of target text field                                *
//*                    The specified field becomes the active field.             *
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************

short ACWin::SetInputFocus ( short fldIndex )
{
   if ( (fldIndex >= ZERO) && (fldIndex < this->skf->fCnt) &&
        (this->skf->fld[fldIndex].ro == false) )
   {
      this->setInputFocus ( this->skf, fldIndex ) ;
   }
   return this->skf->fi ;

}  //* End SetInputFocus() *

//*************************
//*     setInputFocus     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Move the input focus to the target control.                                  *
//*                                                                              *
//* Called by the public SetInputFocus method and the protected focus2NextField  *
//* and focus2PreviousField methods (and potentially from within dialogs).       *
//*                                                                              *
//* This method contains most of the intelligence related to management of       *
//* input focus.                                                                 *
//* -- It is assumed that:                                                       *
//*    a) 'ftrg' is a valid control index                                        *
//*    b) 'ftrg' != 'fcur'                                                       *
//*    c) control referenced by 'ftrg' is not set to read-only.                  *
//*                                                                              *
//* Actions:                                                                     *
//* -- If control losing focus is a menu (ftMB) which is not locked in the       *
//*    visible state, set it to hidden state.                                    *
//* -- If control losing focus employs focus vs. non-focus attributes or         *
//*    formatting, set to non-focus formatting.                                  *
//* -- If the target control is currently hidden, make it visible.               *
//* -- Set input focus to target control.                                        *
//*                                                                              *
//* Input  : skfp : pointer to skForm object                                     *
//*                 (skfp->fi references control which currently has focus)      *
//*          ftrg : index of control which will receive focus                    *
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************
//* Note on menu groups:                                                         *
//* Currently (v:0.0.6), only ACWin objects may contain menu groups.             *
//* Dialog design specifies that all menu controls are both stand-alone and      *
//* always visible, This avoids the dialog having to refresh the entire interior *
//* space; therefore, this method does not need the dialog stats.                *
//********************************************************************************

short ACWin::setInputFocus ( skForm *skfp, short ftrg )
{
   short fcur = skfp->fi ;                   // index of field with focus
   skField *fcptr = &skfp->fld[fcur],        // pointer to field with current focus
           *ftptr = &skfp->fld[ftrg] ;       // pointer to target field
   bool  done = false ;                      // set if refresh is complete

   //* If cursor is visible, set to invisible. *
   if ( this->tset->curVis )
      this->acSetCursorStyle ( csHide ) ;

   //* If current field uses focus/non-focus formatting, *
   //* redraw the field using non-focus formatting.      *
   if ( (fcptr->type == ftPB) || (fcptr->type == ftRB) || (fcptr->type == ftSB) )
   { this->refreshCtrl ( skfp, fcur, false ) ; }

   //* If either current field or the target field *
   //* is a menu control.                          *
   if ( (skfp->fld[fcur].type == ftMB) || (skfp->fld[ftrg].type == ftMB) )
   {
      //* If current field IS NOT a menu, but target IS a menu *
      if ( (fcptr->type != ftMB) && (ftptr->type == ftMB) )
      {
         //* Reset target's "hidden" flag so that when *
         //* refreshed, it will become visible.        *
         //* Also initialize IP and re-parse the text. *
         this->setVisible ( ftptr ) ;

         if ( ftptr->grp != noGROUP )
         {
            //* If the target is the menubar *
            if ( ftptr->hgt == 1 )
            { /* nothing to do at this time */ }

            //* If target is a sub-menu attached to a     *
            //* menubar, and the menubar is not currently *
            //* visible, make it visible.                 *
            // Programmer's Note: If menubar is not found, it could lead to an error.
            else  // (ftptr->hgt > 1)
            {
               short tmb = ftrg - 1 ;     // find index of target menubar
               while ( tmb != ftrg )
               {
                  if ( tmb < ZERO )
                     tmb = skfp->fCnt - 1 ;
                  if ( (skfp->fld[tmb].hgt == 1) && 
                       (skfp->fld[tmb].grp == ftptr->grp) )
                  { this->setVisible ( &skfp->fld[tmb] ) ; break ; } // set menubar as visible
                  --tmb ;
               }
            }     // enable menubar
         }  // (group)

         skfp->fi = ftrg ;                // set the target field

         //* Refresh the menu that has just received focus. *
         if ( skfp->dlg )
            this->refreshCtrl ( skfp, skfp->fi, true ) ;
         //* Note that if no menu was closed, this window refresh is redundant.*
         else                             // refresh window interior
            this->RefreshWin ( false ) ;
         done = true ;                    // signal that update is complete
      }

      //* If current field IS a menu, but target IS NOT a menu *
      else if ( (fcptr->type == ftMB) && (ftptr->type != ftMB) )
      {
         //* Set current control, or control group as hidden, *
         //* and move to target control.                      *
         this->menuExit ( skfp, skfp->fi, ftrg ) ;
         done = true ;                    // signal that update is complete
      }

      //* If BOTH current and target fields are menus *
      else if ( (fcptr->type == ftMB) && (ftptr->type == ftMB) )
      {
         //* If both current and target controls *
         //* belong to the same menu group.      *
         if ( (fcptr->grp != noGROUP) && (fcptr->grp == ftptr->grp) )
         {
            //* If moving from one sub-menu to another sub-menu, set   *
            //* current menu to invisible. and target menu to visible. *
            //* If current control is a menu bar, it remains visible.  *
            if ( fcptr->hgt > 1 )
               this->setHidden ( skfp, fcur, true ) ;
            this->setVisible ( ftptr ) ;     // reset target's "hidden" flag
            skfp->fi = ftrg ;                // set 'fi' to target field
            this->RefreshWin ( false ) ;     // refresh window interior
         }

         //* Current and target ARE NOT members of the same group. *
         else
         {
            this->menuExit ( skfp, fcur, ftrg ) ;   // close the current menu/group
            this->refreshField ( skfp, skfp->fi ) ; // update target field
         }
         done = true ;
      }
   }     // (one or both fields is a menu)

   //* If target has not yet been refreshed, or *
   //* if target may be obscured by other data. *
   if ( ! done )
   {
      skfp->fi = ftrg ;                   // set the target index
      this->refreshField ( skfp, skfp->fi ) ; // update the display
   }
   return skfp->fi ;

}  //* End setInputFocus() *

//*************************
//*    Focus2NextField    *
//*************************
//********************************************************************************
//* Set input focus to the next field, and refresh field contents.               *
//* If already on the last field (index==fCnt-1), wrap around to the first       *
//* field of the array.                                                          *
//* The cursor is placed on the text insertion point of the target field.        *
//* Note on cursor visibility: See note in SetInputFocus().                      *
//* Note: Read-only fields and fields which are set as hidden cannot receive     *
//*       the input focus.                                                       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************
//* This is the ACWin-class pass-through to the private method.                  *
//********************************************************************************

short ACWin::Focus2NextField ( void )
{

   return ( (this->focus2NextField ( this->skf )) ) ;

}  //* End Focus2NextField() *

//*************************
//*    focus2NextField    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called by the public Focus2NextField() method and by the private ACDlg user  *
//* interface to move the input focus to the next available field.               *
//*                                                                              *
//* An "available" field is one which is neither set as read-only, nor set as    *
//* hidden.                                                                      *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object containing user-interface controls.*
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************

short ACWin::focus2NextField ( skForm* skfp )
{
   if ( skfp->fCnt > 1 )                  // must be multiple fields
   {
      short fcur = skfp->fi,              // index of field with focus
            ftrg = fcur ;                 // intex of target field

      ftrg = this->focus2np ( skfp, wcsTAB ) ; // find index of target control
      if ( ftrg != fcur )                 // if a target control was identified
         this->setInputFocus ( skfp, ftrg ) ;
      else if ( skfp->beep )              // no focus change
         this->acBeep () ;
   }
   return skfp->fi ;

}  //* End focus2NextField() *

//*************************
//*    Focus2PrevField    *
//*************************
//********************************************************************************
//* Set input focus to the previous field, and refresh field contents.           *
//* If already on the first field (index==zero), wrap around to the last         *
//* field of the array.                                                          *
//* The cursor is placed on the text insertion point of the target field.        *
//* Note on cursor visibility: See note in SetInputFocus().                      *
//* Note: Read-only fields and fields which are set as hidden cannot receive     *
//*       the input focus.                                                       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************

short ACWin::Focus2PrevField ( void )
{

   return ( (this->focus2PrevField ( this->skf )) ) ;

}  //* End Focus2PrevField() *

//*************************
//*    focus2PrevField    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called by the public Focus2PrevField() method and by the private ACDlg user  *
//* interface to move the input focus to the previous _available_ field.         *
//*                                                                              *
//* An "available" field is one which is neither set as read-only, nor set as    *
//* hidden.                                                                      *
//*                                                                              *
//* Input  : skfp  : pointer to skForm object containing user-interface controls.*
//*                                                                              *
//* Returns: index of field with focus                                           *
//********************************************************************************

short ACWin::focus2PrevField ( skForm* skfp )
{
   if ( skfp->fCnt > 1 )                  // must be multiple fields
   {
      short fcur = skfp->fi,              // index of field with focus
            ftrg = fcur ;                 // intex of target field

      ftrg = this->focus2np ( skfp, wcsSTAB ) ; // find index of target control

      //* If a target control was identified. *
      if ( ftrg != fcur )
         this->setInputFocus ( skfp, ftrg ) ;
      else if ( skfp->beep )              // no focus change
         this->acBeep () ;
   }
   return skfp->fi ;

}  //* End focus2PrevField() *

//*************************
//*       focus2np        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Scan the array of controls, starting at the current position (control with   *
//* focus) and moving in the specified direction until the next available        *
//* control is identified.                                                       *
//*                                                                              *
//* In this context, an "available" field is:                                    *
//* 1) 'ro' flag is reset                                                        *
//* 2a) 'hide' flag is reset OR                                                  *
//* 2b) current and target fields are members of the same group                  *
//*     (excluding the menubar)                                                  *
//*                                                                              *
//* Input  : skfp   : pointer to skForm containing user-interface controls.      *
//*          scandir: character indicating direction of scan                     *
//*                    wcsTAB  == scan forward                                   *
//*                    wcsSTAB == scan backward                                  *
//*          inGrp  : (optional, 'false' by default)                             *
//*                   If set, then if a member of the same group is found, it    *
//*                   will be returned, even if it is set as hidden.             *
//*                   The target's 'hide' flag WILL NOT be reset.                *
//*                                                                              *
//* Returns: index of available field                                            *
//********************************************************************************

short ACWin::focus2np ( skForm *skfp, wchar_t scanDir, bool inGrp )
{
   short fdef = skfp->fCnt,            // number of fields defined
         fcur = skfp->fi,              // index of field with focus
         ftrg = fcur ;                 // intex of target field
   if ( scanDir == wcsTAB )
   {
      do                                  // scan for the next accessible field
      {
         if ( ++ftrg >= fdef ) { ftrg = ZERO ; } // wrap-around to first field
         //* If field is accessible (neither read-only nor hidden). *
         //* OR is a member of the same (menu) group.               *
         if ( !skfp->fld[ftrg].ro )
         {
            if ( !skfp->fld[ftrg].hide ||
                 (inGrp && (skfp->fld[ftrg].grp != noGROUP) && 
                  (skfp->fld[ftrg].grp == (skfp->fld[fcur].grp))) )
            { break ; }
         }
      }
      while ( ftrg != fcur ) ;
   }
   else  // (scanDir == wcsSTAB)
   {
      do
      {
         if ( --ftrg < ZERO ) { ftrg = fdef - 1 ; } // wrap-around to last field
         //* If field is accessible (neither read-only nor hidden). *
         if ( !skfp->fld[ftrg].ro && !skfp->fld[ftrg].hide )
            break ;

         //* If field is accessible (neither read-only nor hidden). OR  *
         //* is a member of the same (menu) group (but not the menubar).*
         if ( !skfp->fld[ftrg].ro )
         {
            if ( !skfp->fld[ftrg].hide ||
                 ((skfp->fld[ftrg].grp != noGROUP) &&
                  (skfp->fld[ftrg].hgt > 1) &&
                  (skfp->fld[ftrg].grp == skfp->fld[fcur].grp)) )
            { break ; }
         }
      }
      while ( ftrg != fcur ) ;
   }
   return ftrg ;

}  //* End focus2np

//*************************
//*       InsertKey       *
//*************************
//********************************************************************************
//* Enable or disable user access to the Insert/Overstrike input mode when user  *
//* is editing a field, and specify the initial mode.                            *
//*                                                                              *
//* Insert/Overstrike toggle is active by default, and the initial mode is       *
//* Insert Mode. For some fields, or some kinds of data, however, it is useful   *
//* to set one mode or the other and lock out user access.                       *
//*                                                                              *
//* Optionally enable or disable cursor style change when Insert/Overstrike mode *
//* is toggled. This provides a visual cue to the user whether Insert mode or    *
//* Overstrike mode is active. This is common in word-processing programs and    *
//* text editors. Optionally, the cursor style for each mode may be specified    *
//* as a member of enum CurStyle (excluding csShow and csHide).                  *
//*                                                                              *
//* Input  : enable  : 'true'  == enable user access to Insert key               *
//*                               (this is the default)                          *
//*                    'false' == disable user access to Insert key              *
//*                               (the mode is locked by application)            *
//*          initMode: initial input mode                                        *
//*                    'true'  == Insert Mode                                    *
//*                    'false' == Overstrike Mode                                *
//*          styles  : (optional 'false' by default)                             *
//*                    Enable or disable automatic change of cursor style when   *
//*                    Insert/Overstrike is toggled.                             *
//*                    'true'  == enable cursor style change                     *
//*                    'false' == disable cursor style change                    *
//*          insStyle: (optional, istyleDFLT by default)                         *
//*                     cursor style for insert mode                             *
//*                     referenced only when the 'styles' flag is set            *
//*          ovrStyle: (optional, ostyleDFLT by default)                         *
//*                     cursor style for overstrike mode                         *
//*                     referenced only when the 'styles' flag is set            *
//*                                                                              *
//* Returns: 'true'  if Insert/Overstrike toggle enabled, else 'false'           *
//********************************************************************************

bool ACWin::InsertKey ( bool enable, bool initMode, bool styles, 
                        CurStyle insStyle, CurStyle ovrStyle )
{
   this->skf->togg = enable ;             // initialize the lock/unlock flag
   this->skf->ins  = initMode ;           // set the specified mode
   this->skf->insc = styles ;             // initialize the cursor-style flag

   switch ( insStyle )                    // set the Insert mode cursor style
   {
      case csBblock:
      case csDflt:
      case csSblock:
      case csBuline:
      case csSuline:
      case csBvbar:
      case csSvbar:
         this->skf->icur = insStyle ;
         break ;
      default:
         this->skf->icur = istyleDFLT ;
         break ;
   }
   switch ( ovrStyle )                    // set the Overstrike mode cursor style
   {
      case csBblock:
      case csDflt:
      case csSblock:
      case csBuline:
      case csSuline:
      case csBvbar:
      case csSvbar:
         this->skf->ocur = ovrStyle ;
         break ;
      default:
         this->skf->ocur = ostyleDFLT ;
         break ;
   }

   //* If enabled, set the current cursor style *
   if ( this->skf->insc != false )
      this->acSetCursorStyle ( (this->skf->ins ? this->skf->icur : this->skf->ocur) ) ;

   return this->skf->togg ;

}  //* End InsertKey() *

//*************************
//*     isInsertMode      *
//*************************
//********************************************************************************
//* Reports the current state of the skForm object's Insert/Overstrike flag.     *
//*                                                                              *
//* If set, Insert Mode:                                                         *
//* A character written to the target field will be inserted at the current      *
//* cursor position, which will push trailing text to the right.                 *
//* If reset, Overstrike Mode:                                                   *
//* A character written to the target field will replace (overwrite) the text    *
//* at the current cursor position.                                              *
//*                                                                              *
//* See also the InsertKey() method, above.                                      *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: current state of the Insert/Overstrike flag                         *
//********************************************************************************

bool ACWin::isInsertMode ( void )
{

   return ( this->skf->ins ) ;

}  //* End isInsertMode() *

//*************************
//*      AlertEnable      *
//*************************
//********************************************************************************
//* Enable or disable auditory alert when user presses an invalid key.           *
//*                                                                              *
//* Input  : enable : if 'true',  enable alert                                   *
//*                   if 'false', disable alert                                  *
//*                                                                              *
//* Returns: current state of the skForm 'beep' flag                             *
//********************************************************************************

bool ACWin::AlertEnable ( bool enable )
{

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

}  //* End AlertEnable() *

//*************************
//*       EditForm        *
//*************************
//********************************************************************************
//* Interactively edit the fields defined within the skForm object.              *
//*                                                                              *
//* Input  : focusFld : (by reference, initial value ignored) receives the index *
//*                     of the field with input focus on return.                 *
//*          exitField: (optional, default: -1) If specified, this is the index  *
//*                     of the ftPB (Pushbutton) control that, when pressed,     *
//*                     will cause the loop to terminate, and a return to caller.*
//*          exitChar : (optional, default: '\0') If specified, when this        *
//*                     character is received, the loop will terminate and       *
//*                     return to caller.                                        *
//*                     Note: If a callback method established which will        *
//*                           determine when to return to caller, then 'exitChar'*
//*                           should contain default value.                      *
//*                                                                              *
//* Returns: keycode of last key pressed                                         *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) Tab and Shift+Tab change fields (if multiple accessible fields defined).  *
//* 2) Left, Right, Home, End maneuver within the field with input focus.        *
//* 3) For multi-row fields, UpArrow and DownArrow keys maneuver within field.   *
//*    For single-row fields UpArrow and DownArrow keys are translated to        *
//*    Shift+Tab and Tab, respectively.                                          *
//* 4) Insert key toggles Insert/Overstrike input mode.                          *
//*    For some types of data entry it is advantageous to initialize the         *
//*    Insert/Overstrike flag, and then ignore the Insert keycode. This is       *
//*    handled by the 'togg' member which enables or disables recognition of     *
//*    of the Insert keycode.                                                    *
//* 5) Delete:                                                                   *
//*    Overwrite the character under the cursor by shifting all trailing         *
//*    characters left by the width of that character.                           *
//* 6) Backspace: The cursor is positioned one character to the left, and then   *
//*    that character is overwritten by shifting all trailing characters left    *
//*    by the width of that character.                                           *
//*                                                                              *
//* -- There is a design decision to be made whether to allow the input method   *
//*    (acRead()) to automatically echo printing characters to the display as    *
//*    soon as they are received.                                                *
//*    -- Display updates within the field-edit methods is simplified if         *
//*       automatic display update is disabled because it gives greater control  *
//*       over when (or if) the display is updated, for instance for GUI keycodes*
//*       which are not handled by an application-level callback method.         *
//*    -- Conversely, the application may want to interact with the user         *
//*       _outside_ of the edit methods, in which case the application will need *
//*       to _temporarily_ enable automatic echo of input.                       *
//*    Toggling between echo and non-echo modes is a non-trivial system          *
//*    configuration task which should not happen every few milliseconds because *
//*    the system will eventually become confused. (poor, brain-dead system :-)  *
//*    On entry, the soft-echo option is set to 'noEcho' and on exit is returned *
//*    to its previous value.                                                    *
//*                                                                              *
//* -- The cursor must stay within the display area of the field with            *
//*    input focus.                                                              *
//*                                                                              *
//* -- The cursor is positioned on a displayed character OR is positioned on     *
//*    the free column immediately following the last displayed character of     *
//*    the current row (if any). This position will generally reference either   *
//*    a newline character or the null terminator. The cursor may not be         *
//*    positioned in empty space beyond that point.                              *
//*                                                                              *
//* -- Doing the two-step:                                                       *
//*    For multi-row Textbox fields the wcsHOME and wcsEND special keycodes      *
//*    indicate a two-step operation.                                            *
//*    -- wcsHOME:                                                               *
//*       1) If cursor is not already at the left edge of the current row,       *
//*          move cursor to left edge of current row.                            *
//*       2) If cursor is already at the left edge of the current row, but       *
//*          not at the field origin, move cursor to field origin.               *
//*    -- wcsEND:                                                                *
//*       1) If cursor is not already at the right end of text for the current   *
//*          row, move cursor to the column after the last character displayed   *
//*          on the row, or if the row is fully occupied, the right edge of      *
//*          the row.                                                            *
//*       2) If cursor is already at the end of the current row, but not at      *
//*          the end of the displayed text, move cursor to end of displayed      *
//*          text.                                                               *
//*                                                                              *
//* -- Scrolling text within the field.                                          *
//*    For multi-row fields only, text is scrolled upward and downward within    *
//*    the field in response to the navigation keys: Up/Down/Left/Right/Home/End *
//*    PageUp/PageDown or the equivalent "selection" keys.                       *
//*                                                                              *
//*  -- Optional 'exitField' and 'exitChar' parameters:                          *
//*     There are three ways to exit the user-interface loop:                    *
//*      1) When the Pushbutton control specified by 'exitField' is pressed.     *
//*         Typically, a window will include an 'Exit', 'Quit', 'Close' or       *
//*         similar pushbutton that may be selected by the user to close the     *
//*         window. If defined, the index of this Pushbutton may be specified    *
//*         by the optional 'exitField' parameter. Then when user selects this   *
//*         control, the loop will terminate and control will be returned        *
//*         to caller. If 'exitField' contains the default value (-1), then      *
//*         the parameter will be ignored.                                       *
//*      2) When the character specified by 'exitChar' is received from the user.*
//*         The optional 'exitChar' parameter may be used to specify that when   *
//*         the specified keycode is received, the loop will terminate and       *
//*         control will be returned to caller. This is an experimental          *
//*         parameter, and thusfar has not proven to be useful; however,         *
//*         it remains a valid option.                                           *
//*      3) Via a signal from the callback method (see below).                   *
//*                                                                              *
//* -- Optional callback method:                                                 *
//*    If activated, the callback method is invoked each time the user presses   *
//*    a key. If the callback method returns the keycode it received, then we    *
//*    process the keycode according to local rules, If the callback method      *
//*    returns the null character ('\0'), it indicates that the keycode was      *
//*    handled at the application level.                                         *
//*    -- For certain keycodes, the callback method is invoked AFTER processing  *
//*       the keycode. These special case keycodes are:                          *
//*       1) wcsCOPY and wcsCUT                                                  *
//*          Copying/moving the data from the field to the clipboard must happen *
//*          before invoking the callback, so application can reference the      *
//*          clipboard data if desired.                                          *
//*       Additional keycodes may be added to this list as the need arises.      *
//*                                                                              *
//*    IMPORTANT NOTE:                                                           *
//*    If the callback method returns wcsCTL_4, it is the signal that EditForm() *
//*    should terminate the loop and return to caller.                           *
//*    -- Note that wcsCTL_4 is the ASCII code for the SIGQUIT signal, and will  *
//*       never be transmitted to the callback method as the 'wkey' parameter.   *
//*                                                                              *
//********************************************************************************

wchar_t ACWin::EditForm ( short& focusFld, short exitField, wchar_t exitChar )
{
   wchar_t wkey ;                         // user input (and return value)
   wchar_t cbKey ;                        // value returned from callback method
   wchar_t ncKey ;                        // value returned from normalkeyConvert method
   short hotIndx = this->skf->fCnt ;      // index of control invoked via hotkey
   FldType ftype ;                        // field type
   bool isSpecial ;                       // "special key" flag
   bool cbCalled = false ;                // prevent double-invocation of callback
   EchoOpt orig_echo = this->tset->inpEch ; // application's echo option

   //* Refresh the field with input focus. This sets the *
   //* color attributes and positions the visible cursor.*
   this->RefreshField ( this->skf->fi ) ;

   //* Validate the 'exitField' parameter. If specified, *
   //* it must reference a Pushbutton control.           *
   if ( exitField >= ZERO )
   {
      if ( (exitField >= this->skf->fCnt) || 
           (this->skf->fld[exitField].type != ftPB) )
      { exitField = -1 ; }
   }

   //* The 'noEcho' option is set during edit and returned *
   //* to caller's value on exit. (see note above)         *
   if ( this->tset->inpEch != noEcho )
      this->acSetEchoOption ( noEcho ) ;

   while ( (wkey = this->winRead ()) != exitChar )
   {
      cbCalled = false ;      // track whether callback invoked

      //*****************************************
      //* Compare to list of "hotkey keycodes". *
      //* Hotkeys set focus to associated field.*
      //*****************************************
      if ( bool((hotIndx = this->skpIsHotkey ( this->skf, wkey )) < this->skf->fCnt) )
      {
         FldType ftType = this->skf->fld[hotIndx].type ; // target field type
         ftype = this->skf->fld[this->skf->fi].type ;    // local copy of current field type

         //* If a valid hotkey has been defined, AND if     *
         //* specified target field is not current field,   *
         //* move the input focus to the associated field.  *
         //* (Target must not be set as read-only.)         *
         if ( hotIndx != this->skf->fi )
         {
            //* This invocation of the callback function alerts the  *
            //* application when a hotkey has been pressed.          *
            //* A typical use would be to enable access to target    *
            //* by resetting the 'ro' or 'hide' flag.                *
            //* Note, however, that application should refrain from  *
            //* interfereing with the automatic change of focus to   *
            //* the target control.                                  *
            if ( (this->callbkptr != NULL) && (ftType != ftPB) && (ftType != ftRB) &&
                 ((ftype != ftTB) ||
                 ((ftype == ftTB) && !((wkey == wcsCOPY) || (wkey == wcsCUT)))) )
            {
               cbKey = this->callbkptr ( this->skf->fi, wkey, false ) ;
               if ( cbKey == NULLCHAR ) { continue ; }      // keycode handled by caller
               else if ( cbKey == wcsCTL_4 ) { wkey = cbKey ; break ; }    // return to caller
               else if ( cbKey != wkey ) { wkey = cbKey ; } // substitute returned value
               cbCalled = true ;
            }
            if ( ! this->skf->fld[hotIndx].ro )
               this->setInputFocus ( this->skf, hotIndx ) ;
         }

         //* If new field has focus and if it is a binary-state *
         //* control, set 'wkey' to Enter, which indicates that *
         //* control has been "selected".                       *
         ftype = this->skf->fld[this->skf->fi].type ;
         if ( (this->skf->fi == hotIndx) && ((ftype == ftPB) || (ftype == ftRB)) )
            wkey = wcsENTER ;
         else
            continue ;
         // Programmer's Note: If the callback invocation causes the keycode 
         // to be invalidated, then setting focus and/or changing the binary 
         // state may no longer be the right thing to do. Unfortunately, the 
         // only way to avoid it is if callback return nullchar to indicate 
         // that NO further action is necessary.
      }

      //* If callback has been established, but not yet    *
      //* invoked, invoke it now.                          *
      //* Exceptions:                                      *
      //* 1) Copy/Cut operations require that data be on   *
      //*    the clipboard before the callback.            *
      //* 2) Note: Non-text controls associated with       *
      //*    external events require specific timing for   *
      //*    the events, so callback invoked below instead.*
      ftype = this->skf->fld[this->skf->fi].type ;
      if ( (this->callbkptr != NULL) && !cbCalled && 
           (!((ftype == ftTB) || (ftype == ftMB)) ||
             ((ftype == ftTB) && !((wkey == wcsCOPY) || (wkey == wcsCUT)))) )
      {
         cbKey = this->callbkptr ( this->skf->fi, wkey, false ) ;
         if ( cbKey == NULLCHAR ) { continue ; }      // keycode handled by caller
         else if ( cbKey == wcsCTL_4 ) { wkey = cbKey ; break ; }    // return to caller
         else if ( cbKey != wkey ) { wkey = cbKey ; } // substitute returned value 
         cbCalled = true ;
      }

      //**************************************************
      //* Compare keycode to list of "special keycodes". *
      //**************************************************
      if ( (isSpecial = this->specialkeyTest ( wkey )) )
      {
          //* Convert certain keycodes dependent upon the   *
          //* field definitions within the skForm object.   *
          if ( (wkey = this->specialkeyConvert ( this->skf, wkey )) == NULLCHAR )
             continue ;
      }

      //* If Textbox control, interpret the keycode received from user. *
      if ( (this->skf->fld[this->skf->fi].type == ftTB) )
      {
         //* For non-special keycodes that nevertheless require special *
         //* handling for ftTB controls, convert to a special key.      *
         if ( !isSpecial )
         {
            if ( (ncKey = this->normalkeyConvert ( this->skf, wkey )) != wkey )
            { wkey = ncKey ; isSpecial = true ; }
         }

         //**************************************
         //* Perform the operation associated   *
         //* with the specified special keycode.*
         //**************************************
         if ( isSpecial && (wkey != exitChar) )
         {
            this->specialkeyProc ( this->skf, wkey ) ;

            //* Special Case: For certain keycodes, the callback method *
            //* is called AFTER the keycode has been processed.         *
            if ( (this->callbkptr != NULL) && !cbCalled &&
                 ((wkey == wcsCOPY) || (wkey == wcsCUT)) )
            {
               cbKey = this->callbkptr ( this->skf->fi, wkey, false ) ;
               if ( cbKey == NULLCHAR ) { continue ; }    // keycode handled by caller
               else if ( cbKey == wcsCTL_4 ) { break ; }  // return to caller
            }
         }     // special key

         //********************************************************************
         //* Else if 'wkey' is a normal (graphing/printing) character, insert *
         //* the character into the field text and update the display.        *
         //* -- Note that if an unhandled control key (CTRL+A - CTRL+Z etc.)  *
         //*    or an unhandled GUI key (ALT+A - ALT+Z etc.), it will be      *
         //*    discarded and NOT processed as a graphing character.          *
         //********************************************************************
         else if ( (wkey != NEWLINE) && (wkey != NULLCHAR) && (wkey != exitChar) )
         { this->normalkeyProc ( this->skf, wkey ) ; }

         //* If an unrecognized or unhandled keycode: Alert user. *
         else if ( this->skf->beep )
            this->acBeep () ;

      }     // textbox fields

      //* If Scrollbox control *
      else if ( this->skf->fld[this->skf->fi].type == ftSB )
      {
         //**************************************
         //* Perform the operation associated   *
         //* with the specified special keycode.*
         //**************************************
         if ( isSpecial || (wkey == wcsENTER) || (wkey == wcsSPACE) )
         {
            this->specialkeyProc ( this->skf, wkey ) ;
         }

         //* If an unrecognized or unhandled keycode: Alert user. *
         else if ( this->skf->beep )
            this->acBeep () ;
      }  // scrollbox fields

      //* If Menubox control *
      else if ( this->skf->fld[this->skf->fi].type == ftMB )
      {
         //* Because processing the special key (Enter/Space) may *
         //* move the input focus, save a copy for the callback.  *
         short savedIndex = this->skf->fi ;

         //* If keycode is not a special key, it may be a menu-item     *
         //* hotkey. If so, move IP to the associated line item, then   *
         //* substitute wcsENTER key to signal that the item has been   *
         //* "selected".                                                *
         if ( !isSpecial && (wkey != wcsENTER) && (wkey != wcsSPACE) )
            wkey = this->menuHotkeyConvert ( &this->skf->fld[savedIndex], wkey ) ;

         //* Please Note: Even though all keycodes associated with   *
         //* ftMB controls are sent for "special" processing, menu   *
         //* controls process ONLY "special keys", NOT "normal"      *
         //* (graphing) keycodes. Exceptions:                        *
         //* a) wcsENTER and SPACE "select" the menu item, or in the *
         //*    case of Menubars, they open the sub-menu.            *
         //* b) "hotkeys" embedded within the individual menu items  *
         //*    are invoked via ordinary graphing keycodes. These    *
         //*    are interpreted as command codes which select the    *
         //*    menu item by moving IP to the specified menu item.   *
         //* c) wcsESCAPE causes the menu (or menu group) to close   *
         //*    without a selection.                                 *
         if ( isSpecial || (wkey == wcsENTER) || (wkey == wcsSPACE) || (wkey == wcsESCAPE) )
         {
            this->specialkeyProc ( this->skf, wkey ) ;

            //* Special Case: For certain keycodes, the callback method *
            //* is called AFTER the keycode has been processed.         *
            //* Note that callback should generally ignore wcsESCAPE    *
            //* keycode because menu has closed without a selection.    *
            if ( (this->callbkptr != NULL) && !cbCalled &&
                 ((wkey == wcsENTER) || (wkey == wcsSPACE) || (wkey == wcsESCAPE)) )
            {
               cbKey = this->callbkptr ( savedIndex, wkey, false ) ;
               if ( cbKey == NULLCHAR ) { continue ; }    // keycode handled by caller
               else if ( cbKey == wcsCTL_4 ) { break ; }  // return to caller
            }
         }
      }  // menubox controls

      //* If Pushbutton or Radiobutton control *
      else if ( (this->skf->fld[this->skf->fi].type == ftPB) || 
                (this->skf->fld[this->skf->fi].type == ftRB) )
      {
         //* The controls of this group process ONLY "special keys", *
         //* NOT "normal" (graphing) keycodes. Only text editor      *
         //* controls process "normal" keys.                         *
         //* Exceptions: wcsENTER and SPACE "select" the control.    *
         if ( isSpecial || (wkey == wcsENTER) || (wkey == wcsSPACE) )
         {
            //* Remember field with focus for post-processing callback invocation.*
            short focusIndex = this->skf->fi ;

            //**************************************
            //* Perform the operation associated   *
            //* with the special keycode.          *
            //**************************************
            if ( (this->specialkeyProc ( this->skf, wkey )) )
            {
               //* Special Case: For certain keycodes, the callback method *
               //* is called AFTER the keycode has been processed.         *
               //* This allows the callback to perform the operation       *
               //* associated with the control which has focus.            *
               if ( this->callbkptr != NULL && !cbCalled )
               {
                  cbKey = this->callbkptr ( focusIndex, wkey, false ) ;
                  if ( cbKey == NULLCHAR ) { continue ; }    // keycode handled by caller
                  else if ( cbKey == wcsCTL_4 ) { break ; }  // return to caller
               }

               //* If the 'exit-window' pushbutton has been selected, *
               //* we're done; return to caller.                      *
               if ( (exitField >= ZERO) && (focusIndex == exitField) && 
                    ((wkey == wcsENTER) || (wkey == wcsSPACE)) )
               { wkey = wcsCTL_4 ; break ; }
            }
            //* Keycode invalid in this context. *
            else if ( this->skf->beep ) { this->acBeep () ; }

         }     // special key
         else if ( this->skf->beep ) { this->acBeep () ; }
      }  // pushbutton and radiobutton controls

      //* Else is a control type which is currently not supported. *
      //*               (This should not happen.)                  *
      else
      { this->acBeep ( 2, 2 ) ; }

   }  // while()

   if ( this->tset->inpEch != orig_echo ) // return to original echo option
      this->acSetEchoOption ( orig_echo ) ;
   this->restoreAttributes () ;           // return to terminal attributes
   focusFld = this->skf->fi ;             // report the field which has focus
   return wkey ;                          // return the last key pressed

}  //* End EditForm() *

//*************************
//*    specialkeyTest     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Determine whether the specified keycode is one of the "special" keys.        *
//* This is a stub for calling the AnsiCmd-class method.                         *
//* a) The 'alert' parameter for the call is always 'false'.                     *
//* b) The 'eopt' parameter for the call is always 'softEchoA' i.e. test         *
//*    for all keycodes.                                                         *
//*                                                                              *
//* Input  : wkey : (by reference) keycode to be tested                          *
//*                                                                              *
//* Returns: 'true'  if keycode is one of the special keycodes,                  *
//*                  otherwise 'false'                                           *
//********************************************************************************
//* Programmer's Note:                                                           *
//* -- In a production build, the soft-echo option used for this test is         *
//*    always softEchoA (all special keycodes recognized), overriding the        *
//*    value specified by the 'tset->inpEch' member variable.                    *
//* -- However, when debugging code is enabled i.e. when the test methods are    *
//*    included in the build, 'tset->inpEch' specifies the soft-echo option      *
//*    used for this test. In this way, the test methods are allowed to modify   *
//*    the soft-echo option for the specific test being performed.               *
//*                                                                              *
//********************************************************************************

bool ACWin::specialkeyTest ( wchar_t& wkey ) const
{

   #if DEBUG_ANSICMD == 0
   EchoOpt echoAll = softEchoA ;    // all 'special' keys are recognized
   return ( (this->isSpecialKey ( wkey, false, &echoAll )) ) ;
   #else    // for debugging only (see note above)
   return ( (this->isSpecialKey ( wkey, false )) ) ;
   #endif   // DEBUG_ANSICMD

}  //* End specialkeyTest() *

//*************************
//*   specialkeyConvert   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called by EditForm() and editDialog() to determine whether the specified     *
//* 'special' keycode is to be converted based on the following criteria:        *
//*                                                                              *
//* A) For skForm objects with only one field, discard inter-field keycodes.     *
//*      wcsTAB    --> '\0' (nullchar)                                           *
//*      wcsSTAB   --> '\0' (nullchar)                                           *
//*      wcsPGUP   --> '\0' (nullchar)                                           *
//*      wcsPGDN   --> '\0' (nullchar)                                           *
//*      wcsUARROW --> '\0' (nullchar)                                           *
//*      wcsDARROW --> '\0' (nullchar)                                           *
//* B) For skForm objects with multiple fields, convert certain inter-field      *
//*    keycodes.                                                                 *
//*      wcsPGDN --> wcsTAB                                                      *
//*      wcsPGUP --> wcsSTAB                                                     *
//*    Exception: For multi-row text fields, scroll the next/previous            *
//*               "page" of text into view.                                      *
//* C) For single-row fields, convert certain keycodes which make no sense       *
//*    within a single-row field.                                                *
//*      wcsDARROW --> wcsTAB                                                    *
//*      wcsPGDN   --> wcsTAB                                                    *
//*      wcsAENTER --> wcsTAB                                                    *
//*      wcsUARROW --> wcsSTAB                                                   *
//*      wcsPGUP   --> wcsSTAB                                                   *
//*    For Pushbuttons and Radiobuttons:                                         *
//*      wcsRARROW --> wcsTAB                                                    *
//*      wcsLARROW --> wcsSTAB                                                   *
//* D) While within a multi-row field, wcsDARROW and wcsUARROW have variable     *
//*    meanings depending on the row referenced by the cursor position.          *
//*    If cur.row == zero, then:                                                 *
//*      wcsUARROW --> wcsSTAB                                                   *
//*    If cur.row == (hgt - 1), then:                                            *
//*      wcsDARROW --> wcsTAB                                                    *
//*    Otherwise wcsDARROW and wcsUARROW are returned unmodified.                *
//*                                                                              *
//* E) If the field is configured for "scroll-only" (Menus and Scrollbox         *
//*    controls), filter special keys to allow only a subset of the special      *
//*    keycodes:                                                                 *
//*    a) Only vertical scrolling is allowed, so other cursor-movement keycodes  *
//*       are discarded.                                                         *
//*    b) Because Home and End keys are two-step operations by default, these    *
//*       keycodes are converted to Ctrl+Home Ctrl+Home and Ctrl+End,            *
//*       respectively to make moves to bottom-of-text and top-of-text are a     *
//*       single operation.                                                      *
//*    c) Clipboard operations are not allowed, so select, copy, cut and paste   *
//*       keycodes are discarded.                                                *
//*    d) Text may not be modified, so backspace, delete and soft-enter keycodes *
//*       are also discarded.                                                    *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : 'special' key to test for conversion.                        *
//*                                                                              *
//* Returns: If keycode converted, returns converted keycode                     *
//*          If keycode does not meet conversion criteria, return null character *
//*          If keycode NOT converted, keycode is returned unmodified            *
//********************************************************************************

wchar_t ACWin::specialkeyConvert ( const skForm* skfp, wchar_t wkey )
{
   bool bleep = false ;                   // set for audible alert

   //* If only one field defined, then inter-field *
   //* keycodes are discarded.                     *
   if ( skfp->fCnt == 1 )
   {
      if ( (wkey == wcsTAB)  || (wkey == wcsSTAB) ||
           (wkey == wcsPGUP) || (wkey == wcsPGDN) )
      { wkey = NULLCHAR ; bleep = true ; } // keycode invalid in current context
   }

   //* Convert PgUp and PgDown. (see note above) *
   else if ( (wkey == wcsPGDN) || (wkey == wcsPGUP) )
   {
      if ( skfp->fld[skfp->fi].hgt == 1 ) 
      {
         if ( wkey == wcsPGDN ) { wkey = wcsTAB ; }
         else                   { wkey = wcsSTAB ; }
      }
   }

   //* Selection keys apply only to Textbox fields *
   if ( (wkey != NULLCHAR) && (skfp->fld[skfp->fi].type != ftTB) )
   {
      if ( (wkey == wcsSRIGHT) || (wkey == wcsSLEFT) || (wkey == wcsSELECT) ||
           (wkey == wcsSUP)    || (wkey == wcsSDOWN) || (wkey == wcsCOPY)   ||
           (wkey == wcsCUT)    || (wkey == wcsPASTE) || (wkey == wcsUNDO) )
      {
         wkey = NULLCHAR ;                // keycode invalid in current context
         bleep = true ;
      }
   }

   //* For single-row fields, convert UpArrow, DownArrow and SoftEnter. *
   //* This includes single-row textbox fields (ftTB), Pushbuttons,     *
   //* Radiobuttons, and Spinners,                                      *
   if ( (wkey != NULLCHAR) && (skfp->fld[skfp->fi].hgt == 1) )
   {
      FldType ftype = skfp->fld[skfp->fi].type ;

      //* For "menubars" i.e. ftMB && hgt==1, DownArrow opens sub-menu.*
      if ( (ftype == ftMB) && (wkey == wcsDARROW) )
      { /* DownArrow keycode is preserved (see below). */ }

      else if ( (wkey == wcsDARROW) ||
                ((wkey == wcsRARROW) && ((ftype == ftPB) || (ftype == ftRB))) )
      { wkey = wcsTAB ; }
      else if ( wkey == wcsAENTER ) { wkey = wcsTAB ; }
      else if ( ((wkey == wcsUARROW) && (ftype != ftMB)) ||
                ((wkey == wcsLARROW) && ((ftype == ftPB) || (ftype == ftRB))) )
      { wkey = wcsSTAB ; }
      //* Discard multi-row-field selection keycodes.*
      else if ( (wkey == wcsSUP) || (wkey == wcsSDOWN) )
      {
         wkey = NULLCHAR ;                // keycode invalid in current context
         bleep = true ;
      }
   }

   //* Menus (ftMB) and Scrolbox controls (ftSB) accept only   *
   //* vertical-scrolling keycodes and inter-field movement    *
   //* keycodes.                                               *
   //* a) Eliminate horizontal-scrolling keycodes.             *
   //* b) Eliminate 'selection' and clipboard keycodes.        *
   //* c) Eliminate editing keycodes (BkSp/Del, etc.).         *
   //* d) Convert Home/End keys to Ctrl+Home and Ctrl+End.     *
   //*    This bypasses the two-step sequence used by Textbox  *
   //*    fields.                                              *
   //* e) Convert right arrow and left arrow keys to Tab and   *
   //*    Shift+Tab, respectively.                             *
   if ( (wkey != NULLCHAR) && 
        ((skfp->fld[skfp->fi].type == ftMB) || 
         (skfp->fld[skfp->fi].type == ftSB)) )
   {
      switch ( wkey )
      {
         case wcsUARROW: //case wcsDARROW:
         case wcsPGUP:   case wcsPGDN:
         case wcsCHOME:  case wcsCEND:
         case wcsTAB:    case wcsSTAB:
            break ;
         case wcsRARROW:
            wkey = wcsTAB ;
            break ;
         case wcsLARROW:
            wkey = wcsSTAB ;
            break ;
         case wcsDARROW:
            //* For Menubars only, convert downArrow to Enter. *
            //* This opens the sub-menu.                       *
            if ( skfp->fld[skfp->fi].hgt == 1 )
               wkey = wcsENTER ;
            break ;
         case wcsHOME:   wkey = wcsCHOME ; break ;
         case wcsEND:    wkey = wcsCEND ;  break ;
         case wcsBKSP: case wcsDELETE: 
            bleep = true ;
            // fall through
         default:
            wkey = NULLCHAR ; break ;
      }
   }

   if ( skfp->beep && bleep )             // make a joyful? noise
      this->acBeep () ;

   return wkey ;

}  //* End specialkeyConvert() *

//*************************
//*   normalkeyConvert    *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called by EditForm() and editDialog() to determine whether the specified     *
//* 'normal' i.e. non-special keycode is to be converted based on the following  *
//* criteria:                                                                    *
//* A) Within a multi-row text field, the Enter key (wcsENTER) is converted      *
//*    to the Alt+Enter (wcsAENTER) "soft-enter" special keycode.                *
//*                                                                              *
//* B) For single-row text fields, the Enter key (newline keycode '\n' 0x0A)     *
//*    is converted to the Tab (wcsTAB) keycode.                                 *
//*                                                                              *
//* C) For Radiobuttons and Pushbuttons, the Space keycode is converted to       *
//*    wcsENTER which signals selection of the control with focus.               *
//*                                                                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : 'normal' key to test for conversion.                         *
//*                                                                              *
//* Returns: If keycode converted, returns converted keycode                     *
//*          If keycode NOT converted, keycode is returned unmodified            *
//********************************************************************************

wchar_t ACWin::normalkeyConvert ( const skForm* skfp, wchar_t wkey )
{
   skField* fptr = &skfp->fld[skfp->fi] ;
   if ( wkey == wcsENTER )
   {
      if ( fptr->type == ftTB )
      {
         if ( fptr->hgt == 1 ) { wkey = wcsTAB ; }
         else                  { wkey = wcsAENTER ; }
      }
   }

   else if ( wkey == wcsSPACE )
   {
      if ( (fptr->type == ftPB) || (fptr->type == ftRB) )
         wkey = wcsENTER ;
   }

   return wkey ;

}  //* End normalkeyConvert() *

//*************************
//*   menuHotkeyConvert   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* For menu controls, if the menu item was selected via hotkey, locate the menu *
//* item and set IP to that item. The application can request the selected item  *
//* via the skField-class getItem() method. If a valid hotkey character was      *
//* specified, the wcsENTER keycode is returned, so the item selection will be   *
//* interpreted as having been selected via the Enter key.                       *
//*                                                                              *
//* Note that comparison is case insensitive.                                    *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be decoded.                                       *
//*                                                                              *
//* Returns: wcsENTER if the specified keycode was successfully converted to     *
//*                   a menu-item number                                         *
//*          Else, the keycode is returned unmodified.                           *
//********************************************************************************

wchar_t ACWin::menuHotkeyConvert ( skField *fptr, wchar_t wkey )
{
   const wchar_t* const ULINE = L"\x1B[4m" ; // aesUNDERLINE escape sequence
   gString gs ;                              // text analysis
   wchar_t testChar = tolower ( wkey, std::locale() ) ; // lower-case test char
   wchar_t hotChar ;                         // line-item hotkey
   int wi ;                                  // character index

   for ( short item = ZERO ; item < fptr->hgt ; ++item )
   {
      gs = &fptr->txt.gstr()[fptr->mrp.roff[item]] ; // isolate the line item
      if ( (wi = gs.find( NEWLINE )) >= ZERO )
         gs.limitChars( wi ) ;
      if ( (wi = gs.after( ULINE )) > ZERO ) // scan for the underline escape sequence
      {
         hotChar = tolower ( gs.gstr()[wi], std::locale() ) ;
         if ( hotChar == testChar )          // if hotkey matches
         {
            //* Set IP at the beginning of the indexed line. *
            fptr->ip = fptr->mrp.roff[item] ;
            this->parseFieldText ( fptr, 1 ) ;
            wkey = wcsENTER ;
            break ;
         }
      }
   }
   return wkey ;

}  //* End menuHotkeyConvert() *

//*************************
//*    specialkeyProc     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Execute the operation associated with the specified "special" key.           *
//*                                                                              *
//* The target field is defined as the skField object referenced by skfp->fi.    *
//*                                                                              *
//* This is a stub which directs execution flow to the method associated with    *
//* target-field type. The field-specific methods SHOULD NOT be called directly. *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: 'true'  if special keycode processed                                *
//*          'false  if special keycode cannot be applied to the target control  *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//*                                                                              *
//********************************************************************************

bool ACWin::specialkeyProc ( skForm* skfp, wchar_t wkey )
{
   bool proc = true ;                     // return value

   switch ( skfp->fld[skfp->fi].type )
   {
      case ftTB:                          // text-editing controls
         this->specialkeyProcText ( skfp, wkey ) ;
         break ;
      case ftPB:                          // pushbutton controls
      case ftRB:                          // radiobutton controls
      case ftSB:                          // scrollbox controls
      case ftMB:                          // menubox controls
        proc = this->specialkeyProcCtrl ( skfp, wkey ) ;
         break ;
      default:                            // other field types not yet supported
         break ;
   }
   return proc ;

}  //* End specialkeyProc() *

//*************************
//*  specialkeyProcText   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Execute the operation associated with the specified "special" key.           *
//* This method handles key input for Textbox controls ONLY (ftTB).              *
//*                                                                              *
//* The target field is defined as the skField object referenced by the          *
//* 'fi' (index) into the skForm's fld[] array.                                  *
//*      Example: skField *fptr = &this->skf->fld[this->skf->fi] ;               *
//*                                                                              *
//* 1) For cursor movements, update the skForm 'ip' (insertion point) and        *
//*    'cur' (cursor offset) members. If the new 'ip' references a position      *
//*    outside the visible field, scroll the text as needed, then update the     *
//*    visible cursor position.                                                  *
//*    Up, Down, Left, Right, Home, End keys.                                    *
//*    Note on DownArrow and UpArrow keys: see Inter-field Movement, below.      *
//*                                                                              *
//* 2) For text-edit keys:                                                       *
//*    Delete:                                                                   *
//*    Overwrite the character referenced by 'ip' by shifting all trailing       *
//*    characters left by the width of that character.                           *
//*    -- If the field contains 'selected' text, delete the selected text.       *
//*                                                                              *
//*    Backspace:                                                                *
//*    Shift the 'ip' one character to the left, then overwrite that             *
//*    character by shifting all trailing characters left by the width of        *
//*    that character.                                                           *
//*                                                                              *
//*    Insert:                                                                   *
//*    Toggle the 'ins' Insert/Overstrike flag, and if enabled, toggle the       *
//*    cursor style.                                                             *
//*                                                                              *
//*    Soft Enter:                                                               *
//*    In multi-row fields only, reformat the data by inserting a newline at     *
//*    'ip' and advancing 'ip' to the beginning of the next row.                 *
//*                                                                              *
//* 3) Inter-field Movement:                                                     *
//*    Tab and Shift+Tab:                                                        *
//*    If multiple fields are defined (fCnt > 1), then move to the next or       *
//*    previous field, respectively.                                             *
//*                                                                              *
//*    PageDown and PageUp:                                                      *
//*    Currently, these keycodes perform the same operation as Tab and           *
//*    Shift+Tab, respectively.                                                  *
//*                                                                              *
//*    DownArrow and UpArrow:                                                    *
//*    -- For single-row fields, these keycodes perform the same operation       *
//*       as Tab and Shift+Tab, respectively.                                    *
//*    -- For multi-row fields, these keycodes move one row down or up,          *
//*       respectively, UNLESS the field contains no additional rows(s) in       *
//*       the indicated direction; so in that case these keycodes perform the    *
//*       same operation as Tab and Shift+Tab, respectively.                     *
//*                                                                              *
//* 4) Clipboard access:                                                         *
//*    The "clipboard" is a two-layer object. The "local clipboard" is a static  *
//*    buffer within the ACWin class, while the system clipboard requires        *
//*    an interface between the application and the system.                      *
//*                                                                              *
//*    Ctrl+C == Copy to clipboard                                               *
//*    Ctrl+X == Cut to clipboard                                                *
//*    These commands copy (or move) data from the current field to the static   *
//*    local buffer. If the system interface is available, then the data in the  *
//*    static buffer is copied to the system clipboard.                          *
//*     a) If no data have been selected, then all the contents of the field     *
//*        will be copied to the clipboard.                                      *
//*         (Think about this: This is the functionality within our NCurses    ) *
//*         (library, but it may not be the right solution for AnsiCmd library.) *
//*     b) If some or all the data in the field have been "selected," then only  *
//*        that data will be copied.                                             *
//*    See below for a discussion of text selections.                            *
//*                                                                              *
//*    Ctrl+V == Paste from clipboard                                            *
//*    If the system interface is available AND if the data on the system        *
//*    clipboard is text data, then first, the data will be copied to the local  *
//*    clipboard buffer. The contents of the local buffer will then be copied    *
//*    to the current field.                                                     *
//*                                                                              *
//* 5) Text selection:                                                           *
//*    Specific key combinations select (or de-select) text within the field.    *
//*    Selected text is visually indicated by the underline attribute being used *
//*    to display that text.                                                     *
//*                                                                              *
//*    Alt+A       : Select all text in the current field.                       *
//*                  If text in the field is already selected, then Alt+A        *
//*                  de-selects the text.                                        *
//*    Shift+Right : Select the character under the cursor and the character     *
//*                  to the right of the current cursor position.                *
//*    Shift+Left  : Select the character under the cursor and the character     *
//*                  to the left of the current cursor position.                 *
//*    Shift+Up    : For multi-row fields only, select text between the current  *
//*                  position and the corresponding position (if any) on the row *
//*                  above.                                                      *
//*    Shift+Down  : For multi-row fields only, select text between the current  *
//*                  position and the corresponding position (if any) on the row *
//*                  below.                                                      *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//*                                                                              *
//********************************************************************************

void ACWin::specialkeyProcText ( skForm* skfp, wchar_t wkey )
{
   #define DEBUG_SKP (0)            // for debugging only

   skField *fptr = &skfp->fld[skfp->fi] ; // pointer to field with focus
   gString gsParse ;                // received parsed display text
   WinPos  eorOff,                  // offset to end-of-row
           eotOff ;                 // offset to end-of-text
   wchar_t prevkey = wkey ;         // temp storage for keycode
   bool    badKey = false ;         // 'true' if keycode invalid in this context

   //********************************************
   //* Form-level, inter-field special keycodes *
   //********************************************
   if ( wkey == wcsTAB )                  //** TAB key - move to next field
   {
      if ( skfp->fCnt > 1 )
         this->focus2NextField ( skfp ) ;
      else { badKey = true ; }            // no additional fields
   }

   else if ( wkey == wcsSTAB )            //** SHIFT+TAB key - move to previous field
   {
      if ( skfp->fCnt > 1 )
         this->focus2PrevField ( skfp ) ;
      else { badKey = true ; }            // no additional fields
   }

   else if ( wkey == wcsINSERT )          //** INSERT Key **
   {
      //* Toggle the Insert/Overstrike flag.*
      if ( skfp->togg )                   // if toggle enabled
      {
         skfp->ins = skfp->ins ? false : true ;

         //* If enabled, also toggle the style of the visible cursor.*
         if ( skfp->insc )
            this->acSetCursorStyle ( (skfp->ins ? skfp->icur : skfp->ocur) ) ;
      }
   }

   //***********************************
   //* Field-specific special keycodes *
   //***********************************
   else
   {
      //* If a block of text is currently selected, and:       *
      //*  1) If current key is not a selection keycode, _or_  *
      //*  2) If current key is a selection keycode, but the   *
      //*     selection is not in the field with focus,        *
      //* then release the selection.                          *
      //* Special Case: If Backspace or Delete key, selection  *
      //*               will be deleted by called method.      *
      //* Very Special Case: If Backspace _and_ if IP is to    *
      //*               the left of the selection, then        *
      //*               release the selection.                 *
      if ( skfp->seli < skfp->fCnt )
      {
         if ( !(wkey == wcsSRIGHT || wkey == wcsSLEFT || wkey == wcsSELECT || 
                wkey == wcsCOPY   || wkey == wcsPASTE || wkey == wcsCUT    || 
                wkey == wcsSUP    || wkey == wcsSDOWN || 
                wkey == wcsDELETE || wkey == wcsBKSP)
              ||
               ((skfp->seli != skfp->fi) &&
                ((wkey == wcsSELECT) || (wkey == wcsSRIGHT) || (wkey == wcsSLEFT) ||
                (wkey == wcsSUP) || (wkey == wcsSDOWN)))
              ||
              ((wkey == wcsBKSP) && (skfp->fld[skfp->fi].ip < skfp->fld[skfp->fi].sela))
            )
         { this->skpClearSelection ( skfp ) ; }
      }

      //********************************
      //* Special keys for:            *
      //* a) single-row text fields    *
      //* b) multi-row text fields     *
      //*    formatted for floShift.   *
      //********************************
      if ( fptr->flow == floShift )
         this->skpShiftedText ( skfp, wkey ) ;

      //**************************************
      //* Special keys for multi-row text    *
      //* fields not formatted for floShift. *
      //**************************************
      else
         this->skpMultirow ( skfp, wkey ) ;

   }     // field-specific keycodes

   //* Unknown "special" keycode. This should never happen.*
   if ( badKey && skfp->beep )
      this->acBeep() ;

   //* Remember the keycode that was just processed *
   skfp->pKey = prevkey ;

}  //* End specialkeyProcText() *

//*************************
//*  specialkeyProcCtrl   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Execute the operation associated with the specified "special" key.           *
//*                                                                              *
//* The target field is defined as the skField object referenced by the          *
//* 'fi' (index) into the skForm's fld[] array.                                  *
//*      Example: skField *fptr = &this->skf->fld[this->skf->fi] ;               *
//*                                                                              *
//* This method handles key input to all controls EXCEPT the Textbox controls:   *
//*  1) ftPB - Pushbuttons                                                       *
//*     a) wcsENTER and SPACE keycodes activate the control.                     *
//*     b) The hotkey (if any) assigned to the control cause the focus to move   *
//*        to the associated control sends a wcsENTER in wkey.                   *
//*     c) Arrow keys:                                                           *
//*        wcsTAB,  wcsRARROW and wcsDARROW move focus to the next field.        *
//*        wcsSTAB, wcsLARROW and wcsUARROW move focus to the previous field.    *
//*     d) All other keycodes will be rejected.                                  *
//*                                                                              *
//*  2) ftRB - Radiobuttons                                                      *
//*     a) wcsENTER and SPACE keycodes activate the control.                     *
//*     b) The hotkey (if any) assigned to the control cause the focus to move   *
//*        to the associated control sends a wcsENTER in wkey.                   *
//*     b) Arrow keys:                                                           *
//*        wcsTAB,  wcsRARROW and wcsDARROW move focus to the next field.        *
//*        wcsSTAB, wcsLARROW and wcsUARROW move focus to the previous field.    *
//*                                                                              *
//*  3) ftMB - Menubox                                                           *
//*     a) Menu controls are configured as stand-alone (context) menus, or as    *
//*        members of a menubar group.                                           *
//*     b) The primary differences between menus and other controls are that:    *
//*        -- they are invisible when they do not have input focus, and          *
//*        -- each line item of a menu can be tied to an external event which    *
//*           when selected, must be handled at the application level.           *
//*     c) In general, the menu becomes visible when it receives the input focus,*
//*        and invisible when it loses focus.                                    *
//*        There are exceptions to the visible/invisible state, related either   *
//*        to the "menubar" which is a single-row menu which manages a menu      *
//*        group, or a context menu which has been configured as always visible. *
//*        Entering a menu control:                                              *
//*        -- When entering a locked context menu, the IP stays where it was,    *
//*           and the field is refreshed with the row containing the IP is       *
//*           displayed using reversed fgnd/bgnd.                                *
//         -- When entering other menu types and configurations, IP is           *
//*           reinitialized to top/first menu item.                              *
//*        Exiting a menu control:                                               *
//*        -- When leaving a locked Menubar, the token highlight is removed from *
//*           the displayed text.                                                *
//*        -- When leaving a locked context menu, it remains in its current      *
//*           state, and focus moves to the new target control.                  *
//*           -- If an item selection was made, then on exit, the selected item  *
//*              will be displayed using an alternate font style.                *
//*        -- Other menus go invisible when they lose focus.                     *
//*     d) Vertical scrolling through menus is supported, but _not_ horizontal   *
//*        scrolling.                                                            *
//*     e) The line item is selected via the Enter or Space keys.                *
//*     f) If the Escape key is received, the menu will be closed without a      *
//*        selection.                                                            *
//*     g) Each line item may optionally be assigned a "hotkey" for selection    *
//*        of that item. The hotkey is indicated by the character displayed as   *
//*        underlined. Unlike hotkeys assigned to individual controls, the menu  *
//*        hotkeys are invoked via ordinary graphing keycodes which correspond   *
//*        to the underlined characters.                                         *
//*        -- Caller should have interpreted the intra-menu hotkeys, so we       *
//*           should not see them here.                                          *
//*                                                                              *
//*  4) ftSB - Scrollbox                                                         *
//*     A Scrollbox is a special case of the Textbox control. Data may be        *
//*     scrolled vertically (but not horizontally), and may not be edited        *
//*     by the user.                                                             *
//*                                                                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: 'true'  if special keycode processed                                *
//*          'false  if special keycode cannot be applied to the target control  *
//*                  in the current context                                      *
//********************************************************************************
//* Note: This method should not be called for text fields; but we check anyway. *
//*                                                                              *
//*                                                                              *
//********************************************************************************

bool ACWin::specialkeyProcCtrl ( skForm* skfp, wchar_t wkey )
{
   const char* const MalformedText = " Menu Group Is Malformed - See Documentation " ;
   const acAttr MalformedAttr = acAttr(acaFGb_GREY | acaBGb_RED | acaBLINK) ;

   skField *fptr = &skfp->fld[skfp->fi] ; // pointer to field with focus
   FldType type  = fptr->type ;     // field type
   wchar_t prevkey = wkey ;         // temp storage for keycode
   wchar_t group ;                  // ID for control group
   bool    goodKey = false ;        // return value

   //* The Insert/Overstrike key is not associated with any field.*
   //* If toggle is enabled, toggle the Insert/Overstrike flag.   *
   if ( (wkey == wcsINSERT) && skfp->togg )
   {
      skfp->ins = skfp->ins ? false : true ;
      //* If enabled, also toggle the style of the visible cursor.*
      if ( skfp->insc )
         this->acSetCursorStyle ( (skfp->ins ? skfp->icur : skfp->ocur) ) ;
      goodKey = true ;
   }

   //* Menu controls interpret keycodes differently *
   //* from other types of controls.                *
   else if ( fptr->type == ftMB )
   {
      //*************************
      //* Space and Enter keys: *
      //*************************
      if ( (wkey == wcsSPACE) || (wkey == wcsENTER) )
      {
         //* For menubars, open the menu associated with the   *
         //* highlighted token. The menubar remains visible.   *
         //* NOTE: It is assumed that the menus following the  *
         //* menubar are correctly sequenced to correspond to  *
         //* the tokens of the menubar. If not, it is an       *
         //* application error.                                *
         if ( fptr->hgt == 1 )
         {
            //* The highlighted token ('smi') indicates the *
            //* sub-menu associated with that token.        *
            //* The target field must be a menu (ftMB) and  *
            //* must be a member of the menubar's group.    *
            int ip = fptr->ip,            // temporary copy of IP
                smi ;                     // receives sub-menu index
            menubarToken ( wkey, fptr->txt.gstr(), ip, smi ) ;

            //* Open the sub-menu *
            if ( ((smi += skfp->fi) < skfp->fCnt) && (skfp->fld[smi].type == ftMB) &&
                 (skfp->fld[skfp->fi].grp == skfp->fld[smi].grp) )
            {
               this->setVisible ( &skfp->fld[smi] ) ;
               this->setInputFocus ( skfp, smi ) ;
               goodKey = true ;
            }
            //* Else, is an application error. Complain. *
            else
            {
               // Programmer's Note: If neither menubar nor target is a member of a group, 
               // the test will be inaccurate. Do we need to enforce that a menubar is in 
               // a designated group? And if so, does a menu group have to contain more 
               // than one member? For now, we just complain about the error.
               this->Footnote ( MalformedText, ZERO, MalformedAttr, true ) ;
               this->setCursor2IP ( skfp ) ; this->acBeep ( 2, 2 ) ;
            }
         }

         //* Multi-item menu: Either a menubar sub-menu or a context menu. *
         //* User has "selected" a menu item. The application's callback   *
         //* (if defined) should have executed any operation associated    *
         //* with the menu item; so if not locked visible, close the menu  *
         //* or menu group.                                                *
         //* Note: if menu is locked in the visible state, selected item   *
         //* number will be saved.                                         *
         else
         {
            this->menuExit ( skfp, skfp->fi, (this->focus2np ( skfp, wcsTAB )), true ) ;
            goodKey = true ;
         }
      }  // (wcsSPACE||wcsENTER)

      //*************
      //* wcsESCAPE *
      //*************
      else if ( wkey == wcsESCAPE )
      {
         //* When user presses the Escape key, the menu  *
         //* (and associated menu group, if any) will    *
         //* lose focus; and if not locked in visible    *
         //* state, the menu(s) will be closed.          *
         this->menuExit ( skfp, skfp->fi, (this->focus2np ( skfp, wcsTAB )) ) ;
         goodKey = true ;
      }

      //***********************
      //* wcsTAB || wcsRARROW *
      //***********************
      else if ( (wkey == wcsTAB) || (wkey == wcsRARROW) )
      {
         //* If field is a menubar, then Tab and Right arrows move cursor  *
         //* position (IP) among the tokens, or move to next field when on *
         //* last token. When menubar loses focus, it is closed (hidden)   *
         //* unless it has been locked open.                               *
         if ( fptr->hgt == 1 )
         {
            int ip = fptr->ip,            // temporary copy of IP
                smi ;                     // receives sub-menu index
            if ( (menubarToken ( wkey, fptr->txt.gstr(), ip, smi )) )
            { fptr->ip = ip ; this->refreshCtrl ( skfp, skfp->fi, true ) ; }
            else
               this->menuExit ( skfp, skfp->fi, (this->focus2np ( skfp, wcsTAB )) ) ;
            goodKey = true ;
         }     // (hgt==1)

         //* If field is a menu associated with a menu group, then      *
         //* Tab and Right arrow move to the next menu of the group     *
         //* (if any), or else to the next available (non-group) field  *
         //* and the menu is closed (hidden) unless locked visible.     *
         else
         {
            short fcur = skfp->fi,
                  ftrg = this->focus2np ( skfp, wcsTAB, true ) ;
            skField *ftptr = &skfp->fld[ftrg] ;
            wchar_t cgrp = fptr->grp, 
                    tgrp = ftptr->grp ;

            //* If target is a member of the same menu group *
            if ( (cgrp != noGROUP) && (cgrp == tgrp) )
            {
               this->setHidden ( skfp, fcur, true ) ; // set current field invisible
               //* If target is set invisible, make it visible. *
               if ( ftptr->hide )
                  this->setVisible ( ftptr ) ;
               skfp->fi = ftrg ;                      // set focus on target field
               this->RefreshWin ( false ) ;  // redraw window erasing current control
               goodKey = true ;
            }
            else
            {
               //* Exit the menu (or group) moving to the right. *
               this->menuExit ( skfp, fcur, (this->focus2np ( skfp, wcsTAB )) ) ;
               goodKey = true ;
            }
         }
      }     // (tab||right-arrow)

      //************************
      //* wcsSTAB || wcsLARROW *
      //************************
      else if ( (wkey == wcsSTAB) || (wkey == wcsLARROW) )
      {
         //* If field is a menubar, then Shift+Tab and Left arrow move     *
         //* cursor position (IP) among the tokens, or move to previous    *
         //* field when on first token. When menubar loses focus, it is    *
         //* closed (hidden) unless it has been locked open.               *
         if ( fptr->hgt == 1 )
         {
            int ip = fptr->ip,            // temporary copy of IP
                smi ;                     // receives sub-menu index
            if ( (menubarToken ( wkey, fptr->txt.gstr(), ip, smi )) )
            { fptr->ip = ip ; this->refreshCtrl ( skfp, skfp->fi, true ) ; }
            else
               this->menuExit ( skfp, skfp->fi, (this->focus2np ( skfp, wcsSTAB )) ) ;
            goodKey = true ;
         }

         //* If field is a menu associated with a menu group, then         *
         //* Shift+Tab and Left arrow move to the previous menu of the     *
         //* group (if any), or else to the previous available (non-group) *
         //* field and the menu is closed (hidden) unless locked visible.  *
         else  // (hgt > 1)
         {
            short fcur = skfp->fi,
                  ftrg = this->focus2np ( skfp, wcsSTAB, true ) ;
            skField *ftptr = &skfp->fld[ftrg] ;
            wchar_t cgrp = fptr->grp, 
                    tgrp = ftptr->grp ;

            //* If target is a member of the same menu group *
            if ( (cgrp != noGROUP) && (cgrp == tgrp) )
            {
               //* If calculated target is the menubar, *
               //* continue to previous control.        *
               if ( ftptr->hgt == 1 )
               {
                  skfp->fi = ftrg ;    // set menubar as field with focus
                  this->menuExit ( skfp, skfp->fi, (this->focus2np ( skfp, wcsSTAB )) ) ;
               }
               //* Target is another sub-menu of the group *
               else
               {
                  this->setHidden ( skfp, fcur, true ) ; // set current field invisible
                  //* If target is set invisible, make it visible. *
                  if ( ftptr->hide )
                     this->setVisible ( ftptr ) ;
                  skfp->fi = ftrg ;                      // set focus on target field
               }
               this->RefreshWin ( false ) ;  // redraw window erasing current control
               goodKey = true ;
            }
            else
            {
               //* Note: If fcur field is group member, is will have   *
               //* been handled above, so just move to previous field. *
               this->setHidden ( skfp, fcur, true ) ; // set current field invisible
               this->focus2PrevField ( skfp ) ;
               goodKey = true ;
            }
         }
      }     // (shift+tab || left-arrow)

      //********************************************
      //* wcsDARROW                                *
      //* Scroll downward through menu line items. *
      //********************************************
      else if ( wkey == wcsDARROW )
      {
         if ( fptr->grp != noGROUP )      // if menu is a group member
         {
            //* If the menubar has focus, the DownArrow has the same *
            //* functionality as the Enter key. Open the sub-menu.   *
            if ( fptr->hgt == 1 )         // if a Menubar
            {
               int ip = fptr->ip,            // temporary copy of IP
                   smi ;                     // receives sub-menu index
               menubarToken ( wcsENTER, fptr->txt.gstr(), ip, smi ) ;
               if ( ((smi += skfp->fi) < skfp->fCnt) && (skfp->fld[smi].type == ftMB) &&
                    (skfp->fld[skfp->fi].grp == skfp->fld[smi].grp) )
               {
                  this->setVisible ( &skfp->fld[smi] ) ;
                  this->setInputFocus ( skfp, smi ) ;
                  goodKey = true ;
               }
               else  // application error (see above)
               {
                  this->Footnote ( MalformedText, ZERO, MalformedAttr, true ) ;
                  this->setCursor2IP ( skfp ) ; this->acBeep ( 2, 2 ) ;
               }
            }
            else                          // if a sub-menu within the group
            {
               //* Target rows exclude the bottom border. *
               if ( fptr->mrp.rcur < (fptr->mrp.rlst - 1) )
                  fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
               else
                  fptr->ip = ZERO ;       // wrap-around to top row
            }
         }
         else                             // if a stand-alone menu
         {
            //* Target rows exclude the top and bottom borders. *
            if ( fptr->mrp.rcur < (fptr->mrp.rlst - 1) )
               fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
            else                          // wrap-around to top row
               fptr->ip = fptr->mrp.roff[1] ;
         }
         if ( ! goodKey )
         {
            this->parseFieldText ( fptr, 1 ) ;           // update field stats
            this->refreshCtrl ( skfp, skfp->fi, true ) ; // refresh the control
            goodKey = true ;
         }
      }     // (wcsDARROW)

      //******************************************
      //* wcsUARROW                              *
      //* Scroll upward through menu line items. *
      //******************************************
      else if ( wkey == wcsUARROW )
      {
         if ( fptr->grp != noGROUP )      // if menu is a group member
         {
            if ( fptr->hgt == 1 )         // if a Menubar
            {
               //* Close menubar and move to previous control. *
               if ( !fptr->lock )         // if not 'lock', set menubar as invisible
               {
                  this->setHidden ( skfp, skfp->fi, true ) ;// set menubar invisible
                  skfp->fi = this->focus2np ( skfp, wcsSTAB ) ; // set fi to previous (available) field
                  this->RefreshWin ( false ) ; // refresh window interior
               }
               else                       // advance the focus to previous field
                  this->focus2PrevField ( skfp ) ;
               goodKey = true ;
            }
            else                          // if a sub-menu within the group
            {
               //* Target rows exclude the bottom border. *
               if ( fptr->mrp.rcur > ZERO )
                  fptr->ip = fptr->mrp.roff[fptr->mrp.rcur - 1] ;
               else                       // wrap-around to bottom row
                  fptr->ip = fptr->mrp.roff[fptr->mrp.rlst - 1] ;
            }
         }
         else                             // if a stand-alone menu
         {
            //* Target rows exclude the top and bottom borders. *
            if ( fptr->mrp.rcur > 1 )
               fptr->ip = fptr->mrp.roff[fptr->mrp.rcur - 1] ;
            else
               fptr->ip = fptr->mrp.roff[fptr->mrp.rlst - 1] ; // wrap-around to bottom row
         }
         if ( ! goodKey )
         {
            this->parseFieldText ( fptr, 1 ) ;           // update field stats
            this->refreshCtrl ( skfp, skfp->fi, true ) ; // refresh the control
            goodKey = true ;
         }
      }     // (wcsUARROW)

      //* The wcsHOME/wcsCHOME/wcsPGUP keycodes all go to top of menu. *
      else if ( (wkey == wcsHOME) || (wkey == wcsCHOME) || (wkey == wcsPGUP) )
      {
         short trgIp = fptr->grp == noGROUP ? fptr->mrp.roff[1]: ZERO ;
         if ( fptr->ip != trgIp )
         {
            fptr->ip = trgIp ;
            this->parseFieldText ( fptr, 1 ) ;           // update field stats
            this->refreshCtrl ( skfp, skfp->fi, true ) ; // refresh the control
            goodKey = true ;
         }
      }

      //* The wcsEND/wcsCEND/wcsPGDN keycodes all go to bottom of menu. *
      else if ( (wkey == wcsEND) || (wkey == wcsCEND) || (wkey == wcsPGDN) )
      {
         if ( fptr->ip != fptr->mrp.roff[fptr->mrp.rlst - 1] )
         {
            fptr->ip = fptr->mrp.roff[fptr->mrp.rlst - 1] ;
            this->parseFieldText ( fptr, 1 ) ;           // update field stats
            this->refreshCtrl ( skfp, skfp->fi, true ) ; // refresh the control
            goodKey = true ;
         }
      }
   }  // (type==ftMB)

   //**********************************************
   //* Scrollbox controls recognize only vertical *
   //* scrolling and inter-field keycodes.        *
   //**********************************************
   else if ( fptr->type == ftSB )
   {
      short oldip = skfp->fi ;            // remember current IP

      //* For Scrollbox controls, the type of data displayed   *
      //* will determine whether the item selection keys,      *
      //* Space/Enter are relevant. If a list of items is      *
      //* displayed, then these keys will "select" the item.   *
      //* If the data are ordinary paragraph text, then the    *
      //* application can simply ignore the "selection".       *
      if ( (wkey == wcsSPACE) || (wkey == wcsENTER) )
      {
         fptr->sitem = fptr->mrp.rcur ;
         if ( (this->focus2NextField ( skfp )) != oldip )
            goodKey = true ;
      }
      //* Moving to another field *
      else if ( (wkey == wcsTAB) || (wkey == wcsSTAB) )
      {
         if ( wkey == wcsTAB ) { this->focus2NextField ( skfp ) ; }
         else                  { this->focus2PrevField ( skfp ) ; }
         if ( skfp->fi != oldip )
            goodKey = true ;
      }
      else if ( wkey == wcsUARROW )
      {
         //* If not already on first text row, move cursor to previous row.*
         if ( fptr->mrp.rcur > ZERO )
         {
            if ( (this->scrollFieldText ( skfp, skfp->fi, -1 )) )
               goodKey = true ;
         }
         //* Cursor is on top row of field. Move to previous field. *
         else if ( (this->focus2PrevField ( skfp )) != oldip )
            goodKey = true ;
      }
      else if ( wkey == wcsDARROW )
      {
         //* If not already on last text row, move cursor to next row.*
         if ( fptr->mrp.rcur < fptr->mrp.rlst )
         {
            if ( (this->scrollFieldText ( skfp, skfp->fi, 1 )) )
               goodKey = true ;
         }
         //* Cursor is on last row of field. Move to next field. *
         else if ( (this->focus2NextField ( skfp )) != oldip )
            goodKey = true ;
      }

      //********************************
      //** Page Up and Page Down Keys **
      //********************************
      else if ( (wkey == wcsPGUP) || (wkey == wcsPGDN) )
      {
         if ( wkey == wcsPGDN )           // set IP to head of bottom visible row
            fptr->ip = fptr->mrp.roff[fptr->mrp.rbot] ;
         else                             // set IP to head of top visible row
            fptr->ip = fptr->mrp.roff[fptr->mrp.rtop] ;
         short steps = (wkey == wcsPGDN ? fptr->hgt : -(fptr->hgt)) ;
         if ( (this->scrollFieldText ( skfp, skfp->fi, steps )) )
         { goodKey = true ; }

      }  // (PageUp/PageDown)

      else if ( wkey == wcsCHOME )
      {
         //* If not already on first text row *
         // Programmer's Note: wcsHOME should not be seen here. 
         //                    It will have been converted.
         if ( fptr->mrp.rcur > ZERO )
         {
            if ( (this->scrollFieldText ( skfp, skfp->fi, -(fptr->mrp.rcur) )) )
            { goodKey = true ; }
         }
      }
      else if ( wkey == wcsCEND )
      {
         //* If not already on last text row *
         // Programmer's Note: wcsEND should not be seen here. 
         //                    It will have been converted.
         if ( fptr->mrp.rcur < fptr->mrp.rlst )
         {
            if ( (this->scrollFieldText ( skfp, skfp->fi, (fptr->mrp.rlst - fptr->mrp.rcur))) )
            { goodKey = true ; }
         }
      }
   }  // (type==ftSB)

   //* Pushbuttons and Radiobuttons *
   else if ( type != ftTB )
   {
      if ( (wkey == SPACE) || (wkey == wcsENTER) )
      {
         //* If Radiobutton, set/reset the button with focus, *
         //* OR the XOR group to which it belongs.            *
         //* Any associated functionality should be handled   *
         //* by the application's callback function.          *
         if ( fptr->type == ftRB )
         {
            if ( ! fptr->state )          // if currently reset
            {
               this->setFieldState ( fptr, true ) ;
               if ( fptr->grp != noGROUP ) // if control is a member of a control group
               {
                  //* Scan the fields for members of the same XOR *
                  //* group at the target field. Reset the state  *
                  //* of those fields, and refresh the display.   *
                  skField* rfptr ;
                  wchar_t xorgp = fptr->grp ;
                  for ( short f = ZERO ; f < skfp->fCnt ; ++f )
                  {
                     if ( (skfp->fld[f].grp == xorgp) && (f != skfp->fi) )
                     {
                        rfptr = &skfp->fld[f] ;
                        this->setFieldState ( rfptr, false ) ;
                        this->refreshCtrl ( skfp, f, false ) ;
                     }
                  }
               }
            }
            else                          // if currently set
            {
               //* If this is a stand-alone control i.e. not part *
               //* of an XOR group, reset the state.              *
               if ( fptr->grp == noGROUP ) // if not a member of an XOR group
                  this->setFieldState ( fptr, false ) ;
            }
         }

         //* If a Pushbutton in a window, the associated functionality *
         //* should be handled by the application's callback function. *
         //* If a Pushbutton in a dialog, we should not see it here.   *
         else if ( fptr->type == ftPB )
         {
            /* nothing to be done at this time. */
         }
         //* Advance to the next field. If old field was a  *
         //* member of an XOR group, move out of the group. *
         group = fptr->grp ;
         if ( group == noGROUP )
            this->focus2NextField ( skfp ) ;
         else
         {
            do
            { this->focus2NextField ( skfp ) ; }
            while ( skfp->fld[skfp->fi].grp == group ) ;
         }
         goodKey = true ;
      }

      //* Moving to another field *
      else if ( (wkey == wcsTAB) || (wkey == wcsSTAB) )
      {
         if ( wkey == wcsTAB ) { this->focus2NextField ( skfp ) ; }
         else                  { this->focus2PrevField ( skfp ) ; }
         goodKey = true ;
      }
   }     // (! ftTB field type)

   if ( !goodKey && skfp->beep )
      this->acBeep() ;

   //* Remember the keycode that was just processed *
   skfp->pKey = prevkey ;

   return goodKey ;

}  //* End specialkeyProcCtrl() *

//*************************
//*      skpMultirow      *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* For multi-row text fields which ARE not configured as 'floShift' data.       *
//*                                                                              *
//* This method is built for speed, not size or elegance. The intent is to do    *
//* as little parsing of the data as possible AND to avoid re-drawing the text   *
//* unnecessarily. Thus, when practical, only the cursor position is updated.    *
//* This method relies upon the reasonably-efficient parseFieldText() method     *
//* to scan the text and record the useful text offsets and the logical cursor   *
//* position.                                                                    *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//*                                                                              *
//*                                                                              *
//********************************************************************************

void ACWin::skpMultirow ( skForm* skfp, wchar_t wkey )
{
   skField *fptr = &skfp->fld[skfp->fi] ; // pointer to field with focus
   gString gsParse ;                // received parsed display text
   WinPos  eorOff,                  // offset to end-of-row
           eotOff ;                 // offset to end-of-text
   short   fIndx = skfp->fi ;       // index of field with focus
   bool    reformat = false,        // set if full re-parsing required
           refresh = false,         // set if display is to be refreshed
           isSelect = true,         // true if already an existing selection
           beep = false ;           // set if user alert is triggered

   //* If a "highlight segment" is active, and a "selection" *
   //* is starting, disable the highlight segment.           *
   if ( fptr->state ) { staleHilightSegment ( fptr, wkey ) ; }

   //**************
   //** HOME Key **
   //**************
   if ( (wkey == wcsHOME) || (wkey == wcsCHOME) )
   {
      bool toptxt = bool(wkey==wcsCHOME),    // if set, move to top-of-text
           midrow = (!toptxt && (fptr->ip > fptr->mrp.bor)) ;

      //* If IP is mid-row and NOT wcsCHOME, go to head of current row.*
      if ( midrow && !toptxt )
      {
         fptr->ip = fptr->mrp.bor ;       // reposition insertion point
         this->parseFieldText ( fptr, ZERO ) ; // recalculate logical cursor
         this->setCursor2IP ( skfp ) ;    // set visible cursor
      }
      //* Else, already at left edge of field OR wcsCHOME. *
      //* If IP > zero, then go to IP == zero.  *
      else if ( !midrow )
      {
         if ( !(this->scrollFieldText ( skfp, skfp->fi, -(skfp->fld[skfp->fi].mrp.rcur) )) )
            beep = true ;
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsHOME)

   //*****************************
   //** END Key || CTRL+END Key **
   //*****************************
   else if ( (wkey == wcsEND) || (wkey == wcsCEND) )
   {
      if ( fptr->ip < fptr->mrp.ni )
      {
         //* IP is at end of current row OR if wcsCEND, *
         //* move to head of last row.                  *
         if ( ((wkey == wcsCEND) || (fptr->ip == fptr->mrp.eor)) && 
              (fptr->mrp.rcur < fptr->mrp.rlst) )
         {
            if ( !(this->scrollFieldText ( skfp, skfp->fi, (fptr->mrp.rlst - fptr->mrp.rcur) )) )
            { beep = true ; }
         }

         //* If not at end of current row, move IP *
         //* to end-of-row. Note that if on last   *
         //* row, then EOR is also end-of-text.    *
         // Programmer's Note: Moving IP to 'eor' may place the logical cursor 
         // outside the visible area; however, the parsing routine will adjust 
         // cursor position so it remains visible.
         if ( fptr->ip < fptr->mrp.eor )
         {
            fptr->ip = fptr->mrp.eor ;       // reposition insertion point
            this->parseFieldText ( fptr, ZERO ) ; // recalculate logical cursor
            this->setCursor2IP ( skfp ) ;    // set visible cursor
         }
      }
      else { beep = true ; }              // already referencing end-of-text

   }  // (wkey==wcsEND || wkey==wcsCEND)

   //********************
   //** LEFT-ARROW Key **
   //********************
   else if ( wkey == wcsLARROW )
   {
      if ( fptr->ip > ZERO )                 // if not at top-of-text
      {
         //* If not at head of current row, simply move the cursor. *
         if ( fptr->ip > fptr->mrp.bor )
         {
            --fptr->ip ;                     // step back by one character
            this->parseFieldText ( fptr, ZERO ) ; // recalculate logical cursor position (wcsLARROW)
            this->setCursor2IP ( skfp ) ;    // position the visible cursor
         }
         else                                // move to end of previous row
         {
            fptr->ip = fptr->mrp.roff[fptr->mrp.rcur] - 1 ;
            if ( fptr->mrp.rcur > fptr->mrp.rtop )    // if target row is visible
            {
               this->parseFieldText ( fptr, ZERO ) ;
               //* If IP is outside the visible area,         *
               //* decrement IP. (logical cursor s/b correct) *
               if ( fptr->mrp.ovr ) { --fptr->ip ; }
               this->setCursor2IP ( skfp ) ; // position the visible cursor
            }
            else                          // target row is above visible space
            {
               //* Decrement row offset by one and re-scan the text. *
               fptr->off = fptr->mrp.roff[fptr->mrp.rtop - 1] ;
               this->parseFieldText ( fptr ) ;
               //* If IP is outside the visible area,         *
               //* decrement IP. (logical cursor s/b correct) *
               if ( fptr->mrp.ovr ) { --fptr->ip ; }
               refresh = true ;                 // signal display refresh
            }
         }
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsLARROW)

   //*********************
   //** RIGHT-ARROW Key **
   //*********************
   else if ( wkey == wcsRARROW )
   {
      if ( fptr->ip < fptr->mrp.ni )         // if not yet at index of null terminator
      {
         //* If IP will still be on current row after increment *
         if ( fptr->ip < fptr->mrp.eov )
         {
            ++fptr->ip ;                     // step forward by one character
            this->parseFieldText ( fptr, ZERO ) ; // recalculate logical cursor position

            if ( fptr->ip == fptr->mrp.eor ) // IP reference end-of-row
            {
               //* If IP references a character (newline) outside *
               //* the visible area BUT not yet at end-of-text.   *
               if ( fptr->mrp.ovr && (fptr->ip < fptr->mrp.ni) )
               {
                  //* If on the bottom row, scroll the next row into view.*
                  if ( fptr->mrp.rcur == fptr->mrp.rbot )
                  {
                     //* Increment row offset by one *
                     fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
                     fptr->cur.col = ZERO ;
                     refresh = true ;        // signal display refresh
                     //this->RefreshField ( fIndx ) ;
                  }
                  //* Target row is already visible, recalculate cursor position.*
                  else
                  {
                     fptr->ip = fptr->mrp.roff[fptr->mrp.rcur+1] ; // index top of next row
                     fptr->cur = { short(fptr->mrp.rcur + 1), ZERO } ; // set logical cursor
                     this->setCursor2IP ( skfp ) ; // position the visible cursor
                  }
               }
               else
                  this->setCursor2IP ( skfp ) ; // position the visible cursor
            }
            else
               this->setCursor2IP ( skfp ) ;    // position the visible cursor
         }

         else     // IP >= end-of-row, move to top of next row
         {
            // Programmer's Note: We "know" that mrp.eor does not reference
            // the null terminator, i.e. mrp.eor < mrp.ni. Therefore, there 
            // is another text row below the current row. The target row 
            // may or may not be visible.
            fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ; // set IP to top of next row

            //* If on the bottom row, scroll the next row into view.*
            if ( fptr->mrp.rcur == fptr->mrp.rbot )
            {
               fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ; // increment offset by one row
               this->parseFieldText ( fptr ) ;  // recalculate field indices
               refresh = true ;                 // signal display refresh
               //this->RefreshField ( fIndx ) ;
            }
            //* Target row is already visible, recalculate cursor position.*
            else
            {
               this->parseFieldText ( fptr, ZERO ) ; // recalculate logical cursor position
               this->setCursor2IP ( skfp ) ;    // position the visible cursor
            }
         }
      }
      else { beep = true ; }              // already referencing end-of-text

   }  // (wkey==wcsRARROW)

   //******************
   //** UP-ARROW Key **
   //******************
   else if ( wkey == wcsUARROW )
   {
      //* If not already on first text row, move cursor to previous row.*
      if ( ! (this->skpInterRow ( skfp, wkey )) )
      {
         // Programmer's Note: This test should actually be for multiple
         // ACCESSIBLE fields.
         if ( skfp->fCnt > 1 )            // if there are additional fields
            this->focus2PrevField ( skfp ) ; // else, process 'wkey' as wcsSTAB
         else { beep = true ; }           // no additional fields
      }
   }  // (wkey==wcsUARROW)

   //********************
   //** DOWN-ARROW Key **
   //********************
   else if ( wkey == wcsDARROW )
   {
      //* If not already on last text row, move cursor to next row.*
      if ( ! (this->skpInterRow ( skfp, wkey )) )
      {
         // Programmer's Note: This test should actually be for multiple
         // ACCESSIBLE fields.
         if ( skfp->fCnt > 1 )            // if there are additional fields
            this->focus2NextField ( skfp ) ; // else, process as wcsTAB
         else { beep = true ; }           // no additional fields
      }
   }  // (wkey==wcsDARROW)

   //********************************
   //** Page Up and Page Down Keys **
   //********************************
   else if ( (wkey == wcsPGUP) || (wkey == wcsPGDN) )
   {
      if ( (skfp->fld[fIndx].type == ftTB) && (skfp->fld[fIndx].hgt > 1) )
      {
         if ( wkey == wcsPGDN )           // set IP to head of bottom visible row
            fptr->ip = fptr->mrp.roff[fptr->mrp.rbot] ;
         else                             // set IP to head of top visible wor
            fptr->ip = fptr->mrp.roff[fptr->mrp.rtop] ;
         short steps = (wkey == wcsPGDN ? fptr->hgt : -(fptr->hgt)) ;
         if ( (this->scrollFieldText ( skfp, skfp->fi, steps )) )
         {
            //* If now on last text row, move to end-of-text.*
            if ( fptr->ip == fptr->mrp.roff[fptr->mrp.rlst] )
            {
               fptr->ip = fptr->mrp.ni ;
               this->parseFieldText ( fptr, ZERO ) ;
               this->setCursor2IP ( skfp ) ;
            }
         }
         else { beep = true ; }
      }
      else { beep = true ; }              // invalid keycode in this context

   }  // (wkey==wcsPGUP || wkey==wcsPGDN)

   //*******************************
   //** BACKSPACE and DELETE Keys **
   //*******************************
   else if ( (wkey == wcsBKSP) ||         //** BKSP || DEL Key **
             (wkey == wcsDELETE) )
   {
      //* Prevents an attempt to either delete empty air or *
      //* to backspace beyond top-of-text.                  *
      if ( ((fptr->txt.gschars()) > 1) && 
           (((wkey == wcsBKSP) && (fptr->ip > ZERO)) || (fptr->sela < fptr->selb) ||
            ((wkey == wcsDELETE) && (fptr->txt.gstr()[fptr->ip] != NULLCHAR))) )
      {
         //* For Backspace, move IP one character toward head of text.*
         if ( wkey == wcsBKSP )
            --fptr->ip ;

         //* If "selected" text, delete the selected sub-string.*
         if ( fptr->sela < fptr->selb )
         {
            gString gsSel ;               // delete the current selection
            this->copySelection ( skfp, skfp->fi, gsSel, true ) ;
            //* If a newline was removed, text must be *
            //* reformatted, else re-index the data.   *
            if ( (gsSel.find( NEWLINE )) >= ZERO )
               this->formatFieldText ( skfp, fIndx ) ;
            else
               this->parseFieldText ( fptr ) ;
         }
         //* Delete the character referenced by 'ip.         *
         //* This shifts all trailing characters left by one.*
         else
         {
            //* If referenced character is a newline, then  *
            //* determine it context:                       *
            //*  a) If newline is preceeded by a space,     *
            //*     then delete both newline _and_ space.   *
            //*  b) Else, delete the newline only.          *
            //* Either way, the data must be reformatted.   *
            if ( fptr->txt.gstr()[fptr->ip] == NEWLINE )
            {
               if ( wkey == wcsBKSP )
               {
                  if ( fptr->txt.gstr()[fptr->ip - 1] == wcsSPACE )
                     fptr->txt.erase( NEWLINE, fptr->ip-- ) ;
               }
               this->skpDeleteChar ( skfp ) ;
               this->formatFieldText ( skfp, fIndx ) ;
            }
            //* Delete the normal (graphing) character.*
            //* (reformatting of data is unnecessary)  *
            else
               this->skpDeleteChar ( skfp ) ;
         }
         refresh = true ;                 // signal display refresh
      }
      else { beep = true ; }              // nothing to delete

   }  // (wkey == wcsBKSP) || (wkey == wcsDELETE)

   //*******************
   //** ALT-ENTER Key **
   //*******************
   // Programmer's Note: See the note about this keycode in specialkeyConvert().
   else if ( (wkey == wcsAENTER) )
   {
      //* Insert a newline character and advance the IP. *
      fptr->txt.insert( NEWLINE, fptr->ip++ ) ;
      //* Remember IP in case IP has moved outside visible space,    *
      //* then reformat the data and update logical cursor position. *
      short true_ip = fptr->ip ;
      if ( (this->formatFieldText ( skfp, fIndx )) )
      {
         fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ; // advance 'off' by one row
         fptr->ip = true_ip ;             // return IP to actual position
         this->parseFieldText ( fptr ) ;
      }
      refresh = true ;                    // signal display refresh
   }  // (wkey == wcsAENTER)

   //* Select text from current position, moving to the right *
   else if ( wkey == wcsSRIGHT )
   {
      //* If not already referencing null terminator *
      if ( fptr->ip < fptr->mrp.ni )
      {
         isSelect = true ;                // true if already a selection in progress
         reformat = false ;               // set if full re-parsing required

         //* If no current selection, _or_ if    *
         //* previous keycode != current keycode *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSRIGHT) && (skfp->pKey != wcsSDOWN)) )
         { isSelect = false ; }

         //* If no current selection OR if existing   *
         //* selection was in the opposite direction. *
         if ( ! isSelect )
         {
            skfp->seli = fIndx ;          // index of field with selected text
            if ( fptr->ip < (fptr->mrp.eor - 1) ) // IP will remain on current row
            {
               fptr->sela = fptr->ip ;    // index head of selection
               fptr->selb = ++fptr->ip ;  // advance the IP and set tail of selection
            }
            else if ( fptr->ip < fptr->mrp.eor ) // IP will reference E-O-R
            {
               fptr->sela = fptr->ip ;    // index head of selection
               //* Tail is first char of next row (if any), *
               //* else tail references null terminator.    *
               if ( fptr->mrp.rcur < fptr->mrp.rlst )
               {
                  fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
                  if ( fptr->mrp.rcur == fptr->mrp.rbot )
                     fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
                  reformat = true ;
               }
               else
                  fptr->ip = fptr->mrp.ni ;
               fptr->selb = fptr->ip ;
            }
            else                          // IP references newline at end of row
            {
               //* This is a special case. The leading newline is not   *
               //* included in the selection. The selection begins with *
               //* the first character of the next row, and IP will     *
               //* reference the second character of the row.           *
               fptr->ip = fptr->sela = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
               fptr->selb = ++fptr->ip ;
               //* If the target row is not in visible space, scroll upward.*
               if ( fptr->mrp.rcur == fptr->mrp.rbot )
                  fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
               reformat = true ;
            }
         }     // initial selection

         //* Continuing the existing selection *
         else
         {
            //* If IP is not yet at end of current row, *
            //* advance IP by one character.            *
            if ( fptr->ip < fptr->mrp.eor )
            {
               fptr->selb = ++fptr->ip ;        // advance the IP and tail of selection

               //* If IP now references the newline of current row, *
               //* (not end-of-text), move to head of next row.     *
               //* (Selection will include the newline character.)  *
               if ( (fptr->ip == fptr->mrp.eor) && (fptr->ip < fptr->mrp.ni) )
               {
                  fptr->selb = fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;

                  //* If target row is not in visible space, *
                  //* scroll data upward by one row.         *
                  if ( fptr->mrp.rcur == fptr->mrp.rbot )
                  {
                     fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
                     reformat = true ;
                  }
               }
            }

            //* Current IP references end of row, move to head of next row.*
            //* Note: This will happen only if the previous selection key  *
            //*       was the Shift+DownArrow combination.                 *
            else           // incremented IP > E-O-R, IP to head of next row
            {
               fptr->selb = fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;

               //* If target row is not in visible space, *
               //* scroll data upward by one row.         *
               if ( fptr->mrp.rcur == fptr->mrp.rbot )
               {
                  fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
                  reformat = true ;
               }
            }
         }     // (continuing selection)

         //* Calculate the logical cursor position, OR   *
         //* Re-parse the data.                          *
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         refresh = true ;                 // signal display refresh
      }
      else { beep = true ; }              // already referencing end-of-text

   }     // (wkey==wcsSRIGHT)

   //* Select text from current position, moving to the left *
   else if ( wkey == wcsSLEFT )
   {
      isSelect = true ;                   // true if already a selection in progress
      reformat = false ;                  // set if full re-parsing required

      //* If no current selection, _or_ if    *
      //* previous keycode != current keycode *
      if ( (fptr->sela == fptr->selb) || 
           ((skfp->pKey != wcsSLEFT) && (skfp->pKey != wcsSUP)) )
      { isSelect = false ; }

      //* If IP not already at top-of-text, OR at top-of-text *
      //* but first character not yet included in selection.  *
      if ( (fptr->ip > ZERO) || 
           ((fptr->ip == ZERO) && isSelect && (fptr->sela == (fptr->ip + 1))) )
      {
         if ( ! isSelect )
         {
            skfp->seli = fIndx ;          // index of field with selected text
            if ( (fptr->selb = fptr->ip + 1) > fptr->mrp.ni )// set right selection index
               fptr->selb = fptr->mrp.ni ;
         }
         if ( fptr->ip > ZERO )
            fptr->sela = fptr->ip-- ;
         else
            fptr->sela = fptr->ip ;

         //* If IP is now on the row above. *
         if ( fptr->ip < fptr->mrp.bor )
         {
            //* If target row is not in visible space, scroll the row  *
            //* above into view and rescan the text.                   *
            //* (We "know" that there actually is a row above because) *
            //* (IP can never be less than zero.                     ) *
            if ( (fptr->mrp.rcur == fptr->mrp.rtop) && (fptr->mrp.rtop > ZERO) )
               fptr->off = fptr->mrp.roff[fptr->mrp.rtop - 1] ;
            reformat = true ;
         }
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         if ( fptr->mrp.ovr ) { --fptr->ip ; } // if field boundary violated, step left
         refresh = true ;                      // signal display refresh
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsSLEFT)

   //* Select text from current position, moving upward *
   else if ( wkey == wcsSUP )             //** SHIFT+UP-ARROW Key **
   {
      //* If the field contains text _and_ if there is a row above *
      if ( (fptr->mrp.ni > ZERO) && (fptr->mrp.rcur > ZERO) )
      {
         isSelect = true ;                // true if already a selection
         skfp->seli = fIndx ;             // indicate field containing selection

         //* Determine whether field already contains selected text. *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSUP) && (skfp->pKey != wcsSLEFT)) )
         { isSelect = false ; }

         //* If no current selection, OR if previous selection was *
         //* in the opposite direction, start a new selection.     *
         if ( ! isSelect )
         {
            if ( (fptr->selb = (fptr->ip + 1)) > fptr->mrp.ni ) // delimit tail of selection
               fptr->selb = fptr->mrp.ni ;
         }
         this->skpInterRow ( skfp, wkey ) ;  // move to row above
         fptr->sela = fptr->ip + 1 ;
         refresh = true ;                    // signal display refresh
      }
      else { beep = true ; }     // nothing to select, or no more text rows

   }     // (wkey==wcsSUP)

   //* Select text from current position, moving downward *
   else if ( wkey == wcsSDOWN )           //** SHIFT+DOWN-ARROW Key **
   {
      //* If the field contains text _and_ if there is a row below *
      if ( (fptr->mrp.ni > ZERO) && (fptr->mrp.rcur < fptr->mrp.rlst) )
      {
         isSelect = true ;                // true if already a selection
         skfp->seli = fIndx ;             // indicate field containing selection

         //* Determine whether field already contains selected text. *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSDOWN) && (skfp->pKey != wcsSRIGHT)) )
         { isSelect = false ; }

         //* If no current selection, OR if previous selection was *
         //* in the opposite direction, start a new selection.     *
         if ( ! isSelect )
            fptr->sela = fptr->ip ;       // delimit head of selection

         this->skpInterRow ( skfp, wkey ) ; // move to row below
         fptr->selb = fptr->ip ;          // delimit tail of selection
         refresh = true ;                 // signal display refresh
      }
      else { beep = true ; }     // nothing to select, or no more text rows

   }  // (wkey==wcsSDOWN)

   //* Select all text in current field. *
   else if ( wkey == wcsSELECT )          //** a.k.a wcsCTL_A Key **
   {
      //* If there was a selection in a _different_ field, *
      //* caller has deselected it.                        *
      if ( fptr->txt.gschars() > 1 )      // if field contains text
      {
         //* De-select the current selection *
         if ( skfp->seli == fIndx )
            this->skpClearSelection ( skfp ) ;

         //* Select all text in current field. *
         else
         {
            fptr->sela = ZERO ;           // set the selection delimiters
            fptr->selb = fptr->txt.gschars() - 1 ;
            skfp->seli = fIndx ;          // indicate field containing selection
            refresh = true ;              // signal display refresh
         }
      }
      else { beep = true ; }              // nothing to select

   }  // (wkey==wcsSELECT)

   //* If there is selected text in this field, *
   //* Copy or Cut selection to the clipboard.  *
   else if ( (wkey == wcsCOPY) || (wkey == wcsCUT) )
   {
//OLD      if ( skfp->seli == fIndx )
      if ( skfp->fld[fIndx].sela < skfp->fld[fIndx].selb )
         this->cbCopy ( skfp, fIndx, bool(wkey == wcsCUT ? true : false) ) ;
      else { beep = true ; }              // no selection in this field
   }

   //* If request to paste clipboard data into the field.*
   else if ( wkey == wcsPASTE )
   {
      short cbBytes ;
      if ( (cbBytes = this->cbPaste ( skfp, skfp->fi )) <= ZERO )
      {
         //* Clipboard is empty, OR paste operation aborted *
         //* due to potential field overflow.               *
         if ( skfp->foot )                // if footnote messages enabled
         {
            if ( skfp->beep )             // auditory alert
               this->acBeep() ;
            this->Footnote ( ((cbBytes < ZERO) ? pasteAborted : 
                             (cbBytes == ZERO) ? pasteNothing : pasteTruncate), 24 ) ;
         }
         else
            beep = true ;
      }
   }

   //* Invalid "special" keycode  *
   //* (this should never happen) *
   else
   { beep = true ; }

   if ( refresh )                         // if specified, update the display
      this->refreshField ( skfp, fIndx ) ;

   if ( beep && skfp->beep )              // if user alert
      this->acBeep() ;

}  //* End skpMultirow() *

//*************************
//*    skpShiftedText     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Process special key input for all single-row text fields and for multi-row   *
//* text fields configured for "floShift" formatting.                            *
//*                                                                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::skpShiftedText ( skForm* skfp, wchar_t wkey )
{
   #define DEBUG_skpST (0)                // for debugging only

   int fIndx = skfp->fi,                  // index of target field
       wChars,                            // characters in the array
       roi ;                              // target row index
   skField *fptr = &skfp->fld[fIndx] ;    // pointer to target field
   bool    reformat = false,              // set if full re-parsing of data is required
           isSelect = false,              // set if selection in progress
           beep = false ;                 // set if user alert is triggered

   //* If a "highlight segment" is active, and a "selection" *
   //* is starting, disable the highlight segment.           *
   if ( fptr->state ) { staleHilightSegment ( fptr, wkey ) ; }

   //**************************
   //* Analyze the field text *
   //**************************
   this->parseFieldText ( fptr, ZERO ) ;

   //***************************
   //** HOME Key || CTRL+HOME **
   //***************************
   if ( (wkey == wcsHOME) || (wkey == wcsCHOME) )
   {
      if ( fptr->ip > ZERO )
      {
         if ( fptr->hgt == 1 )            // single-row fields
            this->hshiftFieldText( skfp, skfp->fi, -(fptr->ip) ) ;
         else                             // multi-row fields
         {
            if ( (wkey != wcsCHOME) && (fptr->ip > fptr->mrp.bor) )
               fptr->ip = fptr->mrp.bor ;
            else
               fptr->ip = ZERO ;
            this->centerFieldIP ( fptr, false, false ) ;
            this->refreshField ( skfp, fIndx ) ;
         }
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsHOME || wkey==wcsCHOME)

   //*****************************
   //** END Key || CTRL+END Key **
   //*****************************
   else if ( (wkey == wcsEND) || (wkey == wcsCEND) )
   {
      if ( fptr->ip < fptr->mrp.ni )
      {
         //* IP is at end of current row OR if wcsCEND, *
         //* move to HEAD of last row.                  *
         if ( ((wkey == wcsCEND) || (fptr->ip == fptr->mrp.eor)) && 
              (fptr->mrp.rcur < fptr->mrp.rlst) )
         {
            if ( !(this->scrollFieldText ( skfp, skfp->fi, (fptr->mrp.rlst - fptr->mrp.rcur) )) )
            { beep = true ; }
         }

         //* If not at end of current row, move IP to end-of-row and *
         //* bring new IP position into visible space.               *
         //* Note that if on last row, then EOR is also end-of-text. *
         if ( fptr->ip < fptr->mrp.eor )
         {
            fptr->ip = fptr->mrp.eor ;
            this->centerFieldIP ( fptr, false, false ) ;
            this->refreshField ( skfp, fIndx ) ;
         }
      }
      else { beep = true ; }              // already referencing end-of-text
   }  // (wkey==wcsEND || wkey==wcsCEND)

   //********************
   //** LEFT-ARROW Key **
   //********************
   else if ( wkey == wcsLARROW )
   {
      if ( fptr->ip > ZERO )              // if not at top-of-text
      {
         reformat = false ;               // true if full re-parsing required

         //* If IP is greater than leftmost VISIBLE character of the row. *
         if ( fptr->ip > fptr->mrp.bov )
            --fptr->ip ;                     // step back by one character

         //* If IP is greater than leftmost character in the row. *
         else if ( fptr->shft > ZERO )
         {
            --fptr->shft ;                   // shift all data right by one character
            --fptr->ip ;                     // step back by one character
            reformat = true ;                // set flag for re-parse
         }
         //* IP is at the leftmost position of the row. *
         //* move to the tail of previous row.          *
         else
         {
            --fptr->ip ;                           // index newline of previous row
            roi = fptr->mrp.rcur - 1 ;             // target row
            if ( roi < fptr->mrp.rtop )            // if target row not visible, adjust 'off'
               fptr->off = fptr->mrp.roff[roi] ;
            fptr->shft = horizShift ( fptr->txt.gscols(wChars), fptr->mrp.roff[roi], 
                                      fptr->ip, (fptr->wid - 1) ) ;
            reformat = true ;
         }
         //* Update the display. *
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         if ( reformat ) { this->refreshField ( skfp, fIndx ) ; }  // redraw the display data
         else            { this->setCursor2IP ( skfp ) ; }   // update visible cursor
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsLARROW)

   //*********************
   //** RIGHT-ARROW Key **
   //*********************
   else if ( wkey == wcsRARROW )
   {
      if ( fptr->ip < fptr->mrp.ni )      // if not yet at index of null terminator
      {
         reformat = false ;               // true if full re-parsing required

         //* If after increment, IP will remain in visible space *
         //* do it the easy way.                                 *
         if ( fptr->ip < fptr->mrp.eov )
            ++fptr->ip ;

         //* If IP will remain on the same row after increment, shift   *
         //* text leftward. Note that this covers all single-row fields.*
         else if ( fptr->ip < fptr->mrp.eor )
         {
            ++fptr->ip ;                  // advance the IP
            ++fptr->shft ;                // shift text leftward
            reformat = true ;             // set flag for re-parse
            // Programmer's Note: There is a visual hiccup if IP references
            // the newline in visible space. It requires an extra right-arrow 
            // key to reach the next row. However, if user want to insert data 
            // BEFORE the newline, this is the correct position.
         }

         //* For multi-row fields, IP is at end of current row.    *
         //* Move to head of next row. Note that single-row fields *
         //* would never reach this point because IP would already *
         //* at null terminator.                                   *
         else
         {
            fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
            //* If next row is outside visible space, *
            //* scroll upward by one row.             *
            if ( fptr->mrp.rcur == fptr->mrp.rbot )
               fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
            fptr->shft = ZERO ;        // shift head of row into visible space
            reformat = true ;          // set flag for re-parse
         }
         //* Update the display. *
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         if ( reformat ) { this->refreshField ( skfp, fIndx ) ; }  // redraw the display data
         else            { this->setCursor2IP ( skfp ) ; }   // update visible cursor
      }
      else { beep = true ; }              // already referencing end-of-text

   }  // (wkey==wcsRARROW)

   //******************
   //** UP-ARROW Key **
   //******************
   else if ( wkey == wcsUARROW )
   {
      //* If not already on first text row, move cursor to previous row.*
      if ( ! (this->skpInterRow ( skfp, wkey )) )
         this->focus2PrevField ( skfp ) ; // else, process as wcsSTAB
   }  // (wkey==wcsUARROW)

   //********************
   //** DOWN-ARROW Key **
   //********************
   else if ( wkey == wcsDARROW )
   {
      //* If not already on last text row, move cursor to next row.*
      if ( ! (this->skpInterRow ( skfp, wkey )) )
         this->focus2NextField ( skfp ) ; // else, process as wcsTAB
   }  // (wkey==wcsDARROW)

   //********************************
   //** Page Up and Page Down Keys **
   //********************************
   else if ( (wkey == wcsPGUP) || (wkey == wcsPGDN) )
   {
      if ( (fptr->type == ftTB) && (fptr->hgt > 1) )
      {
         short trg, steps ;
         bool  scroll = true ;
         if ( wkey == wcsPGDN )           // scroll toward bottom of text
         {
            //* If end-of-text is not already in view, *
            //* scroll down by on page.                *
            if ( fptr->mrp.rbot < fptr->mrp.rlst )
            {
               if ( (trg = fptr->mrp.rbot + fptr->hgt) > fptr->mrp.rlst )
                  trg = fptr->mrp.rlst ;
               steps = trg - fptr->mrp.rcur ;
            }
            //* Else, move to bottom row *
            else if ( fptr->mrp.rcur < fptr->mrp.rlst )
               steps = fptr->mrp.rlst - fptr->mrp.rcur ;
            else if ( fptr->ip < fptr->mrp.ni )
            {
               scroll = false ;           // scrolling is unnecessary
               fptr->ip = fptr->mrp.ni ;
               if ( fptr->ip > fptr->mrp.eov )
               {
                  //* Shift is width of last row minus width of field, minus 1. *
                  short lrindx = fptr->mrp.roff[fptr->mrp.rlst] ; // char index of last row
                  gString gs( &fptr->txt.gstr()[lrindx] ) ;       // load last row
                  fptr->shft = gs.gscols() - fptr->wid + 1 ;
               }
               this->parseFieldText ( fptr ) ;
               this->refreshField ( skfp, fIndx ) ;
            }
            else
            { scroll = false ; beep = true ; }  // IP already at end-of-text
         }
         else                             // scroll toward top of text
         {
            //* If top-of-text is not already in view, *
            //* scroll up by one page.                 *
            if ( fptr->mrp.rtop > ZERO )
            {
               if ( (trg = fptr->mrp.rtop - fptr->hgt) < ZERO )
                  trg = ZERO ;
               steps = trg - fptr->mrp.rcur ; // (this is a negative value)
            }
            //* Else, move to top row *
            else if ( fptr->mrp.rcur > fptr->mrp.rtop )
               steps = -(fptr->mrp.rcur) ;
            else if ( fptr->ip == ZERO )
            { scroll = false ; beep = true ; }  // IP already on top-of-text
         }
         if ( scroll )                    // execute the scroll
         {
            // Programmer's Note: This is functional for ACWin objects ONLY.
            // ACDlg objects currently do not support multi-row text fields, 
            // so this is Ok (for now).
            if ( ! (this->ScrollFieldText ( fIndx, steps )) )
               beep = true ;              // (this is unlikely)
         }
      }
      else { beep = true ; }           // invalid keycode in this context

   }  // (wkey==wcsPGUP || wkey==wcsPGDN)

   //*******************************
   //** BACKSPACE and DELETE Keys **
   //*******************************
   else if ( (wkey == wcsBKSP) ||         //** BKSP || DEL Key **
             (wkey == wcsDELETE) )
   {
      //* Prevents an attempt to either delete empty air or *
      //* to backspace beyond top-of-text.                  *
      if ( ((fptr->txt.gschars()) > 1) && 
           (((wkey == wcsBKSP) && (fptr->ip > ZERO)) || (fptr->sela < fptr->selb) || 
            ((wkey == wcsDELETE) && (fptr->txt.gstr()[fptr->ip] != NULLCHAR))) )
      {
         //* For Backspace, move IP one character toward head of text.*
         //* (indexing will be updated by the call to delete char)    *
         if ( wkey == wcsBKSP )
            --fptr->ip ;

         if ( fptr->sela < fptr->selb )
         {
            gString gsSel ;               // delete the current selection
            this->copySelection ( skfp, skfp->fi, gsSel, true ) ;
            this->parseFieldText ( fptr ) ;
         }
         //* Delete the character referenced by 'ip.         *
         //* This shifts all trailing characters left by one,*
         else
            this->skpDeleteChar ( skfp ) ;

         //* Update the display *
         this->refreshField ( skfp, skfp->fi ) ;
      }
      else { beep = true ; }              // nothing to delete

   }  // (wkey == wcsBKSP) || (wkey == wcsDELETE)

   //*******************
   //** ALT-ENTER Key **
   //*******************
   // Programmer's Note: See the note about this keycode in specialkeyConvert().
   else if ( wkey == wcsAENTER )
   {
      //* Multi-row fields only. Newlines are   *
      //* incompatible with single-row fields.  *
      if ( fptr->hgt > 1 )
      {
         //* Insert a newline character at IP and advance the IP. *
         fptr->txt.insert( NEWLINE, fptr->ip++ ) ;
         fptr->shft = ZERO ;                 // reset the horizontal shift

         //* If new IP is outside visible space, *
         //* scroll text upward by one row.      *
         if ( fptr->mrp.rcur == fptr->mrp.rbot )
            fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ;
         this->parseFieldText ( fptr ) ;
         this->refreshField ( skfp, skfp->fi ) ;
      }
      else { beep = true ; }              // single-row field

   }  // (wkey == wcsAENTER)

   //********************
   //** SHIFT+LEFT Key **
   //********************
   else if ( wkey == wcsSLEFT )
   {
      //* Select text from current position, moving to the left *
      if ( fptr->ip > ZERO )              // if not at top-of-text
      {
         isSelect = true ;                // true if already a selection

         //* If no current selection, _or_ if previous keycode *
         //* was selecting in the opposite direction.          *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSLEFT) && (skfp->pKey != wcsSUP)) )
         {
            isSelect = false ;            // indicate no selection

            //* Initialize the start of text selection ('selb'), and    *
            //* end of selection ('sela'). Current IP is needed for the *
            //* parsing test, below and will be advanced afterward.     *
            skfp->seli = fIndx ;          // index of field containing selected text
            //* If IP is greater than leftmost VISIBLE character of the row. *
            if ( fptr->ip > fptr->mrp.bov )
            {
               //* Set right selection index (not beyond null terminator) *
               if ( (fptr->selb = fptr->ip + 1) > fptr->mrp.ni )
                  fptr->selb = fptr->mrp.ni ;
               fptr->sela = fptr->selb - 1 ;
            }
            //* If IP is greater than leftmost character in the row. *
            else if ( fptr->shft > ZERO )
            {
               //* Set right selection index (not beyond null terminator) *
               if ( (fptr->selb = fptr->ip + 1) > fptr->mrp.ni )
                  fptr->selb = fptr->mrp.ni ;
               fptr->sela = fptr->selb - 1 ;
            }
            //* IP is at the leftmost position of the row. *
            //* move to the tail of previous row.          *
            else
            {
               fptr->selb = fptr->ip ;
               fptr->sela = fptr->selb - 1 ;
            }
         }

         //* If IP is greater than leftmost VISIBLE character of the row. *
         if ( fptr->ip > (fptr->mrp.bor + fptr->shft) )
         {
            if ( isSelect )                  // continue an existing selection
               fptr->sela = fptr->ip ;       // left select references current character
            --fptr->ip ;                     // step leftward by one character

            //* Special Case: If IP now references first character of    *
            //* field text, include the first character in the selection.*
            if ( fptr->ip == ZERO )
               fptr->sela = ZERO ;
         }
         //* If IP is greater than leftmost character in the row. *
         else if ( fptr->shft > ZERO )
         {
            if ( isSelect )                  // continue an existing selection
               fptr->sela = fptr->ip ;       // left select references current character
            --fptr->ip ;                     // step leftward by one character
            --fptr->shft ;                   // shift all data right by one character
            if ( fptr->ip == ZERO )          // special case (see above)
               fptr->sela = ZERO ;
            reformat = true ;                // re-parse the field data
         }
         //* IP is at the leftmost position of the row. *
         //* move to the tail of previous row.          *
         else
         {
            if ( isSelect )                  // continue an existing selection
               fptr->sela = fptr->ip ;       // left select references current character
            --fptr->ip ;                     // index newline of previous row
            roi = fptr->mrp.rcur - 1 ;       // target row
            if ( roi < fptr->mrp.rtop )      // if target row not visible, adjust 'off'
               fptr->off = fptr->mrp.roff[roi] ;

            //* Calculate the new value of 'shft'. *
            fptr->shft = horizShift ( fptr->txt.gscols(wChars), fptr->mrp.roff[roi], 
                                      fptr->ip, (fptr->wid - 1) ) ;
            reformat = true ;                // re-parse the field data
         }

         //* Either calculate logical cursor position only (faster), or *
         //* recalculate all field-parse data.                          *
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         this->refreshField ( skfp, fIndx ) ; // display the results
      }
      else { beep = true ; }              // already referencing top-of-text

   }  // (wkey==wcsSLEFT)

   //*********************
   //** SHIFT+RIGHT Key **
   //*********************
   else if ( wkey == wcsSRIGHT )
   {
      //* Select text from current position, moving to the right.       *
      //* -- IP (character index) moves to the right by one character,  *
      //*    or if spanning rows, possibly by two characters.           *
      //* -- IP in single-row fields of course cannot span rows. (Duh!) *
      //* Programmer's Note: ip == selb, BUT character under IP is not  *
      //*   included in the selection. To put it another way:           *
      //*   (selb - sela) == number of selected characters.             *
      //* Programmer's Note: This is moderately-tricky code, optimized  *
      //*   for speed, so be aware.                                     *
      if ( fptr->ip < fptr->mrp.ni )      // if not yet at index of null terminator
      {
         isSelect = true ;                // true if already a selection
         //* If no current selection, _or_ if previous keycode *
         //* was selecting in the opposite direction.          *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSRIGHT) && (skfp->pKey != wcsSDOWN)) )
         {
            isSelect = false ;               // indicate no selection

            //* Initialize the start of text selection ('sela'), and    *
            //* end of selection ('selb'). Current IP is needed for the *
            //* parsing test, below and will be advanced afterward.     *
            skfp->seli = fIndx ;             // index of field containing selected text
            if ( (fptr->ip < fptr->mrp.eov) || // IP stays in visible space
                 (fptr->ip < fptr->mrp.eor) )  // IP will stay on current line
            { fptr->sela = fptr->ip ; }      // index head of selection
            else     // selection begins at first character of next line
               fptr->sela = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
            fptr->selb = fptr->sela + 1 ;    // end-of-selection at next character
         }

         //* If IP will remain in _visible space_ of the same row *
         if ( fptr->ip < fptr->mrp.eov )
         {
            if ( ! isSelect )                // begin a new selection
               ++fptr->ip ;                  // advance the IP
            else                             // continue an existing selection
               fptr->selb = ++fptr->ip ;     // advance both IP and end-of-selection
            if ( fptr->ip > fptr->mrp.eov )  // if new IP is beyond visible space
            {
               ++fptr->shft ;                // shift text leftward
               reformat = true ;             // re-parse the field data
            }
         }
         //* Else if IP will move to or beyond visible space, BUT will   *
         //* remain on the same row after increment, shift text leftward.*
         else if ( fptr->ip < fptr->mrp.eor )
         {
            if ( ! isSelect )                // begin a new selection
               ++fptr->ip ;                  // advance the IP
            else                             // continue an existing selection
               fptr->selb = ++fptr->ip ;     // advance both IP and end-of-selection
            ++fptr->shft ;                   // shift text leftward
            reformat = true ;                // re-parse the field data
         }
         //* Else, IP is at end-of-row, but not at end-of-text. *
         else
         {
            if ( isSelect )                  // continue an existing selection
               fptr->selb = fptr->ip + 1 ;   // advance end-of-selection
            //* IP references first character of next row.*
            fptr->ip = fptr->mrp.roff[fptr->mrp.rcur + 1] ;
            if ( ! isSelect ) ++fptr->ip ;   // if new selection, step past it
            fptr->shft = ZERO ;              // no text shift (leftmost chars are visible)
            reformat = true ;
         }

         //* Either calculate logical cursor position only (faster), or *
         //* recalculate all field-parse data.                          *
         this->parseFieldText ( fptr, (reformat ? 3 : 0) ) ;
         this->refreshField ( skfp, fIndx ) ; // display the results
      }
      else { beep = true ; }              // already referencing end-of-text

   }     // (wkey==wcsSRIGHT)

   //******************
   //** SHIFT+UP Key **
   //******************
   else if ( wkey == wcsSUP )
   {
      //* Select text from current position, moving upward *
      //* If the field contains text _and_ if there is a row above *
      if ( (fptr->mrp.ni > ZERO) && (fptr->mrp.rcur > ZERO) )
      {
         isSelect = true ;                // true if already a selection
         skfp->seli = fIndx ;             // indicate field containing selection

         //* Determine whether field already contains selected text. *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSUP) && (skfp->pKey != wcsSLEFT)) )
         { isSelect = false ; }

         //* If no current selection, OR if previous selection was *
         //* in the opposite direction, start a new selection.     *
         if ( ! isSelect )
         {
            if ( (fptr->selb = (fptr->ip + 1)) > fptr->mrp.ni ) // delimit tail of selection
               fptr->selb = fptr->mrp.ni ;
         }
         this->skpInterRow ( skfp, wkey ) ;  // move to row above
         fptr->sela = fptr->ip + 1 ;
         this->refreshField ( skfp, fIndx ) ;
      }
      else { beep = true ; }     // nothing to select, or no more text rows
   }

   //********************
   //** SHIFT+DOWN Key **
   //********************
   else if ( wkey == wcsSDOWN )
   {
      //* Select text from current position, moving downward *
      //* If the field contains text _and_ if there is a row below *
      if ( (fptr->mrp.ni > ZERO) && (fptr->mrp.rcur < fptr->mrp.rlst) )
      {
         isSelect = true ;                // true if already a selection
         skfp->seli = fIndx ;             // indicate field containing selection

         //* Determine whether field already contains selected text. *
         if ( (fptr->sela == fptr->selb) || 
              ((skfp->pKey != wcsSDOWN) && (skfp->pKey != wcsSRIGHT)) )
         { isSelect = false ; }

         //* If no current selection, OR if previous selection was *
         //* in the opposite direction, start a new selection.     *
         if ( ! isSelect )
            fptr->sela = fptr->ip ;          // delimit head of selection

         this->skpInterRow ( skfp, wkey ) ;  // move to row below
         fptr->selb = fptr->ip ;          // delimit tail of selection
         this->refreshField ( skfp, fIndx ) ;
      }
      else { beep = true ; }     // nothing to select, or no more text rows
   }

   //********************
   //** SELECT_ALL Key **
   //********************
   else if ( wkey == wcsSELECT )          //** a.k.a. wcsCTL_A Key **
   {
      //* Select all text in current field.         *
      //* If there was a selection in a _different_ *
      //* field, caller has deselected it.          *
      if ( (fptr->txt.gschars()) > 1 )       // if field contains text
      {
         //* De-select the current selection *
         if ( (skfp->seli == fIndx) && (fptr->sela != fptr->selb) )
            this->skpClearSelection ( skfp ) ;

         //* Select all text in current field. *
         else
         {
            fptr->sela = ZERO ;              // set the selection delimiters
            fptr->selb = fptr->mrp.ni ;
            skfp->seli = fIndx ;             // indicate field containing selection
            this->refreshField ( skfp, fIndx ) ; // display the results
         }
      }
      else { beep = true ; }                 // nothing to select

   }

   //***************************************************************
   //** COPY/CUT Keys                                             **
   //** If there is selected text in this field, Copy (wcsCTRL_C) **
   //** or Cut (wcsCTRL_X) selection to the clipboard.            **
   //***************************************************************
   else if ( (wkey == wcsCOPY) || (wkey == wcsCUT) )
   {
//OLD      if ( skfp->seli == fIndx )
      if ( skfp->fld[fIndx].sela < skfp->fld[fIndx].selb )
         this->cbCopy ( skfp, fIndx, bool(wkey == wcsCUT ? true : false) ) ;
      else { beep = true ; }                 // nothing selected in this field
   }

   //*******************************************************
   //** PASTE Key                                         **
   //** If request to paste clipboard data into the field.**
   //*******************************************************
   else if ( wkey == wcsPASTE )              //** a.k.a. wcsCTRL_V Key **
   {
      short cbBytes ;
      if ( (cbBytes = this->cbPaste ( skfp, skfp->fi )) <= ZERO )
      {
         //* Clipboard is empty, OR paste operation aborted *
         //* due to potential field overflow.               *
         // Programmer's Note: To prevent update of the parent window while a 
         // dialog is open, footnotes are disabled when target field is within 
         // an ACDlg object.
         if ( skfp->foot )                // if footnote messages enabled
         {
            if ( skfp->beep )             // auditory alert
               this->acBeep() ;
            this->Footnote ( ((cbBytes < ZERO) ? pasteAborted : 
                             (cbBytes == ZERO) ? pasteNothing : pasteTruncate), 24 ) ;
         }
         else
            beep = true ;
      }
   }

   //* Invalid "special" keycode (this should not happen) *
   else
   { beep = true ; }

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FDUMP != 0 && DEBUG_skpST != 0
   const short KEYS = 16 ;
   const wchar_t* wcharName[KEYS] =
      { L"wcsHOME  ", L"wcsEND   ", 
        L"wcsLARROW", L"wcsRARROW", L"wcsUARROW", L"wcsDARROW",
        L"wcsAENTER", L"wcsSRIGHT", L"wcsSLEFT ", L"wcsSUP   ", L"wcsSDOWN ",
        L"wcsSELECT", L"wcsCOPY  ", L"wcsCUT   ", L"wcsPASTE ", 
        L"*ERROR*  "
      } ;
   short wni = ZERO ;
   switch ( wkey )
   {
      case wcsHOME:   wni =  0 ; break ;  case wcsEND:    wni =  1 ; break ;
      case wcsLARROW: wni =  2 ; break ;  case wcsRARROW: wni =  3 ; break ;
      case wcsUARROW: wni =  4 ; break ;  case wcsDARROW: wni =  5 ; break ;
      case wcsAENTER: wni =  6 ; break ;  case wcsSRIGHT: wni =  7 ; break ;
      case wcsSLEFT:  wni =  8 ; break ;  case wcsSUP:    wni =  9 ; break ;
      case wcsSDOWN:  wni = 10 ; break ;  case wcsSELECT: wni = 11 ; break ;
      case wcsCOPY:   wni = 12 ; break ;  case wcsCUT:    wni = 13 ; break ;
      case wcsPASTE:  wni = 14 ; break ;  default: wni = KEYS - 1 ;  break ;
   }
   this->dumpField2Log ( fptr, wcharName[wni] ) ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FDUMP && DEBUG_skpST

   if ( beep && this->skf->beep )         // if user alert
      this->acBeep() ;

   #undef DEBUG_skpST
}  //* End skpShiftedText() *

//*************************
//*      horizShift       *
//*************************
//********************************************************************************
//* Non-member Method:                                                           *
//* ------------------                                                           *
//* For fields which are configured for 'floShift' formatting, calculate the     *
//* horizontal shift necessary to bring the character referenced by IP into view.*
//*                                                                              *
//* This is done by counting the number of columns occupied by the characters    *
//* between the IP and the first character in the target row.                    *
//*                                                                              *
//* Input  : colPtr   : array of column widths                                   *
//*          rowBase  : character index for head of target row                   *
//*          currIp   : index of character to be shifted into view (IP)          *
//*          maxWidth : available display columns                                *
//*                                                                              *
//* Returns: number of characters to be shifted out of visible space             *
//*          (this IS NOT a column count)                                        *
//********************************************************************************

static int horizShift ( const short* colPtr, short rowBase, short currIp, short maxWidth )
{
   int colAcc = ZERO,                     // column width accumulator
       indx   = currIp ;                  // array index

   for ( indx = currIp ; indx > rowBase ; --indx )
   { if ( (colAcc += colPtr[indx]) >= maxWidth ) { break ; } }
   return ( indx - rowBase ) ;

}  //* End horizShift() *

//*************************
//*      skpInterRow      *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Called when:                                                                 *
//* 1) Target field is a multi-row field, AND                                    *
//* 2) Either wcsDARROW or wcsUARROW has been received.                          *
//*    Note: wcsSDOWN and wcsSUP are also supported; however, for those keycodes,*
//*          the field text is not redrawn.                                      *
//*                                                                              *
//* If target row is not visible, scroll the text up/down by one row; then,      *
//* calculate the 'ip' (insertion point) and 'cur' (logical cursor position)     *
//* values for the vertical cursor movement within the target field and position *
//* the physical cursor.                                                         *
//*                                                                              *
//* Input  : skfp : pointer to skForm object(skfp->fi references target field)   *
//*          wkey : received keycode                                             *
//*                                                                              *
//* Returns: 'true'  if keycode handled and cursor repositioned                  *
//*          'false' if cursor is already at the top/bottom row                  *
//********************************************************************************
//* Notes:                                                                       *
//* Target is one row upward or one row downward and in the same column as in    *
//* the current row. The problems is that the target row may have no text in     *
//* that column, OR that column may not be the base column for the character     *
//* displayed at that position. For these reasons, the calculation is not        *
//* straightforward.                                                             *
//*                                                                              *
//* The parseFieldText() method is used to gather information on the formatted   *
//* text, and the target IP and logical cursor position are calculated from      *
//* that information.                                                            *
//********************************************************************************

bool ACWin::skpInterRow ( skForm* skfp, wchar_t wkey )
{
   #define DEBUG_skpInterRow (0)          // for debugging only

   skField *fptr = &skfp->fld[skfp->fi] ; // pointer to target field

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_skpInterRow != 0
   MRParse oldmrp ;              // save a copy of the current parsing data
   oldmrp = fptr->mrp ;
   int oldOff = fptr->off, oldShft = fptr->shft, oldIp = fptr->ip ;
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_skpInterRow

   const short *colwid ;                  // array of column widths
   const wchar_t *wptr ;                  // pointer to source text
   int chCnt,                             // total number of characters
       curAcc = ZERO,                     // current row column-width accumulator
       trgAcc = ZERO,                     // target  row column-width accumulator
       rcur = fptr->mrp.rcur,             // source row index
       rtrg,                              // target row index
       indx ;                             // scan index
   bool  refresh = true,                  // set if text is to be re-drawn
         status = false ;                 // return value (assumes keycode NOT handled)

   //* Convert text-selection keycodes to reduce clutter, and      *
   //* disable the refresh so caller can update selection markers. *
   if ( wkey == wcsSUP )   { wkey = wcsUARROW ; refresh = false ; }
   if ( wkey == wcsSDOWN ) { wkey = wcsDARROW ; refresh = false ; }

   //* If there is another text row in the specified direction. *
   if ( ((wkey == wcsUARROW) && (fptr->mrp.rcur > ZERO)) ||
        ((wkey == wcsDARROW) && (fptr->mrp.rcur < fptr->mrp.rlst)) )
   {
      colwid = fptr->txt.gscols( chCnt ) ;// array of column widths
      wptr   = fptr->txt.gstr() ;         // pointer to source text

      //* Number of columns between head of current row and IP. *
      for ( indx = fptr->mrp.roff[rcur] ; indx < fptr->ip ; ++indx )
         curAcc += colwid[indx] ;

      if ( wkey == wcsUARROW )            // move cursor upward
      {
         //* If target row is not visible,   *
         //* scroll text downward by one row.*
         //* Cursor row is unchanged.        *
         if ( fptr->mrp.rcur == fptr->mrp.rtop )
         { fptr->off = fptr->mrp.roff[fptr->mrp.rtop - 1] ; refresh = true ; }
         rtrg = fptr->mrp.rcur - 1 ;
      }     // (wkey==wcsUARROW)

      else if ( wkey == wcsDARROW )       // move cursor downward
      {
         //* If target row is not visible,   *
         //* scroll text upward by one row.  *
         if ( fptr->mrp.rcur == fptr->mrp.rbot )
         { fptr->off = fptr->mrp.roff[fptr->mrp.rtop + 1] ; refresh = true ; }
         rtrg = fptr->mrp.rcur + 1 ;
      }     // (wkey==wcsDARROW)

      //* Number of columns between head of current row and IP. *
      for ( indx = fptr->mrp.roff[rtrg] ; trgAcc < curAcc ; ++indx )
      {
         trgAcc += colwid[indx] ;
         if ( (wptr[indx] == NEWLINE) || (wptr[indx] == NULLCHAR) )
         { ++trgAcc ; break ; }
      }

      //* If column count is not at the beginning of *
      //* a character index the previous character.  *
      if ( trgAcc > curAcc )
         trgAcc -= colwid[indx--] ;

      fptr->ip = indx ;                   // new insertion point

      //* If scrolling has occurred, refresh everything. *
      //* Else, update cursor position.                  *
      this->parseFieldText ( fptr ) ;
      if ( refresh ) this->refreshField ( skfp, skfp->fi ) ;
      else           { this->setCursor2IP ( skfp ) ; }

      status = true ;                     // return success
   }

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_skpInterRow != 0
   if ( (ofsdbg.is_open()) )
   {
      gsdbg.compose( "%S off:%d->%d shft:%d->%d ip:%d->%d rcur:%d Acc:%d rtrg:%d Acc:%d",
                     (wkey == wcsUARROW ? L"UP" : L"DN"), 
                     &oldOff, &fptr->off, &oldShft, &fptr->shft,
                     &oldIp, &fptr->ip, &rcur, &curAcc, &rtrg, &trgAcc ) ;
      ofsdbg << gsdbg.ustr() << endl ;
   }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_skpInterRow

   return status ;

   #undef DEBUG_skpInterRow
}  //* End skpInterRow() *

//*************************
//*     skpDeleteChar     *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* For text fields, erase the character referenced by IP.                       *
//* The field will be re-formatted and/or re-indexed as necessary (see below).   *
//* The display is not refreshed.                                                *
//*                                                                              *
//* 1) For fields with flowing text (floChar || floToken || floHyphen):          *
//*    -- For graphing characters:                                               *
//*       Delete the character referenced by IP. The field's 'eov' and 'eor'     *
//*       will be updated, but IP and cursor position will not change.           *
//*    -- For newline characters ('\n'):                                         *
//*       Delete the character. This will cause the next text row (if any) to be *
//*       concatenated with the existing row.                                    *
//*       This may (and probably will) cause the row to be too wide for the      *
//*       field. In this case, the current row and succeeding rows will need to  *
//*       be reformatted to fit the field width.                                 *
//*                                                                              *
//* 2) For fields with fixed formatting (floShift):                              *
//*    -- For graphing characters:                                               *
//*       Delete the character referenced by IP. The field's 'eov' and 'eor'     *
//*       will be updated, but IP and cursor position will not change.           *
//*    -- For newline characters ('\n'):                                         *
//*       Delete the character. This will cause the next text row (if any) to be *
//*       concatenated with the existing row. The field's 'eov' and 'eor' will   *
//*       be updated as well as the indices of remaining rows and the 'rbot'     *
//*       row. The IP and cursor position will not change.                       *
//*                                                                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::skpDeleteChar ( skForm* skfp )
{
   #define DEBUG_DELCHAR (0)           // for debugging only

   skField *fptr = &skfp->fld[skfp->fi] ; // pointer to target field

   //* Do not attempt to delete the null terminator.     *
   //* gString won't let you do it, but it's dumb to try.*
   if ( fptr->ip < fptr->mrp.ni )
   {
      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_DELCHAR != 0
      wchar_t ipChar ; short nli ;
      if ( ofsdbg.is_open() )
      {
         ipChar = fptr->txt.gstr()[fptr->ip] ;
         if ( fptr->txt.gstr()[fptr->ip] == NEWLINE ) { ipChar = wcsSTAB ; }
         gsdbg.compose( "skpDeleteChar( ip:%hd cur:%hd,%hd ipChar:'%C' ) '%S'", 
                        &fptr->ip, &fptr->cur.row, &fptr->cur.col, &ipChar,
                        &fptr->txt.gstr()[fptr->mrp.bor]) ;
         if ( (nli = gsdbg.find( NEWLINE )) >= ZERO )
            gsdbg.limitChars( nli - 1 ) ;
         ofsdbg << gsdbg.ustr() << "\n   --------"  << endl ;
         //ofsdbg << "\n   --------" << endl ;
      }
      #endif // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_DELCHAR

      //* If character referenced by IP is not a newline, *
      //* delete the character and update indexing data.  *
      if ( fptr->txt.gstr()[fptr->ip] != NEWLINE )
      {
         fptr->txt.erase( fptr->ip, 1 ) ;
         this->parseFieldText ( fptr ) ;  // (delete character, not newline)
      }

      //* Else, delete the newline character, then reformat *
      //* the data of the current row and succeeding rows   *
      //* and update the indexing data.                     *
      else
      {
         //* If IP has moved to the row above (backspace key), *
         //* adjust vertical scrolling and horizontal shift    *
         //* as necessary to bring IP into visible space.      *
         if ( fptr->ip < fptr->mrp.bor )
         {
            //* If IP _was_ on the top row, vertical scrolling is needed. *
            if ( fptr->mrp.bor == fptr->mrp.roff[fptr->mrp.rtop] )
               fptr->off = fptr->mrp.roff[fptr->mrp.rtop - 1] ;

            //* Adjust horizontal offset. *
            if ( fptr->flow == floShift )
            {
               int wChars ;
               fptr->shft = horizShift ( fptr->txt.gscols(wChars), 
                                         fptr->mrp.roff[fptr->mrp.rcur - 1],
                                         fptr->ip, (fptr->wid - 1) ) ;
            }
         }
         fptr->txt.erase( fptr->ip, 1 ) ;
         //* Reformat the text data, and if IP is outside *
         //* the visible space (referencing non-displayed *
         //* newline), step back into visible space.      *
         if ( (this->formatFieldText ( skfp, skfp->fi )) )
         { --fptr->ip ; this->centerFieldIP ( fptr, false, false ) ; }
      }
      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_DELCHAR != 0
      if ( ofsdbg.is_open() )
      {
         // Insert a visual cue at IP
         ipChar = fptr->txt.gstr()[fptr->ip] ;
         gsdbg = fptr->txt ;
         if ( ipChar == NEWLINE ) { gsdbg.insert( wcsSTAB, fptr->ip ) ; }
         else { gsdbg.replace( ipChar, wcsFISHEYE, fptr->ip ) ; }
         if ( (nli = gsdbg.find( NEWLINE, (fptr->ip + 2) )) >= ZERO )
            gsdbg.limitChars( nli - 1 ) ;
         ofsdbg << gsdbg.ustr() << "\n   --------" << endl ;
      }
      #endif // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_DELCHAR
   }

   #undef DEBUG_DELCHAR
}  //* End skpDeleteChar() *

//*************************
//*   skpClearSelection   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Clear text selection (if any).                                               *
//* 1) For each field, set 'sela' and 'selb' to zero.                            *
//*    This indicates that there is no selection in the current field.           *
//* 2) If there is a visible selection (underlined text) in the field, redraw    *
//*    the field text without the underline.                                     *
//* 3) Initialize the skForm 'seli' member to indicate that no field contains    *
//*    a selection.                                                              *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi indexes target field)     *
//*          refresh : (optional, default: 'true')                               *
//*                    if 'true',  refresh the field with focus                  *
//*                    if 'false', do not refresh                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::skpClearSelection ( skForm* skfp, bool refresh )
{
   for ( short f = ZERO ; f < skfp->fCnt ; ++f )
   {
      skfp->fld[f].sela = skfp->fld[f].selb = ZERO ;
      skfp->fld[f].state = false ;        // reset the "highlight" flag
      if ( (f == skfp->seli) && refresh )
         this->refreshField ( skfp, f ) ; // redraw field text without selection
   }
   skfp->seli = skfp->fCnt ;              // reset the selection index

}  //* End skpClearSelection() *

//*************************
//*       skpIp2Cur       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Calculate cursor offset from the field origin corresponding to the           *
//* insertion point 'ip' member (or the specified alternate ip value).           *
//*                                                                              *
//* Input  : fptr   : pointer to target field                                    *
//*          iptrg  : indicates the target index for calculating cursor offset   *
//*                   (Although defined as a 'short', it is actually a member)   *
//*                   (of enum ipoTarget which has only file scope.          )   *
//*          ipval  : (optional, null pointer by default)                        *
//*                   If specified, this is a pointer to the 16-bit variable to  *
//*                   receive the calculated IP value (or a copy of fptr->ip).   *
//*                                                                              *
//* Returns: zero-based row/column cursor offset from field origin               *
//********************************************************************************
//* Programmer's Note:                                                           *
//* The calculation is a bit kludgy and esoteric, but it is a functional         *
//* first effort. The algorithm may become smoother in subsequent releases.      *
//*                                                                              *
//* This method is under-utilized. It is currently called only by the Write()    *
//* group, which targets only the current IP. Therefore, the 'iptrg' parameter   *
//* is basically unnecessary.                                                    *
//*                                                                              *
//********************************************************************************

WinPos ACWin::skpIp2Cur ( skField *fptr, short iptrg, short *ipval )
{
   WinPos wpOff( 0, 0 ) ;           // return value (initially at field origin)
   gString gsParse ;                // analysis of parsed text
   int nIndx = (fptr->txt.gschars() - 1), // index of source data null terminator
       chCnt,                       // total source charcters (loop control)
       colAcc = ZERO,               // column accumulator
       si = ZERO ;                  // index into source buffer
   const wchar_t *wptr ;            // pointer to source data
   const short *colArray ;          // pointer to array of column width

   //* Note: Default target is current IP and alternate target  *
   //* is top-of-text (ipoToT). If target is neither of these,  *
   //* then this value will be calculated below.                *
   int trgIp = (iptrg == ipoToT) ? ZERO : (fptr->ip - fptr->shft) ;
   if ( ipval != NULL )
      *ipval = trgIp ;

   //* If target IP is field origin (ipoToT), then 'wpOff'  *
   //* already contains the logical cursor position.        *
   //* Otherwise, calculate logical offset to target IP.    *
   if ( (iptrg != ipoToT) && (nIndx > ZERO) ) 
   {
      //* Single-row fields *
      if ( fptr->hgt == 1 )
      {
         //* For single-row fields, load source text as written.*
         gsParse = fptr->txt ;
         wptr = gsParse.gstr() ;
         colArray = gsParse.gscols( chCnt ) ;
         nIndx = gsParse.gschars() - 1 ;

         if ( iptrg == ipoCur )              // reference current IP
         {
            trgIp = fptr->ip ;
         }
         else if ( iptrg == ipoEoT )         // reference end-of-text
         {
            trgIp = nIndx ;
         }
         else if (iptrg == ipoEoV)           // reference end-of-visible-text
         { /* Not currently implemented for single-row fields */ }
         else
         {
            //* ipoToR and ipoEoR do not apply to single-row *
            //* fields, so we should never arrive here.      *
         }

         //* If specified, copy target IP to caller's variable.*
         if ( ipval != NULL ) { *ipval = trgIp ; }

         //* The logical column-offset is the zero-based offset from *
         //* the first visible character of the row to the IP.       *
         for ( si = fptr->shft ; si < chCnt ; ++si )
         {
            colAcc += colArray[si] ;      // add width of reference character

            //* If a newline but not yet at IP *
            if ( (wptr[si] == NEWLINE) && (si < trgIp) )
            {
               // Programmer's Note: We rely here on the fact that the parsing 
               // routine will only include a newline in the parsed data IF 
               // additional data follow it. To put it another way, the data 
               // should never end with a trailing newline.
               ++wpOff.row ;
               colAcc = ZERO ;
            }

            //* If IP reached, or if end-of-text *
            if ( (si == trgIp) || (wptr[si] == NULLCHAR) )
            {
               wpOff.col = colAcc - colArray[si] ; // save the column offset

               //* If overrun of right edge of field, move left *
               //* by width of last character displayed on row. *
               if ( wpOff.col >= fptr->wid )
               {
                  wpOff.col = fptr->wid - colArray[si - 1] ;
                  if ( ipval != NULL )
                     *ipval = --trgIp ;
               }
               break ;     // done
            }
         }  // for(;;)

      }     // single-row field

      //* Multi-row fields *
      else  // (fptr->hgt > 1)
      {
         //* Calculate logical cursor position for _current_ IP.*
         //* Caller can then compare it with the returned value.*
         this->parseFieldText ( fptr, ZERO ) ;

         wptr = fptr->txt.gstr() ;           // pointer to character array
         colArray = fptr->txt.gscols( chCnt ) ; // pointer to column-width array

         if ( iptrg == ipoCur )              // reference current IP
            trgIp = fptr->ip ;
         else if ( iptrg == ipoEoT )         // reference end-of-text
            trgIp = fptr->mrp.ni ;
         else if ( iptrg == ipoEoR )         // reference end-of-row
            trgIp = fptr->mrp.eor ;
         else if ( iptrg == ipoToR )         // reference top-of-row
            trgIp = fptr->mrp.rcur ;
         else if ( iptrg == ipoEoV )         // reference end-of-visible-text
            ; /* Not currently implemented */

         //* If specified, copy target IP to caller's variable.*
         if ( ipval != NULL ) { *ipval = trgIp ; }

         //* Calculate row offset: current row - top visible row *
         wpOff.row = fptr->mrp.rcur - fptr->mrp.rtop ;

         //* Calculate column offset. Limit is end of visible text. *
         for ( short si = fptr->mrp.rcur ; si <= fptr->mrp.eov ; ++si )
         {
            colAcc += colArray[si] ;         // add width of current character
            //* If IP reached, or if end-of-text *
            if ( (si == trgIp) || (wptr[si] == NULLCHAR) )
            {
               wpOff.col = colAcc - colArray[si] ; // save the column offset

               //* If overrun of right edge of field, move left *
               //* by width of last character displayed on row. *
               if ( wpOff.col >= fptr->wid )
               {
                  wpOff.col = fptr->wid - colArray[si - 1] ;
                  if ( ipval != NULL )
                     *ipval = --trgIp ;
               }
               break ;     // done
            }
         }     // for(;;)

         return ( wpOff ) ;

      }     // multi-row field
   }        // (iptrg != ipoToT)

   return wpOff ;       // return field-relative cursor position

}  //* End skpIp2Cur() *

//*************************
//*     normalkeyProc     *
//*************************
//********************************************************************************
//* Private Method: Called by EditForm and editDialog methods.                   *
//* ---------------                                                              *
//* Add a normal (printing, "graphing") character to the text field with the     *
//* input focus (skfp->fi member)                                                *
//*                                                                              *
//* -- If Insert Mode, insert the new character into the text field, adjust      *
//*    the cursor position and refresh the text in the field.                    *
//* -- If Overstrike Mode, replace the character under the cursor with the       *
//*    new character, adjust the cursor position and refresh the text in the     *
//*    field.                                                                    *
//*                                                                              *
//* Note: The keycodes NEWLINE and NULLCHAR are control codes and should not     *
//*       arrive here, but if they do, they will be silently discarded.          *
//* Note: Although the special keycodes, GUI keys and other command keycodes     *
//*       are _technically_ also defined as graphing characters, if they arrive  *
//*       here, they will be discarded, _not_ displayed.                         *
//* Note: If the 'so' flag is set, then "normal" keys are not processed.         *
//*       Field text will not be allowed.                                        *
//*                                                                              *
//* Input  : skfp : pointer to skForm object (skfp->fi references target field)  *
//*          wkey : keycode to be processed                                      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) Single-row fields are the most common fields, and they are also the       *
//*    easiest to implement and fastest to process; therefore, they are handled  *
//*    seperately.                                                               *
//* 2) Multi-row fields configured for text-editor mode (fptr->flow==floShift).  *
//*    The data in these fields are not reformatted as often as "flowing" text   *
//*    data. Instead the visible text is shifted horizontally to accomodate the  *
//*    inserted character. This is considerably faster than reformatting the     *
//*    data from the insertion point, onward each time a character is entered.   *
//* 3) Multi-row fields configured for word-processing mode.                     *
//*    These fields perform dynamic wrapping at the end of each text row.        *
//*    While this is fancy, it also requires a lot of CPU cycles. The algorithm  *
//*    here is optimized for speed; however, algorithmic simplicity for ease of  *
//*    maintenance must also be considered. Balance in all things, Grasshopper.  *
//*    See list of issues, below.                                                *
//*                                                                              *
//*                                                                              *
//* Issues with formatting of flowing text:                                      *
//* =======================================                                      *
//* When the character added to the row causes the text data to extend beyond    *
//* the width of the field, it is necessary to insert a line break to move some  *
//* text to the next row. Line breaks are inserted just before the last token    *
//* of the row. A "token" is a group of characters delimited by certain          *
//* characters, most often the space character ' ' (20h), but some punctuation   *
//* characters (e.g. dash '-') may also be defined as delimeters.                *
//*                                                                              *
//* Isolating the Token:                                                         *
//* --------------------                                                         *
//* Scanning the source text to identify the target delimeter is conceptually    *
//* straightforward; however, there are some technical issues and design         *
//* decisions to be made.                                                        *
//*                                                                              *
//* 1) targeted token delimeter is after IP:                                     *
//*    a) Referenced token is wrapped to next row, and IP remains on current row.*
//* 2) targeted token delimeter is before IP:                                    *
//*    IP references a character within the token.                               *
//*    The token containining IP is wrapped to the next row, and IP is           *
//*    incremented to account for the added newline.                             *
//*    a) Special case: If IP references the newline which terminates the        *
//*       current row text, then that newline is deleted so the target token     *
//*       is integrated into the text of the next row.                           *
//* 3) When a token at the beginning of line N is broken into tokens A1 and A2   *
//*    by insertion of a space (or hyphen), then token A1 _may_ fit on row N-1.  *
//*    If so, then the newline at the end of N-1 must be deleted so that         *
//*    reformatting of the data will reposition token A1.                        *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

void ACWin::normalkeyProc ( skForm *skfp, wchar_t wkey )
{
   #define DEBUG_NKP (0)                  // for debugging only

   //* The following keycodes are not printing characters.     *
   //* Also, normal (printing) characters may be written ONLY  *
   //* into Textbox controls.                                  *
   //* Note the early return. See note above.                  *
   if ( (wkey == NEWLINE) || (wkey == NULLCHAR) || 
        (this->skf->fld[this->skf->fi].type != ftTB) )
   { return ; }

   //* If a command key or special key, discard it.       *
   //* Note the early return. See note above.             *
   if ( (this->isCommandKey ( wkey, true )) )
   { if ( this->skf->beep ) { this->acBeep () ; } return ; }


   skField *fptr = &skfp->fld[skfp->fi] ;         // pointer to active field
   wchar_t refChar = fptr->txt.gstr()[fptr->ip] ; // character referenced by 'ip'

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_NKP != 0
   wchar_t rc = ((refChar == NEWLINE) || (refChar == NULLCHAR)) ? L'^' : refChar,
           rc2 = rc == NULLCHAR ? NULLCHAR : fptr->txt.gstr()[fptr->ip + 1] ;
   if ( (rc2 == NEWLINE) || (rc2 == NULLCHAR) ) rc2 = L'^' ;
   if ( ofsdbg.is_open() )
   {
      gsdbg.compose( "normalkeyProc( wkey:%C ip:%2hd refChar:%C,%C cur:%2hd,%2hd )", 
                     &wkey, &fptr->ip, &rc, &rc2, &fptr->cur.row, &fptr->cur.col ) ;
      ofsdbg << gsdbg.ustr() << endl ;
   }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_NKP

   //* Single-row and multi-row fields configured for text-editor mode. *
   if ( fptr->flow == floShift )
   {
      if ( refChar == NULLCHAR )          // if at end-of-text, append character
      { fptr->txt.append( wkey ) ; ++fptr->ip ; }
      else if ( skfp->ins )               // insert mode
         fptr->txt.insert( wkey, fptr->ip++ ) ;
      else                                // overstrike mode
         fptr->txt.replace( refChar, wkey, fptr->ip++ ) ;

      //* Update indexing and logical cursor. If return value indicates *
      //* that IP is now outside visible space, adjust horizontal shift.*
      if ( (this->parseFieldText ( fptr )) )
         this->centerFieldIP ( fptr, false, false ) ;
   }

   //* Multi-row fields configured for flowing text. *
   else
   {
      wchar_t wrow[dctLEN + 1] ;          // allocate row buffer
      const wchar_t* wptr = &fptr->txt.gstr()[fptr->mrp.bor] ; // point to current source row
      gString gsRow ;                     // for text analysis
      short si = ZERO, ti = ZERO,         // source/target indices
            rcols,                        // number of columns in row
            toki = fptr->mrp.bor ;        // index of last token in row
      bool reformat = false ;             // set if full reformat required

      //* Insert the given character at the IP position and *
      //* advance IP to character following the insertion.  *
      if ( refChar == NULLCHAR )          // append character to end-of-text
      { fptr->txt.append( wkey ) ; ++fptr->ip ; }
      else if ( skfp->ins )               // (insert mode) insert character at IP
      { fptr->txt.insert( wkey, fptr->ip++ ) ; }
      else                                // (overstrike mode) replace character at IP
      {
         fptr->txt.replace( refChar, wkey, fptr->ip++ ) ;
         if ( refChar == NEWLINE )        // if newline overwritten
            reformat = true ;
      }

      //* Isolate the text of the current row and count the columns.*
      while ( wptr[si] != NEWLINE && wptr[si] != NULLCHAR && si < dctLEN )
         wrow[ti++] = wptr[si++] ;        // copy all chars in the row EXCEPT terminator
      wrow[ti] = NULLCHAR ;               // terminate the captured data
      gsRow = wrow ;                      // load data for analysis
      rcols = gsRow.gscols() ;            // number of columns in the current row

      //* If width of row text is greater than width *
      //* of field, adjust text formatting.          *
      if ( rcols >= fptr->wid )
      {
         if ( fptr->flow == floChar )
         {
            //* If new IP is now beyond the current row, advance IP to   *
            //* next row to compensate for addition of newline character.*
            if ( fptr->ip > fptr->mrp.eor )
               ++fptr->ip ;
            //* Delete all newlines from current row onward *
            //* so that trailing data will be reformatted.  *
            fptr->txt.erase( NEWLINE, fptr->mrp.bor, false, true ) ;
         }
         else  // (flow == floToken || flow == floHyphen)
         {
            //* Set 'token index' on last character of source row. If   *
            //* referenced character is a newline, delete the newline.  *
            toki = fptr->mrp.bor + gsRow.gschars() - 1 ;
            if ( fptr->txt.gstr()[toki] == NEWLINE )
            { fptr->txt.erase( toki, 1 ) ; --toki ; }
            //* If last character of row is a space, then it does not   *
            //* delimit a token, so step over it.                       *
            if ( fptr->txt.gstr()[toki] == wcsSPACE )
               --toki ;

            //* Scan leftward to find beginning of token and insert  *
            //* a line break at beginning of last token in row.      *
            //* Note that it is possible (but unlikely) that no      *
            //* token delimeter will be found. In that case, no line *
            //* break will be inserted.                              *
            while ( toki > fptr->mrp.bor )
            {
               if ( fptr->txt.gstr()[--toki] == wcsSPACE )
               { fptr->txt.insert( NEWLINE, ++toki ) ; break ; }
            }
            //* Adjust IP to compensate for reformatted text. *
            if ( fptr->ip > toki )
               ++fptr->ip ;
         }
         reformat = true ;
      }  // (rcols >= fptr->wid)

      if ( reformat )                     // reformat the data
         this->formatFieldText ( skfp, skfp->fi ) ;
      else                                // update data indexing
         this->parseFieldText ( fptr ) ;
   }

   #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_NKP != 0
   rc  = fptr->txt.gstr()[fptr->ip] ;
   rc2 = fptr->txt.gstr()[fptr->ip + 1] ;
   if ( (rc  == NEWLINE) || (rc  == NULLCHAR) ) rc  = L'^' ;
   if ( (rc2 == NEWLINE) || (rc2 == NULLCHAR) ) rc2 = L'^' ;
   if ( ofsdbg.is_open() )
   {
      gsdbg.compose( "             (        ip:%2hd refChar:%C,%C cur:%2hd,%2hd )", 
                     &fptr->ip, &rc, &rc2, &fptr->cur.row, &fptr->cur.col ) ;
      ofsdbg << gsdbg.ustr() << endl ;
   }
   #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_NKP

   //* Refresh the display and set visible cursor to new position.*
   this->refreshField ( skfp, skfp->fi ) ;

   #undef DEBUG_NKP
}  //* End normalkeyProc() *

//*************************
//*       clearWin        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Clear the window area using the specified background color attribute.        *
//*                                                                              *
//* Input  : acaEx : specifies the background color attribute to be used when    *
//*                  clearing the area. (this is an initialized acaExpand object)*
//*          all   : (optional, 'false' by default) specify target area          *
//*                  if 'false'; erase the interior area only                    *
//*                  if 'true',  erase the entire window including border        *
//*                                                                              *
//* Returns: nothing                                                             *
//*          Important Note: On return, the background attribute remains set to  *
//*                          the specified attribute, and 'txtOff' is set to     *
//*                          origin (upper-left corner of text area).            *
//********************************************************************************

void ACWin::clearWin ( const acaExpand& acaEx, bool all )
{
   short ulr = this->txtBase.row,
         ulc = this->txtBase.col,
         hgt = this->txtRows,
         wid = this->txtCols ;

   //* If border is also to be erased *
   if ( all != false )
   {
      ulr = this->winBase.row,
      ulc = this->winBase.col,
      hgt = this->height,
      wid = this->width ;
   }

   //* Set the background color attribute *
   this->setAttributes ( acaEx, false, true ) ;

   //* Clear the area *
   this->acClearArea ( ulr, ulc, hgt, wid ) ;

   //* Set cursor position *
   this->txtOff = { 0, 0 } ;
   this->SetCursorPos ( this->txtOff ) ;

}  //* End clearWin() *

//*************************
//*       SetTitle        *
//*************************
//********************************************************************************
//* Set the window title.                                                        *
//* Draw the title centered in the top border of the window.                     *
//* Example:      ┌───────┤ ACWin-class Window ├───────┐                         *
//* -- It is recommended that the first and last characters of the text be       *
//*    space characters as shown in the example.                                 *
//* -- The border will occupy the remainder of the top row, with terminator      *
//*    characters on each end.                                                   *
//* -- If necessary, the specified text will be truncated to fit the available   *
//*    space. In this case, the returned value will be 'false'.                  *
//* -- Optionally, the text can be rendered in an alternate color.               *
//* -- Optionally, a new border style can be specified. This parameter is not    *
//*    very useful, but it was implemented for testing purposes and remains      *
//*    available.                                                                *
//*                                                                              *
//* Input  : newTitle : text of window title                                     *
//*                     (empty string or null pointer erases title)              *
//*          titAttr  : (optional, existing border attribute by default)         *
//*                     If specified, this is a logical OR of acAttr values      *
//*                     specifying the foreground/background color attributes.   *
//*                     See the 'acAttr' enumerated type for details.            *
//*          bdrLine  : (optional, ltHORIZ i.e. invalid line type by default)    *
//*                     if specified, new border style: ltSINGLE or ltDUAL       *
//*                                                                              *
//* Returns: 'true'  if title set as specified                                   *
//*          'false' if title truncated to fit window width                      *
//********************************************************************************

bool ACWin::SetTitle ( const char* newTitle, acAttr titAttr, LineType bdrLine )
{
   gString gs( newTitle ) ;               // text formatting
   short maxTit = this->width - 4 ;       // max columns available for title text
   bool  status = true ;                  // return value

   if ( (gs.gscols()) > maxTit )          // if necessary, truncate to fit
   {
      gs.limitCols( maxTit ) ;
      status = false ;
   }
   gs.copy( this->title, gsALLOCMIN ) ;   // save the new title

   //* Optionally update the border line style *
   if ( (bdrLine == ltSINGLE) || (bdrLine == ltDUAL) )
      this->bdrStyle = bdrLine ;

   //* Redraw the border with the new title.*
   this->drawBorder ( titAttr ) ;

   //* Restore current terminal attributes *
   this->restoreAttributes () ;

   return status ;

}  //* End SetTitle() *

//*************************
//*      SetTextAttr      *
//*************************
//********************************************************************************
//* Set the color attributes and modifiers for the window text area.             *
//* This method decodes and stores the new attributes for the text area of       *
//* the window. New text written to the text area will be displayed using        *
//* the specified attributes.                                                    *
//*                                                                              *
//* Important Note: The terminal attributes are not updated here. The specified  *
//*                 attributes will be used for _subsequent_ writes.             *
//*                                                                              *
//* Input  : txtAttr : (acAttr bitfield) a logical OR of forground and           *
//*                    background color attributes and attribute modifiers       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::SetTextAttr ( acAttr txtAttr )
{

   //* Decode the text attribute.*
   this->txtAttr.decode( txtAttr ) ;

}  //* End SetTextAttr() *

//*************************
//*     SetBorderAttr     *
//*************************
//********************************************************************************
//* Set the color attributes and modifiers for the window border and redraw      *
//* the border. Optionally, sets the border style.                               *
//*                                                                              *
//* On return, the cursor is positioned in the field with input focus.           *
//*                                                                              *
//* This method strips the text modifiers that make no sense for the border.     *
//* Only the 'bold text' modifier makes any sense in this context.               *
//*                                                                              *
//* Input  : bdrAttr : (acAttr bitfield) a logical OR of forground and           *
//*                    background color attributes and attribute modifiers       *
//*          bdrStyle: (optional, ltHORIZ i.e. an invalid value by default)      *
//*                    specify the border line style, either ltSINGLE or ltDUAL  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::SetBorderAttr ( acAttr bdrAttr, LineType bdrStyle )
{
   //* For borders, some text-modifier attributes     *
   //* are meaningless or just plain ugly. Clear them.*
   bdrAttr = acAttr(bdrAttr & 
                    ~(acaITALIC | acaUNDERLINE | acaOVERLINE | 
                      acaXOUT | acaCONCEAL)) ;

   //* Decode the border attribute.*
   this->bdrAttr.decode( bdrAttr ) ;

   //* If a _valid_ alternate border style was specified *
   if ( bdrStyle != ltHORIZ )
   {
      if ( bdrStyle != ltDUAL )
         bdrStyle = ltSINGLE ;
      this->bdrStyle = bdrStyle ;
   }

   //* Redraw the border *
   this->drawBorder () ;

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

}  //* End SetBorderAttr() *

//*************************
//*    FormatParagraph    *
//*************************
//********************************************************************************
//* Reformat the provided text to fit within the specified dimensions.           *
//*  1) Remove all newlines from the text.                                       *
//*  2) Perform word wrap to limit width of each row of the paragraph.           *
//*  3) If necessary, truncate the text to limit the height of the paragraph.    *
//*     (but see the 'trunc' and 'truncIndex' parameters)                        *
//*                                                                              *
//* This is a pass-through to the gString-class method.                          *
//*                                                                              *
//* Input  : gsTxt    : on entry, contains raw message text                      *
//*                     on return, contains formatted message text               *
//*          maxRows  : maximum rows for message (>= 1)                          *
//*          maxCols  : maximum columns on any message row (>= 4)                *
//*          trunc    : (optional, 'true' by default)                            *
//*                     if 'true',  truncate the data if necessary to ensure     *
//*                                 that the data do not extend beyond the       *
//*                                 specified target area                        *
//*                     if 'false', format the entire source text array, even    *
//*                                 if doing so requires violating the specified *
//*                                 height of the target area (maxRows).         *
//*                                 (see also 'truncIndex' parameter)            *
//*          hyphenbrk: (optional, 'false' by default)                           *
//*                     if 'false', automatic line breaks occur at space ' '     *
//*                                 characters only. U+0020 and U+3000           *
//*                                 Special Case: the following CJK ideographs   *
//*                                 are also recognized as whitespace            *
//*                                 (see documentation for details)              *
//*                                  '、' comma U+3001 and                        *
//*                                  '。' full stop U+3002                        *
//*                     if 'true',  in addition to the space ' ' characters,     *
//*                                 enable line break at:                        *
//*                                  ASCII hyphen    '-' (U+002D)                *
//*                                  Unicode &mdash; '—' (U+2014)                *
//*                                  Unicode &ndash; '–' (U+2013)                *
//*                                  Unicode &minus; '−' (U+2212)                *
//*                                  Unicode &shy;       (U+00AD) (soft hyphen)  *
//*          truncIndex: (optional, null pointer by default)                     *
//*                      -- referenced _only_ when the 'trunc' flag is reset     *
//*                      if specified, points to a variable to receive the       *
//*                      index at which the data _would_have_been_ truncted to   *
//*                      fit the specified height of the target area if the      *
//*                      'trunc' flag had been set.                              *
//*                      a) A positive value indicates that the data have not    *
//*                         been truncated, AND that the data extend beyond      *
//*                         the specified target area.                           *
//*                      b) A negative value indicates that it was not necessary *
//*                         to truncate the data i.e. the data fit entirely      *
//*                         within the target area.                              *
//*                                                                              *
//* Returns: number of text rows in formatted paragraph                          *
//********************************************************************************

short ACWin::FormatParagraph ( gString& gsTxt, short maxRows, short maxCols, 
                               bool trunc, bool hyphenbrk, int *truncIndex )
{

   return ( (gsTxt.formatParagraph( maxRows, maxCols, trunc, hyphenbrk, truncIndex )) ) ;

}  //* End FormatParagraph() *

//*************************
//*         Write         *
//*************************
//********************************************************************************
//* Write text to the interior of the window.                                    *
//*                                                                              *
//* This group of methods writes directly into the window's text area,           *
//* bypassing any defined text fields.                                           *
//*                                                                              *
//* Notes:                                                                       *
//* ------                                                                       *
//* -- If source data are pre-formatted and positioned to fit within the         *
//*    dimensions of the window, the source is written as provided.              *
//* -- If the source data are unformatted (or incorrectly formatted), this       *
//*    method employs a simple automatic line break algorithm to re-format       *
//*    the data. See the documentation for the formatParagraph() method which    *
//*    describes the automatic text formatting algorithm.                        *
//* -- The output is limited by the dimensions of the window (rows and columns). *
//*    This prevents incursion into or beyond the window border.                 *
//*                       -----------------                                      *
//*                                                                              *
//* Notes on storage of the text data written directly to the window interior:   *
//*  a) The number of direct writes to the interior of the window (bypassing     *
//*     the window controls) is limited to DB_MAX seperate writes without        *
//*     loss of data.                                                            *
//*     These writes are stored in a FIFO buffer, so additional writes beyond    *
//*     DB_MAX will cause the stored data for the oldest write to be discarded.  *
//*  b) Storage of each seperate write is limited to DB_BUFFSIZE which is the    *
//*     size of a data-block storage buffer.                                     *
//* While these limits will seldom (if ever) come into play, be aware.           *
//* Please see the documentation for more information on the DataBlock FIFO.     *
//*                       -----------------                                      *
//*                                                                              *
//* Input  : txtPos : WinPos-class object indicates the zero-based               *
//*                   OFFSET from upper-left corner of window interior           *
//*          txtData: text to write in one of the following formats:             *
//*                   -- gString object by reference                             *
//*                   -- wchar_t pointer                                         *
//*                   -- char pointer                                            *
//*                   -- a single wchar_t character                              *
//*          txtAttr: (optional, default: existing text attribute (acaUSEDFLT))  *
//*                   text attributes and text modifiers for this write ONLY.    *
//*                   Window attributes unchanged.                               *
//*          clear  : (optional, 'false' by default)                             *
//*                   if 'false', existing displayed text unmodified             *
//*                   if 'true',  erase window interior before write             *
//*                               (erased using existing bgnd color)             *
//*                                                                              *
//* Returns: current cursor position (window offset)                             *
//********************************************************************************

WinPos ACWin::Write ( WinPos txtPos, const wchar_t* txtData, acAttr txtAttr, bool clear )
{
   const wchar_t errCHAR = L'?' ;         // indicator for non-printing characters
   const short wbLEN = this->txtRows * this->txtCols + this->txtRows * 2 ;

   wchar_t *wbuff = new wchar_t[wbLEN] ;  // work buffer (dynamic allocation)
   acaExpand altAttr ;                    // decode color attributes
   gString gs( wbLEN ), gstmp ;           // text formatting and analysis
   wchar_t wchar ;                        // character under test
   int srcIndx = ZERO,                    // index into 'txtData' array
       trgIndx = ZERO ;                   // index into 'wbuff' array
   short rowsavail, colsavail ;           // space available for output
   short txtrows, txtcols ;               // dimensions of formatted text

   //* Range check the provided offsets *
   if ( (txtPos.row < ZERO) || (txtPos.row >= this->txtRows) )
      txtPos.row = ZERO ;
   if ( (txtPos.col < ZERO) || (txtPos.col >= this->txtCols) )
      txtPos.col = ZERO ;

   //* Calculate size of output area and segment size *
   rowsavail = this->txtRows - txtPos.row ;
   colsavail = this->txtCols - txtPos.col ;

   //* Set local offset-tracking member to initial position.  *
   //* Note that the final 'txtOff' value is the return value.*
   this->txtOff = txtPos ;

   //* Convert window-relative zero-based offset to *
   //* terminal-window one-based offset.            *
   WinPos wp( this->txtBase, this->txtOff ) ;

   //* If specified, clear window interior (use stored attributes).*
   if ( clear != false )
      this->clearWin ( this->txtAttr ) ;

   //* Set the text attributes, either previously established interior  *
   //* attribute, or alternate attributes specified for this write only.*
   if ( txtAttr == acaUSEDFLT )
      this->setAttributes ( this->txtAttr ) ;
   else
   {
      altAttr.decode( txtAttr ) ;
      this->setAttributes ( altAttr ) ;
   }

   //* Copy data to work buffer, stripping any control codes. *
   while ( (txtData[srcIndx] != NULLCHAR) && (trgIndx < (wbLEN - 2)) )
   {
      wchar = txtData[srcIndx++] ;        // read a character from source

      //* If a control character, substitute    *
      //* a '?' indicating an invalid character.*
      if ( (wchar != NEWLINE) && (iswcntrl ( wchar )) != ZERO )
         wchar = errCHAR ;
      wbuff[trgIndx++] = wchar ;
   }
   wbuff[trgIndx] = NULLCHAR ;            // terminate the string

   //* Store the data (see note above about text limits). *
   //* Give attention to how the indices are incremented. *
   if ( this->saveWrite )
   {
      this->fifo->saveRecord ( wbuff, txtPos, 
                               ((txtAttr == acaUSEDFLT) ? this->txtAttr : altAttr) ) ;

      #if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0 && DEBUG_FIFO != 0
      this->dumpFifo2Log ( this->fifo, L"ACWin Write:" ) ;
      #endif   // DEBUG_ANSICMD && DEBUG_LOG && DEBUG_FIFO
   }

   //* Refresh the text dimensions.       *
   //* If text is too large for the space *
   //* available, truncate for display.   *
   // Programmer's Note: If value returned by formatParagraph() is greater than
   // 'txtrows', the data were truncated, but this doesn't matter for display.
   gs = wbuff ;
   this->ParagraphSize ( gs, txtrows, txtcols ) ;
   if ( (txtrows > rowsavail) || (txtcols > colsavail) )
      gs.formatParagraph ( rowsavail, colsavail, true, true ) ;

   if ( gs.gschars() > 1 )
      wp = this->acWrite ( wp, gs ) ;

   delete [] wbuff ;                      // release the dynamic alocation

   //* Convert terminal-oriented cursor position to window offset.*
   this->txtOff = { short(wp.row - this->txtBase.row), 
                    short(wp.col - this->txtBase.col) } ;

   //* Restore fgnd/bkgnd attributes to current terminal attributes.*
   this->acSetFgBg ( this->termAttr.aesFgnd, this->termAttr.aesBgnd ) ;

   return ( this->txtOff ) ;              // return the current cursor offset

}  //* End Write() *

WinPos ACWin::Write ( WinPos txtPos, const gString& txtData, acAttr txtAttr, bool clear )
{

   return ( this->Write ( txtPos, txtData.gstr(), txtAttr, clear )  ) ;

}  //* End Write() *

WinPos ACWin::Write ( WinPos txtPos, const char* txtData, acAttr txtAttr, bool clear )
{

   gString gs( txtData ) ;
   return ( this->Write ( txtPos, gs.gstr(), txtAttr, clear )  ) ;

}  //* End Write() *

WinPos ACWin::Write ( WinPos txtPos, wchar_t txtData, acAttr txtAttr, bool clear )
{

   gString gs( "%C", &txtData ) ;
   return ( this->Write ( txtPos, gs.gstr(), txtAttr, clear )  ) ;

}  //* End Write() *

//*************************
//*         write         *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Write formatted text to the interior of the window.                          *
//* Called by the public methods to write text to the window.                    *
//* This is primarily for refreshing the display of skField objects.             *
//* Caller is responsible for setting and restoring color attributes.            *
//*                                                                              *
//* Input  : pos   : (by reference) position for write indicates the zero-based  *
//*                  OFFSET from upper-left corner of window interior            *
//*          txt   : (by reference) text data to be written                      *
//*          newln : (optional, 'true' by default)                               *
//*                  specifies whether 'txt' contains newline characters         *
//*                  If set,   call the formatted-write method.                  *
//*                  If reset, call the fast tty method.                         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::write ( const WinPos& pos, const gString& txt, bool newln ) const
{
   //* Convert window-relative zero-based offset to *
   //* terminal-window one-based offset.            *
   WinPos wp( this->txtBase, pos ) ;

   //* Call with 'flush stream' set (this is the default). *
   if ( newln )
      this->acWrite ( wp, txt ) ;
   //* Fast write for single-line strings. *
   else
   { this->acSetCursor ( wp ) ; this->ttyWrite ( txt ) ; }

}  //* End write() *

//*************************
//*       writeSeq        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Write text to the interior of the window.                                    *
//* Unlike the public write methods, this method allows the specified text to    *
//* contain embedded escape sequences.                                           *
//*                                                                              *
//* Note: parameters are not range checked. We "trust" that caller is not a Bozo.*
//*                                                                              *
//* Input  : txtPos : WinPos-class object indicates the zero-based               *
//*                   OFFSET from upper-left corner of window interior           *
//*          txtData: pointer to text to be written                              *
//*          txtAttr: text attributes and text modifiers for this write ONLY.    *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::writeSeq ( const WinPos& txtPos, const wchar_t* txtData, acAttr txtAttr )
{

   this->setAttributes ( txtAttr == acaUSEDFLT ? this->txtAttr : txtAttr ) ;
   this->writeSeq ( txtPos, txtData ) ;
   this->restoreAttributes () ;

}  //* End writeSeq() *

//*************************
//*       writeSeq        *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Write text to the interior of the window.                                    *
//* Unlike the public write methods, this method allows the specified text to    *
//* contain embedded escape sequences.                                           *
//*                                                                              *
//* Note: This is the same as above EXCEPT THAT color attributs are not set     .*
//*       or restored (although the text itself may contain attribute sequences).*
//*                                                                              *
//* Input  : txtPos : WinPos-class object indicates the zero-based               *
//*                   OFFSET from upper-left corner of window interior           *
//*          txtData: pointer to text to be written                              *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: The 'txtOff' value will be stale after this write,        *
//* but it's better than requesting the cursor position and waiting all day.     *
//********************************************************************************

void ACWin::writeSeq ( const WinPos& txtPos, const wchar_t* txtData )
{

   //* Set "absolute" i.e. terminal-relative cursor position.*
   this->acSetCursor ( (this->txtBase.row + txtPos.row), 
                       (this->txtBase.col + txtPos.col) ) ;
   this->ttyWrite ( txtData ) ;

}  //* End writeSeq() *

//*************************
//*     GetCursorPos      *
//*************************
//********************************************************************************
//* Get the current cursor offset within the window's text area.                 *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: current cursor position (window offset)                             *
//********************************************************************************

WinPos ACWin::GetCursorPos ( void )
{

   return ( this->txtOff ) ;

}  //* End GetCursorPos() *

//*************************
//*     SetCursorPos      *
//*************************
//********************************************************************************
//* Set the cursor offset within the window's text area.                         *
//*                                                                              *
//* Input  : wPos : (by reference) offset into the text area of the window       *
//*                 (0-based offset)                                             *
//*                                                                              *
//* Returns: current cursor position (window offset)                             *
//********************************************************************************

WinPos ACWin::SetCursorPos ( const WinPos& wPos )
{
   this->txtOff = { ZERO, ZERO } ;           // initialize the offset

   if ( (wPos.row >= ZERO) && (wPos.row < this->txtRows) )  // Range check
      this->txtOff.row = wPos.row ;
   if ( (wPos.col >= ZERO) && (wPos.col < this->txtCols) )
      this->txtOff.col = wPos.col ;

   //* Set "absolute" i.e. terminal-relative cursor position.*
   this->acSetCursor ( (this->txtBase.row + this->txtOff.row), 
                       (this->txtBase.col + this->txtOff.col) ) ;

   return ( this->txtOff ) ;

}  //* End SetCursorPos() *

//* Overload using individual row/column arguments.*
WinPos ACWin::SetCursorPos ( short rOff, short cOff )
{

   WinPos wp( rOff, cOff ) ;
   return ( (this->SetCursorPos ( wp )) ) ;

}  //* End SetCursorPos() *

//*************************
//*      drawBorder       *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Draw the window border using the border style and color attributes stored    *
//* in the 'bdrStyle' and'bdrAttr' members (includes title (if defined)).        *
//*                                                                              *
//* Important Note: This methods sets, but DOES NOT RESTORE color attributes.    *
//*                 It is the caller's responsibility to adjust color attributes *
//*                 as desired after returning from this call.                   *
//*                                                                              *
//* Input  : titAttr: (optional, existing border attribute by default)           *
//*                   If specified, this is the alternate color for title text.  *
//*                   The border itself is drawn in the existing colors.         *
//*                                                                              *
//* Returns: nothing                                                             *
//*          Note: On return the fg/bg attributes are either the border colors   *
//*                _OR_ the specified alternate color attribute.                 *
//********************************************************************************

void ACWin::drawBorder ( acAttr titAttr )
{
   gString gsOut ;                        // text formatting
   wchar_t ul, ur, ll, lr, h, v, lt, rt ; // characters for drawing border

   //* Initialize the drawing characters according to the line type *
   switch ( this->bdrStyle )
   {
      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 ( this->bdrStyle )
         {
            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 ( this->bdrStyle )
         {
            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 ;
   } ;

   //* Set the border color attributes and optional text modifiers.*
   this->setAttributes ( this->bdrAttr ) ;

   //* Draw top border *
   gsOut.compose( "%C", &ul ) ;
   gsOut.padCols( this->width - 1, h ) ;
   gsOut.append( "%C", &ur ) ;
   this->acSetCursor ( this->winBase ) ;
   this->ttyWrite ( gsOut ) ;

   //* Create the vertical bar for left and right borders.*
   //* Draw the right border, then the left border.       *
   gsOut.clear() ;
   for ( short irows = 2 ; irows < this->height ; ++irows )
      gsOut.append( "%C\n", &v ) ;
   this->acWrite ( (this->winBase.row + 1), (this->winBase.col + this->width - 1), gsOut, false ) ;
   this->acWrite ( (this->winBase.row + 1), this->winBase.col, gsOut, false ) ;

   //* Draw bottom border *
   gsOut.compose( "%C", &ll ) ;
   gsOut.padCols( this->width - 1, h ) ;
   gsOut.append( "%C", &lr ) ;
   this->acSetCursor ( (this->winBase.row + this->height - 1), this->winBase.col ) ;
   this->ttyWrite ( gsOut ) ;

   //* If a title was specified, position it in the top border *
   if ( this->title[0] != '\0' )
   {  //* Center of top border row *
      WinPos wpb( this->winBase.row, (this->winBase.col + (this->width / 2)) ) ;

      gsOut = this->title ;                     // get title text
      gsOut.limitCols( this->width - 4 ) ;      // truncate title text if necessary
      gsOut.insert( rt ) ;                      // title delimeters
      gsOut.append( lt ) ;
      wpb.col -= gsOut.gscols() / 2 ;           // left column of title text
      if ( wpb.col <= this->winBase.col )       // (may be off-by-one)
         wpb.col = this->winBase.col + 1 ;
      this->acSetCursor ( wpb ) ;
      this->ttyWrite ( gsOut ) ;

      if ( titAttr != acaUSEDFLT )              // if using border colors
      {
         // Programmer's Note: If using alternate colors for title text, 
         // remove the delimeters from the string and overwrite the text ONLY.
         acaExpand tmpAttr ;
         tmpAttr.decode( titAttr ) ;
         this->setAttributes ( titAttr ) ;
         gsOut.shiftChars( -1 ) ;               // remove the delimiters
         gsOut.limitChars( gsOut.gschars() - 2 ) ;
         ++wpb.col ;
         this->acSetCursor ( wpb ) ;
         this->ttyWrite ( gsOut ) ;
      }
   }
}  //* End drawBorder()*

//*************************
//*   restoreAttributes   *
//*************************
//********************************************************************************
//* Private Method:                                                              *
//* ---------------                                                              *
//* Set the color attributes and modifiers to stored terminal-window values.     *
//*                                                                              *
//* The setting stored in the 'termAttr' member are assumed to be correct.       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void ACWin::restoreAttributes ( void )
{

   this->setAttributes ( this->termAttr ) ;

}  //* End restoreAttributes() *

//*************************
//*     ParagraphSize     *
//*************************
//********************************************************************************
//* Calculate the dimensions (height and width) of the specified block of text.  *
//*                                                                              *
//* Input  : gs        : (by reference) text to be analyzed                      *
//*          msgHeight : (by reference) receives number of text rows             *
//*          msgWidth  : (by reference) receives number of text columns          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Note that text may, or may not include a trailing newline just before the    *
//* null terminator. The test for this looks odd, but it avoids double-counting  *
//* the last text row.                                                           *
//*                                                                              *
//* Programmer's Note: This method is overloaded for pointers to int32_t.        *
//* We have found that it is too easy to get confused between 32-bit text        *
//* indices and 16-bit window coordinates.                                       *
//********************************************************************************

void ACWin::ParagraphSize ( const gString& gs, short& msgHeight, short& msgWidth )
{
   msgHeight = ZERO ;                     // height currently unknown
   msgWidth  = ZERO ;                     // width currently unknown

   if ( gs.gschars() == 1 )               // empty string (defensive programming)
   { /* caller is a moron */ }

   else if ( (gs.find( NEWLINE )) < ZERO )// single-row
   {
      msgHeight = 1 ;
      msgWidth  = gs.gscols() ;
   }

   else                                   // multi-row
   {
      int newCols = ZERO,                 // column-count accumulator
          txtChars ;                      // number of source characters
      const wchar_t *wptr = gs.gstr() ;   // pointer to character array
      const short *colarray = gs.gscols( txtChars ) ; // pointer to column-count array

      for ( short i = ZERO ; i < txtChars ; ++i )
      {
         newCols += colarray[i] ;
         if ( (wptr[i] == NEWLINE) || (wptr[i] == NULLCHAR) )
         {
            if ( (wptr[i] == NEWLINE) || (wptr[i-1] != NEWLINE) )
               ++msgHeight ;

            if ( newCols > msgWidth )
               msgWidth = newCols ;
            newCols = ZERO ;
         }
      }
   }

}  //* End ParagraphSize() *

void ACWin::ParagraphSize ( const gString& gs, int& msgHeight, int& msgWidth )
{
   short hgt, wid ;
   this->ParagraphSize ( gs, hgt, wid ) ;
   msgHeight = hgt ;
   msgWidth  = wid ;

}  //* End ParagraphSize() *

//*************************
//*        DrawBox        *
//*************************
//********************************************************************************
//* Draw a rectangle within the window's text area.                              *
//*                                                                              *
//* Input  : pos   : offset from upper-left corner of text area to               *
//*                  upper-left corner of box (0-based)                          *
//*          height: number of rows for box                                      *
//*          width : number of columns for box                                   *
//*          lType : (optional, ltSINGLE by default) line style                  *
//*                  member of enum LineType: (ltSINGLE or ltDUAL)               *
//*          bxAttr: (optional, acaUSEDFLT by default)                           *
//*                  If specified, these are the color attributes used.          *
//*                  If not specified, stored text attributes are used.          *
//*          clear : (optional, 'false' by default)                              *
//*                  if 'false', interior of box is cleared using 'bxAttr'       *
//*                  if 'true',  interior of box is cleared using the interior   *
//*                              color attributes of the parent window           *
//*                                                                              *
//* Returns: 'true'  if box fits within the window text area                     *
//*          'false' if invalid offset or dimensions specified                   *
//********************************************************************************

bool ACWin::DrawBox ( WinPos wpOrig, short height, short width, 
                      LineType lType, acAttr bxAttr, bool clear )
{
   bool status = false ;               // return value

   //* Range check the parameters *
   if ( (wpOrig.row >= ZERO) && (wpOrig.row < this->txtRows) &&
        (wpOrig.col >= ZERO) && (wpOrig.col < this->txtCols) &&
        ((wpOrig.row + height) <= this->txtRows) &&
        ((wpOrig.col + width) <= this->txtCols)
      )
   {
      acaExpand acax ;              // encode color attribute
      status = true ;               // declare success

      //* Convert offset (0-based) to terminal relative offset (1-based).*
      WinPos wp( this->txtBase, wpOrig ) ;

      //* Set the color attributes *
      if ( bxAttr == acaUSEDFLT )
         acax = this->txtAttr ;
      else
         acax.decode( bxAttr ) ;
      this->setAttributes ( acax ) ;

      //* Save the box definition to the storage FIFO. *
      if ( this->saveWrite )
      {
         gString gs( dbFifoTemplate, &height, &width, &lType, &clear ) ;
         this->fifo->saveRecord ( gs.gstr(), wpOrig, acax ) ;
      }

      //* Draw the box *
      this->acDrawBox ( wp, height, width, lType ) ;

      //* If box interior attribute is different from box attribute, *
      //* clear interior of box using the interior color.            *
      if ( (bxAttr != acaUSEDFLT) && (clear != false) )
      {
         this->setAttributes ( this->txtAttr ) ;
         this->acClearArea ( (wp.row + 1), (wp.col + 1), (height - 2), (width - 2) ) ;
      }

      this->restoreAttributes () ;  // restore terminal attributes
   }
   return status ;

}  //* End DrawBox() *

//*************************
//*       DrawLine        *
//*************************
//********************************************************************************
//* Draw a horizontal or vertical line within the window's text area.            *
//* Horizontal lines are drawn left-to-right.                                    *
//* Vertical lines are drawn top-to-bottom.                                      *
//*                                                                              *
//* This is a specialized instance of the AnsiCmd acDrawLine() method.           *
//* The only difference is that the parameters are range checked to fit entirely *
//* within the window.                                                           *
//* Note that if an endpoint of the line intersects the border of the window,    *
//* the endpoint will be drawn using the color attributes of the border, while   *
//* the portion of the line within the text area of the window will be drawn     *
//* using the color attributes of the window interior.                           *
//*                                                                              *
//* Note: If the line intersects the window border, the endpoint                 *
//*       character will be automatically selected to match the                  *
//*       intersection of the drawn line and the border line style.              *
//*       As a corollary to this automatic character selection,                  *
//*       avoid positioning the start of a line such that it would               *
//*       obscure the window title (if any).                                     *
//*                                                                              *
//* Input  : pos     : offset from upper-left corner of window (0-based)         *
//*          length  : length of the line: number of number of rows (vertical),  *
//*                    or number of columns (horizontal).                        *
//*          vertical: 'true'  : vertical line                                   *
//*                    'false' : horizontal line                                 *
//*          lType   : (optional, ltSINGLE by default) line style                *
//*                     member of enum LineType: either ltSINGLE or ltDUAL       *
//*          begChar : (optional, null character by default)                     *
//*                     specify the beginning endpoint character                 *
//*          endChar : (optional, null character by default)                     *
//*                     specify the final endpoint character                     *
//*          lnAttr  : (optional, acaUSEDFLT by default)                         *
//*                    If specified, these are the color attributes used.        *
//*                    If not specified, stored text attributes are used.        *
//*                                                                              *
//* Returns: cursor position at the end of the line:                             *
//*          For horizontal lines, the column following the last character       *
//*          (limited at right edge of window)                                   *
//*          For vertical lines, the column below the last character             *
//*          (limited at bottom edge of window)                                  *
//********************************************************************************

WinPos ACWin::DrawLine ( const WinPos& wpOrig, short length, bool vertical, 
                         LineType lType, wchar_t begChar, wchar_t endChar, acAttr lnAttr )
{
   gString gsOut ;                  // text formatting
   WinPos wp = wpOrig ;             // working copy of origin (and return value)
   WinPos wpTerm ;                  // terminal-relative cursor position
   bool bdrTerm = false ;           // 'true' if line terminates in the border

   if ( lType != ltDUAL )     lType = ltSINGLE ;   // limit line-type options

   //* Range check position of origin and line length *
   if ( length < 2 )       length = 2 ;      // minimum length == 2 endpoints
   if ( wp.row < ZERO )    wp.row = ZERO ;   // no negative offsets
   if ( wp.col < ZERO )    wp.col = ZERO ;
   if ( vertical )      //** vertical line **
   {
      if ( length > this->height )  length = this->height ;
      if ( (wp.row + length) > this->height )   wp.row = ZERO ;

      //* If line intersects the window border, set endpoint character(s),*
      //* and set border attributes and write character(s).               *
      if ( (wp.row == ZERO) || ((wp.row + length) == this->height) )
         this->setAttributes ( this->bdrAttr ) ;
      if ( wp.row == ZERO )                     // set begChar
      {
         if ( this->bdrStyle == ltDUAL )
         { begChar = (lType == ltDUAL) ? wcsTTEEd : wcsTTEEdh ; }
         else
         { begChar = (lType == ltDUAL) ? wcsTTEEdv : wcsTTEE ; }
         gsOut.compose( "%C\n", &begChar ) ;
         wpTerm = this->acWrite ( this->winBase.row, 
                                  short(this->winBase.col + wp.col), gsOut ) ;
         begChar = L'\0' ;
         ++wp.row ;
         if ( --length == 1 )    // write the final character
         {
            // Programmer's Note: This final character cannot intersect 
            // bottom border because minimum window height == 3.
            this->setAttributes ( this->txtAttr ) ;
            if ( endChar == L'\0' )   endChar = lType ;
            gsOut.compose( "%C\n", &endChar ) ;
            wpTerm = this->acWrite ( wpTerm, gsOut ) ;
            endChar = L'\0' ;
            ++wp.row ;
            length = ZERO ;      // line complete
         }
      }
      if ( (wp.row + length) == this->height )  // set endChar
      {
         if ( this->bdrStyle == ltDUAL )
         { endChar = (lType == ltDUAL) ? wcsBTEEd : wcsBTEEdh ; }
         else
         { endChar = (lType == ltDUAL) ? wcsBTEEdv : wcsBTEE ; }
         gsOut.compose( "%C", &endChar ) ;
         wpTerm = { short(this->winBase.row + this->height - 1),
                    short(this->winBase.col + wp.col) } ;
         this->acWrite ( wpTerm, gsOut ) ;   // write end char into bottom border
         endChar = L'\0' ;
         if ( --length == 1 )
         {
            // Programmer's Note: The initial character cannot intersect
            // the top border because minimum window height == 3.
            this->setAttributes ( this->txtAttr ) ;
            --wpTerm.row ;
            if ( begChar == L'\0' )   begChar = lType ;
            gsOut.compose( "%C\n", &begChar ) ;
            wpTerm = this->acWrite ( wpTerm, gsOut ) ;
            wp.row = this->height - 1 ;
            length = ZERO ;      // line complete
         }
         bdrTerm = true ;        // line terminates in the window border
      }
   }        //** vertical line **
   else     //** horizontal line **
   {
      if ( length > this->width )  length = this->width ;
      if ( (wp.col + length) > this->width )    wp.col = ZERO ;
      if ( (wp.col == ZERO) || ((wp.col + length) == this->width) )
         this->setAttributes ( this->bdrAttr ) ;
      if ( wp.col == ZERO )                     // set begChar
      {
         if ( this->bdrStyle == ltDUAL )
         { begChar = (lType == ltDUAL) ? wcsLTEEd : wcsLTEEdv ; }
         else
         { begChar = (lType == ltDUAL) ? wcsLTEEdh : wcsLTEE ; }
         gsOut.compose( "%C", &begChar ) ;
         wpTerm = this->acWrite ( short(this->winBase.row + wp.row), 
                                  this->winBase.col, gsOut ) ;
         begChar = L'\0' ;
         ++wp.col ;
         if ( --length == 1 )    // write the final character
         {
            // Programmer's Note: This final character cannot intersect 
            // right border because minimum window width == 3.
            this->setAttributes ( this->txtAttr ) ;
            if ( endChar == L'\0' )   endChar = lType ;
            gsOut.compose( "%C", &endChar ) ;
            wpTerm = this->acWrite ( wpTerm, gsOut ) ;
            endChar = L'\0' ;
            ++wp.col ;
            length = ZERO ;      // line complete
         }
      }
      if ( (wp.col + length) == this->width )   // set endChar
      {
         if ( this->bdrStyle == ltDUAL )
         { endChar = (lType == ltDUAL) ? wcsRTEEd : wcsRTEEdv ; }
         else
         { endChar = (lType == ltDUAL) ? wcsRTEEdh : wcsRTEE ; }
         gsOut.compose( "%C", &endChar ) ;
         wpTerm = { short(this->winBase.row + wp.row),
                    short(this->winBase.col + this->width - 1) } ;
         this->acWrite ( wpTerm, gsOut ) ;
         endChar = L'\0' ;
         if ( --length  == 1 )
         {
            // Programmer's Note: The initial character cannot intersect
            // the Left border because minimum window width == 3.
            this->setAttributes ( this->txtAttr ) ;
            --wpTerm.col ;
            if ( begChar == L'\0' )   begChar = lType ;
            gsOut.compose( "%C", &begChar ) ;
            wpTerm = this->acWrite ( wpTerm, gsOut ) ;
            wp.col = this->width - 1 ;
            length = ZERO ;      // line complete
         }
         bdrTerm = true ;        // line terminates in the window border
      }
   }        //** horizontal line **

   //* Convert window-relative origin to terminal relative *
   wpTerm.row = this->winBase.row + wp.row ;
   wpTerm.col = this->winBase.col + wp.col ;

   //* If line length is STILL at or above minimum *
   if ( length >= 2 )
   {
      //* Draw the portion of the line that lies in the interior of the window.*
      if ( lnAttr == acaUSEDFLT )
         this->setAttributes ( this->txtAttr ) ;
      else
      {
         acaExpand acax( lnAttr ) ;
         this->setAttributes ( acax ) ;
      }
      wpTerm = this->acDrawLine ( wpTerm, length, vertical, lType, begChar, endChar ) ;
   }

   //* Convert terminal-relative position to window-relative   *
   //* and ensure that final position stays within the window. *
   wp.row = wpTerm.row - this->winBase.row ;
   wp.col = wpTerm.col - this->winBase.col ;
   if ( bdrTerm )       // if line terminates in the window border
   {
      if ( vertical )   wp.row = this->height - 1 ;
      else              wp.col = this->width - 1 ;
   }
   if ( wp.row >= this->height )    wp.row = this->height - 1 ;
   if ( wp.col >= this->width )     wp.col = this->width - 1 ;

   this->restoreAttributes () ;     // restore terminal attributes
   return wp ;

}  //* End DrawLine() *

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

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

