/* Portable Windowed Applications: Pollsters, Onlookers, and Others Microsoft Windows Source Copyright 1998 by OG Software, Inc., Ann Arbor, MI 48104 INTRODUCTION This file contains the POO source for windowed applications for a Windows environment. The POO source is consistent with the ANSI C standard; it uses no C++ features but is consistent with the C++ language. There are a number of variables below that control how the POO software is compiled. Depending on your environment, you may have to adjust their values. The POO software is in one, large file for your convenience. Thus, to use the software, you only need to copy this file and to add one dependency to your application. A single file avoids pollution of your application's name space because all function and variable references are within this file. Further, if perusing the source, all references to variables, functions, etc., can be found with a single search. For this reason, contractions for names are used in comments so that scans will find only code references, e.g., the initial O_ is dropped from the names of functions. Lines of the form "=...=" demarcate the major sections in this file; each starts with an overview. Within these major sections, lines of the form "-...-" demarcate the individual functions, which start with an introduction and contain embedded commentary. The major sections are INTRODUCTION DATA STRUCTURES AND ASSOCIATED DEFINED CONSTANTS FUNCTION PROTOTYPES AND MACROS INTERNAL VARIABLES AND TABLES POO MAIN PROGRAM, API FUNCTIONS, AND THREADS API CO-CONSPIRATORS WINDOWING SYSTEM INTERFACE COMMAND PROCESSING VIRTUAL MEMORY FILE FACILITY MEMORY FILE FACILITY When building your application, your C compiler may require that you explicitly specify the 'windowed' and 'multithreaded' options. There are no standards for these options, and some IDEs hide them. See the documentation for your C/C++ compiler. Your compiler may require a .DEF file; the POO software does not require a .RC file. Depending on your Windows environment and your compiler, the two variables below may be either defined or undef'd. If the first is defined, the file system is accessed using Win32(s) API functions; otherwise, the POSIX I/O functions. If the second is defined, the POO software runs multithreaded; otherwise, single. Unless you are compiling for Windows 3.1, I recommend that both be defined. The Win32 API has no inherent advantage over the POSIX I/O library, but using it may reduce your frustration with your C compiler. Multithreaded applications are less frustrating. If you are compiling for Windows 3.1, neither of these variables may be defined: the Win32 API is not available, and the POO software cannot be compiled multithreaded. The variable POO_..._COMPILER must be defined so that the vendor idiosyncrasies can be managed automatically, i.e., the header or library inconsistencies. In general, these issues are handled in this section. Only Borland and Watcom are supported directly. If you use a different compiler, I would suggest that you try BORLAND before WATCOM. If neither works for your compiler, you will have to define modifications for your compiler. */ #define POO_NATIVE_WIN32 #define POO_MULTITHREADED #define POO_BORLAND_COMPILER /* The value assigned to the variable below controls how alphabetics in path names are treated: 0 to ignore alphabetic case, and 1 to convert all alphabetics to uppercase (DOS). To avoid problems, the assigned value should be consistent with the file system. Ignoring case is the safe alternative but can cause some surprises. */ #define POO_UPPERCASEPATHNAMES 0 /* Headers and Associated Additions This software requires the ANSI C is... functions and a few of the string functions, e.g., strcpy and strlen, and abs and sprintf. */ #include #include #include #include #include #ifdef POO_NATIVE_WIN32 /* Win32 API Rendition When running native, the VM file facility is implemented based on the Windows I/O functions rather than the POSIX ones. In addition, the calls on getcwd and getenv are replaced for consistency. It's assumed that all of the required functions and the few needed from the C library are reentrant. There is no inherent reason for these functions to be otherwise, but this is a leap of faith. Based on this assumption, the Windows's thread function is called directly when running native. But, if you are using the Watcom compiler, this leap is positively deadly, hence the modifications here. */ #define MAXLENPATH (MAX_PATH+8) #ifdef POO_WATCOM_COMPILER #include #define O_BeginThread(f,x) _beginthread(&f,NULL,8192,x) #else #define O_BeginThread(f,x) CreateThread(NULL,0, \ (LPTHREAD_START_ROUTINE)f,(LPVOID)x,0,&WindABH) #endif #define O_GetCWD(s,l) GetCurrentDirectory(l,s) #define O_GetEnv(var,val) \ ! GetEnvironmentVariable(var,val=WorkTemp,Work->asize-1) #define MoveTo(gc,x,y) MoveToEx(gc,x,y,NULL) #define FSTIME FILETIME #else /* Windows/POSIX Rendition Compiler vendors for PC environments don't seem overly committed to the POSIX standards. The variable POO_..._COMPILER is intended to get around vendor idiosyncrasies. Hopefully a paragraph for your compiler is provided; but, if not, you'll get some messages. Headers and the thread function are the principal issues. The _beginthread function provides required initialization of the C run-time library, but the POO software uses both localtime and readdir, both of which are intrinsically NOT reentrant. In theory, one could write an application that beats on these functions and that consequently fails in some manner. */ #include #include #include #include #define O_GetCWD(s,l) getcwd(s,l) #define O_GetEnv(var,val) (val = getenv(var)) == NULL #define FSTIME time_t /* Borland/POSIX Modifications Based on __WIN32__, some repairs for the Borland headers are made. */ #ifdef POO_BORLAND_COMPILER #include #include #define MAXLENPATH (MAXPATH+8) #ifdef POO_MULTITHREADED #include #define O_BeginThread(f,x) _beginthread(f,8192,x) #endif #ifdef __WIN32__ #define MoveTo(gc,x,y) MoveToEx(gc,x,y,NULL) #else #define S_IRUSR S_IREAD #define S_IWUSR S_IWRITE #define S_ISDIR(m) ((m) & S_IFDIR) #define S_ISREG(m) ((m) & S_IFREG) #endif #endif /* Watcom/POSIX Modifications There is also a correction for fstat in LoadVMF. */ #ifdef POO_WATCOM_COMPILER #include #define MAXLENPATH (PATH_MAX+8) #ifdef POO_MULTITHREADED #include #define O_BeginThread(f,x) _beginthread(&f,NULL,8192,x) #endif #define MoveTo(gc,x,y) MoveToEx(gc,x,y,NULL) #endif #endif /* The last macro provides clarity because calling WindThread with a NULL terminates the application, which is not otherwise apparent. */ #define TERMINATE O_WindThread(NULL) /* System-Dependent Defined Constants In Windows and OS/2, the path name separation character is the backslash (\); in Unix, the slash (/). For user portability, this software treats PSCAs the same as PSCs and converts all PSCAs to PSCs before passing a path name to the system, which applies to file names entered by the user or passed to a POO API function. In Windows and OS/2, DEFAULTFILEFORMAT should be defined the same as the CRLF flag so that lines are terminated by the two character sequence \r\n in newly created files. In Unix, only a \n is used to terminate lines in a text file. This software reads and writes files in either format; line termination is transparent to your application, which will see only a \n, consistent with C. DIRECTORYOFLASTRESORT is used to initialize the current directory if and only if nothing else works. */ #define PSC '\\' #define PSCA '/' #define DEFAULTFILEFORMAT 0x00000004L #define DIRECTORYOFLASTRESORT "C:" #define MAXLENFILE FILENAME_MAX #define POO_CLASS "POO_Wnd" #define POO_STYLE (DT_TOP|DT_LEFT|DT_NOPREFIX|DT_SINGLELINE) /* Memory Management Macros All memory allocations are done using the VM_... macros and are made in pages, i.e., multiples of 4096 bytes. The ACQUIRE and POINTER returns are treated as distinct but may be identical, e.g., in Unix. These macros were designed to be portable among the supported systems, and any resemblance to an actual design is purely coincidental. They are placed here because .._HNDL is required to define the structs in the next section. */ typedef unsigned long VM_SIZE; #define VM_HNDL HGLOBAL #define VM_PAGE GMEM_MOVEABLE #define VM_ACQUIRE(h,s) h = GlobalAlloc(VM_PAGE,s) #define VM_POINTER(h) GlobalLock(h) #define VM_RELEASE(h) { GlobalUnlock(h); GlobalFree(h); } /* System-Dependent Thread Management Macros These macros are used to hide dependencies on the threadedness of this software by avoiding conditional code based on MULTITHREADED. The API state bit/semaphore signals that the processing for an API event has been completed. When single threaded, the event loop runs until the API state bit is set off. When multithreaded, the thread that called the API function waits on the API semaphore. The ..._APPL macros are used to implement user flow control, i.e., to suspend the application when the next API call occurs, and are used only in KeyFcn. The implementations are based on a mutex so that all versions are consistent. A semaphore would be more appropriate, but semaphores are not part of the POSIX threads standard. When single threaded, the state bit suffices. The return mutex is created and permanently locked by the window thread when it starts. It's used to suspend application threads as appropriate. */ #ifdef POO_MULTITHREADED #define RELEASE_API State &= ~sbAPI; \ ReleaseSemaphore(APISem,1L,&SemCnt) #define BLOCK_APPL if (WindABH == ApplABH) break; \ if (!(State & sbSTOP)) { \ if (WaitForSingleObject(UsrMtx,0) != WAIT_TIMEOUT) \ State |= sbSTOP; } #define RELEASE_APPL if (WindABH == ApplABH) break; \ if (State & sbSTOP) \ { State ^= sbSTOP; ReleaseMutex(UsrMtx); } #define TOGGLE_APPL if (WindABH == ApplABH) break; \ if (State & sbSTOP) \ { State ^= sbSTOP; ReleaseMutex(UsrMtx); } \ else if (WaitForSingleObject(UsrMtx,0) != WAIT_TIMEOUT) \ State |= sbSTOP; #define SUSPEND_THREAD \ WaitForSingleObject(RtnMtx,INFINITE) #else #define RELEASE_API State &= ~sbAPI #define BLOCK_APPL State |= sbSTOP #define RELEASE_APPL State &= ~sbSTOP #define TOGGLE_APPL State ^= sbSTOP #define ExitProcess(x) exit(x) #endif /* System-Depended Noise Maker Macros */ #define WOOF MessageBeep(MB_ICONEXCLAMATION) #define BARK MessageBeep(MB_ICONQUESTION) #define GROWL MessageBeep(MB_ICONASTERISK) /* In all of the supported windowing environments, your application must follow the prototype */ extern int appl (void); /* POO Application Prototype */ /*================================================================ DATA STRUCTURES AND ASSOCIATED DEFINED CONSTANTS The POO software uses three data structs: ANTs, VMFs, and WNDs. An API event struct is used to process API function calls and exists only while it's being processed. A VM file struct represents a file in virtual memory, and a window struct represents a window. The VM file and window structs are maintained in singly linked lists anchored in FileList and WndList, respectively. As far as the windowing system is concerned, all windows created by this software are alike, but this software distinguishes three subclasses: pollsters, onlookers, and others. Both pollsters and onlookers can be created by either the application or user; but others, only by the application. A poll file is always associated with a pollster, and an output stream is always associated with an other. In some respects, VM files and windows are independent. A VM file need not be associated with a window but can be associated with multiple windows. In general, an unassociated VM file arises when the user associates a new file with a onlooker such that the old VM file is not freed. The API Function Event Struct This struct is used to pass the arguments to an API function to its co-conspirator function, e.g., copen and Open. API functions are called and executed by the application thread; co-conspirators are called and executed by the window thread to process the event generated by the API function. The API event struct is created by the API function, passed to WaitEvent as an argument, to the event processor as an event parameter, and to the co-conspirator as its argument. Co-conspirators return values in this structure. When single threaded, the co-conspirators are called from WaitEvent. The WM_POO_... constants are event identifiers. */ typedef struct { void *alw; /* Open identifier; copen return.*/ char *buf; /* Text/buffer argument; name/title.*/ int len; /* Length of text or buffer; initial size.*/ int rtn; /* Return value; extra size.*/ int eno; /* WM_POO_... value.*/ } ANT; #define WM_POO_OPEN WM_USER+1 #define WM_POO_PUT WM_USER+2 #define WM_POO_GET WM_USER+3 #define WM_POO_CLOSE WM_USER+4 #define WM_POO_USER WM_USER+5 /* The VM File Struct All output streams generated by an application and all files in the underlying disk file system that are accessed for input or displayed in pollsters or onlookers are held in virtual memory files. The VM file facility is designed to handle output streams from an application that the user is expected to read or at least browse. Because a printed page requires a few kilobytes, I would expect applications to generate output streams measured in kilobytes. Thus, retaining the output streams in VM files should not stress the paging capacity of the system; all handle several megabytes easily, and this amount of output would be daunting to anyone. This design assumes virtual memory is flat and is less effective in conjunction with other memory architectures, e.g., segmented, because files are restricted to flat regions. In Windows 3.1, VM files are limited to 64K bytes. If a file exceeds flat memory, it cannot be loaded and, therefore, cannot be used with this software. If an output stream becomes too large, it's truncated to the space available. Having a VM file in a single, contiguous memory block makes many operations simple and is well suited to the sequential output streams produced by applications. Other implementations could avoid the flat memory issue, but I'm unwilling to regress. This VM file facility is suitable for applications with multiple current directories and builds and retains the complete path names of all files referenced. A VM file struct has space for the path name; and, a window struct, its current directory. Retention of the complete path name has two advantages. First, regardless of any subsequent changes in current directories, the user can be provided precise information, e.g., the file to which a window's output stream is currently directed. Second, files need not and are not open unless they are being read or written because they can be re-opened at any time. A VM file is managed using a VM file struct, and the set of VM files is maintained in a single linked list. The VM file struct is defined in this section; the functions that implement VM files are in "VIRTUAL MEMORY FILE FACILITY." */ typedef struct aVMF VMF; struct aVMF { VMF *next; /* Link to the next VM file */ char *base; /* Base address (first byte) */ char *eof; /* First unused byte */ long posn; /* Position */ long flags; /* Flag bits bX... */ FSTIME mtime; /* Nominal modification time */ long size; /* Sometimes (eof - base) */ long xsize; /* Extra space parameter */ long asize; /* Size of data allocation */ VM_HNDL memhndl; /* Memory handle */ char pn[MAXLENPATH+MAXLENFILE]; /* Complete path name */ }; /* The next, asize, and memhndl components are strictly managerial: next points to the next VM file in the singly linked list, asize is the number of bytes of virtual memory allocated for the data, and memhndl is the system-dependent parameter needed to release the virtual memory. The data in a VM file is defined by the base and eof components: the former points to the first data byte of the VM file; and the latter to the first unused byte and, BY DESIGN, is always a \n, a newline. Thus the software can scan a VM file for a newline and be confident the scan will stop. When a file is associated with a window, the base and eof pointers are copied to the window struct, where they may be changed if the window is restricted to a subset of the VM file by the user. Nominally, one would expect the size component to be the number of data bytes in the file, but little effort is made to maintain it since differencing the eof and base pointers provides this value. The xsize component is used when expanding or truncating a VM file. Both the initial and extra size are provided by the application but modified appropriately by the POO software. The use of the posn component depends on the window subclass. For pollsters and others with the UPDATE flag set, i.e., when output has been directed to a file, this component holds the displacement from the base pointer to the first byte that has not been written to the file. For onlookers, it provides the position of the window over the file but is updated only when appropriate. When a file is initially loaded, it's set from the memory file or to zero, i.e., the beginning of the file. The pn and mtime components are related and set by CreateVMF to the complete path name and last modification time, respectively. For a NEW file, a file that does not exist in the file system, the mtime value is set to the time the application started. Files are uniquely identified by comparing BOTH the pn and mtime values. Thus, for example, multiple copies of the same disk file can exist as distinct VM files. The flags component holds various flag bits, defined constants of the form b{UPPERCASE}. Within the text, the b is dropped so that, if you scan this file for a flag, you will find only references in the code. The VM File Flags */ #define bHEX 0x00000001L #define bEOF 0x00000002L #define bCRLF 0x00000004L #define bTRUNC 0x00000008L #define bUPDATE 0x00000010L #define bSTORE 0x00000020L #define bLOCK 0x00000040L #define bSAVE 0x00000080L #define bNEW 0x00000100L #define bSTAT 0x00000200L #define bLOAD 0x00000400L #define bFILE 0x00001000L #define bDIR 0x00002000L /* A VM file is NEW if unnamed or if the name does not correspond to a file in the file system but the path is a valid directory. It's STAT if the name corresponds to either a regular file (FILE) or a directory (DIR). The LOAD flag is set if the VM file was loaded from the file system. The SAVE flag is set if file position information should be retained in the memory file and can be set only if the LOAD flag is set. When a file is loaded, it's scanned to determine whether the HEX flag should be set; if not HEX, the file is assumed to contain text. For a text file, the CRLF flag indicates whether lines are terminated with a \n (Unix) or \r\n sequence (DOS), and the EOF flag indicates whether a terminal Ctrl+Z (26) has been deleted. A VM file can be displayed in a window as text or in HEX dump format; the HEX flag sets the default display mode. The CRLF flag for a NEW VM file is system dependent. The TRUNC flag determines the buffer management strategy used for the VM file. Normally, files are expanded as necessary, but the application can elect to truncate old data to make room for new. If expansion fails, the software changes to truncation. The UPDATE and STORE flags are used to implement output direction. The LOCK flag is set if the file should be retained as a VM file regardless of associations and disassociations with a window. In fact, it's set only in the memory file. The Window Struct All window structures are created by CreateWND and destroyed when the window is closed. The component ordering is based on usage and a memory cache line of 64 bytes, which is not critical but could have an imperceptible impact on performance. Components that depend on the windowing system are placed near the end because their size and number vary. For example, in Windows 3.1, handles are 16-bits; in Win32, 32 bits. Using the Win32s interface avoids this issue and provides flat memory in Windows 3.1. There are a number of components dependent on the underlying windowing system, a number associated with the portion of the file under the window, a number related to input direction established by the user, a few managerial components, and three buffers: input and extra for handling input through the window, tmp for temporary storage, and the current directory. The settings of the MAXLEN... defined constants are more or less arbitrary but should be such that a window struct fits in a page. Input through the window, the input buffer, is copied to the tmp buffer without checking to verify that it will fit. Likewise, valid path names are copied to cwd without looking at the length so that it should be large enough. */ #define MAXLENNAME 16 #define MAXLENINPUT 512 #define MAXLENXTRA 1024 typedef struct aWND WND; struct aWND { WND *next; /* Link to next WND (MRU) */ DWORD ptime; /* Time of last DrawWindow */ HWND wnd; /* Client window handle */ long SSN; /* Yes, each has one.*/ long flags; /* b... flags bits */ ANT *apiarg; /* API event struct */ VMF *dout; /* Current output file association */ char *base; /* Beginning of output file */ char *eof; /* End of output file */ VMF *din; /* Current input file association */ char *dinbase; /* Beginning of input file */ char *dineof; /* End of input file */ char *dinptr; /* Input file read pointer */ char *cur; /* Caret character */ char *fcur; /* First input character drawn last time */ char *end; /* End of input line */ char *top; /* Line at top of window */ char *bot; /* Line at bottom of window */ int ytop; /* Relative line number of top */ int ybot; /* Relative line number of bottom */ int shft; /* Shift (signed) applied to all lines */ int ptrfcn; /* Pointer function */ int px, py; /* Pointer position at last DOWN */ int ww, wh; /* Client window size */ int tw, th; /* Font size */ int fw, fh; /* Window frame adjustments */ RECT inr; /* Input rectangle, see DrawInput */ HDC gc; /* Graphics context */ COLORREF bg, fg; /* Background and foreground colors */ HFONT fn; /* Font */ HBRUSH bb, fb; /* Background and foreground brush */ HPEN fp; /* Foreground pen */ long mem[5]; /* See MemoryCmd */ VM_HNDL memhndl; /* Memory handle */ char input[MAXLENINPUT]; /* Input buffer */ char tmp[MAXLENINPUT]; /* Temporary working space */ char extra[MAXLENXTRA]; /* Typeahead/residual input */ char bgn[32],fgn[32]; /* Color names */ char fnn[80]; /* Font name */ char wndname[MAXLENNAME]; /* Window name */ char cwd[MAXLENPATH]; /* Current directory */ }; /* The next component points to the next window in the list. Unlike the file list, the window list is managed as an MRU cache (see the event processing function) to reduce the search time. The list is searched both by the open identifier (the struct pointer cast as a void *) and the system's window identifier. Open identifiers to API functions are validated from the list, while event processing searches using the system's window identifier. The SSN is included in the first cache line so that converting the open identifier to the SSN would be straightforward. The ptime component holds the time the window was last drawn and is used to prevent run on, which I find objectionable. If you like run on, change DrawWindow. The flags component is heavily referenced; it represents the state of the window and contains four bytes of bit flags: a file byte, a state byte, a subclass byte, and a mode byte. The flag bits are defined constants of the form b{UPPERCASE}, see below. The base and eof components are set from the associated VM file, i.e., they are copies. The user can change these pointers so that the window is associated with only a subset of the VM file. The top/bot, ytop/ybot, and shft components define the location of the window over the file. The first two are pointers into the file, while the next two are relative line numbers within the window. shft determines horizontal position. Two drawing modes are supported by DrawWindow: top-down and bottom-up. The usual mechanism is to set either top or bot and its y... and then call DrawWindow, which distrusts everyone and corrects anything that looks amiss. The ytop and ybot values are relative line numbers, where 0 always corresponds to the top of the window. In DrawWindow, these values are the y coordinates of the lines in the window but are converted to relative line numbers at the end of DrawWindow. This tactic avoids a system dependency: X and Windows place (0,0) at the upper left corner of the window, while OS/2 places it at the lower left. The shift value is applied to all text displayed in the window to provide horizontal scrolling. But there are no limits on the shift so that the window may be entirely blank, i.e., the window may be so far from the left or right of the text that the window contains only nothingness. If the user loses the text, there are several mechanisms for finding it, e.g., a "col 0" command. Input through the window is managed by InputMgr using the fcur, cur, and end components and the input buffer. For others, the apiarg is used for input through the window, the extra buffer is used to hold residual and typeahead input, and input direction is managed using the din... components and EOF flag. The Window Flags The HEX and EOF VM file flags are also used as window flags. The HEX flag determines whether the file associated with the window is displayed as text or in hex dump format; when a file is associated with a window the HEX flag in the VM file is copied to the window flags. The display mode can be toggled. As a window flag, EOF is used to indicate whether an EOF should be given when the end of the input file (din) is reached. It's set if input is directed by < but not set if directed by << or pasted. The FOCUS, CARET, INPUT, and INFO flags are related and discussed in DrawInput, also see InputMgr. The INSERT flag controls whether insert or overstrike mode is in effect and is toggled with the Insert key. The STATIC flag controls whether Put updates the window. The TYPE flag is used to implement the NB command. If TYPE is set, input is solicited through the window and appended to the VM file associated with the window. */ #define bFOCUS 0x00000100L #define bCARET 0x00000200L #define bINPUT 0x00000400L #define bINFO 0x00000800L #define bINSERT 0x00001000L #define bSTATIC 0x00002000L #define bTYPE 0x00004000L /* The window subclass byte contains a flag bit for each subclass: POLL, ONLKR, and OTHER. The ECHO, CLOSE, LINE, and STET flags are set in Open from the modifiers prefixed to the window name. If ECHO is set, input returned by Get is appended to the file associated with the window. If CLOSE is set, the window cannot be closed. If LINE is set, input and output are line oriented rather than stream: newlines are not returned by Get, residual input is discarded, and a newline may be appended to output text. If STET is set, Put does not try to process an initial substring of C control characters in text output. */ #define bPOLL 0x00010000L #define bONLKR 0x00020000L #define bOTHER 0x00040000L #define bCLOSE 0x00100000L #define bECHO 0x00200000L #define bLINE 0x00400000L #define bSTET 0x00800000L /* The window format flags are set initially to the defaults for the subclass (Default...) but can be manipulated by the user with the MODE command. INT and INB determine the vertical placement of the input area within the window; INH, its height; and INW, its width. These bits are referenced only in DrawInput. TIC and BAR affect the appearance of the vertical scroll bar, and BD whether a border is drawn around the text when nothingness is under the window. These last three flags are referenced only in DrawWindow. */ #define bINT 0x01000000L #define bINB 0x02000000L #define bINH 0x04000000L #define bINW 0x08000000L #define bTIC 0x10000000L #define bBAR 0x20000000L #define bBD 0x40000000L /*================================================================ FUNCTION PROTOTYPES AND MACROS I'm willing to be unforgiven for BEGIN and END, which were chosen primarily for their visual mass both on the screen and printed page and secondarily for their historical significance. They are used in place of braces when justified by the context, e.g., for delimiting functions and large or complex blocks. GLOBAL and INTERN are used to make the scope explicit. Although it's unlikely one would want to change GLOBAL, changing INTERN might be convenient, albeit dangerous. When multithreaded, you CANNOT simply change INTERN and then call functions in this file directly. If you immediately grasp the significance of this rule, you most likely know why and what must be done; otherwise, don't even think about it. */ #define BEGIN { #define END } #define GLOBAL #define INTERN static /* Application Programming Interface The c... and n... functions are the complete C and Fortran API, respectively, for a POO application and are the only functions in this file that have global scope, i.e., are known outside of this file. In general, each API function creates an API event struct and calls WaitEvent, which invokes the appropriate co-conspirator, the O_... functions. API functions are executed by the application thread; co-conspirators, by the window thread. cappl requires no co-conspirator; indeed, the window thread has not been created when it's executed. The POO main program, WinMain, receives control from the system, and its prototype is prescribed by the system. After some initialization, it calls your application, appl. int WinMain (Prescribed by Windows) */ GLOBAL int cappl (char *application_name); GLOBAL void *copen (char *window_title, int size, int xsize); GLOBAL int cputs (void *open_rtn, char *text, int length); GLOBAL int cgets (void *open_rtn, char *buffer, int length); GLOBAL int cclose (void *open_rtn); GLOBAL int nappl (char *application_name); GLOBAL void *nopen (char *window_title, int *size, int *xsize); GLOBAL int nputs (void **open_rtn, char *text, int *length); GLOBAL int ngets (void **open_rtn, char *buffer, int *length); GLOBAL int nclose (void **open_rtn); INTERN int O_WaitEvent (int event, ANT *args); INTERN void O_WindThread (void *arg); INTERN int O_Time (char *buffer, FSTIME *st); INTERN WND *O_Open (ANT *arg); INTERN WND *O_Put (ANT *arg); INTERN WND *O_Get (ANT *arg); INTERN WND *O_Ctrl (ANT *arg); /* Windowing System Interface All of the initial functions, CreateWnd...EventProc, DrawWindow, and DrawInput are highly system dependent. Most of the others are system independent or contain only trivial dependencies. Keyboard and mouse events are table driven and processed in either KeyFcn or ButtonFcn. */ INTERN WND *O_CreateWND (VMF *lf, char *title, long subclass); INTERN long O_SetAttribute(WND *lw, int cmd, char *opnd); INTERN int O_GetFontSize (WND *lw); INTERN int O_Activate (WND *lw); LRESULT CALLBACK O_EventProc (HWND,UINT,WPARAM,LPARAM); INTERN int O_KeyFcn (WND *lw, int arg); INTERN int O_ButtonFcn (WND *lw, int press, int x, int y); INTERN char *O_LineBefore (WND *lw, char *t); INTERN char *O_LineAfter (WND *lw, char *t); INTERN int O_DrawWindow (WND *lw); INTERN char *O_HexDump (char *buf, char *b, char *d, char *e); INTERN int O_DrawInput (WND *lw, int both); INTERN int O_InputMgr (WND *lw, char *text, int length); /* Command Processing All commands entered through a window are parsed by ParseCommand and processed by Command. Annotate is an extension of Command in which all the code related to the NB command has been aggregated for clarity. Most commands are completely processed in Command, but SetAttribute and ParseGeometry contain most of the code needed to change a window attribute, e.g., colors, font, and geometry. */ INTERN int O_Command (WND *lw, int cmd, char *opnds); INTERN WND *O_Annotate (WND *lw, int op, char *opnds, char **e); INTERN int O_ParseCommand (char *beg, char *end, char *opnds); INTERN int O_ParseGeometry (WND *lw, char *geometry); /* Virtual Memory Files These functions implement the VM file facility. All interactions with the file system are contained in CreateVMF, LoadVMF, and StoreVMF. ExpandVMF...AssociateVMF involve VM files and possibly windows. The POSIX-based renditions are system independent. */ INTERN VMF *O_CreateVMF (char *p, char *f, long s, char **e); INTERN VMF *O_DestroyVMF (VMF *lf); INTERN VMF *O_LoadVMF (char *p, char *f, long x, char **e); INTERN VMF *O_StoreVMF (VMF *lf, int op, char **e); INTERN VMF *O_ExpandVMF (WND *lw, VMF *lf, long delta); INTERN char *O_AppendVMF (WND *lw, VMF *lf, char *t, long l); INTERN VMF *O_AssociateVMF (WND *lw, VMF *lf); /* Memory File The memory file facility is an aggregation of the sections of code that deal directly with the memory file. Many of the functions have only one caller and are not proper functions, i.e., assume the context of their call and are dependent on it. The purpose of the memory file is to retain window attributes, function key assignments, and file information between invocations of a POO application. These functions are system independent. */ INTERN VMF *O_MemoryLoad (int load); INTERN VMF *O_MemoryStore (void); INTERN int O_MemoryCmd (WND *lw, int cmd, char *opnd); INTERN int O_MemoryKey (int cmd, char *opnd); INTERN char *O_MemoryVMF (VMF *lf, int op); /* Miscellaneous Macro Functions Macros are used primarily for functions too short to warrant being an actual function and to hide minor system dependencies. When a single statement, the definitions do not include the semicolon so that they must be written with the semicolon in the code. Valid is a necessary Win16 Windows-ism and is used to determine if its first argument lies between its second (inclusive) and third (exclusive), all of which are char * pointers. It's retained in all environments for clarity. APIEvent is called when an event occurs that requires intervention by an API co-conspirator, i.e., if the API arg component is set. It's used only when soliciting input through the window. Extension of this basic mechanism to other situations may require that it become a proper function. */ typedef unsigned long LP; #define O_Valid(x,a,b) ((LP)(a) <= (LP)(x) && (LP)(x) < (LP)(b)) #define O_APIEvent(r) (lw->apiarg)->rtn = r; \ (lw->apiarg)->eno = 0; O_Get(lw->apiarg) /* Windowing System Macros These macros provide succinct and system-independent expressions for functions that are frequently needed. Consequently, a number of minor system dependencies are hidden. */ #define CARET_OFF if (lw->flags & bCARET) \ { lw->flags ^= bCARET; DestroyCaret(); } #define DRAG_OFF if (lw->ptrfcn) \ { lw->ptrfcn = 0; ReleaseCapture(); } #define UPD_CARET O_DrawInput(lw,0) #define UPD_INPUT O_DrawInput(lw,1) #define UPD_WND InvalidateRect(lw->wnd,NULL,FALSE) /*================================================================ INTERNAL VARIABLES AND TABLES The variables in the first group are initialized in the main program before calling appl. ThisI, PrevI, SysCmdLine, and SysShow are Windows-isms. LWSize is used in CreateWND; LFSize, in CreateVMF. State contains flag bits indicating application status. WndList and FileList are the anchors for the singly linked lists of windows and VM files, respectively. MemoryFile is the VM file identifier for the memory file. Work... have special roles, e.g., the current directory. The sb... constants are state flags. The UCPN flag is set from the pertinent defined constant and determines whether path names are converted to upper case. The ROFS, read-only file system, flag is set for Onlooker. The application flags are set in cappl. */ static HINSTANCE ThisI, PrevI; /* This and previous instances */ static char *SysCmdLine; /* WinMain argument 3 */ static int SysShow; /* WinMain argument 4 */ static int State; /* POO/appl state bits */ #define sbUCPN 1 #define sbROFS 2 #define sbPOLL 256 #define sbONLKR 512 #define sbOTHER 1024 #define sbDONE 2048 #define sbSTOP 4096 #define sbAPI 8192 static VM_SIZE LWSize; /* Window struct size (pages)*/ static VM_SIZE LFSize; /* File struct size (64 bytes) */ static VMF *Work; /* Work space and CWD VM file */ static char *WorkInput; /* Cut/copy/paste input buffer */ static char *WorkTemp; /* Very temporary working space */ static VMF *MemoryFile; /* Memory file */ static VMF *FileList; /* Linked list of VM files */ static WND *WndList; /* Linked (MRU) list of windows */ static long SSN; /* Yes, they all have one.*/ /* The variables in this group are for interaction with either the operating or windowing systems, e.g., anchor block handles. The mutex and semaphores are used in WaitThread to serialize API calls and await completion of the processing required for the API event. The user semaphore is under user control and suspends execution of the application if an API call occurs. When single threaded, the API and STOP state flags replace the semaphores. The struct Msg is used for all events. In Windows and OS/2, the event time is in this struct but not passed to EventProc; the event time is used to stop run on. */ #ifdef POO_MULTITHREADED static HANDLE Mother; /* API mutex (mutually exclusive).*/ static HANDLE UsrMtx; /* User flow control implementation.*/ static HANDLE RtnMtx; /* For suspending application threads.*/ static HANDLE APISem; /* API event completion semaphore.*/ static long SemCnt; /* Semaphore post count.*/ #endif static DWORD ApplABH; /* In Windows, 77.*/ static DWORD WindABH; /* In Windows, 77 or thread ID.*/ static MSG Msg; /* Current message structure.*/ /* The application and memory file names are set in cappl from the string provided by the application. Window creation is inter- locked with cappl: if the memory file doesn't exist when the first open request occurs, a cappl("Other") is issued. */ #define MAXLENAPP 8 static char ApplicationName[MAXLENAPP+1]; static char MemoryFileName[MAXLENAPP+5]; /* +.MEM */ static char *MemoryPathVar[] = { "$POOMEMDIR", "$HOME", ".", NULL, "Copyright 1998 by OG Software, Inc." }; static char *Pollster = "Pollster"; static char *Onlooker = "Onlooker"; static char *Other = "Other"; static long DefaultPOLL = (bINB|bINH|bINW|bINSERT|bPOLL); static long DefaultONLKR = (bTIC|bBD|bINH|bINSERT|bONLKR); static long DefaultOTHER = (bINT|bINH|bINW|bINSERT|bOTHER); /*---------------------------------------------------------------- Key and Button Tables Most keys and the mouse buttons are quadrupled based on the states of the Shift and Ctrl keys and then processed based on the tables in this section. In this manner, a key press is translated to a function index, one of the f... constants, and then processed by calling KeyFcn. Likewise, a button press is translated to an index p... and then processed by ButtonFcn. Many of the key and button functions are context dependent, e.g., depend on whether the user is entering input through the window. Most of the functions are evident from their names, see KeyFcn or ButtonFcn, which contain only minor system dependencies. Although NONE and IGNORE do nothing, they are not the same; NONE is noisy. */ #define fNONE 0 #define fUP 1 #define fDOWN 2 #define fLEFT 3 #define fWLEFT 4 #define fRIGHT 5 #define fWRIGHT 6 #define fPGUP 7 #define fPGDN 8 #define fHOME 9 #define fHOME0 10 #define fWHOME 11 #define fEND 12 #define fEND0 13 #define fWEND 14 #define fCOL0 15 #define fDEL 16 #define fBKSP 17 #define fINS 18 #define fDUMP 19 #define fSTATIC 20 #define fENTER 32 #define fCMD 33 #define fEOF 34 #define fESCAPE 35 #define fTERM 36 #define fAPPB 37 #define fAPPT 38 #define fAPPR 39 #define fDELALL 64 #define fDELAFT 65 #define fDELBOW 66 #define fCPYINP 67 #define fCUTINP 68 #define fINSINP 69 #define fCPYVMF 70 #define fINSVMF 71 #define fDELIN 73 #define fIGNORE 127 #define pNONE 0 #define pDRAGTEXT 1 #define pPOSNTEXT 2 #define pINSERT 3 #define pIGNORE 127 /* Extended Key Table In Windows and OS/2, the Pause key requires only one table entry even though Pause and Shift+Pause use one virtual key code, while Ctrl+Pause and Shift+Ctrl+Pause use another. In Windows, one must be careful with Enter and Backspace because two events are fielded for each key press: once as a virtual key (ignored), and once as a CHAR. In Windows, Ctrl+Enter appears to always generate noise. */ static unsigned char ExtKeyFcn[60] = { /* Key Label : Key Shift Ctrl Shift+Ctrl */ /* Up */ fUP, fUP, fNONE, fNONE, /* Down */ fDOWN, fDOWN, fNONE, fNONE, /* Left */ fLEFT, fWLEFT, fCOL0, fNONE, /* Right */ fRIGHT, fWRIGHT, fCOL0, fNONE, /* Page Up */ fPGUP, fPGUP, fNONE, fNONE, /* Page Down */ fPGDN, fPGDN, fNONE, fNONE, /* Delete */ fDEL, fCUTINP, fDELIN, fNONE, /* Backspace */ fIGNORE, fIGNORE, fNONE, fNONE, /* Enter */ fIGNORE, fIGNORE, fCMD, fCMD, /* Home */ fHOME, fWHOME, fHOME0, fNONE, /* End */ fEND, fWEND, fEND0, fNONE, /* Insert */ fINS, fINSINP, fCPYINP, fNONE, /* Tab */ fIGNORE, fIGNORE, fIGNORE, fIGNORE, /* Pause */ fAPPT, fAPPT, fAPPB, fAPPB }; /* Control Key Table The tables honor the well-known equivalence of Enter and Ctrl+M and Backspace and Ctrl+H. */ static unsigned char CtrlKeyFcn[27] = { /* A B C D E F G */ fDELAFT, fDELBOW, fCPYINP, fTERM, fNONE, fNONE, fNONE, /* H=BKSP I=TAB J K L M=ENTER N */ fBKSP, fIGNORE, fNONE, fNONE, fNONE, fENTER, fNONE, /* O P Q R S T U */ fDUMP, fNONE, fAPPR, fNONE, fAPPB, fNONE, fDELALL, /* V W X Y Z Esc */ fINSINP, fNONE, fCUTINP, fNONE, fEOF, fESCAPE }; /* Function Key Table The function keys F1...F12 are expanded to 48 based on the states of the Shift and Ctrl keys: F1...F12 correspond to 1...12, Shift+ to 13...24, Ctrl+ to 25...36, and Shift+Ctrl+ to 37...48. They are processed using KeyFcn and the fX... functions unless the user has assigned a string to the function key, which takes precedence over the built-in functions. For the user's convenience, table entries are replicated so that the user's string assignments don't eliminate the built-in functions until all related keys have been assigned. Function key string assignments are maintained in the memory file by MemoryKey, which maintains the vector of displacements so that they can be found easily and quickly. The array of displacements is initialized by MemoryLoad, which loads the memory file. */ static int FcnKeyStr[48]; /* Memory file displacements.*/ static unsigned char FcnKeyFcn[48] = { /* Key: Key Shift Ctrl Shift+Ctrl */ /* F1 */ fNONE, fNONE, fNONE, fNONE, /* F2 */ fNONE, fNONE, fNONE, fNONE, /* F3 */ fNONE, fNONE, fNONE, fNONE, /* F4 */ fNONE, fNONE, fNONE, fNONE, /* F5 */ fINSVMF, fINSVMF, fINSVMF, fINSVMF, /* F6 */ fSTATIC, fSTATIC, fSTATIC, fSTATIC, /* F7 */ fCPYVMF, fCPYVMF, fCPYVMF, fCPYVMF, /* F8 */ fNONE, fNONE, fNONE, fNONE, /* F9 */ fNONE, fNONE, fNONE, fNONE, /* F10 */ fNONE, fNONE, fNONE, fNONE, /* F11 */ fNONE, fNONE, fNONE, fNONE, /* F12 */ fNONE, fNONE, fNONE, fNONE }; /* Pointer/Mouse Button Table Functions that support dragging set the ptrfcn component in the window structure; since it's set on the press, the Shift and Ctrl keys need not be held while dragging. A drag operation may be terminated for many reasons, e.g., the pointer leaves the window. */ static unsigned char ButtonFcn[8] = { /* Button: Button Shift Ctrl Shift+Ctrl */ /* Left */ pDRAGTEXT, pPOSNTEXT, pNONE, pNONE, /* Right */ pINSERT, pNONE, pNONE, pNONE }; /*---------------------------------------------------------------- Variables and Tables Associated with Command Processing One could view a WORDTAB as defining an enumerated type in that it establishes a relationship between a long and a word. In general, they are referenced in both directions, both to convert a word to a long and to convert a long to a word. The tables that use this struct require a final entry of the form {x,NULL}. Word to long conversion is used to process commands (ParseCommand) and command operands (SetAttribute) and, in general, the search is case insensitive and satisfied with an initial substring match but may require a unique match in the table. Long to word conversion is satisfied with the first match so that the preferred alias should appear first in the table. */ typedef struct { long index; /* Index */ char *cmd; /* String pointer */ } WORDTAB; /* Command Table and Indexes The command indexes are not arbitrary, e.g., BACKGROUND...MODE must be 1...5 (see MemoryCmd), and are also used as convenient constants. The command indexes provide logic control: function key commands are 1...48 plus FKFB; indexes less than 100 correspond to commands that can be given in "set" (with an operand) or "display" (no operand) forms, and indexes of 100 or more correspond to active commands that result in some specific action. When adding commands, failure to observe this numbering scheme may have consequences, not necessarily dire, perhaps just unwanted. */ #define FKFB 256 #define MAXLENOPNDS (MAXLENINPUT-32) #define oNONE 0 #define oBACKGROUND 1 #define oFOREGROUND 2 #define oFONT 3 #define oGEOMETRY 4 #define oMODE 5 #define oCOLUMN 6 #define oPOSITION 7 #define oWHOAMI 8 #define oYO 9 #define oCLOSE 100 #define oEXIT 101 #define oSTOP 102 #define oMEMORY 103 #define oPOLLSTER 104 #define oONLOOKER 105 #define oVMF 106 #define oTYPE 107 #define oCONTINUE 32 #define oLOAD 33 #define oSTORE 35 #define oAPPEND 36 #define oCD 37 #define oFIND 110 #define oCF 111 /* ParseCommand uses CommandTable to match the first blank-delimited word in a command line; the match must be unique. The usual X abbreviations for the attributes are included in this table. */ static WORDTAB CommandTable[] = { { oBACKGROUND, "background" }, { oFOREGROUND, "foreground" }, { oFONT, "font" }, { oGEOMETRY, "geometry" }, { oMODE, "mode" }, { oCOLUMN, "column" }, { oPOSITION, "position" }, { oYO, "yo" }, { oPOLLSTER, "pollster" }, { oONLOOKER, "onlooker" }, { oTYPE, "nb" }, { oVMF, "vmf" }, { oCONTINUE, "continue" }, { oLOAD, "load" }, { oSTORE, "store" }, { oAPPEND, "append" }, { oCD, "cd" }, { oEXIT, "exit" }, { oEXIT, "xit" }, { oEXIT, "quit" }, { oSTOP, "stop" }, { oCLOSE, "close" }, { oMEMORY, "memory" }, { oFIND, "find" }, { oCF, "cf" }, { oBACKGROUND, "bg" }, { oFOREGROUND, "fg" }, { oFONT, "fn" }, { oNONE, "none"}, { oWHOAMI, "?" }, { 0, NULL } /* REQUIRED TERMINATOR */ }; /* This table is initialized when the first FIND command is issued and is used to classify characters in FIND commands. */ static char FindTable[256]; /* Command...FIND */ /* Color Table (VGA) The color table supports the standard VGA colors: red, green, and blue together with their combinations. When you mix red and blue, you get purple; magenta is something special, but I'll concede. The modifiers 'light' and 'bright' do nothing; they are just noise that is tolerated; 'dark' changes the standard colors to the usual X colors 'Dark...'. Similarly, X accepts either gray or grey. The implementation of the modifier 'dark' depends on the order of the values in the color table. */ static WORDTAB ColorModTable[] = {{ 0, "light"}, { 0, "bright"}, { 1, "dark"}, {0, NULL}}; static WORDTAB ColorTable[] = { { RGB(255,255,255), "default background" }, { RGB(0,0,0), "default foreground" }, { RGB(0,0,0), "black" }, { RGB(255,255,255), "white" }, { RGB(255,0,0), "red" }, { RGB(0,255,0), "green" }, { RGB(0,0,255), "blue" }, { RGB(255,255,0), "yellow" }, { RGB(255,0,255), "purple" }, { RGB(255,0,255), "magenta" }, { RGB(0,255,255), "cyan" }, { RGB(192,192,192), "gray" }, { RGB(192,192,192), "grey" }, { RGB(128,0,0), "DarkRed" }, { RGB(0,128,0), "DarkGreen" }, { RGB(0,0,128), "DarkBlue" }, { RGB(128,128,0), "DarkYellow" }, { RGB(128,0,128), "DarkPurple" }, { RGB(128,0,128), "DarkMagenta" }, { RGB(0,128,128), "DarkCyan" }, { RGB(128,128,128), "DarkGray" }, { RGB(128,128,128), "DarkGrey" }, { 0, NULL } /* REQUIRED TERMINATOR */ }; /* Font Table This table includes the standard font face names for both Windows and OS/2 as well as some convenient aliases for Times Roman. See SetAttribute. */ #define fnALIAS 0x01000000L static WORDTAB FontTable[] = { { SYSTEM_FONT, "default" }, { SYSTEM_FIXED_FONT, "fixed" }, { SYSTEM_FONT, "variable"}, { ANSI_VAR_FONT, "ansivar" }, { ANSI_FIXED_FONT, "ansifixed" }, { DEVICE_DEFAULT_FONT, "device" }, { OEM_FIXED_FONT, "oem" }, { FF_MODERN|FIXED_PITCH, "Courier" }, { FF_SWISS|VARIABLE_PITCH, "Helvetica" }, { FF_ROMAN|VARIABLE_PITCH, "Times Roman" }, { fnALIAS + 9, "Tms Rmn" }, { fnALIAS + 9, "Roman" }, { 0, NULL } /* REQUIRED TERMINATOR */ }; #define NSF 7 /* The number of Windows' stock font names.*/ /* Mode Table The mode table is used to process the operand in MODE commands. The top, bottom, high, and wide flags determine the size and location of the input rectangle (see DrawInput); sb and tic, the scroll bar; and BD, the border (see DrawWindow). The INSERT and HEX flags can be more easily toggled using the keyboard. */ static WORDTAB ModeTable[] = { { bINT, "top" }, { bINB, "bottom" }, { bINH, "high" }, { bINW, "wide" }, { bTIC, "tic" }, { bBAR, "sb"}, { bBD, "bd" }, { bINSERT, "insert" }, { 0, NULL } /* REQUIRED TERMINATOR */ }; /* Miscellaneous Table The miscellaneous word table is used to parse operands in MEMORY and VMF commands using SetAttribute(..,NONE,operand). Many aliases are provided, and command processing simply looks for a legitimate index. */ static WORDTAB MiscTable[] = { { 1, "on" }, { 2, "off" }, { 1, "yes" }, { 2, "no" }, { 3, "\\r\\n" }, { 3, "crlf" }, { 3, "pc" }, { 4, "\\n" }, { 4, "lf" }, { 4, "unix" }, { 5, "hex" }, { 5, "binary" }, { 0, NULL } /* REQUIRED TERMINATOR */ }; /*================================================================ POO MAIN PROGRAM, API FUNCTIONS, AND THREADS This section contains the main program for the application, the C and Fortran API functions, WaitEvent, WindThread, and Time. Both WaitEvent and WindThread have split personalities, a multithreaded one and single threaded one, which have little code in common. Elsewhere, all dependencies on the threadedness of this software are handled with macros. The main program, the code that receives control from the system when a POO application is invoked, is contained in this software for two reasons. First, its name is not the same in all of the supported windowing systems. By placing this function in the POO software, a uniform and thus portable prototype is provided for your application, namely int appl (void). Second, according to the ANSI C standard, a return in the main program has the same effect as calling the exit function. But the user should be given an opportunity to browse the windows and to save pertinent results when a windowed application is done. Further, if your application is itself multithreaded, the mere fact that appl returns is not sufficient grounds to conclude that the application is done. Thus, when appl returns, the POO main program defers termination and services the application's windows until the user terminates it. The C API consists of cappl, copen, cclose, cputs, and cgets; the Fortran, of nappl, nopen, nclose, nputs, and ngets. The Fortran function nappl calls cappl, while the others are implemented directly. In general, the API functions have little content: they fill an API event struct, pass it to WaitEvent, and return the value returned by WaitEvent. The only exception is cappl. Time allows the representation of time to be hidden and supports the POO's requirements: it obtains the current time, converts the time to external format, or both. This function was added late in the development process to address issues arising in some Windows environments. To maintain consistency, it was propagated to the X and OS/2 versions. WaitEvent is a conduit to the appropriate API co-conspirator Open, Put, Get, or Ctrl. Because it's in the API path, it's home for a common prologue and epilogue for the API functions although both are minimal. WindThread is the window thread when multithreaded; it's started by WaitEvent to open the first window and then runs continuously in an event loop. Whether multithreaded or single, it's called if appl returns. The TERMINATE macro is defined as WindThread(NULL). WaitThread and WindThread contain the major dependencies on the threadedness of this software. In effect, this file contains a single threaded and a multithreaded version of these functions. Elsewhere, dependencies on the threadedness are hidden in macros, e.g., the ..._APPL macros. When single threaded, each API event is processed before another can occur. But, when multithreaded, thread execution must be managed to ensure that the threads don't stumble over each other at critical times, e.g., when referencing global variables. In this context, you might think of each thread as running on its own processor so that timing is critical. The single threaded version of WaitEvent processes pending events, calls the appropriate API co-conspirator, and then looks for more events before returning. Unless the user has set the STOP state flag to suspend the application or input must be solicited through the window, WaitEvent never waits for events. Thus, when single threaded, your application will be responsive only to the extent that it regularly calls API functions. Processor intensive codes that don't call API functions regularly may be dangerous if run single threaded; some systems do not like such applications, and most users will not appreciate them. The multithreaded version of WaitEvent uses a couple mutexes and a semaphore to manage the communication between the application threads and the window thread. A mutex (mutually exclusive) is either owned by a thread or not owned. If a thread requests a mutex, it either becomes its owner or waits until the current owner releases it. Only the current owner of a mutex can release it. In the POO software, semaphores are like railroad semaphores. When a thread encounters a semaphore, the thread is suspended if the semaphore is red and continues if it's green. Any thread can change a semaphore. In computing vernacular, red is termed set or reset or zero, and green is posted or nonzero. The basic strategy for handling API function calls is as follows: generate an API event struct, get the mutex, set the API semaphore red, post the API event to the event queue processed by the window thread, and wait on the API semaphore. For its part, the window thread is continuously looking for events and, after processing an API event, sets the API semaphore green. The mutex effectively serializes API function calls so that there can be only one API event in the queue at the same time. Consequently, the window thread can manipulate its global variables with impunity, i.e., just as if single threaded. Mutexes and condition variables, which are like semaphores, are part of the POSIX threads standard. POSIX semaphores are part of another POSIX standard. To limit its dependencies, the X version uses a condition variable to implement the API semaphore even though a POSIX semaphore would be more natural. The multithreaded implementation of the STOP state flag would also be more naturally implemented with a semaphore but, for the reason just noted, is done with the user mutex. WaitThread must acquire the user mutex before proceeding but immediately relinquishes it. The user mutex is owned by the window thread if the STOP state flag is set and unowned otherwise. Thus WaitThread is suspended if the STOP state flag is set and unimpeded otherwise. This flag is set by Ctrl+S, reset by Ctrl+Q, and toggled by Pause. To avoid the obvious deadlock, the window thread ignores these keystrokes when appropriate, e.g., the mutex cannot be obtained immediately. Finally, with regard to API events, the application always has the right of way, which is reasonable because most applications cannot handle rejection and have almost no ability to block user actions. On the other hand, the user can suspend the application with a single keystroke and thus avoid contention. If the user initiates an operation that cannot be completed as a single event and an API event occurs, the user operation is aborted or suspended. Events generated by the API functions do not have higher priority than those generated by the user, but any resources needed to process an API event are made available. For example, an NB command will be terminated without warning if the application tries to obtain input through the window. Whether multithreaded or single, the POO software terminates the application when the last window is closed. When multithreaded, this issue is taken care of by the window thread. When single, event processing must take the window list into account and look for QUIT messages, which terminate the application. It is not essential that the application be terminated when the last window is closed. The POO software could allow execution to continue in the hope that the application would create a window; but, in the interim, the application would be beyond the control of the user. If you are more daring, it's not difficult to change this software to support background windowed applications! The POO Main Program In X and OS/2, main receives control when the system invokes a C application; in Windows, it's WinMain and arguments are passed. In both Windows and OS/2, the main program contains essential code to deal with system dependencies. In Windows, it's the arguments passed to the main program, primarily the two HINSTANCEs and the last argument, the show state for the first window. All of these arguments are saved in internal variables and referenced only by CreateWND. In OS/2, it's the anchor block handle, which led to the ...ABH variables, which are used in all versions to determine if the window thread exists (not equal). Most of the initialization in the main program could be done with initial values rather than in code. All memory allocations are done in page size blocks. The size of a window struct rounded up to the nearest page and the size of a VM file struct rounded up to the nearest multiple of 64 are saved for use by CreateWND and CreateVMF, respectively. The function key array is initialized to -1, i.e., no assignments. This array of memory file displacements is changed by MemoryLoad and, thereafter, is maintained by MemoryKey. Using a VM file for Work, which plays a number of roles, is handy rather than essential; the same functionality could be achieved by a collection of internal variables. A VM file is used because there is a mechanism for creating it and because its components provide the data types that are needed. The data space (WorkInput) of MAXLENINPUT bytes is used to cut and paste input between windows. The next, base, and eof pointers are used to cut and paste VM files between windows. The path name component holds the POO current directory. The mtime is the time that the application started, which is used as the mtime for NEW VM files. If the POO application returns, WindThread is called so that the application's windows do NOT disappear, i.e., the user can browse. */ int PASCAL WinMain (HINSTANCE ti, HINSTANCE pi, LPSTR cl, int cs) BEGIN VM_HNDL hndl; int k; long size; ThisI = ti; PrevI = pi; SysCmdLine = cl; SysShow = cs; WindABH = ApplABH = 77; SSN = 0L; LWSize = ((sizeof(WND) + 4095) >> 12) << 12; LFSize = ((sizeof(VMF) + 63) >> 6) << 6; for (k = 0; k < 48; ++k) FcnKeyStr[k] = -1; FindTable[0] = '\0'; MemoryFile = FileList = NULL; WndList = NULL; State = POO_UPPERCASEPATHNAMES & sbUCPN; size = LFSize + MAXLENINPUT + sizeof(TEXTMETRIC); size = ((size + 4095L) >> 12) << 12; VM_ACQUIRE(hndl,size); Work = (VMF *)VM_POINTER(hndl); if (! Work) return 0; Work->memhndl = hndl; Work->next = NULL; O_Time(NULL,&(Work->mtime)); *(WorkInput = (char *)(void *)Work + (size_t)LFSize) = '\0'; WorkTemp = WorkInput + MAXLENINPUT; Work->asize = size - LFSize - MAXLENINPUT; Work->size = Work->xsize = Work->posn = Work->flags = 0; if (! O_GetCWD(Work->pn,MAXLENPATH)) (void)strcpy(Work->pn,DIRECTORYOFLASTRESORT); k = appl(); State |= sbDONE; O_WindThread((void *)&WindABH); return k; END /*---------------------------------------------------------------- cappl (application name) is used both to start and to terminate your application. cappl("alphanumeric...") should be called to load the memory file before your application tries to open a window. If Open is called before cappl, Open issues a cappl("Other") and sets memory file updating off before opening the window. Subsequent cappl calls are ignored but return the number of windows that have been opened, which is not necessarily the number currently open. cappl("anything else..."), e.g., cappl(""), terminates the application, i.e., a language-independent exit. Although C and Fortran have exit functions, neither should be used in multithreaded, windowed applications; neither will terminate a POO application properly. The first call to cappl is used to set the application name and load the memory file. If successful, cappl returns the number of windows opened, which will usually be zero. Otherwise, it returns -1, which is foreboding because MemoryLoad doesn't give up easily, only if it cannot acquire any memory. The application should be terminated in this case, but it's possible that it may accomplish something worthwhile. */ int cappl (char *appname) BEGIN VMF *lf; int k; char *s, *t; if (! appname || ! isalnum(*appname)) TERMINATE; if (MemoryFile) return (int)SSN; /* The application and memory file names are set from the argument string. If denotes the longest initial alphanumeric substring truncated to MAXLENAPP characters, the application name is and the memory file name is .mem. The above limitations ensure that the memory file name is valid in all of the supported systems. */ for (k = 0; isalnum(*(appname+k)) && k < MAXLENAPP; ++k) MemoryFileName[k] = ApplicationName[k] = *(appname+k); ApplicationName[k] = '\0'; (void)strcpy(&MemoryFileName[k],".mem"); /* The application names "Pollster" and "Onlooker" are reserved. For Pollster, the argument string is scanned for the name of the poll file; Annotate loads the poll file and creates a pollster for it. For Onlooker, MemoryLoad loads files based on the LOAD records in the memory file; if there are none, the argument string is scanned for a file name; and, if still nothing, the current directory is loaded. An onlooker is then created for each loaded file. Having created one or more windows, Pollster and Onlooker call WindThread so that they always run single threaded, which is appropriate as they are interactive applications. If the argument string is scanned for a file name, the scan starts with the first character (the 9-th) after the application name. If this character is a colon (:), it's skipped. The file name ends with the first blank or semicolon(;) so that Fortran applications need not provide a null terminated string. */ if (! strcmp(ApplicationName,Pollster)) { if (! O_MemoryLoad(oNONE)) return -1; State |= sbPOLL; s = WorkTemp; t = appname + 8; if (*t == ':') ++t; while (*t && *t != ' ' && *t != ';') *s++ = *t++; *s = '\0'; if (s == WorkTemp) (void)strcpy(s,"Poll"); O_Annotate(NULL,oLOAD,WorkTemp,NULL); O_WindThread((void *)&WindABH); } else if (! strcmp(ApplicationName,Onlooker)) { if (! O_MemoryLoad(oLOAD)) return -1; State |= sbONLKR|sbROFS; for (lf = FileList; lf; lf = lf->next) if (lf != MemoryFile) O_CreateWND(lf,NULL,bONLKR); if (! WndList) { s = WorkTemp; t = appname + 8; if (*t == ':') ++t; while (*t && *t != ' ' && *t != ';') *s++ = *t++; if (s == WorkTemp) *s++ = '.'; *s = '\0'; O_CreateWND(O_LoadVMF(NULL,WorkTemp,0,&s),NULL,bONLKR); } O_WindThread((void *)&WindABH); } /* For all other application names, MemoryLoad is called to load the memory file, and all windows must be opened explicitly. */ else { if (! O_MemoryLoad(oNONE)) return -1; State |= sbOTHER; } return (int)SSN; END /*---------------------------------------------------------------- Application Programming Interface Functions As noted above, the API functions have little actual content. The interlocks between the open function and cappl and between the other API functions and the open function are in WaitEvent. C API Functions */ void *copen (char *title, int initsize, int xtrasize) { ANT args; args.alw = NULL; args.buf = title; args.len = initsize; args.rtn = xtrasize; O_WaitEvent(WM_POO_OPEN,&args); return args.alw; } int cputs (void *tw, char *text, int len) { ANT args; args.alw = tw; args.buf = text; args.len = len; return O_WaitEvent(WM_POO_PUT,&args); } int cgets (void *tw, char *appbuf, int len) { ANT args; args.alw = tw; args.buf = appbuf; args.len = len; return O_WaitEvent(WM_POO_GET,&args); } int cclose (void *tw) { ANT args; args.alw = tw; return O_WaitEvent(WM_POO_CLOSE,&args); } int cuser (void *tw, char *cmd, int fcn) { ANT args; long k, kl, seed; double x; args.alw = tw; args.buf = cmd; args.len = fcn; args.rtn = O_WaitEvent(WM_POO_USER,&args); if (fcn < 0) { kl = (seed = -fcn)*100000L; for (x = k = 0; k < kl; ++k) { seed = 2147483647L & (seed * 663608941L); x += ((double)seed)/2147483648.0; }} return args.rtn; } /* Fortran API Functions */ int nappl (char *appname) { return cappl(appname); } void *nopen (char *title, int *initsize, int *xtrasize) { ANT args; args.alw = NULL; args.buf = title; args.len = *initsize; args.rtn = *xtrasize; O_WaitEvent(WM_POO_OPEN,&args); return args.alw; } int nputs (void **tw, char *text, int *len) { ANT args; args.alw = *tw; args.buf = text; args.len = *len; return O_WaitEvent(WM_POO_PUT,&args); } int ngets (void **tw, char *buffer, int *len) { ANT args; args.alw = *tw; args.buf = buffer; args.len = *len; if (!(*tw) && *len == 8) { (void)strcpy(buffer,"\t\a\f\v\b\r\n"); return 8; } return O_WaitEvent(WM_POO_GET,&args); } int nclose (void **tw) { ANT args; args.alw = *tw; return O_WaitEvent(WM_POO_CLOSE,&args); } /*---------------------------------------------------------------- WaitEvent (event, API event struct) is a conduit to the API co-conspirators Open, Put, Get, and Ctrl. Aside from the initial interlocks, the single and multithreaded versions of WaitEvent have nothing in common except purpose. */ int O_WaitEvent (int event, ANT *args) BEGIN if (! WndList) { if (event != WM_POO_OPEN) return -2; if (! MemoryFile) { if (cappl(Other) < 0) return -2; MemoryFile->flags &= ~bUPDATE; }} args->eno = event; #ifdef POO_MULTITHREADED /* Multithreaded A malicious application can wrack havoc here by creating multiple threads that immediately open a window. If the first one here is interrupted before the window thread changes the ABH variable, the second may erroneously start the window thread a second time! In any case, Mother is not used when starting the window thread. The window thread will set the API state bit off and post the API semaphore after it processes the API event. The API semaphore is protected by Mother, which serializes the application threads so that there is ONLY one API event at a time. */ if (ApplABH == WindABH) { Mother = CreateMutex(NULL,FALSE,NULL); UsrMtx = CreateMutex(NULL,FALSE,NULL); State &= ~sbSTOP; APISem = CreateSemaphore(NULL,0L,1L,NULL); State |= sbAPI; O_BeginThread(O_WindThread,args); WaitForSingleObject(APISem,INFINITE); } else { if (State & sbSTOP) { WaitForSingleObject(UsrMtx,INFINITE); ReleaseMutex(UsrMtx); } WaitForSingleObject(Mother,INFINITE); WaitForSingleObject(APISem,0); State |= sbAPI; if (PostThreadMessage(WindABH,event,0,(long)args)) WaitForSingleObject(APISem,INFINITE); else { State &= ~sbAPI; args->rtn = -2; } ReleaseMutex(Mother); } #else /* Single Threaded If there are some windows, the first task is to process all queued events, which is appropriate because they occurred before the API event. In Windows, the PeekMessage return is nonzero for a QUIT message, while the GetMessage return is zero. Thus, the QUIT test must be in the loop. Because the queued events may close the last window, the window list must be tested after the loop. */ if (WndList) { while (WndList && PeekMessage(&Msg,NULL,0,0,PM_REMOVE)) { if (Msg.message == WM_QUIT) TERMINATE; TranslateMessage(&Msg); DispatchMessage(&Msg); } if (! WndList) TERMINATE; } /* The STOP state flag must be examined after processing the pending events because one of the pending events may change it. The STOP state flag is the single threaded rendition of the user semaphore. The message must be set to something other than WM_QUIT. */ Msg.message = WM_PAINT; while (State & sbSTOP && GetMessage(&Msg,NULL,0,0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); if (! WndList) TERMINATE; } if (Msg.message == WM_QUIT) TERMINATE; /* With all queued events processed, the API event is processed by calling the appropriate API co-conspirator directly, which can be done because there is only one thread. The co-conspirators return NULL or the valid open identifier, which can be used to reference the window struct because there is only one thread. Get sets the api... component if input must be solicited through the window, which requires a standard event loop. The API state flag plays the role of the API semaphore when single threaded; it is set off when input has been obtained. The user, of course, could close the window rather than enter input so that the window list must be tested continuously. */ BEGIN WND *lw; if (event == WM_POO_PUT) lw = O_Put(args); else if (event == WM_POO_GET) lw = O_Get(args); else if (event == WM_POO_OPEN) lw = O_Open(args); else lw = O_Ctrl(args); if (lw && lw->apiarg) { State |= sbAPI; while (WndList && (State & sbAPI) && GetMessage(&Msg,NULL,0,0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } if (! WndList || Msg.message == WM_QUIT) TERMINATE; } END /* In general, all pending events will have been processed before the API event, but input through the window will always leave at least one pending, e.g., the release for the key or button event that terminated the input request. */ while (WndList && PeekMessage(&Msg,NULL,0,0,PM_REMOVE)) { if (Msg.message == WM_QUIT) TERMINATE; TranslateMessage(&Msg); DispatchMessage(&Msg); } if (! WndList) TERMINATE; #endif return args->rtn; END /*---------------------------------------------------------------- WindThread (NULL | &WindABH | API event struct) is the window thread when multithreaded but also provides services when single threaded. The only code shared by the two versions of this function terminates the application in proper fashion. There are four calls to WindThread: from WaitEvent via the system's begin thread function with an ANT argument to open the first window and then to become the window thread, from cappl with an argument of &WindABH to implement Pollster and Onlooker, from the POO main program with an argument of &WindABH if appl returns, and from cappl with a NULL to terminate the application. When single threaded, the first obviously cannot occur, but there are numerous TERMINATE aka WindThread(NULL) calls from WaitEvent if it fields a QUIT message or finds the window list empty. The use of &WindABH here is not essential, any identifiable void * pointer would suffice because a non-NULL argument is either an API event struct pointer or anything else. WindABH is anything else. */ void O_WindThread (void *arg) BEGIN VM_HNDL hndl; WND *lw; VMF *lf; #ifdef POO_MULTITHREADED /* Multithreaded WindThread(NULL) aka TERMINATE terminates the application, but there are a couple cases because the window thread may or may not be running, i.e., the application may or may not have opened a window. If it is NOT running, this code will fall through to the common code at the end, i.e., termination of the process. If it is running, a QUIT is posted to get its attention, and the calling thread, an application thread, suspends itself on the return semaphore, which is always red. In Windows, the QUIT must be posted to the window thread; the more common PostQuitMessage will not work in this situation because it puts the message on the queue of the thread that issues the call. */ if (! arg) { if (WindABH != ApplABH) { PostThreadMessage(WindABH,WM_QUIT,0,0); WOOF; SUSPEND_THREAD; }} /* Multithreaded WindThread(&WindABH) is called in two situations: if appl returns or if cappl calls to start Pollster or Onlooker, which are interactive applications and always run single threaded. If appl returns, there are two cases because the window thread may or may not be running. If it is NOT running, there are no windows and the application is terminated because the event loop below only runs if there are windows. If it is running, the application suspends itself quietly and without any notification of the window thread so that the window thread continues running, which gives the user an opportunity to browse and terminate when desired. If cappl calls with &WindABH, the window thread is NOT running and there is likely a pollster or one or more onlookers. Nothing need be done because the event loop below will either run or not. If there are windows, it runs Pollster and Onlooker single threaded and terminates appropriately. If cappl did not successfully create any windows, termination is the only option anyway. */ else if (arg == (void *)&WindABH) { if (WindABH != ApplABH) { WOOF; SUSPEND_THREAD; }} /* Multithreaded WindThread(API event struct) opens the window specified by the open API event and then becomes the window thread. It is crucial, of course, to change WindABH to something other than ApplABH, it's initial value, so that this software can tell whether the window thread is running. There are no other threads actually running at this point because WaitEvent is the only other thread and is waiting on the API semaphore. */ else { WindABH = GetCurrentThreadId(); RtnMtx = CreateMutex(NULL,TRUE,NULL); if (O_Open((ANT *)arg)) { arg = (void *)&WindABH; RELEASE_API; }} /* When multithreaded, this is where the action is. This event loop continues until either the last window is closed or a QUIT message is received, i.e., the get message function returns zero. If this loop dies, the application dies with it. In Windows, all of the WM_POO_... events are processed here since they are posted as thread messages; thread messages have a NULL window value and cannot be dispatched as window messages. The API semaphore is set green after processing an API event other than a Get that must solicit input through the window, which sets the API argument component in the window struct. This component must be set non-NULL by any API event that cannot be completely processed; at present, all API events are completed except when input must be solicited through the window. The various conditions that terminate input through the window lead to APIEvent, a macro, and, after appropriate processing, the API semaphore is set green. */ if (arg == (void *)&WindABH) while (WndList && GetMessage(&Msg,NULL,0,0)) BEGIN if (WM_POO_OPEN <= Msg.message && Msg.message <= WM_POO_USER) { ANT *args; args = (ANT *)(VOID *)(Msg.lParam); if (Msg.message == WM_POO_PUT) lw = O_Put(args); else if (Msg.message == WM_POO_GET) lw = O_Get(args); else if (Msg.message == WM_POO_OPEN) lw = O_Open(args); else lw = O_Ctrl(args); if (!(lw && lw->apiarg)) { RELEASE_API; }} else { TranslateMessage(&Msg); DispatchMessage(&Msg); } END #else /* Single Threaded WindThread either terminates the application (if the argument is NULL) or enters an event loop. If appl is the caller, this provides the user an opportunity to browse the output after the application has terminated. If cappl is the caller, the event loop runs as either Pollster or Onlooker, which are interactive and always run single threaded. */ if (arg) while (WndList && GetMessage(&Msg,NULL,0,0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } #endif /* Application Termination The taxes have all been paid, and it's time to read the will and distribute the assets. File updating will take care of pollsters and output direction for others, while saving the file positions will take care of Onlooker because MemoryStore will find the SAVE flag set in the memory file. For disk files associated with an onlooker, the position is based on the first onlooker in the list because the window list is an MRU cache so that order implies use with respect to time. MemoryStore is NOT a proper function; it's an extension of this code that assumes its context, e.g., knowing that all files will be destroyed when it returns, it corrupts the memory file. */ for (lf = FileList; lf; lf = lf->next) BEGIN if (lf == MemoryFile) continue; else if (lf->flags & bUPDATE) O_StoreVMF(lf,0,NULL); else if (lf->flags & bSAVE) { for (lw = WndList; lw; lw = lw->next) { if (lw->dout == lf && lw->flags & bONLKR) break; } if (lw && O_Valid(lw->top,lf->base,lf->eof)) lf->posn = lw->top - lf->base; } END O_MemoryStore(); MemoryFile = NULL; /* All appropriate processing of the VM files has been completed so that it's simply a matter of releasing the memory; DestroyVMF is not required. */ while (FileList) { hndl = FileList->memhndl; FileList = FileList->next; if (hndl) { VM_RELEASE(hndl); }} /* Finally, all window assets are released, which is transparent in terms of this code; in effect, the Windows function DestroyWindow calls EventProc with a DESTROY message. */ while (WndList) { lw = WndList; CARET_OFF; DRAG_OFF; DestroyWindow(lw->wnd); } /* Although Work contains many things, none need be saved so that it suffices to release the memory. */ if (Work) { VM_RELEASE(Work->memhndl); } ExitProcess(0); return; END /*---------------------------------------------------------------- Time (buffer pointer, pointer to system-dependent time) returns the current time in the argument or produces an external representation of either the current time or the time provided; it returns the number of characters placed in the buffer. The POO software is indifferent to the external representation produced by this function, which duplicates the functionality of strftime in many respects. */ int O_Time (char *buffer, FSTIME *ft) BEGIN char *Months = "JanFebMarAprMayJunJulAugSepOctNovDec", *Days = "SunMonTueWedThuFriSat", *s, *t; #ifdef POO_NATIVE_WIN32 FILETIME tmp; SYSTEMTIME st; if (! buffer) { GetSystemTime(&st); SystemTimeToFileTime(&st,ft); return 0; } if (! ft) GetLocalTime(&st); else { if (! FileTimeToLocalFileTime(ft,&tmp)) return 0; if (! FileTimeToSystemTime(&tmp,&st)) return 0; } s = buffer; t = Days + 3*st.wDayOfWeek; *s = *t; *(s+1) = *(t+1); *(s+2) = *(t+2); s += 4; *(s-1) = ' '; t = Months + 3*(st.wMonth - 1); *s = *t; *(s+1) = *(t+1); *(s+2) = *(t+2); s += 3; s += sprintf(s," %2i %2i:",(int)(st.wDay),(int)(st.wHour)); s += sprintf(s,"%.2i:%.2i ",(int)(st.wMinute),(int)(st.wSecond)); sprintf(s,"%4u",(int)(st.wYear)); #else time_t tmp; struct tm *st; if (! buffer) { *ft = time(NULL); return 0; } if (! ft) { tmp = time(NULL); ft = &tmp; } st = localtime(ft); s = buffer; t = Days + 3*st->tm_wday; *s = *t; *(s+1) = *(t+1); *(s+2) = *(t+2); s += 4; *(s-1) = ' '; t = Months + 3*st->tm_mon; *s = *t; *(s+1) = *(t+1); *(s+2) = *(t+2); s += 3; s += sprintf(s," %2i %2i:",st->tm_mday,st->tm_hour); s += sprintf(s,"%.2i:%.2i ",st->tm_min,st->tm_sec); sprintf(s,"%4u",1900 + st->tm_year); #endif return 24; END /*================================================================ API CO-CONSPIRATORS The strategy for maintaining the integrity of the global data held by this software in the context of its threadedness was outlined in the previous section. In effect, the application threads are limited to the API functions and WaitEvent, and the window thread is the only thread that executes the remainder of this software. The strategy used in this software is not the only one possible, but it's simple to implement. In effect, the co-conspirators Open, Put, Get, and Ctrl are the real API functions: they are executed by the window, or only, thread and can reference and change any data with impunity. All of the API co-conspirators have the same calling sequence, an API event struct containing the arguments that were passed to the API function, return results in this struct, and return either the window struct pointer or NULL. Open, Put, and Ctrl are straightforward because they can process the event completely. But, if Get must solicit input through the window, the processing of the API event cannot be completed immediately. This type of situation, i.e., when other events must be processed before an API event can be completed, is handled by saving the API event struct pointer in the window struct and then waiting for APIEvent to trigger completion of the API event when an appropriate event occurs, e.g., the user presses Enter. Since a general mechanism is not required, APIEvent is simply a macro that calls Get. Thus, although the mechanism needed to address the larger issue is in place, modifications may require that APIEvent be promoted to a function with sufficient logic to handle any expanded responsibilities. Except when the API component is set (by Get), the co-conspirators ignore the API semaphore so that it's the caller's responsibility, e.g., the event processor when multithreaded. When single threaded the API flag is set only if the API component is set. ------------------------------------------------------------------ Open (WM_POO_OPEN processing) expects its three arguments in the components buf @...: string, len size in bytes (others only), and rtn extra size in bytes (others only) and returns in alw the window struct pointer or NULL. Open creates a pollster, onlooker, or other depending on the name in the string argument as follows: "Pollster:<poll file name>" creates a pollster for the specified file name relative to the POO current directory, "Onlooker:<file name>" creates an onlooker for the specified file name relative to the POO current directory, and "<name>:<title>" creates a named other and associates with it an unnamed and empty VM file. For a pollster, the file must look like a poll file if it exists and is created if it doesn't exist; for an onlooker, it must exist as a VM file or be loadable from the file system as a regular file or directory; and, for others, the size and management policy for the output stream (VM file) are determined by the size and extra size values. The Size and Extra Size Parameters for Output Streams The size parameter in len determines the amount of virtual memory that is initially allocated for the VM file, i.e., output stream. The file is created by CreateVMF, which always rounds allocations up to an integral number of pages less LFSize. If this parameter is not positive, a minimal size file is allocated, 4096 - LFSize; otherwise, the size + 1 is passed to CreateVMF. When the space allocated for a VM file is exhausted, it's either expanded or the oldest data is truncated. The extra size parameter in rtn determines the management strategy as well as the minimal amount used when expanding or truncating a VM file. If the extra size is negative, the TRUNC flag is set so that ExpandVMF discards at least the minimal amount of oldest data. If zero or positive, the VM file is expanded by at least the minimal amount; but, if expansion fails because memory is not available, the strategy is changed to truncation automatically. The minimal amount is the extra size rounded up to the nearest multiple of 1024 but limited to half the initial allocation. The management strategy has no adverse impact on output direction. When output is directed, data is written to the output file if and only if it is about to be discarded. Thus, for example, a VM file that is being expanded is held in virtual memory until it's either destroyed or the strategy is changed to truncation. */ WND *O_Open (ANT *args) BEGIN long flags, size; WND *lw; VMF *lf; char *title, *s, *t; /* The legitimate modifiers prefixed to the window name are: {E|e}@ to enable input echoing to the output stream (ECHO), {C|c}@ to prevent the user from closing the window (CLOSE), {L|l}@ to enable line mode rather than C stream mode (LINE), {X|x}@ to disable C control character processing (STET), and {H|h}@ to establish a binary output stream (HEX). Here, the modifiers are simply OR'd together as they are stripped from the beginning of the name/title string. After the window is created, they are XOR'd to the default window flags, which has the effect of reversing the default settings. By default, the ECHO, CLOSE, LINE, and STET flags are off (zero). Thus, this code need not be changed if the defaults are changed, you simply have to remember that prefixing the modifier reverses the default. The HEX modifier affects the associated VM file and is propagated into the window flags automatically. Only the CLOSE flag is meaningful for pollsters and onlookers. Unrecognized modifier characters are discarded, and leading blanks before the name are deleted. */ flags = 0; title = args->buf; if (title) { while (*(title+1) == '@') { if (*title == 'E' || *title == 'e') flags |= bECHO; else if (*title == 'C' || *title == 'c') flags |= bCLOSE; else if (*title == 'L' || *title == 'l') flags |= bLINE; else if (*title == 'X' || *title == 'x') flags |= bSTET; else if (*title == 'H' || *title == 'h') flags |= bHEX; title += 2; } while (*title && *title == ' ') ++title; } /* The name/title string for pollsters and onlookers is parsed here and must be of the form {"Pollster" | "Onlooker"}:<blanks><file name>[;] where the optional semicolon(;) is for Fortran applications since null terminated strings are not natural in Fortran. A blank in the file name is a blank; it does not mark the end of the file name even though embedded blanks can cause problems. The file name is processed just as the operand in POLLSTER and ONLOOKER commands. An other is created if the name/title string does not have one of forms above, e.g., if the argument is a NULL. For others, the VM file for the output stream is created using CreateVMF, and then the window is created using CreateWND, which parses the name/title string for the name and window title. Since the memory file is scanned for the window attributes for the window and they are qualified by the window name, unique window names are recommended. But, in some circumstances, window classes may be appropriate, after all pollsters and onlookers are window classes and share attributes. The HEX flag is applied to the VM file, not the window; but, when associating a HEX file with a window, the HEX window flag is set. As a window flag, the HEX flag controls the mode in which the data is displayed and can be toggled by the user. */ lw = NULL; lf = NULL; if (title && !strncmp(title,Pollster,8) && *(title+8) == ':') { for (s = title+9; *s == ' '; ++s); for (t = WorkTemp; *s && *s != ';'; *t++ = *s++); *t = '\0'; if (*WorkTemp) lw = O_Annotate(NULL,oLOAD,WorkTemp,NULL); } else if (title && !strncmp(title,Onlooker,8) && *(title+8) == ':') { for (s = title+9; *s == ' '; ++s); for (t = WorkTemp; *s && *s != ';'; *t++ = *s++); *t = '\0'; if (*WorkTemp) lf = O_LoadVMF(NULL,WorkTemp,0,&s); if (lf) lw = O_CreateWND(lf,NULL,bONLKR); } else { size = (args->len > 0) ? args->len + 1 : 1; lf = O_CreateVMF(NULL,NULL,size,&s); if (lf && flags & bHEX) { lf->flags |= bHEX; flags &= ~bLINE; } if (lf) lw = O_CreateWND(lf,title,bOTHER); } /* For all subclasses, the modifier flags less the HEX flag are XOR'd to the window flags. The HEX flag is set correctly when CreateWND associates the VM file with the window. For others, the extra size parameter is used to set the extra size component in the VM file. The extra size is at least 1K because the minimal file size is at least 2K, see CreateVMF. */ if (lw) { if (lw->flags & bOTHER) { if (args->rtn < 0) lf->flags |= bTRUNC; if (! args->rtn) lf->xsize = 1024; else lf->xsize = ((abs(args->rtn) + 1023) >> 10) << 10; size = (lf->asize >> 11) << 10; if (lf->xsize > size) lf->xsize = size; } lw->flags ^= (flags & ~bHEX); } else if (lf) O_DestroyVMF(lf); args->alw = lw; return lw; END /*---------------------------------------------------------------- Put (WM_POO_PUT processing) expects its three arguments in the components alw window identifier, buf output data, and len length of data in bytes and returns in rtn a non-negative value if successful, -1 if the data could not be appended to the output stream, and -2 if the identifier is not valid. Ordinarily, one would expect that the output data pointer would be non-NULL and that the first character, if text, would be nonzero, but these possibilities cannot be ignored. Clearly, Put could do nothing in these ostensible error situations, but there is a use for these situations. Specifically, Put updates the window to show the end of the output stream starting at the bottom of the window if len is nonzero and returns -1 or 0 to indicate whether the user suspended window updates; normally Put updates the window. Thus, cputs(*,[NULL|""],{0|-1}) and ngets(*,0,{0|-1}) are useful. The potential interaction between this function and the NB command is ignored here because it's viewed as a user responsibility and is consistent with the general principal that the application has the right of way. Although it would be reasonable to reset the TYPE flag here, i.e., terminate the NB, this is not done. Thus, the user's input and application's output may be interspersed. The user can insure that there are no interactions by suspending the application before entering the NB command. */ WND *O_Put (ANT *args) BEGIN WND *lw; VMF *lf; int len, upd; char *text, *s; /* The identifier in alw is validated by searching the window list but excludes pollsters and onlookers. Although your application can open pollsters and onlookers, it's not allowed to append data to the associated file. An other should always have an associated VM file, but there is no harm in checking, particularly because the consequences of a mistake would be very bad. */ for (lw = WndList; lw && lw != args->alw; lw = lw->next); if (! lw || lw->flags & (bPOLL|bONLKR) || ! lw->dout) { args->rtn = -2; return NULL; } /* Binary Output Stream Processing For binary output, the output pointer must be non-NULL and the length must be positive; otherwise, the special processing for error situations is applied, e.g., a window update, etc. All of the work is actually done in AppendVMF, which always sets the window's eof pointer to the new end of the file and may call ExpandVMF; AppendVMF returns NULL if unsuccessful. Updating is automatic unless the user has set the STATIC flag with F6. */ if ((lw->dout)->flags & bHEX) { if (args->buf && args->len > 0) { s = O_AppendVMF(lw,NULL,args->buf,args->len); if (!(lw->flags & bSTATIC)) { lw->bot = lw->eof - 1; lw->ybot = -1; UPD_WND; } args->rtn = (s) ? args->len : -1; return lw; } args->buf = NULL; } /* Ostensible Error Situations Rather than doing nothing in these situations, Put updates the window if the length argument is nonzero and returns -1 or 0 depending on whether the STATIC flag is or is not set. */ if (! args->buf || (! *(args->buf) && args->len <= 0)) { if (args->len) { lw->base = (lw->dout)->base; lw->eof = (lw->dout)->eof; lw->bot = lw->eof - 1; lw->ybot = -1; lw->shft = 0; UPD_WND; } args->rtn = (lw->flags & bSTATIC) ? -1 : 0; return lw; } /* C Control Character Processing for a Text Output Stream For text output, the length is not required: if less than 1, it's computed based on the assumption that the text is terminated by a null character (zero), e.g., cputs(*,text,0) and fputs(*,text) are analogous. To handle the processing of an initial substring of some C control characters, the local variables 'text' and 'len' are set from the pertinent arguments, and 'upd' is set to update the window. For output text, an initial substring consisting of the ANSI C control characters is processed in a manner consistent with the standard, specifically \a "produces an audible...alert" (ANSI); \b "moves the active position to the previous position on the current line" (ANSI) or deletes the line termination, \r (not followed by a \n) "moves the active position to the initial position of the current line" (ANSI) or deletes the previous line if at the beginning of a new line; \f changes the window updating so that the text to be appended appears at the top of the window; \v cancels window updating; and \0 empties the VM file (rewind). In the ANSI standard, the "active position" is where new text is added to an output stream. It should be noted that \b and \r do more than the ANSI standard specifies; according to the standard they affect the active position only within the current line, but this code does not restrict them to the current line. For \b and \r, some care must be taken to do nothing for empty VM files and to avoid anything ill-advised when at the beginning of a VM file. */ else BEGIN upd = 1; lf = lw->dout; text = args->buf; len = args->len; if (len <= 0) len = strlen(text); if (!(lw->flags & bSTET)) for (; len; --len, ++text) BEGIN if (*text == '\a') WOOF; else if (*text == '\f') upd = 0; else if (*text == '\0') { if (lf->flags & bUPDATE) O_StoreVMF(lf,0,NULL); lw->shft = 0; lf->posn = 0; *(lf->eof = lw->eof = lw->base = lf->base) = '\n'; } else if (*text == '\v') upd = -1; else if (*text == '\b') { if (lf->eof == lf->base) continue; s = lf->eof - 1; if (*s == '\n' && s != lf->base && *(s-1) == '\r') --s; *(lw->eof = lf->eof = s) = '\n'; } else if (*text == '\r') { if (len > 1 && *(text+1) == '\n') break; if (lf->eof == lf->base) continue; s = lf->eof - 1; while (s > lf->base && *(s-1) != '\n') --s; *(lw->eof = lf->eof = s) = '\n'; } else break; END /* Text Output Stream Processing The residual text, if any, is appended to the output stream at the "active position." As for binary output, the work is really done by AppendVMF. Since AppendVMF MAY CHANGE the VM file pointer in the window struct, it MUST BE RELOADED after AppendVMF returns. AppendVMF leaves space for at least two additional characters at the end of a text file if the last character of the file is not a \n. Thus, Put can implement the LINE modifier without looking at the available space. In VM files, the eof character is not part of the file but must be a \n. If there is no residual text, s is set to the last text character in the file; it's used to update the window. */ if (len) { s = O_AppendVMF(lw,NULL,text,len); lf = lw->dout; if (! s) { upd = 1; s = lf->eof - 1; len = -1; } else if (lw->flags & bLINE && *(lf->eof - 1) != '\n') { text = lf->eof; if (lf->flags & bCRLF) *text++ = '\r'; *text++ = '\n'; *(lw->eof = lf->eof = text) = '\n'; }} else s = lf->eof - 1; /* It remains to update the window unless the application specified that updating be skipped or the user requested STATIC operation. DrawWindow automatically corrects both the pointer and relative line number, i.e., the settings here need not be impeccable. The return value for the API function is the length of the residual text, which may be zero. */ if (upd >= 0 && !(lw->flags & bSTATIC)) { if (upd) { lw->bot = lw->eof - 1; lw->ybot = -1; } else { lw->bot = NULL; lw->top = s; lw->ytop = 0; } UPD_WND; } END args->rtn = len; return lw; END /*---------------------------------------------------------------- Get (WM_POO_GET processing) expects its three arguments in the components alw window identifier, buf input buffer, and len buffer length and returns in rtn the number of characters placed in the buffer; -1 if an end-of-file was encountered; or -2 if the identifier is not valid, the buffer pointer is NULL, or the length less than 2. Get is compatible with standard C stream input: at most length-1 characters up to and including the next newline, whichever occurs first, is placed in the buffer and a null character is appended. If a newline is not encountered, any residual input is retained for the next Get. But, if the LINE flag is set, at most length-1 characters of the next line are placed in the buffer, a null is appended, and any residual input is discarded; a newline is never returned. Whether the LINE flag is set or not, the arguments must be consistent with C stream input, e.g., if you can find a card and card reader, the length should be 82. There are four sets of components in the window structure that are used for input control. The input buffer and related pointers and flags are used to manage input that is being entered through the window. The extra buffer holds residual input from a previous get and typeahead; a typeahead EOF is a Ctrl+Z (26) in the extra buffer. The extra buffer holds a null terminated string with embedded newlines. The user can delete the last line in the extra buffer with a Ctrl+Delete, which moves the line to the input buffer. The din... components hold information if the input is directed (< or << commands) or pasted (F5). Since they are pointers to a VM file, line termination may use \r\n or \n. When such input is exhausted, the file is freed, and the application receives an EOF return only if input was directed with a < command. The user can discard directed input by issuing a "<-" or "<<-". Finally, the api... component is either NULL or a pointer to the API event struct that caused input to be solicited through the window. While processing a window event that provides input, control is returned here by APIEvent to complete event processing if the API component is not NULL When input is obtained through the window, this function is called twice: first to process the API event and second to process an event that terminates or provides input, namely Enter, Ctrl+Z (EOF), a CLOSE of the window, input direction, or file pasting. On the first call, the eno component will be WM_POO_GET; on the second, it will not have this value. To obtain input through the window, Get saves the API event struct in the api... component and returns: it's assumed that the window thread is running or that the caller will start an event loop. Unlike Put, Get resets the TYPE flag, which indicates that an NB command is in progress; the user may be surprised but should have suspended the application before issuing the NB. Further, any current input is discarded if Get must solicit input through the window. The application has the right of way! */ WND *O_Get (ANT *args) BEGIN WND *lw; VMF *lf; int len, k; char *s, *b, *buffer, *t; /* Entry via APIEvent, Input Termination If the eno component is not ...GET, then this call is via APIEvent and the API component of the window struct is an API event struct. The API event struct components are valid since they have already been checked in the previous ...GET call, and buffer has been set to the null string. If the rtn component is negative, APIEvent is processing an EOF. Otherwise, input has been directed or is available in the extra buffer, and the original API event can be completed. */ if (args->eno != WM_POO_GET) { lw = (WND *)(args->alw); if (args->rtn < 0) {args->rtn = -1; lw->apiarg = NULL; RELEASE_API; return lw;} buffer = args->buf; } /* Entry from an API Function If the eno component is ...GET, this is a standard API function call. Thus, the open identifier is validated by searching the window list, and the buffer and length components are examined to make sure they are valid. */ else BEGIN for (lw = WndList; lw && lw != args->alw; lw = lw->next); if (! lw || lw->flags & (bPOLL|bONLKR) || ! args->buf || args->len < 1) { args->rtn = -2; return NULL; } if (args->len > 1 || (lw->dout)->flags & bHEX) *(args->buf) = '\0'; else { args->rtn = -2; return lw; } /* If input has been directed or pasted, the din components hold the relevant data. If not, the extra buffer may contain residual or typeahead data. Input direction is tested first so that the user can enter typeahead while the application churns through the input provided by direction. Input can be directed using the < or << commands or by pasting. When directed input is exhausted, there are two options depending on how the input was directed. If the user directed input with a < command, the EOF window flag is on, and an EOF is returned. If done with a << command or by pasting, the EOF flag is off, and no EOF is provided to the application. Thus, in these situations, the end of the directed input is transparent to the application, and input is obtained from the extra buffer or through the window. */ buffer = NULL; if (lw->din) { if (! O_Valid(lw->dinptr,lw->dinbase,lw->dineof)) { lf = lw->din; lw->din = NULL; O_DestroyVMF(lf); if (lw->flags & bEOF) { args->rtn = -1; return lw; } if (*(lw->extra)) buffer = args->buf; } else buffer = args->buf; } else if (*(lw->extra)) buffer = args->buf; /* If input must be solicited through the window, the application has the right of way; thus, the TYPE flag is set off, which terminates the NB command that set it. The API event struct is saved in the API component so that APIEvent is called when input becomes avail- able. Activate raises the window and switches the keyboard focus to it. */ if (! buffer) { lw->flags &= ~bTYPE; lw->apiarg = args; O_InputMgr(lw,NULL,0); O_Activate(lw); return lw; } END /* Completing GET Event Processing for Directed Input Whatever the situation was, at this point, buffer is set and input is available, i.e., the API event can be completed by moving the available input to the buffer. Directed input is complicated by the fact that the input lines may be terminated by either \r\n or just a \n. The special case when the first input character is a newline is handled first; the usual code would handle it correctly except for one unusual case, a file that begins with a \n character. In the general case, up to len characters up to the next \n are moved to the buffer, and the conditions when the move terminates determine what must be done. If the move stops with k greater than or equal to zero, then s is a \n, at least one character has been moved, and the previous byte must be replaced by a \n if it's a \r. In any case, the buffer is large enough to return the \n this time, and the next input character is the one after s. If k is negative (-1), there is no room for the \n, and it's the next input character. A null character (zero) is appended to the input being returned in all cases. If the LINE flag is set, the remainder of the current input line is discarded, and the din... read pointer is updated. */ b = buffer; len = 0; if (lw->din) { s = lw->dinptr; k = len = args->len; if ((lw->dout)->flags & bHEX) { for (k = 0; k < len && s != lw->dineof; ++k) *b++ = *s++; len = k; } else if (*s == '\n') { *b++ = *s++; *b = '\0'; lw->dinptr = s; len = 1; } else { while (k-- && *s != '\n') *b++ = *s++; if (k > 0) { if (*(s-1) == '\r') --b; else --k; *b++ = '\n'; *b = '\0'; ++s; len -= (k+1); } else if (! k) { if (*(s-1) == '\r') { *(b-1) = '\n'; ++s ; } *b = '\0'; --len; } else { *(b-1) = '\0'; --s ; --len; }} if (lw->flags & bLINE && *(s-1) != '\n') while (*s++ != '\n'); lw->dinptr = s; } /* Completing GET Event Processing for Residual/Typeahead Input The residual or typeahead input is in the extra buffer in the form [ ...\n | 26]...\0, i.e., a null terminated string with embedded newlines and EOF's, which are represented as 26's. This is a bit easier that directed input because there are no \r's. The input is taken from the front, and the residual data, if any, is moved to the front. If LINE is set, residual input in the current line is discarded. This code is unclean but avoids errors in some compilers, which could not compile the most obvious code! */ else if ((lw->dout)->flags & bHEX) { if (*(lw->extra) == 26) { len = -1; *(lw->extra) = '\0'; } else { O_InputMgr(lw,"Must direct hex input.",21); lw->cur = lw->input; *(lw->extra) = '\0'; return lw; }} else if (*(s = lw->extra)) { if (*s == 26) { len = -1; ++s; } else { char z; z = '\n'; k = args->len - 1; for (len = 0; len < k && *s != z; ++len) *b++ = *s++; if (len < k) { *b++ = *s++; ++len; } *b = '\0'; if (lw->flags & bLINE && *s) while (*s++ != '\n'); } t = lw->extra; while (*s) *t++ = *s++; *t = '\0'; } /* LINE and ECHO Handling Up to this point, the input has been moved to the application's buffer consistent with C stream input. Thus, whether the LINE flag is set or not, the application must provide buffer and length arguments consistent with C stream input. If the LINE flag is set, a terminal \n is discarded. In this respect, the LINE flag makes little difference; it's primary impact is that residual input in a line is discarded rather than being saved for the next Get. Implementation of the ECHO flag is straightforward but interacts with the LINE flag because the latter flag implies a \n at the end of the input line. If ECHO is set, input is echoed ONLY WHEN AND AS it's returned to the application. Thus, using this software, interspersed gets and puts could play havoc with the output stream because portions of the input line would be interspersed with the output. Most systems echo input lines when entered. Window updating is required only if the ECHO flag is set. But, if this call came via APIEvent, normal event processing ensures that the window is updated so that it suffices to just set the window parameters appropriately. */ if (len > 0) BEGIN if (lw->flags & bLINE) { t = buffer + len; if (*(t-1) == '\n') { *(--t) = '\0'; --len; } if (lw->flags & bECHO) { *t = '\n'; O_AppendVMF(lw,NULL,buffer,len+1); *t = '\0'; }} else if (lw->flags & bECHO) O_AppendVMF(lw,NULL,buffer,len); if (lw->flags & bECHO) { lw->bot = lw->eof - 1; lw->ybot = -1; if (args->eno == WM_POO_GET) UPD_WND; } END /* If input through the window has been terminated and the API event completed, the API window component must be set NULL and the API semaphore must be set green so that the application can continue. If the API event is completely processed by a single call, the caller will take care of the API semaphore. */ if (args->eno != WM_POO_GET) { lw->apiarg = NULL; RELEASE_API; } args->rtn = len; return lw; END /*---------------------------------------------------------------- Ctrl (WM_POO_CTRL processing) has different expectations depending on the eno component of the API event struct. The rtn component, the value returned by the API function, is set to -2 if the identifier is not valid and -1 if the eno component is not valid. */ WND *O_Ctrl (ANT *args) BEGIN WND *lw; int h; for (lw = WndList; lw && lw != args->alw; lw = lw->next); if (! lw) { args->rtn = -2; return NULL; } switch (args->eno) BEGIN /* WM_POO_CLOSE If eno is ...CLOSE, Ctrl looks only at alw, the window identifier, and sets rtn to 0 if the window is closed. Because the application set the CLOSE flag, it does not prevent closure. The window will be gone when close returns so that subsequent API calls with the window identifier should return -2. */ case WM_POO_CLOSE: lw->flags &= ~bCLOSE; SendMessage(lw->wnd,WM_CLOSE,0,0); args->rtn = 0; return NULL; /* WM_POO_USER If eno is ...USER, Ctrl expects three arguments alw window identifier, buf buffer containing a null terminated string, and len a KeyFcn function index and sets rtn to either the number of characters returned in the buffer or 1 or 0 depending on whether the CARET flag is set. Use of this functionality is discouraged; it contradicts the basic design philosophy of the POO, i.e., the separation of powers rule. This capability is not documented elsewhere and has no Fortran API. It's designed for and has been used extensively for testing this software because it provides the ability to test key processing and commands with software rather than manually. If the buffer argument is given and is not the null string, the null terminated string contained in the buffer replaces the input. If the len argument is a potentially valid KeyFcn function index, it's passed to KeyFcn. The window is updated depending on the consequences of these two actions: 0 implies the caret; 1, the input; and 2, the whole window. Finally, if the window's input buffer contains information, i.e., the INFO flag is set, it's placed in the buffer (buf). The return is set to the length of the input returned or to 0 or 1 depending on the CARET flag, i.e., there is input (INPUT) and the window has the keyboard focus (FOCUS). */ case WM_POO_USER: h = -1; if (args->buf && *(args->buf)) { CARET_OFF; lw->flags &= ~(bINPUT|bINFO); O_InputMgr(lw,args->buf,strlen(args->buf)); h = 1; } if (args->len > fNONE) { Msg.time = lw->ptime; h = O_KeyFcn(lw,args->len); } if (h < 0) { if (h == -2) GROWL; } else if (h < 2) O_DrawInput(lw,h); else { O_DrawWindow(lw); ValidateRect(lw->wnd,NULL); } if (args->buf && lw->flags & bINFO) { args->rtn = h = (int)(lw->end - lw->input); *(strncpy(args->buf,lw->input,h) + h) = '\0'; } else args->rtn = (lw->flags & bCARET) ? 1 : 0; break; /* Obviously, this is the place to insert additional functionality with minimal effort. It suffices to define a new WM_POO_... that is less than ...USER, insert the usual API function, and add the code here. */ default: args->rtn = -1; END /* switch on event type.*/ return lw; END /*================================================================ WINDOWING SYSTEM INTERFACE This section includes most of the functions directly dependent on the underlying windowing system, e.g., window creation and drawing and event/message processing. A few auxiliary, pure C functions are interspersed where appropriate. CreateWND creates windows of all subclasses, SetAttribute sets a window attribute, GetFontSize saves the font size, Activate raises and sets focus to a window, EventProc is the table-driven event processor, KeyFcn implements keyboard functions, ButtonFcn implements button (mouse) functions, LineBefore finds the beginning of the previous line, LineAfter finds the beginning of the next line, DrawWindow draws a window, HexDump produces lines for drawing in hex dump format, DrawInput draws input being entered through a window, and InputMgr manages input entered through a window. CreateWND, SetAttribute, and EventProc contain many calls to API functions in the windowing system and, to a great extent, their implementations are prescribed by the windowing system. KeyFcn and ButtonFcn contain only minor dependencies. The drawing functions are relatively independent of the windowing system. LineBefore, LineAfter, HexDump and InputMgr are independent of the system. Most of the principal functions in this section are relatively boring because the windowing systems leave only room for variation on their themes. Window creation requires a sequence of calls to the appropriate API functions, and the only real variation is in their names and calling sequences. Event processing is similar; each event is identified by its type, the window, and parameters; most of the differences among the versions of EventProc stem from the process of deciphering the parameters, e.g., the key that was pressed. The actual event processing is often independent of the system, e.g., KeyFcn and ButtonFcn. The drawing functions are almost portable, but OS/2 provides some relief by placing (0,0) at the lower left corner rather than the upper left. For obvious reasons, I tried to minimize the differences among the systems and pass from system-dependent code to system-independent code as rapidly as possible. Further, for a number of reasons, this software avoids features of the systems that are unique and restricts itself to using mainstream API functions only: ones that have been supported for a long time, will likely continue to be supported, are simple, and are reliable. Most of these have close analogues in all of the systems. This programming strategy generally enhances both portability and robustness and is one that I have followed for many years. DrawWindow is very robust with respect to the data contained in the window struct that controls drawing; it carefully checks the validity of the data needed and corrects it if necessary. This is quite intentional and allows the remaining software considerable freedom when drawing is needed because close suffices. DrawWindow is not sophisticated however. It supports only a bottom-up and a top-down drawing algorithm; both always draw the entire window, stopping only when either the data or window space is exhausted. Detailed information on what was drawn and where it was drawn is saved in the window struct. Both \r\n and \n line termination are supported for text files. DrawInput draws the input that the user is entering through the window. Normally, user input is drawn as the last line, e.g., in a command window, but DrawInput draws it in an input rectangle in explicit reverse video. The user can set the location and size of the input rectangle from a small set of options, e.g., top, middle or bottom. The primary objective is high visibility. Finally, InputMgr is a somewhat complex, multifunctional manager for the input displayed in the window. Although it exchanges no information with DrawInput, they are clearly related. Normally, InputMgr handles additions to the input, e.g., key press events, including function keys that have been assigned a string. But Command uses it to append messages to the user's input and, under some circumstances, usurps its role. WINDOWING TERMINOLOGY: The supported systems use different terms for similar things, and none seems to hold the moral high ground. Because I started with X, I tend to use X terminology. Thus, these windowing systems are driven by events rather than messages, which is the Windows and OS/2 term. The graphics context for a window controls how things are drawn, e.g., characters and lines; it's termed a device context in Windows and a presentation space in OS/2. Carets and cursors are everywhere but neither term is quite suited to the role for which computing appropriated them. Names for events is a fertile field, e.g., an expose event in X is the same thing as a PAINT message in Windows and OS/2. Stacking order and focus are common terms in all systems! ------------------------------------------------------------------ CreateWND (VM file, title, subclass) creates a window with appropriate attributes based on the subclass argument (POLL, ONLKR, or OTHER), associates the VM file with it, and returns the window identifier if successful and NULL otherwise. Failure cannot occur easily. If space for the window struct cannot be obtained, it's highly likely the system is near death anyway. Each window has a name and title. The names of pollsters and onlookers are "Pollster" and "Onlooker", respectively, and their titles are generated by CreateWND. The name and title of an other is parsed from the second argument string that's passed by Open; if they cannot be parsed successfully, they are generated by this function. The window name is used to select initial attribute commands from the memory file and should be unique although it's not required. Each window has an SSN. All windows are top-level windows, the desktop is their parent, and have the standard appearance provided by the windowing system, e.g., a title bar and whatever is normally associated with it. There are no menus associated with the windows aside from those provided by the windowing system, e.g., the system menu. Standard scroll bars are not supported, but a similar mechanism is provided. There is no caret associated with the text displayed in a window; there is no need because it cannot be edited. There is, of course, an input caret, but it's displayed only when the user is entering input. Actual window creation is routine and centers around the window creation function provided by the windowing system. The details vary, but these API functions require roughly the same sort of information. A graphics context is associated with each window; in X, a second GC is used for managing the caret. The basic organization of the X version differs from Windows and OS/2 versions. In X, all relevant information is assembled before creating the window and its graphics context. In Windows and OS/2, information gathering and creation are merged since the available mechanisms encourage modification of an existing object rather than direct creation of the desired object. Thus, in Windows and OS/2, SetAttribute is used to establish the initial attributes and to change them; while it's used only to change attributes in X. */ WND *O_CreateWND (VMF *lf, char *title, long subclass) BEGIN static int xorg = 0, yorg = 0; VM_HNDL hndl; WND *lw; int h; char *s, *t; /* Window Registration Window registration is peculiar to Windows and OS/2 and requires, or at least asks for, data that are associated with the task. In Windows, it's the two HINSTANCEs that the system passes to the POO main program; the POO main program stores these values and tries to forget them. In OS/2, it's the anchor block handle, the ...ABH variables, which are global variables and used in all versions for deciding whether the window thread is running. Registration is a form of initialization required by Windows and OS/2; there is no analogue in X. In theory, PrevI is the previous instance in Windows but NULL in Windows NT. This code ASSUMES, quite reasonably, that PrevI and ThisI are not the same; and, after registering the class, PrevI is set to ThisI. The window class is registered if and only if PrevI and ThisI differ. The class styles VREDRAW and HREDRAW specify that the window be redrawn if its size changes. The OWNDC is used because a context is maintained for each window in order to retain attributes. Here, the window class name and event processor are the only arguments pertinent, and the remainder are provided standard values. Since the background varies, this argument is meaningless here. */ if (PrevI != ThisI) { WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpszClassName = POO_CLASS; wc.lpfnWndProc = O_EventProc; wc.hInstance = ThisI; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hIcon = LoadIcon(NULL,IDI_APPLICATION); wc.hCursor = LoadCursor(NULL,IDC_ARROW); wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; if (! RegisterClass (&wc)) return NULL; PrevI = ThisI; } /* CreateWND has the unique responsibility for allocating space for window structs but links them into the list only if successful. */ VM_ACQUIRE(hndl,LWSize); lw = (WND *)VM_POINTER(hndl); if (! lw) return NULL; lw->memhndl = hndl; /* Window Name, Title, and Flags Initialization For pollsters and onlookers, the window name is the subclass name, and their window titles take the form <application name> { Pollster | Onlooker } <SSN> but there will be no Pollster Pollsters or Onlooker Onlookers. If the memory file is associated with an onlooker when it's created, the title is the application name followed by "Memory File". For others, the title argument is parsed for both the window name and title and should be of the form <window name>:<window title>; The title starts with the first character after the colon if one is found and may be terminated by either a null character or a semicolon (;). Before parsing the title argument, the name is set to Other, and the title to the application name followed by Other and the SSN. If the window name and title are successfully parsed, they replace these default values. If necessary, the window name is truncated; the system may truncate the title. When parsing, blanks are not trimmed. */ s = ApplicationName; lw->SSN = ++SSN; t = lw->input; if (subclass & bPOLL) { (void)strcpy(lw->wndname,Pollster); if (State & sbPOLL) sprintf(t,"Pollster %li",lw->SSN); else sprintf(t,"%s Pollster %li",s,lw->SSN); lw->flags = DefaultPOLL|bTYPE; O_InputMgr(lw,NULL,0); } else if (subclass & bONLKR) { (void)strcpy(lw->wndname,Onlooker); if (lf == MemoryFile) sprintf(t,"%s Memory File",s); else if (State & sbONLKR) sprintf(t,"Onlooker %li",lw->SSN); else sprintf(t,"%s Onlooker %li",s,lw->SSN); lw->flags = DefaultONLKR; } else if (subclass & bOTHER) { (void)strcpy(lw->wndname,Other); sprintf(t,"%s Other %li",s,lw->SSN); if (title && *title) { for (s = title; *s && *s != ':' && *s != ';'; ++s); if (*s == ':') { h = (int)(s - title); if (h >= MAXLENNAME) h = MAXLENNAME - 1; if (h) *(strncpy(lw->wndname,title,h) + h) = '\0'; for (title = ++s; *s && *s != ';'; ++s); } h = (int)(s - title); if (0 < h && h < MAXLENINPUT) *(strncpy(t,title,h) + h) = '\0'; } lw->flags = DefaultOTHER; } else { VM_RELEASE(hndl); return NULL; } /* The size of a window and size of its client area differ because of the decoration provided by the windowing system. The distinction between these two sizes is important in Windows and OS/2, while X deals only with the size of the client area. Any old geometry will do because it cannot be determined until the font is known, the font cannot be established until the GC is obtained, and the GC cannot be obtained without the window! */ lw->ww = lw->wh = CW_USEDEFAULT; lw->px = xorg; lw->py = yorg; lw->fw = GetSystemMetrics(SM_CXFRAME) << 1; lw->fh = GetSystemMetrics(SM_CYCAPTION) + (GetSystemMetrics(SM_CYFRAME) << 1); lw->wnd = CreateWindow( POO_CLASS,t, /* Windows class name and title */ WS_OVERLAPPEDWINDOW, /* Standard style */ lw->px, lw->py, /* X and Y position */ lw->ww, lw->wh, /* Width and height */ (HWND) NULL, /* Parent is the desk top*/ (HMENU) NULL, /* No menu */ ThisI, /* Instance */ (LPVOID) lw); /* CREATESTRUCT (WM_CREATE) */ if (! lw->wnd) { VM_RELEASE(hndl); return NULL; } lw->next = WndList; WndList = lw; /* The API and pointer state components are initialized, the current directory set to the POO current directory, and the I/O direction components are initialized. The current directory must be set before the file association since it may be changed by association for an onlooker. The current directory and file associations must be made before the memory file is scanned because the memory file may change the current directory or direct I/O; output direction does not change the file association but does change the path name and flags of the associated VM file struct. */ lw->apiarg = NULL; lw->ptrfcn = 0; (void)strcpy(lw->cwd,Work->pn); lw->din = lw->dout = NULL; O_AssociateVMF(lw,lf); /* The only remaining task is to get and to configure the graphics context. The MemoryCmd(...,NONE,NULL) function scans the memory file for BACKGROUND, FOREGROUND, FONT, GEOMETRY, MODE, and other commands appropriate for the window and should be viewed as an extension of this function. SetAttribute is called by MemoryCmd if appropriate or here to establish the default if MemoryCmd did not change the pertinent struct components. The processing of the GEOMETRY command must be deferred until the font is known so that the window size may be specified in pixels or in characters and lines; ParseGeometry uses the font size in the window struct. Thus, MemoryCmd returns the geometry string in the extra component rather than trying to process it. The window geometry must be initialized for ParseGeometry, which expects the window struct to contain the current geometry. The default xorg and yorg are updated based on the position of the window after calling ParseGeometry so that the position of the next window is based on the actual position of the last window. SetTextAlign(lw->gc,TA_LEFT|TA_TOP) and SetROP2(lw->gc,R2_COPYPEN) are the defaults in Windows so there is no need to issue them after getting the GC for the window, but it would guard against changes in these defaults. */ lw->bb = lw->fb = NULL; lw->fp = NULL; lw->fn = NULL; lw->gc = GetDC(lw->wnd); SetBkMode(lw->gc,TRANSPARENT); *(lw->extra) = '\0'; O_MemoryCmd(lw,oNONE,NULL); if (! lw->bb) O_SetAttribute(lw,oBACKGROUND,ColorTable[0].cmd); if (! lw->fb) O_SetAttribute(lw,oFOREGROUND,ColorTable[1].cmd); if (! lw->fn) O_SetAttribute(lw,oFONT,FontTable[0].cmd); lw->ww = 72*lw->tw; lw->wh = 20*lw->th; O_ParseGeometry(lw,lw->extra); *(lw->extra) = '\0'; xorg = lw->px + lw->fh; yorg = lw->py + lw->fh; /* The above, of course, has simply been the "Ready, Aim." Fire! This is the only place SysShow is referenced, and it is handled per the apparent Windows' rule. */ SetWindowPos(lw->wnd,HWND_TOP,lw->px,lw->py,lw->ww+lw->fw, lw->wh+lw->fh,SWP_NOACTIVATE|SWP_NOREDRAW|SWP_NOZORDER); ShowWindow(lw->wnd,SysShow); UpdateWindow(lw->wnd); SysShow = SW_SHOW; return lw; END /*---------------------------------------------------------------- SetAttribute (window, command, operand) changes the BACKGROUND, FOREGROUND, FONT, GEOMETRY, or MODE values for the window. The command and operand arguments are produced by ParseCommand, e.g., the operand argument is a multistring, but may be any legitimate values. SetAttribute returns a nonzero value if the attribute is successfully changed and zero otherwise. This function is also used to match miscellaneous operands, e.g., for MEMORY and VMF commands. SetAttribute is a collection of distinct functions that are simply aggregated, i.e., it's a multifunction designed to isolate system dependencies. In X, the initial window attributes are naturally established during the creation process, and this function is used principally to change attributes. In Windows and OS/2, it's used both to set the initial attributes and to change them. In Windows and OS/2, the legal operand values for all attributes are in the appropriate table, and the table is searched for an initial substring, case independent match that is not required to be unique so that table ordering may affect the search. The OS/2 and Windows the color tables are the same; the font tables have many elements in common. In X, the color and font values are not table driven. The color name must be exactly as specified in /usr/X11/.../rgb.txt; and the font, one of the names in /usr/X11/.../fonts.alias or some other value that the X library understands. In X, the color and font names are more or less standard, but these files are not small and replicating them in tables would be impractical. There are usually a couple hundred color names, and hundreds of fonts. In short, this function and the operands that the user can use in these attribute commands are quite system dependent. */ long O_SetAttribute (WND *lw, int cmd, char *opnd) BEGIN WORDTAB *wt; long flags, i, k; int n; char *d, *s, *t; if (! opnd || ! *opnd) return 0; /* BACKGROUND/FOREGROUND Attributes The color table is used to validate color values. Since the first two entries are "default background" and "default foreground", a match to the first entry is adjusted if the operand is a substring of "default". If the color is valid, all appropriate changes in the window struct are made, e.g., the complete color name is moved into the window struct. The primary colors red, green, and blue and their mixtures cyan, purple, and yellow will work for a color display in all of the supported systems. In Windows and OS/2, the color can be preceded by the modifiers 'light', 'bright', or 'dark'; the first two are ignored, while the last leads to the half colors. In X, the dark versions may be provided by names like "DarkRed", etc. In Windows and OS/2, the color name need not be saved in the window struct; but, in X, it must be saved somewhere because the color cannot be used to obtain the name. Thus, the name is saved in all systems. In Windows, brushes for both colors are needed for filling, and a foreground pen is needed for drawing lines. When SelectObject is used to select a new object into a GC, it returns the previous value, e.g., the default initially. Thus, for the pen, the window struct actually holds the default, and actual one is in the GC. */ if (cmd == oBACKGROUND || cmd == oFOREGROUND) BEGIN COLORREF color; HBRUSH newb; HPEN newp; for (flags = 0, wt = ColorModTable; wt->cmd; ++wt) { for (d = wt->cmd, s = opnd; *s && *d; ++d, ++s) if (! isalpha(*s) || *d != tolower(*s)) break; if (! *s) break; } if (wt->cmd) { flags = wt->index; while (*opnd++); } for (wt = ColorTable; wt->cmd; ++wt) { for (d = wt->cmd, s = opnd; *s && *d; ++d, ++s) { if (*d == ' ' && *d == *s) continue; if (! isalpha(*s) || tolower(*d) != tolower(*s)) break; } if (! *s) break; } if (! wt->cmd) return 0; n = (int)(wt - ColorTable); if (! n && cmd == oFOREGROUND && (int)(s - opnd) < 9) ++n; else if ((n > 3 && n < 13) && flags) n += 9; opnd = ColorTable[n].cmd; color = ColorTable[n].index; newb = CreateSolidBrush(color); if (! newb) return 0; if (cmd == oFOREGROUND) { newp = CreatePen(PS_SOLID,1,color); if (! newp) return 0; if (lw->fp) { lw->fp = SelectObject(lw->gc,lw->fp); DeleteObject(lw->fp); } lw->fp = SelectObject(lw->gc,newp); if (lw->fb) DeleteObject(lw->fb); lw->fb = newb; SetTextColor(lw->gc,lw->fg = color); s = lw->fgn; } else { if (lw->bb) DeleteObject(lw->bb); lw->bb = newb; SetBkColor(lw->gc,lw->bg = color); s = lw->bgn; } for (k = 0; k < 31 && *opnd; ++k) *s++ = *opnd++; *s = '\0'; return 1; END /* FONT Attribute The font operands are the font face name, an optional font size of the form <width>{x|X}<height>, and the face modifier. Validation of the first operand, the face name, is based on the table and is a bit complicated because a few face names consist of two words. The algorithm used does not require that the two words be enclosed in quotes so that the face name may consume the first two operands. The ALIAS flag in the table index does the obvious; and, after removing ALIAS, one is left with the actual font index. The width and height of a font are NOT free parameters; only some combinations are supported by the windowing system. Nevertheless, whatever the user specifies is tried. In Windows, the table includes names for the stock fonts, which have fixed sizes so that the name itself suffices and additional operands are ignored. Further, stock fonts should not be deleted. In the window struct, the font type is distinguished by the first character of the font name: blank for a stock font. */ else if (cmd == oFONT) BEGIN HFONT newf; TEXTMETRIC *tm; LOGFONT *lfp; for (n = 0; (d = FontTable[n].cmd) != NULL; ++n) BEGIN for (s = opnd; *s && *d; ++d, ++s) { if (*s == *d) continue; if (! isalpha(*s)) break; if (tolower(*d) != tolower(*s)) break; } if (*s) continue; if (! isalpha(*(s+1))) break; while (*d && *d != ' ') ++d; if (! *d) break; else ++d; for (++s; *s && *d; ++d, ++s) { if (*s == *d) continue; if (! isalpha(*s)) break; if (tolower(*d) != tolower(*s)) break; } if (! *s) break; END if (! d) return 0; /* If the first operand was matched, then s points to the null at the end of the face name so that s+1 is the first character of the next operand, i.e., the font size. The Windows stock fonts are easy because their size is fixed. Otherwise, a logical font struct must be built for the face and size specified. */ if (n < NSF) newf = GetStockObject((int)FontTable[n].index); else BEGIN lfp = (LOGFONT *)(void *)WorkTemp; if (FontTable[n].index & fnALIAS) n = (int)(FontTable[n].index & 255L); lfp->lfPitchAndFamily = (char)(FontTable[n].index & 255L); (void)strcpy(lfp->lfFaceName,FontTable[n].cmd); /* In Windows, a logical font is matched when installed in a GC, and the user's size may not be available. In practice, it appears to work well if only the width is specified, which uses the default height implemented here. If both are specified, the process used here fails frequently, i.e., CreateFontIndirect returns NULL. In most cases, the default height is successful for most widths. In Windows, some fonts cannot be duplicated in that applying the process used here may not reproduce the same font. Specifically, if a font is selected by CreateFontIndirect and then the same face and the font size values from the TEXTMETRIC struct are passed to CreateFontIndirect, it may return a slightly larger size font. In particular, the Helvetica and Times Roman fonts appear to grow. The font size operand should have the form <width>{x|X},height>. The width and height are restricted, but only for sensibility. If not specified, 9x20, the size of the default font, is used. */ if (isdigit(*(s+1))) { for (i = 0, ++s; isdigit(*s); i = 10*i + (*s++ - '0')); if (i < 4 || i > 32) return 0; lfp->lfWidth = (int)i; lfp->lfHeight = (int)(2*i - 1); if (*s == 'x' || *s == 'X') { for (k = 0, ++s; isdigit(*s); k = 10*k + (*s++ - '0')); if (k <= i || k > 96) return 0; lfp->lfHeight = (int)k; } if (*s) return 0; } else { lfp->lfWidth = 9; lfp->lfHeight = 20; } /* This is an undocumented feature: the third operand selects italic or bold. Only the first character is examined so "invalid" would be interpreted as italic, so.... Finally, all the mandatory stuff must be filled in before trying to create the font. */ if (*(s+1) && *(s+2) != 'n') ++s; lfp->lfItalic = 0; if (*s == 'i' || *s == 'I') { lfp->lfItalic = (char)(1); s = "Italic"; } else if (*s == 'b' || *s == 'B') { lfp->lfWeight = FW_BOLD; s = "BOLD"; } else { lfp->lfWeight = FW_REGULAR; s = ""; } lfp->lfEscapement = lfp->lfOrientation = 0; lfp->lfUnderline = lfp->lfStrikeOut = 0; lfp->lfCharSet = ANSI_CHARSET; lfp->lfOutPrecision = OUT_RASTER_PRECIS; lfp->lfClipPrecision = CLIP_DEFAULT_PRECIS; lfp->lfQuality = DEFAULT_QUALITY; newf = CreateFontIndirect(lfp); END if (! newf) return 0; /* I think that went well. The previous font must be selected out of the GC and deleted if the first character of its name is a blank, i.e., it's a logical font. SelectObject puts a new font in a GC and returns the previous one. Thus the first installs the default and returns the font that has been in use, and the second installs the new font and returns the default. In short, the font component in the window structure is NULL or the default font; the actual font handle is in the GC. The previous font is deleted only if the first character of its name is not blank; the first character is a flag: blank for stock fonts, which shouldn't be deleted, and nonblank for logical fonts, which should be deleted. Finally, the window struct must be changed, i.e., the actual font sizes and the font name must be set properly, including the first character of the font name. */ if (lw->fn) { lw->fn = SelectObject(lw->gc,lw->fn); if (*(lw->fnn) != ' ') DeleteObject(lw->fn); } lw->fn = newf; lw->fn = SelectObject(lw->gc,lw->fn); tm = (TEXTMETRIC *)(void *)WorkTemp; if (GetTextMetrics(lw->gc,tm)) { lw->tw = tm->tmAveCharWidth; lw->th = tm->tmHeight; } else { lw->tw = 9; lw->th = 20; } d = lw->fnn; if (n < NSF) { *d++ = ' '; s = ""; } sprintf(d,"%s %ix%i %s",FontTable[n].cmd,lw->tw,lw->th,s); return 1; END /* MODE Attributes Although rather cryptic, the MODE command allows the user to set and reset various flags that control how the window is drawn. The operand should be of the form [+|-]<flag word>{+|-}<flag word>... where the flag words are in the mode table and the signs turn the flag on or off. After the operand string has been processed, a few special rules are applied: If both the TOP and BOTTOM flags are set, the one set in the current flags is unset; center is obtained by setting neither. Nevertheless, both can be set, and top takes precedence. If BAR is set, TIC is set, but not conversely. The flags are changed only if the complete operand is valid. */ else if (cmd == oMODE) BEGIN for (flags = lw->flags; *opnd; opnd = t) { if (*opnd == '-') { n = 0; ++opnd; } else { n = 1; if (*opnd == '+') ++opnd; } for (t = opnd; *t; ++t) if (*t == '-' || *t == '+') break; k = (int)(t - opnd); if (! k) continue; for (wt = ModeTable; wt->cmd; ++wt) { for (i = k, d = wt->cmd, s = opnd; i && *d; ++s, --i) if (*d++ != ((isupper(*s)) ? tolower(*s) : *s)) break; if (! i) break; } if (! wt->cmd) return 0; /* if no match in table.*/ if (n) flags |= wt->index; else flags &= ~(wt->index); } if (!(flags ^ (bINT|bINB))) flags ^= (lw->flags) & (bINT|bINB); if (flags & bBAR) flags |= bTIC; lw->flags = flags; return 1; END /* Geometry Syntax Validation CreateWND->MemoryCmd->SetAttribute is the only path that will get here, and the geometry string will have to be parsed again after the font has been set. Without its content, this code, extracted from ParseGeometry, clearly shows the parse is very permissive. By validating geometries before accepting them, erroneous ones in the memory file won't take precedence. */ else if (cmd == oGEOMETRY) BEGIN s = opnd; if (*s == '=') ++s; if (isdigit(*s)) while (isdigit(*s)) ++s; else if (*s == 'm' || *s == 'M') ++s; if (*s == 'x' || *s == 'X') ++s; if (isdigit(*s)) while (isdigit(*s)) ++s; else if (*s == 'm' || *s == 'M') ++s; if (*s == '+' || *s == '-') { ++s; if (isdigit(*s)) while (isdigit(*s)) ++s; else if (*s == 'c' || *s == 'C') ++s; if (*s == '+' || *s == '-') ++s; if (isdigit(*s)) while (isdigit(*s)) ++s; else if (*s == 'c' || *s == 'C') ++s; } else if (*s == 'c' || *s == 'C') ++s; return (*s) ? 0 : 1; END /* The miscellaneous table contains a collection of words that may be used as command operands. The strings are all lower case, and the the index values nonzero. The miscellaneous operand table is used for MEMORY and VMF commands, and SetAttribute simply returns the index from the table. */ else BEGIN for (wt = MiscTable; wt->cmd; ++wt) { for (d = wt->cmd, s = opnd; *s && *d; ++d, ++s) if (*d != ((isupper(*s)) ? tolower(*s) : *s)) break; if (! *s) break; } return (wt->cmd) ? wt->index : 0; END END /*---------------------------------------------------------------- GetFontSize (window) sets the font size components in the window struct. This function is called only if the text size is required and is less than 1, which should never occur. For example, the width is tested before being used as a divisor. In theory, of course, this function isn't called because the software is designed correctly. So far, so good. Defaults are provided because the caller cannot handle failure. */ int O_GetFontSize (WND *lw) BEGIN TEXTMETRIC fm; if (GetTextMetrics(lw->gc,&fm)) { lw->tw = fm.tmAveCharWidth; lw->th = fm.tmHeight; } else { lw->tw = 9; lw->th = 20; } GROWL; return lw->th; END /*---------------------------------------------------------------- Activate (window) returns 1 if the window has the focus and 0 otherwise, but only after activating the window. If the window already has the focus, the input is redrawn if it exists. If not, e.g., the window is iconified, the window is restored, which posts appropriate expose or focus events so that it's not necessary to redraw the window explicitly. Activate's purpose is to hide system-dependent code in Get and KeyFcn. */ int O_Activate (WND *lw) BEGIN if (lw->flags & bFOCUS) { UPD_INPUT; return 1; } if (IsIconic(lw->wnd)) ShowWindow(lw->wnd,SW_RESTORE); else SetActiveWindow(lw->wnd); return 0; END /*---------------------------------------------------------------- EventProc processes all window events. In most respects, the code here is dictated by the system and common sense, e.g., if the system sends an expose event, the window must be redrawn appropriately. Common sense dictates the processing of most key and button events, e.g., most keys have a symbol inscribed on them. Most events are defined narrowly and only a small number must be processed. EventProc is a collection of relatively independent chunks of code and is coded in the traditional manner: one large switch on the event type. It has little content and may seem anticlimactic. EventProc consists of a short prologue, the event switch, and a short epilogue. The prologue code manages the window list; it locates the window struct for the event window and maintains the list as an MRU cache with respect to events. Event processing is designed to get past system-dependent code as quickly as possible and calls on other functions that contain few system dependencies to actually perform the processing required. The epilogue performs whatever drawing may be required to finish processing the event. EventProc follows a minimalist approach: if there is no specific reason to process an event, it's ignored or default processing is used. This minimalist strategy has also been followed in terms of the features available in these systems, many of which are simply ignored. This strategy has distinct advantages: dependence on the windowing system is reduced, and the capabilities required in the windowing system can be restricted to those that are essential and, therefore, unlikely to be modified by the vendor. Thus the windows provided by this software may not have the usual look and feel of X, Windows, or OS/2. Rather, they have those features and characteristics that are common to these systems and that can be implemented in each without using any capabilities peculiar to any. The minimalist strategy usually provides robust software because it reduces stress and is also followed in the actual coding to reduce stress on the C compiler. Keyboard and Button Events Both key and button events are quadrupled based on the states of the Shift and Ctrl keys, but the Alt key is always ignored. For example, the standard two button mouse provides an eight button interface for the user willing to use the Shift and Ctrl keys, and the twelve function keys expand to forty-eight distinct functions, which should be enough for anyone. All key and button events are table driven; the event processor extracts the function associated with an event from the tables and then calls either KeyFcn or ButtonFcn. Both KeyFcn and ButtonFcn are quite system independent and return an integer to indicate the drawing that should be performed, if any. The following tables are used: ExtKeyFcn for the extended keys, CtrlKeyFcn for Ctrl + an alphabetic key, FcnKeyStr for function keys assigned strings, FcnKeyFcn for the function keys, and ButtonFcn for the mouse buttons. The potential functions assigned in these tables are the defined constants with names of the form fX... for keys and pX... for the buttons. These tables are static, but the user can assign strings to the function keys, which take precedence over the table. Most table entries are based on common practice, past or present. The windowing system may usurp some keys and/or key combinations. From the user's perspective, the function key mapping is Fn to n, Shift+Fn to n+12, Ctrl+Fn to n+24, and Shift+Ctrl+Fn to n+36. The FcnKeyFcn table, however, is arranged in the same manner as the other tables so that this code uses a different mapping, which is fortunately invisible to the user. F1...F48 are the commands for managing string assignments to the function keys. Traditionally, placing the pointer in a window and pressing the left or only mouse button raises the window (places it on top of all other windows) and provides it the focus. In X, the window manager determines the focus mechanism, and different WMs use different mechanisms for awarding focus. Some of the popular WMs award focus based on pointer location only, which clearly requires that the pointer be in the window in which the user intends to enter input. The left button is oversubscribed as usual. Standard scroll bars are not provided, but scrolling is supported in two ways. First, there is a function that allows the user to drag the text in the window to a different location, thus exposing other parts of the text. This is handy for small movements and replaces the usual text selection mechanism, which isn't really needed because there are no editing capabilities. Second, there is a scrolling function that can be used to position the file according to the rule (pointer position) file position = (file size)* ------------------ (window height) by either placing the pointer where the scroll bar should be or using the Shift key. In general, file position refers to the line displayed at the top of a window. The FOCUS, INPUT, and CARET flags play a significant role in event processing. In particular, some key functions depend on INPUT, and some button functions depend on CARET. For example, functions associated with the usual cursor keys are applied to the caret if INPUT and to the window otherwise. Hopefully, the keys provide the obvious function, and the user is unaware of the machinations here. See DrawInput and InputMgr. */ LRESULT CALLBACK O_EventProc (HWND wnd, UINT msgid, WPARAM wp, LPARAM lp) BEGIN WND *lw, *plw; int k, h; char *s, c; /* Prologue The window list is maintained as an MRU cache and some functions assume that the event window is first in the list so that changes in this regard may produce unpredictable consequences. Since even simple user actions can cause hundreds of events, it's likely that the MRU cache does save time. In Windows and OS/2 PM, this could be avoided by using the extra data that can be associated with a window; I elected to do it the same way in all systems. If the search fails, somebody has a problem, i.e., either the POO software failed or the windowing system called the wrong event processor. Neither seems likely, but this case must be handled because this event processor is not designed for this situation. */ if (msgid != WM_CREATE) BEGIN for (plw = NULL, lw = WndList; lw; plw = lw, lw = lw->next) if (lw->wnd == wnd) break; if (! lw) return DefWindowProc(wnd,msgid,wp,lp); if (plw) { plw->next = lw->next; lw->next = WndList; WndList = lw; } END /* In Windows, EventProc is not called on to handle API events, which are posted to the window thread and will have a NULL window value. See WindThread; API events don't occur when single threaded. The epilogue code is based on the value of the variable 'h', which should be in [-2,2]. Events that do not require drawing may either return directly or set h to -1. */ switch (msgid) BEGIN /* Keyboard Messages In Windows, key events come in three flavors: DOWN, CHAR, and UP. This code processes only DOWNs for the extended keys, e.g., the arrow keys and the function keys; subsequent key events for these keys are ignored. The graphics keys and Ctrl+alphabetic are ignored here and processed when the CHAR event comes along. The UP messages are uniformly ignored. The DEADCHAR messages that are generated for diacritics are ignored. KEYDOWN Processing If the FOCUS flag is off, there should be no keyboard events for this window, but ignoring this possibility could be harmful. In this case, it's wise to follow the windowing system's lead so the FOCUS flag is set. It's never occurred. The extended keys are quadrupled based on the state of the Shift and Ctrl keys. The virtual key codes, integers that indicate what key was pressed, are assumed sequential only for the function keys; they are generally in sequential order, but I have not seen any Microsoft guarantees in this context. The virtual key code in conjunction with the shift states provide the index needed to get the associated function index from the table, i.e., the argument to KeyFcn. The value returned by KeyFcn indicates what, if any, drawing is necessary, which is done in the epilogue. The table is ignored for a function key if it's been assigned a character string, which takes precedence over the table. The function key array is maintained by MemoryKey and contains the displacements into the memory file of the function key command; ParseCommand is used to extract the assigned string, which is then passed to InputMgr. In short, the effect is the same as if you type the assigned string with one caveat. If a function key is assigned a string that ends with either of the two character sequences "\n" or "^M", special processing is applied. First, these two characters are trimmed before calling InputMgr. Second, /n generates an Enter so that the input is processed in context, while ^M generates a Ctrl+Shift+Enter so that your input is processed as a command. The windowing system may overlay some idiosyncrasies. Windows appears to emit a noise when Ctrl+Enter is pressed even though it appears to generate the appropriate events. In Windows, the Pause key uses VK_PAUSE if Ctrl is not down and VK_CANCEL if it is. Thus these two virtual key codes use the same group of table entries, and all four combinations can be assigned a function. I have not found Print Screen in Windows; if you find it, add a table entry. */ case WM_KEYDOWN: if (!(lw->flags & bFOCUS)) lw->flags |= bFOCUS; h = (GetKeyState(VK_SHIFT)<0) + 2*(GetKeyState(VK_CONTROL)<0); k = LOWORD(wp); if (k == VK_UP) ; else if (k == VK_DOWN) h += 4; else if (k == VK_LEFT) h += 8; else if (k == VK_RIGHT) h += 12; else if (k == VK_PRIOR) h += 16; else if (k == VK_NEXT) h += 20; else if (k == VK_DELETE) h += 24; else if (k == VK_BACK) h += 28; else if (k == VK_RETURN) h += 32; else if (k == VK_HOME) h += 36; else if (k == VK_END) h += 40; else if (k == VK_INSERT) h += 44; else if (k == VK_TAB) h += 48; else if (k == VK_PAUSE) h += 52; else if (k == VK_CANCEL) h += 52; else if (VK_F1 <= k && k <= VK_F12) { k = ((k - VK_F1) << 2) + h; if (FcnKeyStr[k] < 0) h = O_KeyFcn(lw,FcnKeyFcn[k]); else { char *b; s = b = MemoryFile->base + FcnKeyStr[k]; while (*s != '\n') ++s; O_ParseCommand(b,s,lw->tmp); s = lw->tmp; while (*s) ++s; if (s > lw->tmp + 1 && ( (*(s-1) == 'n' && *(s-2) == '\\') || (*(s-1) == 'M' && *(s-2) == '^') )) s -= 2; if (s != lw->tmp) O_InputMgr(lw,lw->tmp,(int)(s-lw->tmp)); if (*s) h = O_KeyFcn(lw,(*s == '^') ? fCMD : fENTER); else h = 1; } break; } else { h = -1; break; } h = O_KeyFcn(lw,ExtKeyFcn[h]); break; /* CHAR Processing Considering all the code invested to process the few keys above, the code below for processing CHAR events for the standard keys is anticlimactic. The twenty-six Ctrl+alphabetic and Esc keys are processed here exactly as the extended keys above, i.e., a table and KeyFcn. Characters that pass the isprint test are passed to InputMgr, which manages the input buffer and INPUT flag. All other CHAR messages are ignored. In Windows, Enter, Backspace, and Tab generate CHARs for Ctrl+M, Ctrl+H, and Ctrl+I, respectively. In this respect, the extended and control key function tables must be coordinated, i.e., one has to be IGNOREd to avoid the two-for-one sale. */ case WM_CHAR: c = (char) wp; if (0 < c && c <= 27) h = O_KeyFcn(lw,(int)CtrlKeyFcn[c-1]); else if (isprint(c)) { h = 1; O_InputMgr(lw,&c,1); } else h = -1; break; /* KEYUP and DEADCHAR Processing Neither of these messages is processed. Although rare, a KEYUP that should be or has been sent to another window, the window that got the corresponding KEYDOWN, may occur so that processing KEYUPs can be dangerous. For example, if a window for another application is over a POO window and the user hits a key that terminates the application on the KEYDOWN, then the system, in sheer desperation, may saddle EventProc with the KEYUP. Thus, acting on KEYUP messages could be hazardous, e.g., the poor user may get two actions for the price of one and may not appreciate the windfall. DEADCHAR messages are used to add a diacritic to letters, e.g., acute and grave accents and umlauts. Specifically, the DEADCHAR provides a diacritic that applies to the next CHAR. I don't have a non-US keyboard, so couldn't test the appropriate code even if I was familiar with the common keystrokes used so that ignoring them seemed prudent. */ case WM_KEYUP: return 0; case WM_DEADCHAR: h = -1; break; /* SYSCOMMAND and COMMAND Processing These messages are processed by the default processor. They are fielded simply because you may wish to do something. In Win32s, Alt key combinations appear here with a wp of 0xF100 and the ASCII character code in lp. Function key 10 appears as 0xF100 and 0; unfortunately, this same combination occurs when a window is restored from its icon in Windows 3.1 (not Windows 95). Thus, F10 cannot be grabbed back from the clutches of Windows? Some pointer actions show up here: the System Menu, 0xF093; Max Box, 0xF030; Min Box, 0xF020; Title Bar, 0xF012; and the various parts of the sizing border as 0xF001...0xF008. */ case WM_SYSCOMMAND: return DefWindowProc(wnd,msgid,wp,lp); case WM_COMMAND: return DefWindowProc(wnd,msgid,wp,lp); /* Button Processing Analogous to keys, the buttons are quadrupled based on the states of the Shift and Ctrl keys, a table is accessed for the function, and ButtonFcn is called to execute the function. ButtonFcn takes both explicit and implicit arguments; in particular, the function must be in the pointer component of the window struct. ButtonFcn returns a value appropriate for the epilogue. For point-and-click functions, ButtonFcn sets the pointer function to zero. For drag functions, it's left unchanged (nonzero) and is set to zero when the drag terminates, i.e., the button is released or the pointer leaves the window. See references to the DRAG_OFF macro. Since the function is established by the button press, the Shift and Ctrl keys can be released while dragging; on the other hand, pressing another button while dragging changes the function and terminates the current, possibly drag, function. In Windows and OS/2, capturing the pointer for drag type functions appears to provide the only reliable mechanism for detecting when the pointer is moved out of the window. This is implemented here so that ButtonFcn need not consider this issue and so that adding functions with this property is straightforward. Since pointer capture must be released explicitly, many of the events fielded in this event processor cancel the capture. In many respects, it is quite analogous to the handling of the caret. */ case WM_LBUTTONDOWN: DRAG_OFF; lw->ptrfcn = ButtonFcn[(GetKeyState(VK_SHIFT) < 0) + 2*(GetKeyState(VK_CONTROL) < 0)]; h = O_ButtonFcn(lw,1,LOWORD(lp),HIWORD(lp)); if (lw->ptrfcn) SetCapture(lw->wnd); break; case WM_RBUTTONDOWN: DRAG_OFF; lw->ptrfcn = ButtonFcn[4 + (GetKeyState(VK_SHIFT) < 0) + 2*(GetKeyState(VK_CONTROL) < 0)]; h = O_ButtonFcn(lw,1,LOWORD(lp),HIWORD(lp)); if (lw->ptrfcn) SetCapture(lw->wnd); break; /* Button up messages cancel pointer capture and terminate any drag function; otherwise, they are ignored. */ case WM_LBUTTONUP: case WM_RBUTTONUP: DRAG_OFF; return 0; /* If a mouse button is not down, the drag function is terminated but this should never happen since the UP should have occurred. Most of these messages occur with no button down and no drag function and are effectively ignored. If a drag is in process but the pointer has strayed outside the window, the drag is terminated; otherwise, the drag function is in the window struct and ButtonFcn will implement the drag given the new pointer position. */ case WM_MOUSEMOVE: if (!(wp & (MK_LBUTTON|MK_RBUTTON))) lw->ptrfcn = 0; if (! lw->ptrfcn) { h = -1; break; } h = LOWORD(lp); if (h < 0 || h > lw->ww) lw->ptrfcn = 0; k = HIWORD(lp); if (k < 0 || k > lw->wh) lw->ptrfcn = 0; if (lw->ptrfcn) h = O_ButtonFcn(lw,0,h,k); else { ReleaseCapture(); h = -1; } break; /* Drawing Messages: ERASEBACKGROUND and PAINT Erasing the window is taken care of by DrawWindow and is ignored in its event form. Expose (PAINT in OS/2 and Windows) events are rather fundamental and are generated when the contents of the window become invalid, which may occur either explicitly when this code invalidates the window or implicitly when the windowing system detects that some part of the window is invalid, e.g., a change in stacking order. The event includes a list (one or more) of rectangles that are invalid, but this software uses a primitive strategy: to draw or not to draw the whole window, which is simple and robust but a bit wasteful perhaps. */ case WM_ERASEBKGND: return 0; case WM_PAINT: h = 2; break; /* Window Management Events The CREATE message is sent by the window creation API function before it returns to its caller, namely CreateWND. Thus, there is no point in searching the window list for the event window, and lw will always be NULL. The window struct pointer is passed in the CREATESTRUCT, but there is nothing to do here because everything is done in CreateWND. */ case WM_CREATE: lw = (WND *)(((CREATESTRUCT *)lp)->lpCreateParams); return 0; /* In Windows, the CLOSE and DESTROY messages are related in that the usual process would be to call DestroyWindow on the CLOSE, and it calls with a DESTROY message before returning. Thus, one has a choice of loading one or the other or both with the housekeeping. Actions related to this software are done on the CLOSE, while ones related to the windowing system are done on the DESTROY. In Windows, CLOSE does not appear to require that the window be closed. If the application set the CLOSE flag, the user is not allowed to close the window. CLOSE must handle anything that may be outstanding, i.e., an API call, the caret, a drag, and I/O direction. If there are no more windows, the application is left suspended on the API semaphore. For pollsters and others, output direction must be completed before the VM file is destroyed. For onlookers, the current file position is stored; it may be saved when the file is destroyed. DestroyVMF is not a dumb function; it may not actually destroy the VM file if it's being used otherwise. DESTROY does system-dependent housekeeping. Depending on which book/manual you believe, resources must be explicitly deleted or released, e.g., brushes, pen, font, and graphics context, so that Windows doesn't end up with a bunch of zombie objects. In Windows, when an object is selected into a GC, the previous object of like kind is returned. Thus, in fact, the window struct usually holds the previous object; by selecting it into the GC, the object that must be destroyed is obtained. The window struct must be released, and the window list downdated. Finally, if there are no more windows, a QUIT message is posted to terminate the application. */ case WM_CLOSE: if (lw->flags & bCLOSE) return 0; if (lw->next && lw->apiarg) { O_APIEvent(-2); } CARET_OFF; DRAG_OFF; if (lw->din) { VMF *lf; lf = lw->din; lw->din = NULL; O_DestroyVMF(lf); } if (lw->dout) { VMF *lf; lf = lw->dout; if (lf->flags & bUPDATE) { if (!(lw->flags & bONLKR)) { O_StoreVMF(lf,0,NULL); lf->flags &= ~bUPDATE; }} else if (lw->flags & bONLKR) if (O_Valid(lw->top,lf->base,lf->eof)) lf->posn = lw->top - lf->base; lw->dout = NULL; O_DestroyVMF(lf); } DestroyWindow(lw->wnd); return 0; case WM_DESTROY: DeleteObject(lw->bb); DeleteObject(lw->fb); if (lw->fp) DeleteObject(SelectObject(lw->gc,lw->fp)); if (lw->fn) { lw->fn = SelectObject(lw->gc,lw->fn); if (*(lw->fnn) != ' ') DeleteObject(lw->fn); } ReleaseDC(lw->wnd,lw->gc); WndList = lw->next; VM_RELEASE(lw->memhndl); return 0; /* In Windows, there are two focus messages: SETFOCUS when a window gets it and KILLFOCUS when it loses it. In OS/2, there are many focus messages. In Windows and OS/2, there is only one caret. By convention, when a window gets the focus, the caret should be created if appropriate; and, when a window loses the focus, the caret should be destroyed. Regardless, the FOCUS and CARET flags and the caret must be appropriately managed. */ case WM_SETFOCUS: lw->flags |= bFOCUS; if (lw->flags & bINPUT) UPD_INPUT; DRAG_OFF; return 0; case WM_KILLFOCUS: CARET_OFF; DRAG_OFF; lw->flags &= ~bFOCUS; return 0; /* A SIZE event occurs when the size of the window changes and should be attended with a number of events, e.g., PAINTs. When a window changes size, the details for redrawing it depend on its subclass. Pollsters and onlookers are redrawn with the current top line at the top of the window, while others are redrawn with the current bottom line at the bottom of the window. */ case WM_SIZE: lw->ww = LOWORD(lp); lw->wh = HIWORD(lp); if (lw->flags & bOTHER) lw->ybot = -1; else { lw->ytop = 0; lw->bot = NULL; } /* SIZE should fall through to MOVE */ case WM_MOVE: DRAG_OFF; return 0; /* "All those not here, please raise your hands." */ default: return DefWindowProc(wnd,msgid,wp,lp); END /* Epilogue This epilogue is intended for keyboard and pointer events, which are expected to properly set the drawing variable h, normally the return from KeyFcn or ButtonFcn. The return indicates that the event was processed. */ if (h < 0) { if (h == -2) GROWL; } else if (h < 2) O_DrawInput(lw,h); else { O_DrawWindow(lw); ValidateRect(wnd,NULL); } return 0; END /*---------------------------------------------------------------- KeyFcn (window, fXXXX) performs the following, possibly context dependent, functions NONE make a noise, UP move window up one line, DOWN move window down one line, PGUP move window up one window's worth, PGDN move window down one window's worth, LEFT move window left one character OR move caret left one character, RIGHT move window right one character OR move caret right one character, DEL delete caret character, BKSP backspace (and delete), INS toggle insert/overstrike mode, HOME move window to first line OR move caret to first character, END move window to last line OR move caret to last character, WLEFT move window left one character, WRIGHT move window right one character, WHOME move window to first line, WEND move window to last line, HOME0 move window to first line + COL0, END0 move window to last line + COL0, COL0 reset horizontal shift, CMD process input as a command, ENTER process input in context, EOF end of file signal, TAB Tab key processing (currently ignored), ESCAPE Esc key processing, CPYINP copy input to work buffer, CUTINP copy input to work buffer and delete it, INSINP insert work buffer as input, CPYVMF copy associated VM file to work VM file, INSVMF insert work VM file (input direction), DELALL delete input line but stay in input mode, DELBOL delete beginning of line (before the caret), DELEOL delete end of line (caret and after), APPB block API calls, APPR release API calls, QUIT terminate application, and IGNORE precisely, and returns -2 make a noise, -1 no drawing and no noise, 0 redraw the caret, 1 redraw the input, and 2 redraw the window, which the caller should implement. Key presses are implemented by referencing the appropriate table for the corresponding function and then calling KeyFcn; changing the table suffices to change the function provided by the key. Some of these functions depend on whether input is being entered through the window, the INPUT flag. Because KeyFcn was designed to process key events, the window should have the focus so that the INPUT and CARET flags should be equivalent. If called under other circumstances, INPUT may be on and CARET off. In Windows and OS/2, the event struct is a global variable and is referenced here to obtain the time of the event; DrawWindow stores the time of the last window update in the window struct. Functions that move the window compare these two times to avoid the dreaded run-on that occurs in some applications when a frustrated or lead fingered user presses a typamatic key. In short, a key event that would move the window and that occurred before the last redraw is ignored. This strategy cannot be used in X because the event time is from the X server and is not comparable to the time available to this software. */ int O_KeyFcn (WND *lw, int arg) BEGIN VMF *lf; int cmd; char *s, *t; switch (arg) BEGIN /* UP, DOWN, PGUP, and PGDN move the window relative to the (fixed) text, which occurs immediately since there is no caret associated with the text. These functions are ignored when they cannot be performed, i.e., at the beginning or end of a file. They do NOT depend on the INPUT flag. */ case fUP: if (Msg.time < lw->ptime || ! lw->dout) return -1; if ((s = O_LineBefore(lw,lw->top)) != lw->top) { lw->top = s; lw->ytop = 0; } else if (lw->top != lw->bot) ++(lw->ytop); else break; lw->bot = NULL; return 2; case fDOWN: if (Msg.time < lw->ptime || ! lw->dout) return -1; if ((s = O_LineAfter(lw,lw->bot)) != lw->bot) { lw->bot = s; lw->ybot = -1; } else if (lw->top != lw->bot) --(lw->ybot); else break; return 2; case fPGUP: if (Msg.time < lw->ptime || ! lw->dout) return -1; s = O_LineBefore(lw,lw->top); if (s == lw->top) break; lw->bot = s; lw->ybot = -1; return 2; case fPGDN: if (Msg.time < lw->ptime || ! lw->dout) return -1; s = O_LineAfter(lw,lw->bot); if (s == lw->bot) break; lw->top = s; lw->ytop = 0; lw->bot = NULL; return 2; /* LEFT, RIGHT, HOME, and END are context dependent. If INPUT is off, they move the window over the text; otherwise, they move the input caret. HOME and END do not reset the shift value, i.e., the horizontal position of the window over the text. WLEFT, WRIGHT, WHOME, and WEND move the window over the text but, unlike the previous functions, do not depend on the INPUT flag. The HOME0 and END0 functions move the window to the beginning or end of the VM file and reset the horizontal position. These two functions do not depend on the INPUT flag. COL0 resets the horizontal position of the window over the text. */ case fLEFT: if (lw->flags & bINPUT) { if (lw->cur > lw->input) { --(lw->cur); return 0; } else return -1; } /* else fall through to WLEFT */ case fWLEFT: if (Msg.time < lw->ptime || ! lw->dout) return -1; --(lw->shft); return 2; case fRIGHT: if (lw->flags & bINPUT) { if (lw->cur < lw->end) { ++(lw->cur); return 0; } else return -1; } /* else fall through to WRIGHT */ case fWRIGHT: if (Msg.time < lw->ptime || ! lw->dout) return -1; ++(lw->shft); return 2; case fHOME: if (lw->flags & bINPUT) { lw->fcur = lw->cur = lw->input; return 1; } /* else fall through to HOME0/WHOME */ case fHOME0: case fWHOME: if (Msg.time < lw->ptime || ! lw->dout) return -1; lw->top = lw->base; lw->ytop = 0; lw->bot = NULL; if (arg == fHOME0) lw->shft = 0; return 2; case fEND: if (lw->flags & bINPUT) { lw->cur = lw->end; return 0; } /* else fall through to END0/WEND */ case fEND0: case fWEND: if (Msg.time < lw->ptime || ! lw->dout) return -1; lw->bot = lw->eof - 1; lw->ybot = -1; if (arg == fEND0) lw->shft = 0; return 2; case fCOL0: lw->shft = 0; return 2; case fDUMP: lw->flags ^= bHEX; lw->bot = NULL; return 2; /* DEL deletes the caret character, while BKSP moves the caret left one character and then deletes it. These functions are ignored if they cannot be executed. If executed, INFO is set off because the message has been modified. */ case fDEL: if (lw->flags & bINPUT && lw->cur < lw->end) { for (s = lw->cur; s < lw->end; ++s) *s = *(s+1); --(lw->end); lw->flags &= ~bINFO; return 1; } break; case fBKSP: if (lw->flags & bINPUT && lw->cur > lw->input) { for (s = lw->cur; s < lw->end; ++s) *(s-1) = *s; --(lw->cur); --(lw->end); lw->flags &= ~bINFO; return 1; } break; /* INS toggles between insert and overstrike modes regardless of the circumstances. In insert mode, the caret is a thick line just before the caret character; in overstrike, a block covering it. Aside from the change in the caret, there is no indication of the mode, i.e., no screen space is sacrificed for INSERT or OVERSTRIKE. If INFO is set, the caret is a thin line and is mode independent. */ case fINS: lw->flags ^= bINSERT; return (lw->flags & bCARET) ? 0 : -1; /* The CMD and ENTER functions initiate processing of the input that has been entered through the window. CMD processes the input as a command if the INPUT flag is on and is ignored otherwise. It's used to implement Ctrl+Shift+Enter and function keys assigned a string ending with "^M". ParseCommand is called to parse the input. If it fails, a return less than NONE, the cryptic error message in the operand string buffer is appended to the input. Otherwise, the command is executed by Command, which returns a value consistent with KeyFcn. ENTER processing is context dependent and takes into account the INPUT, INFO, TYPE, and ENTER flags and, for others, the API event component, which is not NULL if and only if the application is soliciting input through the window. This component is always NULL for pollsters and onlookers. As a special case, if INPUT is off, the first window in the window list (MRU order) that is soliciting input for the application is is located. If one exists, it's raised to the top (stacking order) and given the focus. Thus, the user can find the window that is soliciting input. If INFO is on, the message is deleted. But, if the application is soliciting input, the window is initialized for input. If TYPE is on, it's processed by Annotate, which takes care of everything and returns a value consistent with KeyFcn. If neither INFO nor TYPE are on, the window's subclass determines the processing. For pollsters and onlookers, all such input is a command; for others, it's input for the application so that it's appended to the extra buffer and, if the window is soliciting, the event must be brought to the attention of the API function. */ case fCMD: if (lw->flags & bINPUT) if (lw->flags & bINFO) { lw->flags ^= (bINFO|bINPUT); if (lw->apiarg || lw->flags & bTYPE) O_InputMgr(lw,NULL,0); return 2; } else { cmd = (int)O_ParseCommand(lw->input,lw->end,lw->tmp); if (cmd < oNONE) { O_InputMgr(lw,lw->tmp,-1); return 1; } return O_Command(lw,cmd,lw->tmp); } break; case fENTER: if (lw->flags & bINPUT) BEGIN if (lw->flags & bINFO) { lw->flags ^= (bINFO|bINPUT); if (lw->apiarg || lw->flags & bTYPE) O_InputMgr(lw,NULL,0); return 2; } if (lw->flags & bTYPE) { if (! O_Annotate(lw,oSTORE,NULL,NULL)) GROWL; return 2; } if (lw->flags & bOTHER) { *(lw->end) = '\0'; s = lw->input; t = lw->extra; while (*t++); --t; cmd = (int)(t - lw->extra) + (int)(lw->end - s) + 2; if (cmd >= MAXLENXTRA) return -2; while (*s) *t++ = *s++; *t++ = '\n'; *t = '\0'; lw->flags ^= bINPUT; if (lw->apiarg) { O_APIEvent(0); } return 2; } else { cmd = (int)O_ParseCommand(lw->input,lw->end,lw->tmp); if (cmd < oNONE) { O_InputMgr(lw,lw->tmp,-1); return 1; } return O_Command(lw,cmd,lw->tmp); } END else BEGIN for (lw = WndList; lw; lw = lw->next) if (lw->apiarg) { if (!(lw->flags & bINPUT)) O_InputMgr(lw,NULL,0); O_Activate(lw); break; } END break; /* EOF is designed to terminate input processing regardless of how it was initiated, i.e., an NB command (TYPE flag) or solicited by the application. As in similar situations, the window must be redrawn to eliminate the input rectangle, which is always a casualty. */ case fEOF: if (lw->flags & bOTHER) { if (*(t = lw->extra)) { while (*t++); if ((int)(t - lw->extra) >= MAXLENXTRA - 2) return -2; --t;} *t++ = 26; *t = '\0'; if (lw->apiarg) { O_APIEvent(0); }} else if (lw->flags & bPOLL && lw->flags & bTYPE) PostMessage(lw->wnd,WM_CLOSE,0,0); if (!(lw->flags & bCARET)) break; CARET_OFF; lw->flags &= ~(bTYPE|bINPUT|bINFO); return 2; /* DELIN is an unusual function (Ctrl+Delete) and is ignored except in others that have typeahead input available. It moves the last line of typeahead input in the extra buffer to the input and then deletes it from the typeahead. How the input is affected depends on the caret location and the insert/overstrike mode. It's handy for deleting erroneous typeahead, e.g., if the user presses Enter after entering a command in an other. */ case fDELIN: if (!(lw->flags & bOTHER) || ! *(t = lw->extra)) break; while (*t) ++t; s = --t; while (s != lw->extra && *(s-1) != 26 && *(s-1) != '\n') --s; if (*s == 26) O_InputMgr(lw,"<EOF>",5); else if (t == s) O_InputMgr(lw,"<NULL>",6); else O_InputMgr(lw,s,(int)(t-s)); *s = '\0'; return 1; /* ESCAPE is a context dependent function intended for processing an Esc key event and could be described as a literal implementation of the escape key. If input is showing, it is eliminated without fanfare; if input is not showing, the window is eliminated! Thus, for example, pressing Esc often enough terminates the application when the last window disappears. */ case fESCAPE: if (lw->flags & bINPUT) { CARET_OFF; lw->flags &= ~(bTYPE|bINPUT|bINFO); return 2; } PostMessage(lw->wnd,WM_CLOSE,0,0); break; /* The CPYINP, CUTINP, and INSINP functions implement a basic cut and paste capability for input through the window. The copy and cut operations are ignored unless the INPUT flag is set. The copy function does not require any redrawing, but the cut and paste operations require that the window or input be redrawn, respectively. The paste function takes into account the caret and insert mode. */ case fCPYINP: if (!(lw->flags & bINPUT)) break; *(lw->end) = '\0'; (void)strcpy(WorkInput,lw->input); break; case fCUTINP: if (!(lw->flags & bINPUT)) break; *(lw->end) = '\0'; (void)strcpy(WorkInput,lw->input); lw->flags &= ~(bINPUT|bINFO); if (lw->apiarg || lw->flags & bTYPE) O_InputMgr(lw,NULL,0); return 2; case fINSINP: if (!(*WorkInput)) break; O_InputMgr(lw,WorkInput,strlen(WorkInput)); return 1; /* CPYVMF and INSVMF provide copy and paste capability for the file associated with a window. The implementation uses the next, base, and eof components of Work. CPYVMF copies only the portion of the VM file currently associated with the window, not the whole VM file (see DELBOW and DELAFT). For pollsters and onlookers, the VM file is managed by expansion only; thus, a copy of the copy file is not made. For others, a copy is made because the copy file may or may soon be managed by truncation. Thus, the user can paste an output stream of an application into an input stream. INSVMF depends on the subclass. For pollsters and onlookers, the file is appended to the VM file associated with it; the updated file is associated with the window, but the window position is not changed. For others, the VM file is pasted into the input stream with no EOF at the end, i.e. input quietly reverts to the window. In effect, it's treated as input direction so that an INSVMF when input is directed is illegal. WARNING: INSVMF always destroys the cut-paste VM file so that, if you want to paste twice, you must also copy twice. */ case fCPYVMF: if (!(lw->dout)) return -2; lf = Work->next; Work->next = NULL; if (lf) O_DestroyVMF(lf); if (lw->base == lw->eof) break; if (lw->flags & bOTHER) { Work->size = (long)(lw->eof - lw->base); lf = O_CreateVMF(NULL,NULL,Work->size,&s); if (!lf) return -2; if ((lw->dout)->flags & bHEX) lf->flags |= bHEX; Work->next = lf; O_AppendVMF(NULL,lf,lw->base,Work->size); Work->base = lf->base; Work->eof = lf->eof; } else { Work->next = lw->dout; Work->base = lw->base; Work->eof = lw->eof; } break; case fINSVMF: if (!(Work->next)) return -2; if (lw->flags & bPOLL) { if ((Work->next)->flags & bHEX) return -2; for (s = Work->base; s < Work->eof; ++s) { for (t = s; *s != '\n' && s < Work->eof; ++s); if (s == Work->eof) --s; if (! O_AppendVMF(lw,NULL," ",2)) break; if (! O_AppendVMF(lw,NULL,t,(long)(s-t) + 1)) break; } lf = lw->dout; lw->base = lf->base; lw->eof = lf->eof; lw->ytop = 0; lw->bot = NULL; cmd = 2; } else if (lw->flags & bONLKR) { if (!(lw->dout)) return -2; O_AppendVMF(lw,NULL,Work->base,Work->eof - Work->base); lf = lw->dout; lw->base = lf->base; lw->eof = lf->eof; lw->ytop = 0; lw->bot = NULL; cmd = 2; } else if (lw->flags & bOTHER) { if (lw->din) return -2; cmd = -1; lw->din = Work->next; lw->flags &= ~bEOF; lw->dinptr = lw->dinbase = Work->base; lw->dineof = Work->eof; if (lw->apiarg) { O_APIEvent(0); lw->flags &= ~bINPUT; cmd = 2; }} lf = Work->next; Work->next = NULL; O_DestroyVMF(lf); return cmd; /* STATIC toggles the STATIC flag, which applies only to others. If STATIC is on, the automatic window updating is skipped, but the application can explicitly update the window. If STATIC is toggled off, the complete output stream is associated with the window, and the window is updated to show the end of the output stream. */ case fSTATIC: if (lw->flags & (bPOLL|bONLKR)) break; lw->flags ^= bSTATIC; if (lw->flags & bSTATIC) break; lw->base = (lw->dout)->base; lw->eof = (lw->dout)->eof; lw->bot = lw->eof-1; lw->ybot = -1; lw->shft = 0; return 2; /* The DELBOW, DELAFT, and DELALL functions have dual roles depending on whether the user is entering input through the window or not. If input is being entered through the window, they apply to the input relative to the caret; otherwise, they apply to the portion of the file associated with the window. For input editing, DELALL deletes the current input but leaves INPUT on, DELBOW deletes all characters before the caret, and DELAFT deletes all characters after the caret (inclusive). Because these operations change the input, they set INFO off, and update the input. For file association, DELALL associates the whole file with the window, DELBOW disassociates the portion before the top line, and DELAFT disassociates the portion after the last line. Although these features were not included in the original design, I found myself in dire need of the convenience they offer. Neither DELBOW nor DELAFT update the window. */ case fDELALL: if (lw->flags & bINPUT) { lw->flags &= ~bINFO; lw->end = lw->fcur = lw->cur = lw->input; return 1; } else if (lw->dout) { lw->base = (lw->dout)->base; lw->eof = (lw->dout)->eof; lw->ytop = 0; lw->bot = NULL; return 2; } break; case fDELBOW: if (lw->flags & bINPUT) { lw->flags &= ~bINFO; t = lw->input; s = lw->cur; while (s < lw->end) *t++ = *s++; lw->end = t; lw->fcur = lw->cur = lw->input; return 1; } else if (lw->dout) { if (O_Valid(lw->top,(lw->dout)->base,(lw->dout)->eof)) lw->base = lw->top; } break; case fDELAFT: if (lw->flags & bINPUT) { lw->flags &= ~bINFO; lw->end = lw->cur; return 1; } else if (lw->dout) { if (O_Valid(lw->bot,(lw->dout)->base,(lw->dout)->eof)) { if ((lw->dout)->flags & bHEX) lw->eof = lw->bot+16; else { s = lw->bot; while (*s++ != '\n'); lw->eof = s; }}} break; /* APPT, APPB, and APPR provide the user control over the application process or, more specifically, its threads. If blocked, any thread that calls an API function is suspended until released by the user. For historical reasons, these functions should be associated with Ctrl+S and Ctrl+Q. TERM is self explanatory. */ case fAPPT: TOGGLE_APPL; break; case fAPPB: BLOCK_APPL; break; case fAPPR: RELEASE_APPL; break; case fTERM: PostQuitMessage(0); break; default: return (arg == fIGNORE) ? -1 : -2; END return -1; END /*---------------------------------------------------------------- ButtonFcn (window, press, x, y) performs the pointer function DRAGTEXT drag the text around the window, POSNTEXT implement a proportional scroll bar, or INSERT insert input from the window in the pointer component of the window struct and returns -2 make a noise, -1 no drawing and no noise, 0 redraw the caret, 1 redraw the input, and 2 redraw the window, which the caller should implement. These functions depend on the pointer position (x,y), of course, but may also depend on the context, i.e., whether input is being entered through the window. The press argument should be nonzero for button press events and zero otherwise. This function depends on the coordinate system used by the windowing system. In X and Windows, (0,0) is the upper left corner; in OS/2, the lower left. The x and y arguments are relative to (0,0). Button events are implemented by referencing the button table for the corresponding function and then calling ButtonFcn; changing the table suffices to change the function provided by the button. If a function does not initiate a drag, the pointer component in the window struct should be set to zero. For drags, it should be left unchanged; it's reset by subsequent events, e.g., when the pointer leaves the window. Because the function is set when the drag is initiated, the Shift and Ctrl keys can be released while dragging. Although a switch would be natural, the implementation is done as a set of if {} blocks so that the function can be changed here depending on pointer location. Obviously, the if's must be in the right order. */ int O_ButtonFcn (WND *lw, int press, int x, int y) BEGIN int k, h; char *s, *t; /* DRAGTEXT is changed to a POSNTEXT if the pointer is in the scroll bar and either BAR or TIC. DRAGTEXT initiates a drag function. A press initiates dragging and simply records the position of the pointer, which effectively attaches the pointer to the character under it. For subsequent movement of the pointer, the text is moved under the window to maintain this attachment. The text movement is by lines and characters, not pixels, and is somewhat accelerated so that the character originally under the pointer can be moved to the edge or outside the window without having to initiate a new drag. As for window movement, vertical movement is limited by the text so that at least one line of the VM file appears in the window, but horizontal movement is unrestricted. */ if (lw->ptrfcn == pDRAGTEXT) BEGIN if (! lw->dout) { lw->ptrfcn = 0; return -2; } if (press) BEGIN if (x > lw->ww - lw->th && lw->flags & (bBAR|bTIC)) lw->ptrfcn = pPOSNTEXT; /* and fall out the bottom.*/ else { lw->px = x; lw->py = y; return -1; } END else BEGIN if (lw->tw < 1 || lw->th < 1) O_GetFontSize(lw); lw->shft -= (h = (x - lw->px)/((lw->tw > 2) ? lw->tw - 2 : 2)); k = y - lw->py; if (k >= lw->tw) { lw->px = x; lw->py = y; for (h = k; k >= 0; k -= lw->th) if ((s = O_LineBefore(lw,lw->top)) != lw->top) { lw->top = s; lw->ytop = 0; } else if ((lw->ytop+2)*lw->th <= lw->wh) ++(lw->ytop); else if (h == k) return -1; else break; lw->bot = NULL; return 2; } else if (k <= -(lw->tw)) { lw->px = x; lw->py = y; for (h = k; k < 0; k += lw->th) if ((s = O_LineAfter(lw,lw->bot)) != lw->bot) { lw->bot = s; lw->ybot = -1; } else if (lw->ybot) --(lw->ybot); else if (h == k) return -1; else break; lw->top = NULL; return 2; } else if (h) { lw->px = x; return 2; } else return -1; END END /* POSNTEXT implements a point-and-click scroll bar but depends on neither the BAR and TIC flags nor the horizontal (x) position of the pointer; these values determine whether a DRAGTEXT is changed to a POSNTEXT, but POSNTEXT is itself independent of them. POSNTEXT moves the window so that the line containing the (pointer position) file position = (file size)* ------------------. (window height) is at the top of the window and relies on the robust character of DrawWindow to take care of the details. The implementation uses floating-point, which isn't an "arcane" feature of some computers even though it was described that way in articles about the Pentium divide flaw. Yes, a floating-point divide is used in the implementation so.... */ if (lw->ptrfcn == pPOSNTEXT) BEGIN lw->ptrfcn = 0; if (! lw->dout) return -2; lw->top = lw->base + (int)((float)(lw->eof - lw->base) *((float)(y)/(float)(lw->wh))); lw->ytop = 0; lw->bot = NULL; return 2; END /* INSERT is a point-and-click function for line re-entry but depends on pointer location and the CARET flag; the INPUT flag is not used in ButtonFcn because button events imply nothing about focus so that the CARET and INPUT flags are not equivalent. If CARET is on AND the pointer is in the input rectangle, INSERT is equivalent to pressing Enter; otherwise, it replaces the input with the line under the pointer. Even though described as line re-entry, INSERT really doesn't care how a line was generated; it simply replaces the input with the line under the pointer. WARNING: The implicit Enter feature can be a dangerous in some circumstances, e.g., if the input changes the window geometry in such a manner that the pointer is no longer in the window and you release the button before moving the pointer back to the window and the application under the pointer processes button release events, strange things can happen. This may sound unlikely but can happen. This software always ignores button releases. */ if (lw->ptrfcn == pINSERT) BEGIN lw->ptrfcn = 0; if (lw->flags & bCARET && lw->inr.top <= y && y <= lw->inr.bottom && lw->inr.left <= x && x <= lw->inr.right) return O_KeyFcn(lw,fENTER); else if (lw->dout && !((lw->dout)->flags & bHEX)) { if (lw->th < 1) O_GetFontSize(lw); k = lw->ytop; y /= lw->th; if (y < lw->ytop || y > lw->ybot) return -2; for (s = lw->top; y > k; ++k) s = O_LineAfter(lw,s); for (t = s; t < lw->eof && *t != '\n'; ++t); if (*(t-1) == '\r') --t; O_InputMgr(lw,NULL,0); if (t != s) { O_InputMgr(lw,s,(int)(t - s)); lw->fcur = lw->cur = lw->input; } return 1; } END k = lw->ptrfcn; lw->ptrfcn = 0; return (k == pIGNORE) ? -1 : -2; END /*---------------------------------------------------------------- LineBefore and LineAfter (window, pointer) return a pointer to the line before or after, respectively, the line containing the argument if such a line exists and otherwise their argument. If the argument does not correspond to the first character of a line, LineBefore returns the line containing it. What constitutes a 'line' depends on the HEX window flag. If HEX is on, each line consists of 16 bytes of data starting at the beginning of the VM file that is associated with the window, i.e., the base pointer in the window struct. For text, a line consists of the characters between successive newlines with the obvious caveat for the beginning and end. These functions are used to implement vertical movement, e.g., KeyFcn...fUP; they are NOT used in DrawWindow. */ char *O_LineBefore (WND *lw, char *t) BEGIN size_t k; if (! lw || ! lw->dout) return t; if (! O_Valid(t,lw->base,lw->eof) || t == lw->base) return t; if (lw->flags & bHEX) { k = (size_t)(t - lw->base); if (k & 15) k = (k >> 4) << 4; else if (k >= 16) k -= 16; t = lw->base + k; } else { --t; while (t > lw->base && *(t-1) != '\n') --t; } return t; END char *O_LineAfter (WND *lw, char *t) BEGIN char *s; if (! lw || ! lw->dout) return t; if (! O_Valid(t,lw->base,lw->eof)) return t; if (lw->flags & bHEX) s = lw->base + ((((size_t)(t - lw->base)) >> 4) << 4) + 16; else { s = t; while (*s++ != '\n'); } return (s < lw->eof) ? s : t; END /*---------------------------------------------------------------- DrawWindow (window) draws the window based on the window struct and returns 0. The (re)drawing is based on bot/ybot or top/ytop and the copies of the base and eof pointers for the file in the window struct; these latter values can be changed by the user to restrict the window to a portion of the file. When calling this function, the values in the window struct need only be close since they are automatically corrected by this function. When DrawWindow returns, top/ytop and bot/ybot reflect the data actually drawn in the window. Outside this function, ytop and ybot are relative line numbers with 0 the top and anything illegal the bottom or top as appropriate. The horizontal position of the text in the window is based on the shift component. The shift may result in an empty window if the window has been shifted too far to the right or left. Although this software will not shift the window vertically beyond the text, it has no inhibitions horizontally. The POO software assumes that DrawWindow is robust and capable of dealing with all situations, valid or otherwise, without failure. DrawWindow safeguards itself against invalid data in the window struct so that the code elsewhere can be imprecise when setting the drawing components in the window struct. The setting of these components without any tests, etc., may appear irresponsible; it is, but it's based on faith in DrawWindow. In short, DrawWindow perseveres in the face of adversity, likewise DrawInput. Two basic drawing algorithms are provided: a bottom-up algorithm that starts with the 'line' containing the bot pointer at ybot and a top-down algorithm that starts with the 'line' containing the top pointer at ytop. The 'containing' is correct; the algorithms adjust these pointers as necessary. Lines in text files are based on newlines, while binary files have aligned, fixed length lines. Both algorithms start wherever and continue drawing until the data is exhausted or the window is full and finish by setting top/ytop and bot/ybot to reflect what was drawn and where. DrawWindow implements two optional components, namely a vertical scroll bar and the text border. The vertical scroll bar has two components: a transparent vertical bar along the right edge of the window and a tic within this bar showing the relative position of the top line in the window relative to the whole file. If the bar is drawn, the tic is also drawn; but, the tic can be drawn without the bar. The standard opaque vertical scroll bar is not supported. The border around the text simply provides visual separation between blank space within the text and the nothingness that surrounds it. Rectangles are window relative with (0,0) at the upper left corner in X and Windows and at the lower left corner in OS/2. When filling a rectangle or drawing text in a rectangle, the extents must be one larger than desired because the filling is actually done one pixel less in each dimension. */ int O_DrawWindow (WND *lw) BEGIN HDC gc; RECT r; char *base, *eof, *top, *bot, *s; int wh, th, k, maxk, hide; /* If on, the caret must be turned off before drawing. Both the window and font sizes are relatively crucial here. The font width and height are set when the font is changed and should be correct; in practice, GetFontSize is never called. Similarly, the window width and height are set by SIZE events but.... After setting the RECT r to the entire window, two special cases are handled: a window with no associated VM file and a window associated with a VM file that is empty. Both should produce an empty window, but there may be input. */ CARET_OFF; if (lw->tw < 1 || lw->th < 1) O_GetFontSize(lw); th = lw->th; GetClientRect(lw->wnd,&r); wh = lw->wh = r.bottom; lw->ww = r.right; gc = lw->gc; if (! lw->dout || lw->base == lw->eof) { lw->top = lw->bot = lw->base; lw->ytop = lw->ybot = 0; FillRect(gc,&r,lw->bb); return O_DrawInput(lw,1); } /* The base and eof pointers in the window struct should be valid, i.e., between the base and eof pointers in the VM file struct. The window copies are used to restrict the portion of the VM file associated with the window, i.e., the portion of the VM file that can be viewed in the window. This test is low cost insurance in that they should be valid, but using an invalid pointer could have dire consequences. */ if (! O_Valid(lw->base,(lw->dout)->base,(lw->dout)->eof)) lw->base = (lw->dout)->base; if (! O_Valid(lw->eof,lw->base,((lw->dout)->eof)+1)) lw->eof = (lw->dout)->eof; /* The selection of the drawing algorithm, bottom up or top down, is based on the bot and top pointers. Following the same philosophy used for base and eof, they are tested to make sure that at least one of them is valid, i.e., not NULL and within the portion of the file associated with the window. This code gives preference to bot: bot is tested first. If neither are valid, pollsters and onlookers are positioned using the posn component, while others are positioned at the end of the file. Elsewhere, ytop and ybot are relative line numbers with line 0 at the top regardless of the window coordinate system. But, within this function, they are the y coordinates of the lines that are drawn. DrawWindow converts the relative line numbers to y coordinates and adjusts inappropriate values, ones that make no sense, and then converts them back to relative line numbers before returning. */ base = lw->base; eof = lw->eof; top = lw->top; bot = lw->bot; if (bot && O_Valid(bot,base,eof)) { top = NULL; lw->ybot *= th; if (lw->ybot < 0 || lw->ybot > (wh - th)) lw->ybot = wh - wh % th - th; } else if (top && O_Valid(top,base,eof)) { bot = NULL; lw->ytop *= th; if (lw->ytop < 0 || lw->ytop > (wh - th)) lw->ytop = 0; } else if (lw->flags & (bPOLL|bONLKR)) { size_t p; bot = NULL; lw->ytop = 0; p = (size_t)((lw->dout)->posn); top = base + ((p < (size_t)(eof - base)) ? p : 0); } else { bot = eof-1; lw->ybot = wh - wh % th - th; top = NULL; } /* If the window is too small to be useable or iconified, blank it and pretend that it was drawn with only one line. Theory would suggest that a window cannot be iconified if INPUT is set but.... */ if (lw->shft >= 0) { k = 0; hide = lw->shft; } else { k = -(lw->shft); hide = 0; } if (wh < th || lw->ww < th || IsIconic(lw->wnd)) { if (wh < th) FillRect(gc,&r,lw->bb); lw->top = lw->bot = (bot) ? bot : top; lw->ytop = lw->ybot = 0; return O_DrawInput(lw,1); } /* The setting of maxk is ad hoc but appears to avoid problems that arise when very long lines are drawn; it truncates them to maxk characters. Without maxk, very long lines appear to wrap around the window in Windows 3.1 despite the automatic clipping. The empty rectangle drawn along the left edge takes care of the horizontal shift and is always at least one character wide so that the text doesn't get jammed into the window border. */ maxk = lw->ww >> 2; r.right = (lw->tw)*(k + 1); FillRect(gc,&r,lw->bb); r.left = r.right; r.right = lw->ww; /* Whether displaying text or binary data, the drawing process is the same, i.e., successive lines are drawn starting with the line that contains the bot pointer at ybot or the line that contains the top pointer starting at ytop. In either case, successive lines are drawn until the data is exhausted or the window filled. In either situation, an empty rectangle is drawn first if the first line is not drawn at the edge of the window, and another empty rectangle is drawn at the end if the last line is not at the other edge of the window. When the drawing is completed, the local variables top, bot, ytop, and ybot contain the values that are saved in the window struct. The y values, of course, are converted to relative line numbers; they are window coordinates here. Hex Dump Mode Drawing--------------------------------------------- The bot/top pointers are always 'corrected' to the nearest lower multiple of 16. The responsibility for producing the text line rests entirely with HexDump, except that it's implicit that 16 bytes are processed per window line. The current version of HexDump supports a single, standard sort of hex dump but could produce numerous different formats with no impact on the code here, e.g., two floating-point doubles per line. This code uses the tmp component as a buffer and draws whatever HexDump returns. The user can toggle the HEX flag with Ctrl+O so that a VM file can be viewed as either text or binary. */ if (lw->flags & bHEX) BEGIN char *buf; buf = lw->tmp; if (bot) BEGIN /* Bottom-up hexdump */ top = bot = bot - ((size_t)(bot - base) & 15); r.top = lw->ybot + th; FillRect(gc,&r,lw->bb); r.bottom = r.top; r.top -= th; while (r.top >= 0 && O_Valid(top,base,eof)) { k = (int)(O_HexDump(buf,(lw->dout)->base,top,eof) - buf); top -= 16; FillRect(gc,&r,lw->bb); if ( k > hide) DrawText(gc,buf+hide,k-hide,&r,POO_STYLE); lw->ytop = r.bottom = r.top; r.top -= th; } if (r.bottom > 0) { r.top = 0; FillRect(gc,&r,lw->bb); } top += 16; END /* Bottom-up hexdump */ else BEGIN /* Top-down hexdump */ bot = top = top - ((size_t)(top - base) & 15); r.bottom = lw->ytop; FillRect(gc,&r,lw->bb); r.top = r.bottom; r.bottom += th; while (r.bottom <= wh && O_Valid(bot,base,eof)) { k = (int)(O_HexDump(buf,(lw->dout)->base,bot,eof) - buf); bot += 16; FillRect(gc,&r,lw->bb); if ( k > hide) DrawText(gc,buf+hide,k-hide,&r,POO_STYLE); lw->ybot = r.top; r.top = r.bottom; r.bottom += th; } if (r.top < wh) { r.bottom = wh; FillRect(gc,&r,lw->bb); } bot -= 16; END /* Top-down hexdump */ END /* Text Mode Drawing ------------------------------------------------ The bot/top pointers are always 'corrected' to the first character of the line containing the pointer. Since both text and hex modes correct toward the beginning of the file, switching modes usually causes precession of the window contents. */ else BEGIN if (bot) BEGIN /* Bottom-up text */ while (bot > base && *(bot-1) != '\n') --bot; for (top = bot; top < eof && *top != '\n'; ++top); ++top; r.top = lw->ybot + th; FillRect(gc,&r,lw->bb); r.bottom = r.top; r.top = lw->ybot; while (r.top >= 0 && top > base) { s = --top; while (top > base && *(top-1) != '\n') --top; FillRect(gc,&r,lw->bb); k = (int)(s - top) - hide; if (*(s-1) == '\r') --k; if ( k > 0) DrawText(gc,top+hide,(k < maxk) ? k : maxk,&r, POO_STYLE); lw->ytop = r.bottom = r.top; r.top -= th; } if (r.bottom > 0) { r.top = 0; FillRect(gc,&r,lw->bb); } END /* Bottom-up text */ else BEGIN /* Top-down text */ while (top > base && *(top-1) != '\n') --top; r.bottom = lw->ytop; FillRect(gc,&r,lw->bb); r.top = r.bottom; r.bottom += th; s = (bot = top) - 1; while (r.bottom <= wh && ++s < eof) { bot = s; while (s < eof && *s != '\n') ++s; FillRect(gc,&r,lw->bb); k = (int)(s - bot) - hide; if (*(s-1) == '\r') --k; if ( k > 0) DrawText(gc,bot+hide,(k < maxk) ? k : maxk,&r, POO_STYLE); lw->ybot = r.top; r.top = r.bottom; r.bottom += th; } if (r.top < wh) { r.bottom = wh; FillRect(gc,&r,lw->bb); } END /* Top-down text */ END /* The drawing of the window has been completed, and it remains to draw the optional components: the transparent vertical scroll bar and the border around the text. The scroll bar is similar to the usual Windows and OS/2 scroll bars only in function. The border around the text serves to mark the edge of nothingness. Vertical Scroll Bar The scroll bar is optional, i.e., the BAR and TIC flags. Because everyone knows where the scroll bar is at this point, I elected to implement a transparent scroll bar to avoid sacrificing the window space but have found it visually confusing. Thus, by default, the bar is not drawn, while the TIC is drawn for onlookers. Neither is particularly useful in others because the VM file is expanding constantly so that the TIC moves quickly toward the bottom of the window and then oozes downward. */ if (lw->flags & (bTIC|bBAR)) { k = (int)((float)wh* ((float)(top - base)/(float)(eof - base))); MoveTo(gc,lw->ww,k); LineTo(gc,lw->ww - th,k); if (lw->flags & bBAR) { MoveTo(gc,k = lw->ww - th,0); LineTo(gc,k,wh); }} /* Border If there is open space above, to the left, or below the text that was drawn in the window, a line is drawn around the text so that the user can differentiate between white space within the text and the nothingness around it. The lines are drawn tw pixels outside the text. Usually only one or two of these lines are needed, but all may be present for very small files. By default, BD is off for pollsters and others and on for onlookers. */ if (lw->flags & bBD) { r.left -= lw->tw; if (r.left < 0) r.left = 0; if (lw->ytop - th >= 0) { k = lw->ytop - lw->tw; MoveTo(gc,lw->ww,k); LineTo(gc,r.left,k); } else MoveTo(gc,r.left,0); k = lw->ybot + 2*th; if (k <= wh) k -= (th - lw->tw); if (r.left > 0) LineTo(gc,r.left,k); else MoveTo(gc,r.left,k); if (k < wh) LineTo(gc,lw->ww,k); } /* Finally, the drawing components of the window struct are updated, including the conversion to relative line numbers for ytop and ybot. If INPUT is on, DrawInput must be called. The time of this draw is saved in the ptime component, which is used to prevent run on, i.e., continued scrolling after the key is released. */ lw->top = top; lw->bot = bot; lw->ytop /= th; lw->ybot /= th; if (lw->flags & bINPUT) O_DrawInput(lw,1); lw->ptime = GetTickCount(); return 0; END /*---------------------------------------------------------------- HexDump (buffer, base, data, eof) returns in the buffer a line in hex dump format, i.e., DDDDDDDD XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX CCCCCCCCCCCCCCCC where blanks rather than leading zeros are used in the relative displacement D...D and blanks are substituted for characters with no graphic in the character field C...C. This function returns a a pointer to the next available buffer character so that data can be added easily or the length computed. The relative displacement of the 16 bytes of data, which is delimited by the base and eof arguments, is computed relative to the base. DrawWindow is the only caller, but other functions assume 16 byte lines when HEX is on, e.g., LineBefore. DrawWindow does not look at the text placed in the buffer, i.e., this function can generate about anything it wants for the 16 byte lines. */ char *O_HexDump (char *buf, char *base, char *data, char *eof) BEGIN static char hex[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; long w; int k, i; char *s; if (! O_Valid(data,base,eof)) return buf; /* The displacement relative to the base address is generated as an 8 character hex value, and then leading zeros are replaced up to but not including the right most. */ s = buf; w = (long)(data - base); k = (int)(w >> 24) & 255; *s++ = hex[k >> 4]; *s++ = hex[k & 15]; k = (int)(w >> 16) & 255; *s++ = hex[k >> 4]; *s++ = hex[k & 15]; k = (int)(w >> 8 ) & 255; *s++ = hex[k >> 4]; *s++ = hex[k & 15]; k = (int)w & 255; *s++ = hex[k >> 4]; *s = hex[k & 15]; while (*buf == '0' && buf < s) *buf++ = ' '; /* Up to 16 bytes of hex data with blank separators every four bytes are generated, a total of 38 characters. At the same time, each character for which there is a graphic is appended to the end of the line; a blank is used for non-graphic characters. A different criterion could be used for the character display. */ *(buf = s+1) = ' '; base = s = ++buf + 38; for (i = 0; i < 16 && data < eof; ++data, ++i) { if (!(i & 3)) *buf++ = ' '; *buf++ = hex[(*data >> 4) & 15]; *buf++ = hex[*data & 15]; *s++ = (char)((isgraph(*data)) ? *data : ' '); } /* To finish, it suffices to place blanks between the last hex digit and the first character, at least two but more if fewer than 16 bytes were available. */ while (buf < base) *buf++ = ' '; return s; END /*---------------------------------------------------------------- DrawInput (window, {zero => caret | nonzero => input}) draws the input and/or caret if the window has keyboard focus. The input rectangle is determined by the pertinent flags and saved in the window struct for use in ButtonFcn. DrawInput returns 0. In most respects, the input is drawn in a very unusual manner. The usual xterm or DOS window accepts input as the last line in the window. DrawInput uses an input rectangle divorced from the text in the window and always draws in reverse video. Thus, when the application solicits input, it's obvious. Further, this technique obviates the need to redraw the window while input is entered because only the input rectangle must be redrawn and allows the user to see a prior portion of the output stream while entering input. The user has limited control of both the location and the size of the input rectangle, see the MODE command. The Input Caret DrawInput is the sole proprietor of the caret. Outside DrawInput, the position of the caret is maintained using the cur component in the window struct. This is handy elsewhere but not particularly useful when the caret has to be drawn. To draw the caret, its x,y position is required. The y coordinate is relatively easy because it depends only on the location of the input rectangle. The x coordinate is a bit more complex. Texts on windowing systems often duck this issue by assuming a fixed size font, which reduces this to a multiply, or suggest keeping track of the caret position as characters are added or deleted, which would add code throughout that depends on the current font. The POO software vests all responsibility for the caret in DrawInput. DrawInput computes the position of the caret, draws it, and sets the CARET flag. The caret is complicated because the font may be proportional or fixed and because the input may or may not fit in the rectangle. DrawInput takes these factors into account and ensures that the caret is in the rectangle, i.e., the input is shifted as necessary so that the caret character and caret are in the rectangle. The fcur component in the window struct is the first input character drawn in the input rectangle; fcur should not be manipulated elsewhere because DrawInput assumes that it's the value used the last time. The caret size depends on whether insert or overstrike mode is in effect and on the INFO flag. The insert caret is a thick line before the character; the overstrike, a pillow covering it; and the informational caret is a thin line. The difference between the insert caret and the INFO caret is subtle; but context and content usually suffice to distinguish the situation. The CARET flag is widely referenced directly and indirectly with the CARET_OFF macro, but DrawInput is the only place that the CARET flag is set. The caret is drawn and the CARET flag set if and only if the INPUT and FOCUS flags are set. Thus, for example, although the input may be drawn in a window, the caret will not be drawn unless the keyboard is currently associated with the window. */ int O_DrawInput (WND *lw, int both) BEGIN HDC gc; RECT r; SIZE ss; int k; char *s; /* It cannot be assumed that the call came from DrawWindow so the caret status must be handled; for DrawWindow calls, it'll always be off. If INPUT is off, somebody goofed but there is no gain in compounding the problem. If the text width or height are not set properly, they are obtained now because they will be needed. */ CARET_OFF; if (!(lw->flags & bINPUT)) return 0; if (lw->tw < 1 || lw->th < 1) O_GetFontSize(lw); /* The input rectangle is based on the flags as follows: INT places it at top of window, INB places it at the bottom of the window, INH makes it two lines high, and INW makes it full width. The input rectangle is centered vertically if neither INT nor INB. If not full width, it's inset by the font height from the edges provided the window is wide enough. The input rectangle is saved in the window struct for testing the pointer position in ButtonFcn. */ k = lw->th; if (lw->flags & bINH) k += k; if (lw->flags & bINT) { r.top = 0; r.bottom = k; } else if (lw->flags & bINB) r.top = (r.bottom = lw->wh) - k; else r.bottom = (r.top = (lw->wh - k) >> 1) + k; if (r.top < 0) r.top = 0; if (r.bottom > lw->wh) r.bottom = lw->wh; r.right = lw->ww - (r.left = (lw->flags & bINW) ? 0 : lw->th); if (r.right <= r.left) { r.left = 0; r.right = lw->ww; } lw->inr = r; /* The x position of the cursor character and the value of fcur, the pointer to the first input character drawn in the rectangle, must be coordinated so that the caret is in the rectangle. The caret position is computed by starting with the previous value of fcur and the current value of cur and adjusting the new fcur value so that the caret position is in the rectangle. For a proportional font, the sum of the parts is NOT necessarily the whole! In most cases, the first try satisfies the criterion so that fcur is not changed; many iterations should be rare. If fcur is changed, the input must be redrawn; otherwise, it's redrawn only if specified by the last argument. Within the input rectangle, the first input character is offset from the left edge of the rectangle by the average character width and the caret must be at least the font height from the right edge. If there is no room, only the caret character is drawn. */ s = lw->fcur; gc = lw->gc; if (lw->flags & bFOCUS) { k = r.right - r.left - 2*lw->th; if (k < 1) { s = lw->cur; ss.cx = 0; } else while (k) { if (lw->cur == s) { ss.cx = 0; break; } GetTextExtentPoint(gc,s,(int)(lw->cur - s),&ss); if (s != lw->input && ss.cx < lw->th) --s; else if (ss.cx > k) ++s; else break; } if (s != lw->fcur) { lw->fcur = s; both = 1; } ss.cx += r.left + lw->tw; } /* If requested or necessary, the input is drawn vertically centered in the input rectangle in explicit reverse video. The input from fcur (s) to the end is drawn; the windowing system clips the text to the rectangle so that the last character may be incomplete. */ if (both) { FillRect(gc,&r,lw->fb); r.left += lw->tw; SetTextColor(gc,lw->bg); DrawText(gc,s,(int)(lw->end - s),&r,POO_STYLE|DT_VCENTER); SetTextColor(gc,lw->fg); } /* Finally, if the window has keyboard focus, the caret is created and drawn at the appropriate location. */ if (lw->flags & bFOCUS) { if (lw->flags & bINFO) k = 1; else if (lw->flags & bINSERT || lw->cur == lw->end) k = 2; else GetCharWidth(gc,(UINT)*(lw->cur),(UINT)*(lw->cur),&k); CreateCaret(lw->wnd,(HBITMAP)NULL,k,lw->th); SetCaretPos(ss.cx,r.top + (((r.bottom - r.top) - lw->th) >> 1)); ShowCaret(lw->wnd); lw->flags |= bCARET; } return 0; END /*---------------------------------------------------------------- InputMgr (window, text, length) is responsible for managing input entered through the window and for appending messages to the user's input and always returns 0. InputMgr(*,NULL,*) initializes for input: INPUT on and INFO off. InputMgr(*,text,len > 0) inserts or overstrikes the current input with the specified text starting at the caret; INFO off. The remaining services require that the text be null terminated and set INFO on but are ignored if INPUT is off. InputMgr(*,string,0) replaces the input with the text string, and InputMgr(*,string,len < 0) appends a blank followed by the string. These latter services are used extensively by Command to generate messages, e.g., display commands; Command relies on InputMgr to ignore calls when INPUT is off. In some situations, Command usurps the role of InputMgr and places text in the input buffer directly, i.e., when it's more convenient and efficient to build the message in the input buffer. Input Implementation Input though the window is implemented using the input and extra buffers (MAXLENINPUT); the end, cur, and fcur pointers into the input buffer; the INSERT, INPUT, and INFO flags; and indirectly the FOCUS, CARET, and TYPE flags. The flags play the following roles: INSERT insert/overstrike mode (toggled by the Insert key), INPUT input in progress, INFO input buffer contains a message, FOCUS window has keyboard focus, CARET set by DrawInput if INPUT and FOCUS, TYPE NB command in progress (set by Command...TYPE). The user's input consists of the text in the buffer up to but not including the end character; new text is inserted before the cursor (caret) character or replaces it; and the "first cursor" pointer is owned by DrawInput and should not be changed. The input facility is also used to display messages. For example, if the user enters a bad command, a cryptic comment is appended to the command and INFO is set on. Commands with no operand, which display the current value of an attribute, append the current value and set INFO on. If INFO is set and the user presses Enter, the input is discarded; but, if INFO is set and the user types, INFO is set off and the changed message becomes valid input. When appending information to the user's input, the caret is not moved. These features are for the convenience of the user. */ int O_InputMgr (WND *lw, char *text, int len) BEGIN char *s; /* InputMgr(*,NULL,*) initializes for input and is used primarily to solicit input for the application or the NB command. */ if (! text) { lw->flags &= ~bINFO; lw->flags |= bINPUT; lw->end = lw->fcur = lw->cur = lw->input; } /* InputMgr(*,text,len > 0) inserts or overstrikes the current input with the specified text starting at the cursor, e.g., key presses generate such calls with a length of 1. Input initialization is automatic if INPUT is off. If the input would exceed the buffer, the call is ignored. If insert mode, the current input is moved to make room for the new text. After overstriking the input with the new text starting at the caret, the end pointer is updated as necessary. */ else if (len > 0) { lw->flags &= ~bINFO; if (!(lw->flags & bINPUT)) { lw->flags |= bINPUT; lw->end = lw->fcur = lw->cur = lw->input; } if (( (int)(((lw->flags & bINSERT) ? lw->end : lw->cur) - lw->input) + len ) >= MAXLENINPUT) { WOOF; return 0; } if (lw->flags & bINSERT) { for (s = lw->end - 1; lw->cur <= s; --s) *(s + len) = *s; lw->end += len; } while (len-- > 0) *(lw->cur)++ = *text++; if (lw->cur > lw->end) lw->end = lw->cur; } /* InputMgr(*,string,0) replaces the input with the text string, and InputMgr(*,string,len < 0) appends a blank followed by the string. In these situations, the text is always appended so that the input mode, insert or overstrike, is not relevant. The caret is not moved when appending information. */ else if (lw->flags & bINPUT) { if (! len) lw->end = lw->fcur = lw->cur = lw->input; if (*text) { s = lw->input + MAXLENINPUT - 1; if (len && lw->end < s) *(lw->end)++ = ' '; while (*text && lw->end < s) *(lw->end)++ = *text++; } lw->flags |= bINFO; } return 0; END /*================================================================ COMMAND PROCESSING The command-driven user interface is based on commands of the form <command word> <blank delimited operands> entered through a window. In general, the window is an implicit operand, i.e., its colors are changed or its current directory is applied in conjunction with file name operands. The functions in this section are: Command implements all commands, Annotate implements operations related to the TYPE flag, ParseCommand parses commands, and ParseGeometry parses X style geometry strings. Command processing is vested primarily in ParseCommand and Command and the auxiliary functions that Command may call, which covers a lot of ground, roughly everything except the main application and the API functions. Indeed, if Command does not call a function either directly or indirectly, then the user has necessarily been denied access to some functionality. There are commands for setting window attributes, positioning the window over the associated VM file, setting a window's current directory and I/O direction, assigning strings to the 48 function keys, creating pollsters and onlookers and VM files, terminating the application, controlling the memory file, and miscellaneous other capabilities. Command is called from KeyFcn when the user terminates an input line containing a command. In this context, a command character is NOT used; command processing is applied if the user terminates the input with [Shift+]Ctrl+Enter or if the context is such that normal input makes no sense or has been disabled by the user. It is also called from various functions in the memory file facility when the application is being started or a window created. Many commands simply set or display a value. Normally, this would be provided using set and display commands; however, in this case, the number of values is small, and the usual mechanism would add overhead. Thus a command is provided for each value that the user can control, e.g., the font. The set and display are distinguished by the presence or absence of an operand, e.g., a "font Courier" sets the font, while a "font" displays the current font. Action commands, e.g., STOP, have no display counterpart, and there are a few display-only commands. To display a value, Command replaces the user's input with a line containing the current value. Thus, it's displayed in the input rectangle, which is the same mechanism used to show error messages. When an error or informational message is being displayed in the input rectangle, it's eliminated if the user presses Enter or Esc and converted to input if the user modifies it. ParseCommand is table driven and produces the command index and multistring operand required by Command. The command indexes are defined constants of the form o[UPPERCASE]. A multistring is a sequence of null terminated character strings terminated by a null string, i.e., <string 1>\0...<string n>\0\0. Annotate is an aggregation of various chunks of code. The common theme of the three functions in Annotate is that they all involve processing associated with the TYPE flag. If TYPE is set, input is continuously solicited through the window until terminated by the user, but the disposition of the input depends on the window's subclass (pollsters, onlookers, and others). ------------------------------------------------------------------ Command (window, command index, multistring operand) executes the specified command relative to the window. The last two arguments are usually produced by ParseCommand, which returns the command index and usually places the multistring <operand 1>\0<operand 2>\0...<operand n>\0\0 in the tmp component of the window struct but doesn't change the input. Although there is no direct interaction between Command and ParseCommand, this function does assume that the command index is more or less reasonable. Command applies the command relative to the window but does not assume that it's in the window's input buffer unless INPUT is set. Command consists of a prologue, one large switch, and an epilogue. The prologue handles the function key commands and provides some preprocessing for commands with no operand. The switch is on the command index. Most commands drop through to the epilogue, but a few establish an unusual state and return directly. The epilogue processes errors and establishes the usual state. Command is called only from KeyFcn to process a command and from MemoryCmd for I/O commands applicable to a window being created. Consequently, it takes no overt action with regard to the window and returns a value consistent with that of KeyFcn (1 updates the input rectangle and 2 the window) so that KeyFcn can simply pass Command's return to its caller. MemoryCmd interprets a return of 2 as successful. */ int O_Command (WND *lw, int cmd, char *opnds) BEGIN RECT wp; WORDTAB *wt; WND *alw; VMF *lf; int k; char *s, *t; /* Command Prologue The error/message pointer, s, is initialized to NULL so individual commands can ignore it. If s is not NULL, the epilogue appends the string s to the input and does error processing. Function key commands are recognized by the presence of the FKFB bit in cmd, which should be FKFB + n, where 1 <= n <= 48. If there is an operand, it suffices to store the command into the memory file, which is exactly what MemoryKey does; MemoryKey fails only if there is no space. If there is no operand, the assigned string, if any, is displayed. The switch is skipped because cmd is set to NONE, but the epilogue is executed. The mapping from the key number, 1...48, is because the FcnKeyFcn table is organized on the shift state and then on the key, 1...12. The FcnKeyStr array holds displacements into the memory file; it's initialized in MemoryLoad and maintained by MemoryKey. */ s = NULL; if (cmd & FKFB) { k = cmd - FKFB - 1; cmd = oNONE; if (k < 0 || k >= 48) s = "?????"; else if (*opnds) { if (! O_MemoryKey(k+1,opnds)) s = "failed"; } else if (FcnKeyStr[k = 4*k - 47*(k/12)] >= 0) { s = opnds = MemoryFile->base + FcnKeyStr[k]; while (*s != '\n') ++s; if (*(s-1) == '\r') --s; while(*(s-1) == ' ') --s; k = (int)(s - opnds); O_InputMgr(lw,NULL,0); O_InputMgr(lw,opnds,k); lw->cur = lw->input; s = ""; } else s = "unassigned"; } /* Command indexes greater than 100 are used for action commands and have no display version; some take no operands, e.g., STOP. If there is no operand, a display-type command, the input is set to the first command word in the table with the index cmd. The table is arranged so that aliases appear latter so that the effect is to replace abbreviations and aliases. InputMgr(...,0) replaces the current input with the string, puts the caret at the beginning of the line, and sets the INFO flag if INPUT is set. The caret is explicitly moved here to the end of the command word so that it's convenient for the user to change the value. If INPUT is not set, setting the caret makes no difference. */ else if (! *opnds && cmd < 100) { for (wt = CommandTable; wt->cmd && wt->index != cmd; ++wt); O_InputMgr(lw,wt->cmd,0); lw->cur = lw->end; } /* Command Switch Although obviously related in some cases, each command is simply implemented here as an 'in-line' function in the context of the prologue and epilogue. Thus, the set version, with operands, and display version, without operands, for each command are together, which enhances readability and modifiability. If an error comment is appropriate, it suffices to set s. In many cases, s is set by another function, e.g., a ...VMF function. For display type commands, the code above has already set the input to the full command word, and the code for a command can either set s to append information or usurp the role of InputMgr and append it directly. In this latter case, it MUST update the end pointer but should do nothing if INPUT is off. */ if (cmd > oNONE) switch (cmd) BEGIN /* For BACKGROUND, FOREGROUND, FONT, GEOMETRY, and MODE commands, the work is actually done in SetAttribute, and the command is recorded in the memory file if successfully executed. Thus, these commands appear system independent because the real code is not here. */ case oBACKGROUND: case oFOREGROUND: s = (cmd == oFOREGROUND) ? lw->fgn : lw->bgn; if (! *opnds) break; if (! O_SetAttribute(lw,cmd,opnds)) s = "invalid"; else { O_MemoryCmd(lw,cmd,s); s = NULL; } break; /* When the font changes, the windows previous contents may no longer fit so that the window may seem to move. For others, the bottom line is maintained at the bottom; for pollsters and onlookers, the top line. In Windows, the first character of the font name, fnn, is a flag: blank for stock fonts. */ case oFONT: if (! *opnds) { s = lw->fnn; if (*s == ' ') ++s; } else if (! O_SetAttribute(lw,cmd,opnds)) s = "invalid"; else { O_MemoryCmd(lw,cmd,lw->fnn); if (lw->flags & bOTHER) lw->ybot = -1; else { lw->ytop = 0; lw->bot = NULL; }} break; /* ParseGeometry uses the ww, wh, px, and py components as defaults for elements omitted in the new geometry string. Since the window position is not maintained in the window struct, it's obtained from the windowing system. Thus it's needed to set and to display. Before trying to change the geometry, an active drag operation, if any, is cancelled. ParseGeometry returns the new geometry in the ww, wh, px, and py components and assumes (0,0) is the upper left corner of the screen. When changing the geometry, the bottom of the window is maintained for all subclasses, which is consistent with geometry changes made by the windowing system. The default for DrawWindow is a bottom-up draw. The geometry display shows two geometries. The first gives the actual geometry in <characters>x<lines>+xorg+yorg; the second, in pixels. The actual geometry, the one that is displayed, and the geometry in the memory file are usually not the same, e.g., the origin is always displayed in the form +x+y. */ case oGEOMETRY: GetWindowRect(lw->wnd,&wp); if (*opnds) { DRAG_OFF; lw->px = wp.left; lw->py = wp.top; if (! O_ParseGeometry(lw,opnds)) { s = "invalid"; break; } SetWindowPos(lw->wnd,HWND_TOP,lw->px,lw->py, lw->ww + lw->fw,lw->wh + lw->fh,0); O_MemoryCmd(lw,cmd,lw->tmp); } else { if (lw->tw < 1 || lw->th < 1) O_GetFontSize(lw); sprintf(s = lw->tmp,"%ix%i+%i+%i (%ix%i)", lw->ww/lw->tw,lw->wh/lw->th,wp.left,wp.top,lw->ww,lw->wh); } break; /* As already noted, SetAttribute actually does the work and ignores all operands other than the first, which should be a sequence of the various flags separated by sign characters. The set/display renditions are merged so that the string produced by the for loop is passed to either MemoryCmd or the epilogue. Thus, successful MODE commands always record the state of all switches. Trying to remember the switch names is probably a bad idea, it's easier to display the mode, modify its signs as desired, and hit Enter. */ case oMODE: if (*opnds && ! O_SetAttribute(lw,cmd,opnds)) { s = "invalid"; break; } for (t = WorkTemp, wt = ModeTable; wt->cmd; ++wt) { *t++ = (char)((lw->flags & wt->index) ? '+' : '-'); s = wt->cmd; while (*s) *t++ = *s++; } *t = '\0'; s = WorkTemp; if (*opnds) { O_MemoryCmd(lw,cmd,WorkTemp); s = NULL; } break; /* The COLUMN and POSITION commands are redundant but useful; they provide horizontal and vertical control, respectively. Because DrawWindow is adept, the single operand is fairly arbitrary. "COLUMN n" places the n-th character of each line at the left edge of the window, where 0 is the same as 1 and negative values provide blank spaces at the left edge. "POSITION n" places the line containing the n-th character of the file at the top of the window but also allows a fraction, e.g., "POS .5" moves the window to the center of the file. The POSITION display uses the displacement of the first character of the top line and includes the displacement of the bottom line and the size of the file. */ case oCOLUMN: if (! lw->dout) break; if (*opnds) { s = opnds; if (*s == '-' || *s == '+') ++s; for (cmd = 0; isdigit(*s); cmd = 10*cmd + (*s++ - '0')); if (*s) s = "invalid"; else { s = NULL; if (*opnds == '-') cmd = -cmd; lw->shft = (cmd > 0) ? cmd - 1 : cmd; }} else sprintf(s = lw->tmp,"%i", (lw->shft >= 0) ? lw->shft+1 : lw->shft); break; case oPOSITION: if (! lw->dout) break; if (*opnds) { double f, fd, fs; f = fd = 0.0; s = opnds; if (*s == '.') { ++s; fd = 1.; } while (isdigit(*s)) { f = 10.*f + (*s++ - '0'); fd *= .1; } fs = (double)(lw->eof - lw->base); if (fd) f = (f*fd)*fs; if (! *s && f < fs) { lw->top = lw->base + (size_t)f; lw->ytop = 0; lw->bot = s = NULL; } else s = "invalid"; } else sprintf(s = lw->tmp,"%li to %li of %li bytes ", (long)(lw->top - lw->base),(long)(lw->bot - lw->base), (long)(lw->eof - lw->base)); break; /* The ? command is display-only: the names of the application and window plus status information. If multithreaded, the state flags may be wrong in rare instances; the DONE state flag is set if appl returns, but the application may have started other threads. The application's date/time stamp is appended. */ case oWHOAMI: O_InputMgr(lw,ApplicationName,0); if (WindABH == ApplABH) O_InputMgr(lw,"ST",-1); if (State & sbSTOP) O_InputMgr(lw,"suspended",-1); if (State & sbDONE) O_InputMgr(lw,"done",-1); if (State & sbAPI) O_InputMgr(lw,"API",-1); O_InputMgr(lw,"::",-1); O_InputMgr(lw,lw->wndname,-1); sprintf(lw->tmp,"#%li",lw->SSN); O_InputMgr(lw,lw->tmp,-1); if (lw->apiarg) O_InputMgr(lw,"awaiting input",-1); if (lw->flags & bSTATIC) O_InputMgr(lw,"static",-1); s = lw->tmp; *(s+1) = *s = ':'; *(s+2) = ' '; O_Time(s+3,&(Work->mtime)); break; /* The YO command displays a line of the form YO <path name> <size>[B|C] [of <space>B] <modification time> where the B or C means bytes (binary) or characters. If no operand is given, the data is for the file associated with the window and the allocated space is included. Otherwise, the data is for the operand relative to the window's current directory and the space is omitted. Others are unnamed unless directed. Because this code inserts part of this information in the input directly, it's ignored if INPUT is off. */ case oYO: if (!(lw->flags & bINPUT)) break; if (*opnds) { lf = O_CreateVMF(lw->cwd,opnds,0,&s); if (! lf) break; O_InputMgr(lw,"yo",0); O_InputMgr(lw,lf->pn,-1); if (lf->flags & bNEW) { s = "doesn't exist."; O_DestroyVMF(lf); break; }} else if (! lw->dout) { s = "None!"; break; } else { lf = lw->dout; O_InputMgr(lw,lf->pn,-1); lf->size = (long)(lf->eof - lf->base); } s = lw->end; if (lf->flags & bDIR) { if (*(s-1) != PSC) *s++ = PSC; } t = (lf->flags & bHEX) ? " %liB" : " %liC"; s += sprintf(s,t,lf->size); if (! *opnds) s += sprintf(s," of %liB",lf->asize); *s++ = ' '; s += O_Time(s,&(lf->mtime)); lw->end = s; lw->fcur = lw->cur = lw->input; s = ""; if (*opnds) O_DestroyVMF(lf); break; /* The POLLSTER command creates a pollster for the specified file and is executed in Annotate, which only looks at the first operand and requires that the file look like a poll file. The TYPE mechanism is used to implement pollsters. */ case oPOLLSTER: if (*opnds) O_Annotate(lw,oLOAD,opnds,&s); else s = "file name?"; break; /* The ONLOOKER command creates an onlooker for the specified file. CreateWND is unlikely to fail, but the file remains loaded in any case. */ case oONLOOKER: if (*opnds) { lf = O_LoadVMF(lw->cwd,opnds,0,&s); if (! lf) break; if (! O_CreateWND(lf,NULL,bONLKR)) s = " Gone blind!"; } else s = "file name?"; break; /* The VMF command creates an unnamed VM file with the specified size and flags. The two operands may be given in any order. The file size must be of the form <integer>[p|b|k|P|B|K], where the letter indicates the unit (pages, bytes, or kilobytes). The file type operand must be {\r\n|crlf|pc} | {\n|lf|unix} | {hex|binary}, which allows for text or binary VM files and, for the former, the line termination format. The miscellaneous operand table is used to process this operand. If no operands are given, a minimal VM file with the system's line termination format is created. Other operands or extra operands are viewed as erroneous. An onlooker is created to display the new file. If the onlooker cannot be created, the VM file is destroyed because there is no mechanism for using an unnamed file. Any file associated with an onlooker is expanded automatically so the user can paste into the VM file created as long as memory is available. */ case oVMF: BEGIN long n, type; char *t; n = type = 0; for (s = NULL; ! s && *opnds; opnds += strlen(opnds) + 1) if (! n && isdigit(*(t = opnds))) { for (n = 0; isdigit(*t); n = 10*n + (*t++ - '0')); if (*t == 'p' || *t == 'P') { ++t; n = n << 12; } else if (*t == 'b' || *t == 'B') { ++t; ++n; } else if (*t == 'k' || *t == 'K') { ++t; n = n << 10; } else n = n << 10; if (*t) s = "Size?"; } else if (! type) { type = O_SetAttribute(lw,oNONE,opnds); if (type < 3 || type > 5) s = "Type?"; } else s = "operand?"; if (s) break; lf = O_CreateVMF(NULL,NULL,n,&s); if (! lf) break; if (type == 3) lf->flags |= bCRLF; else if (type == 4) lf->flags &= ~bCRLF; else if (type == 5) lf->flags |= bHEX; END /* In-line function to create a VM file.*/ alw = O_CreateWND(lf,NULL,bONLKR); if (! alw) { O_DestroyVMF(lf); s = " Gone blind!"; } break; /* The CD command does what one would expect, perhaps more as each window has its own current directory. */ case oCD: if (*opnds) { lf = O_CreateVMF(lw->cwd,opnds,0,&s); if (lf && lf->flags & bDIR) (void)strcpy(lw->cwd,lf->pn); else s = "is not a directory."; O_DestroyVMF(lf); } else s = lw->cwd; break; /* The LOAD and CONTINUE commands, the synonyms for < and <<, can be applied only to onlookers and others; they cannot be used with pollsters or an onlooker if the memory file is associated with it. Only the first operand, a file name, is processed. In an onlooker, these commands change the associated VM file: LOAD destroys the current VM file, but CONTINUE does not. In an other, i.e., an application window, these commands direct the input for the window and differ only in what happens when the end of the file is encountered: one and only one EOF is returned for LOAD, while input continues through the window for CONTINUE. Input direction is implemented using the din... components of the window struct and the window's EOF flag. The din file is deleted when the end is reached. If the operand is "-", input direction is terminated. The display versions are functional only in others and the command is changed to indicate the actual situation, e.g., if directed with <<, it will be displayed as "<< pathname" regardless of which command is used to solicit the information. The display includes the position of the read pointer and the size of the file. */ case oCONTINUE: case oLOAD: if (lw->flags & bPOLL || lw->dout == MemoryFile) { s = "not allowed."; break; } if (*opnds == '-' && ! *(opnds+1) && lw->flags & bOTHER) { if (lw->din) { lf = lw->din; lw->din = NULL; O_DestroyVMF(lf); } break; } if (*opnds) { if (lw->flags & bONLKR) { lf = O_LoadVMF(lw->cwd,opnds,0,&s); if (! lf) break; if (lw->dout && !((lw->dout)->flags & bUPDATE) && O_Valid(lw->top,lw->base,lw->eof)) (lw->dout)->posn = lw->top - (lw->dout)->base; lf = O_AssociateVMF(lw,lf); if (lf && cmd == oLOAD) O_DestroyVMF(lf); } else if (! lw->din) { lf = O_LoadVMF(lw->cwd,opnds,0,&s); if (! lf) break; lf->flags &= ~bSAVE; lf->posn = 0; lw->din = lf; lw->dinptr = lw->dinbase = lf->base; lw->dineof = lf->eof; lw->flags &= ~bEOF; if (cmd == oLOAD) lw->flags |= bEOF; if (lw->apiarg) { O_APIEvent(0); }} else s = " input already directed."; } else if (lw->din) { O_InputMgr(lw,((lw->flags & bEOF) ? "<" : "<<"),0); O_InputMgr(lw,(lw->din)->pn,-1); sprintf(s = lw->tmp,"@%liB of %liB", (long)(lw->dinptr - lw->dinbase), (long)(lw->dineof - lw->dinbase)); } else s = " None!"; break; /* The STORE and APPEND commands, the synonyms for > and >>, can be applied only to onlookers and others; they cannot be used with pollsters or an onlooker if the memory file is associated with it. Only the first operand, a file name, is processed. In an onlooker, these commands store in or append to the specified file in the file system the portion of the VM file associated with the window. The operation is performed immediately. StoreVMF fails if ROFS (read-only file system) is set and the file exists; ROFS is set only in the Onlooker application and can be overridden. Marvelous! In an other, these commands direct the output to a file, new or existing, in the file system; but the command does not perform any I/O operations and output continues to appear in the window. The actual output operations will be performed when and if a portion of the VM file must be discarded. Output direction is implemented by setting the file name in the VM file struct and by using the posn and UPDATE and STORE flags. If the operand is "-", output direction is terminated, but the path name is NOT eliminated from the file struct so that YO continues to display it. The display versions are functional only in others and the command is changed to indicate the actual situation, i.e., "> <filename>" if no writes have occurred and ">> <filename>" otherwise. */ case oSTORE: case oAPPEND: if (lw->flags & bPOLL || lw->dout == MemoryFile) { s = "not allowed."; break; } if (*opnds == '-' && ! *(opnds+1) && lw->flags & bOTHER) { if (lw->dout) (lw->dout)->flags &= ~bUPDATE; break; } if (*opnds) { if (! lw->dout) { s = " No file."; break; } lf = O_CreateVMF(lw->cwd,opnds,0,&s); if (! lf) break; if (lw->flags & bONLKR) { lf->base = lw->base; lf->eof = lw->eof; k = strcmp(opnds + strlen(opnds) + 1,"Marvelous"); k = ! k && State & sbROFS; if (k) State ^= sbROFS; O_StoreVMF(lf,cmd,&s); if (k) State |= sbROFS; } else { if (!((lw->dout)->flags & bUPDATE)) (lw->dout)->posn = 0; (lw->dout)->flags |= bUPDATE|((cmd != oSTORE) ? bSTORE : 0); (void)strcpy((lw->dout)->pn,lf->pn); } O_DestroyVMF(lf); } else if (lw->dout && ((lw->dout)->flags & bUPDATE)) { O_InputMgr(lw,(((lw->dout)->flags & bSTORE) ? ">>" : ">"),0); s = (lw->dout)->pn; } else s = "None!"; break; /* The CLOSE, EXIT, and STOP commands ignore all operands and provide the obvious functionality. Closing the last window has the same effect as an EXIT command. The EXIT and STOP commands differ only in their treatment of the memory file; a STOP explicitly sets its UPDATE flag off so that the memory file will not be updated. STOP is supposed to fall through to EXIT. */ case oCLOSE: if (lw->flags & bCLOSE) s = "Not hardly."; else PostMessage(lw->wnd,WM_CLOSE,0,0); break; case oSTOP: MemoryFile->flags &= ~bUPDATE; case oEXIT: PostQuitMessage(0); break; /* The MEMORY command processes the first operand if any and ignores the remainder; the miscellaneous operand table is used to match the operand. The UPDATE status of the memory file is set based on the operand. If no operand is specified and the memory file is associated with the window, the update status of the memory file is displayed; in all other cases, an onlooker is created for the memory file. */ case oMEMORY: if (! MemoryFile) { s = " Amnesia!"; break; } if (*opnds) { cmd = (int)O_SetAttribute(NULL,oNONE,opnds); if (cmd == 1) MemoryFile->flags |= bUPDATE; else if (cmd == 2) MemoryFile->flags &= ~bUPDATE; else s = "invalid"; } else if (lw->dout == MemoryFile) s = (MemoryFile->flags & bUPDATE) ? "on" : "off"; else O_CreateWND(MemoryFile,NULL,bONLKR); break; /* The NB command is used to set the TYPE flag, which has significant consequences for all input entered through the window but affects each window subclass differently. While the TYPE flag is set, input is solicited continuously and appended to the associated VM file. The TYPE flag is turned off by an Esc (ESCAPE) or Ctrl+Z (EOF) or if the application requests input through the window. Because users are more adaptable than applications, the application always has the right of way. If the application is soliciting input through the window when the NB command is entered, it's suggested that the user try again later. After an NB, pollsters resume soliciting input, onlookers start soliciting input that is appended to the associated VM file, and others solicit input for a nota bene (N.B.) to the output stream. */ case oTYPE: if (lw->apiarg) s = " Later!"; else if (! O_Annotate(lw,oTYPE,opnds,NULL)) s = "unsupported"; break; /* The FIND command provides the ability to find a string in the VM file associated with a window, but its implementation is a bit unusual. For HEX files, the string need not fit within a single HEX line (16 bytes); for text files, the string must be contained in a single line because there is no mechanism for embedding line termination in the string. Unlike other commands, this command doesn't disappear after it's executed; FIND's stay around until you hit Esc so that successive occurrences can be found easily. But, if the FIND fails, it's an error, "failed", so that the input status is changed to INFO and the FIND is deleted if you press Enter. Thus, if successful, this code returns directly to avoid the epilogue, which would dutifully eliminate the input. Finally, the search itself is a bit unusual in that it is neither a substring nor word search. Specifically, the rule is that the characters preceding and following the string must be unlike the first and last characters of the string itself, respectively. In this case, 'like' is based on the find table, which is initialized here. But, if the first (last) character is a question mark (?), it's discarded and the above requirement is not imposed. The search can be either forward or backward: forward unless a second operand is given and it begins with a lowercase 'b'. The search starts with the line after or before the top line in the window and moves the window so the line containing the string is at the top of the window. */ case oFIND: if (! lw->dout || ! *opnds) { s = "?????"; break; } BEGIN char *b, *e; int dir, chk; if (! FindTable[0]) BEGIN for (k = 0; k < 256; ++k) { char c; c = '?'; if (isalnum(k)) c = 'a'; else if (ispunct(k)) c = '.'; else if (isspace(k)) c = 's'; FindTable[k] = c; } FindTable[(unsigned)'_'] = 'a'; END k = strlen(opnds); dir = (*(opnds+k+1) == 'b') ? -1 : 1; chk = *(opnds+k-1) == '?' && k > 1; if (chk) --k; if (*opnds == '?' && k > 1) { --k; ++opnds; chk += 2; } chk ^= 3; b = lw->base; e = lw->eof - k; s = lw->top; if (dir > 0) { if (lw->flags & bHEX) s += 15; else while (*s != '\n') ++s; } for ( s += dir; O_Valid(s,b,e); s += dir) { if (*s != *opnds || strncmp(s,opnds,k)) continue; if (chk & 2 && s != lw->base && FindTable[(unsigned)*s] == FindTable[(unsigned)*(s-1)]) continue; if (chk & 1 && FindTable[(unsigned)*(s+k-1)] == FindTable[(unsigned)*(s+k)]) continue; lw->bot = NULL; lw->top = s; lw->ytop = 0; s = NULL; return 2; } END s = "failed."; break; /* The CF command compares the VM files associated with two windows, the window through which the command is entered and the window with the SSN specified as the operand. The comparison is always character-by-character (byte-by-byte) regardless of the types of the files. Comparison of files with different types, i.e., one text and one HEX, does not work well because the compare always starts with the first character of the second lines displayed in the windows and text lines and hex lines differ. The comparison is based on the portion of the VM file associated with the window. Like the FIND command, the CF command does not disappear. In this case, the input CF command is replaced by cf [SSN] <[char]!=[char]> @ col [number]. All whitespace characters are displayed as a blank so that it is possible to have "< != >" if the lines are the same except for terminal whitespace characters. Although informational in nature, the result has input status so that pressing Enter continues the comparison; CF looks only at the first operand. When the end of either file is reached, EOF is appended to the input and the input has informational status, i.e., pressing Enter deletes it. */ case oCF: if (! lw->dout) { s = "?????"; break; } s = "SSN?"; for (cmd = 0; isdigit(*opnds); cmd = 10*cmd + (*opnds++ - '0')); if (*opnds || (int)(lw->SSN) == cmd) break; for (alw = WndList; alw && alw->SSN != cmd; alw = alw->next); if (! alw || ! alw->dout) break; alw->flags &= ~bINPUT; s = lw->top; t = alw->top; s = O_LineAfter(lw,lw->top); t = O_LineAfter(alw,alw->top); if (s == lw->top || t == alw->top) { s = "EOF"; break; } CARET_OFF; k = sprintf(lw->input,"cf %li",alw->SSN); lw->end = lw->cur = (lw->fcur = lw->input) + k; while (s < lw->eof && t < alw->eof) { if (*s == *t) { ++s; ++t; continue; } alw->top = t; alw->bot = NULL; alw->ytop = 0; O_DrawWindow(alw); k = (int)(t - alw->top) + 1; lw->top = s; lw->bot = NULL; lw->ytop = 0; s = lw->cur; *s++ = ' '; *s++ = '<'; *s++ = (char)((isspace(*(lw->top))) ? ' ' : *(lw->top)); *s++ = ' '; *s++ = '!'; *s++ = '='; *s++ = ' '; *s++ = (char)((isspace(*t)) ? ' ' : *t); *s++ = '>'; lw->end = lw->cur = s + sprintf(s," @ col %i",k); return 2; } alw->bot = --t; lw->bot = --s; alw->ybot = lw->ybot = 0; O_DrawWindow(alw); O_InputMgr(lw,"EOF",-1); return 2; /* A tautology. */ default: s = "not implemented."; break; END /* switch on command index, cmd.*/ /* Command Epilogue The epilogue is responsible for processing errors, managing the input if any, and returning the appropriate value. If s points to a string (cryptic error comment or message), it's appended to the current input line, and the input is given INFO status. When appending this string, InputMgr doesn't change the caret position, which will usually leave it immediately after or before the command operand, which should be convenient for the user. If the user presses Enter without changing the input, it's deleted because it has INFO status; if the user changes it, then the input loses INFO status and becomes input. If s is NULL, the command was successfully executed, but there are two situations. If input is being solicited, the input should be re-initialized; otherwise, if the INPUT flag is on, the command was entered by the user and should be eliminated, i.e., a window update. */ if (s) { O_InputMgr(lw,s,-1); return (lw->flags & bINPUT) ? 1 : 0; } if (lw->apiarg || lw->flags & bTYPE) O_InputMgr(lw,NULL,0); else if (lw->flags & bINPUT) lw->flags &= ~(bINPUT|bINFO); return 2; END /*---------------------------------------------------------------- Annotate (window, operation, operand) implements the annotation capability, i.e., pollsters and the NB command. This function contains three related functions: Annotate (window,TYPE,*) implements NB commands and is called only from Command, Annotate (window,STORE,*) implements KeyFcn...ENTER if the TYPE flag is set, and Annotate (window|NULL,LOAD,opnd) creates pollsters. These chunks of code are collected here for clarity. The TYPE flag is the basic mechanism for implementing pollsters, appending input text to the VM file associated with an onlooker, and nota bene (N.B.) annotations for others. While this flag is set, input is continuously solicited through the window and is appended to the VM file associated with it. The TYPE flag is reset in KeyFcn by ESCAPE and EOF or if the application solicits input through the window. Because the application always has the right of way, you should suspend an application while entering a nota bene in an active I/O stream. */ WND *O_Annotate (WND *lw, int op, char *opnd, char **e) BEGIN VMF *lf; long k; char *s, *t; switch (op) BEGIN /* Annotate (lw,TYPE,*) implements the NB command for all subclasses and is called from Command...TYPE. An NB while an NB is operative is ignored. Since the purpose of an annotation is to append subsequent user input to the VM file associated with the window, an NB is rejected if there is no VM file or it's a HEX file. The last argument is ignored. For all window subclasses, some initial processing of the VM file is provided: the entire VM file is associated with the window, a terminal Ctrl+Z is deleted, and a \n is appended if necessary. The first ensures that the user can see the new data, but it would suffice to change only eof. The Ctrl+Z should never occur. Adding the \n is advisable but may have side effects for output streams. The preceding suffices for pollsters and onlookers because Command will initiate input when it finds the TYPE flag set. For others, a line of the form N.B. <application name> [date/time] is appended. This line is constructed in the tmp component, which should be free. If AppendVMF fails, the NB fails. Subsequent user input is appended after this header and indented. */ case oTYPE: if (lw->flags & bTYPE) return lw; lf = lw->dout; if (! lf || lf->flags & bHEX) return NULL; lw->base = lf->base; lw->eof = lf->eof; if (lf->base != lf->eof && *((lf->eof)-1) == 26) { lf->flags |= bEOF; *(--(lf->eof)) = '\n'; } if (lf->base != lf->eof && *((lf->eof)-1) != '\n') O_AppendVMF(lw,NULL,"\n",1); lw->flags |= bTYPE; if (lw->flags & (bPOLL|bONLKR)) return lw; s = strcpy(lw->tmp,"N.B. ") + 6; t = ApplicationName; while (*t) *s++ = *t++; *s++ = ' '; *s++ = '['; s += O_Time(s,NULL); *s++ = ']'; *s++ = '\n'; s = O_AppendVMF(lw,NULL,lw->tmp,s - lw->tmp); if (s) { lw->bot = s; ++(lw->ybot); return lw; } else { lw->flags &= ~bTYPE; return NULL; } /* Annotate (lw,STORE,*) is called from KeyFcn...ENTER when the TYPE flag is set. The last argument is ignored. For pollsters and others, two blanks are prefixed to the input to enhance the visual presentation; for onlookers, it's appended without change. If AppendVMF fails, the NB is terminated, and the input is deleted. Normally, the TYPE flag remains set to continue soliciting input for the annotation. */ case oSTORE: if (lw->flags & (bPOLL|bOTHER)) O_AppendVMF(lw,NULL," ",2); *(lw->end)++ = '\n'; s = O_AppendVMF(lw,NULL,lw->input,(long)(lw->end - lw->input)); if (! s) { lw->flags &= ~(bTYPE|bINPUT); return NULL; } lw->bot = s; ++(lw->ybot); O_InputMgr(lw,NULL,0); return lw; /* Annotate (window|NULL,LOAD,file name) creates a pollster for the specified poll file and returns it or NULL. This function is used to implement the POLLSTER command, creation of a pollster by Open, and the Pollster application. For commands, the window is given so that the file name is taken relative to the window's current directory; otherwise, it's NULL and the POO current directory is used. Because additional space is requested, LoadVMF returns a file only if the name corresponds to a text file. As for an NB command, some initial processing is provided. LoadVMF always deletes a terminal Ctrl+Z from text files, but appending a \n may be appropriate. The settings of the posn and STORE flag, which determine how the file is updated, are based on the EOF flag. The whole file is updated if a Ctrl+Z was deleted. The poll file test is not essential and can be aggravating, but it's prudent to have some reason to believe that it's a poll file since the disk file will be changed. If LoadVMF returns "doesn't exist.", the file is created. */ case oLOAD: lf = O_LoadVMF(t = (lw) ? lw->cwd : NULL,opnd,4095,&s); if (lf) { if (lf->base != lf->eof && *((lf->eof)-1) != '\n') O_AppendVMF(NULL,lf,"\n",1); if (lf->flags & bEOF) lf->posn = 0; else { lf->posn = lf->eof - lf->base; lf->flags |= bSTORE; } s = lf->eof; do for (--s; s > lf->base && *(s-1) != '\n'; --s); while (s > lf->base && *s == ' '); for (t = s+1; *t != '\n' && *t != ']'; ++t); if (*s != '[' || *t != ']') { s = "not a poll file."; break; }} else { if (*s == 'd') lf = O_CreateVMF(t,opnd,4095,&s); if (! lf) break; } /* The header line is of the form [current date/time] <file position> where the '[' is the first character of the line and the position is the displacement of the header in the file, i.e., precisely the value needed in a POSITION command. The format of the date/time is determined solely by Time. If the pollster is created, the UPDATE flag is set; the posn and STORE flags were set above. Neither appending the header nor creating the window should fail. If they occur in a POLLSTER command, the command will be ignored. */ s = WorkTemp; k = lf->eof - lf->base; *s++ = '['; s += O_Time(s,NULL); *s = ']'; t = s + 10; do { *t-- = (char)((k % 10) + '0'); k /= 10; } while (k); while (s < t) *t-- = ' '; s += 11; *s++ = '\n'; s = O_AppendVMF(NULL,lf,WorkTemp,s - WorkTemp); if (! s) break; lw = O_CreateWND(lf,s = NULL,bPOLL); if (! lw) break; lf->flags |= bUPDATE; return lw; END if (op == oLOAD) { if (lf) O_DestroyVMF(lf); if (e) *e = s; } return NULL; END /*---------------------------------------------------------------- ParseCommand (begin, end, temp) parses the character string [begin,end) relative to the command syntax described below and returns the command index, i.e., NONE,... or FKFB+n, 1 <= n <= 48, and the blank-delimited operands in temp, i.e., a sequence of null terminated strings followed by a null string, a multistring. If the command is not recognized or is ambiguous, -1 is returned and a cryptic comment is returned in the operand buffer, temp. If temp is the NULL pointer, no operands are parsed and, obviously, no comments are returned. If NONE, which is in the command table, is returned, temp may be unchanged. The command consists of the characters from begin (inclusive) to end (exclusive) but a terminal \r and all leading and trailing blanks are discarded. The command word, the first blank-delimited string, is limited to twelve characters and must be an initial substring of a unique entry in the command table, [ < | << | > | >> ], which are synonyms for LOAD, CONTINUE, STORE, and APPEND, or {f|F}<integer>, a function key command. The command operands are the blank-delimited strings after the command word, but the single operand in the function key commands F1...F48 is the longest blank-delimited string after the command. Blanks can be embedded in an operand by enclosing it in quotes (") or primes ('), but there is no provision for such an operand to contain its delimiter. Quoted strings may contain primes, and conversely. If an operand starts with a quote or prime and the terminal quote or prime isn't found, the operand consists of the remainder of the input. Null operands, e.g., "", are discarded without comment. The operands are treated as case sensitive here but may be treated as case insensitive when actually processed. */ int O_ParseCommand (char *s, char *end, char *tmp) BEGIN WORDTAB *wt; size_t len; int index; char *t, *tbeg; /* After verifying that the arguments describe a string, leading and trailing blanks are trimmed. If nothing is left, NONE is returned, and the operand buffer is unchanged. */ if (! O_Valid(s,s,end)) return oNONE; while (*s == ' ' && s < end) ++s; if (*(end-1) == '\r') --end; while (s < end && *(end-1) == ' ') --end; if (s >= end) return oNONE; len = (size_t)(end - s); /* I/O direction using the < and > characters is a de facto standard although the meaning of << is ambiguous. The meaning implemented here is natural and useful even though it may not be viable in other (legacy) contexts. For these commands, the operand scan starts immediately after the < or > so that the blank separator is not required. */ if (*s == '<' || *s == '>') BEGIN if (len > 1 && *s == *(s+1)) { index = (*s == '<') ? oCONTINUE : oAPPEND; s += 2; } else { index = (*s == '<') ? oLOAD : oSTORE; ++s; } END /* Any line beginning with "{f|F}<digit>..." is parsed as a function key command to avoid overpopulation in the command table and to provide the longest blank-delimited string as the only operand. The return is direct to avoid the usual operand processing. In the operand, embedded blanks require no special attention; but, to obtain leading (trailing) blanks, an initial (terminal) quote (") or prime (') is required. Thus, an initial or terminal quote or prime is deleted, but the string is not examined otherwise so that embedded quotes and primes require no special notation. All trailing blanks were discarded above. */ else if (len > 1 && (*s=='f' || *s=='F') && isdigit(*(s+1))) BEGIN for (++s, index = 0; s < end && isdigit (*s);) index = 10*index + (*s++ - '0'); if (index < 1 || index > 48) { if (tmp) (void)strcpy(tmp," Function key?"); return -1; } if (tmp) { while (s < end && *s == ' ') ++s; if (*s == '"' || *s == '\'') ++s; if (*(end-1) == '"' || *(end-1) == '\'') --end; while (s < end) *tmp++ = *s++; *tmp++ = '\0'; *tmp = '\0'; } return index + FKFB; END /* The line does not correspond to any of the special commands so the command table is its last chance. If a new command longer than 12 characters is inserted in this table, this code must be changed because it ignores longer command words. Further, it's assumed that all alphabetics in the command table are in lower case. Thus, the initial blank-delimited string is moved and converted to lower case before scanning the command table. A nonalphabetic character could trigger an error but would unnecessarily restrict the table. The search of the table accepts any match but requires that there be only one. Thus, a single character suffices for a few commands, two are required for some, and three are needed for a couple. */ else BEGIN for (t = s; s < end && *s != ' '; ++s); len = (size_t)(s - t); if (len > 12) { if (tmp) (void)strcpy(tmp," Command?"); return -1; } *(strncpy(tbeg = WorkTemp,t,len) + len) = '\0'; for (t = tbeg; *t; ++t) if (isalpha(*t)) *t = (char)tolower(*t); for (index = -1, wt = CommandTable; wt->cmd; ++wt) if (strlen(wt->cmd) >= len && ! strncmp(tbeg,wt->cmd,len)) { if (index < 0) index = (int)wt->index; else { if (tmp) (void)strcpy(tmp," Ambiguous command!"); return -1; }} if (index < 0) { if (tmp) (void)strcpy(tmp," Command?"); return -1; } END /* If tmp is NULL, no operands are wanted by the caller so that it suffices to return the command index. In most cases, tmp is the tmp component of a window struct. */ if (! tmp) return index; /* It remains to parse the blank-delimited operands from (s,end], the remainder of the line. They are returned in tmp, i.e., <first operand>\0...<last operand>\0\0. The length check could be deleted because the input line is less than MAXLENINPUT and the multistring requires no more space than the input. */ for (tbeg = tmp; s < end; ++s) BEGIN char c; while (s < end && *s == ' ') ++s; if (s >= end) break; if (*s == '"' || *s == '\'') { c = *s; ++s; } else c = ' '; for (t = s; s < end && *s != c; ++s); len = (size_t)(s - t); if ((int)(tmp - tbeg) + len + 2 >= MAXLENOPNDS) break; if (len) { while (len-- > 0) *tmp++ = *t++; *tmp++ = '\0'; } END *tmp++ = '\0'; *tmp = '\0'; return index; END /*---------------------------------------------------------------- ParseGeometry (window, geometry) parses an X style geometry string of the form [=]<width>{x|X}<height>{+|-}<x-origin>{+|-}<y-origin> in a permissive but robust manner and returns 1 (successful) or 0. If the geometry string is parsed without error, the new geometry is returned in the components ww, wh, px, and py; otherwise, they are unchanged. The origin values returned are screen coordinates relative to the upper left corner of the screen, (0,0). All geometry elements are optional, but the syntax prevents the recognition of a y-origin value unless the x-origin is provided. Because the new geometry is initialized from the window struct, it's implicit that the struct contains the current geometry so that omitted components cause no change. CreateWND and Command ensure that these components are set properly. Nevertheless, this function always positions the window so it's within the screen, a requirement that is imposed even if the geometry is a null string. The geometry string is parsed by looking for what is expected and using it if it's syntactically correct. Thus, syntax errors are not discovered until the very end. For example, "65" changes only the width, "x20" only the height, while "xc" and "c" change only the position. */ int O_ParseGeometry (WND *lw, char *geostr) BEGIN char *g, *sx, *sy; int w, h, x, y, sw, sh; /* The units for width and height may be either characters and lines or pixels so that the font sizes must be known. Thus, processing of the geometry is deferred until the font is set when creating a window. The screen sizes are used to restrict the position so that the window lies entirely within the screen, which may change the requested size or position. */ if (lw->tw < 1 || lw->th < 1) O_GetFontSize(lw); sx = sy = "+"; x = lw->px; y = lw->py; w = lw->ww; h = lw->wh; sw = GetSystemMetrics(SM_CXSCREEN); sh = GetSystemMetrics(SM_CYSCREEN); /* Leading blanks should have been trimmed, but there is no harm in making sure as they would lead to a syntax error. An initial '=' is ignored to be consistent with X. The window size can take the form [<integer width>|m|M][x|X][<integer height>|m|M]. The m|M means the maximum allowable if a border is included; if done by the windowing system, a maximized window usually has no border. Width and height values less than 100 are assumed to be in units of characters and lines and pixels otherwise. */ if (*geostr) BEGIN g = geostr; while (*g && (*g == ' ')) ++g; if (*g == '=') ++g; if (isdigit(*g)) { for (w = 0; isdigit(*g); w = 10*w + (*g++ - '0')); if (w < 100) w *= lw->tw; } else if (*g == 'm' || *g == 'M') { w = sw - lw->fw; ++g; } if (*g == 'x' || *g == 'X') ++g; if (isdigit(*g)) { for (h = 0; isdigit(*g); h = 10*h + (*g++ - '0')); if (h < 100) h *= lw->th; } else if (*g == 'm' || *g == 'M') { h = sh - lw->fh; ++g; } /* The origin values are screen coordinates relative to the corner of the screen and window implied by the sign characters, e.g., upper left for +x+y and lower right for -x-y. For example, -0+0 places the upper right corner of the window at the upper right corner of the screen, while +5-4 places the lower left corner of the window 5 pixels from the left edge and 4 up from the bottom. The position may have the form [+|-][integer|c|C][+|-][integer|c|C] | c | C. If the c|C is used, the sign is ignored. If the position is simply c|C, the window is centered in the screen, i.e., the same as +c+c. If the parse does not end on the null at the end of the geometry string, it's an error. */ if (*g == '+' || *g == '-') { sx = g++; if (isdigit(*g)) for (x = 0; isdigit(*g); x = 10*x + (*g++ - '0')); else if (*g == 'c' || *g == 'C') { x = (sw - lw->fw - w) >> 1; ++g; } if (*g == '+' || *g == '-') { sy = g++; if (isdigit(*g)) for (y = 0; isdigit(*g); y = 10*y + (*g++ - '0')); else if (*g == 'c' || *g == 'C') { y = (sh - lw->fh - h) >> 1; ++g; }}} else if (*g == 'c' || *g == 'C') { ++g; x = (sw - w) >> 1; y = (sh - h) >> 1; } if (*g) return 0; END /* This section imposes a few rules on the new geometry: the window sizes are adjusted if too small or large, and then the origin is adjusted so that the window will be within the screen. */ if (w < 4*lw->th) w = 4*lw->th; if (w > sw - lw->fw) w = sw - lw->fw; lw->ww = w; w += lw->fw; if (h < lw->th) h = lw->th; if (h > sh - lw->fh) h = sh - lw->fh; lw->wh = h; h += lw->fh; if (x > sw - w) x = 0; lw->px = (*sx == '-') ? sw - w - x : x; if (y > sh - h) y = 0; lw->py = (*sy == '-') ? sh - h - y : y; return 1; END /*================================================================ VIRTUAL MEMORY FILE FACILITY The VM file facility consists of the functions in this section, the VM file struct, several defined constants for system-dependent features, and several global variables. CreateVMF creates a VM file structure, generates the complete path name, and sets various flags; DestroyVMF releases the VM file structure allocated for a VM file if appropriate; LoadVMF returns an existing, named VM file or loads a file or directory from the file system into a VM file, StoreVMF stores or appends a VM file to a new or existing file in the file system; ExpandVMF expands or truncates the data space available for a VM file; AppendVMF appends data to a VM file or allocates space for appending data to a VM file; and AssociateVMF associates a VM file with a window. The VM file struct is described in DATA STRUCTURES AND ASSOCIATED DEFINED CONSTANTS. Its flags can be summarized as follows: FILE regular file, DIR directory (one file name per line unsorted), CRLF \r\n rather than \n line termination, HEX binary file, i.e., not text, EOF Ctrl+Z (26) trimmed from the disk file, NEW path name is not the name of a disk file, STAT known to file system as a regular file or directory, LOAD loaded from the disk file of the same name, UPDATE output direction to the disk file in effect, STORE partially stored (see UPDATE), TRUNC buffer management policy, SAVE save file position in the memory file, and LOCK locked against destruction. The system-dependent defined constants DEFAULTFILEFORMAT, PSC and PSCA (path name separation character and alternate), MAXLENPATH, and MAXLENFILE, and variables FileList, LFSize, and the mtime and pn components of Work belong to this facility. The UCPN and ROFS state flags are also used. The set of VM files is maintained in a singly linked list that is anchored by the internal variable FileList. Because the VM file struct is small, aside from the space reserved for the path name, and has no value without the data itself, a single block of memory is allocated for holding both the file and the struct. The data is placed at the beginning of this block and is likely page aligned; all I/O operations are performed in page size blocks. The VM file struct is placed at the end of the block; the space for the struct is rounded up in units of 64 bytes (see LFSize) and is probably cache aligned, i.e., on a cache boundary and smaller than a cache line aside from the path name. Only CreateVMF and DestroyVMF are aware of this memory allocation tactic. All I/O operations are done in binary so that the I/O software simply moves bytes. Thus, a VM file is a copy of the corresponding disk file, and conversely, except that a terminal Ctrl+Z, if any, is discarded and a \n is appended. This final \n is not part of the VM file but ensures that a scan for the next \n terminates if started from within the file. To handle both Unix and PC files, the line termination format of a file is indicated by the CRLF flag. When a file is loaded from the file system, LoadVMF determines whether it contains binary or textual data and, in the latter case, its line termination. The CRLF flag is used only when appending data to a text file; NEW files use the system's format, DEFAULTFILEFORMAT. When reading VM files, either format or an admixture of them is permissible and invisible to the application. Thus files need not be converted even if data is appended to them. All path names are generated by CreateVMF. The heuristic approach determines whether the name is absolute or relative, inserts the current directory or its drive letter as appropriate, processes both Unix and Windows environmental variables, converts alternate path name separation characters, and may convert the alphabetics to uppercase. All system dependencies are implemented based on defined constants or values derived from them. This software and the file system should treat alphabetic case in path names in the same manner. In this software, the treatment is determined by the value of UPPERCASE...NAMES, which should be set based on the file system. In Windows and OS/2, it may be either 0 or 1; in Unix, it should be 0 because names are case sensitive. The software identifies VM files by comparing complete path names AND mtime values. All VM files associated with an other have the same mtime value, namely, the time the application started. Files loaded from the file system use the last modification time. Thus, it's possible to have two VM files for the same disk file if it's modified between references. Likewise, a file to which output is directed is distinct from the VM file from which it's directed. The data in a VM file is completely defined by the base and eof components in the VM file struct. When associated with a window, these values are copied to the base and eof components in the window struct, but the user can restrict the window to a subfile by explicitly changing these components in the window struct and can subsequently restore them from the VM file struct. The initial current directory obtained from the system, Work->pn, is absolute. Since all POO directory changes are made by using CreateVMF to build the complete path name, window directories are absolute. Thus, all path names are absolute, theoretically. ------------------------------------------------------------------ CreateVMF (cwd, filename, init_size, errptr) generates the complete path name for a VM file from the cwd and filename arguments, allocates virtual memory for the VM file, and returns the struct. If cwd is NULL, the POO current directory is used. If filename is NULL or the file system doesn't recognize the complete path name but does recognize the path as a directory, the struct components are pn the null string or complete path name, size zero, mtime the application start time, and flags NEW | FILE | DEFAULTFILEFORMAT, i.e., for an unnamed VM file or a nonexistent disk file. If the complete path name is that of a regular file or directory in the file system, they are pn the complete path name, size the number of bytes in the file, mtime the last modification time, and flags STAT | {FILE | DIR} | DEFAULTFILEFORMAT. In both cases, the base and eof pointers are equal, and xsize and posn are zero. If a struct is returned, it's in the file list and should be deleted using DestroyVMF. Success or failure can be determined from either the return or the last argument: if one is NULL, the other is not. The cryptic strings returned are: " No memory!" if memory is not available, "is too large." if the file is unreasonably large, " Name too long." if the path length exceeds its maximum, " Undef'd var!" if an undefined variable occurs, "Erroneous .." if there is no prior path component, "is not a file or directory?" if the path name corresponds to neither a regular file nor a directory, and "path error." if the system doesn't recognize the name or path. */ VMF *O_CreateVMF (char *cwd, char *name, long rsize, char **e) BEGIN VM_HNDL hndl; VM_SIZE size; VMF *lf; size_t k; char *p, *s, *t, *pn; /* The first problem, acquiring space in which the complete path name can be constructed, is resolved by obtaining space for the file itself and then using its data space for the construction. Thus, if necessary, the size is increased to LFSize, which exceeds the maximum path length. Nevertheless, a truely malicious user can engender a problem. This tactic has the added benefit that the creation of an unnamed file is completed almost immediately in a natural way. */ size = ((unsigned long)rsize > LFSize) ? rsize : LFSize; size = ((size + LFSize + 4095) >> 12) << 12; if (size != (size_t)size) { *e = "is too large."; return NULL; } VM_ACQUIRE(hndl,size); s = (char *)VM_POINTER(hndl); if (! s) { *e = " No memory!"; return NULL; } *e = NULL; lf = (VMF *)(s + (size_t)(size - LFSize)); lf->asize = size - LFSize; lf->memhndl = hndl; lf->flags = bNEW|bFILE|(DEFAULTFILEFORMAT & bCRLF); lf->mtime = Work->mtime; lf->base = lf->eof = s; *s = '\n'; lf->posn = lf->size = lf->xsize = 0; if (! name || ! *name) { *(lf->pn) = '\0'; lf->next = FileList; return (FileList = lf); } /* The heuristic approach for constructing the complete path name is designed to handle both Unix and PC style names. Although a drive letter may be inserted, one is never deleted. Consequently, for portability, absolute paths should begin with a PSC. First, an initial path name is constructed from the filename and cwd strings, the latter if the file name looks relative. In the process, Unix or PC variables are replaced by their values. Before starting the scan of the file name, the cwd argument is set to the POO current directory if NULL, and the variable t is set NULL to show that a variable is not being processed. */ if (! cwd) cwd = Work->pn; t = NULL; while (*name) BEGIN /* Variables within the file name in either the Unix-like notation of ${...} or $...PSC or PC notation of %...% are replaced if defined. An undefined variable is viewed as an error, a feature required by MemoryLoad. If this code is changed to ignore undefined variables, it should do so only if the memory file exists. Null names, e.g., %% or ${}, are not processed. The variable name is moved into the space in which the path name is being constructed because it must be null terminated. Since PC variables appear to require upper case, the name is converted if the POO directory appears PC-ish. */ if (*name == '%' || *name == '$') { p = s; t = name + 1; if (*name == '%') for (; *t; *p++ = *t++) { if (*t == '%') { ++t; break; }} else if (*t == '{') for (++t; *t; *p++ = *t++) { if (*t == '}') { ++t; break; }} else while (*t && *t != PSC && *t != PSCA) *p++ = *t++; if (p != s) { pn = t; *p = '\0'; if (*(Work->pn+1) == ':') for (p = s; *p; ++p) if (islower(*p)) *p = (char)toupper((int)*p); if (O_GetEnv(s,t)) { *e = " Undef'd var!"; VM_RELEASE(hndl); return NULL; }} else t = NULL; } /* The value of size is nonzero because it was just used to allocate space for the VM file. Here, it's simply a convenient mechanism for catching the first character, which requires special care. The decision to prefix the current directory/drive letter is based on the first character of the file name and the current directory, i.e., whether they look PC-ish. Because this code handles both Unix and PC style names, there are three cases: a name beginning with a PSC, a name beginning with a drive letter, and a relative name. The current directory is prefixed to the file name only in the third case, but its drive letter is used in the first if the directory is PC-ish. */ if (size) { size = 0; p = (t) ? t : name; if (*p == PSC || *p == PSCA) { if (isalpha(*cwd) && *(cwd+1) == ':') { *s++ = *cwd; *s++ = ':'; }} else if (isalpha(*p) && *(p+1) == ':') { *s++ = *p++; *s++ = *p++; if (t) t = p; else name = p; if (*p != PSC && *p != PSCA) *s++ = PSC; } else { while (*cwd) *s++ = *cwd++; *s++ = PSC; }} /* If t is not NULL, it points to the value of a variable, and pn is the character of the file name where the scan must be resumed; otherwise, the current character is next. */ if (t) { while (*t) *s++ = *t++; t = NULL; name = pn; } else *s++ = *name++; END /* A PSC is added so that paths ending in a PSC, PSC., or PSC.. don't require special handling; it's removed later. To simplify the processing of these special sequences, PSCAs are converted to PSCs. Because this is done here rather than during the scan, PSCAs may be used in variables. This opportunity is taken to convert the alphabetics if the UCPN state bit is set. */ *s++ = PSC; *s = '\0'; k = State & sbUCPN; for (p = pn = lf->base; *p; p++) if (*p == PSCA) *p = PSC; else if (k && islower(*p)) *p = (char)toupper((int)*p); /* In the initial path name, consecutive PSCs are replaced by a PSC, PSC.PSC sequences are replaced by a PSC, PSC..PSC sequences are processed, and then the PSC appended above is deleted. */ for (p = pn; *p; ++p) if (*p == PSC && *(p+1) == PSC) { t = p--; do *t = *(t+1); while (*t++); } for (p = pn; *p; ++p) if (*p == PSC && *(p+1) == '.') BEGIN if (*(p+2) == PSC) { t = p--; do *t = *(t+2); while (*t++); } else if (*(p+2) == '.' && *(p+3) == PSC) { s = p+3; if (p != pn) for (--p; p != pn && *p != PSC; --p); if (p == pn && *p != PSC) { *e = " Erroneous .."; VM_RELEASE(hndl); return NULL; } t = p--; do *t = *s++; while (*t++); } END *(--p) = '\0'; k = (int)(p - pn); /* Finally, idiosyncrasies involving the root of the file system are handled. The result is a complete path name pn of length k that can be compared with its brethren with some confidence, and it's copied to the struct. The copy in the data area may be used below. */ if (k <= 1) { *pn = PSC; *(pn+1) = '.'; *(pn+2) = '\0'; k = 2; } else if (k == 2 && *(pn+1) == ':') { *(pn+2) = PSC; *(pn+3) = '\0'; k = 3; } else if (k >= sizeof(lf->pn)) { *e = " Name too long."; VM_RELEASE(hndl); return NULL; } (void)strcpy(lf->pn,pn); /* With the name in hand, the system's perspective is solicited. If recognized, the path name must correspond to either a regular file or a directory, e.g., no pipes or devices. The system's view is returned as flags (STAT, FILE, and DIR) and in size and mtime. In some of the supported systems, the mtime values provided by the file system interface may be both inconsistent and unreliable. The mtime for a directory is set to the current time so that they are always (re)loaded when referenced (see LoadVMF). If the complete path name is not recognized, just the path portion is tried. If the path is a directory, the VM file is NEW and will be created as a disk file when appropriate. But the file name may be illegal, and the file system may not be willing to create it. */ #ifdef POO_NATIVE_WIN32 BEGIN DWORD r; if ((r = GetFileAttributes(lf->pn)) != 0xFFFFFFFF) { if (r & FILE_ATTRIBUTE_DIRECTORY) { lf->flags = bSTAT|bDIR|(DEFAULTFILEFORMAT & bCRLF); lf->size = 0; O_Time(NULL,&(lf->mtime)); } else { HANDLE fh; BY_HANDLE_FILE_INFORMATION fi; fh = CreateFile(lf->pn,0,0,NULL,OPEN_EXISTING,0,NULL); if (GetFileInformationByHandle(fh,&fi)) { if (fi.nFileSizeHigh || fi.nFileSizeLow & 0xF0000000) *e = "is too large."; else { lf->size = fi.nFileSizeLow; lf->flags = bSTAT|bFILE; lf->mtime = fi.ftLastWriteTime; }} else *e = "is not available."; CloseHandle(fh); }} else { p = pn + k - 1; while (pn < p && *p != PSC) --p; if (p == pn || *(p-1) == ':') ++p; *p = '\0'; r = GetFileAttributes(pn); if (r == 0xFFFFFFFF || !(r & FILE_ATTRIBUTE_DIRECTORY)) *e = " Path error."; } END #else BEGIN struct stat info; if (! stat(lf->pn,&info)) { if (S_ISDIR(info.st_mode)) { lf->flags = bSTAT|bDIR|(DEFAULTFILEFORMAT & bCRLF); lf->size = 0; O_Time(NULL,&(lf->mtime)); } else if (S_ISREG(info.st_mode)) { lf->flags = bSTAT|bFILE; lf->mtime = info.st_mtime; lf->size = info.st_size; } else *e = "is not a file or directory?"; } else { p = pn + k - 1; while (pn < p && *p != PSC) --p; if (p == pn || *(p-1) == ':') ++p; *p = '\0'; if (stat(pn,&info) || !(S_ISDIR(info.st_mode))) *e = " Path error."; } END #endif if (*e) { VM_RELEASE(hndl); return NULL; } /* Once the terminal \n is appended, the VM file is inserted in the file list. The terminal \n is not part of the file; it's part of the representation used for VM files by this software. */ *(lf->eof) = '\n'; lf->next = FileList; return (FileList = lf); END /*---------------------------------------------------------------- DestroyVMF (VM file) deletes the specified VM file provided that it's not LOCKed, not associated with a window, and not associated with the VM file copy facility. The caller should interpret the return, NULL or the VM file, as informational because it's certainly not an error if the file isn't destroyed. The associations are checked because it saves a lot of bookkeeping and because it's reliable; it would be unfortunate to destroy a VM file that's in use. Consequently, other sections of this software are quite cavalier about calling DestroyVMF. On the other hand, they usually have to eliminate an association before calling. */ VMF *O_DestroyVMF (VMF *lf) BEGIN WND *lw; VMF *plf; if (! lf) return NULL; /* Easy case.*/ /* Deleting an element in a singly linked list requires some work as one must find its predecessor. Although this could be deferred until the associations have been checked, it's done first because it also serves to verify that the argument is a valid VM file. */ if (FileList == lf) plf = NULL; else { for (plf = FileList; plf && plf->next != lf; plf = plf->next); if (! plf) return lf; } /* The LOCK flag is set only in the memory file but could be set in other files. The VM file copy/paste pointer is tested next; this file is always destroyed after it's pasted. Finally, the input and output associations of each window are examined. Input files are destroyed when the EOF is encountered, and output files when the window is closed or when the user associates a new file with the window (onlooker). */ if (lf->flags & bLOCK) return lf; if (lf == Work->next) return lf; for (lw = WndList; lw; lw = lw->next) if (lw->dout == lf || lw->din == lf) return lf; /* If the file was LOADed from the file system, the memory file is updated, i.e., the LOAD record is changed to a position record. Actually destroying the file is trivial. */ if (lf->flags & bLOAD) O_MemoryVMF(lf,oSTORE); if (plf) plf->next = lf->next; else FileList = lf->next; if (lf->memhndl) { VM_RELEASE(lf->memhndl); } return NULL; END /*---------------------------------------------------------------- LoadVMF (cwd, filename, esize, errptr) returns the VM file identified by the complete path name that is constructed by CreateVMF. The complete path name may or may not be that of an existing VM file and may or may not be that of a file in the file system. The path name returned by CreateVMF is compared to those in the file list to determine if it already exists, which resolves the first issue. The flag bits returned by CreateVMF resolve the second. The principal role of LoadVMF is to load files from the file system into VM files. It will not return a NEW VM file unless it already exists. The value of esize puts restrictions on the VM file and controls the setting of the SAVE flag and the updating of the memory file (MemoryVMF). LoadVMF may return any of the CreateVMF strings or "doesn't exist." if the path is ok but the file doesn't exist, " Open error." if the file could not be opened, or " Read error." if the file could not be read successfully. Further, if esize is positive, i.e., a text file is required, it may return " Directory!" if the file is a directory or " Binary file!" if the file contains binary data. */ VMF *O_LoadVMF (char *dir, char *name, long esize, char **e) BEGIN VMF *alf, *lf; VM_SIZE size; char *s, *t; /* CreateVMF, the keeper of the VM file name space, constructs the complete path name in a minimal size VM file and returns various information in the flags. But, before looking at the flags, the file list is searched because the VM file may exist already. The search compares both the path name and the mtime value. NEW VM files have an mtime value equal to the application start time so that they are uniquely identified by their path names. Directories always have the 'current' time. For regular files, multiple VM files can exist if the disk file is modified between references. Although output direction sets the path name in the VM file for an other, it will not be recognized here since the mtime values differ so that, if you direct output to a file, you can still have the file loaded as a VM file. */ lf = O_CreateVMF(dir,name,0,e); if (! lf) return NULL; for (alf = lf->next; alf; alf = alf->next) { #ifdef POO_NATIVE_WIN32 if (CompareFileTime(&(alf->mtime),&(lf->mtime))) continue; #else if (alf->mtime != lf->mtime) continue; #endif if (strcmp(alf->pn,lf->pn)) continue; O_DestroyVMF(lf); if (esize <= 0) return alf; if (alf->flags & bHEX) { *e = " Binary file?"; alf = NULL; } else if (alf->flags & bDIR) { *e = " Directory?"; alf = NULL; } return alf; } /* A NEW file, a file that doesn't exist in the file system or as a VM file, is not acceptable when calling LoadVMF. If esize is positive, a directory is not acceptable; likewise a binary file, but that cannot be detected without loading the file. */ if (lf->flags & bNEW) { O_DestroyVMF(lf); *e = "doesn't exist."; return NULL; } if (lf->flags & bDIR && esize > 0) { O_DestroyVMF(lf); *e = " Directory!"; return NULL; } /* If control is still here, the complete path name corresponds to a file or directory in the file system that must be loaded into a VM file. Because a directory doesn't really have a proper size, the initial VM file created above is used and expanded as necessary; it's also used for puny files. Otherwise, an unnamed file with the required size is created, information is copied from the initial VM file struct to the new one, and the initial one is destroyed. If esize is positive, at least esize extra bytes are allocated for data; this feature is used to load the memory file and poll files since they usually grow in size. If the file cannot be created, the cryptic message provided by CreateVMF is returned. */ size = lf->size; if (esize > 0) size += (lf->xsize = esize); if (size >= (unsigned long)lf->asize) { alf = O_CreateVMF(NULL,NULL,size,e); if (alf) { alf->flags = lf->flags; alf->mtime = lf->mtime; alf->size = lf->size; alf->xsize = lf->xsize; (void)strcpy(alf->pn,lf->pn); } O_DestroyVMF(lf); lf = alf; if (! lf) return NULL; } /* With sufficient space available, the file can be loaded into the VM file. The file is transferred as binary data and is retained in the VM file just as provided by the file system. The bytes read are examined to determine the HEX and CRLF flags. The binary file test (HEX flag) is unsatisfactory. A positive test for a text file that does not require that the whole file be examined would be ideal. Until one comes along, this code looks for any character less than or equal to 8, a \b. Rather than scan the whole file, the file is regarded as text when (if) the number of bytes scanned exceeds 16 times the number of newlines. The CRLF flag is set using the character preceding the first \n. The line termination need not correspond to the file format of the system in which the application is running, is transparent to the application, and is used only to append data to the file. Thus files can be imported and exported without conversion even if text is appended to them. Finally, a terminal Ctrl+Z (26) is trimmed from the end of the file (EOF flag). */ s = lf->base; if (lf->flags & bFILE && lf->size) BEGIN #ifdef POO_NATIVE_WIN32 HANDLE fh; DWORD r; fh = CreateFile(lf->pn,GENERIC_READ,0,NULL,OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN,NULL); if (fh == INVALID_HANDLE_VALUE) *e = " Open error."; else BEGIN long bytes; bytes = lf->size; if (ReadFile(fh,s,bytes,&r,NULL)) { s += r; bytes -= r; } CloseHandle(fh); #else int fd, len; struct stat info; fd = open(lf->pn,O_RDONLY|O_BINARY); len = (fd != -1) ? fstat(fd,&info) : 1; #ifdef POO_WATCOM_COMPILER if (!(info.st_mode & S_IFMT)) info.st_mode |= S_IFREG; #endif len = (len) ? 0 : S_ISREG(info.st_mode); if (! len) { if (fd != -1) close(fd); *e = " Open error."; } else BEGIN long bytes; for (bytes = lf->size; bytes > 0; bytes -= len) { if ((len = read(fd,s,4096)) < 1) break; s += len; } close(fd); #endif if (! bytes) { *s = '\0'; for (t = lf->base; (unsigned)*t > 8; ++t) if (*t == '\n') { ++bytes; if (bytes > 16 && (long)(t - lf->base) > (bytes << 4)) { t = s; break; }} if (t != s) { lf->flags |= bHEX; if (esize > 0) *e = " Binary file!"; } else { if (*(s-1) == 26) { --s; lf->flags |= bEOF; } *s = '\n'; for (t = lf->base; *t != '\n'; ++t); if ((t == s && DEFAULTFILEFORMAT & bCRLF) || (t > lf->base && *(t-1) == '\r')) lf->flags |= bCRLF; }} else *e = " Read error."; END END /* As a VM file, a directory is an unsorted list of file names, one per line, except that the first entry is always the complete path name of the directory and the . entry is ignored. The .. entry appears as itself. File names are followed by the file size and the last modification time; subdirectory names have no additional information. The size of the VM file required cannot be determined so that the initial VM file is expanded as needed, which can fail, at least in theory. ExpandVMF changes the VM file pointer and data space so that pointers cannot be retained while building the directory. */ else if (lf->flags & bDIR) BEGIN #ifdef POO_NATIVE_WIN32 HANDLE fh; WIN32_FIND_DATA dd; char *z; t = lf->pn; while (*t) *s++ = *t++; if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; z = t; if (*(z-1) != PSC) *z++ = PSC; *(z+2) = *z = '*'; *(z+1) = '.'; *(z+3) = '\0'; fh = FindFirstFile(lf->pn,&dd); *t = '\0'; while (fh != INVALID_HANDLE_VALUE) { t = dd.cFileName; if (*t != '.' || *(t+1) != '\0') { if (State & sbUCPN) { for (z = t; *t; ++t) if (islower(*t)) *t = (char)toupper((int)*t); t = z; } if ((long)(s - lf->base) + MAXLENFILE + 40 >= lf->asize) { *(lf->eof = s) = '\n'; alf = O_ExpandVMF(NULL,lf,4096); if (! alf) { *e = "No memory!"; break; } lf = alf; s = lf->eof; } z = s+14; while (*t) *s++ = *t++; if (!(dd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { while (s < z) *s++ = ' '; s += sprintf(s," %7luB ",dd.nFileSizeLow); s += O_Time(s,&(dd.ftLastWriteTime)); } if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; } if (! FindNextFile(fh,&dd)) break; } if (fh != INVALID_HANDLE_VALUE) FindClose(fh); #else char *z, *fn; int m; struct stat ss; DIR *dd; struct dirent *de; dd = opendir(lf->pn); if (! dd) *e = " Open error."; else {t = lf->pn; while (*t) *s++ = *t++; m = (int)(t - lf->pn); if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; while ((de = readdir(dd)) != NULL) { t = de->d_name; if (*t == '.' && *(t+1) == '\0') continue; if ((long)(s - lf->base) + MAXLENFILE + 64 >= lf->asize) { *(lf->eof = s) = '\n'; alf = O_ExpandVMF(NULL,lf,4096); if (! alf) { *e = "No memory!"; break; } lf = alf; s = lf->eof; } fn = lf->pn + m; if (*(fn-1) != PSC) *fn++ = PSC; z = s+14; while (*t) { *s++ = (State & sbUCPN && islower(*t)) ? (char)toupper((int)*t) : *t; *fn++ = *t++; } *fn = '\0'; if (! stat(lf->pn,&ss) && S_ISREG(ss.st_mode)) { while (s < z) *s++ = ' '; s += sprintf(s," %7luB ",(unsigned long)ss.st_size); s += O_Time(s,&(ss.st_mtime)); } if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; } if (closedir(dd)) *e = " Read error."; *(lf->pn + m) = '\0'; } #endif END /* If anything went wrong, e points to some cryptic string, and the file is destroyed; otherwise, the VM file struct can be completed and returned after some bookkeeping. */ if (*e) { O_DestroyVMF(lf); return NULL; } *(lf->eof = s) = '\n'; lf->size = (long)(s - lf->base); /* MemoryVMF maintains file status/position records in the memory file for all files loaded from the file system. Thus the user can see what files are or have been loaded. In addition, if called to generate a LOAD record, it sets the posn component in the VM file struct if a position record already exists for the file. Thus, onlookers seem to remember their position in a file. The SAVE flag is for files loaded from the file system unless they are poll files. During termination of Onlooker, SAVE generates a LOAD record in the memory file, which serves to provide Onlooker the ability to restore its state. */ if (esize >= 0) O_MemoryVMF(lf,oLOAD); if (esize <= 0) lf->flags |= bSAVE; lf->flags |= bSTAT|bLOAD; return lf; END /*---------------------------------------------------------------- StoreVMF (VM file, operation, errptr) writes the VM file to the file system and returns the VM file if successful and NULL otherwise. The errptr argument may be NULL if the caller really doesn't care about the error string. StoreVMF is called to implement I/O direction in pollsters and others and STORE and APPEND commands in onlookers and to update the memory file. Pollsters and output direction in others, i.e., an op of zero, is implemented using the UPDATE and STORE flags and posn component: the portion of the file from base+posn to eof-1 is written (STORE off) or appended (STORE on) to the file. StoreVMF updates the STORE flag and posn component. The STORE and APPEND commands in an onlooker are implemented by calling CreateVMF for the specified file, copying the base and eof pointers from the file associated with the onlooker to the VM file returned by CreateVMF, calling StoreVMF for this "fake" VM file, and then destroying it. Finally, StoreVMF explicitly protects the memory file by comparing the path name and requiring a MEMORY operation. Since MemoryStore is the only user of the MEMORY operation, this mechanism protects the memory file under normal circumstances. */ VMF *O_StoreVMF (VMF *lf, int op, char **e) BEGIN #ifdef POO_NATIVE_WIN32 HANDLE fh; DWORD k; #else int fd; unsigned k; #endif long size; char *s; /* The first task is to sort out the various situations with highest priority going to protection of the memory file. This is done by looking at the complete path name and checking the op argument; otherwise, op determines what should be done. */ s = lf->base; if (e) *e = NULL; if (MemoryFile && ! strcmp(lf->pn,MemoryFile->pn)) { if (op != oMEMORY) { if (e) *e = "illegal."; return NULL; }} /* A STORE or APPEND command in an onlooker is restricted and needs operational adjustments. Clearly, writing to a directory isn't allowed even though writing a DIR VM file is fine. If the file is NEW, the ROFS flag is ignored and the operation is changed to a STORE because the file must be created. If the file exists in the file system, the ROFS flag is pertinent and the operation adjusted. These operational adjustments are necessary because the open calls fail (by design) if the open parameter and file status disagree. */ else if (op) { if (lf->flags & bDIR) { if (e) *e = "is a directory!"; return NULL; } if (lf->flags & bNEW) op = oSTORE; else if (State & sbROFS) { if (e) *e = "ROFS!"; return NULL; } else if (op == oSTORE) op = oMEMORY; } /* If the file is being updated, i.e., output direction in an other or a pollster, the operation is selected based on the flags, and the proper starting point is computed. */ else if (lf->flags & bUPDATE) { op = (lf->flags & bSTORE) ? oAPPEND : oMEMORY; lf->flags |= bSTORE; s += (size_t)(lf->posn); lf->posn = lf->eof - lf->base; } /* A STORE open fails if the file exists, and an APPEND if it doesn't. A MEMORY open either empties it or creates it depending on whether it exists or not. If op is not set correctly at this point, e.g., is still zero because the UPDATE flag is not set, the result is "illegal." */ #ifdef POO_NATIVE_WIN32 if (op == oSTORE) fh = CreateFile(lf->pn,GENERIC_WRITE,0, NULL,CREATE_NEW,FILE_FLAG_WRITE_THROUGH,NULL); else if (op == oAPPEND) { fh = CreateFile(lf->pn,GENERIC_WRITE,0, NULL,OPEN_EXISTING,FILE_FLAG_WRITE_THROUGH,NULL); if (SetFilePointer(fh,0,NULL,FILE_END) == 0xFFFFFFFF) { CloseHandle(fh); fh = INVALID_HANDLE_VALUE; }} else if (op == oMEMORY) fh = CreateFile(lf->pn,GENERIC_WRITE,0, NULL,CREATE_ALWAYS,FILE_FLAG_WRITE_THROUGH,NULL); else fh = INVALID_HANDLE_VALUE; if (fh == INVALID_HANDLE_VALUE) #else if (op == oSTORE) fd = open(lf->pn, O_WRONLY|O_BINARY|O_CREAT|O_EXCL,S_IRUSR|S_IWUSR); else if (op == oAPPEND) fd = open(lf->pn, O_WRONLY|O_BINARY|O_APPEND); else if (op == oMEMORY) fd = open(lf->pn, O_WRONLY|O_BINARY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR); else fd = -1; if (fd < 0) #endif { if (e) *e = "illegal."; return NULL; } /* The writes are done in pages. In the supported systems, 4096 is sufficient to get meaningful work done without incurring a lot of overhead but other values could be used. */ #ifdef POO_NATIVE_WIN32 if (! WriteFile(fh,s,size = lf->eof - s,&k,NULL)) lf = NULL; else if (k != size) lf= NULL; CloseHandle(fh); #else for (size = lf->eof - s; size > 0; size -= k, s += k) { k = (unsigned)((size > 4096) ? 4096 : size); if (k != write(fd,s,k)) { lf = NULL; break; }} close(fd); #endif if (e) *e = (lf) ? NULL : "write error."; return lf; END /*---------------------------------------------------------------- ExpandVMF (window, VM file, delta) expands the data space for the VM file or truncates data to make room for additional data. If the TRUNC flag is not set, the space is increased by min(delta,xsize) and then rounded up by CreateVMF to pages. Otherwise, xsize bytes are truncated; for text files, the truncation point is moved to the next line boundary so that more than xsize bytes may be discarded. ExpandVMF returns the new file, which is the same as the old if truncating, or NULL. If a new file is actually created, ExpandVMF tries to destroy the old. WARNING: Whether expanding or truncating, pointers to the file and displacements if truncating are effectively invalidated by calling this function. If expansion fails for an other, the file management strategy is changed automatically to truncation so that for others, ExpandVMF never fails. Thus, ExpandVMF is written in the form if (expansion) {...} if (truncation) {...} { common epilogue: copy s...eof to the new file } so that the buffer management strategy for an other can be changed to truncation if expansion fails. This strategy saves AppendVMF lots of code. For pollsters and onlookers, data may be lost when memory is not available. Although no error is noted, it is likely the user will notice when memory is exhausted. There are only two callers of this function, AppendVMF and LoadVMF when loading a directory. AppendVMF, however, is widely used. Although some systems provide the ability to expand a previously allocated block, this function uses brute force so that the same code is valid in all systems. Consequently, both expansion and truncation require that existing data be moved. The application should choose the initial and extra size values to minimize the overhead as necessary. The window/file management at the end may or may not successfully destroy the old file when a new one is created because the old one may be in use for many things. */ VMF *O_ExpandVMF (WND *lw, VMF *lf, long delta) BEGIN VMF *nlf; size_t k; unsigned long size; char *d, *s; if (! lf) return NULL; s = lf->base; /* The value of delta, the number of additional bytes required, is usually based on current need, but xsize is the minimal amount by which the size of the file is adjusted. The application specifies the extra size value when opening an output stream, but Open sets the xsize component to an appropriate multiple of 1024. A VM file is expanded by creating an unnamed VM file, copying the appropriate components of the struct and the data, and destroying the old VM file. Thus, pointers to the old file are invalidated, but displacements remain valid. */ if (!(lf->flags & bTRUNC)) { if (delta < lf->xsize) delta = lf->xsize; nlf = O_CreateVMF(NULL,NULL,lf->asize + delta,&d); if (nlf) { nlf->flags = lf->flags; nlf->mtime = lf->mtime; nlf->posn = lf->posn; nlf->xsize = lf->xsize; (void)strcpy(nlf->pn,lf->pn); } else if (! lw || lw->flags & (bPOLL|bONLKR)) return NULL; else lf->flags |= bTRUNC; } /* Truncation is used only for a VM file associated with an other if explicitly requested in the open or if expansion fails. Pointers and displacements are invalidated by truncation. If the UPDATE flag is set, only the portion of the VM file that is being discarded is written to the file. Because StoreVMF would write the whole file, the eof is set and restored around the call; the posn is set to zero. When the application opens an other, the extra size is set to the value provided by the application rounded to the closest multiple of 1024 and then reduced to half the size of the file. For text files, the truncation point is moved to the next line boundary. The file is truncated by expanding it into itself, i.e., setting nlf to lf. */ if (lf->flags & bTRUNC) { if ((long)(lf->eof - lf->base) <= lf->xsize) return lf; nlf = lf; s += (size_t)lf->xsize; if (!(lf->flags & bHEX)) while (*s++ != '\n'); if (lf->flags & bUPDATE) { d = lf->eof; lf->eof = s; O_StoreVMF(lf,0,NULL); lf->eof = d; lf->posn = 0; }} /* It remains to copy the existing data s...lf->eof to the new file. The ANSI memcpy function accepts a size_t as an argument, and the result is undefined if the source and destination overlap. This code avoids both issues. The data is copied in 1024 byte blocks, which should at least get memcpy's heart going. When truncating, the value of xsize prevents overlap. */ d = nlf->base; nlf->size = lf->eof - s; for (size = nlf->size; size > 1024; size -= 1024) { (void)memcpy(d,s,1024); d += 1024; s += 1024; } k = (size_t)size; if (k) { (void)memcpy(d,s,k); d += k; } *(nlf->eof = d) = '\n'; /* If the old file is the memory file, the LOCK and UPDATE flags are set off so that it can be destroyed and the name is set null to avoid any possible confusion. Clearly, the memory file pointer must be changed. If the old file is associated with the window, the file extents are set to the whole file, and the window position is adjusted. This adjusted position may be invalid after truncation, but it will likely be changed before being used and, if not, DrawWindow is robust enough to handle the situation anyway. Finally, the VM file is destroyed; fortunately, DestroyVMF is astute enough to look around before actually destroying it. */ if (lf == MemoryFile) { lf->flags &= ~(bLOCK|bUPDATE); *(lf->pn) = '\0'; MemoryFile = nlf; } if (lw && lw->dout == lf) { lw->base = nlf->base; lw->eof = nlf->eof; lw->top = nlf->base + (size_t)(lw->top - lf->base); lw->bot = NULL; lw->dout = nlf; } if (lf != nlf) O_DestroyVMF(lf); return nlf; END /*---------------------------------------------------------------- AppendVMF (window, VM file, data, length) appends the data or uninitialized space of the specified length to the file associated with the window or the specified file if the window is NULL. If successful, the return is the old value of the eof pointer (the first byte of the data appended) except as noted below. If unsuccessful, NULL is returned. WARNING: The VM file pointer in the window struct is changed by ExpandVMF so that a local copy of this pointer MUST be reloaded after calling AppendVMF. Indeed, any pointer must be reloaded; displacements are safe only if the file is expanded. When appending data, multiple calls to ExpandVMF may be required even though len bytes are requested because, despite the request, ExpandVMF may provide less than the requested space if the VM file is being managed by truncation. Further, for textual data, the expansion of each \n to \r\n may necessitate an additional call. When multiple calls are required, the value returned by AppendVMF corresponds to the first data byte appended after the last call to ExpandVMF. If multiple ExpandVMF calls are required, there is the possibility that a secondary call may fail; but this can occur only when doing a copy-paste to an onlooker. For others, ExpandVMF changes the VM file management strategy to truncation and does not fail when the requested memory space is not available. If a copy-paste fails, it should be visually apparent to the user, and no commentary is provided. The calls from Annotate and the Memory... functions take the form AppendVMF(NULL,poll or memory file,NULL,...) so that the return is the first byte appended or NULL. All other calls take the form AppendVMF(window,NULL,...). */ char *O_AppendVMF (WND *lw, VMF *lf, char *data, long len) BEGIN long k; char *s, *eof; if (lw && lw->dout) lf = lw->dout; /* To associated file.*/ else if (lf) lw = NULL; /* To specified file.*/ else return NULL; /* Should never occur.*/ /* If appending uninitialized space, the window is NULL, and the file a poll file or the memory file so that ExpandVMF expands the file or fails. Clearly, the caller should fill the space allocated with some data; the return corresponds to the first byte of the space. Annotate and the Memory functions ignore failure without comment; and the loss of the data may or may not be immediately apparent. */ if (! data) { if ((long)(lf->eof - lf->base) + len + 3 >= lf->asize) { lf = O_ExpandVMF(lw,lf,len+3); if (! lf) return NULL; } s = (eof = lf->eof) + (size_t)len; if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; } /* The primary distinction between appending binary data and text is that binary data is not inspected or changed. Multiple calls to ExpandVMF occur only if truncating. */ else if (lf->flags & bHEX) while (len > 0) { if ((long)(lf->eof - lf->base) + len + 1 >= lf->asize) { lf = O_ExpandVMF(lw,lf,len+1); if (! lf) return NULL; } s = lf->eof; k = lf->asize - (long)(s - lf->base) - 1; if (k > len) k = len; len -= k; while (k--) *s++ = *data++; eof = lf->eof; lf->eof = s; } /* When appending text, each \r is deleted, and each \n is changed to \r\n (CRLF) or left a \n. Consequently, lines in the text may be delimited by \r\n or \n or an admixture. ExpandVMF may be called multiple times if truncating or if expansion of the \n's requires too much space. Although not obvious, if the text does not end with a \n, there is room for appending a \r\n; Put uses this feature in connection with the LINE modifier. */ else while (len > 0) { if ((long)(lf->eof - lf->base) + len + 3 >= lf->asize) { lf = O_ExpandVMF(lw,lf,len+3); if (! lf) return NULL; } eof = lf->base + (size_t)lf->asize - 3; for (s = lf->eof; s < eof && len; --len) { if (*data == '\r') { ++data; continue; } if (*data == '\n' && lf->flags & bCRLF) *s++ = '\r'; *s++ = *data++; } eof = lf->eof; *(lf->eof = s) = '\n'; } /* The terminal \n added here is part of the VM file representation; it is NOT part of the file. The window struct is updated so that the data just appended can be displayed. */ *(lf->eof = s) = '\n'; if (lw) lw->eof = lf->eof; return eof; END /*---------------------------------------------------------------- AssociateVMF (window, VM file) associates the file with the window and returns the file that was previously associated with it. Since DestroyVMF ignores requests to destroy a file if it's in use, the caller can destroy the file returned mindlessly. AssociateVMF is called by CreateWND and by Command when a new file is being associated with an onlooker. The whole file is associated with the window, and the HEX flag is copied from the file to the window struct. As a window flag, HEX controls the display mode; as a file flag, it's informational. For pollsters and others, the position is set to display the last line of the file at the top. This is correct for pollsters and works for others because the file is empty anyway. For onlookers, the position is set from the posn component of the VM file, e.g., the same as last time. If the VM file was loaded from the file system (LOAD), the current directory of the window is changed to the path of the file. This will usually occur for pollsters and onlookers but never for an other. */ VMF *O_AssociateVMF (WND *lw, VMF *lf) BEGIN VMF *alf; char *s, *d, *e; if (! lf) return NULL; alf = lw->dout; lw->dout = lf; lw->flags = (lw->flags & ~bHEX) | (lf->flags & bHEX); lw->eof = lf->eof; lw->base = lf->base; lw->shft = lw->ytop = 0; lw->bot = NULL; if (lw->flags & bONLKR) lw->top = lf->base + (size_t)lf->posn; else lw->top = (lf->base == lf->eof) ? lf->base : lf->eof - 1; if (lf->flags & bLOAD) { s = lf->pn; e = d = lw->cwd; while (*s) { if (*s == PSC) e = d; *d++ = *s++; } if (lf->flags & bDIR) e = d; else if (e == lw->cwd) ++e; *e = '\0'; } return alf; END /*================================================================ MEMORY FILE FACILITY The memory file is used to save window attributes, function key assignments, and file usage information between invocations. The functions in the MF facility are: MemoryLoad loads the memory file and processes initialization commands, MemoryStore reorganizes and stores the memory file when the application is being terminated, MemoryCmd manages window attributes, MemoryKey manages function key string assignments, and MemoryVMF manages file status/position information. These functions are more encapsulations of system-independent code than proper functions. For example, MemoryLoad is called only by cappl, MemoryStore is called only by WindThread, and both assume the context of the call. The others are more conventional but see little action. Several internal variables and arrays are used in conjunction with the memory file facility. Variable/ Memory-> Load Store Cmd Key VMF --------------------------------------------- MemoryFile X X R R R MemoryFileName R R MemoryPathVar R FcnKeyStr X R X ApplicationName R R Work->pn X In this table, R means referenced, while X means referenced and/or changed. The POO current directory, Work->pn, is referenced in MemoryLoad implicitly when it calls CreateVMF and LoadVMF with a NULL for the directory argument. Finally, the MEMORY command allows the user to control updating of the memory file (when the application is being terminated) and to open an onlooker for the memory file (see Command). The memory file is provided for the user's benefit, i.e., as a convenient mechanism for retaining information between invocations. For this reason, it's implemented as a plain text file so that the user can view it and edit it manually. Normally, it's loaded when the application calls cappl and updated when the application is being terminated. The MF facility does not provide a mechanism for access by the application. For the user's convenience, the memory file is not structured and is implemented using the commands that the user enters through a window. Although the implementation entails multiple scans of the memory file, efficiency and optimization are not important: it's scanned when loaded initially, when creating a new window, when loading a file, and when destroying some VM files. The MF implementation is syntactically and semantically analogous to the resource management capability in the X Window System. The MF facility generates/processes lines of the form [qualifier{*|.}]<command> <operand>... MemoryCmd F<digit><digit> "..." MemoryKey {<|@}<path name> <position> MemoryVMF <whitespace character>... -... where the optional qualifier is either the window name or subclass and command is appropriately restricted. Lines beginning with a whitespace character are ignored but retained, i.e., comments. If the first character is a minus(-), the line has been marked for deletion; it's deleted when the file is updated. The MF facility does not edit the memory file; it marks lines for deletion and appends new lines as necessary. Although the Memory... functions generate only a restricted set of lines, the user is free to add many others, e.g., CD and I/O direction commands qualified by the window name and unqualified attribute commands. Command lines are parsed by locating the first blank, asterisk(*), or period(.). If a blank occurs first, the entire line is parsed as a command; if an asterisk or period, the preceding characters must constitute a valid qualifier, and the subsequent characters are parsed as a command. Case is significant in the qualifier; command parsing isn't case sensitive and allows initial substring abbreviations; and the treatment of case in the operands depends on the command. The window names and subclass names of pollsters and onlookers are "Pollster" and "Onlooker", respectively. The names of others are provided by the application, and their subclass name is "Other". Although not required, it's strongly recommended that window names be unique and consist of alphanumeric characters. Commands are assigned precedence based both on the qualifier, if any, and their order within the file. The precedence of an unqualified command is 1; subclass qualified, 2; and name qualified, 3. The highest precedence command is used with ties resolved by using the last occurrence. The name of the memory file is constructed by cappl. It's located using the absolute or relative paths assigned to the variables in the memory path table, namely POOMEMDIR, HOME, and the POO current directory. If the memory file cannot be loaded, it's created on the path of highest precedence that does not cause a conflict. There is no attempt to avoid the problems that necessarily arise as a consequence of multiple, simultaneous invocations, each of which will update the memory file on termination. The user must be aware of this issue and either terminate the invocations in the desired order or set updating off. If an application is routinely used in this manner, inserting a "MEMORY OFF" in the memory file may be convenient. The MF facility saves displacements into the memory file that are subsequently used to locate lines of interest, e.g., function key commands. This works because the file is NOT edited. The MF functions append new commands to the memory file and mark outdated commands for deletion by replacing the first character of the line with a minus(-); MemoryStore ignores such lines. If the memory file cannot be expanded as necessary, data is lost. Displacements are fixed while the application is running. ------------------------------------------------------------------ MemoryLoad (LOAD|NONE) loads or creates the memory file and scans it for initialization commands, i.e., function key assignments and unqualified MEMORY, CD, and LOAD commands, the latter only if the argument is LOAD. This function is called only from cappl and should be viewed as an extension of it; MemoryLoad does nothing if the memory file already exists. This function is responsible for saving the displacements of the function key commands found in the memory file in the function key array, which is subsequently updated by MemoryKey. The MEMORY and CD commands processed here may be inserted manually by the user. The LOAD commands may be inserted by MemoryStore or by the user. Normally, the memory file is updated when the application is being terminated so that a MEMORY ON command would be superfluous; however, there are circumstances in which a MEMORY OFF would be appropriate, e.g., when multiple invocations are frequently run at the same time. Inserting a MEMORY OFF is generally ill-advised as it's invisible and can be frustrating, e.g., it's easy to forget to look for it if the memory file doesn't get updated. Allowing unqualified CDs in the memory file to change the current directory may be redundant in some circumstances, e.g., Windows and OS/2 allow a user specified current directory to be associated with an executable. The capability supported here, however, works in all systems and is independent of how the application is run, a command or double-click with the mouse. A CD with an absolute path can be used to fix the run-time environment. If MemoryLoad is called with an argument of LOAD, unqualified LOAD commands load the indicated file. In general, this facility is used only by the Onlooker application, which places a LOAD command in the memory file for each file open when it's terminated and opens an onlooker for each file that exists when its invoked. Thus it appears to maintain its state between invocations. This is the only use of this capability because all other applications call with NONE. WorkInput, the input cut buffer, is used to hold command operands. Since this function is called before any windows exist, this cannot cause a conflict, but it must be set empty afterwards. This is the reason lines longer than MAXLENINPUT are ignored when the the memory file is scanned. */ VMF *O_MemoryLoad (int load) BEGIN VMF *lf, *alf; int cmd, k; char *tmp, *s, *t, *e; long p; if (MemoryFile) return MemoryFile; tmp = WorkInput; /* Access to the file system requires two items: the name of the file and a path, either absolute or relative. The name of the memory file is constructed by cappl, the only caller of this function, as the application name with a ".mem" suffix; the case depends on the appropriate defined constant, i.e., the system, and is taken care of by cappl. The memory file name could be constructed here. The potential paths for the memory file are obtained using the values (paths) assigned to the environmental variables in the memory path array; the paths may be relative or absolute. Because CreateWND processes environmental variables in either the Windows or Unix notation, the potential memory files are tried by simply concatenating the strings in the path table and the memory file name with an intervening PSC. The path table entries are "$POOMEMDIR", "$HOME", and ".". CreateVMF, and therefore LoadVMF, fail if an environmental variable is not defined. If the file cannot be loaded, a provisional file is created to to save having to go through the list a second time if the file cannot be loaded from any of the paths. Subsequent code sorts out the situation. If two path variables are assigned the same value and the memory file does not exist on this path, it'll be created for the first variable and then found by LoadVMF for the second; thus the comparison of the two file pointers. If the memory path array ploy doesn't succeed, an unnamed file is created, which can fail only if there is no memory available, but MemoryStore cannot write an unnamed file, i.e., no update. */ for (alf = lf = NULL, k = 0; MemoryPathVar[k]; ++k) BEGIN t = tmp; s = MemoryPathVar[k]; while (*s) *t++ = *s++; *t++ = PSC; s = MemoryFileName; while (*s) *t++ = *s++; *t++ = '\0'; lf = O_LoadVMF(NULL,tmp,1024,&t); if (lf && lf != alf) break; else lf = NULL; if (alf) continue; alf = O_CreateVMF(NULL,tmp,1024,&t); if (alf && alf->flags & bSTAT) /* Avoid conflicts.*/ { O_DestroyVMF(alf); alf = NULL; } END if (!(lf || alf)) alf = O_CreateVMF(NULL,NULL,1024,&t); /* If the memory file was loaded from the file system, the VM file created above must be destroyed. If it was created, the extra size component is set and the usual header -<application name> POO Memory File is inserted to identify it as a memory file. In any case, s must point to the end of the file. */ if (lf) { if (alf) O_DestroyVMF(alf); s = lf->eof; } else if (alf) { lf = alf; lf->xsize = 1024; s = lf->base; *s++ = '-'; t = ApplicationName; while (*t) *s++ = *t++; t = " POO Memory File"; while (*t) *s++ = *t++; } else { *WorkInput = '\0'; return NULL; } /* If the memory file does not end with a newline, the proper line termination sequence is appended here so that the other Memory... functions can append lines without concern. In this context, LoadVMF always deletes a terminal Ctrl+Z. The LOCK and UPDATE flags are set. The LOCK flag is used only in the memory file and prevents DestroyVMF from doing anything. The UPDATE flag controls updating of the memory file, which is similar to its usual role. The SAVE flag is set only if the argument is LOAD and enables updating the memory file with file status when the application is being terminated. The SAVE flag is used only in conjunction with the Onlooker application and is the mechanism used to restore its state. */ if (s != lf->base && *(s-1) != '\n') { if (lf->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; } *(lf->eof = s) = '\n'; lf->flags |= bLOCK|bUPDATE; if (load == oLOAD) lf->flags |= bSAVE; else load = oNONE; /* If the memory file was actually loaded from the file system, it's scanned for initialization commands, i.e., function key string assignments and MEMORY, CD, and LOAD (<) commands. In this case, the commands may not be qualified; qualified commands apply to a specific window and are processed when the window is created. Lines beginning with whitespace, a minus sign(-), or an at-sign(@) are ignored. Lines longer than the maximum input length are also ignored. Otherwise, the line is parsed as a command, but commands with no operands are marked for deletion. */ if (lf->flags & bLOAD) for (s = lf->base; s < lf->eof; ++s) BEGIN for (t = s; *s != '\n'; ++s); if (isspace(*t) || *t == '-' || *t == '@') continue; if ((int)(s - t) >= MAXLENINPUT) continue; cmd = O_ParseCommand(t,s,tmp); if (cmd <= oNONE) continue; if (!(*tmp)) { *t = '-'; continue; } /* If the function key has already been assigned a string, the prior one is marked for deletion. Memory commands with an operand of "on" or "off" are processed and marked for deletion otherwise. In this case, SetAttribute matches the operand to the miscellaneous operand table. Multiple, unqualified CD commands are processed cumulatively and applied to the POO current directory. If the operand isn't a directory, the CD is marked for deletion. LOADs are processed only if MemoryLoad was called with an argument of LOAD. If LoadVMF succeeds, the file position is set from the position in the LOAD and the SAVE flag is set; otherwise, the line is marked for deletion. In general, these lines are placed in the memory file by MemoryStore, but there is nothing to prevent a user from placing a LOAD in the memory file. Because the LoadVMF call uses -1, LoadVMF doesn't call MemoryVMF, but the memory file isn't official yet so that MemoryVMF would demur anyway. Since the LOAD is processed only if done with a less than(<), the memory file contains a proper file load record, namely the current line. */ if (cmd & FKFB) { k = cmd - FKFB - 1; k = 4*k - 47*(k/12); if (FcnKeyStr[k] >= 0) *(lf->base + FcnKeyStr[k]) = '-'; FcnKeyStr[k] = (int)(t - lf->base); } else if (cmd == oMEMORY) { cmd = (int)O_SetAttribute(NULL,oNONE,tmp); if (cmd == 1) lf->flags |= bUPDATE; else if (cmd == 2) lf->flags &= ~bUPDATE; else *t = '-'; } else if (cmd == oCD) { alf = O_CreateVMF(NULL,tmp,0,&e); if (alf && alf->flags & bDIR)(void)strcpy(Work->pn,alf->pn); else *t = '-'; O_DestroyVMF(alf); } else if (cmd == load && *t == '<') { alf = O_LoadVMF(NULL,tmp,-1,&e); if (alf) { for (t = tmp + strlen(tmp) + 1; *t == ' '; ++t); for (p = 0; isdigit(*t); ++t) p = 10*p + (*t - '0'); if (p) alf->posn = p; } else *t = '-'; } END /* Memory scan.*/ *WorkInput = '\0'; return (MemoryFile = lf); END /*---------------------------------------------------------------- MemoryStore creates a reorganized copy of the memory file and calls StoreVMF to store it. This function is called only from WindThread when the application is being terminated and should be viewed as an extension of it. It should not be called in other circumstances as the memory file is corrupted by this function. Since WindThread eliminates all files, giving no quarter, as soon as this function returns, this is not an issue. This function could open the memory file and write the lines as it processes them, but this tactic would lay waste to the role of the VM file facility as the interface to the file system. In any case, efficiency has low priority here. Consequently, this function creates a new VM file, fills it from the old memory file, and then calls StoreVMF. The updated memory file is (re)organized as follows: -<application name> POO Memory File, comments, function key commands F1...F48, commands, file position lines beginning with an at-sign, and file load commands beginning with a less than. The file position and load lines are saved only if the SAVE flag is set in the memory file, i.e., for the Onlooker application. In all other applications, file information is provided only while the application is running and is not retained in the memory file. The first line plays no actual role and serves simply to identify the contents of the file. The comments are placed first because the user must have inserted them manually, which suggests they are of some value. When an onlooker is opened for the memory file, it always shows the beginning of the file, the comments. The relative order of commands and comments in the memory file is maintained; thus command precedence is not changed. This reorganization is not essential and is solely for the user's convenience. One could simply write the existing memory file or delete just the lines marked for deletion, i.e., beginning with a minus(-). */ VMF *O_MemoryStore (void) BEGIN VMF *lf, *alf; long p; int i, k; char *s, *t; if (! MemoryFile || ! *(MemoryFile->pn)) return NULL; if (!(MemoryFile->flags & bUPDATE)) return MemoryFile; /* The first task is to create a new, unnamed VM file in which the updated memory file can be assembled. The current size plus the length of the header line is used; line termination is based on the existing memory file. The header line is inserted first to identify the file as a memory file for this application. */ p = (long)(MemoryFile->eof - MemoryFile->base) + MAXLENAPP + 18; lf = O_CreateVMF(NULL,NULL,p,&s); if (! lf) return MemoryFile; s = lf->base; *s++ = '-'; t = ApplicationName; while (*t) *s++ = *t++; t = " POO Memory File"; while (*t) *s++ = *t++; if (MemoryFile->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; /* The comments are copied in order but will be together. */ for (t = MemoryFile->base; t < MemoryFile->eof; ++t) if (! isspace(*t)) while (*t != '\n') ++t; else { while (*t != '\n') *s++ = *t++; *s++ = '\n'; } /* Function key assignments are copied next but must be marked for deletion so that they are not included a second time. Marking these lines for deletion corrupts the existing memory file and compromises the function key array. */ for (i = 0; i < 48; ++i) if (FcnKeyStr[k = 4*i - 47*(i/12)] >= 0) { t = MemoryFile->base + FcnKeyStr[k]; *s++ = *t++; *(t-1) = '-'; while (*t != '\n') *s++ = *t++; *s++ = '\n'; } /* This section copies all lines that are not comments, not marked for deletion, and not file status. In theory, this should copy all unqualified and qualified commands but will also copy lines inserted by the user that aren't explicitly recognized. Function key commands have been marked for deletion; otherwise, they would be copied again in this loop. */ for (t = MemoryFile->base; t < MemoryFile->eof; ++t) if (isspace(*t) || *t == '-' || *t == '<' || *t == '@') while (*t != '\n') ++t; else { while (*t != '\n') *s++ = *t++; *s++ = '\n'; } /* The existing file position lines are copied; but file LOAD lines are generated directly from the VM file list for all files that have the SAVE flag set. The existing LOAD lines could be copied, but this would include pollster files and the position information would have to be changed anyway. It's WindThread's responsibility to set the posn components correctly before calling this function. */ if (MemoryFile->flags & bSAVE) BEGIN MemoryFile->flags &= ~bSAVE; for (t = MemoryFile->base; t < MemoryFile->eof; ++t) if (*t != '@') while (*t != '\n') ++t; else { while (*t != '\n') *s++ = *t++; *s++ = '\n'; } for (alf = FileList; alf; alf = alf->next) { if (!(alf->flags & bSAVE)) continue; t = alf->pn; *s++ = '<'; while (*t) *s++ = *t++; *s = ' '; t = s+10; p = alf->posn; while (p) { *t-- = (char)((p % 10) + '0'); p /= 10; } while (s < t) *t-- = ' '; s += 11; if (MemoryFile->flags & bCRLF) *s++ = '\r'; *s++ = '\n'; } END /* StoreVMF only needs the eof pointer and file name, and WindThread eliminates all files anyway so that there is little reason to do more at this point. */ lf->eof = s; (void)strcpy(lf->pn,MemoryFile->pn); O_StoreVMF(lf,oMEMORY,NULL); return NULL; END /*---------------------------------------------------------------- MemoryCmd (window, command, operand) incorporates the capabilities required to establish and maintain the background, foreground, font, geometry, and mode for windows opened by an application. It's called from CreateWND for the NONE command to establish the initial window attributes and, in this context, executes CD and I/O direction commands qualified by the window name. It's called from Command to update the memory file for the five attribute commands above. These two roles are closely related because the commands saved in the memory file (Command) are precisely the ones needed when the window is created the next time the application is invoked. Thus, this function implements a capability analogous to the resource management facility in the X Window System and saves attribute commands in the form <window name>*<command> <operands>. Further, when creating a window, the scan of the memory file also takes into account commands that are unqualified or qualified by the subclass name. In selecting the attributes for a new window, precedence is based on the qualifier and the order in the memory file. The location and precedence of the five window attribute commands handled by this function are maintained in an array so that the indexes of these commands must be 1...5. The array elements are of the form 4*<memory file displacement> + <precedence>. When requested to update the memory file, only lines of precedence 3 are updated; otherwise, a new line of precedence 3 is appended. Since this software saves only lines of precedence 3, lines of lower precedence can occur only if inserted manually by the user. */ int O_MemoryCmd (WND *lw, int cmd, char *opnd) BEGIN static long mempoll[5], memonlkr[5], which = 0; int len, k, p; char *s, *t, *b, *e, *c; long f0, f1, *mv; if (! MemoryFile) return 0; /* The first task is to establish the array that should be used to manage the attribute commands. For pollsters and onlookers, the arrays and associated control are static variables embedded in this function; for others, the array in the window struct is used. Thus, pollsters and onlookers have shared memory. This design is quite natural because the window names of pollsters and onlookers are their subclass names. Thus, the arrays correspond to the subclasses rather than the individual windows and should be shared by all windows in the subclass. */ if (lw->flags & bPOLL) mv = mempoll; else if (lw->flags & bONLKR) mv = memonlkr; else mv = lw->mem; len = strlen(lw->wndname); /* MemoryCmd (window, command, operand) calls come only from Command after a BACKGROUND, FOREGROUND, FONT, GEOMETRY, or MODE command has been successfully executed. But the operand passed to MemoryCmd must be a simple string so that, as appropriate, the command operands must be assembled by Command before calling this function. The process is quite straightforward because it's simply a matter of computing the length of the line required for the qualified command, allocating space for it as necessary, and generating it from the window name, command index, and operand. The complete command word is accessed in the command table. Because this code assumes that the command table can be accessed using the command index for these attribute commands and the indexes must be 1...5, this code imposes restrictions on the command table. If a precedence 3 line already exists, it's updated if it's long enough; otherwise, it's marked for deletion. If a line of lower precedence is currently being used; it's no longer applicable and is effectively discarded from consideration. New lines are allocated in units of 64 bytes to provide for future updates. Rather than find space to build the line and then append it to the memory file, AppendVMF is used to allocate uninitialized space and the line is built in place with padding blanks. The array is updated for the new command. Color names are always enclosed in quotes because X has multiword color names. This is not necessary for Windows or OS/2 because the code in SetAttribute takes care of multiword names. */ if (oBACKGROUND <= cmd && cmd <= oMODE) BEGIN if (!(MemoryFile->flags & bUPDATE)) return 1; len += strlen(c = CommandTable[cmd-1].cmd) + strlen(opnd) + 4; if ((mv[cmd-1] & 3) == 3) { s = b = MemoryFile->base + (size_t)(mv[cmd-1] >> 2); for (e = b; *e != '\n'; ++e); if (*(e-1) == '\r') --e; if (len > (int)(e-b)) { *b = '-'; b = NULL; }} else b = NULL; if (! b) { len = ((len + 63) >> 6) << 6; s = b = O_AppendVMF(NULL,MemoryFile,NULL,len); if (! b) return 0; e = b + len; } t = lw->wndname; while (*t) *s++ = *t++; *s++ = '*'; while (*c) *s++ = *c++; *s++ = ' '; /* Command word */ if (cmd <= oFOREGROUND) *s++ = '"'; while (*opnd) *s++ = *opnd++; /* Operand */ if (cmd <= oFOREGROUND) *s++ = '"'; while (s < e) *s++ = ' '; /* Padding? */ mv[cmd-1] = ((b - MemoryFile->base) << 2) + 3; END /* MemoryCMD (window,NONE,NULL) calls come only from CreateWND, and the objective is to establish the attributes of the window being created. There are two sections of code to handle these calls: the first when creating a pollster or an onlooker after one has already been created, i.e., when the shared attribute commands are already known, and the second in all other situations. When the shared command array is known for the subclass, creating a subsequent window in the subclass is easy because the memory file need not be scanned. Because the window and subclass names are the same for pollsters and onlookers, the precedence of the commands saved in the array is either 1 or 3. The pertinent commands are located using the displacements in the memory array. After delimiting the line, the qualifier is skipped without inspection since it was parsed successfully before. Most of the work is actually done by ParseCommand and SetAttribute. GEOMETRY commands must be treated differently because the geometry string cannot be processed until the font has been set; it's saved in the window's typeahead buffer so that CreateWND can process it. For a GEOMETRY command, SetAttribute checks only the syntax; thus erroneous geometries do not displace legitimate ones. Although the qualifier and command index are not (re)checked, an operand error will result in the line being marked for deletion. */ else if (cmd == oNONE && (lw->flags & which)) for (k = oBACKGROUND-1; k < oMODE; ++k) BEGIN if (! mv[k]) continue; b = MemoryFile->base + (size_t)(mv[k] >> 2); for (e = b; *e != '\n'; ++e); if ((int)(mv[k] & 3) == 3) b += len + 1; cmd = O_ParseCommand(b,e,lw->tmp); if (O_SetAttribute(lw,cmd,lw->tmp)) { if (cmd == oGEOMETRY) (void)strcpy(lw->extra,lw->tmp); } else { *b = '-'; mv[k] = 0L; } END /* When the memory file must be scanned for the applicable commands, the process is a bit longer. As in the preceding section, GEOMETRY commands are checked syntactically and saved in the typeahead buffer. But MODE commands also require special treatment because SetAttribute actually changes the window flags. Thus the flags are saved and restored so that they correspond to the MODE command of highest precedence. For this reason, Command always provides an operand containing the state of all MODE flags when a MODE command is saved in the memory file. When scanning the memory file, lines beginning with a whitespace character, a minus, a less than, or an at-sign are ignored. The first are comments; the second, lines marked for deletion; and the last, file status lines. Otherwise, the line must be delineated, and its precedence must be computed. For pollsters and onlookers, a single compare suffices because the window and subclass names are the same. For others, two compares are required. */ else if (cmd == oNONE) BEGIN f1 = f0 = lw->flags; for (k = oBACKGROUND-1; k < oMODE; ++k) mv[k] = 0L; for (e = MemoryFile->base; e < MemoryFile->eof; ++e) BEGIN if (isspace(*e) || *e == '-' || *e == '<' || *e == '@') { while (*e != '\n') ++e; continue; } for (p = 1, b = c = e; *e != '\n'; ++e) if (*e == ' ' || *e == '*' || *e == '.') break; if (*e == ' ') while (*(++e) != '\n'); else if (*e != '\n') { k = (int)(e - b); c = e+1; while (*(++e) != '\n'); if (k == len && ! strncmp(b,lw->wndname,k)) p = 3; else if (lw->flags & (bPOLL|bONLKR)) continue; else if (k == 5 && ! strncmp(b,"Other",k)) p = 2; else continue; } /* Once the command portion of the line has been delineated, it's fodder for ParseCommand, which takes care of the line termination issue automatically. If ParseCommand fails or the command has no operand, the line is ignored. For the attribute commands, the precedence must be compared to the command currently in effect. Nevertheless, except for MODE and GEOMETRY commands, multiple attribute commands may be executed by SetAttribute. For BACKGROUND, FOREGROUND, and FONT commands, each undoes the effect of the previous one so that calling SetAttribute suffices for them; but, for MODE and GEOMETRY commands, special care is necessary as explained earlier. If SetAttribute succeeds, any prior line of precedence 3 is marked for deletion; if it fails, the line is marked for deletion. */ cmd = O_ParseCommand(c,e,lw->tmp); if (oBACKGROUND <= cmd && cmd <= oMODE && *(lw->tmp)) { if (p < (mv[cmd-1] & 3)) continue; if (O_SetAttribute(lw,cmd,lw->tmp)) { if (cmd == oGEOMETRY) (void)strcpy(lw->extra,lw->tmp); else if (cmd == oMODE) { f1 = lw->flags; lw->flags = f0; } if ((mv[cmd-1] & 3) == 3) *(MemoryFile->base + (size_t)(mv[cmd-1] >> 2)) = '-'; mv[cmd-1] = ((b - MemoryFile->base) << 2) + p; } else *b = '-'; } /* For others, only CD and I/O direction commands qualified by the window name are processed; otherwise, the line is ignored. If Command fails, e.g., an invalid operand, the line is marked for deletion. Multiple CDs are cumulative, which is similar to the treatment of unqualified CDs by MemoryLoad. For output direction, everything is handled by Command. For input direction, some special action is necessary because Command rejects input direction commands if it's already directed and because Command loads the necessary file if successful. Before calling Command, input direction is saved and reset so that the prior direction can be restored if Command fails and the previous file destroyed if Command succeeds. */ else if (p == 3 && *(lw->tmp) && lw->flags & bOTHER) { if (cmd == oCD || cmd == oSTORE || cmd == oAPPEND) { if (O_Command(lw,cmd,lw->tmp) == 2) continue; } else if (cmd == oLOAD || cmd == oCONTINUE) { VMF *lf; lf = lw->din; lw->din = NULL; if (O_Command(lw,cmd,lw->tmp) == 2) { if (lf) O_DestroyVMF(lf); continue; } lw->din = lf; } *b = '-'; } END /* of memory file scan.*/ /* With the scan completed, the flags corresponding to the highest precedence MODE command can be placed in the window struct. The control variable for the static memory arrays is also updated. */ lw->flags = f1; which |= (lw->flags & (bPOLL|bONLKR)); END return 1; END /*---------------------------------------------------------------- MemoryKey (key, string) maintains function key string assignments in lines of the form F<key number> "<string>" and is called only by Command. ParseCommand parses the operand as the longest, blank-delimited string and removes any initial or terminal quote, which thus allows initial or trailing blanks. For this reason, this function always uses an initial and terminal quote (") and, therefore, doesn't worry about whether there are or are not leading or trailing blanks. This is not the same as a C string since embedded characters are not examined. If the operand string is "-", any existing string assignment is discarded. This notation is consistent with that used to delete I/O direction and imposes no significant restrictions. The displacements of the function key commands are maintained in the function key array, which is set by MemoryLoad when it scans the memory file and subsequently maintained by this function. When the user presses a function key, this array is examined to see if a string has been assigned, i.e., the element is non-negative. If so, it's used to locate the command in the memory file, and then ParseCommand is used to obtain the string. The function keys F1...F12 are expanded to 48 based on the states of the Shift and Ctrl keys: F1...F12 correspond to 1...12, Shift+ to 13...24, Ctrl+ to 25...36, and Shift+Ctrl+ to 37...48. The mapping from 1...48 to the array indexes is n -> 4(n-1) - 47((n-1)/12) because the array is organized first on the key states and then on the key. Thus, the first four entries correspond to F1, Shift+F1, Ctrl+F1, and Shift+Ctrl+F1. This organization is consistent with the other key tables, and only the software sees this mapping. */ int O_MemoryKey (int key, char *opnd) BEGIN int len, k; char *s, *b, *e; /* Like all of the Memory... functions, nothing is done if there is no memory file or the arguments look amiss. The line consists of F<digit><digit>, a space, and the string enclosed in quotes("). Thus the line requires operand length + 6 characters. */ if (! MemoryFile || ! opnd) return 0; len = strlen(opnd) + 6; if (key & FKFB) key -= FKFB; if (key < 1 || key > 48) return 0; k = 4*(key-1) - 47*((key-1)/12); b = (FcnKeyStr[k] >=0 ) ? MemoryFile->base + FcnKeyStr[k] : NULL; /* The existing assignment, if any, is deleted if the operand is "-". If the function key has already been assigned a string, the space for the current string is used for the new string if sufficient; otherwise, the existing line is marked for deletion. If it's not sufficient and if space is not available, the net effect will be to delete the current assignment, and Command notes the failure. */ if (*opnd == '-' && !(*(opnd+1))) { if (b) { FcnKeyStr[k] = -1; *b = '-'; } return 1; } else if (b) { for (e = b; *e != '\n'; ++e); if (*(e-1) == '\r') --e; if (len <= (int)(e-b)) len = (int)(e-b); else { FcnKeyStr[k] = -1; *b = '-'; b = NULL; }} /* New lines are allocated in units of 64 bytes to try to minimize the necessity for allocating new lines, which may be a poor tactic for some users and great for others. AppendVMF is used to obtain uninitialized space so that the command can be assembled in place. */ if (! b) { len = ((len + 63) >> 6) << 6; b = O_AppendVMF(NULL,MemoryFile,NULL,len); if (! b) return 0; } /* Whether old or new, the len bytes starting at b are sufficient for the command; blanks are appended after the terminal quote to fill the existing or uninitialized space. */ s = b; *s++ = 'F'; *s++ = (char)((key/10) + '0'); *s++ = (char)((key % 10) + '0'); *s++ = ' '; *s++ = '"'; while (*opnd) *s++ = *opnd++; *s++ = '"'; while (s < b + len) *s++ = ' '; FcnKeyStr[k] = (int)(b - MemoryFile->base); return 1; END /*---------------------------------------------------------------- MemoryVMF (VM file, oLOAD|oSTORE|oMEMORY) maintains status/position information for files loaded from the file system since the application started in lines of the form {<|@}<complete path name> <position>. The initial character indicates whether the file is loaded (<) or has been destroyed (@); while the position is the displacement of the window, i.e., the first character of the top line displayed in the window, when the file is initially loaded. The LOAD (LoadVMF) calls search the memory file and set the posn component in the VM file struct if a record for the file is found; if no record is found, one is appended to the memory file. The first character is set to a '<' to show that the file is loaded. The STORE (DestroyVMF) calls update the record with the current position and change the first character to an '@' to show that the file is not loaded. The record should exist but is created if it doesn't. The MEMORY (MemoryStore) calls are similar to a STORE except that the first character is set to a '<' to show that the file should be loaded when the memory file is loaded for the next invocation of the application. At present, this mechanism is used only by the Onlooker application and provides it the ability to restore its state when last terminated. */ char *O_MemoryVMF (VMF *lf, int op) BEGIN long k; int len; char *s, *t, *b, *e; /* If there is no memory file or if the argument is the memory file or an unnamed file, then no record is made. */ if (! MemoryFile || MemoryFile == lf) return NULL; len = strlen(lf->pn); if (! len) return NULL; /* This scan looks for the longest line that corresponds to the file and marks for deletion all other such lines, which allows for the possibility that the user may have inserted one or more lines manually. This function generates a line just long enough for the initial character, the path name, a blank, and a 10 digit position. If two lines of the same length occur, the later one is used on the theory it implies more recent data given the way the memory file is managed. */ for (b = e = s = MemoryFile->base; s < MemoryFile->eof; ++s) { for (t = s; *s != '\n'; ++s); k = (long)(s-t); if (!(*t == '<' || *t == '@')) continue; if (k <= (len+1) || *(t+len+1) != ' ') continue; if (strncmp(t+1,lf->pn,len)) continue; if (k < (long)(e-b)) { *t = '-'; continue; } if (b != e) *b = '-'; b = t; e = s; } if (b != e) { *b = (char)((op == oSTORE) ? '@' : '<'); if (*(e-1) == '\r') --e; } /* LOAD requests are easy if the search found a line for the file, but the posn component is set only if the displacement is positive and less than the size of the file, which may have changed since the line was generated by this software. */ if (op == oLOAD && b != e) { for (s = b+len+1; *s == ' '; ++s); for (k = 0; isdigit(*s); ++s) k = 10*k + (*s - '0'); if (s != e && *s != ' ') k = 0; if (k > 0 && k < lf->size) lf->posn = k; return b; } /* If control is still here, the task is to update the existing line (b,e) or to append a new one. When using an existing line, the position may or may not fit in the available space so that it may be necessary to mark the line for deletion and allocate a new one. */ if (b != e) { t = b+len+1; s = e-1; k = lf->posn; while (k && t < s) { *s-- = (char)((k % 10) + '0'); k /= 10; } if (! k) { while (t < s) *s-- = ' '; return b; } *b = '-'; } /* Whatever the problem was, the solution now is to append a new line for the file. To enable future updates to the new line, enough space is allocated for a 10 digit (32 bit) displacement, which is sufficient even for files for which this software is not intended, i.e., extremely large. Uninitialized space is requested by calling AppendVMF, and the line is constructed in place with the position right justified. Right justifying the position is efficient here but doesn't do much for the readability of the memory file because the position alignments vary with the length of the file name. */ b = O_AppendVMF(NULL,MemoryFile,NULL,len+12); if (! b) return NULL; *b = (char)((op == oSTORE) ? '@' : '<'); for (s = b+1, t = lf->pn; *t; *s++ = *t++); *(t = s) = ' '; s += 10; k = lf->posn; while (k) { *s-- = (char)((k % 10) + '0'); k /= 10; } while (t < s) *s-- = ' '; return b; END