Index: /trunk/makefile
===================================================================
--- /trunk/makefile	(revision 8)
+++ /trunk/makefile	(revision 8)
@@ -0,0 +1,18 @@
+
+#
+# makefile:
+#
+#       simple makefile which basically only goes into the
+#       src\helpers subdirectory.
+#
+
+PROJECT_BASE_DIR = $(MAKEDIR)
+
+INCLUDE = $(PROJECT_BASE_DIR)\include\helpers;$(INCLUDE)
+
+all:
+    @cd src\helpers
+    nmake -nologo "MAINMAKERUNNING=YES"
+    @cd ..\..
+
+
Index: /trunk/src/helpers/animate.c
===================================================================
--- /trunk/src/helpers/animate.c	(revision 8)
+++ /trunk/src/helpers/animate.c	(revision 8)
@@ -0,0 +1,347 @@
+
+/*
+ *@@sourcefile animate.c:
+ *      contains a bit of helper code for animations.
+ *
+ *      This is a new file with V0.81. Most of this code used to reside
+ *      in common.c with previous versions.
+ *      Note that with V0.9.0, the code for icon animations has been
+ *      moved to the new comctl.c file.
+ *
+ *      Usage: All PM programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  anm*   Animation helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\animate.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_DOSDEVICES
+#define INCL_DOSDEVIOCTL
+
+#define INCL_WIN
+#define INCL_WINSYS
+
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPIPRIMITIVES
+#define INCL_GPIBITMAPS             // added UM 99-10-22; needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\animate.h"
+
+#include "helpers\datetime.h"
+#include "helpers\winh.h"
+#include "helpers\gpih.h"
+
+/*
+ *@@category: Helpers\PM helpers\Animation helpers
+ */
+
+/*
+ *@@ anmBlowUpBitmap:
+ *      this displays an animation based on a given bitmap.
+ *      The bitmap is "blown up" in that it is continually
+ *      increased in size until the original size is reached.
+ *      The animation is calculated so that it lasts exactly
+ *      ulAnimation milliseconds, no matter how fast the
+ *      system is.
+ *
+ *      This function does not return until the animation
+ *      has completed.
+ *
+ *      You should run this routine in a thread with higher-
+ *      than-normal priority, because otherwise the kernel
+ *      might interrupt the thread, causing a somewhat jerky
+ *      display.
+ *
+ *      Returns the count of animation steps that were drawn.
+ *      This is dependent on the speed of the system.
+ */
+
+BOOL anmBlowUpBitmap(HPS hps,               // in: from WinGetScreenPS(HWND_DESKTOP)
+                     HBITMAP hbm,           // in: bitmap to be displayed
+                     ULONG ulAnimationTime) // in: total animation time (ms)
+{
+    ULONG   ulrc = 0;
+    ULONG   ulInitialTime,
+            ulNowTime;
+            // ulCurrentSize = 10;
+
+    if (hps)
+    {
+        POINTL  ptl = {0, 0};
+        RECTL   rtlStretch;
+        ULONG   ul,
+                ulSteps = 20;
+        BITMAPINFOHEADER bih;
+        GpiQueryBitmapParameters(hbm, &bih);
+        /* ptl.y = WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN)
+                        - BMPSPACING
+                        - bih.cy; */
+        ptl.x = (WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN)
+                        - bih.cx) / 2;
+        ptl.y = (WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN)
+                        - bih.cy) / 2;
+
+        // we now use ul for the current animation step,
+        // which is a pointer on a scale from 1 to ulAnimationTime;
+        // ul will be recalculated after each animation
+        // step according to how much time the animation
+        // has cost on this display so far. This has the
+        // following advantages:
+        // 1) no matter how fast the system is, the animation
+        //    will always last ulAnimationTime milliseconds
+        // 2) since large bitmaps take more time to calculate,
+        //    the animation won't appear to slow down then
+        ulInitialTime = dtGetULongTime();
+        ul = 1;
+        ulSteps = 1000;
+        do {
+            LONG cx = (((bih.cx-20) * ul) / ulSteps) + 20;
+            LONG cy = (((bih.cy-20) * ul) / ulSteps) + 20;
+            rtlStretch.xLeft   = ptl.x + ((bih.cx - cx) / 2);
+            rtlStretch.yBottom = ptl.y + ((bih.cy - cy) / 2);
+            rtlStretch.xRight = rtlStretch.xLeft + cx;
+            rtlStretch.yTop   = rtlStretch.yBottom + cy;
+
+            WinDrawBitmap(hps, hbm, NULL, (PPOINTL)&rtlStretch,
+                    0, 0,       // we don't need colors
+                    DBM_STRETCH);
+
+            ulNowTime = dtGetULongTime();
+
+            // recalculate ul: rule of three based on the
+            // time we've spent on animation so far
+            ul = (ulSteps
+                    * (ulNowTime - ulInitialTime)) // time spent so far
+                    / ulAnimationTime;      // time to spend altogether
+
+            ulrc++;                         // return count
+
+        } while (ul < ulSteps);
+
+        // finally, draw the 1:1 version
+        WinDrawBitmap(hps, hbm, NULL, &ptl,
+                0, 0,       // we don't need colors
+                DBM_NORMAL);
+
+    } // end if (hps)
+
+    return (ulrc);
+}
+
+#define LAST_WIDTH 2
+#define LAST_STEPS 50
+#define WAIT_TIME  10
+
+/* ******************************************************************
+ *                                                                  *
+ *   Other animations                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ anmPowerOff:
+ *      displays an animation that looks like a
+ *      monitor being turned off; hps must have
+ *      been acquired using WinGetScreenPS,
+ *      ulSteps should be around 40-50.
+ */
+
+VOID anmPowerOff(HPS hps, ULONG ulSteps)
+{
+    RECTL       rclScreen, rclNow, rclLast, rclDraw;
+    ULONG       ul = ulSteps;
+    ULONG       ulPhase = 1;
+
+    WinQueryWindowRect(HWND_DESKTOP, &rclScreen);
+
+    WinShowPointer(HWND_DESKTOP, FALSE);
+
+    memcpy(&rclLast, &rclScreen, sizeof(RECTL));
+
+    // In order to draw the animation, we tell apart three
+    // "phases", signified by the ulPhase variable. While
+    // ulPhase != 99, we stay in a do-while loop.
+    ul = 0;
+    ulPhase = 1;
+
+    do {
+        ULONG ulFromTime = dtGetULongTime();
+
+        if (ulPhase == 1)
+        {
+            // Phase 1: "shrink" the screen by drawing black
+            // rectangles from the edges towards the center
+            // of the screen. With every loop, we draw the
+            // rectangles a bit closer to the center, until
+            // the center is black too. Sort of like this:
+
+            //          ***************************
+            //          *       black             *
+            //          *                         *
+            //          *      .............      *
+            //          *      . rclNow:   .      *
+            //          *  ->  . untouched .  <-  *
+            //          *      .............      *
+            //          *            ^            *
+            //          *            !            *
+            //          ***************************
+
+            // rclNow contains the rectangle _around_ which
+            // the black rectangles are to be drawn. With
+            // every iteration, rclNow is reduced in size.
+            rclNow.xLeft = ((rclScreen.yTop / 2) * ul / ulSteps );
+            rclNow.xRight = (rclScreen.xRight) - rclNow.xLeft;
+            rclNow.yBottom = ((rclScreen.yTop / 2) * ul / ulSteps );
+            rclNow.yTop = (rclScreen.yTop) - rclNow.yBottom;
+
+            if (rclNow.yBottom > (rclNow.yTop - LAST_WIDTH) ) {
+                rclNow.yBottom = (rclScreen.yTop / 2) - LAST_WIDTH;
+                rclNow.yTop = (rclScreen.yTop / 2) + LAST_WIDTH;
+            }
+
+            // draw black rectangle on top of rclNow
+            rclDraw.xLeft = rclLast.xLeft;
+            rclDraw.xRight = rclLast.xRight;
+            rclDraw.yBottom = rclNow.yTop;
+            rclDraw.yTop = rclLast.yTop;
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            // draw black rectangle left of rclNow
+            rclDraw.xLeft = rclLast.xLeft;
+            rclDraw.xRight = rclNow.xLeft;
+            rclDraw.yBottom = rclLast.yBottom;
+            rclDraw.yTop = rclLast.yTop;
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            // draw black rectangle right of rclNow
+            rclDraw.xLeft = rclNow.xRight;
+            rclDraw.xRight = rclLast.xRight;
+            rclDraw.yBottom = rclLast.yBottom;
+            rclDraw.yTop = rclLast.yTop;
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            // draw black rectangle at the bottom of rclNow
+            rclDraw.xLeft = rclLast.xLeft;
+            rclDraw.xRight = rclLast.xRight;
+            rclDraw.yBottom = rclLast.yBottom;
+            rclDraw.yTop = rclNow.yBottom;
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            // remember rclNow for next iteration
+            memcpy(&rclLast, &rclNow, sizeof(RECTL));
+
+            // done with "shrinking"?
+            if ( rclNow.xRight < ((rclScreen.xRight / 2) + LAST_WIDTH) ) {
+                ulPhase = 2; // exit
+            }
+
+        } else if (ulPhase == 2) {
+            // Phase 2: draw a horizontal white line about
+            // where the last rclNow was. This is only
+            // executed once.
+
+            //          ***************************
+            //          *       black             *
+            //          *                         *
+            //          *                         *
+            //          *        -----------      *
+            //          *                         *
+            //          *                         *
+            //          *                         *
+            //          ***************************
+
+            rclDraw.xLeft = (rclScreen.xRight / 2) - LAST_WIDTH;
+            rclDraw.xRight = (rclScreen.xRight / 2) + LAST_WIDTH;
+            rclDraw.yBottom = (rclScreen.yTop * 1 / 4);
+            rclDraw.yTop = (rclScreen.yTop * 3 / 4);
+            WinFillRect(hps, &rclDraw, CLR_WHITE);
+
+            ulPhase = 3;
+            ul = 0;
+
+        } else if (ulPhase == 3) {
+            // Phase 3: make the white line shorter with
+            // every iteration by drawing black rectangles
+            // above it. These are drawn closer to the
+            // center with each iteration.
+
+            //          ***************************
+            //          *       black             *
+            //          *                         *
+            //          *                         *
+            //          *       ->  ----  <-      *
+            //          *                         *
+            //          *                         *
+            //          *                         *
+            //          ***************************
+
+            rclDraw.xLeft = (rclScreen.xRight / 2) - LAST_WIDTH;
+            rclDraw.xRight = (rclScreen.xRight / 2) + LAST_WIDTH;
+            rclDraw.yTop = (rclScreen.yTop * 3 / 4);
+            rclDraw.yBottom = (rclScreen.yTop * 3 / 4) - ((rclScreen.yTop * 1 / 4) * ul / LAST_STEPS);
+            if (rclDraw.yBottom < ((rclScreen.yTop / 2) + LAST_WIDTH))
+                rclDraw.yBottom = ((rclScreen.yTop / 2) + LAST_WIDTH);
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            rclDraw.xLeft = (rclScreen.xRight / 2) - LAST_WIDTH;
+            rclDraw.xRight = (rclScreen.xRight / 2) + LAST_WIDTH;
+            rclDraw.yBottom = (rclScreen.yTop * 1 / 4);
+            rclDraw.yTop = (rclScreen.yTop * 1 / 4) + ((rclScreen.yTop * 1 / 4) * ul / LAST_STEPS);
+            if (rclDraw.yTop > ((rclScreen.yTop / 2) - LAST_WIDTH))
+                rclDraw.yBottom = ((rclScreen.yTop / 2) - LAST_WIDTH);
+
+            WinFillRect(hps, &rclDraw, CLR_BLACK);
+
+            ul++;
+            if (ul > LAST_STEPS) {
+                ulPhase = 99;
+            }
+        }
+
+        ul++;
+
+        while (dtGetULongTime() < ulFromTime + WAIT_TIME) {
+            // PSZ p = NULL; // keep compiler happy
+        }
+    } while (ulPhase != 99);
+
+    // sleep a while
+    DosSleep(500);
+    WinFillRect(hps, &rclScreen, CLR_BLACK);
+    DosSleep(500);
+
+    WinShowPointer(HWND_DESKTOP, TRUE);
+}
+
+
+
Index: /trunk/src/helpers/cctl_chart.c
===================================================================
--- /trunk/src/helpers/cctl_chart.c	(revision 8)
+++ /trunk/src/helpers/cctl_chart.c	(revision 8)
@@ -0,0 +1,1275 @@
+
+/*
+ *@@sourcefile cctl_chart.c:
+ *      implementation for the "chart" common control.
+ *      See comctl.c for an overview.
+ *
+ *      This has been extracted from comctl.c with V0.9.3 (2000-05-21) [umoeller].
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ *@@added V0.9.3 (2000-05-21) [umoeller].
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Chart control
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Chart Control                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctlCreateChartBitmap:
+ *      this creates a new bitmap and paints the
+ *      chart into it. This bitmap will be used
+ *      in WM_PAINT of ctl_fnwpChart to quickly paint
+ *      the chart image.
+ *
+ *      The bitmap will be created with the specified
+ *      size. The chart will consume all available
+ *      space in the bitmap. The returned bitmap is
+ *      not selected into any presentation space.
+ *
+ *      Note: Description text will be drawn with the
+ *      current font specified for the memory PS, and
+ *      with the current character cell box.
+ *
+ *      This function gets called automatically from
+ *      WM_PAINT if we determine that the bitmap for
+ *      painting has not been created yet or has been
+ *      invalidated (because data or styles changed).
+ *
+ *      However, you can also use this function
+ *      independently to have a chart bitmap created,
+ *      wherever you might need it.
+ *      You will then have to fill in the CHARTDATA
+ *      and CHARTSTYLE structures before calling
+ *      this function.
+ *
+ *      If (paRegions != NULL), this function will
+ *      create GPI regions for data item. Each GPI
+ *      region will then contain the outline of the
+ *      corresponding pie chart slice. This allows
+ *      you to easily relate coordinates to pie slieces,
+ *      for example for mouse clicks.
+ *
+ *      In that case, paRegions must point to an array
+ *      of HRGN items, each of which will contain a
+ *      region handle later. It is the responsibility
+ *      of the caller to free the regions later, using
+ *      GpiDestroyRegion on each region handle (with
+ *      the hpsMem specified with this function).
+ *
+ *      If you're not interested in the regions, set
+ *      paRegions to NULL.
+ *
+ *      This returns NULLHANDLE if an error occured.
+ *      This can mean the following:
+ *      --  The data is invalid, because the total is 0.
+ *      --  The bitmap could not be created (memory?).
+ */
+
+HBITMAP ctlCreateChartBitmap(HPS hpsMem,                // in: memory PS to use for bitmap creation
+                             LONG lcx,                  // in: width of bitmap
+                             LONG lcy,                  // in: height of bitmap
+                             PCHARTDATA pChartData,     // in: chart data
+                             PCHARTSTYLE pChartStyle,   // in: chart style
+                             LONG lBackgroundColor,     // in: color around the chart (RGB)
+                             LONG lTextColor,           // in: description text color (RGB)
+                             HRGN* paRegions)           // out: GPI regions for each data item
+{
+    HBITMAP hbmReturn = NULLHANDLE;
+
+    // sum up the values to get the total
+    ULONG   ul = 0;
+    double  dTotal = 0;
+    double*  pdThis = pChartData->padValues;
+    for (ul = 0; ul < pChartData->cValues; ul++)
+    {
+        dTotal += *pdThis;
+        pdThis++;
+    }
+
+    // avoid division by zero
+    if (dTotal > 0)
+    {
+        RECTL       rclWholeStatic;
+        ULONG       ulYBottomNow = 0;
+
+        // get window rectangle (bottom left is 0, 0)
+        rclWholeStatic.xLeft = 0;
+        rclWholeStatic.yBottom = 0;
+        rclWholeStatic.xRight = lcx;
+        rclWholeStatic.yTop = lcy;
+
+        // create bitmap of that size
+        if ((hbmReturn = gpihCreateBitmap(hpsMem,
+                                          rclWholeStatic.xRight,
+                                          rclWholeStatic.yTop)))
+        {
+            // successfully created:
+
+            // allocate array for storing text positions later
+            PPOINTL     paptlDescriptions = (PPOINTL)malloc(sizeof(POINTL)
+                                                            * pChartData->cValues);
+            POINTL      ptlCenter;
+
+            FIXED       fxPieSize = (LONG)(pChartStyle->dPieSize * 65536),
+                        fxDescriptions = (LONG)(pChartStyle->dDescriptions * 65536);
+
+            // associate bitmap with memory PS
+            GpiSetBitmap(hpsMem, hbmReturn);
+
+            // switch the HPS to RGB mode
+            gpihSwitchToRGB(hpsMem);
+
+            // fill bitmap with static's background color
+            gpihBox(hpsMem,
+                    DRO_FILL,
+                    &rclWholeStatic,
+                    lBackgroundColor);
+
+            // We'll paint into the bitmap in two loops:
+            // +--  The outer "3D" loop is executed
+            // |    pChartStyle->ulThickness-fold, if
+            // |    CHS_3Dxxx has been enabled; otherwise
+            // |    just once.
+            // |
+            // |    +-- The inner "slice" loop goes thru the
+            // |        data fields in pChartData and draws
+            // |        the pies accordingly.
+            // |
+            // +--  We then increase the base Y point (ulYBottomNow)
+            //      by one and draw again, thereby getting the 3D
+            //      effect.
+
+            // 1) outer 3D loop
+            do // while (   (pChartStyle->ulStyle & CHS_3D_BRIGHT)...
+            {
+                RECTL       rclArc;
+                PLONG       plColorThis;
+                PSZ         *ppszDescriptionThis = NULL;
+                PPOINTL     pptlDescriptionThis;
+                HRGN*       phRegionThis;
+
+                ARCPARAMS   ap;
+                AREABUNDLE  ab;
+
+                double      dStartAngle = pChartData->usStartAngle,
+                            dSweepAngle = 0;
+
+                // this is only TRUE for the last loop
+                BOOL        fNowDrawingSurface =
+                              (
+                                   ((pChartStyle->ulStyle & CHS_3D_BRIGHT) == 0)
+                                ||
+                                   (ulYBottomNow == pChartStyle->ulThickness - 1)
+                              );
+
+                // // _Pmpf(("Looping, ulYBottomNow: %d", ulYBottomNow));
+
+                // calculate pie rectangle for this loop;
+                // this is the size of the static control
+                // minus the "3D thickness", if enabled
+                memcpy(&rclArc, &rclWholeStatic, sizeof(RECTL));
+                if (pChartStyle->ulStyle & CHS_3D_BRIGHT)
+                        // this includes CHS_3D_DARKEN
+                {
+                    rclArc.yBottom = rclWholeStatic.yBottom
+                                                + ulYBottomNow;
+                    rclArc.yTop = rclWholeStatic.yTop
+                                                - pChartStyle->ulThickness
+                                                + ulYBottomNow;
+                }
+
+                // calculate center point
+                ptlCenter.x = rclArc.xRight / 2;
+                ptlCenter.y = ((rclArc.yTop - rclArc.yBottom) / 2) + ulYBottomNow;
+
+                // Now, the "arc" APIs really suck. The following
+                // has cost me hours of testing to find out:
+
+                // a) The arc functions expect some kind of
+                //    "default arc" to be defined, which they
+                //    refer to. We define the arc as elliptical;
+                //    this will be used as the "current arc"
+                //    for subsequent arc calls.
+                //    (P, S) and (R, Q) define the end points
+                //    of the major axes of the ellipse.
+                //    The center of the arc will later be
+                //    specified with GpiPartialArc (while GpiFullArc
+                //    uses the current pen position...
+                //    Who created these APIs?!? This might be a most
+                //    flexible way to do things, but where's the
+                //    simple stuff?!?)
+                ap.lP = ptlCenter.x;        // X-axis X
+                ap.lS = 0;                  // X-axis Y
+                ap.lR = 0;                  // Y-axis X
+                ap.lQ = ((rclArc.yTop - rclArc.yBottom) / 2);
+                                            // Y-axis Y
+                GpiSetArcParams(hpsMem, &ap);
+
+                // b)  The line primitives determine lines
+                //     to be drawn around the pie slices.
+                //     We don't want any.
+                GpiSetLineType(hpsMem, LINETYPE_INVISIBLE);
+
+                // c)  Strangely, GpiSetPattern does work,
+                //     while GpiSetColor doesn't (see below).
+                GpiSetPattern(hpsMem, PATSYM_SOLID);
+
+                // initialize per-item data pointers for
+                // loop below
+                pdThis = pChartData->padValues;
+                plColorThis = pChartData->palColors;
+                ppszDescriptionThis = pChartData->papszDescriptions;
+                pptlDescriptionThis = paptlDescriptions;
+                phRegionThis = paRegions;
+
+                // 2) inner "pie slice loop":
+                // this loop goes over the data pointers
+                // and paints accordingly. At the end of
+                // the loop, we'll advance all those pointers.
+                for (ul = 0; ul < pChartData->cValues; ul++)
+                {
+                    HRGN        hrgnThis;
+                    SHORT       sSweepAngle,
+                                sStartAngle;
+
+                    // calculate the angle to sweep to:
+                    // a simple rule of three
+                    dSweepAngle = *pdThis           // current data pointer
+                                  * pChartData->usSweepAngle
+                                                    // maximum angle
+                                  / dTotal;         // total data sum
+
+                    // d)  And now comes the real fun part.
+                    //     GpiPartialArc is too dumb to draw
+                    //     anything on its own, it must _always_
+                    //     appear within an area or path definition.
+                    //     Unfortunately, this isn't really said
+                    //     clearly anywhere.
+                    //     Even worse, in order to set the color
+                    //     with which the slice is to be drawn,
+                    //     one has to define an AREABUNDLE because
+                    //     the regular GpiSetColor functions don't
+                    //     seem to work here. Or maybe it's my fault,
+                    //     but with this awful documentation, who knows.
+                    //     We use the current color defined in the
+                    //     pie chart data (this pointer was set above
+                    //     and will be advanced for the next slice).
+                    ab.lColor = *plColorThis;
+
+                    // "3D mode" enabled with darkened socket?
+                    if (    (pChartStyle->ulStyle & CHS_3D_DARKEN)
+                            // not last loop?
+                         && (!fNowDrawingSurface)
+                       )
+                        // darken the current fill color
+                        // by halving each color component
+                        gpihManipulateRGB(&ab.lColor,
+                                          1,        // multiplier
+                                          2);       // divisor
+
+                    // set the area (fill) color
+                    GpiSetAttrs(hpsMem,
+                                PRIM_AREA,
+                                ABB_COLOR,
+                                0,
+                                (PBUNDLE)&ab);
+
+                    GpiSetCurrentPosition(hpsMem, &ptlCenter);
+
+                    // round the angle values properly
+                    sStartAngle = (SHORT)(dStartAngle + .5);
+                    sSweepAngle = (SHORT)(dSweepAngle + .5);
+
+                    // now draw the pie slice;
+                    // we could use an area, but since we need
+                    // to remember the coordinates of the slice
+                    // for mouse click handling, we require a
+                    // region. But areas cannot be converted
+                    // to regions, so we use a path instead.
+                    GpiBeginPath(hpsMem,
+                                 1);    // path ID, must be 1
+                    GpiPartialArc(hpsMem,
+                                  &ptlCenter,
+                                  fxPieSize,    // calculated from CHARTSTYLE
+                                  MAKEFIXED(sStartAngle, 0),
+                                  MAKEFIXED(sSweepAngle, 0));
+                        // this moves the current position to the outer
+                        // point on the ellipse which corresponds to
+                        // sSweepAngle
+                    GpiEndPath(hpsMem);
+
+                    // convert the path to a region;
+                    // we'll need the region for mouse hit testing later
+                    hrgnThis = GpiPathToRegion(hpsMem, 1,
+                                               FPATH_ALTERNATE);
+                        // after this, the path is deleted
+                    GpiPaintRegion(hpsMem, hrgnThis);
+                    if (phRegionThis)
+                        // region output requested by caller:
+                        // store region, the caller will clean this up
+                        *phRegionThis = hrgnThis;
+                    else
+                        // drop region
+                        GpiDestroyRegion(hpsMem, hrgnThis);
+
+                    // descriptions enabled and last run?
+                    if (    (ppszDescriptionThis)
+                         && (fNowDrawingSurface)
+                       )
+                    {
+                        if (*ppszDescriptionThis)
+                        {
+                            // yes: calculate position to paint
+                            // text at later (we can't do this now,
+                            // because it might be overpainted by
+                            // the next arc again)
+
+                            GpiSetCurrentPosition(hpsMem, &ptlCenter);
+                            // move the current position to
+                            // the center outer point on the ellipse
+                            // (in between sStartAngle and sSweepAngle);
+                            // since we're outside an area, this will not
+                            // paint anything
+                            GpiPartialArc(hpsMem,
+                                          &ptlCenter,
+                                          fxDescriptions, // calculated from CHARTSTYLE
+                                          MAKEFIXED(sStartAngle, 0),
+                                            // only half the sweep now:
+                                          MAKEFIXED(sSweepAngle / 2, 0));
+
+                            // store this outer point in the array
+                            // of description coordinates for later
+                            GpiQueryCurrentPosition(hpsMem, pptlDescriptionThis);
+                        }
+                    }
+
+                    // increase the start angle by the sweep angle for next loop
+                    dStartAngle += dSweepAngle;
+
+                    // advance the data pointers
+                    pdThis++;
+                    plColorThis++;
+                    if (ppszDescriptionThis)
+                        ppszDescriptionThis++;
+                    pptlDescriptionThis++;
+                    if (phRegionThis)
+                        phRegionThis++;
+                } // end for (ul...)
+
+                // go for next "3D thickness" iteration
+                ulYBottomNow++;
+            } while (   (pChartStyle->ulStyle & CHS_3D_BRIGHT)
+                     && (ulYBottomNow < pChartStyle->ulThickness)
+                    );
+
+            // now paint descriptions
+            if (pChartStyle->ulStyle & CHS_DESCRIPTIONS)
+            {
+                // we use two pointers during the iteration,
+                // which point to the item corresponding
+                // to the current data item:
+                // 1)  pointer to center point on outer border
+                //     of partial arc
+                //     (calculated above)
+                PPOINTL     pptlDescriptionThis = paptlDescriptions;
+                // 2)  pointer to current description string
+                PSZ*        ppszDescriptionThis = pChartData->papszDescriptions;
+
+                // description strings valid?
+                if (ppszDescriptionThis)
+                {
+                    // set presentation color
+                    GpiSetColor(hpsMem, lTextColor);
+
+                    // set text aligment to centered
+                    // both horizontally and vertically;
+                    // this affects subsequent GpiCharStringAt
+                    // calls in that the output text will
+                    // be centered around the specified
+                    // point
+                    GpiSetTextAlignment(hpsMem,
+                                        TA_CENTER,      // horizontally
+                                        TA_HALF);       // center vertically
+
+                    // loop thru data items
+                    for (ul = 0;
+                         ul < pChartData->cValues;
+                         ul++)
+                    {
+                        POINTL  ptlMiddlePoint;
+
+                        // when drawing the arcs above, we have,
+                        // for each pie slice, stored the middle
+                        // point on the outer edge of the ellipse
+                        // in the paptlDescriptions POINTL array:
+
+                        //                ++++
+                        //                +    +
+                        //                +      +
+                        //    ptlCenter\  +       +
+                        //              \ +        + <-- current partial arc
+                        //               \+        +
+                        //     +++++++++++X        +
+                        //     +                   +
+                        //      +                  +
+                        //       +               XX  <-- point calculated above
+                        //        +             +
+                        //           +        +
+                        //             ++++++
+
+                        // now calculate a middle point between
+                        // that outer point on the ellipse and
+                        // the center of the ellipse, which will
+                        // be the center point for the text
+
+                        //                ++++
+                        //                +    +
+                        //                +      +
+                        //    ptlCenter\  +       +
+                        //              \ +        + <-- current partial arc
+                        //               \+        +
+                        //     ++++++++++++        +
+                        //     +             XX    + <-- new middle point
+                        //      +                  +
+                        //       +               XX  <-- point calculated above
+                        //        +             +
+                        //           +        +
+                        //             ++++++
+
+                        ptlMiddlePoint.x =
+                                ptlCenter.x
+                                + ((pptlDescriptionThis->x - ptlCenter.x) * 2 / 3);
+                        ptlMiddlePoint.y =
+                                ptlCenter.y
+                                - (ptlCenter.y - pptlDescriptionThis->y) * 2 / 3;
+
+                        // FINALLY, draw the description
+                        // at this point; since we have used
+                        // GpiSetTextAlignment above, the
+                        // text will be centered on exactly
+                        // that point
+                        GpiCharStringAt(hpsMem,
+                                        &ptlMiddlePoint,
+                                        strlen(*ppszDescriptionThis),
+                                        *ppszDescriptionThis);
+
+                        pptlDescriptionThis++;
+                        ppszDescriptionThis++;
+                    } // end for (ul = 0; ul < pChartData->cValues; ul++)
+                } // end if (ppszDescriptionThis)
+            } // end if (pChartStyle->ulStyle & CHS_DESCRIPTIONS)
+
+            // cleanup
+            free(paptlDescriptions);
+
+            // deselect (free) bitmap
+            GpiSetBitmap(hpsMem, NULLHANDLE);
+        } // end if (pChtCData->hbmChart ...)
+    } // end if (dTotal > 0)
+
+    return (hbmReturn);
+}
+
+/*
+ *@@ CleanupBitmap:
+ *      this frees the resources associated with
+ *      the chart bitmap.
+ *
+ *@@changed V0.9.2 (2000-02-29) [umoeller]: fixed maaajor memory leak
+ */
+
+VOID CleanupBitmap(PCHARTCDATA pChtCData)
+{
+    if (pChtCData)
+    {
+        // bitmap already created?
+        if (pChtCData->hbmChart)
+        {
+            // free current bitmap
+            // GpiSetBitmap(pChtCData->hpsMem, NULLHANDLE);
+            // delete bitmap; fails if not freed!
+            GpiDeleteBitmap(pChtCData->hbmChart);
+            pChtCData->hbmChart = NULLHANDLE;
+        }
+
+        // destroy regions, but not the array itself
+        // (this is done in CleanupData)
+        if (pChtCData->paRegions)
+        {
+            ULONG   ul;
+            HRGN    *phRegionThis = pChtCData->paRegions;
+            for (ul = 0;
+                 ul < pChtCData->cd.cValues;
+                 ul++)
+            {
+                if (*phRegionThis)
+                {
+                    GpiDestroyRegion(pChtCData->hpsMem, *phRegionThis);
+                    *phRegionThis = NULLHANDLE;
+                }
+                phRegionThis++;
+            }
+        }
+    }
+}
+
+/*
+ *@@ CleanupData:
+ *      this frees all allocated resources of the chart
+ *      control, except the bitmap (use CleanupBitmap
+ *      for that) and the CHARTCDATA itself.
+ *
+ *      Note: CleanupBitmap must be called _before_ this
+ *      function.
+ */
+
+VOID CleanupData(PCHARTCDATA pChtCData)
+{
+    if (pChtCData)
+        if (pChtCData->cd.cValues)
+        {
+            // _Pmpf(("Cleaning up data"));
+            // values array
+            if (pChtCData->cd.padValues)
+                free(pChtCData->cd.padValues);
+
+            // colors array
+            if (pChtCData->cd.palColors)
+                free(pChtCData->cd.palColors);
+
+            // strings array
+            if (pChtCData->cd.papszDescriptions)
+            {
+                ULONG   ul;
+                PSZ     *ppszDescriptionThis = pChtCData->cd.papszDescriptions;
+                for (ul = 0;
+                     ul < pChtCData->cd.cValues;
+                     ul++)
+                {
+                    if (*ppszDescriptionThis)
+                        free(*ppszDescriptionThis);
+
+                    ppszDescriptionThis++;
+                }
+
+                free(pChtCData->cd.papszDescriptions);
+            }
+
+            pChtCData->cd.cValues = 0;
+
+            // GPI regions array
+            if (pChtCData->paRegions)
+            {
+                free(pChtCData->paRegions);
+                pChtCData->paRegions = NULL;
+            }
+        }
+}
+
+/*
+ *@@ SetChartData:
+ *      implementation for CHTM_SETCHARTDATA
+ *      in ctl_fnwpChart.
+ *
+ *@@added V0.9.2 (2000-02-29) [umoeller]
+ */
+
+VOID SetChartData(HWND hwndChart,
+                  PCHARTCDATA pChtCData,
+                  PCHARTDATA pcdNew)
+{
+    ULONG        ul = 0;
+    PSZ          *ppszDescriptionSource,
+                 *ppszDescriptionTarget;
+
+    // free previous values, if set
+    if (pChtCData->hbmChart)
+        CleanupBitmap(pChtCData);
+
+    CleanupData(pChtCData);
+
+    // _Pmpf(("Setting up data"));
+
+    if (pChtCData->hpsMem == NULLHANDLE)
+    {
+        // first call:
+        // create a memory PS for the bitmap
+        SIZEL szlPage = {0, 0};
+        gpihCreateMemPS(WinQueryAnchorBlock(hwndChart),
+                        &szlPage,
+                        &pChtCData->hdcMem,
+                        &pChtCData->hpsMem);
+        // _Pmpf(("Created HPS 0x%lX", pChtCData->hpsMem));
+        // _Pmpf(("Created HDC 0x%lX", pChtCData->hdcMem));
+    }
+
+    pChtCData->cd.usStartAngle = pcdNew->usStartAngle;
+    pChtCData->cd.usSweepAngle = pcdNew->usSweepAngle;
+    pChtCData->cd.cValues = pcdNew->cValues;
+
+    // copy values
+    pChtCData->cd.padValues = (double*)malloc(sizeof(double) * pcdNew->cValues);
+    memcpy(pChtCData->cd.padValues,
+           pcdNew->padValues,
+           sizeof(double) * pcdNew->cValues);
+
+    // copy colors
+    pChtCData->cd.palColors = (LONG*)malloc(sizeof(LONG) * pcdNew->cValues);
+    memcpy(pChtCData->cd.palColors,
+           pcdNew->palColors,
+           sizeof(LONG) * pcdNew->cValues);
+
+    // copy strings
+    pChtCData->cd.papszDescriptions = (PSZ*)malloc(sizeof(PSZ) * pcdNew->cValues);
+    ppszDescriptionSource = pcdNew->papszDescriptions;
+    ppszDescriptionTarget = pChtCData->cd.papszDescriptions;
+    for (ul = 0;
+         ul < pcdNew->cValues;
+         ul++)
+    {
+        if (*ppszDescriptionSource)
+            *ppszDescriptionTarget = strdup(*ppszDescriptionSource);
+        else
+            *ppszDescriptionTarget = NULL;
+        ppszDescriptionSource++;
+        ppszDescriptionTarget++;
+    }
+
+    // create an array of GPI region handles
+    pChtCData->paRegions = (HRGN*)malloc(sizeof(HRGN) * pcdNew->cValues);
+    // initialize all regions to null
+    memset(pChtCData->paRegions, 0, sizeof(HRGN) * pcdNew->cValues);
+
+    pChtCData->lSelected = -1;     // none selected
+
+    WinInvalidateRect(hwndChart, NULL, FALSE);
+}
+
+/*
+ *@@ PaintChart:
+ *      implementation for WM_PAINT
+ *      in ctl_fnwpChart.
+ *
+ *@@added V0.9.2 (2000-02-29) [umoeller]
+ */
+
+VOID PaintChart(HWND hwndChart,
+                PCHARTCDATA pChtCData,
+                HPS hps,
+                PRECTL prclPaint)
+{
+    RECTL   rclStatic;
+    WinQueryWindowRect(hwndChart, &rclStatic);
+
+    // _Pmpf(("ctl_fnwpChart: WM_PAINT, cValues: %d", pChtCData->cd.cValues));
+
+    // do we have any values yet?
+    if (pChtCData->cd.cValues == 0)
+    {
+        CHAR szDebug[200];
+        sprintf(szDebug, "Error, no values set");
+        WinFillRect(hps,
+                    &rclStatic,
+                    CLR_WHITE);
+        WinDrawText(hps,
+                    strlen(szDebug),
+                    szDebug,
+                    &rclStatic,
+                    CLR_BLACK,
+                    CLR_WHITE,
+                    DT_LEFT | DT_TOP);
+    }
+    else
+    {
+        // valid values, apparently:
+        LONG lForegroundColor = winhQueryPresColor(hwndChart,
+                                                   PP_FOREGROUNDCOLOR,
+                                                   TRUE,       // inherit presparams
+                                                   SYSCLR_WINDOWTEXT);
+
+        // yes: check if we created the bitmap
+        // already
+        if (pChtCData->hbmChart == NULLHANDLE)
+        {
+            // no: do it now
+            HPOINTER    hptrOld = winhSetWaitPointer();
+
+            // get presentation font
+            FONTMETRICS FontMetrics;
+            LONG        lPointSize;
+            LONG lLCIDSet = gpihFindPresFont(hwndChart,
+                                             TRUE,      // inherit PP
+                                             pChtCData->hpsMem,
+                                             "8.Helv",
+                                             &FontMetrics,
+                                             &lPointSize);
+            // set presentation font
+            if (lLCIDSet)
+            {
+                GpiSetCharSet(pChtCData->hpsMem, lLCIDSet);
+                if (FontMetrics.fsDefn & FM_DEFN_OUTLINE)
+                    gpihSetPointSize(pChtCData->hpsMem, lPointSize);
+            }
+
+
+            gpihSwitchToRGB(hps);
+
+            pChtCData->hbmChart = ctlCreateChartBitmap(
+                      pChtCData->hpsMem,        // mem PS
+                      rclStatic.xRight,         // cx
+                      rclStatic.yTop,           // cy
+                      &pChtCData->cd,           // data
+                      &pChtCData->cs,           // style
+                      // background color:
+                      winhQueryPresColor(hwndChart,
+                                         PP_BACKGROUNDCOLOR,
+                                         TRUE,       // inherit presparams
+                                         SYSCLR_DIALOGBACKGROUND),
+                      // description text color:
+                      lForegroundColor,
+                      pChtCData->paRegions);
+                              // out: regions array
+            // _Pmpf(("Created bitmap 0x%lX", pChtCData->hbmChart));
+
+            // unset and delete font
+            GpiSetCharSet(pChtCData->hpsMem, LCID_DEFAULT);
+            if (lLCIDSet)
+                GpiDeleteSetId(pChtCData->hpsMem, lLCIDSet);
+
+            WinSetPointer(HWND_DESKTOP, hptrOld);
+        }
+
+        if (pChtCData->hbmChart)
+        {
+            POINTL  ptlDest = { 0, 0 };
+            WinDrawBitmap(hps,
+                          pChtCData->hbmChart,
+                          NULL,     // draw whole bitmap
+                          &ptlDest,
+                          0, 0,     // colors (don't care)
+                          DBM_NORMAL);
+
+            // do we have the focus?
+            if (pChtCData->fHasFocus)
+                // something selected?
+                if (pChtCData->lSelected != -1)
+                    if (pChtCData->paRegions)
+                    {
+                        HRGN* pRegionThis = pChtCData->paRegions; // first region
+                        pRegionThis += pChtCData->lSelected;      // array item
+
+                        if (*pRegionThis)
+                        {
+                            SIZEL   sl = {2, 2};
+                            GpiSetColor(hps, lForegroundColor);
+                            GpiFrameRegion(hps,
+                                           *pRegionThis,
+                                           &sl);
+                        }
+                    }
+        }
+    }
+}
+
+/*
+ *@@ ctl_fnwpChart:
+ *      window procedure for the "chart" control.
+ *
+ *      This is not a stand-alone window procedure, but must only
+ *      be used with static controls subclassed by ctlChartFromStatic.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-02-29) [umoeller]: added resize support
+ *@@changed V0.9.2 (2000-02-29) [umoeller]: fixed baaad PM resource leaks, the bitmap was never freed
+ */
+
+MRESULT EXPENTRY ctl_fnwpChart(HWND hwndChart, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT     mrc = 0;
+    PCHARTCDATA   pChtCData = (PCHARTCDATA)WinQueryWindowULong(hwndChart, QWL_USER);
+
+    if (pChtCData)
+    {
+        PFNWP OldStaticProc = pChtCData->OldStaticProc;
+
+        switch (msg)
+        {
+            /*
+             *@@ CHTM_SETCHARTDATA:
+             *      user msg to set the pie chart data,
+             *      which is copied to the control's
+             *      memory.
+             *      This can be sent several times to
+             *      have the chart refreshed with new
+             *      values.
+             *
+             *      Parameters:
+             *      --  PCHARTDATA mp1: new values.
+             *              These will be copied to the chart's internal data.
+             *      --  mp2: unused.
+             */
+
+            case CHTM_SETCHARTDATA:
+                // _Pmpf(("CHTM_SETCHARTDATA, mp1: 0x%lX", mp1));
+                if (mp1)
+                {
+                    PCHARTDATA   pcdNew = (PCHARTDATA)mp1;
+                    SetChartData(hwndChart, pChtCData, pcdNew);
+                }
+            break;
+
+            /*
+             *@@ CHTM_SETCHARTSTYLE:
+             *      user msg to set the chart style,
+             *      which is copied to the control's
+             *      memory.
+             *      This can be sent several times to
+             *      have the chart refreshed with new
+             *      styles.
+             *
+             *      Parameters:
+             *      --  PCHARTSTYLE mp1: new style data.
+             *              This will be copied to the chart's internal data.
+             *      --  mp2: unused.
+             */
+
+            case CHTM_SETCHARTSTYLE:
+                // _Pmpf(("CHTM_SETCHARTSTYLE, mp1: 0x%lX", mp1));
+                memcpy(&(pChtCData->cs), mp1, sizeof(CHARTSTYLE));
+                if (pChtCData->hbmChart)
+                {
+                    CleanupBitmap(pChtCData);
+                    WinInvalidateRect(hwndChart, NULL, FALSE);
+                }
+            break;
+
+            /*
+             *@@ CHTM_ITEMFROMPOINT:
+             *      this can be _sent_ to the chart control
+             *      to query the chart item which surrounds
+             *      the given point. Specify the coordinates
+             *      in two SHORT's in mp1 (as with WM_BUTTON1*
+             *      messages), in window coordinates.
+             *
+             *      Parameters:
+             *      -- SHORT SHORT1FROMMP(mp1): x
+             *      -- SHORT SHORT1FROMMP(mp1): y
+             *      -- mp2: unused.
+             *
+             *      Returns:
+             *      -- LONG: data item index (counting from
+             *               0) or -1 if none, e.g. if no
+             *               data has been set yet or if
+             *               the point is outside the pie
+             *               slices.
+             *
+             *@@added V0.9.2 (2000-02-29) [umoeller]
+             */
+
+            case CHTM_ITEMFROMPOINT:
+            {
+                LONG lRegionFound = -1; // none
+
+                // get mouse coordinates
+                POINTL ptlMouse;
+                ptlMouse.x = SHORT1FROMMP(mp1);
+                ptlMouse.y = SHORT2FROMMP(mp1);
+
+                // data set?
+                if (pChtCData->cd.cValues)
+                {
+                    // regions defined?
+                    if (pChtCData->paRegions)
+                    {
+                        ULONG   ul;
+                        HRGN*   phRegionThis = pChtCData->paRegions;
+                        for (ul = 0;
+                             ul < pChtCData->cd.cValues;
+                             ul++)
+                        {
+                            if (*phRegionThis)
+                            {
+                                if (GpiPtInRegion(pChtCData->hpsMem,
+                                                  *phRegionThis,
+                                                  &ptlMouse)
+                                     == PRGN_INSIDE)
+                                {
+                                    // _Pmpf(("Clicked in region %d", ul));
+                                    lRegionFound = ul;
+                                    break;
+                                }
+                            }
+                            phRegionThis++;
+                        }
+
+                    }
+                }
+
+                mrc = (MPARAM)lRegionFound;
+            break; }
+
+            /*
+             * WM_BUTTON1DOWN:
+             *
+             */
+
+            case WM_BUTTON1DOWN:
+                if (pChtCData->cs.ulStyle & CHS_SELECTIONS)
+                {
+                    LONG lRegionFound = (LONG)WinSendMsg(hwndChart,
+                                                         CHTM_ITEMFROMPOINT,
+                                                         mp1,
+                                                         NULL);
+
+                    // selections allowed:
+                    // we then accept WM_CHAR msgs, so
+                    // we need the focus
+                    WinSetFocus(HWND_DESKTOP, hwndChart);
+                        // this invalidates the window
+                    if (pChtCData->lSelected != lRegionFound)
+                    {
+                        // selection changed:
+                        pChtCData->lSelected = lRegionFound;
+                        // repaint
+                        WinInvalidateRect(hwndChart, NULL, FALSE);
+                    }
+                }
+                else
+                    // no selections allowed:
+                    // just activate the window
+                    WinSetActiveWindow(HWND_DESKTOP, hwndChart);
+                        // the parent frame gets activated this way
+                mrc = (MPARAM)TRUE;
+            break;
+
+            /*
+             * WM_SETFOCUS:
+             *      we might need to redraw the selection.
+             */
+
+            case WM_SETFOCUS:
+                if (pChtCData->cs.ulStyle & CHS_SELECTIONS)
+                {
+                    // selections allowed:
+                    pChtCData->fHasFocus = (BOOL)mp2;
+                    if (pChtCData->lSelected != -1)
+                        WinInvalidateRect(hwndChart, NULL, FALSE);
+                }
+            break;
+
+            /*
+             * WM_WINDOWPOSCHANGED:
+             *
+             */
+
+            case WM_WINDOWPOSCHANGED:
+            {
+                // this msg is passed two SWP structs:
+                // one for the old, one for the new data
+                // (from PM docs)
+                PSWP pswpNew = (PSWP)mp1;
+                // PSWP pswpOld = pswpNew + 1;
+
+                // resizing?
+                if (pswpNew->fl & SWP_SIZE)
+                    if (pChtCData->hbmChart)
+                    {
+                        // invalidate bitmap so that
+                        // it will be recreated with new size
+                        CleanupBitmap(pChtCData);
+                        WinInvalidateRect(hwndChart, NULL, FALSE);
+                    }
+
+                // return default NULL
+            break; }
+
+            /*
+             * WM_PRESPARAMCHANGED:
+             *
+             */
+
+            case WM_PRESPARAMCHANGED:
+                if (pChtCData->hbmChart)
+                {
+                    // invalidate bitmap so that
+                    // it will be recreated with new
+                    // fonts and colors
+                    CleanupBitmap(pChtCData);
+                    WinInvalidateRect(hwndChart, NULL, FALSE);
+                }
+            break;
+
+            /*
+             * WM_PAINT:
+             *      paint the chart bitmap, which is created
+             *      if necessary (calling ctlCreateChartBitmap).
+             */
+
+            case WM_PAINT:
+            {
+                RECTL   rclPaint;
+                HPS     hps = WinBeginPaint(hwndChart,
+                                            NULLHANDLE, // obtain cached micro-PS
+                                            &rclPaint);
+
+                PaintChart(hwndChart,
+                           pChtCData,
+                           hps,
+                           &rclPaint);
+
+                WinEndPaint(hps);
+                mrc = 0;
+            break; }
+
+            /*
+             * WM_DESTROY:
+             *      clean up resources
+             */
+
+            case WM_DESTROY:
+                CleanupBitmap(pChtCData);
+                CleanupData(pChtCData);
+
+                // _Pmpf(("Destroying HPS 0x%lX", pChtCData->hpsMem));
+                if (!GpiDestroyPS(pChtCData->hpsMem));
+                    // _Pmpf(("  Error!"));
+                // _Pmpf(("Destroying HDC 0x%lX", pChtCData->hdcMem));
+                if (!DevCloseDC(pChtCData->hdcMem));
+                    // _Pmpf(("  Error!"));
+                free(pChtCData);
+
+                mrc = (*OldStaticProc)(hwndChart, msg, mp1, mp2);
+            break;
+
+            default:
+                mrc = (*OldStaticProc)(hwndChart, msg, mp1, mp2);
+        }
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ ctlChartFromStatic:
+ *      this function turns an existing static text control into
+ *      a chart control (for visualizing data) by subclassing its
+ *      window procedure with ctl_fnwpChart.
+ *
+ *      This way you can easily create a chart control as a static
+ *      control in any Dialog Editor;
+ *      after loading the dlg template, simply call this function
+ *      with the hwnd of the static control to make it a chart.
+ *
+ *      The pie chart consumes all available space in the static control.
+ *
+ *      In XWorkplace, this is used for the pie chart on the new
+ *      XFldDisk "Details" settings page to display the free space
+ *      on a certain drive.
+ *
+ *      Note: even though you can use _any_ type of static control
+ *      with this function, you should use a static _text_ control,
+ *      because not all types of static controls react to fonts and
+ *      colors dragged upon them. The static _text_ control does.
+ *
+ *      <B>Chart data:</B>
+ *
+ *      The pie chart control operates on an array of "double" values.
+ *      Each value in that array corresponds to a color in a second
+ *      array of (LONG) RGB values and, if description texts are
+ *      enabled, to a third array of PSZ's.
+ *
+ *      The data on which the pie chart operates is initialized to
+ *      be void, so that the pie chart will not paint anything
+ *      initially. In order to have the pie chart display something,
+ *      post or send a CHTM_SETCHARTDATA message (comctl.h) to the static
+ *      control after it has been subclassed.
+ *
+ *      CHTM_SETCHARTDATA takes a CHARTDATA structure (comctl.h) in mp1,
+ *      which must contain the chart data and corresponding colors to be
+ *      displayed.
+ *
+ *      The total sum of the "double" values will represent the angle in
+ *      CHARTDATA.usSweepAngle.
+ *
+ *      For example, if two values of 50 and 100 are passed to the
+ *      control and usSweepAngle is 270 (i.e. a three-quarter pie),
+ *      the chart control will calculate the following:
+ *
+ *      1)  The sum of the data is 150.
+ *
+ *      2)  The first sub-arc will span an angle of 270 * (50/150)
+ *          = 90 degrees.
+ *
+ *      3)  The second sub-arc will span an angle of 270 * (100/150)
+ *          = 180 degrees.
+ *
+ *      You can also have descriptions displayed along the different
+ *      chart items by specifying CHARTDATA.papszDescriptions and
+ *      setting the CHS_DESCRIPTIONS flag (below).
+ *
+ *      <B>Chart styles:</B>
+ *
+ *      Use CHTM_SETCHARTSTYLE with a PCHARTSTYLE (comctl.h) in mp1.
+ *      This can be sent to the chart control several times.
+ *
+ *      Presently, only pie charts are implemented. However, we do
+ *      have several "sub-styles" for pie charts:
+ *      -- CHS_3D_BRIGHT: paint a "3D" socket below the actual chart.
+ *      -- CHS_3D_DARKEN: like CHS_3D_BRIGHT, but the socket will be made
+ *                        darker compared to the surface.
+ *
+ *      General styles:
+ *      -- CHS_DESCRIPTIONS: show descriptions on the chart
+ *                           (CHARTDATA.papszDescriptions data).
+ *      -- CHS_SELECTIONS: allow pie chart slices to be selectable,
+ *                         using the mouse and the keyboard.
+ *
+ *      <B>Display:</B>
+ *
+ *      The chart control creates an internal bitmap for the display
+ *      only once (ctlCreateChartBitmap). This bitmap is refreshed if
+ *      neccessary, e.g. because chart data or styles have changed.
+ *
+ *      The chart control uses presentation parameters, as listed below.
+ *      Presentation parameters are inherited from the parent window.
+ *      If a presparam is not set, the corresponding system color is
+ *      used. The following color pairs are recognized:
+ *
+ *      --  PP_BACKGROUNDCOLOR / SYSCLR_DIALOGBACKGROUND:
+ *          background of the chart control (outside the chart).
+ *      --  PP_FOREGROUNDCOLOR / SYSCLR_WINDOWTEXT:
+ *          text color, if description texts are enabled and valid.
+ *      --  PP_FONTNAMESIZE:
+ *          text font, if description texts are enabled and valid.
+ *          If this presparam is not set, the system font is used.
+ *
+ *      The control reacts to fonts and colors dropped upon it, if
+ *      it has been subclassed from a static _text_ control (see above).
+ *      It also recalculates the bitmap when it's resized.
+ *
+ *      <B>Example usage:</B>
+ *
+ +          // get static control:
+ +          HWND    hwndChart = WinWindowFromID(hwndDialog, ID_...);
+ +          CHARTSTYLE      cs;
+ +          CHARTDATA       cd;
+ +          // define data:
+ +          double          adData[3] = { 100, 200, 300 };
+ +          // define corresponding colors:
+ +          LONG            alColors[3] = { 0x800000, 0x008000, 0x000080 };
+ +          // define correspdonding descriptions:
+ +          PSZ             apszDescriptions[3] = { "item 1", "item 3", "item 3" };
+ +
+ +          ctlChartFromStatic(hwndChart);     // create chart
+ +
+ +          cs.ulStyle = CHS_3D_DARKEN | CHS_DESCRIPTIONS;
+ +          cs.ulThickness = 20;
+ +          WinSendMsg(hwndChart, CHTM_SETCHARTSTYLE, &cs, NULL);
+ +
+ +          cd.usStartAngle = 15;       // start at 15ø from right
+ +          cd.usSweepAngle = 270;      // three-quarter pie (for the sum of the
+ +                                      // above values: 100+200+300 = 600)
+ +          cd.cValues = 3;             // array count
+ +          cd.padValues = &adData[0];
+ +          cd.palColors = &alColors[0];
+ +          cd.papszDescriptions = &apszDescriptions[0];
+ +          WinSendMsg(hwndChart, CHTM_SETCHARTDATA, &cd, NULL);
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL ctlChartFromStatic(HWND hwndChart)     // in: static control to subclass
+{
+    if (hwndChart)
+    {
+        PFNWP OldStaticProc = WinSubclassWindow(hwndChart, ctl_fnwpChart);
+
+        if (OldStaticProc)
+        {
+            PCHARTCDATA pChtCData = (PCHARTCDATA)malloc(sizeof(CHARTCDATA));
+            memset(pChtCData, 0, sizeof(CHARTCDATA));
+            pChtCData->OldStaticProc = OldStaticProc;
+            pChtCData->fHasFocus = FALSE;
+            WinSetWindowPtr(hwndChart, QWL_USER, pChtCData);
+            return (TRUE);
+        }
+    }
+
+    return (FALSE);
+}
+
+
Index: /trunk/src/helpers/cctl_checkcnr.c
===================================================================
--- /trunk/src/helpers/cctl_checkcnr.c	(revision 8)
+++ /trunk/src/helpers/cctl_checkcnr.c	(revision 8)
@@ -0,0 +1,997 @@
+
+/*
+ *@@sourcefile cctl_checkcnr.c:
+ *      implementation for the checkbox container common control.
+ *      See comctl.c for an overview.
+ *
+ *      This has been extracted from comctl.c with V0.9.3 (2000-05-21) [umoeller].
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ *@@added V0.9.3 (2000-05-21) [umoeller].
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Checkbox containers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+// linked list of CHEXKBOXCNROWNER struct pointers
+HMTX        G_hmtxCnrOwnersList = 0;
+PLINKLIST   G_pllCnrOwners = 0;
+
+/* ******************************************************************
+ *                                                                  *
+ *   Checkbox container record cores                                *
+ *                                                                  *
+ ********************************************************************/
+
+HBITMAP  G_hbmCheckboxes = NULLHANDLE;
+ULONG    G_cxCheckbox = 0;
+
+/*
+ *@@ ctlDrawCheckbox:
+ *
+ *      The system checkbox bitmap (G_hbmCheckboxes)
+ *      is organized into 4 columns and three rows as
+ *      follows:
+ *
+ +                         not depressed     depressed
+ +                       ÚÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄ¿
+ +         checkbox      ³       ³  chk   ³       ³  chk  ³
+ +                       ÃÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄŽ
+ +         radio button  ³       ³  chk   ³       ³  chk  ³
+ +                       ÃÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄŽ
+ +         chbx indeterm.³       ³  ind   ³       ³  ind  ³
+ +                       ÀÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÙ
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+BOOL ctlDrawCheckbox(HPS hps,               // in: paint PS
+                     LONG x,                // in: target lower left corner x
+                     LONG y,                // in: target lower left corner y
+                     USHORT usRow,          // in: 0 - 2
+                     USHORT usColumn,       // in: 0 - 3
+                     BOOL fHalftoned)       // in: if TRUE, the checkbox will be halftoned (grayed)
+{
+    POINTL  aptl[4];
+    BOOL    brc = FALSE;
+
+    if (G_hbmCheckboxes)
+    {
+        // paint checkbox;
+        LONG lHits;
+
+        memset(aptl, 0, sizeof(POINTL) * 4);
+
+        // aptl[0]: target bottom-left
+        aptl[0].x = x;
+        aptl[0].y = y;
+
+        // aptl[1]: target top-right (inclusive!)
+        aptl[1].x = aptl[0].x + G_cxCheckbox - 1;
+        aptl[1].y = aptl[0].y + G_cxCheckbox - 1;
+
+        // aptl[2]: source bottom-left:
+        // depends on record selection
+        aptl[2].x = usColumn * G_cxCheckbox;
+        aptl[2].y = usRow * G_cxCheckbox;       // top checkbox row
+
+        // aptl[3]: source top-right (non-inclusive!)
+        aptl[3].x = aptl[2].x + G_cxCheckbox;
+        aptl[3].y = aptl[2].y + G_cxCheckbox;
+
+        if (fHalftoned)
+        {
+            HDC     hdc = GpiQueryDevice(hps);
+            HWND    hwnd = WinWindowFromDC(hdc);
+            HAB     hab = WinQueryAnchorBlock(hwnd);
+            HBITMAP hbmHalftoned = gpihCreateHalftonedBitmap(hab,
+                                                             G_hbmCheckboxes,
+                                                             CLR_WHITE);
+            lHits = GpiWCBitBlt(hps,           // hpsTarget
+                                hbmHalftoned,
+                                4,
+                                &aptl[0],
+                                ROP_SRCCOPY,        // mix
+                                0);                 // options
+            GpiDeleteBitmap(hbmHalftoned);
+        }
+        else
+            lHits = GpiWCBitBlt(hps,           // hpsTarget
+                                G_hbmCheckboxes,
+                                4,
+                                &aptl[0],
+                                ROP_SRCCOPY,        // mix
+                                0);                 // options
+        if (lHits == GPI_OK)
+            brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ CHECKBOXCNROWNER:
+ *
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+typedef struct _CHECKBOXCNROWNER
+{
+    HWND        hwndCnr;            // container window handle
+    USHORT      usCnrID;            // container item ID
+    HWND        hwndOwner;          // owner of that container
+    PFNWP       pfnwpCnrOrig;       // original window proc of hwndCnr
+    PFNWP       pfnwpOwnerOrig;     // original window proc of hwndOwner
+
+    HAB         habCnr;
+
+    PCHECKBOXRECORDCORE preccClicked;   // != NULL if mb1 is currently down on recc
+    PCHECKBOXRECORDCORE preccSpace;     // != NULL if space key is down with recc
+    RECTL       rclReccClicked;     // rectangle of that record
+} CHECKBOXCNROWNER, *PCHECKBOXCNROWNER;
+
+/*
+ *@@ CnrCheckboxClicked:
+ *
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+VOID CnrCheckboxClicked(PCHECKBOXCNROWNER pcbco,
+                        PCHECKBOXRECORDCORE precc,
+                        BOOL fToggleAndNotify) // if TRUE, toggle state and notify owner (CN_RECORDCHECKED)
+{
+    if (precc->ulStyle & WS_VISIBLE)
+    {
+        if (precc->recc.flRecordAttr & CRA_DISABLED)
+            // disabled:
+            WinAlarm(HWND_DESKTOP, WA_WARNING);
+        else
+        {
+            if (precc->ulStyle & BS_AUTOCHECKBOX)
+            {
+                if (fToggleAndNotify)
+                    precc->usCheckState = 1 - precc->usCheckState;
+
+                WinSendMsg(pcbco->hwndCnr,
+                           CM_INVALIDATERECORD,
+                           (MPARAM)&precc,
+                           MPFROM2SHORT(1,
+                                        CMA_NOREPOSITION));
+            }
+
+            if (fToggleAndNotify)
+                WinPostMsg(pcbco->hwndOwner,        // was: WinSendMsg
+                           WM_CONTROL,
+                           MPFROM2SHORT(pcbco->usCnrID,
+                                        CN_RECORDCHECKED),
+                           (MPARAM)precc);
+        }
+    }
+}
+
+/*
+ *@@ ctl_fnwpSubclCheckboxCnr:
+ *      window proc for subclassed containers.
+ *      See ctlMakeCheckboxContainer for details.
+ *
+ *@@added V0.9.0 (99-11-29) [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpSubclCheckboxCnr(HWND hwndCnr, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT             mrc = 0;
+    PCHECKBOXCNROWNER   pcbco = 0;
+    PFNWP               pfnwpOrig = 0;
+
+    if (WinRequestMutexSem(G_hmtxCnrOwnersList, 5000) == NO_ERROR)
+    {
+        PLISTNODE   pNode = lstQueryFirstNode(G_pllCnrOwners);
+        while (pNode)
+        {
+            pcbco = (PCHECKBOXCNROWNER)pNode->pItemData;
+            if (pcbco->hwndCnr == hwndCnr)
+            {
+                pfnwpOrig = pcbco->pfnwpOwnerOrig;
+                break;
+            }
+            pNode = pNode->pNext;
+        }
+        DosReleaseMutexSem(G_hmtxCnrOwnersList);
+    }
+
+    if (pfnwpOrig)
+    {
+        switch (msg)
+        {
+            case WM_BUTTON1DOWN:
+            {
+                POINTL ptlClick;
+                ptlClick.x = SHORT1FROMMP(mp1);
+                ptlClick.y = SHORT2FROMMP(mp1);
+
+                // find record whose icon has this point
+                pcbco->preccClicked
+                    = (PCHECKBOXRECORDCORE)cnrhFindRecordFromPoint(hwndCnr,
+                                                                   &ptlClick,
+                                                                   &pcbco->rclReccClicked,
+                                                                   CMA_ICON,
+                                                                   0);
+
+                if (pcbco->preccClicked)
+                {
+                    if (pcbco->preccClicked->ulStyle & WS_VISIBLE)
+                    {
+                        // add "depressed" style
+                        pcbco->preccClicked->ulStyle |= BS_BITMAP;
+                        // mouse was clicked on icon (checkbox):
+                        CnrCheckboxClicked(pcbco,
+                                           pcbco->preccClicked,
+                                           FALSE);
+                    }
+                    else
+                        // not visible:
+                        pcbco->preccClicked = NULL;
+                }
+
+                mrc = pfnwpOrig(hwndCnr, msg, mp1, mp2);
+                        // apperently, the cnr captures the mouse
+            break; }
+
+            case WM_MOUSEMOVE:
+                if (pcbco->preccClicked)
+                {
+                    // mouse moved while checkbox is depressed:
+                    POINTL ptlMove;
+                    ptlMove.x = SHORT1FROMMP(mp1);
+                    ptlMove.y = SHORT2FROMMP(mp1);
+
+                    if (WinPtInRect(pcbco->habCnr,
+                                    &pcbco->rclReccClicked,
+                                    &ptlMove))
+                    {
+                        // mouse is in checkbox:
+                        if ((pcbco->preccClicked->ulStyle & BS_BITMAP) == 0)
+                        {
+                            // checkbox is not drawn depressed:
+                            // add "depressed" style
+                            pcbco->preccClicked->ulStyle |= BS_BITMAP;
+                            CnrCheckboxClicked(pcbco,
+                                               pcbco->preccClicked,
+                                               FALSE);
+                        }
+                    }
+                    else
+                    {
+                        // mouse is outside of checkbox:
+                        if (pcbco->preccClicked->ulStyle & BS_BITMAP)
+                        {
+                            // checkbox is drawn depressed:
+                            // remove "depressed" style
+                            pcbco->preccClicked->ulStyle &= ~BS_BITMAP;
+                            CnrCheckboxClicked(pcbco,
+                                               pcbco->preccClicked,
+                                               FALSE);
+                        }
+                    }
+                }
+            break;
+
+            case WM_BUTTON1UP:
+                if (pcbco->preccClicked)
+                {
+                    if (pcbco->preccClicked->ulStyle & BS_BITMAP)
+                    {
+                        pcbco->preccClicked->ulStyle &= ~BS_BITMAP;
+                        CnrCheckboxClicked(pcbco,
+                                           pcbco->preccClicked,
+                                           TRUE);
+                    }
+
+                    pcbco->preccClicked = NULL;
+                }
+                mrc = pfnwpOrig(hwndCnr, msg, mp1, mp2);
+            break;
+
+            /*
+             * WM_CHAR:
+             *
+             */
+
+            case WM_CHAR:
+            {
+                USHORT fsFlags = SHORT1FROMMP(mp1);
+                CHAR   ch = CHAR1FROMMP(mp2);
+                            // never use the USHORT, because for
+                            // the "key-up" message, the hi-char
+                            // is different (duh...)
+
+                // _Pmpf(("WM_CHAR fsFlags %lX, usch %d", fsFlags, ch));
+
+                if (ch == ' ')
+                {
+                    // space: toggle checkbox
+                    if ((fsFlags & KC_KEYUP) == 0)
+                    {
+                        // space down:
+                        // filter up repetitive key msgs
+                        if (pcbco->preccSpace == NULL)
+                        {
+                            PCHECKBOXRECORDCORE precc = (PCHECKBOXRECORDCORE)WinSendMsg(
+                                                                   hwndCnr,
+                                                                   CM_QUERYRECORDEMPHASIS,
+                                                                   (MPARAM)CMA_FIRST,
+                                                                   (MPARAM)CRA_CURSORED);
+                            if ((precc) && (precc != (PCHECKBOXRECORDCORE)-1))
+                            {
+                                if (precc->ulStyle & WS_VISIBLE)
+                                {
+                                    pcbco->preccSpace = precc;
+
+                                    // add "depressed" style
+                                    pcbco->preccSpace->ulStyle |= BS_BITMAP;
+                                    CnrCheckboxClicked(pcbco,
+                                                       pcbco->preccSpace,
+                                                       FALSE);
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        // space up:
+                        if (pcbco->preccSpace)
+                        {
+                            if (pcbco->preccSpace->ulStyle & BS_BITMAP)
+                            {
+                                pcbco->preccSpace->ulStyle &= ~BS_BITMAP;
+                                CnrCheckboxClicked(pcbco,
+                                                   pcbco->preccSpace,
+                                                   TRUE);
+                            }
+
+                            pcbco->preccSpace = NULL;
+                        }
+                    }
+
+                    // do not pass spaces on
+                    mrc = (MPARAM)TRUE;     // processed
+                    break;
+                }
+
+                mrc = pfnwpOrig(hwndCnr, msg, mp1, mp2);
+            break; }
+
+            /*
+             * WM_DESTROY:
+             *      remove list item; this gets sent
+             *      to the parent first, then the children,
+             *      so we remove this in the container
+             *      (this way the list item is still valid
+             *      in ctl_fnwpSubclCheckboxCnrOwner)
+             */
+
+            case WM_DESTROY:
+                if (WinRequestMutexSem(G_hmtxCnrOwnersList, 5000) == NO_ERROR)
+                {
+                    lstRemoveItem(G_pllCnrOwners, pcbco);
+                    if (lstCountItems(G_pllCnrOwners) == 0)
+                    {
+                        lstFree(G_pllCnrOwners);
+                        G_pllCnrOwners = NULL;
+                    }
+                    DosReleaseMutexSem(G_hmtxCnrOwnersList);
+                }
+
+                mrc = pfnwpOrig(hwndCnr, msg, mp1, mp2);
+            break;
+
+            default:
+                mrc = pfnwpOrig(hwndCnr, msg, mp1, mp2);
+        }
+    }
+    else
+        mrc = WinDefWindowProc(hwndCnr, msg, mp1, mp2);
+
+    return (mrc);
+}
+
+
+/*
+ *@@ ctl_fnwpSubclCheckboxCnrOwner:
+ *      window proc for subclassed container owners.
+ *      See ctlMakeCheckboxContainer for details.
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpSubclCheckboxCnrOwner(HWND hwndOwner, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT             mrc = 0;
+    PCHECKBOXCNROWNER   pcbco = 0;
+    PFNWP               pfnwpOrig = 0;
+
+    if (WinRequestMutexSem(G_hmtxCnrOwnersList, 5000) == NO_ERROR)
+    {
+        PLISTNODE   pNode = lstQueryFirstNode(G_pllCnrOwners);
+        while (pNode)
+        {
+            pcbco = (PCHECKBOXCNROWNER)pNode->pItemData;
+            if (pcbco->hwndOwner == hwndOwner)
+            {
+                pfnwpOrig = pcbco->pfnwpOwnerOrig;
+                break;
+            }
+            pNode = pNode->pNext;
+        }
+        DosReleaseMutexSem(G_hmtxCnrOwnersList);
+    }
+
+    if (pfnwpOrig)
+    {
+        switch (msg)
+        {
+            /*
+             * WM_CONTROL:
+             *
+             */
+
+            case WM_CONTROL:
+            {
+                USHORT  usItemID = SHORT1FROMMP(mp1),
+                        usNotifyCode = SHORT2FROMMP(mp1);
+
+                if (usItemID == pcbco->usCnrID)
+                {
+                    switch (usNotifyCode)
+                    {
+                        case CN_ENTER:
+                        {
+                            PNOTIFYRECORDENTER pnre = (PNOTIFYRECORDENTER)mp2;
+                            PCHECKBOXRECORDCORE precc = (PCHECKBOXRECORDCORE)pnre->pRecord;
+
+                            if (precc)
+                                CnrCheckboxClicked(pcbco,
+                                                   precc,
+                                                   TRUE);
+                        break; }
+                    }
+                }
+
+                mrc = pfnwpOrig(hwndOwner, msg, mp1, mp2);
+            break; }
+
+            /*
+             * WM_DRAWITEM:
+             *      cnr owner draw; this is where we draw
+             *      the checkboxes
+             */
+
+            case WM_DRAWITEM:
+            {
+                USHORT usID = SHORT1FROMMP(mp1);
+                if (usID == pcbco->usCnrID)
+                {
+                    // get generic DRAWITEM structure
+                    POWNERITEM poi = (POWNERITEM)mp2;
+
+                    // _Pmpf(("WM_DRAWITEM poi->idItem %d", poi->idItem));
+
+                    // check if we're to draw the icon
+                    // (and not the text)
+                    if (poi->idItem == CMA_ICON)
+                    {
+                        PCNRDRAWITEMINFO pcdi = (PCNRDRAWITEMINFO)poi->hItem;
+                        PCHECKBOXRECORDCORE precc = (PCHECKBOXRECORDCORE)pcdi->pRecord;
+
+                        if (precc->ulStyle & WS_VISIBLE)
+                        {
+                            USHORT usRow,
+                                   usColumn;
+
+                            switch (precc->usCheckState)
+                            {
+                                case 0: // unchecked
+                                    usRow = 2;
+                                    usColumn = 0;
+                                break;
+
+                                case 1: // checked
+                                    usRow = 2;
+                                    usColumn = 1;
+                                break;
+
+                                case 2: // indeterminate
+                                    usRow = 0;
+                                    usColumn = 1;
+                                break;
+                            }
+
+                            if (precc->ulStyle & BS_BITMAP)
+                                // button currently depressed:
+                                // add two to column
+                                usColumn += 2;
+
+                            ctlDrawCheckbox(poi->hps,
+                                            poi->rclItem.xLeft,
+                                            poi->rclItem.yBottom,
+                                            usRow,
+                                            usColumn,
+                                            // halftoned?
+                                            ((precc->recc.flRecordAttr & CRA_DISABLED) != 0));
+                            mrc = (MPARAM)FALSE;
+                                        // we still need the cnr to draw the
+                                        // emphasis
+                        }
+                        else
+                            mrc = (MPARAM)TRUE; // tell cnr that we've drawn the item;
+                                    // don't even draw emphasis
+                    }
+                    else if (poi->idItem == CMA_TEXT)
+                    {
+                        // for text, buttons etc.:
+                        PCNRDRAWITEMINFO pcdi = (PCNRDRAWITEMINFO)poi->hItem;
+                        PCHECKBOXRECORDCORE precc = (PCHECKBOXRECORDCORE)pcdi->pRecord;
+                        if (precc->recc.flRecordAttr & CRA_DISABLED)
+                        {
+                            RECTL rcl2;
+                            LONG lBackground, lForeground;
+                            ULONG flCmd = DT_LEFT | DT_TOP | DT_ERASERECT;
+                            if ((pcdi->pRecord->flRecordAttr) & CRA_SELECTED)
+                            {
+                                // disabled and selected:
+                                lBackground = WinQuerySysColor(HWND_DESKTOP,
+                                                               SYSCLR_SHADOWTEXT, 0);
+                                lForeground = winhQueryPresColor(poi->hwnd,
+                                                                 PP_BACKGROUNDCOLOR,
+                                                                 FALSE, // no inherit
+                                                                 SYSCLR_WINDOW);
+                            }
+                            else
+                            {
+                                // disabled and not selected:
+                                lBackground = winhQueryPresColor(poi->hwnd,
+                                                                 PP_BACKGROUNDCOLOR,
+                                                                 FALSE,
+                                                                 SYSCLR_WINDOW);
+                                lForeground = winhQueryPresColor(poi->hwnd,
+                                                                 PP_FOREGROUNDCOLOR,
+                                                                 FALSE, // no inherit
+                                                                 SYSCLR_WINDOWTEXT);
+                                flCmd |= DT_HALFTONE;
+                            }
+
+                            // _Pmpf(("back: 0x%lX, fore: 0x%lX", lBackground, lForeground));
+
+                            GpiCreateLogColorTable(poi->hps, 0, LCOLF_RGB, 0, 0, NULL);
+                            GpiSetBackColor(poi->hps, lBackground);
+                            GpiSetColor(poi->hps, lForeground);
+
+                            memcpy(&rcl2, &(poi->rclItem), sizeof(rcl2));
+                            /* WinDrawText(poi->hps,
+                                        strlen(precc->recc.pszTree),
+                                        precc->recc.pszTree,
+                                        &rcl2,
+                                        lForeground, lBackground,
+                                        DT_TEXTATTRS | DT_HALFTONE); */
+                            winhDrawFormattedText(poi->hps,
+                                                  &rcl2,
+                                                  precc->recc.pszTree,
+                                                  flCmd);
+                            mrc = (MPARAM)TRUE;
+                        }
+                        else
+                            // tell cnr to draw the item
+                            mrc = (MPARAM)FALSE;
+                    }
+                    else
+                        // tell cnr to draw the item
+                        mrc = (MPARAM)FALSE;
+                }
+                else
+                    mrc = pfnwpOrig(hwndOwner, msg, mp1, mp2);
+            break; }
+
+            default:
+                mrc = pfnwpOrig(hwndOwner, msg, mp1, mp2);
+        }
+    }
+    else
+        mrc = WinDefWindowProc(hwndOwner, msg, mp1, mp2);
+
+    return (mrc);
+}
+
+/*
+ *@@ ctlMakeCheckboxContainer:
+ *      this turns a regular container into a "checkbox"
+ *      container. This means that the container record
+ *      icons are painted as checkboxes, whose status
+ *      is determined using an extended record core
+ *      structure (CHECKBOXRECORDCORE).
+ *
+ *      This function assumes that all records in the
+ *      container use this special extended record core
+ *      ONLY. You cannot use regular RECORDCORE's when
+ *      using this function. Of course, you can extend
+ *      the CHECKBOXRECORDCORE structure to the bottom
+ *      if you need more fields.
+ *
+ *      This subclasses BOTH the container and the container
+ *      owner to intercept a number of messages and to
+ *      implement owner draw. This is totally transparent
+ *      to the caller; QWL_USER is not used for this, so
+ *      you can still store your own stuff in there.
+ *
+ *      Returns FALSE upon errors.
+ *
+ *      To set up a checkbox container, call this function
+ *      (which switches the container to Tree view automatically).
+ *      Then, allocate and insert CHECKBOXRECORDCORE's as usual.
+ *
+ *      To set and query a record's check state easily, use
+ *      ctlSetRecordChecked and ctlQueryRecordChecked.
+ *
+ *      The checkboxes work almost identically to regular
+ *      checkboxes. The user can interact with the checkbox
+ *      by clicking on the checkbox itself or by double-clicking
+ *      on the record text or by pressing the Enter or Space keys.
+ *
+ *      Set CHECKBOXRECORDCORE.ulStyle to one of the regular
+ *      checkbox button style bits; if one of the "auto" flags
+ *      is set, the checkbox is toggled automatically.
+ *
+ *      Even if "auto" is off, when the user changes the check
+ *      box selection, hwndCnrOwner gets sent a WM_CONTROL message
+ *      with the following parameters:
+ *
+ *      -- mp1: USHORT id: usCnrID, as passed to this func
+ *      -- mp1: USHORT usNotifyCode: CN_RECORDCHECKED (999)
+ *      -- mp2: PCHECKRECORDCORE precc: the record which was (un)checked.
+ *
+ *      Note that the subclassed container owner proc filters out
+ *      CN_ENTER, so you'll never receive that (but CN_RECORDCHECKED
+ *      instead).
+ *
+ *      If the "auto" style bits have not been set, you must call
+ *      ctlSetRecordChecked yourself then.
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+BOOL ctlMakeCheckboxContainer(HWND hwndCnrOwner,    // in: owner (and parent) of container
+                              USHORT usCnrID)       // in: ID of container in hwndCnrOwner
+{
+    BOOL    brc = FALSE;
+
+    HWND    hwndCnr = WinWindowFromID(hwndCnrOwner, usCnrID);
+
+    // create mutex-protected list of container owners
+    if (G_hmtxCnrOwnersList == 0)
+        if (DosCreateMutexSem(NULL,
+                              &G_hmtxCnrOwnersList, 0, FALSE) != NO_ERROR)
+            return (FALSE);
+
+    if (G_pllCnrOwners == NULL)
+        G_pllCnrOwners = lstCreate(TRUE);
+
+    if (hwndCnr)
+    {
+        if (G_hbmCheckboxes == NULLHANDLE)
+        {
+            // first call:
+            BITMAPINFOHEADER bmih;
+            // load checkboxes bitmap
+            G_hbmCheckboxes = WinGetSysBitmap(HWND_DESKTOP,
+                                              SBMP_CHECKBOXES);
+
+            // _Pmpf(("hbmCheckboxes: 0x%lX", G_hbmCheckboxes));
+
+            // and compute size of one checkbox
+            // (4 columns, 3 rows)
+            GpiQueryBitmapParameters(G_hbmCheckboxes,
+                                     &bmih);
+            G_cxCheckbox = bmih.cx / 4;
+        }
+
+        BEGIN_CNRINFO()
+        {
+            cnrhSetView(CV_TREE | CV_ICON | CA_TREELINE | CA_OWNERDRAW | CV_MINI);
+            cnrhSetTreeIndent(20);
+            cnrhSetBmpOrIconSize(G_cxCheckbox, G_cxCheckbox);
+            // cnrhSetSortFunc(fnCompareName);
+        } END_CNRINFO(hwndCnr);
+
+        if (    (hwndCnrOwner)
+             && (G_hmtxCnrOwnersList)
+           )
+        {
+            // subclass container owner
+            PFNWP pfnwpOwnerOrig = WinSubclassWindow(hwndCnrOwner, ctl_fnwpSubclCheckboxCnrOwner);
+            /* _Pmpf(("Subclassed hwnd 0x%lX: orig 0x%lX",
+                    hwndCnrOwner, pfnwpOwnerOrig)); */
+            if (pfnwpOwnerOrig)
+            {
+                // owner successfully subclassed:
+                // subclass container too
+                PFNWP pfnwpCnrOrig = WinSubclassWindow(hwndCnr, ctl_fnwpSubclCheckboxCnr);
+                if (pfnwpCnrOrig)
+                {
+                    // cnr successfully subclassed:
+                    // create storage for both subclassed cnr and owner
+                    PCHECKBOXCNROWNER pcbco = (PCHECKBOXCNROWNER)malloc(sizeof(CHECKBOXCNROWNER));
+                    if (pcbco)
+                    {
+                        memset(pcbco, 0, sizeof(CHECKBOXCNROWNER));
+                        pcbco->hwndCnr = hwndCnr;
+                        pcbco->usCnrID = usCnrID;
+                        pcbco->hwndOwner = hwndCnrOwner;
+                        pcbco->pfnwpCnrOrig = pfnwpCnrOrig;
+                        pcbco->pfnwpOwnerOrig = pfnwpOwnerOrig;
+
+                        pcbco->habCnr = WinQueryAnchorBlock(hwndCnr);
+
+                        if (WinRequestMutexSem(G_hmtxCnrOwnersList, 5000) == NO_ERROR)
+                        {
+                            lstAppendItem(G_pllCnrOwners, pcbco);
+                            DosReleaseMutexSem(G_hmtxCnrOwnersList);
+                            brc = TRUE;
+                        }
+                    }
+
+                    if (!brc)
+                        // failed: unsubclass cnr
+                        WinSubclassWindow(hwndCnr, pfnwpCnrOrig);
+                }
+
+                if (!brc)
+                    // failed: unsubclass owner
+                    WinSubclassWindow(hwndCnrOwner, pfnwpOwnerOrig);
+            }
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ fncbFindCheckRecord:
+ *      helper callback for finding a checkbox
+ *      record according to an item ID. Used
+ *      with cnrhForAllRecords.
+ *
+ *      Returns 1 if found to make
+ *      cnrhForAllRecords stop searching.
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+ULONG EXPENTRY fncbFindCheckRecord(HWND hwndCnr,             // in: container
+                                   PRECORDCORE preccThis,    // in: current record (from cnrhForAllRecords)
+                                   ULONG ulItemID,           // in: item ID to find
+                                   ULONG ulppRecc)           // out: PRECORDCORE* if found
+{
+    ULONG   ulrc = 0;
+    PCHECKBOXRECORDCORE precc = (PCHECKBOXRECORDCORE)preccThis;
+    if (precc)
+    {
+        if (precc->usItemID == ulItemID)
+        {
+            // found:
+            PCHECKBOXRECORDCORE *pprecc = (PCHECKBOXRECORDCORE*)ulppRecc;
+            // store
+            *pprecc = precc;
+            // and stop
+            ulrc = 1;
+        }
+    }
+    return (ulrc);
+}
+
+/*
+ *@@ ctlFindCheckRecord:
+ *      this searches the given checkbox container
+ *      for the record which matches the given item
+ *      ID. Returns NULL if not found.
+ *
+ *@@added V0.9.1 (99-12-03) [umoeller]
+ */
+
+PCHECKBOXRECORDCORE ctlFindCheckRecord(HWND hwndCnr,
+                                       ULONG ulItemID)
+{
+    PCHECKBOXRECORDCORE precc = 0;
+
+    cnrhForAllRecords(hwndCnr,
+                      NULL,         // start with root
+                      fncbFindCheckRecord,
+                      (ULONG)ulItemID,          // input
+                      (ULONG)&precc);
+
+    return (precc);
+}
+
+/*
+ *@@ ctlSetRecordChecked:
+ *      this searches the given checkbox container
+ *      for the record which matches the given item
+ *      ID and updates that record check state.
+ *
+ *      The check state may be:
+ *      -- 0: not checked (as with regular checkboxes)
+ *      -- 1: checked (as with regular checkboxes)
+ *      -- 2: indeterminate (as with regular checkboxes)
+ *
+ *      Returns FALSE if the record could not be found.
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+BOOL ctlSetRecordChecked(HWND hwndCnr,          // in: container prepared with
+                                                // ctlMakeCheckboxContainer
+                         ULONG ulItemID,        // in: record item ID
+                         USHORT usCheckState)   // in: 0, 1, 2, 3
+{
+    BOOL brc = FALSE;
+    PCHECKBOXRECORDCORE precc = ctlFindCheckRecord(hwndCnr, ulItemID);
+
+    if (precc)
+    {
+        precc->usCheckState = usCheckState;
+        WinSendMsg(hwndCnr,
+                   CM_INVALIDATERECORD,
+                   (MPARAM)&precc,
+                   MPFROM2SHORT(1,
+                                CMA_NOREPOSITION));
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ ctlQueryRecordChecked:
+ *      this searches the given checkbox container
+ *      for the record which matches the given item
+ *      ID and returns that record's check state.
+ *
+ *      Returns:
+ *      -- 0: not checked (as with regular checkboxes)
+ *      -- 1: checked (as with regular checkboxes)
+ *      -- 2: indeterminate (as with regular checkboxes)
+ *      -- -1: record not found.
+ *
+ *@@added V0.9.0 (99-11-28) [umoeller]
+ */
+
+ULONG ctlQueryRecordChecked(HWND hwndCnr,          // in: container prepared with
+                                                   // ctlMakeCheckboxContainer
+                            ULONG ulItemID,        // in: record item ID
+                            USHORT usCheckState)   // in: 0, 1, 2, 3
+{
+    ULONG ulrc = -1;
+    PCHECKBOXRECORDCORE precc = ctlFindCheckRecord(hwndCnr, ulItemID);
+
+    if (precc)
+        ulrc = precc->usCheckState;
+
+    return (ulrc);
+}
+
+/*
+ *@@ ctlEnableRecord:
+ *      this searches the given checkbox container
+ *      for the record which matches the given item
+ *      ID and updates that record enablement. If
+ *      the record is disabled, it's painted halftoned
+ *      by ctl_fnwpSubclCheckboxCnr.
+ *
+ *@@added V0.9.1 (99-12-03) [umoeller]
+ */
+
+BOOL ctlEnableRecord(HWND hwndCnr,
+                     ULONG ulItemID,
+                     BOOL fEnable)
+{
+    BOOL brc = FALSE;
+    PCHECKBOXRECORDCORE precc = ctlFindCheckRecord(hwndCnr, ulItemID);
+
+    if (precc)
+    {
+        ULONG ulAttr = precc->recc.flRecordAttr;
+        if (fEnable)
+            precc->recc.flRecordAttr &= ~CRA_DISABLED;
+        else
+            precc->recc.flRecordAttr |= CRA_DISABLED;
+
+        if (precc->recc.flRecordAttr != ulAttr)
+            // attrs changed: repaint
+            WinSendMsg(hwndCnr,
+                       CM_INVALIDATERECORD,
+                       (MPARAM)&precc,
+                       MPFROM2SHORT(1,
+                                    CMA_NOREPOSITION));
+
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
+
Index: /trunk/src/helpers/cctl_progbar.c
===================================================================
--- /trunk/src/helpers/cctl_progbar.c	(revision 8)
+++ /trunk/src/helpers/cctl_progbar.c	(revision 8)
@@ -0,0 +1,413 @@
+
+/*
+ *@@sourcefile cctl_progbar.c:
+ *      implementation for the progress bar common control.
+ *      See comctl.c for an overview.
+ *
+ *      This has been extracted from comctl.c with V0.9.3 (2000-05-21) [umoeller].
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ *@@added V0.9.3 (2000-05-21) [umoeller].
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Progress bars
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Progress bars                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ PaintProgress:
+ *      this does the actual painting of the progress bar, called
+ *      from ctl_fnwpProgressBar.
+ *      It is called both upon WM_PAINT and WM_UPDATEPROGRESSBAR
+ *      with different HPS's then.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.5 (2000-09-22) [umoeller]: fixed ypos of text
+ */
+
+VOID PaintProgress(PPROGRESSBARDATA pData, HWND hwndBar, HPS hps)
+{
+    POINTL  ptl1, ptlText, aptlText[TXTBOX_COUNT];
+    RECTL   rcl, rcl2;
+
+    CHAR    szPercent[10] = "";
+
+    // switch to RGB mode
+    GpiCreateLogColorTable(hps, 0, LCOLF_RGB, 0, 0, NULL);
+
+    if (pData->ulPaintX <= pData->ulOldPaintX)
+    {
+        // draw frame and background only if this is either
+        //    a "real" WM_PAINT (i.e., the window was overlapped
+        //   and needs repainting; then ulPaintX == ulOldPaintX)
+        //   or if ulNow has _de_creased
+        GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONDARK, 0));
+        ptl1.x = 0;
+        ptl1.y = 0;
+        GpiMove(hps, &ptl1);
+        ptl1.y = (pData->rclBar.yTop);
+        GpiLine(hps, &ptl1);
+        ptl1.x = (pData->rclBar.xRight);
+        GpiLine(hps, &ptl1);
+        GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONLIGHT, 0));
+        ptl1.y = 0;
+        GpiLine(hps, &ptl1);
+        ptl1.x = 0;
+        GpiLine(hps, &ptl1);
+
+        pData->rclBar.xLeft = 1;
+        pData->rclBar.yBottom = 1;
+        WinFillRect(hps, &(pData->rclBar),
+            WinQuerySysColor(HWND_DESKTOP, SYSCLR_SCROLLBAR, 0));
+    }
+
+    // draw percentage?
+    if (pData->ulAttr & PBA_PERCENTFLAGS)
+    {
+        // make string
+        sprintf(szPercent, "%d %%", ((100 * pData->ulNow) / pData->ulMax) );
+
+        // calculate string space
+        GpiQueryTextBox(hps, strlen(szPercent), szPercent,
+                TXTBOX_COUNT, (PPOINTL)&aptlText);
+
+        // calculate coordinates
+        ptlText.x = pData->rclBar.xLeft +
+                        (   (   (pData->rclBar.xRight-pData->rclBar.xLeft)
+                              - (aptlText[TXTBOX_BOTTOMRIGHT].x-aptlText[TXTBOX_BOTTOMLEFT].x)
+                            )
+                        / 2);
+        ptlText.y = 2 + pData->rclBar.yBottom +     // fixed V0.9.5 (2000-09-22) [umoeller]
+                        (   (   (pData->rclBar.yTop-pData->rclBar.yBottom)
+                              - (aptlText[TXTBOX_TOPLEFT].y-aptlText[TXTBOX_BOTTOMLEFT].y)
+                            )
+                        / 2);
+
+        // do we need to repaint the background under the percentage?
+        if (    (   (ptlText.x
+                        + (  aptlText[TXTBOX_BOTTOMRIGHT].x
+                           - aptlText[TXTBOX_BOTTOMLEFT].x)
+                    )
+                  > pData->ulPaintX)
+            &&  (pData->ulPaintX > pData->ulOldPaintX)
+           )
+        {
+            // if we haven't drawn the background already,
+            // we'll need to do it now for the percentage area
+            rcl.xLeft      = ptlText.x;
+            rcl.xRight     = ptlText.x + (aptlText[TXTBOX_BOTTOMRIGHT].x-aptlText[TXTBOX_BOTTOMLEFT].x);
+            rcl.yBottom    = ptlText.y;
+            rcl.yTop       = ptlText.y + (aptlText[TXTBOX_TOPLEFT].y-aptlText[TXTBOX_BOTTOMLEFT].y);
+            WinFillRect(hps, &rcl,
+                WinQuerySysColor(HWND_DESKTOP, SYSCLR_SCROLLBAR, 0));
+        }
+    }
+
+    // now draw the actual progress
+    rcl2.xLeft = pData->rclBar.xLeft;
+    rcl2.xRight = (pData->ulPaintX > (rcl2.xLeft + 3))
+            ? pData->ulPaintX
+            : rcl2.xLeft + ((pData->ulAttr & PBA_BUTTONSTYLE)
+                           ? 3 : 1);
+    rcl2.yBottom = pData->rclBar.yBottom;
+    rcl2.yTop = pData->rclBar.yTop-1;
+
+    if (pData->ulAttr & PBA_BUTTONSTYLE)
+    {
+        RECTL rcl3 = rcl2;
+        // draw "raised" inner rect
+        rcl3.xLeft = rcl2.xLeft;
+        rcl3.yBottom = rcl2.yBottom;
+        rcl3.yTop = rcl2.yTop+1;
+        rcl3.xRight = rcl2.xRight+1;
+        gpihDraw3DFrame(hps, &rcl3, 2,
+                        WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONLIGHT, 0),
+                        WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONDARK, 0));
+        // WinInflateRect(WinQueryAnchorBlock(hwndBar), &rcl2, -2, -2);
+        rcl2.xLeft += 2;
+        rcl2.yBottom += 2;
+        rcl2.yTop -= 2;
+        rcl2.xRight -= 2;
+    }
+
+    if (rcl2.xRight > rcl2.xLeft)
+    {
+        GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP,
+                    // SYSCLR_HILITEBACKGROUND,
+                    SYSCLR_BUTTONMIDDLE,
+                0));
+        ptl1.x = rcl2.xLeft;
+        ptl1.y = rcl2.yBottom;
+        GpiMove(hps, &ptl1);
+        ptl1.x = rcl2.xRight;
+        ptl1.y = rcl2.yTop;
+        GpiBox(hps, DRO_FILL | DRO_OUTLINE,
+            &ptl1,
+            0,
+            0);
+    }
+
+    // now print the percentage
+    if (pData->ulAttr & PBA_PERCENTFLAGS)
+    {
+        GpiMove(hps, &ptlText);
+        GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP,
+                // SYSCLR_HILITEFOREGROUND,
+                SYSCLR_BUTTONDEFAULT,
+            0));
+        GpiCharString(hps, strlen(szPercent), szPercent);
+    }
+
+    // store current X position for next time
+    pData->ulOldPaintX = pData->ulPaintX;
+}
+
+/*
+ *@@ ctl_fnwpProgressBar:
+ *      this is the window procedure for the progress bar control.
+ *
+ *      This is not a stand-alone window procedure, but must only
+ *      be used with static rectangle controls subclassed by
+ *      ctlProgressBarFromStatic.
+ *
+ *      We need to capture WM_PAINT to draw the progress bar according
+ *      to the current progress, and we also update the static text field
+ *      (percentage) next to it.
+ *
+ *      This also evaluates the WM_UPDATEPROGRESSBAR message.
+ *
+ *@@changed V0.9.0 [umoeller]: moved this code here
+ *@@changed V0.9.1 (99-12-06): fixed memory leak
+ */
+
+MRESULT EXPENTRY ctl_fnwpProgressBar(HWND hwndBar, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    HPS                 hps;
+    PPROGRESSBARDATA    ppd = (PPROGRESSBARDATA)WinQueryWindowULong(hwndBar, QWL_USER);
+
+    PFNWP   OldStaticProc = NULL;
+
+    MRESULT mrc = NULL;
+
+    if (ppd)
+    {
+        OldStaticProc = ppd->OldStaticProc;
+
+        switch(msg)
+        {
+
+            /*
+             *@@ WM_UPDATEPROGRESSBAR:
+             *      post or send this message to a progress bar
+             *      to have a new progress displayed.
+             *      Parameters:
+             *      --  ULONG mp1   current value
+             *      --  ULONG mp2   max value
+             *      Example: mp1 = 100, mp2 = 300 will result
+             *      in a progress of 33%.
+             */
+
+            case WM_UPDATEPROGRESSBAR:
+            {
+                if (    (ppd->ulNow != (ULONG)mp1)
+                     || (ppd->ulMax != (ULONG)mp2)
+                   )
+                {
+                    ppd->ulNow = (ULONG)mp1;
+                    ppd->ulMax = (ULONG)mp2;
+                }
+                else
+                    // value not changed: do nothing
+                    break;
+
+                // check validity
+                if (ppd->ulNow > ppd->ulMax)
+                    ppd->ulNow = ppd->ulMax;
+                // avoid division by zero
+                if (ppd->ulMax == 0)
+                {
+                    ppd->ulMax = 1;
+                    ppd->ulNow = 0;
+                    // paint 0% then
+                }
+
+                // calculate new X position of the progress
+                ppd->ulPaintX =
+                    (ULONG)(
+                              (    (ULONG)(ppd->rclBar.xRight - ppd->rclBar.xLeft)
+                                 * (ULONG)(ppd->ulNow)
+                              )
+                              /  (ULONG)ppd->ulMax
+                           );
+                if (ppd->ulPaintX != ppd->ulOldPaintX)
+                {
+                    // X position changed: redraw
+                    // WinInvalidateRect(hwndBar, NULL, FALSE);
+                    hps = WinGetPS(hwndBar);
+                    PaintProgress(ppd, hwndBar, hps);
+                    WinReleasePS(hps);
+                }
+            break; }
+
+            case WM_PAINT:
+            {
+                RECTL rcl;
+                hps = WinBeginPaint(hwndBar, NULLHANDLE, &rcl);
+                PaintProgress(ppd, hwndBar, hps);
+                WinEndPaint(hps);
+            break; }
+
+            /*
+             * WM_DESTROY:
+             *      free PROGRESSBARDATA
+             *      (added V0.9.1 (99-12-06))
+             */
+
+            case WM_DESTROY:
+            {
+                free(ppd);
+                mrc = OldStaticProc(hwndBar, msg, mp1, mp2);
+            break; }
+
+            default:
+                mrc = OldStaticProc(hwndBar, msg, mp1, mp2);
+       }
+    }
+    return (mrc);
+}
+
+/*
+ *@@ ctlProgressBarFromStatic:
+ *      this function turns an existing static rectangle control
+ *      into a progress bar by subclassing its window procedure
+ *      with ctl_fnwpProgressBar.
+ *
+ *      This way you can easily create a progress bar as a static
+ *      control in any Dialog Editor; after loading the dlg template,
+ *      simply call this function with the hwnd of the static control
+ *      to make it a status bar.
+ *
+ *      This is used for all the progress bars in XWorkplace and
+ *      WarpIN.
+ *
+ *      In order to _update_ the progress bar, simply post or send
+ *      WM_UPDATEPROGRESSBAR to the static (= progress bar) window;
+ *      this message is equal to WM_USER and needs the following
+ *      parameters:
+ *      --  mp1     ULONG ulNow: the current progress
+ *      --  mp2     ULONG ulMax: the maximally possible progress
+ *                               (= 100%)
+ *
+ *      The progress bar automatically calculates the current progress
+ *      display. For example, if ulNow = 4096 and ulMax = 8192,
+ *      a progress of 50% will be shown. It is possible to change
+ *      ulMax after the progress bar has started display. If ulMax
+ *      is 0, a progress of 0% will be shown (to avoid division
+ *      by zero traps).
+ *
+ *      ulAttr accepts of the following:
+ *      --  PBA_NOPERCENTAGE:    do not display percentage
+ *      --  PBA_ALIGNLEFT:       left-align percentage
+ *      --  PBA_ALIGNRIGHT:      right-align percentage
+ *      --  PBA_ALIGNCENTER:     center percentage
+ *      --  PBA_BUTTONSTYLE:     no "flat", but button-like look
+ *
+ *@@changed V0.9.0 [umoeller]: moved this code here
+ */
+
+BOOL ctlProgressBarFromStatic(HWND hwndChart, ULONG ulAttr)
+{
+    PFNWP OldStaticProc = WinSubclassWindow(hwndChart, ctl_fnwpProgressBar);
+    if (OldStaticProc)
+    {
+        PPROGRESSBARDATA pData = (PPROGRESSBARDATA)malloc(sizeof(PROGRESSBARDATA));
+        pData->ulMax = 1;
+        pData->ulNow = 0;
+        pData->ulPaintX = 0;
+        pData->ulAttr = ulAttr;
+        pData->OldStaticProc = OldStaticProc;
+        WinQueryWindowRect(hwndChart, &(pData->rclBar));
+        (pData->rclBar.xRight)--;
+        (pData->rclBar.yTop)--;
+
+        WinSetWindowULong(hwndChart, QWL_USER, (ULONG)pData);
+        return (TRUE);
+    }
+    else return (FALSE);
+}
+
+
Index: /trunk/src/helpers/cctl_splitwin.c
===================================================================
--- /trunk/src/helpers/cctl_splitwin.c	(revision 8)
+++ /trunk/src/helpers/cctl_splitwin.c	(revision 8)
@@ -0,0 +1,997 @@
+
+/*
+ *@@sourcefile cctl.splitwin.c:
+ *      implementation for split windows.
+ *      See comctl.c for an overview.
+ *
+ *      This has been extracted from comctl.c with V0.9.3 (2000-05-21) [umoeller].
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ *@@added V0.9.3 (2000-05-21) [umoeller].
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Split windows
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Split windows                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ PaintSplitWindow:
+ *      implementation for WM_PAINT in ctl_fnwpSplitWindow.
+ *
+ *@@added V0.9.1 (2000-02-05) [umoeller]
+ */
+
+VOID PaintSplitWindow(HWND hwndSplit)
+{
+    HPS     hps = WinBeginPaint(hwndSplit, (HPS)0, NULL);
+    HWND    hwndSplitBar = WinWindowFromID(hwndSplit, ID_SPLITBAR);
+    PSPLITBARDATA pData = (PSPLITBARDATA)WinQueryWindowULong(hwndSplitBar,
+                                                             QWL_USER);
+
+    if ((pData->hwndLinked1) && (pData->hwndLinked2))
+    {
+        // "3D-sunk" style?
+        if (pData->sbcd.ulCreateFlags & SBCF_3DSUNK)
+        {
+            // yes: draw sunk frame around split windows
+            POINTL  ptl1;
+            SWP     swp;
+
+            // switch to RGB mode
+            GpiCreateLogColorTable(hps, 0, LCOLF_RGB, 0, 0, NULL);
+
+            WinQueryWindowPos(pData->hwndLinked1, &swp);
+            GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONDARK, 0));
+            ptl1.x = swp.x - 1;
+            ptl1.y = swp.y - 1;
+            GpiMove(hps, &ptl1);
+            ptl1.y = swp.y + swp.cy;
+            GpiLine(hps, &ptl1);
+            ptl1.x = swp.x + swp.cx;
+            GpiLine(hps, &ptl1);
+            GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONLIGHT, 0));
+            ptl1.y = swp.y - 1;
+            GpiLine(hps, &ptl1);
+            ptl1.x = swp.x - 1;
+            GpiLine(hps, &ptl1);
+
+            WinQueryWindowPos(pData->hwndLinked2, &swp);
+            GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONDARK, 0));
+            ptl1.x = swp.x - 1;
+            ptl1.y = swp.y - 1;
+            GpiMove(hps, &ptl1);
+            ptl1.y = swp.y + swp.cy;
+            GpiLine(hps, &ptl1);
+            ptl1.x = swp.x + swp.cx;
+            GpiLine(hps, &ptl1);
+            GpiSetColor(hps, WinQuerySysColor(HWND_DESKTOP, SYSCLR_BUTTONLIGHT, 0));
+            ptl1.y = swp.y - 1;
+            GpiLine(hps, &ptl1);
+            ptl1.x = swp.x - 1;
+            GpiLine(hps, &ptl1);
+        }
+    }
+
+    WinEndPaint(hps);
+}
+
+/*
+ *@@ ctl_fnwpSplitWindow:
+ *      this is the window procedure of the "invisible"
+ *      split window. One of these windows is created
+ *      for each split view and has exactly three children:
+ *
+ *      1)  the split bar (ctl_fnwpSplitBar);
+ *
+ *      2)  the left or bottom window linked to the split bar;
+ *
+ *      3)  the right or top window linked to the split bar.
+ *
+ *      See ctlCreateSplitWindow for more info and a detailed
+ *      window hierarchy.
+ *
+ *      This stand-alone window procedure must be registered.
+ *      ctlCreateSplitWindow does this for you.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpSplitWindow(HWND hwndSplit, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = (MRESULT)0;
+
+    switch(msg)
+    {
+        /*
+         * WM_WINDOWPOSCHANGED:
+         *
+         */
+
+        case WM_WINDOWPOSCHANGED:
+        {
+            // this msg is passed two SWP structs:
+            // one for the old, one for the new data
+            // (from PM docs)
+            PSWP pswpNew = (PSWP)mp1;
+
+            if (pswpNew->fl & SWP_SIZE)
+            {
+                // _Pmpf(("ctl_fnwpSplitWindow, WM_WINDOWPOSCHANGED"));
+                ctlUpdateSplitWindow(hwndSplit);
+            }
+            mrc = WinDefWindowProc(hwndSplit, msg, mp1, mp2);
+        break; }
+
+        /*
+         * WM_PAINT:
+         *
+         */
+
+        case WM_PAINT:
+            PaintSplitWindow(hwndSplit);
+        break;
+
+        /*
+         *@@ SPLM_SETLINKS:
+         *      this specifies the windows which the
+         *      split window will link. This updates
+         *      the internal SPLITBARDATA and changes
+         *      the parents of the two subwindows to
+         *      the split window.
+         *
+         *      Parameters:
+         *      -- HWND mp1:   left or bottom subwindow
+         *      -- HWND mp2:   right or top  subwindow
+         */
+
+        case SPLM_SETLINKS:
+        {
+            HWND    hwndSplitBar = WinWindowFromID(hwndSplit, ID_SPLITBAR);
+            PSPLITBARDATA pData = (PSPLITBARDATA)WinQueryWindowULong(hwndSplitBar,
+                                                                     QWL_USER);
+            if (pData)
+            {
+                pData->hwndLinked1 = (HWND)mp1;
+                pData->hwndLinked2 = (HWND)mp2;
+                // change parents of the windows to link
+                WinSetParent(pData->hwndLinked1, hwndSplit,
+                             FALSE);        // no redraw
+                WinSetParent(pData->hwndLinked2, hwndSplit,
+                             FALSE);        // no redraw
+            }
+        break; }
+
+        default:
+            mrc = WinDefWindowProc(hwndSplit, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ TrackSplitBar:
+ *      implementation for WM_BUTTON1DOWN in ctl_fnwpSplitBar.
+ *
+ *@@added V0.9.1 (2000-02-05) [umoeller]
+ */
+
+VOID TrackSplitBar(HWND hwndBar,
+                   PSPLITBARDATA pData)
+{
+    TRACKINFO   track;
+    RECTL       rclBar;
+
+    track.cxBorder = 2;
+    track.cyBorder = 2;
+    track.cxGrid   = 1;
+    track.cyGrid   = 1;
+    track.cxKeyboard = 8;
+    track.cyKeyboard = 8;
+
+    WinQueryWindowRect(hwndBar, &rclBar);
+    WinMapWindowPoints(hwndBar,
+                       HWND_DESKTOP,        // map to screen coords.
+                       (PPOINTL)&rclBar,
+                       2);          // 2 points == rectangle
+
+    // limit tracking space to parent window;
+    // this is either the client or another split window
+    WinQueryWindowRect(pData->sbcd.hwndParentAndOwner,
+                       &track.rclBoundary);
+    WinMapWindowPoints(pData->sbcd.hwndParentAndOwner,
+                       HWND_DESKTOP,        // map to screen coords.
+                       (PPOINTL)&track.rclBoundary,
+                       2);      // 2 points, since we have a RECTL
+
+    // decrease tracking limits by what was
+    // specified by the caller
+    if (pData->sbcd.ulCreateFlags & SBCF_HORIZONTAL)
+    {
+        // horizontal split bar
+        track.rclBoundary.yBottom += pData->sbcd.ulLeftOrBottomLimit;
+        track.rclBoundary.yTop -= pData->sbcd.ulRightOrTopLimit;
+        track.rclBoundary.xLeft = rclBar.xLeft;
+        track.rclBoundary.xRight = rclBar.xRight;
+    }
+    else
+    {
+        // vertical split bar
+        track.rclBoundary.xLeft += pData->sbcd.ulLeftOrBottomLimit;
+        track.rclBoundary.xRight -= pData->sbcd.ulRightOrTopLimit;
+        track.rclBoundary.yBottom = rclBar.yBottom;
+        track.rclBoundary.yTop = rclBar.yTop;
+    }
+
+    // initial tracking rectangle = split bar
+    memcpy(&track.rclTrack, &rclBar, sizeof(rclBar));
+
+    track.ptlMinTrackSize.x =
+    track.ptlMaxTrackSize.x = track.rclTrack.xRight
+                            - track.rclTrack.xLeft;     // width of status bar
+    track.ptlMinTrackSize.y =
+    track.ptlMaxTrackSize.y = track.rclTrack.yTop
+                            - track.rclTrack.yBottom;   // height of status bar
+
+    track.fs = TF_MOVE | TF_ALLINBOUNDARY;
+
+    // now do the tracking: this is a modal
+    // operation and returns only after the
+    // mouse has been released
+    if (WinTrackRect(HWND_DESKTOP, 0, &track))
+    {
+        // OK, successfully moved: reposition the
+        // windows which are linked to the split bar
+
+        if (pData->sbcd.ulCreateFlags & SBCF_HORIZONTAL)
+        {
+            // horizontal split bar
+            ULONG ulNewYPos = track.rclTrack.yBottom
+                            - track.rclBoundary.yBottom;
+            // add the limit specified by the caller
+            ulNewYPos += pData->sbcd.ulLeftOrBottomLimit;
+
+            if (pData->sbcd.ulCreateFlags & SBCF_PERCENTAGE)
+            {
+                // status bar pos is determined by
+                // a percentage:
+                // well, we'll need a new percentage then
+                RECTL rclClient;
+                WinQueryWindowRect(pData->sbcd.hwndParentAndOwner,
+                                   &rclClient);
+                pData->sbcd.lPos = ulNewYPos
+                                   * 100 / (rclClient.yTop - rclClient.yBottom);
+            }
+            else
+                // absolute split bar positioning:
+                if (pData->sbcd.lPos > 0)
+                    // from bottom: easy
+                    pData->sbcd.lPos = ulNewYPos;
+                else
+                {
+                    // negative -> from top:
+                    RECTL rclClient;
+                    WinQueryWindowRect(pData->sbcd.hwndParentAndOwner,
+                                       &rclClient);
+                    // calc new negative
+                    pData->sbcd.lPos = ulNewYPos - rclClient.yTop;
+                }
+        }
+        else
+        {
+            // vertical split bar:
+            ULONG ulNewXPos = track.rclTrack.xLeft
+                            - track.rclBoundary.xLeft;
+            // add the limit specified by the caller
+            ulNewXPos += pData->sbcd.ulLeftOrBottomLimit;
+
+            if (pData->sbcd.ulCreateFlags & SBCF_PERCENTAGE)
+            {
+                // status bar pos is determined by
+                // a percentage:
+                // well, we'll need a new percentage then
+                RECTL rclClient;
+                WinQueryWindowRect(pData->sbcd.hwndParentAndOwner,
+                                   &rclClient);
+                pData->sbcd.lPos = ulNewXPos
+                                   * 100 / (rclClient.xRight - rclClient.xLeft);
+            }
+            else
+                // absolute split bar positioning:
+                if (pData->sbcd.lPos > 0)
+                    // from left: easy
+                    pData->sbcd.lPos = ulNewXPos;
+                else
+                {
+                    // negative -> from right:
+                    RECTL rclClient;
+                    WinQueryWindowRect(pData->sbcd.hwndParentAndOwner,
+                                       &rclClient);
+                    // calc new negative
+                    pData->sbcd.lPos = ulNewXPos - rclClient.xRight;
+                }
+        }
+
+        // update parent (== "split window")
+        ctlUpdateSplitWindow(WinQueryWindow(hwndBar, QW_PARENT));
+    }
+}
+
+/*
+ *@@ PaintSplitBar:
+ *      implementation for WM_PAINT in ctl_fnwpSplitBar.
+ *
+ *@@added V0.9.1 (2000-02-05) [umoeller]
+ *@@changed V0.9.1 (2000-02-05) [umoeller]: fixed paint errors with sunken 3D style
+ */
+
+VOID PaintSplitBar(HWND hwndBar,
+                   PSPLITBARDATA pData)
+{
+    HPS     hps;
+    RECTL   rcl,
+            rclBar;
+    POINTL  ptl1;
+    hps = WinBeginPaint(hwndBar, NULLHANDLE, &rcl);
+    WinQueryWindowRect(hwndBar, &rclBar);
+    // switch to RGB mode
+    GpiCreateLogColorTable(hps, 0, LCOLF_RGB, 0, 0, NULL);
+
+    // inflate top and right, because WinFillRect
+    // considers this inclusive
+    /* rclBar.xRight++;
+    rclBar.yTop++; */
+
+    WinFillRect(hps,
+                &rclBar,
+                WinQuerySysColor(HWND_DESKTOP,
+                                 SYSCLR_INACTIVEBORDER,
+                                 0));
+
+    if ((pData->sbcd.ulCreateFlags & SBCF_3DSUNK) == 0)
+    {
+        GpiSetColor(hps,
+                    WinQuerySysColor(HWND_DESKTOP,
+                                     SYSCLR_BUTTONLIGHT,
+                                     0));
+        // draw left border (bottom to up)
+        ptl1.x = 0;
+        ptl1.y = 0;
+        GpiMove(hps, &ptl1);
+        ptl1.y = (rclBar.yTop - rclBar.yBottom) - 1;
+        if (pData->sbcd.ulCreateFlags & SBCF_VERTICAL)
+            // vertical:
+            GpiLine(hps, &ptl1);
+        else
+            GpiMove(hps, &ptl1);
+
+        // draw top border (to right)
+        ptl1.x = (rclBar.xRight - rclBar.xLeft) - 1;
+        if (pData->sbcd.ulCreateFlags & SBCF_VERTICAL)
+            // vertical:
+            GpiMove(hps, &ptl1);
+        else
+            GpiLine(hps, &ptl1);
+
+        GpiSetColor(hps,
+                    WinQuerySysColor(HWND_DESKTOP,
+                                     SYSCLR_BUTTONDARK,
+                                     0));
+        // draw right border (to bottom)
+        ptl1.y = 0;
+        if (pData->sbcd.ulCreateFlags & SBCF_VERTICAL)
+            // vertical:
+            GpiLine(hps, &ptl1);
+        else
+            GpiMove(hps, &ptl1);
+
+        if (!(pData->sbcd.ulCreateFlags & SBCF_VERTICAL))
+        {
+            // horizontal:
+            // draw bottom border
+            ptl1.x = 0;
+            GpiLine(hps, &ptl1);
+        }
+    }
+
+    WinEndPaint(hps);
+}
+
+/*
+ *@@ ctl_fnwpSplitBar:
+ *      window procedure for all split bars, horizontal and vertical.
+ *      This paints the split bar and handles dragging the split bar
+ *      and repositioning the "linked" windows afterwards.
+ *
+ *      This is not a stand-alone window procedure, but must only
+ *      be used with ctlCreateSplitWindow, which creates and subclasses
+ *      a static control as necessary.
+ *
+ *      See ctlCreateSplitWindow for more info and a detailed
+ *      window hierarchy.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (99-12-07): fixed memory leak
+ */
+
+MRESULT EXPENTRY ctl_fnwpSplitBar(HWND hwndBar, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    PSPLITBARDATA pData = (PSPLITBARDATA)WinQueryWindowULong(hwndBar, QWL_USER);
+
+    PFNWP   OldStaticProc = NULL;
+
+    MRESULT mrc = NULL;
+
+    if (pData)
+    {
+        OldStaticProc = pData->OldStaticProc;
+
+        switch(msg)
+        {
+            /*
+             * WM_MOUSEMOVE:
+             *      change mouse pointer when over split bar.
+             */
+
+            case WM_MOUSEMOVE:
+                pData->hptrOld = WinSetPointer(HWND_DESKTOP, pData->hptrMove);
+            break;
+
+            /*
+             * WM_BUTTON1DOWN:
+             *      this will start moving the split bar.
+             *      We use WinTrackRect for this.
+             */
+
+            case WM_BUTTON1DOWN:
+                TrackSplitBar(hwndBar, pData);
+            break;
+
+            /*
+             * WM_PAINT:
+             *      paint the split bar
+             */
+
+            case WM_PAINT:
+                PaintSplitBar(hwndBar, pData);
+            break;
+
+            case WM_DESTROY:
+                free (pData);       // added V0.9.1 (99-12-07)
+                mrc = OldStaticProc(hwndBar, msg, mp1, mp2);
+            break;
+
+            default:
+                mrc = OldStaticProc(hwndBar, msg, mp1, mp2);
+        }
+    }
+    return (mrc);
+}
+
+/*
+ *@@ ctlCreateSplitWindow:
+ *      this creates a new split window view according to
+ *      the given SPLITBARCDATA structure. A split view
+ *      links two existing windows together with a split
+ *      bar in between, which can be moved with the mouse.
+ *
+ *      Split windows are used for example in XWorkplace
+ *      for the WPS class list object and in WarpIN for
+ *      the database.
+ *
+ *      After the split view has been created using this
+ *      function, all window resizing/positioning is
+ *      automatically performed, that is:
+ *
+ *      a) if the parent window of the split view (e.g.
+ *         the frame) is resized, the subwindows are
+ *         adjusted;
+ *
+ *      b) if the split bar is moved, the linked windows
+ *         are adjusted also.
+ *
+ *      <B>Creating a split view</B>
+ *
+ *      To create a split view, you need to perform the
+ *      following steps:
+ *
+ *      1)  Create two windows that shall be separated by
+ *          a split bar. These can be _any_ PM windows:
+ *          a simple control window (e.g. a container),
+ *          a frame you have created, or even a dialog
+ *          which has been loaded using WinLoadDlg.
+ *
+ *          Note: With frame windows, make sure they have the
+ *          FCF_NOBYTEALIGN flag set, or they'll look funny.
+ *
+ *      2)  Fill a SPLITBARCDATA structure with the necessary
+ *          data for the split view (see comctl.h for details).
+ *
+ *      3)  Call this function with that structure, which creates
+ *          and returns the "split window", the invisible parent
+ *          of the windows to be split.
+ *          See the window hierarchy below.
+ *
+ *      4)  Send a SPLM_SETLINKS message to the split window
+ *          (returned by this function), with the two windows
+ *          to be linked in mp1 and mp2. This has been implemented
+ *          using a message so that you can re-link windows later
+ *          at any time. (This is how the XWorkplace "WPS Class List"
+ *          window does it when the settings notebook is changed.)
+ *
+ *          Sending this message will change parentship (not ownership)
+ *          of those two windows to the invisible split window.
+ *
+ *      5)  Position the split window (which is returned from this
+ *          function) within your existing windows using
+ *          WinSetWindowPos(..., SWP_SIZE), which will automatically
+ *          reposition the two linked subwindows of the split window.
+ *          This works because the window procedure of the split
+ *          window (ctl_fnwpSplitWindow) handles WM_WINDOWPOSCHANGED to
+ *          recalculate the positions of the child windows.
+ *
+ *      This function returns the HWND of the "split window".
+ *      The handle of the split _bar_, if you absolutely need it,
+ *      can be found using
+ +          WinWindowFromID(hwndWhatThisReturns, ID_SPLITBAR).
+ *
+ *      After SPLM_SETLINKS, the following window hierarchy
+ *      is established (parentship):
+ *
+ +      SPLITBARCDATA.hwndClient (whatever you have specified;
+ +         |                      e.g. FID_CLIENT of your frame window.
+ +         |                      You must intercept WM_SIZE and
+ +         |                      call WinSetWindowPos on the "split window".)
+ +         |
+ +         +--  hwndReturn ("split window" created and returned by this
+ +                 |       function; uses ctl_fnwpSplitWindow)
+ +                 |
+ +                 +-- hwndLink1 with SPLM_SETLINKS (parent changed)
+ +                 |      |
+ +                 |      +-- ... (your child windows remain untouched)
+ +                 |
+ +                 +-- ID_SPLITBAR (uses ctl_fnwpSplitBar)
+ +                 |
+ +                 +-- hwndLink2 with SPLM_SETLINKS (parent changed)
+ +                        |
+ +                        +-- ... (your child windows remain untouched)
+ *
+ *      Note that only parentship of hwndSplit1 and hwndSplit2
+ *      is changed. Ownership remains untouched. So you can specify
+ *      any window as the owner of hwndLink1/2 so that messages
+ *      can be forwarded properly.
+ *
+ *      After the above hierarchy has been established, there are
+ *      two situations where the "linked" windows will be repositioned
+ *      and/or resized:
+ *
+ *      1)  ctl_fnwpSplitBar will automatically resize and reposition
+ *          the left and right "linked" windows if the user moves the
+ *          split bar.
+ *
+ *      2)  If the "split window" itself receives WM_WINDOWPOSCHANGED,
+ *          e.g. because WinSetWindowPos has been invoked on the
+ *          split window (which you should do when WM_SIZE is received
+ *          by your parent window), the "linked" windows will
+ *          automatically be repositioned and resized. This is done
+ *          in ctl_fnwpSplitWindow.
+ *
+ *      As a result, you must implement the following in your window
+ *      procedures for the windows passed to this function:
+ *
+ *      1)  The window procedure of SPLITBARCDATA.hwndClient (the
+ *          parent of the split window) must intercept WM_SIZE and
+ *          do a WinSetWindowPos on the split window. This will
+ *          readjust the split bar and SPLITBARCDATA.hwndLink1/2.
+ *
+ *      2)  The two linked windows should in turn handle WM_SIZE
+ *          correctly because whenever the split window is resized,
+ *          it invokes a WinSetWindowPos on SPLITBARCDATA.hwndLink1/2
+ *          in turn.
+ *
+ *          If you're using a single standard control as your subwindow
+ *          (e.g. a container), this is no problem. However, if you
+ *          specify frame windows, you might want to reposition
+ *          the controls in those windows in turn.
+ *
+ *          Note that dialog windows do not receive WM_SIZE;
+ *          you'll need to handle WM_WINDOWPOSCHANGED instead.
+ *
+ *      3)  Your window procedure should forward WM_SYSCOMMAND to
+ *          the owner (top) window so that Alt-F4 etc. hotkeys
+ *          still work.
+ *
+ *      If you wish to <B>nest split windows,</B> you can do so by
+ *      specifying the "split window" (the HWND which is returned
+ *      by this function) as the "hwndLink1/2" to another call
+ *      of this function. This way you can create a complex
+ *      split window hierarchy (similar to what Netscape does
+ *      with the FRAMESET tag). So to create a split view like
+ *      this:
+ *
+ +          +---------+------------+
+ +          |         |            |
+ +          |         |  2         |
+ +          |         |            |
+ +          |  1      +------------+
+ +          |         |            |
+ +          |         |  3         |
+ +          |         |            |
+ +          +---------+------------+
+ *
+ *      First invoke ctlCreateSplitWindow to link "2" and "3"
+ *      together (creating a horizontal split bar), which returns
+ *      a "split window"; then link "1" and that split window
+ *      together again.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+HWND ctlCreateSplitWindow(HAB hab,
+                          PSPLITBARCDATA psbcd) // in: split window control data
+{
+    HWND hwndSplit = NULLHANDLE,
+         hwndBar = NULLHANDLE;
+
+    if (psbcd)
+    {
+        // register "split window" class
+        WinRegisterClass(hab,
+                         WC_SPLITWINDOW,
+                         ctl_fnwpSplitWindow,
+                         CS_SIZEREDRAW | CS_SYNCPAINT,
+                         0);        // additional bytes to reserve
+
+        hwndSplit = WinCreateWindow(psbcd->hwndParentAndOwner,  // parent
+                                    WC_SPLITWINDOW,
+                                    "",
+                                    WS_VISIBLE,
+                                    0, 0, 10, 10,
+                                    psbcd->hwndParentAndOwner,  // owner
+                                    HWND_TOP,
+                                    psbcd->ulSplitWindowID,
+                                    NULL,
+                                    NULL);
+        if (hwndSplit)
+        {
+            hwndBar    = WinCreateWindow(psbcd->hwndParentAndOwner,  // parent
+                                         WC_STATIC,
+                                         "",
+                                         WS_VISIBLE            // wnd style
+                                           | SS_TEXT,
+                                         0, 0, 10, 10,
+                                         psbcd->hwndParentAndOwner,  // owner
+                                         HWND_TOP,
+                                         ID_SPLITBAR,          // win ID
+                                         NULL,                 // cdata
+                                         NULL                  // presparams
+                                     );
+            if (hwndBar)
+            {
+                // create SPLITBARDATA to store in split bar's QWL_USER
+                PSPLITBARDATA pData = (PSPLITBARDATA)malloc(sizeof(SPLITBARDATA));
+                if (pData)
+                {
+                    // set parent for split bar
+                    WinSetParent(hwndBar, hwndSplit, FALSE);
+
+                    memset(pData, 0, sizeof(SPLITBARDATA));
+
+                    // copy control data
+                    memcpy(&(pData->sbcd), psbcd, sizeof(SPLITBARCDATA));
+                    // set other data
+                    /* WinQueryWindowRect(hwndBar, &(pData->rclBar));
+                    (pData->rclBar.xRight)--;
+                    (pData->rclBar.yTop)--; */
+                    // subclass static control to make it a split bar
+                    pData->OldStaticProc = WinSubclassWindow(hwndBar, ctl_fnwpSplitBar);
+                    pData->hptrOld = NULLHANDLE;
+                    pData->hptrMove = WinQuerySysPointer(HWND_DESKTOP,
+                                (psbcd->ulCreateFlags & SBCF_HORIZONTAL)
+                                    ? SPTR_SIZENS
+                                    : SPTR_SIZEWE,
+                                FALSE);     // don't make copy
+                    pData->fCaptured = FALSE;
+                    pData->hwndLinked1 =
+                    pData->hwndLinked2 = NULLHANDLE;
+
+                    WinSetWindowULong(hwndBar, QWL_USER, (ULONG)pData);
+                }
+            }
+        }
+    }
+
+    return (hwndSplit);
+}
+
+/*
+ *@@ ctlUpdateSplitWindow:
+ *      this function takes a split window as input and
+ *      repositions all its children (the split bars
+ *      and the other subwindows which are separated by the
+ *      split bars) according to the parent.
+ *
+ *      This function gets called from ctl_fnwpSplitWindow
+ *      when WM_SIZE is received. Normally, you won't have
+ *      to call this function independently; you should do
+ *      a WinSetWindowPos on the split window instead.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL ctlUpdateSplitWindow(HWND hwndSplit)
+{
+    BOOL    brc = FALSE;
+    HWND    hwndSplitBar = WinWindowFromID(hwndSplit, ID_SPLITBAR);
+
+    if (hwndSplitBar)
+    {
+        PSPLITBARDATA psbd = (PSPLITBARDATA)WinQueryWindowULong(hwndSplitBar, QWL_USER);
+
+        if (psbd)
+        {
+            PSPLITBARCDATA  psbcd = &(psbd->sbcd);
+            RECTL           rclSplit,
+                            rclBar;
+            ULONG           ul3DOfs = 0;
+
+            // _Pmpf(("Entering ctlUpdateSplitWindow for hwndSplit 0x%lX", hwndSplit));
+
+            // query the rectangle of the split window's parent;
+            // this is either the client or another split window
+            WinQueryWindowRect(hwndSplit, &rclSplit);
+
+            /* _Pmpf(("  rect: %d, %d, %d, %d",
+                            rclSplit.xLeft,
+                            rclSplit.yBottom,
+                            rclSplit.xRight - rclSplit.xLeft,
+                            rclSplit.yTop - rclSplit.yBottom)); */
+
+            // set anti-recursion flag;
+            // this is neccessary, because ctl_fnwpSplitWindow
+            // calls this func again when
+            // WM_WINDOWPOSCHANGED comes in
+            // psbd->fNoAdjust = TRUE;
+            // set split window to the same
+            /* WinSetWindowPos(hwndSplit, HWND_TOP,
+                            rclSplit.xLeft,
+                            rclSplit.yBottom,
+                            rclSplit.xRight - rclSplit.xLeft,
+                            rclSplit.yTop - rclSplit.yBottom,
+                            SWP_MOVE | SWP_SIZE); */
+            // psbd->fNoAdjust = FALSE;
+
+            // update split bar
+            if (psbcd->ulCreateFlags & SBCF_HORIZONTAL)
+            {
+                // _Pmpf(("  Calc horizontal"));
+                // horizontal split bar:
+                if (psbcd->ulCreateFlags & SBCF_PERCENTAGE)
+                    // take height of client and apply percentage
+                    rclBar.yBottom = (rclSplit.yTop - rclSplit.yBottom)
+                                    * psbcd->lPos
+                                    / 100;
+                else
+                    if (psbcd->lPos > 0)
+                        // offset from bottom:
+                        rclBar.yBottom = psbcd->lPos;
+                    else
+                        // offset from right:
+                        rclBar.yBottom = (rclSplit.yTop - rclSplit.yBottom)
+                                         + psbcd->lPos;  // which is negative
+
+                rclBar.yTop = rclBar.yBottom + WinQuerySysValue(HWND_DESKTOP,
+                                                                SV_CXSIZEBORDER);
+                rclBar.xLeft = 0;
+                // take width of client
+                rclBar.xRight = (rclSplit.xRight - rclSplit.xLeft);
+            }
+            else
+            {
+                // _Pmpf(("  Calc vertical"));
+                // vertical split bar:
+                if (psbcd->ulCreateFlags & SBCF_PERCENTAGE)
+                    // take width of client and apply percentage
+                    rclBar.xLeft = (rclSplit.xRight - rclSplit.xLeft)
+                                    * psbcd->lPos
+                                    / 100;
+                else
+                    if (psbcd->lPos > 0)
+                        // offset from left:
+                        rclBar.xLeft = psbcd->lPos;
+                    else
+                        // offset from right:
+                        rclBar.xLeft = (rclSplit.xRight - rclSplit.xLeft)
+                                       + psbcd->lPos;  // which is negative
+
+                rclBar.xRight = rclBar.xLeft + WinQuerySysValue(HWND_DESKTOP,
+                                                                SV_CXSIZEBORDER);
+                rclBar.yBottom = 0;
+                // take height of client
+                rclBar.yTop = (rclSplit.yTop - rclSplit.yBottom);
+            }
+
+            // reposition split bar
+            brc = WinSetWindowPos(hwndSplitBar,
+                                  HWND_TOP,
+                                  rclBar.xLeft,
+                                  rclBar.yBottom,
+                                  rclBar.xRight - rclBar.xLeft,
+                                  rclBar.yTop - rclBar.yBottom,
+                                  SWP_MOVE | SWP_SIZE);
+
+            /* _Pmpf(("  Set splitbar hwnd %lX to %d, %d, %d, %d; rc: %d",
+                            hwndSplitBar,
+                            rclBar.xLeft,
+                            rclBar.yBottom,
+                            rclBar.xRight - rclBar.xLeft,
+                            rclBar.yTop - rclBar.yBottom,
+                            brc)); */
+
+            // reposition left/bottom window of split bar
+            if (psbcd->ulCreateFlags & SBCF_3DSUNK)
+                ul3DOfs = 1;
+            // else 0
+
+            // now reposition the linked windows
+            if (psbcd->ulCreateFlags & SBCF_HORIZONTAL)
+            {
+                // horizontal:
+                // reposition bottom window of split bar
+                WinSetWindowPos(psbd->hwndLinked1,
+                                HWND_TOP,
+                                ul3DOfs,
+                                ul3DOfs,
+                                rclBar.xRight - rclBar.xLeft - ul3DOfs*2,
+                                rclBar.yBottom - ul3DOfs*2,       // the window rect is non-inclusive
+                                SWP_MOVE | SWP_SIZE);
+
+                // reposition top window of split bar
+                WinSetWindowPos(psbd->hwndLinked2,
+                                HWND_TOP,
+                                ul3DOfs,
+                                rclBar.yTop + ul3DOfs,    // the window rect is non-inclusive
+                                rclBar.xRight - rclBar.xLeft - ul3DOfs*2,
+                                rclSplit.yTop - rclBar.yTop - ul3DOfs*2,
+                                SWP_MOVE | SWP_SIZE);
+            }
+            else
+            {
+                // vertical:
+                // reposition left window of split bar
+                WinSetWindowPos(psbd->hwndLinked1,
+                                HWND_TOP,
+                                ul3DOfs,
+                                ul3DOfs,
+                                rclBar.xLeft - ul3DOfs*2,       // the window rect is non-inclusive
+                                rclBar.yTop - rclBar.yBottom - ul3DOfs*2,
+                                SWP_MOVE | SWP_SIZE);
+
+                // reposition right window of split bar
+                WinSetWindowPos(psbd->hwndLinked2,
+                                HWND_TOP,
+                                rclBar.xRight + ul3DOfs,    // the window rect is non-inclusive
+                                ul3DOfs,
+                                rclSplit.xRight - rclBar.xRight - ul3DOfs*2,
+                                rclBar.yTop - rclBar.yBottom - ul3DOfs*2,
+                                SWP_MOVE | SWP_SIZE);
+            }
+
+            // repaint split window (3D frame)
+            WinInvalidateRect(hwndSplit,
+                              NULL,         // all
+                              FALSE);       // don't repaint children
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ ctlQuerySplitPos:
+ *      this returns the position of the split bar
+ *      (the child control of hwndSplit; see ctlCreateSplitWindow
+ *      for the window hierarchy).
+ *
+ *      The meaning of the return value depends on what you
+ *      specified with ulCreateFlags in the SPLITBARCDATA
+ *      structure passed to ctlCreateSplitWindow, that is,
+ *      it can be a percentage or an offset from the left
+ *      or from the right of the split window.
+ *
+ *      This returns NULL upon errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+LONG ctlQuerySplitPos(HWND hwndSplit)
+{
+    ULONG lrc = 0;
+
+    // the split bar data is stored in QWL_USER of the
+    // split bar (not the split window)
+    HWND    hwndSplitBar = WinWindowFromID(hwndSplit, ID_SPLITBAR);
+    if (hwndSplitBar)
+    {
+        PSPLITBARDATA psbd = (PSPLITBARDATA)WinQueryWindowULong(hwndSplitBar, QWL_USER);
+        if (psbd)
+            lrc = psbd->sbcd.lPos;
+    }
+
+    return (lrc);
+}
+
+
Index: /trunk/src/helpers/cctl_tooltip.c
===================================================================
--- /trunk/src/helpers/cctl_tooltip.c	(revision 8)
+++ /trunk/src/helpers/cctl_tooltip.c	(revision 8)
@@ -0,0 +1,1569 @@
+
+/*
+ *@@sourcefile cctl_tooltip.c:
+ *      implementation for the "tooltip" common control.
+ *      See comctl.c for an overview.
+ *
+ *      This has been extracted from comctl.c with V0.9.3 (2000-05-21) [umoeller].
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ *@@added V0.9.3 (2000-05-21) [umoeller].
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Tooltips
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+// screen size
+LONG        G_cxScreen = 0,
+            G_cyScreen = 0;
+
+// linked list of all tools which were subclassed for tooltip
+HMTX        G_hmtxSubclassedTools = NULLHANDLE;
+PLINKLIST   G_pllSubclassedTools = NULL;      // linked list of SUBCLASSEDTOOL items
+
+/* ******************************************************************
+ *                                                                  *
+ *   "Tooltip" control                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctlRegisterTooltip:
+ *      this registers the Tooltip window class (ctl_fnwpTooltip)
+ *      for an application. This is required before the tooltip
+ *      control can be used.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL ctlRegisterTooltip(HAB hab)
+{
+    return (WinRegisterClass(hab,
+                             COMCTL_TOOLTIP_CLASS,
+                             ctl_fnwpTooltip,
+                             CS_HITTEST,     // class styles;
+                                             // CS_FRAME not working,
+                                             // CS_CLIPSIBLINGS not working
+                             sizeof(PVOID)*2));   // addt'l bytes to reserve:
+                                    // one pointer for QWL_USER,
+                                    // one more for instance data
+}
+
+/*
+ *@@ SUBCLASSEDTOOL:
+ *      structure created for each control which is
+ *      subclassed by the tooltip control (ctl_fnwpTooltip).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+typedef struct _SUBCLASSEDTOOL
+{
+    HWND        hwndTool;
+    PFNWP       pfnwpOrig;
+    HWND        hwndTooltip;
+    HAB         hab;
+} SUBCLASSEDTOOL, *PSUBCLASSEDTOOL;
+
+/*
+ *@@ ctl_fnwpSubclassedTool:
+ *      window procedure for tools which were subclassed
+ *      to support tooltips.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpSubclassedTool(HWND hwndTool, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = 0;
+
+    PSUBCLASSEDTOOL pst = NULL;
+
+    PLISTNODE pNode = lstQueryFirstNode(G_pllSubclassedTools);
+    while (pNode)
+    {
+        PSUBCLASSEDTOOL pstThis = (PSUBCLASSEDTOOL)pNode->pItemData;
+        if (pstThis->hwndTool == hwndTool)
+        {
+            pst = pstThis;
+            break;
+        }
+        pNode = pNode->pNext;
+    }
+
+    switch (msg)
+    {
+        case WM_MOUSEMOVE:
+        case WM_BUTTON1DOWN:
+        case WM_BUTTON1UP:
+        case WM_BUTTON2DOWN:
+        case WM_BUTTON2UP:
+        case WM_BUTTON3DOWN:
+        case WM_BUTTON3UP:
+        {
+            QMSG qmsg;
+            qmsg.hwnd = hwndTool;
+            qmsg.msg = msg;
+            qmsg.mp1 = mp1;
+            qmsg.mp2 = mp2;
+            /* WinQueryMsgPos(pst->hab,
+                           &qmsg.ptl);
+            qmsg.time = WinQueryMsgTime(pst->hab); */
+            // _Pmpf(("ctl_fnwpSubclassedTool: sending TTM_RELAYEVENT"));
+            WinSendMsg(pst->hwndTooltip,
+                       TTM_RELAYEVENT,
+                       (MPARAM)0,
+                       (MPARAM)&qmsg);
+            mrc = (pst->pfnwpOrig)(hwndTool, msg, mp1, mp2);
+        break; }
+
+        case WM_DESTROY:
+            mrc = (pst->pfnwpOrig)(hwndTool, msg, mp1, mp2);
+            lstRemoveItem(G_pllSubclassedTools, pst);         // this frees the item
+            if (lstCountItems(G_pllSubclassedTools) == 0)
+            {
+                // last item: destroy list
+                lstFree(G_pllSubclassedTools);
+                G_pllSubclassedTools = NULL;
+            }
+        break;
+
+        default:
+            mrc = (pst->pfnwpOrig)(hwndTool, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ SubclassToolForToolinfo:
+ *      this gets called from ctl_fnwpTooltip if a control
+ *      is to be subclassed to support mouse messaging
+ *      (TTF_SUBCLASS flag).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL SubclassToolForToolinfo(HWND hwndTooltip,
+                             HWND hwndTool)
+{
+    BOOL    brc = FALSE;
+    PFNWP   pfnwpOrig = WinSubclassWindow(hwndTool,
+                                          ctl_fnwpSubclassedTool);
+    if (pfnwpOrig)
+    {
+        PSUBCLASSEDTOOL pst = (PSUBCLASSEDTOOL)malloc(sizeof(SUBCLASSEDTOOL));
+        if (pst)
+        {
+            pst->pfnwpOrig = pfnwpOrig;
+            pst->hwndTooltip = hwndTooltip;
+            pst->hwndTool = hwndTool;
+            pst->hab = WinQueryAnchorBlock(hwndTool);
+
+            if (G_pllSubclassedTools == NULL)
+                G_pllSubclassedTools = lstCreate(TRUE);   // auto-free items
+
+            lstAppendItem(G_pllSubclassedTools, pst);
+        }
+    }
+    return (brc);
+}
+
+/*
+ *@@ TOOLTIPDATA:
+ *      private data structure stored in the window
+ *      words of ctl_fnwpTooltip to hold information for
+ *      a tooltip instance.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+typedef struct _TOOLTIPDATA
+{
+    HWND        hwndOwner;          // from WM_CREATE
+    HAB         hab;                // from WM_CREATE
+    ULONG       ulTooltipID;        // from WM_CREATE
+
+    BOOL        fIsActive;          // TRUE per default; changed by TTM_ACTIVATE
+
+    ULONG       idTimerInitial,     // if != 0, "initial" timer is running
+                idTimerAutopop;     // if != 0, "autopop" (hide) timer is running
+
+    ULONG       ulTimeoutInitial,   // "initial" timer timeout (ms)
+                ulTimeoutAutopop,   // "autopop" (hide) timer timeout (ms)
+                ulTimeoutReshow;    // "reshow" timer timeout (ms)
+
+    LINKLIST    llTools;            // linked list of TOOLINFO structures
+                                    // containing the tools
+    FONTMETRICS fontmetrics;        // current font
+    LONG        lForeColor,         // current foreground color
+                lBackColor,         // current background color
+                l3DHiColor,         // 3D border light color
+                l3DLoColor;         // 3D border dark color
+    PSZ         pszPaintText;       // text to paint (new buffer)
+
+    POINTL      ptlPointerLast;     // last mouse pointer position
+
+    PTOOLINFO   ptiMouseOver;       // tool info over which the mouse resides
+
+    BOOL        fIsVisible;         // TRUE if tooltip is visible
+
+    CHAR        szTextBuf[256];     // static buffer for copying/loading strings
+} TOOLTIPDATA, *PTOOLTIPDATA;
+
+// timer IDs
+#define TOOLTIP_ID_TIMER_INITIAL    1
+#define TOOLTIP_ID_TIMER_AUTOPOP    2
+
+// tooltip window border (free spaces)
+#define TOOLTIP_CX_BORDER           5
+#define TOOLTIP_CY_BORDER           3
+
+/*
+ *@@ UpdateTooltipPresColors:
+ *      this gets called during WM_CREATE and WM_PRESPARAMCHANGED
+ *      in ctl_fnwpTooltip to set the colors in TOOLTIPDATA according
+ *      to the control's presparams.
+ */
+
+VOID UpdateTooltipPresColors(HWND hwndTooltip,      // in: tooltip control
+                             PTOOLTIPDATA pttd)     // in/out: tooltip data struct (QWL_USER) with color fields
+{
+    // tooltip background color:
+    pttd->lBackColor = winhQueryPresColor(hwndTooltip,
+                                          PP_MENUBACKGROUNDCOLOR,
+                                          FALSE,             // no inherit
+                                          SYSCLR_ENTRYFIELD);
+    // tooltip text color:
+    pttd->lForeColor = winhQueryPresColor(hwndTooltip,
+                                          PP_MENUFOREGROUNDCOLOR,
+                                          FALSE,             // no inherit
+                                          SYSCLR_WINDOWTEXT);
+    // 3D border light color:
+    pttd->l3DHiColor = winhQueryPresColor(hwndTooltip,
+                                          PP_MENUHILITEFGNDCOLOR,
+                                          FALSE,             // no inherit
+                                          SYSCLR_WINDOWTEXT);
+    // 3D border dark color:
+    pttd->l3DLoColor = winhQueryPresColor(hwndTooltip,
+                                          PP_MENUHILITEBGNDCOLOR,
+                                          FALSE,             // no inherit
+                                          SYSCLR_WINDOWTEXT);
+}
+
+/*
+ *@@ PaintTooltip:
+ *      this gets called from ctl_fnwpTooltip upon WM_PAINT.
+ *
+ *@@added V0.9.1 (99-11-30) [umoeller]
+ */
+
+VOID PaintTooltip(HWND hwndTooltip,
+                  PTOOLTIPDATA pttd)
+{
+    HPS         hps = WinBeginPaint(hwndTooltip, NULLHANDLE, NULL);
+    POINTL      ptl = {0, 0};
+    RECTL       rclWindow,
+                rclWindowBak;
+    ULONG       ulStyle = WinQueryWindowULong(hwndTooltip, QWL_STYLE);
+
+    ULONG       ulRounding = 0;
+    if (ulStyle & TTS_ROUNDED)
+        ulRounding = TT_ROUNDING;
+
+
+    gpihSwitchToRGB(hps);
+
+    // get tooltip size; this has been properly calculated
+    // by TTM_SHOWTOOLTIPNOW earlier
+    WinQueryWindowRect(hwndTooltip, &rclWindow);
+    memcpy(&rclWindowBak, &rclWindow, sizeof(RECTL));
+
+    if (ulStyle & TTS_SHADOW)
+    {
+        // if shadows are on, the window is too large;
+        // make rectl smaller for rest of paint
+        rclWindow.xRight -= TT_SHADOWOFS;
+        rclWindow.yBottom += TT_SHADOWOFS;
+        // restore old pattern
+        GpiSetPattern(hps, PATSYM_DEFAULT);
+    }
+
+    // draw shadow
+    if (ulStyle & TTS_SHADOW)
+    {
+        GpiSetColor(hps, CLR_BLACK);
+        GpiSetPattern(hps, PATSYM_HALFTONE);
+        ptl.x = rclWindowBak.xLeft + TT_SHADOWOFS;
+        ptl.y = rclWindowBak.yBottom;
+        GpiMove(hps, &ptl);
+        ptl.x = rclWindowBak.xRight - 1;
+        ptl.y = rclWindowBak.yTop - TT_SHADOWOFS;
+        GpiBox(hps,
+               DRO_FILL,
+               &ptl,
+               ulRounding, ulRounding);
+        // restore pattern
+        GpiSetPattern(hps, PATSYM_DEFAULT);
+    }
+
+    // draw "real" rectangle
+    ptl.x = rclWindow.xLeft;
+    ptl.y = rclWindow.yBottom;
+    GpiMove(hps, &ptl);
+    ptl.x = rclWindow.xRight - 1;
+    ptl.y = rclWindow.yTop - 1;
+    GpiSetColor(hps, pttd->lBackColor);
+    GpiBox(hps,
+           DRO_FILL,
+           &ptl,
+           6, 6);
+
+    GpiSetColor(hps, pttd->lForeColor);
+    GpiBox(hps,
+           DRO_OUTLINE,
+           &ptl,
+           ulRounding, ulRounding);
+
+    if (pttd->pszPaintText)
+    {
+        RECTL rclText;
+        GpiSetColor(hps, pttd->lForeColor);
+        GpiSetBackColor(hps, pttd->lBackColor);
+        rclText.xLeft = rclWindow.xLeft + TOOLTIP_CX_BORDER;
+        rclText.xRight = rclWindow.xRight - TOOLTIP_CX_BORDER;
+        rclText.yBottom = rclWindow.yBottom + TOOLTIP_CY_BORDER;
+        rclText.yTop = rclWindow.yTop - TOOLTIP_CY_BORDER;
+        winhDrawFormattedText(hps,
+                              &rclText,
+                              pttd->pszPaintText,
+                              DT_LEFT | DT_TOP | DT_WORDBREAK);
+    }
+
+    WinEndPaint(hps);
+}
+
+/*
+ *@@ TtmShowTooltip:
+ *      implementation for TTM_SHOW_TOOLTIP.
+ *
+ *      Depending on fShow, this shows or hides the
+ *      tool tip at the position specified by the
+ *      current tool or the mouse pointer (specified
+ *      by the tool's style flags).
+ *
+ *@@added V0.9.1 (2000-02-04) [umoeller]
+ */
+
+VOID TtmShowTooltip(HWND hwndTooltip,
+                    PTOOLTIPDATA pttd,
+                    BOOL fShow)  // if TRUE: show, else: HIDE
+{
+    if (fShow)
+    {
+        /*
+         * show tooltip::
+         *
+         */
+
+        POINTL      ptlPointer;
+        HPS         hps;
+
+        // free old text
+        if (pttd->pszPaintText)
+        {
+            free(pttd->pszPaintText);
+            pttd->pszPaintText = NULL;
+        }
+
+        WinQueryPointerPos(HWND_DESKTOP, &ptlPointer);
+
+        if (    (ptlPointer.x == pttd->ptlPointerLast.x)
+             && (ptlPointer.y == pttd->ptlPointerLast.y)
+           )
+        {
+            pttd->pszPaintText = NULL;
+            // mouse not moved since timer was started:
+            // find the current TOOLINFO
+            if (pttd->ptiMouseOver)
+            {
+                TOOLINFO    tiTemp;
+                memcpy(&tiTemp, pttd->ptiMouseOver, sizeof(TOOLINFO));
+                // get the text for the TOOLINFO
+                WinSendMsg(hwndTooltip,
+                           TTM_GETTEXT,
+                           (MPARAM)0,
+                           (MPARAM)&tiTemp);
+                if (tiTemp.lpszText)
+                    pttd->pszPaintText = strdup(tiTemp.lpszText);
+                else
+                    pttd->pszPaintText = NULL;
+            }
+
+            if (pttd->pszPaintText)
+            {
+                // find out how much space we need
+                RECTL   rcl = { 0, 0, 300, 1000 };
+                POINTL  ptlTooltip;
+                LONG    cx, cy;
+                ULONG   ulStyle = WinQueryWindowULong(hwndTooltip, QWL_STYLE);
+
+                G_cxScreen = WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN),
+                G_cyScreen = WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN);
+
+                hps = WinGetPS(hwndTooltip);
+                winhDrawFormattedText(hps,
+                                      &rcl,
+                                      pttd->pszPaintText,
+                                      DT_LEFT | DT_TOP | DT_WORDBREAK | DT_QUERYEXTENT);
+                WinReleasePS(hps);
+
+                // calc width and height of tooltip
+                cx = rcl.xRight + 2*TOOLTIP_CX_BORDER;
+                cy = (rcl.yTop - rcl.yBottom) + 2*TOOLTIP_CY_BORDER;
+
+                // calc x and y pos of tooltip
+                if (pttd->ptiMouseOver->uFlags & TTF_CENTERTIP)
+                {
+                    // center below control:
+                    if (pttd->ptiMouseOver->uFlags & TTF_IDISHWND)
+                    {
+                        SWP swpTool;
+                        WinQueryWindowPos(pttd->ptiMouseOver->uId, &swpTool);
+                        ptlTooltip.x = swpTool.x;
+                        ptlTooltip.y = swpTool.y;
+                        // convert x, y to desktop points
+                        WinMapWindowPoints(WinQueryWindow(pttd->ptiMouseOver->uId,
+                                                          QW_PARENT), // hwndFrom
+                                           HWND_DESKTOP,            // hwndTo
+                                           &ptlTooltip,
+                                           1);
+                        ptlTooltip.x += ((swpTool.cx - cx) / 2L);
+                        ptlTooltip.y -= cy;
+                    }
+                    // else xxx rectangle def
+                }
+                else
+                {
+                    // use pointer pos:
+                    ptlTooltip.x = ptlPointer.x - cx/2;
+                    ptlTooltip.y = ptlPointer.y - cy;
+                }
+
+                // if "shy mouse" is enabled, make
+                // sure the tool tip is not under the
+                // mouse pointer
+                if (ulStyle & TTF_SHYMOUSE)
+                {
+                    // we need to subtract the current mouse
+                    // pointer's hot spot from the current
+                    // pointer position
+                    HPOINTER    hptr = WinQueryPointer(HWND_DESKTOP);
+                    POINTERINFO pi;
+                    if (WinQueryPointerInfo(hptr, &pi))
+                    {
+                        /* ULONG cyPointer = WinQuerySysValue(HWND_DESKTOP,
+                                                           SV_CYPOINTER); */
+                        // calc bottom edge of mouse pointer rect
+                        ULONG yBottomPointer = (ptlPointer.y - pi.yHotspot);
+                        // _Pmpf(("yHotspot: %d", pi.yHotspot));
+                        if (   (ptlTooltip.y + cy) // top edge of tool tip
+                             > yBottomPointer
+                           )
+                        {
+                            ptlTooltip.y = ptlPointer.y - cy - pi.yHotspot;
+                        }
+                    }
+                }
+
+                // constrain to screen
+                if (ptlTooltip.x < 0)
+                    ptlTooltip.x = 0;
+                if (ptlTooltip.y < 0)
+                    ptlTooltip.y = 0;
+                if (ptlTooltip.x + cx > G_cxScreen)
+                    ptlTooltip.x = G_cxScreen-cx;
+                if (ptlTooltip.y + cy > G_cyScreen)
+                    ptlTooltip.y = G_cyScreen-cy;
+
+                // if shadow is enabled,
+                // enlarge; the shadow might by
+                // off-screen now, but that's OK
+                if (ulStyle & TTS_SHADOW)
+                {
+                    cx += TT_SHADOWOFS;
+                    cy += TT_SHADOWOFS;
+                    ptlTooltip.y -= TT_SHADOWOFS;
+                }
+
+                // notify owner
+                WinSendMsg(pttd->hwndOwner,
+                           WM_CONTROL,
+                           MPFROM2SHORT(pttd->ulTooltipID,  // tooltip control wnd ID
+                                        TTN_SHOW),
+                           0);
+
+                // set tooltip position at the pos we calculated
+                // and show tooltip
+                WinSetWindowPos(hwndTooltip,
+                                HWND_TOP,
+                                ptlTooltip.x,
+                                ptlTooltip.y,
+                                cx, cy,
+                                SWP_MOVE | SWP_SIZE | SWP_ZORDER | SWP_SHOW);
+                pttd->fIsVisible = TRUE;
+
+                // start autopop timer
+                pttd->idTimerAutopop = WinStartTimer(pttd->hab,
+                                                     hwndTooltip,
+                                                     TOOLTIP_ID_TIMER_AUTOPOP,
+                                                     pttd->ulTimeoutAutopop);
+            }
+        }
+    } // end if (mp1)
+    else
+    {
+        /*
+         * hide tooltip::
+         *
+         */
+
+        if (pttd->fIsVisible)
+        {
+            WinSendMsg(pttd->hwndOwner,
+                       WM_CONTROL,
+                       MPFROM2SHORT(pttd->ulTooltipID,  // tooltip control wnd ID
+                                    TTN_POP),
+                       0);
+            WinShowWindow(hwndTooltip, FALSE);
+        }
+
+        // stop autopop timer
+        if (pttd->idTimerAutopop)
+        {
+            WinStopTimer(pttd->hab,
+                         hwndTooltip,
+                         TOOLTIP_ID_TIMER_AUTOPOP);
+            pttd->idTimerAutopop = 0;
+        }
+    }
+
+    // store new visibility
+    pttd->fIsVisible = fShow;
+}
+
+/*
+ *@@ ctl_fnwpTooltip:
+ *      window procedure for the "tooltip" control. This control is
+ *      largely source-code compatible to the Win32 version and has
+ *      been modelled according to the Win32 programmer's reference.
+ *
+ *      A tooltip control is a small pop-up window that displays a
+ *      single line of descriptive text giving the purpose of "tools"
+ *      in an application.
+ *      A "tool" is either a window, such as a child window or control,
+ *      or an application-defined rectangular area within a window.
+ *      See the TTM_ADDTOOL message for details.
+ *
+ *      To clarify: There is usually one tooltip control, which is hidden
+ *      most of the time, for many tools (parts of a visible window).
+ *      When the user puts the cursor on a tool and leaves it there for
+ *      approximately one-half second, the tooltip control is set up for
+ *      that tool and made visible. The tooltip control appears near the
+ *      pointer and disappears when the user clicks a mouse button or moves
+ *      the pointer off of the tool.
+ *
+ *      The Win32 concept has been extended with this implementation to
+ *      display even longer texts, including line breaks. The tooltip
+ *      window is formatted accordingly.
+ *
+ *      All default window styles are ignored when you create the tooltip,
+ *      but will rather be set by this window proc automatically. The
+ *      only valid window styles are the TTS_* flags (see notes below).
+ *
+ *      Presentation parameters are fully supported.
+ *
+ *      To create a tooltip control using comctl.c, use
+ +          ctlRegisterTooltip(hab);
+ +          hwndTooltip = WinCreateWindow(HWND_DESKTOP, // parent
+ +                           COMCTL_TOOLTIP_CLASS, // wnd class (comctl.h)
+ +                           NULL,          // window text
+ +                           TTS_ALWAYSTIP, // window style, ignored except for TTS_* flags
+ +                           0, 0, 0, 0,    // window pos and size, ignored
+ +                           hwndOwner,     // owner window -- important!
+ +                           NULLHANDLE,    // hwndInsertBehind, ignored
+ +                           ulID,          // window ID, optional
+ +                           NULL,          // control data
+ +                           NULL);         // presparams
+ +
+ *      ctl_fnwpTooltip automatically sets the size, position, and visibility
+ *      of the tooltip control. The size of the tooltip window will vary
+ *      depending on the text to be displayed and on the font presentation
+ *      parameters, which are supported by this control (OS/2 only).
+ *
+ *      Note: OS/2 normally does not destroy windows which are only owned by
+ *      another window. As a result, when the tooltip's owner is destroyed,
+ *      you must explicitly also destroy the tooltip window.
+ *
+ *      Under both Win32 and OS/2, a tooltip control has two class-specific styles:
+ *
+ *      -- TTS_ALWAYSTIP: the tooltip appears when the cursor is on a tool,
+ *          regardless of whether the tooltip control's owner window is active
+ *          or inactive. Without this style, the tooltip control appears when the
+ *          tool's owner window is active, but not when it is inactive.
+ *
+ *      -- TTS_NOPREFIX: this prevents the system from stripping the ampersand (&)
+ *          character from a string. If a tooltip control does not have the TTS_NOPREFIX
+ *          style, the system automatically strips ampersand characters, allowing an
+ *          application to use the same string as both a menu item and tooltip text.
+ *
+ *      The tooltip control can be (de)activated using the TTM_ACTIVATE message.
+ *
+ *      To register tools with the tooltip control (to make it do anything
+ *      meaningful), send the TTM_ADDTOOL message to the tooltip control.
+ *
+ *      This control supports the following presentation parameters (if the
+ *      PP_* value is not found, the SYSCLR_* system color is used):
+ *      --  PP_MENUBACKGROUNDCOLOR / SYSCLR_ENTRYFIELD: tooltip background color.
+ *      --  PP_MENUFOREGROUNDCOLOR / SYSCLR_WINDOWTEXT: tooltip text color.
+ *      --  PP_MENUHILITEFGNDCOLOR / SYSCLR_WINDOWTEXT: 3D border light color.
+ *      --  PP_MENUHILITEBGNDCOLOR / SYSCLR_WINDOWTEXT: 3D border dark color.
+ *      --  PP_FONTNAMESIZE: font to use for the tooltip. The default is "8.Helv"
+ *              on Warp 3 and "9.WarpSans" on Warp 4.
+ *
+ *      So per default, if no presentation parameters are set, the control
+ *      paints the background like an entry field and the text and the border
+ *      in the same window text color (usually black).
+ *      The tooltip does _not_ inherit presentation parameters from the parent,
+ *      so unless you explicitly set presparams, the tooltip looks pretty much
+ *      like the Win32 counterpart (with the default system colors, black border
+ *      and text on yellow background).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpTooltip(HWND hwndTooltip, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = 0;
+
+    PTOOLTIPDATA pttd = (PTOOLTIPDATA)WinQueryWindowPtr(hwndTooltip, 1);
+
+    TRY_LOUD(excpt1, NULL)
+    {
+        switch (msg)
+        {
+            /*
+             * WM_CREATE:
+             *      this message is sent to the window procedure of
+             *      the window being created, thus offering it an
+             *      opportunity to initialize that window.
+             *
+             *      The window procedure receives this after the window
+             *      is created but before the window becomes visible.
+             */
+
+            case WM_CREATE:
+            {
+                PCREATESTRUCT pcs = (PCREATESTRUCT)mp2;
+
+                // allocate and initialize tooltip data
+                pttd = (PTOOLTIPDATA)malloc(sizeof(TOOLTIPDATA));
+                if (pttd)
+                {
+                    CHAR        szFont[256];
+                    memset(pttd, 0, sizeof(TOOLTIPDATA));
+                    WinSetWindowPtr(hwndTooltip, 1, pttd);
+
+                    pttd->hwndOwner = pcs->hwndOwner;
+                    pttd->hab = WinQueryAnchorBlock(hwndTooltip);
+                    pttd->ulTooltipID = pcs->id;
+
+                    pttd->fIsActive = TRUE;
+
+                    // default timeouts
+                    pttd->ulTimeoutInitial = 1000;
+                    pttd->ulTimeoutAutopop = 5000;
+                    pttd->ulTimeoutReshow = 500;
+
+                    // get colors from presparams/syscolors
+                    UpdateTooltipPresColors(hwndTooltip, pttd);
+
+                    // check if font presparam set
+                    if (WinQueryPresParam(hwndTooltip,
+                                          PP_FONTNAMESIZE, 0,
+                                          NULL,
+                                          sizeof(szFont),
+                                          szFont,
+                                          QPF_NOINHERIT)
+                            == 0)
+                    {
+                        // no: set default font presparam
+                        // (we never want the System Proportional font)
+                        winhSetWindowFont(hwndTooltip,
+                                          NULL);      // default (WarpSans or 8.Helv)
+                    }
+
+                    pttd->pszPaintText = strdup("undefined");
+
+                    lstInit(&pttd->llTools, TRUE);      // auto-free items
+
+                    // override CREATESTRUCT
+                    WinSetWindowPos(hwndTooltip,
+                                    HWND_TOP,
+                                    50, 50,
+                                    100, 100,
+                                    SWP_MOVE | SWP_SIZE | SWP_ZORDER | SWP_HIDE);
+
+                    mrc = (MPARAM)FALSE;
+                }
+                else
+                    // malloc failed:
+                    mrc = (MPARAM)TRUE;
+            break; }
+
+            /*
+             * WM_DESTROY:
+             *      clean up upon destruction.
+             */
+
+            case WM_DESTROY:
+                // stop timers
+                if (pttd->ulTimeoutInitial)
+                    WinStopTimer(pttd->hab,
+                                 hwndTooltip,
+                                 pttd->ulTimeoutInitial);
+                if (pttd->idTimerAutopop)
+                    WinStopTimer(pttd->hab,
+                                 hwndTooltip,
+                                 pttd->idTimerAutopop);
+                if (pttd->pszPaintText)
+                    free(pttd->pszPaintText);
+                lstClear(&pttd->llTools);
+                free(pttd);
+
+                mrc = (MPARAM)0;
+            break;
+
+            /*
+             * WM_HITTEST:
+             *      since we have the CS_HITTEST class style set,
+             *      we get this message. We return HT_TRANSPARENT
+             *      always so the tooltip does not block mouse clicks
+             *      for the windows which lie below.
+             */
+
+            case WM_HITTEST:    // done
+                mrc = (MPARAM)HT_TRANSPARENT;
+            break;
+
+            /*
+             * WM_TIMER:
+             *      posted when either the "initial" timer
+             *      or the "autopop" timer times out.
+             *      We'll show or hide the tooltip then.
+             */
+
+            case WM_TIMER:      // done
+            {
+                USHORT  usTimer = SHORT1FROMMP(mp1);
+
+                switch (usTimer)
+                {
+                    case TOOLTIP_ID_TIMER_INITIAL:
+                        // _Pmpf(("WM_TIMER: Stopping initial timer: %d", usTimer));
+                        WinStopTimer(pttd->hab,
+                                     hwndTooltip,
+                                     usTimer);
+                        pttd->idTimerInitial = 0;
+
+                        if (pttd->fIsActive)
+                            // show tooltip
+                            WinPostMsg(hwndTooltip, TTM_SHOWTOOLTIPNOW, (MPARAM)TRUE, 0);
+                    break;
+
+                    case TOOLTIP_ID_TIMER_AUTOPOP:
+                        // _Pmpf(("WM_TIMER: Stopping autopop timer: %d", usTimer));
+                        WinStopTimer(pttd->hab,
+                                     hwndTooltip,
+                                     usTimer);
+                        pttd->idTimerAutopop = 0;
+                        WinPostMsg(hwndTooltip, TTM_SHOWTOOLTIPNOW, (MPARAM)FALSE, 0);
+                    break;
+
+                    default:
+                        mrc = WinDefWindowProc(hwndTooltip, msg, mp1, mp2);
+                } // end switch
+
+            break; }
+
+            /*
+             * WM_PAINT:
+             *
+             */
+
+            case WM_PAINT:      // done
+                PaintTooltip(hwndTooltip, pttd);
+            break;
+
+            /*
+             * WM_PRESPARAMCHANGED:
+             *      presentation parameter has changed.
+             */
+
+            case WM_PRESPARAMCHANGED:
+            {
+                LONG    lPPIndex = (LONG)mp1;
+                switch (lPPIndex)
+                {
+                    case 0:     // layout palette thing dropped
+                    case PP_MENUBACKGROUNDCOLOR:
+                    case PP_MENUFOREGROUNDCOLOR:
+                    case PP_MENUHILITEFGNDCOLOR:
+                    case PP_MENUHILITEBGNDCOLOR:
+                        // re-query our presparams
+                        UpdateTooltipPresColors(hwndTooltip, pttd);
+                }
+            break; }
+
+            /*
+             * WM_SYSCOLORCHANGE:
+             *      system color has changed.
+             */
+
+            case WM_SYSCOLORCHANGE:
+                UpdateTooltipPresColors(hwndTooltip, pttd);
+            break;
+
+           /*
+            *@@ TTM_ACTIVATE:
+            *      activates or deactives the tooltip control.
+            *
+            *      Parameters:
+            *      -- BOOL mp1: if TRUE, activate, if FALSE, deactivate.
+            *      -- mp2: always 0.
+            *
+            *      Return value: 0 always.
+            */
+
+            case TTM_ACTIVATE:      // done
+                pttd->fIsActive = (BOOL)mp1;
+                if (!pttd->fIsActive)
+                    // disable: hide tooltip
+                    WinPostMsg(hwndTooltip, TTM_SHOWTOOLTIPNOW, (MPARAM)FALSE, 0);
+            break;
+
+            /*
+             *@@ TTM_ADDTOOL:
+             *      registers a tool with a tooltip control.
+             *
+             *      Parameters:
+             *      --  mp1: always 0.
+             *      --  PTOOLINFO mp2: information about the tool.
+             *
+             *      Return value: TRUE if successful or FALSE otherwise.
+             *
+             *      A tooltip control can support any number of tools. To
+             *      support a particular tool, you must register the tool
+             *      with the tooltip control by sending the TTM_ADDTOOL
+             *      message to the tooltip control. The message includes
+             *      the address of a TOOLINFO structure, which provides
+             *      information the tooltip control needs to display text
+             *      for the tool. The cbSize member is required and must
+             *      specify the size of the structure.
+             *
+             *      A tooltip control supports tools implemented as windows
+             *      (such as child windows or control windows) and as
+             *      rectangular areas within a window's client area.
+             *
+             *      --  When you add a tool implemented as a rectangular area, the
+             *          "hwnd" member of TOOLINFO must specify the handle of the
+             *          window that contains the area, and the "rect" member must
+             *          specify the client coordinates of the area's bounding
+             *          rectangle. In addition, the "uId" member must specify the
+             *          application-defined identifier for the tool.
+             *
+             *      --  When you add a tool implemented as a window, the "uId"
+             *          member of TOOLINFO must contain the window handle of the
+             *          tool. Also, the "uFlags" member must specify the TTF_IDISHWND
+             *          value, which tells the tooltip control to interpret the "uId"
+             *          member as a window handle.
+             *
+             *      When you add a tool to a tooltip control, the "lpszText"
+             *      member of the TOOLINFO structure must specify the address
+             *      of the string to display for the tool. You can change the
+             *      text any time after adding the tool by using the
+             *      TTM_UPDATETIPTEXT message.
+             *
+             *      If the high-order word of lpszText is zero, the low-order
+             *      word must be the identifier of a string resource. When
+             *      the tooltip control needs the text, the system loads
+             *      the specified string resource from the application instance
+             *      identified by the "hinst" member of TOOLINFO.
+             *
+             *      If you specify the LPSTR_TEXTCALLBACK value in the lpszText
+             *      member, the tooltip control notifies the window specified
+             *      in the hwnd member of TOOLINFO whenever the tooltip control
+             *      needs to display text for the tool. The tooltip control
+             *      sends the TTN_NEEDTEXT notification message to the window.
+             *      The message includes the address of a TOOLTIPTEXT structure,
+             *      which contains the window handle as well as the
+             *      application-defined identifier for the tool. The window
+             *      examines the structure to determine the tool for which text
+             *      is needed, and it fills the appropriate structure members
+             *      with information that the tooltip control needs to display
+             *      the string.
+             *
+             *      Many applications create toolbars containing tools that
+             *      correspond to menu commands. For such tools, it is convenient
+             *      for the tooltip control to display the same text as the
+             *      corresponding menu item. Windows automatically strips the
+             *      ampersand (&) accelerator characters from all strings passed
+             *      to a tooltip control, unless the control has the TTS_NOPREFIX style.
+             *
+             *      OS/2 note: This implementation does this too, but will not
+             *      strip the ampersand, but the tilde (~) character.
+             *
+             *      To retrieve the text for a tool, use the TTM_GETTEXT message.
+             */
+
+            case TTM_ADDTOOL:       // done
+                mrc = (MPARAM)FALSE;
+                if (mp2)
+                {
+                    PTOOLINFO ptiPassed = (PTOOLINFO)mp2,
+                              ptiNew = (PTOOLINFO)malloc(sizeof(TOOLINFO));
+                    if (ptiNew)
+                    {
+                        memcpy(ptiNew, ptiPassed, ptiPassed->cbSize);
+                        lstAppendItem(&pttd->llTools,
+                                      ptiNew);
+
+                        if (ptiPassed->uFlags & TTF_IDISHWND)
+                            if (ptiPassed->uFlags & TTF_SUBCLASS)
+                                SubclassToolForToolinfo(hwndTooltip,
+                                                        ptiPassed->uId);
+
+                        mrc = (MPARAM)TRUE;
+                    }
+                }
+            break;
+
+            /*
+             *@@ TTM_DELTOOL:
+             *      removes a tool from a tooltip control.
+             *
+             *      Parameters:
+             *      --  mp1: always 0.
+             *      --  PTOOLINFO mp2: information about the tool;
+             *              all fields except cbSize, hwnd, uID are
+             *              ignored.
+             *
+             *      Return value: 0 always.
+             */
+
+            case TTM_DELTOOL:       // done
+            {
+                PTOOLINFO ptiSearch = (PTOOLINFO)mp2;
+                if (ptiSearch)
+                {
+                    PLISTNODE pToolNode = lstQueryFirstNode(&pttd->llTools);
+                    while (pToolNode)
+                    {
+                        PTOOLINFO ptiThis = (PTOOLINFO)pToolNode->pItemData;
+                        if (    (ptiThis->hwnd == ptiSearch->hwnd)
+                             && (ptiThis->uId == ptiSearch->uId)
+                           )
+                        {
+                            // found:
+                            lstRemoveNode(&pttd->llTools, pToolNode);
+                            break;
+                        }
+                    }
+                    pToolNode = pToolNode->pNext;
+                }
+            break; }
+
+            /*
+             *@@ TTM_NEWTOOLRECT:
+             *      sets a new bounding rectangle for a tool
+             *      already registered with a tooltip control.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PTOOLINFO mp2: information about the tool;
+             *              all fields except hwnd, uID, and rect
+             *              are ignored.
+             *
+             *      Return value: 0 always.
+             *
+             *      If an application includes a tool implemented as a rectangular
+             *      area and the size or position of the control changes, it can
+             *      use the TTM_NEWTOOLRECT message to report the change to the
+             *      tooltip control. An application does not need to report size
+             *      and position changes for a tool implemented as a window.
+             *      Reporting that is not necessary because the tooltip control
+             *      uses the window handle of a tool to determine if the cursor
+             *      is on the tool, not the tool's bounding rectangle.
+             */
+
+            case TTM_NEWTOOLRECT:
+            break;
+
+            /*
+             *@@ TTM_RELAYEVENT:
+             *      passes a mouse message to a tooltip control
+             *      for further processing.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PQMSG mp2: pointer to a QMSG struct (Win95: MSG struct)
+             *          containing a mouse message. Only the above messages
+             *          are processed.
+             *
+             *      Return value: 0 always.
+             *
+             *      A tooltip control needs to receive mouse messages to determine
+             *      when to display the tooltip window. Because mouse messages are
+             *      sent only to the window that contains the cursor, you must
+             *      use the TTM_RELAYEVENT message to relay mouse messages to the
+             *      tooltip control.
+             *
+             *      If a tool is implemented as a rectangular area in an
+             *      application-defined window, the window procedure receives all
+             *      mouse messages and can relay the ones listed below to the
+             *      tooltip control, using this message.
+             *
+             *      However, if a tool is implemented as a system-defined window,
+             *      the mouse messages are sent to that window and are not readily
+             *      available to the application. There are two ways you can have
+             *      the tooltip control notified of these mouse messages:
+             *
+             *      -- You can specify the TTF_SUBCLASS flag when adding the tool
+             *         to the tooltip control. The tooltip control will then subclass
+             *         the tool window to get mouse-related messages, and you're off.
+             *
+             *      -- You can implement a message hook for your process and check
+             *         for your tool windows there and send TTM_RELAYEVENT to the
+             *         tooltip control. In that case, you need to intercept the
+             *         messages specified below.
+             *
+             *      When a tooltip control receives a relayed WM_MOUSEMOVE message,
+             *      it determines whether the cursor is in the bounding rectangle of
+             *      a tool. If the cursor is there, the tooltip control sets a timer.
+             *      At the end of the time-out duration, the tooltip control checks
+             *      the position of the cursor to see whether it has moved. If the
+             *      cursor has not, the tooltip control retrieves the text for the tool,
+             *      copies the text into the tooltip window, and shows the window.
+             *      The tooltip control continues to show the window until it receives
+             *      a relayed button-up or button-down message or until a relayed
+             *      WM_MOUSEMOVE message indicates that the cursor has moved outside
+             *      the bounding rectangle of the tool.
+             *
+             *      For the timer descriptions, see TTM_SETDELAYTIME.
+             *
+             *      When it is about to be displayed, a tootip control sends the
+             *      TTN_SHOW notification to the owner window. A tooltip control
+             *      sends the TTN_POP notification when it is about to be hidden.
+             *      Each notification is sent in the context of a WM_NOTIFY message
+             *      (OS/2: WM_CONTROL).
+             *
+             *      Relevant messages:
+             *      --  WM_MOUSEMOVE
+             *      --  WM_BUTTON1DOWN  (Win95: WM_LBUTTONDOWN)
+             *      --  WM_BUTTON1UP    (Win95: WM_LBUTTONUP)
+             *      --  WM_BUTTON2DOWN  (Win95: WM_RBUTTONDOWN)
+             *      --  WM_BUTTON2UP    (Win95: WM_RBUTTONUP)
+             *      --  WM_BUTTON3DOWN  (Win95: WM_MBUTTONDOWN)
+             *      --  WM_BUTTON3UP    (Win95: WM_MBUTTONUP)
+             */
+
+            case TTM_RELAYEVENT:
+            {
+                PQMSG pqmsg = (PQMSG)mp2;
+                if (pqmsg)
+                {
+                    POINTL      ptlPointer;
+                    PLISTNODE   pToolNode;
+
+                    if (pttd->idTimerInitial)
+                    {
+                        // _Pmpf(("TTM_RELAYEVENT: Stopping timer: %d", pttd->idTimerInitial));
+                        WinStopTimer(pttd->hab,
+                                     hwndTooltip,
+                                     TOOLTIP_ID_TIMER_INITIAL);
+                        pttd->idTimerInitial = 0;
+                    }
+
+                    WinQueryPointerPos(HWND_DESKTOP, &ptlPointer);
+
+                    // find TOOLINFO from mouse position
+                    pttd->ptiMouseOver = NULL;
+                    pToolNode = lstQueryFirstNode(&pttd->llTools);
+                    while (pToolNode)
+                    {
+                        PTOOLINFO pti = (PTOOLINFO)pToolNode->pItemData;
+                        if (    (pti->uFlags &TTF_IDISHWND)
+                             && (pti->uId == pqmsg->hwnd)
+                           )
+                        {
+                            pttd->ptiMouseOver = pti;
+                            break;
+                        }
+                        pToolNode = pToolNode->pNext;
+                    }
+
+                    if (    (ptlPointer.x != pttd->ptlPointerLast.x)
+                         || (ptlPointer.y != pttd->ptlPointerLast.y)
+                         || (pqmsg->msg == WM_BUTTON1DOWN)
+                         || (pqmsg->msg == WM_BUTTON2DOWN)
+                         || (pqmsg->msg == WM_BUTTON3DOWN)
+                       )
+                    {
+                        // mouse pos changed:
+                        WinPostMsg(hwndTooltip,
+                                   TTM_SHOWTOOLTIPNOW,
+                                   (MPARAM)FALSE,
+                                   0);
+                        memcpy(&pttd->ptlPointerLast, &ptlPointer, sizeof(POINTL));
+
+                        if (    (pttd->ptiMouseOver)
+                             && (pttd->fIsActive)
+                           )
+                        {
+                            // tool found and tooltip is activated:
+                            pttd->idTimerInitial = WinStartTimer(pttd->hab,
+                                                                 hwndTooltip,
+                                                                 TOOLTIP_ID_TIMER_INITIAL,
+                                                                 pttd->ulTimeoutInitial);
+                            // _Pmpf(("TTM_RELAYEVENT: Started timer: %d", pttd->idTimerInitial));
+                        }
+                    }
+                } // end if (pqmsg)
+            break; }
+
+            /*
+             *@@ TTM_SETDELAYTIME:
+             *      overrides a few default timeout values for the
+             *      tooltip control.
+             *
+             *      A tooltip control actually has three time-out durations
+             *      associated with it. The initial duration is the length
+             *      of time that the cursor must remain stationary within
+             *      the bounding rectangle of a tool before the tooltip window
+             *      is displayed. The reshow duration is the length of the delay
+             *      before subsequent tooltip windows are displayed when the
+             *      cursor moves from one tool to another. The autopopup duration
+             *      is the length of time that the tooltip window remains
+             *      displayed before it is hidden. That is, if the cursor remains
+             *      stationary within the bounding rectangle after the tooltip
+             *      window is displayed, the tooltip window is automatically
+             *      hidden at the end of the autopopup duration.
+             *
+             *      You can adjust all of the time-out durations by using this
+             *      message.
+             *
+             *      Parameters:
+             *      -- USHORT mp1: parameter selection. One of the following:
+             *              -- TTDT_AUTOMATIC: automatically calculates the initial,
+             *                 reshow, and autopopup durations based on the value of iDelay.
+             *              -- TTDT_AUTOPOP: sets the length of time before the tooltip
+             *                  window is hidden if the cursor remains stationary
+             *                  in the tool's bounding rectangle after the tooltip window
+             *                  has appeared.
+             *              -- TTDT_INITIAL: sets the length of time that the cursor must
+             *                  remain stationary within the bounding rectangle of
+             *                  a tool before the tooltip window is displayed.
+             *              -- TTDT_RESHOW: sets the length of the delay before subsequent
+             *                  tooltip windows are displayed when the cursor is moved
+             *                  from one tool to another.
+             *      -- ULONG mp2: new duration, in milliseconds.
+             *
+             *      Return value: 0 always.
+             */
+
+            case TTM_SETDELAYTIME:  // done
+            {
+                ULONG   iDelay = (ULONG)mp2;
+                switch (SHORT1FROMMP(mp1))
+                {
+                    case TTDT_AUTOMATIC:
+                        pttd->ulTimeoutInitial = iDelay;
+                        pttd->ulTimeoutAutopop = iDelay * 5;
+                        pttd->ulTimeoutReshow = iDelay / 2;
+                    break;
+
+                    case TTDT_AUTOPOP:
+                        pttd->ulTimeoutAutopop = iDelay;
+                    break;
+
+                    case TTDT_INITIAL:
+                        pttd->ulTimeoutInitial = iDelay;
+                    break;
+
+                    case TTDT_RESHOW:
+                        pttd->ulTimeoutReshow = iDelay;
+                    break;
+                }
+            break; }
+
+            /*
+             *@@ TTM_GETTEXT:
+             *      retrieves the text for a tool in the tooltip control.
+             *
+             *      Parameters:
+             *      -- mp1: always 0
+             *      -- PTOOLINFO mp2: pointer to a TOOLINFO structure. When sending the
+             *          message, the hwnd and uId members identify a tool. If the tooltip
+             *          control includes the tool, the lpszText member receives the pointer
+             *          to the string.
+             *
+             *      Return value: 0 always.
+             *
+             *      Additional note: On input, if TOOLINFO.lpszText == LPSTR_TEXTCALLBACK,
+             *      this sends the TTN_NEEDTEXT notification to TOOLINFO.hwnd.
+             */
+
+            case TTM_GETTEXT:       // done, I think
+            {
+                PTOOLINFO pti = (PTOOLINFO)mp2;
+
+                if (pti->lpszText == LPSTR_TEXTCALLBACK)
+                {
+                    // TTN_NEEDTEXT notification desired:
+                    // compose values for that msg
+                    TOOLTIPTEXT     ttt;
+                    memset(&ttt, 0, sizeof(ttt));
+                    ttt.hdr.hwndFrom = hwndTooltip;
+
+                    if (pti->uFlags & TTF_IDISHWND)
+                        // HWND mode:
+                        ttt.uFlags = TTF_IDISHWND;
+                    ttt.hdr.idFrom = pti->uId;      // HWND or app-def'd ID
+                    ttt.hdr.code = TTN_NEEDTEXT;
+                    WinSendMsg(pti->hwnd,
+                               WM_CONTROL,
+                               MPFROM2SHORT(pttd->ulTooltipID,  // tooltip control wnd ID
+                                            TTN_NEEDTEXT),
+                               &ttt);
+
+                    // in case of error: set lpszText to NULL; this
+                    // is not specified in the docs however.
+                    pti->lpszText = NULL;
+
+                    if (ttt.szText[0] != 0)
+                    {
+                        // owner copied string to ttt.szText:
+                        // copy that to our buffer in the window words
+                        // to make it static
+                        strcpy(pttd->szTextBuf, ttt.szText);    // maxlen is 80 chars
+                        pti->lpszText = pttd->szTextBuf;
+                    }
+                    else if (   (ttt.lpszText != 0)
+                             && (((ULONG)ttt.lpszText & 0xFFFF0000) != 0)
+                            )
+                        // owner specified pointer to static buffer somewhere:
+                        pti->lpszText = ttt.lpszText;
+                    else if ((ULONG)ttt.lpszText & 0xFFFF)
+                    {
+                        if (WinLoadString(pttd->hab,
+                                          ttt.hinst,
+                                          (ULONG)ttt.lpszText,
+                                          sizeof(pttd->szTextBuf),  // 256 chars
+                                          pttd->szTextBuf))
+                            pti->lpszText = pttd->szTextBuf;
+                    }
+                }
+            break; }
+
+            /*
+             *@@ TTM_UPDATETIPTEXT:
+             *      sets the tooltip text for a tool.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PTOOLINFO mp2: pointer to a TOOLINFO structure. The "hinst"
+             *          and "lpszText" members must specify the instance handle and
+             *          the pointer to the text.
+             *          The "hwnd" and "uId" members identify the tool to update.
+             */
+
+            case TTM_UPDATETIPTEXT:
+            break;
+
+            /*
+             *@@ TTM_HITTEST:
+             *      tests a point to determine whether it is within the
+             *      bounding rectangle of the specified tool and, if the
+             *      point is within, retrieves information about the tool.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PHITTESTINFO mp2: pointer to a TTHITTESTINFO structure.
+             *          When sending the message, the "hwnd" member must specify
+             *          the handle of a tool and the "pt" member must specify the
+             *          coordinates of a point. If the return value is TRUE, the
+             *          "ti" member receives information about the tool that
+             *          occupies the point.
+             *
+             *      Return value: returns TRUE if the tool occupies the specified
+             *      point or FALSE otherwise.
+             */
+
+            case TTM_HITTEST:
+            break;
+
+            /*
+             *@@ TTM_WINDOWFROMPOINT:
+             *      this message allows a subclass procedure to cause a tooltip
+             *      to display text for a window other than the one beneath the
+             *      mouse cursor.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- mp2: PPOINTL lppt (Win95: (POINT FAR *)lppt):
+             *          pointer to a POINTL structure that defines the point to be checked.
+             *
+             *      Return value: the handle to the window that contains the point, or
+             *      NULL if no window exists at the specified point.
+             *
+             *      This message is intended to be processed by an application that
+             *      subclasses a tooltip. It is not intended to be sent by an
+             *      application. A tooltip sends this message to itself before
+             *      displaying the text for a window. By changing the coordinates
+             *      of the point specified by lppt, the subclass procedure can cause
+             *      the tooltip to display text for a window other than the one
+             *      beneath the mouse cursor.
+             */
+
+            case TTM_WINDOWFROMPOINT:
+            break;
+
+            /*
+             *@@ TTM_ENUMTOOLS:
+             *      this message retrieves the information that a tooltip control
+             *      maintains about a certain tool.
+             *
+             *      Parameters:
+             *      -- USHORT mp1: zero-based index of the tool for which to
+             *         retrieve information.
+             *      -- PTOOLINFO mp2: pointer to a TOOLINFO structure that
+             *         receives information about the tool. Before sending
+             *         this message, the cbSize member must specify the size
+             *         of the structure.
+             *
+             *      Return value: TRUE if any tools are enumerated or FALSE otherwise.
+             */
+
+            case TTM_ENUMTOOLS:         // done
+            {
+                PTOOLINFO ptiTarget = (PTOOLINFO)mp2;
+                mrc = (MPARAM)FALSE;
+                if (ptiTarget)
+                {
+                    PTOOLINFO ptiFound = (PTOOLINFO)lstItemFromIndex(&pttd->llTools,
+                                                                     SHORT1FROMMP(mp1));
+                    if (ptiFound)
+                    {
+                        memcpy(ptiTarget, ptiFound, ptiTarget->cbSize);
+                        mrc = (MRESULT)TRUE;
+                    }
+                }
+            break; }
+
+            /*
+             *@@ TTM_GETCURRENTTOOL:
+             *      this message retrieves the information that the tooltip control
+             *      maintains for the _current_ tool; that is, the one for which
+             *      the tooltip control is currently displaying text.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PTOOLINFO mp2: pointer to a TOOLINFO structure that receives
+             *          information about the current tool.
+             *
+             *      Return value: TRUE if successful or FALSE otherwise.
+             */
+
+            case TTM_GETCURRENTTOOL:        // done
+            {
+                PTOOLINFO ptiTarget = (PTOOLINFO)mp2;
+                mrc = (MPARAM)FALSE;
+                if (ptiTarget)
+                {
+                    if (pttd->ptiMouseOver)
+                    {
+                        memcpy(ptiTarget, pttd->ptiMouseOver, ptiTarget->cbSize);
+                        mrc = (MPARAM)TRUE;
+                    }
+                }
+            break; }
+
+            /*
+             *@@ TTM_GETTOOLCOUNT:
+             *      returns the number of tools in the tooltip control.
+             *
+             *      No parameters.
+             */
+
+            case TTM_GETTOOLCOUNT:          // done
+                mrc = (MPARAM)lstCountItems(&pttd->llTools);
+            break;
+
+            /*
+             *@@ TTM_GETTOOLINFO:
+             *      this message retrieves the information that a tooltip
+             *      control maintains about a tool.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PTOOLINFO mp2:
+             *          pointer to a TOOLINFO structure. When sending the message,
+             *          the hwnd and uId members identify a tool, and the cbSize
+             *          member must specify the size of the structure. If the
+             *          tooltip control includes the tool, the structure receives
+             *          information about the tool.
+             *
+             *      Return value: TRUE if successful or FALSE otherwise.
+             */
+
+            case TTM_GETTOOLINFO:       // done
+            {
+                PTOOLINFO ptiSearch = (PTOOLINFO)mp2;
+                mrc = (MPARAM)FALSE;
+                if (ptiSearch)
+                {
+                    PLISTNODE pToolNode = lstQueryFirstNode(&pttd->llTools);
+                    while (pToolNode)
+                    {
+                        PTOOLINFO ptiThis = (PTOOLINFO)pToolNode->pItemData;
+                        if (    (ptiThis->hwnd == ptiSearch->hwnd)
+                             && (ptiThis->uId == ptiSearch->uId)
+                           )
+                        {
+                            // found:
+                            memcpy(ptiSearch, ptiThis, ptiSearch->cbSize);
+                            mrc = (MPARAM)TRUE;
+                            break;
+                        }
+                        pToolNode = pToolNode->pNext;
+                    }
+                }
+            break; }
+
+            /*
+             *@@ TTM_SETTOOLINFO:
+             *      this message sets the information that a tooltip control
+             *      maintains for a tool.
+             *
+             *      Parameters:
+             *      -- mp1: always 0.
+             *      -- PTOOLINFO mp2: pointer to a TOOLINFO structure that
+             *          specifies the information to set.
+             *
+             *      Return value: 0 always.
+             */
+
+            case TTM_SETTOOLINFO:
+            break;
+
+            /*
+             *@@ TTM_SHOWTOOLTIPNOW:
+             *      depending on BOOL mp1, shows or hides the tooltip.
+             *      This is required because we cannot show or hide
+             *      the window during processing of WM_MOUSEMOVE etc,
+             *      which will lead to strange PM hangs.
+             *
+             *      This is not part of the Win95 message set but used
+             *      in the OS/2 implementation only. This calls TtmShowTooltip
+             *      in turn.
+             */
+
+            case TTM_SHOWTOOLTIPNOW:
+                TtmShowTooltip(hwndTooltip, pttd, (BOOL)mp1);
+            break;
+
+            default:
+                mrc = WinDefWindowProc(hwndTooltip, msg, mp1, mp2);
+        }
+    }
+    CATCH(excpt1) {} END_CATCH();
+
+    return (mrc);
+}
+
+
Index: /trunk/src/helpers/cnrh.c
===================================================================
--- /trunk/src/helpers/cnrh.c	(revision 8)
+++ /trunk/src/helpers/cnrh.c	(revision 8)
@@ -0,0 +1,1960 @@
+
+/*
+ *@@sourcefile cnrh.c:
+ *      contains various PM container helper functions.
+ *
+ *      These functions used to be in winh.c, but they became
+ *      too many finally, so these were moved to this file with
+ *      V0.9.0.
+ *
+ *      Usage: All PM programs.
+ *
+ *      Function prefixes:
+ *      --  cnrh*   container helper functions (was: winh* before V0.9.0)
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@header "helpers\cnrh.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_WIN
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPIPRIMITIVES
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\winh.h"
+#include "helpers\cnrh.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Container helpers\Details view helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Details view field infos                                       *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ cnrhClearFieldInfos:
+ *      removes all existing FIELDINFO's from
+ *      the given container. Returns the number
+ *      of remaining FIELDINFO's, which should
+ *      be 0, or -1 upon errors.
+ *
+ *@@added V0.9.1 (2000-02-13) [umoeller]
+ */
+
+ULONG cnrhClearFieldInfos(HWND hwndCnr,
+                          BOOL fInvalidate) // in: if TRUE, invalidate container
+{
+    ULONG ulFlags = CMA_FREE;
+    if (fInvalidate)
+        ulFlags |= CMA_INVALIDATE;
+
+    return (ULONG)(WinSendMsg(hwndCnr,
+                              CM_REMOVEDETAILFIELDINFO,
+                              (MPARAM)NULL,
+                              MPFROM2SHORT(0,      // all
+                                           ulFlags)));
+}
+
+/*
+ *@@ cnrhSetFieldInfo:
+ *      this sets a FIELDINFO structure to the given
+ *      data. Note that ppFieldInfo is a double pointer
+ *      to the actual FIELDINFO data.
+ *
+ *      This gets called from cnrhSetFieldInfos to set
+ *      each individual field info.
+ *      You can also use this function separately with
+ *      the cnrhAllocFieldInfos macro defined in cnrh.h.
+ *
+ *      After setting the data, <B>*ppFieldInfo</B> is advanced
+ *      to the next FIELDINFO structure so that you can
+ *      call this function several times for all the
+ *      FIELDINFOS that you have allocated. After the last
+ *      column, this pointer will be NULL.
+ *
+ *      Since the pointer is modified, do not invoke this
+ *      function on the original pointer returned from
+ *      cnrhAllocFieldInfos, because you'll need that
+ *      pointer for cnrhAllocFieldInfos later.
+ *
+ *      <B>Example usage</B>:
+ *
+ +          PFIELDINFO pFieldInfoFirst, pFieldInfo2;
+ +          if (pFieldInfoFirst = cnrhAllocFieldInfos(hwndFilesCnr, NO_OF_COLUMNS))
+ +                          // macro defined in cnrh.h
+ +          {
+ +              pFieldInfo2 = pFieldInfoFirst;
+ +
+ +              // "File name" column
+ +              cnrhSetFieldInfo(&pFieldInfo2,
+ +                                  FIELDOFFSET(RECORDCORE, pszIcon),
+ +                                      // icon text offset in original RECORDCORE
+ +                                  "File name",
+ +                                  CFA_STRING,
+ +                                  CFA_LEFT,
+ +                                  FALSE);     // no draw lines
+ +              // "Size" column
+ +              cnrhSetFieldInfo(&pFieldInfo2,
+ +                                  FIELDOFFSET(FILERECORDCORE, ulSize),
+ +                                      // size data field in sample extended RECORDCORE
+ +                                  "Size",
+ +                                  CFA_ULONG,
+ +                                  CFA_RIGHT,
+ +                                  FALSE);     // no draw lines
+ +              ... // set other field infos
+ +          }
+ +
+ +          // insert field infos
+ +          cnrhInsertFieldInfos(hwndFilesCnr,
+ +                                  pFieldInfoFirst,
+ +                                  NO_OF_COLUMNS);
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (99-12-18) [umoeller]: fixed memory leak
+ */
+
+VOID cnrhSetFieldInfo(PFIELDINFO *ppFieldInfo2,  // in/out: double ptr to FIELDINFO
+                      ULONG ulFieldOffset,       // in: FIELDOFFSET(YOURRECORDCORE, yourField)
+                      PSZ pszColumnTitle,        // in: column title; this must be a static string!!
+                      ULONG ulDataType,          // in: column data type (CFA_* flags)
+                      ULONG ulOrientation,       // in: vertical and horizontal orientation (CFA_* flags)
+                      BOOL fDrawLines)           // in: if TRUE, we'll draw lines around the columns
+{
+    if (ppFieldInfo2)
+        if (*ppFieldInfo2)
+        {
+            ULONG flData = ulDataType | ulOrientation;
+            if (fDrawLines)
+                flData |= CFA_HORZSEPARATOR | CFA_SEPARATOR;
+
+            (*ppFieldInfo2)->cb = sizeof(FIELDINFO);
+            (*ppFieldInfo2)->flData = flData;
+            (*ppFieldInfo2)->flTitle = CFA_FITITLEREADONLY | ulOrientation;
+            (*ppFieldInfo2)->offStruct = ulFieldOffset;
+            (*ppFieldInfo2)->pTitleData = pszColumnTitle;   // strdup removed, V0.9.1 (99-12-18) [umoeller]
+            (*ppFieldInfo2)->pUserData   = NULL;
+            *ppFieldInfo2 = (*ppFieldInfo2)->pNextFieldInfo;
+        }
+}
+
+/*
+ *@@ cnrhInsertFieldInfos:
+ *      this inserts field infos for Details view
+ *      into the specified container.
+ *
+ *      pFieldInfoFirst should be the PFIELDINFO
+ *      returned by cnrhAllocFieldInfos.
+ *
+ *      This inserts the FIELDINFOs at the end,
+ *      should any columns already exist in the container.
+ *      Also, the container is invalidated.
+ *
+ *      Returns the return value of CM_INSERTDETAILFIELDINFO,
+ *      which is the total no. of field infos in the container
+ *      or null upon errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG cnrhInsertFieldInfos(HWND hwndCnr,                // in: cnr for Details view
+                           PFIELDINFO pFieldInfoFirst,  // in: first field info as returned
+                                                        // by cnrhAllocFieldInfos
+                           ULONG ulFieldCount)          // in: no. of field infos
+{
+    FIELDINFOINSERT fii;
+    fii.cb = sizeof(FIELDINFOINSERT);
+    fii.pFieldInfoOrder = (PFIELDINFO)CMA_END;
+    fii.fInvalidateFieldInfo = TRUE;
+    fii.cFieldInfoInsert = ulFieldCount;
+
+    return ((ULONG)WinSendMsg(hwndCnr,
+                              CM_INSERTDETAILFIELDINFO,
+                              (MPARAM)pFieldInfoFirst,
+                              (MPARAM)&fii));
+}
+
+/*
+ *@@ cnrhSetFieldInfos:
+ *      this combines cnrhAllocFieldInfos,
+ *      cnrhSetFieldInfo, and cnrhInsertFieldInfos
+ *      into a one-shot func for setting details view
+ *      column field infos. This one has proven to
+ *      be EXTREMELY useful.
+ *
+ *      To pass all the arguments normally passed to
+ *      cnrhSetFieldInfo, we use an array of XFIELDINFO
+ *      structures, which takes the same parameters.
+ *
+ *      <B>XFIELDINFO.ulDataType</B> specifies the data type of the record
+ *      core field. This can be one of the following:
+ *      --  CFA_BITMAPORICON: bit-map or icon data
+ *      --  CFA_DATE: date format (CDATE structure)
+ *      --  CFA_STRING: null-terminated string
+ *      --  CFA_TIME: time format (CTIME structure)
+ *      --  CFA_ULONG: unsigned number data
+ *
+ *      You can add the following optional flags to
+ *      ulDataType:
+ *      --  CFA_FIREADONLY (CFA_STRING only): disable editing
+ *      --  CFA_INVISIBLE: make column invisible
+ *      --  CFA_OWNER: enable owner draw for this column
+ *
+ *      If (fDrawLines == TRUE), we'll automatically add
+ *      CFA_HORZSEPARATOR | CFA_SEPARATOR.
+ *
+ *      <B>XFIELDINFO.ulOrientation</B> specifies the vertical and
+ *      horizontal orientation of both the column title
+ *      and the column data. This should be OR'ed from
+ *      the following
+ *      --  CFA_CENTER, CFA_LEFT, CFA_RIGHT (horizontal);
+ *          the default is CFA_LEFT
+ *      --  CFA_BOTTOM, CFA_TOP, CFA_VCENTER (vertical);
+ *          the default is CFA_VCENTER.
+ *
+ *      Note that the container automatically displays
+ *      data according to the settings in the "Country" object.
+ *
+ *      The column title will always be set to string format
+ *      and CFA_FITITLEREADONLY.
+ *
+ *      <B>XFIELDINFO.ulFieldOffset</B> should be set to the return value
+ *      of the FIELDOFFSET macro, which is also redefined in
+ *      cnrh.h (to work with C++).
+ *
+ *      The return value is the PFIELDINFO which corresponds
+ *      to the column index specified in ulFieldReturn, or
+ *      NULL upon errors. This is useful for setting the
+ *      position of the split bar afterwards (using the
+ *      cnrhSetSplitBarAfter macro).
+ *
+ *      Example usage:
+ +          XFIELDINFO      xfi[3];
+ +          PFIELDINFO      pfi;
+ +
+ +          xfi[0].ulFieldOffset = FIELDOFFSET(DATABASERECORD, pszApplication);
+ +          xfi[0].pszColumnTitle = "Application";
+ +          xfi[0].ulDataType = CFA_STRING;
+ +          xfi[0].ulOrientation = CFA_LEFT;
+ +
+ +          xfi[1].ulFieldOffset = FIELDOFFSET(RECORDCORE, pszIcon);
+ +          xfi[1].pszColumnTitle = "Package name";
+ +          xfi[1].ulDataType = CFA_STRING;
+ +          xfi[1].ulOrientation = CFA_LEFT;
+ +
+ +          xfi[2].ulFieldOffset = FIELDOFFSET(DATABASERECORD, pszAuthor);
+ +          xfi[2].pszColumnTitle = "Author";
+ +          xfi[2].ulDataType = CFA_STRING;
+ +          xfi[2].ulOrientation = CFA_CENTER;
+ +
+ +          pfi = cnrhSetFieldInfos(hwndCnr,
+ +                                  &xfi[0],
+ +                                  (sizeof(xfi) / sizeof(XFIELDINFO)),
+ +                                      // smart way of calculating the array item count
+ +                                  FALSE,         // no draw lines
+ +                                  0);            // return first column
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PFIELDINFO cnrhSetFieldInfos(HWND hwndCnr,            // in: container hwnd
+                             PXFIELDINFO paxfi,       // in: pointer to an array of ulFieldCount XFIELDINFO structures
+                             ULONG ulFieldCount,      // in: no. of items in paxfi array (> 0)
+                             BOOL fDrawLines,         // in: if TRUE, we'll draw lines around the columns
+                             ULONG ulFieldReturn)     // in: the column index to return as PFIELDINFO
+{
+    PFIELDINFO  pFieldInfoFirst,
+                pFieldInfo2,
+                pFieldInfoReturn = NULL;
+
+    if ((pFieldInfoFirst = cnrhAllocFieldInfos(hwndCnr, ulFieldCount)))
+    {
+        ULONG ul = 0;
+        PXFIELDINFO pxfi = NULL;
+
+        pFieldInfo2 = pFieldInfoFirst;
+        pxfi = paxfi;
+        for (ul = 0; ul < ulFieldCount; ul++)
+        {
+            if (ul == ulFieldReturn)
+                // set return value
+                pFieldInfoReturn = pFieldInfo2;
+
+            // set current field info;
+            // this will modify pFieldInfo to point to the next
+            cnrhSetFieldInfo(&pFieldInfo2,
+                             pxfi->ulFieldOffset,
+                             pxfi->pszColumnTitle,
+                             pxfi->ulDataType,
+                             pxfi->ulOrientation,
+                             fDrawLines);
+            pxfi++;
+        }
+
+        // insert field infos
+        if (cnrhInsertFieldInfos(hwndCnr,
+                                 pFieldInfoFirst,
+                                 ulFieldCount) == 0)
+            pFieldInfoReturn = NULL;
+    }
+
+    return (pFieldInfoReturn);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Container helpers\Record core helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Record core management                                         *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ cnrhAllocRecords:
+ *      this is a shortcut to allocating memory for
+ *      container record cores.
+ *
+ *      If (ulCount == 1), this returns the new record core.
+ *      If (ulCount > 1), this returns the _first_ record
+ *      core; the following record cores may be reached
+ *      by following the RECORDCORE.preccNextRecord pointers
+ *      (i.e. we have a linked list here).
+ *
+ *      The record cores returned by the container are
+ *      automatically zeroed out, and their "cb" field
+ *      is automatically set to the size of the record core.
+ *
+ *      Note that this function presently does _not_ work
+ *      with MINIRECORDCOREs.
+ *
+ *@@changed V0.9.0 [umoeller]: function prototype changed to allocate more than one record
+ */
+
+PRECORDCORE cnrhAllocRecords(HWND hwndCnr,    // in: cnr to allocate from
+                             ULONG cbrecc,
+                                 // in: total size of your record core.
+                                 // If you're using the default recc's, this
+                                 // must be sizeof(RECORDCORE).
+                             ULONG ulCount)  // in: number of records to allocate (> 0)
+{
+    PRECORDCORE      precc;
+    precc = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                    CM_ALLOCRECORD,
+                                    (MPARAM)(cbrecc-sizeof(RECORDCORE)),
+                                    (MPARAM)ulCount);
+
+    return (precc);
+}
+
+/*
+ *@@ cnrhInsertRecords:
+ *      shortcut to inserting record cores into a container
+ *      which have been allocated using cnrhAllocRecords.
+ *
+ *      If (<B>ulCount</B> == 1), this inserts precc.
+ *      If (ulCount > 1), this assumes precc to be the first
+ *      record to be inserted, while precc->preccNextRecord
+ *      must point to the next record in a linked list (as
+ *      returned with cnrhAllocRecords). (changed V0.9.0)
+ *
+ *      Note that ulCount here must be exactly the same as
+ *      specified with cnrhAllocRecords.
+ *
+ *      If (<B>pszText</B> != NULL), this will automatically set precc->pszIcon,
+ *      precc->pszText, precc->pszName, precc->pszTree to pszText to have
+ *      the same record titles in all views. If (pszText == NULL), those
+ *      fields will be left alone. (changed V0.9.0)
+ *
+ *      <B>flRecordAttr</B> should have the record attributes as
+ *      specified in the RECORDCORE structure.
+ *
+ *      Emphasis flags:
+ *      --  CRA_SELECTED:      record is selected
+ *                             (other records will be deselected in Tree views
+ *                             and when single selection is enabled)
+ *      --  CRA_CURSORED:      cursor (keyboard focus) is on the record
+ *                             (other records will be de-cursored)
+ *      --  CRA_SOURCE:        record has source emphasis (drag'n'drop,
+ *                             open context menus)
+ *      --  CRA_TARGET:        record has target emphasis (drag'n'drop)
+ *      --  CRA_PICKED:        record picked (Lazy Drag)
+ *      --  CRA_INUSE:         record has in-use emphasis (diagonal lines,
+ *                             as with open objects with the WPS)
+ *
+ *      Miscellaneous flags:
+ *      --  CRA_FILTERED:      record has been filtered (is invisible)
+ *      --  CRA_DROPONABLE:    record can be dropped something upon
+ *      --  CRA_RECORDREADONLY: record is read-only (no text edit with Alt+MB1)
+ *      --  CRA_EXPANDED:      record is expanded (Tree view)
+ *      --  CRA_COLLAPSED:     record is collapsed (Tree view)
+ *
+ *      plus the following half-documented from PMSTDDLG.H (haven't tested these):
+ *      --  CRA_IGNORE         record is to be ignored
+ *      --  CRA_DISABLED       has no visible effect, but can be used with
+ *                             cnrhOwnerDrawRecord
+ *      --  CRA_LOCKED         maybe "locked in place"?!?
+ *      --  CRA_OWNERFREE      owner must free record
+ *      --  CRA_OWNERDRAW      owner must draw record; I'd recommend using
+ *                             CA_OWNERDRAW in CNRINFO.flWindowAttr instead.
+ *
+ *      This func returns the total number of records in the container
+ *      or NULL upon errors (return value of CM_INSERTRECORD, changed V0.9.0).
+ *
+ *@@changed V0.9.0 [umoeller]: function prototype changed to insert more than one record
+ *@@changed V0.9.0 [umoeller]: added error checking for pszText
+ *@@changed V0.9.2 (2000-02-19) [umoeller]: added fInvalidate field
+ */
+
+ULONG cnrhInsertRecords(HWND hwndCnr,   // in: container to insert into
+                        PRECORDCORE preccParent,
+                            // in: record core below which precc should
+                            // be inserted (tree view only). If NULL, precc
+                            // is inserted at "root" level
+                        PRECORDCORE precc,
+                            // in: record core to insert (allocated using
+                            // cnrhAllocRecords)
+                        BOOL fInvalidate,
+                        PSZ pszText,
+                            // in: text for recc. in all views (or NULL)
+                        ULONG flRecordAttr,
+                            // in: CRA_* flags
+                        ULONG ulCount)  // in: number of records to insert (> 0)
+{
+    ULONG           ulrc = 0;
+    RECORDINSERT    ri;
+
+    if (precc)
+    {
+        // RECORDCORE stuff
+        precc->flRecordAttr = flRecordAttr;
+        // precc->preccNextRecord = NULL;
+
+        if (pszText) // V0.9.0
+        {
+            precc->pszIcon = pszText;
+            precc->pszText = pszText;
+            precc->pszName = pszText;
+            precc->pszTree = pszText;
+        }
+
+        // setup RECORDINSERT struct
+        ri.cb = sizeof(RECORDINSERT);
+        ri.pRecordOrder = (PRECORDCORE)CMA_END;
+        ri.pRecordParent = (PRECORDCORE)preccParent;
+        ri.zOrder = CMA_TOP;
+        ri.fInvalidateRecord = fInvalidate;  // V0.9.2 (2000-02-19) [umoeller]
+        ri.cRecordsInsert = ulCount;        // V0.9.0
+
+        ulrc = (ULONG)WinSendMsg(hwndCnr,
+                                 CM_INSERTRECORD,
+                                 (MPARAM)precc,
+                                 (MPARAM)&ri);
+    }
+    return (ulrc);
+}
+
+/*
+ *@@ cnrhInsertRecordAfter:
+ *      similar to cnrhInsertRecords, but this inserts
+ *      a single record _after_ another one. Parent records
+ *      and multiple records are not supported.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.4 (2000-06-14) [umoeller]: added fInvalidate
+ */
+
+ULONG cnrhInsertRecordAfter(HWND hwndCnr,
+                            PRECORDCORE precc,
+                            PSZ pszText,
+                            ULONG flRecordAttr,
+                            PRECORDCORE preccAfter,
+                            BOOL fInvalidate)           // in: invalidate records?
+{
+    ULONG           ulrc = 0;
+    RECORDINSERT    ri;
+
+    if (precc)
+    {
+        // RECORDCORE stuff
+        precc->flRecordAttr = flRecordAttr;
+        precc->preccNextRecord = NULL;
+
+        if (pszText) // V0.9.0
+        {
+            precc->pszIcon = pszText;
+            precc->pszText = pszText;
+            precc->pszName = pszText;
+            precc->pszTree = pszText;
+
+            // setup RECORDINSERT struct
+            ri.cb = sizeof(RECORDINSERT);
+            ri.pRecordOrder = (PRECORDCORE)preccAfter;
+            ri.pRecordParent = 0;   // no parent here
+            ri.zOrder = CMA_TOP;
+            ri.fInvalidateRecord = fInvalidate; // V0.9.4 (2000-06-14) [umoeller]
+            ri.cRecordsInsert = 1;
+
+            ulrc = (ULONG)WinSendMsg(hwndCnr,
+                                     CM_INSERTRECORD,
+                                     (MPARAM)precc,
+                                     (MPARAM)&ri);
+        }
+    }
+    return (ulrc);
+}
+
+/*
+ *@@ cnrhMoveRecord:
+ *      this moves a single record within the same container
+ *      window. This only makes sense in ordered views (i.e.
+ *      Name, Text, and Details views).
+ *
+ *      For moving records with child records, use cnrhMoveTree
+ *      instead.
+ */
+
+BOOL cnrhMoveRecord(HWND hwndCnr,
+                    PRECORDCORE preccMove,        // in: record to move
+                    PRECORDCORE preccInsertAfter)
+                         // in: where to move the record to
+                         // (can be CMA_FIRST or CMA_END)
+{
+    // rule out possible errors:
+    if (    (preccMove)     // this better be valid
+         && (preccInsertAfter)  // this also, because otherwise the record just disappears
+         && (preccMove != preccInsertAfter)
+                            // same here
+       )
+    {
+        RECORDINSERT ri;
+        ULONG        ulrc = 0;
+
+        // remove record
+        WinSendMsg(hwndCnr,
+                   CM_REMOVERECORD,
+                   (MPARAM)(&preccMove),
+                   MPFROM2SHORT(1,
+                                CMA_INVALIDATE)); // no free
+
+        // and re-insert that record at the new position
+        ri.cb = sizeof(RECORDINSERT);
+        ri.pRecordOrder = (PRECORDCORE)preccInsertAfter;
+        ri.pRecordParent = 0;   // no parent here
+        ri.zOrder = CMA_TOP;
+        ri.fInvalidateRecord = TRUE;
+        ri.cRecordsInsert = 1;
+        ulrc = (ULONG)WinSendMsg(hwndCnr,
+                                 CM_INSERTRECORD,
+                                 (MPARAM)preccMove,
+                                 (MPARAM)&ri);
+
+        return (ulrc != 0);
+    }
+    else return (FALSE);
+}
+
+/*
+ *@@ cnrhMoveTree:
+ *      this function moves a container record core and all its
+ *      children from one tree to another.
+ *      See the CM_MOVETREE message for more explanations.
+ *
+ *      The problem with that message is that automatic container
+ *      sorting does not work in tree view. This function however
+ *      is smart enough to maintain container sorting and insert
+ *      the moved record core at the correct position below the
+ *      target record core, if you specify a container sort function
+ *      (pfnCnrSort, which works just in CNRINFO).
+ *
+ *      Otherwise, we'll just insert the item as the first child
+ *      of preccNewParent.
+ *
+ *      This function also has a workaround for a nasty container
+ *      bug which results in a crash in PMMERGE.DLL when a tree
+ *      is moved to the last position in the target parent record
+ *      (at least with Warp 4 FP 7).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL cnrhMoveTree(HWND hwndCnr,          // in: container control
+                  PRECORDCORE preccMove, // in: record core to move
+                  PRECORDCORE preccNewParent, // in: new parent for preccMove
+                                              // or NULL if move to root
+                  PFNCNRSORT pfnCnrSort) // in: sort function to use or NULL
+{
+    TREEMOVE    tm;
+    PRECORDCORE preccInsertAfter = (PRECORDCORE)CMA_FIRST;
+    BOOL        fBugWorkAround = FALSE,
+                brc = FALSE;
+
+    if (pfnCnrSort)
+    {
+        // permanent sort activated:
+
+        // since the automatic container sort does
+        // not work when moving a record core tree in
+        // Tree view, we must find the recc after
+        // which we'll insert the tree ourselves
+        PRECORDCORE preccSearch = preccNewParent;
+        BOOL        fFirstRun = TRUE;
+        ULONG       ulFirstCode;
+
+        // set the code for first-loop query:
+        if (preccNewParent)
+            // if new parent is non-root
+            ulFirstCode = CMA_FIRSTCHILD;
+        else
+            // if new parent is root
+            ulFirstCode = CMA_FIRST;
+
+        while (TRUE)
+        {
+            preccSearch =
+                (PRECORDCORE)WinSendMsg(hwndCnr,
+                                        CM_QUERYRECORD,
+                                        // the following gets either the
+                                        // first child recc of the target
+                                        // record core or the next child
+                                        // for consecutive loops
+                                        (MPARAM)preccSearch,
+                                        MPFROM2SHORT(
+                                                ((fFirstRun)
+                                                ? ulFirstCode  // first loop
+                                                : CMA_NEXT  // works for next child too
+                                            ),
+                                            CMA_ITEMORDER)
+                                        );
+            fFirstRun = FALSE;
+
+            if (    (preccSearch == NULL)
+                 || ((ULONG)preccSearch == -1)
+               )
+            {
+                // no more items found:
+                // keep preccInsertAfter, which might be CMA_FIRST
+                // or the preccSearch we have found previously.
+
+                if (preccInsertAfter != (PRECORDCORE)CMA_FIRST)
+                {
+                    // Unfortunately, there is a bug in the container
+                    // control which prohibits CM_MOVETREE from working
+                    // if preccInsertAfter turns out to be the last
+                    // record core in preccNewParent. This applies to
+                    // preccInsertAfter == CMA_LAST also, and CMA_LASTCHILD
+                    // doesn't work either.
+                    // Duh.
+                    // We'll fix this later.
+
+                    fBugWorkAround = TRUE;
+                }
+                break;
+            }
+
+            if (((*pfnCnrSort)(preccSearch, preccMove, 0)) < 0)
+            {
+                // found record core is < our tree:
+                // mark current as "insert after" for later and go on
+                preccInsertAfter = preccSearch;
+            }
+            else
+                break;
+        }
+
+        /* _Pmpf(("preccInsertAfter %s",
+                (preccInsertAfter == (PRECORDCORE)CMA_FIRST) ? "CMA_FIRST"
+                : (preccInsertAfter == (PRECORDCORE)CMA_LAST) ? "CMA_LAST"
+                : (preccInsertAfter == NULL) ? "NULL"
+                : preccInsertAfter->pszIcon
+             )); */
+    } // end if (CnrInfo.pSortRecord)
+
+    if (fBugWorkAround)
+        // this is TRUE only if preccInsertAfter has turned
+        // out to be the last child of preccNewParent. This
+        // will make the container crash, so we insert as
+        // first and sort the whole damn container later.
+        // Not terribly fast, but better than crashing. ;-)
+        preccInsertAfter = (PRECORDCORE)CMA_FIRST;
+
+    // set record to be moved
+    tm.preccMove = preccMove;
+    // set target record core
+    tm.preccNewParent = preccNewParent;
+    tm.pRecordOrder = preccInsertAfter;
+    tm.flMoveSiblings = FALSE;
+                // move only preccMove
+    brc = (BOOL)WinSendMsg(hwndCnr,
+                           CM_MOVETREE,
+                           (MPARAM)&tm,
+                           (MPARAM)NULL);
+
+    if (brc)
+        if (fBugWorkAround)
+            WinSendMsg(hwndCnr, CM_SORTRECORD, (MPARAM)pfnCnrSort, (MPARAM)NULL);
+
+    return (brc);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Container helpers\View management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   View management                                                *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ cnrhSelectAll:
+ *      this selects or deselects all records in hwndCnr,
+ *      depending on fSelect. Since multiple selections are
+ *      not supported in Tree views in the first place, we
+ *      only go for the "root" records (and no child records).
+ *
+ *      Invoking this function on a container in Tree view
+ *      will result in not much but display flickering anyways.
+ *
+ *      This returns the number of records which were processed.
+ */
+
+ULONG cnrhSelectAll(HWND hwndCnr,
+                    BOOL fSelect)
+{
+    ULONG ulrc = 0;
+    PRECORDCORE precc2 = NULL;
+
+    do {
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                                    CM_QUERYRECORD,
+                                    (MPARAM)precc2,
+                                    MPFROM2SHORT(
+                                        ((precc2 == NULL) ? CMA_FIRST : CMA_NEXT),
+                                        CMA_ITEMORDER)
+                                    );
+        if ((precc2 == 0) || ((LONG)precc2 == -1))
+            // last record or error:
+            break;
+
+        // select this one
+        cnrhSelectRecord(hwndCnr, precc2, fSelect);
+        ulrc++;
+
+    } while (TRUE);
+
+    return (ulrc);
+}
+
+/*
+ *@@ cnrhFindRecordFromPoint:
+ *      this returns the record under the given
+ *      point or NULL if none could be found
+ *      (e.g. point is on whitespace).
+ *
+ *      The point must be specified in container
+ *      coordinates, relative to the container
+ *      origin (which might be invisible if the
+ *      container viewport has been scrolled up).
+ *
+ *      With fsExtent, specify what part of the
+ *      record should be checked. See
+ *      cnrhScrollToRecord for more.
+ *
+ *      This is not terribly fast, because it
+ *      will send at least one message for each
+ *      record which is currently visible in the
+ *      container.
+ *
+ *      With fl, specify any or none of the following:
+ *      --  FRFP_RIGHTSPLITWINDOW: test right split Details view
+ *              instead of main cnr.
+ *      --  FRFP_SCREENCOORDS: *pptl specifies the point to
+ *              check in _screen_ coordinates, instead of
+ *              container ccordinates.
+ *
+ *@@added V0.9.1 (99-11-29) [umoeller]
+ *@@changed V0.9.4 (2000-08-08) [umoeller]: fixed viewport/window confusion; now works after cnr resize
+ */
+
+PRECORDCORE cnrhFindRecordFromPoint(HWND hwndCnr,
+                                    PPOINTL pptl,   // in: point to check for
+                                    PRECTL prclFoundRecord,
+                                            // out: if != NULL and record was
+                                            // found, this receives the record rectl
+                                            // (or subrectl, depending on fsExtent)
+                                    ULONG fsExtent,
+                                            // in: one or several of
+                                            // CMA_ICON, CMA_TEXT, CMA_TREEICON
+                                    ULONG fl)
+{
+    PRECORDCORE preccReturn = NULL;
+    RECTL       rclViewport;
+    HAB         habCnr = WinQueryAnchorBlock(hwndCnr);
+
+    POINTL      ptlCheck;
+
+    // _Pmpf(("Entering cnrhFindRecordFromPoint"));
+
+    ptlCheck.x = pptl->x;
+    ptlCheck.y = pptl->y;
+
+    if (fl & FRFP_SCREENCOORDS)
+    {
+        // _Pmpf(("  mapping screen %d/%d to cnr", ptlCheck.x, ptlCheck.y));
+        WinMapWindowPoints(HWND_DESKTOP,
+                           hwndCnr,
+                           &ptlCheck,
+                           1);
+        // _Pmpf(("  got %d/%d", ptlCheck.x, ptlCheck.y));
+    }
+
+    // get cnr viewport (currently visible client area,
+    // relative to cnr origin)
+    if (WinSendMsg(hwndCnr,
+                   CM_QUERYVIEWPORTRECT,
+                   (MPARAM)&rclViewport,
+                   MPFROM2SHORT(CMA_WINDOW, // CMA_WORKSPACE, V0.9.4 (2000-08-08) [umoeller]
+                                ((fl & FRFP_RIGHTSPLITWINDOW) != 0))))
+                                        // right split window?
+    {
+        PRECORDCORE         preccFound = (PRECORDCORE)CMA_FIRST;
+        QUERYRECFROMRECT    qrfr;
+        QUERYRECORDRECT     qrr;
+
+        // now enumerate all records which are visible in
+        // the viewport
+        do
+        {
+            qrfr.cb = sizeof(qrfr);
+            qrfr.rect.xLeft = rclViewport.xLeft;
+            qrfr.rect.yBottom = rclViewport.yBottom;
+            qrfr.rect.xRight = rclViewport.xRight;
+            qrfr.rect.yTop = rclViewport.yTop;
+            qrfr.fsSearch = CMA_PARTIAL | CMA_ITEMORDER;
+
+            // _Pmpf(("  Enumerating recs in viewport %d, %d, %d, %d",
+            //             rclViewport.xLeft, rclViewport.yBottom, rclViewport.xRight, rclViewport.yTop));
+
+            preccFound = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                                 CM_QUERYRECORDFROMRECT,
+                                                 (MPARAM)preccFound, // CMA_FIRST on first loop
+                                                 (MPARAM)&qrfr);
+            // _Pmpf(("   CM_QUERYRECORDFROMRECT returned 0x%lX", preccFound));
+
+            if (    (preccFound != NULL)
+                 && (preccFound != (PRECORDCORE)-1)
+               )
+            {
+                RECTL rclRecord;
+                qrr.cb = sizeof(qrr);
+                qrr.pRecord = preccFound;
+                qrr.fRightSplitWindow = ((fl & FRFP_RIGHTSPLITWINDOW) != 0);
+                qrr.fsExtent = fsExtent;
+                if (WinSendMsg(hwndCnr,
+                               CM_QUERYRECORDRECT,
+                               (MPARAM)&rclRecord,
+                               (MPARAM)&qrr))
+                {
+                    if (WinPtInRect(habCnr,
+                                    &rclRecord,
+                                    &ptlCheck))
+                    {
+                        // yes, it's in the record:
+                        // return that
+                        preccReturn = preccFound;
+
+                        // if rectangle is queried,
+                        // copy that
+                        if (prclFoundRecord)
+                            memcpy(prclFoundRecord, &rclRecord, sizeof(RECTL));
+                        break;
+                    }
+                }
+            }
+            else
+                // last one or error:
+                break;
+        } while (TRUE);
+    }
+    // else
+    //     _Pmpf(("  CM_QUERYVIEWPORTRECT failed."));
+
+    return (preccReturn);
+}
+
+/*
+ *@@ cnrhScrollToRecord:
+ *      scrolls a given container control to make a given
+ *      record visible.
+ *
+ *      Returns:
+ *      --  0:       OK, scrolled
+ *      --  1:       record rectangle query failed (error)
+ *      --  2:       cnr viewport query failed (error)
+ *      --  3:       record is already visible (scrolling not necessary)
+ *      --  4:       cnrinfo query failed (error)
+ *      --  5:       parent record rectangle query failed (error)
+ *
+ *      Note: All messages are _sent_ to the container, not posted.
+ *      Scrolling therefore occurs synchroneously before this
+ *      function returns.
+ *
+ *      This function an improved version of the one (W)(C) Dan Libby, found at
+ *      http://zebra.asta.fh-weingarten.de/os2/Snippets/Howt6364.HTML
+ *      Improvements (C) 1998 Ulrich Mller.
+ *
+ *@@changed V0.9.4 (2000-08-07) [umoeller]: now posting scroll messages to avoid sync errors
+ */
+
+ULONG cnrhScrollToRecord(HWND hwndCnr,       // in: container window
+                         PRECORDCORE pRec,   // in: record to scroll to
+                         ULONG fsExtent,
+                                 // in: this determines what parts of pRec
+                                 // should be made visible. OR the following
+                                 // flags:
+                                 // -- CMA_ICON: the icon rectangle
+                                 // -- CMA_TEXT: the record text
+                                 // -- CMA_TREEICON: the "+" sign in tree view
+                         BOOL KeepParent)
+                                 // for tree views only: whether to keep
+                                 // the parent record of pRec visible when scrolling.
+                                 // If scrolling to pRec would make the parent
+                                 // record invisible, we instead scroll so that
+                                 // the parent record appears at the top of the
+                                 // container workspace (Win95 style).
+
+{
+    QUERYRECORDRECT qRect, qRect2;
+    RECTL           rclRecord, rclParentRecord, rclCnr, rclCnr2;
+    POINTL          ptlRecord, ptlParentRecord;
+    CNRINFO         CnrInfo;
+    HAB             hab = WinQueryAnchorBlock(hwndCnr);
+    BOOL            KeepParent2;
+    LONG            lYOfs;
+
+    qRect.cb = sizeof(qRect);
+    qRect.pRecord = (PRECORDCORE)pRec;
+    qRect.fsExtent = fsExtent;
+
+    // query record location and size of container
+    if (!WinSendMsg(hwndCnr, CM_QUERYRECORDRECT, &rclRecord, &qRect))
+        return 1;
+    if (!WinSendMsg(hwndCnr, CM_QUERYVIEWPORTRECT, &rclCnr, MPFROM2SHORT(CMA_WINDOW, FALSE)) )
+        return 2;
+
+    // check if left bottom point of pRec is currently visible in container
+    ptlRecord.x = (rclRecord.xLeft);
+    ptlRecord.y = (rclRecord.yBottom);
+    // ptlRecord.x = (rclRecord.xLeft + rclRecord.xRight) / 2;
+    // ptlRecord.y = (rclRecord.yBottom + rclRecord.yTop) / 2;
+    if (WinPtInRect(hab, &rclCnr, &ptlRecord))
+         return 3;
+
+    if (KeepParent)
+    {
+        if (!WinSendMsg(hwndCnr, CM_QUERYCNRINFO, (MPARAM)&CnrInfo, (MPARAM)sizeof(CnrInfo)))
+            return 4;
+        else
+            KeepParent2 = (CnrInfo.flWindowAttr & CV_TREE);
+    }
+    else
+        KeepParent2 = FALSE;
+
+    // calculate offset to scroll to make pRec visible
+    lYOfs = (rclCnr.yBottom - rclRecord.yBottom)    // this would suffice
+          + (rclRecord.yTop - rclRecord.yBottom);  // but we make the next rcl visible too
+
+    if (KeepParent2)
+    {
+        qRect2.cb = sizeof(qRect2);
+        qRect2.pRecord = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                                 CM_QUERYRECORD,
+                                                 (MPARAM)pRec,
+                                                 MPFROM2SHORT(CMA_PARENT,
+                                                              CMA_ITEMORDER));
+        qRect2.fsExtent = fsExtent;
+
+        // now query PARENT record location and size of container
+        if (!WinSendMsg(hwndCnr, CM_QUERYRECORDRECT, &rclParentRecord, &qRect2))
+            return 5;
+
+        ptlParentRecord.x = (rclParentRecord.xLeft);
+        ptlParentRecord.y = (rclParentRecord.yTop);
+        // ptlParentRecord.x = (rclParentRecord.xLeft + rclParentRecord.xRight) / 2;
+        // ptlParentRecord.y = (rclParentRecord.yBottom + rclParentRecord.yTop) / 2;
+        rclCnr2 = rclCnr;
+        WinOffsetRect(hab, &rclCnr2, 0, -lYOfs);
+        // if ( (rclParentRecord.yBottom - rclRecord.yBottom) > (rclCnr.yTop - rclCnr.yBottom) )
+        if (!(WinPtInRect(hab, &rclCnr2, &ptlParentRecord)))
+        {
+            lYOfs = (rclCnr.yTop - rclParentRecord.yTop) // this would suffice
+                  - (rclRecord.yTop - rclRecord.yBottom);  // but we make the previous rcl visible too
+        }
+    }
+
+    if (!KeepParent2)
+        // scroll horizontally
+        WinPostMsg(hwndCnr,
+                   CM_SCROLLWINDOW,
+                   (MPARAM)CMA_HORIZONTAL,
+                   (MPARAM)(rclRecord.xLeft - rclCnr.xLeft));
+
+    // scroll vertically
+    WinPostMsg(hwndCnr,
+               CM_SCROLLWINDOW,
+               (MPARAM)CMA_VERTICAL,
+               (MPARAM)lYOfs);
+
+    return 0;
+}
+
+/*
+ *@@ cnrhShowContextMenu:
+ *      this function shows the given menu as a context
+ *      menu for the given record core (using WinPopupMenu).
+ *      This function may be used when receiving WM_CONTROL
+ *      with CN_CONTEXTMENU from the container, which has
+ *      the preccSource in mp2.
+ *
+ *      In detail, this function does the following:
+ *
+ *      1)  query the coordinates where to show the menu;
+ *          if (preccSource), this will be next to the
+ *          record core, otherwise (i.e. menu requested
+ *          for whitespace) the mouse coordinates over
+ *          the container;
+ *
+ *      2)  give preccSource (or, if NULL, the whole
+ *          container) source emphasis;
+ *
+ *      3)  call WinPopupMenu.
+ *
+ *      Note: It is the responsibility of the caller to catch
+ *      WM_MENUEND in the window procedure of hwndMenuOwner later
+ *      to remove the source emphasis for preccSource again.
+ *
+ *      This function returns FALSE if an error occured.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL cnrhShowContextMenu(HWND hwndCnr,
+                         PRECORDCORE preccSource, // in: mp2 of CN_CONTEXTMENU
+                         HWND hMenu,              // in: menu to show
+                         HWND hwndMenuOwner)      // in: menu owner (where the
+                                                  // WM_COMMAND will go to)
+{
+    BOOL    brc = FALSE;
+    if (hMenu)
+    {
+        BOOL        fQueried = FALSE;
+
+        POINTL ptl;
+        if (preccSource)
+        {
+            CNRINFO     CnrInfo;
+            cnrhQueryCnrInfo(hwndCnr, &CnrInfo);
+
+            if ((CnrInfo.flWindowAttr & CV_DETAIL) == 0)
+            {
+                // if we're not in Details view:
+                // calculate the point where to show the context
+                // menu; we use the lower-right corner of the
+                // source record core
+                QUERYRECORDRECT qRect;
+                RECTL           rclRecc;
+                qRect.cb = sizeof(qRect);
+                qRect.pRecord = preccSource;
+                qRect.fsExtent = CMA_TEXT;
+                WinSendMsg(hwndCnr,
+                           CM_QUERYRECORDRECT,
+                           &rclRecc,
+                           &qRect);
+                ptl.x = rclRecc.xRight;
+                ptl.y = rclRecc.yBottom;
+                    // now we have the lower-right corner in cnr coords
+
+                // clip if this is outside the container window
+                WinQueryWindowRect(hwndCnr, &rclRecc);
+                if (ptl.x > rclRecc.xRight)
+                    ptl.x = rclRecc.xRight;
+                if (ptl.y > rclRecc.yTop)
+                    ptl.y = rclRecc.yTop;
+
+                // convert this to screen coordinates
+                WinMapWindowPoints(hwndCnr,
+                           HWND_DESKTOP,
+                           &ptl,
+                           1);
+                fQueried = TRUE;
+            }
+        }
+
+        if (!fQueried)
+            // else: use mouse coordinates for context menu
+            WinQueryPointerPos(HWND_DESKTOP, &ptl);
+
+        // give preccSource source emphasis;
+        // if preccSource == NULL, the whole container will be
+        // given source emphasis
+        WinSendMsg(hwndCnr,
+                   CM_SETRECORDEMPHASIS,
+                   (MPARAM)preccSource,     // might be NULL for whole container
+                   MPFROM2SHORT(TRUE,  // set emphasis
+                            CRA_SOURCE));
+
+        // finally, show context menu
+        brc = WinPopupMenu(HWND_DESKTOP,               // menu parent
+                           hwndMenuOwner,              // menu owner
+                           hMenu,
+                           (SHORT)ptl.x,
+                           (SHORT)ptl.y,
+                           0,                          // ID
+                           PU_NONE
+                              | PU_MOUSEBUTTON1
+                              | PU_KEYBOARD
+                              | PU_HCONSTRAIN
+                              | PU_VCONSTRAIN);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ cnrhQuerySourceRecord:
+ *      this helper function evaluates a given container
+ *      to find out which records have been selected while
+ *      a context menu is open.
+ *
+ *      This is for implementing a WPS-like (probably CUA) behavior
+ *      when invoking actions on container records. That is:
+ *
+ *      1)  If the user opens a context menu on a selected object,
+ *          the selected action should be invoked on _all_ selected
+ *          objects.
+ *
+ *      2)  If the user opens a context menu on an object which is
+ *          _not_ selected, the action should be invoked on that
+ *          object only (which should be given source emphasis),
+ *          no matter what other objects are selected.
+ *
+ *      This function expects in preccSource the record core which
+ *      currently has (or just had) source emphasis.
+ *
+ *      1)  In your own programs, you should have used cnrhShowContextMenu
+ *          above, which sets record source emphasis correctly. Unfortunately,
+ *          PM posts WM_MENUEND _before_ WM_COMMAND, so if you remove source
+ *          emphasis in WM_MENUEND, it is unknown which record was selected at
+ *          the time WM_COMMAND comes in... so you need to store that record
+ *          and pass it to this function later. Sorry.
+ *
+ *      2)  With the WPS, this function works within the wpMenuItemSelected
+ *          method, because that one is invoked during the processing of
+ *          WM_COMMAND and the WPS automatically does the source emphasis
+ *          stuff right.
+ *
+ *      The result of this evaluation is stored in *pulSelection,
+ *      which can be:
+ *
+ *      --   SEL_WHITESPACE (1): the context menu was opened on the
+ *                          whitespace of the container (preccSource
+ *                          is NULL);
+ *                          this func then returns NULL also.
+ *      --   SEL_SINGLESEL (2): the context menu was opened for a
+ *                          single selected object:
+ *                          this func then returns that record core
+ *                          (which is preccSource).
+ *      --   SEL_MULTISEL (3): the context menu was opened on one
+ *                          of a multitude of selected record;
+ *                          this func then returns the first of the
+ *                          selected records. Use
+ *                          cnrhQueryNextSelectedRecord to get the others.
+ *      --   SEL_SINGLEOTHER (4): the context menu was opened for a
+ *                          single record _other_ than the selected
+ *                          records:
+ *                          this func then returns that record
+ *                          (which is preccSource).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PRECORDCORE cnrhQuerySourceRecord(HWND hwndCnr,          // in:  cnr
+                                  PRECORDCORE preccSource, // in: record which had source emphasis
+                                  PULONG pulSelection)   // out: selection flags
+{
+    PRECORDCORE preccReturn = NULL;
+
+    if (preccSource == NULL)
+    {
+        // this probably means that the whole container has
+        // source emphasis --> context menu on folder whitespace
+        *pulSelection = SEL_WHITESPACE;
+                // and return NULL
+    }
+    else if (((LONG)preccSource) != -1) // no error?
+    {
+        // check whether the source record is also selected
+        if ((preccSource->flRecordAttr & CRA_SELECTED) == 0)
+        {
+            // no:
+            // only one object, but not one of the selected ones
+            // (if any at all)
+            preccReturn = preccSource;
+            *pulSelection = SEL_SINGLEOTHER;
+        }
+        else
+        {
+            // yes, source record _is_ selected:
+            // check whether we have more than one selected record?
+
+            // get first selected record
+            PRECORDCORE preccSelected = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                                                CM_QUERYRECORDEMPHASIS,
+                                                                (MPARAM)CMA_FIRST,
+                                                                (MPARAM)CRA_SELECTED);
+            // return that one
+            preccReturn = preccSelected;
+
+            // are several objects selected?
+            preccSelected = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                                   CM_QUERYRECORDEMPHASIS,
+                                                   (MPARAM)preccSelected,
+                                                            // get next selected
+                                                   (MPARAM)CRA_SELECTED);
+            if (preccSelected)
+                // several objects
+                *pulSelection = SEL_MULTISEL;
+            else
+                // only one object
+                *pulSelection = SEL_SINGLESEL;
+        }
+    }
+
+    return (preccReturn);
+}
+
+/*
+ *@@ cnrhQueryNextSelectedRecord:
+ *      if cnrhQuerySourceRecord above returns SEL_MULTISEL
+ *      you can use this helper func to loop thru all the
+ *      selected records. This will return the next record
+ *      after preccCurrent which is selected or NULL if it's the last.
+ *
+ *      If you're not using cnrhQuerySourceRecord (because your
+ *      records do not have source emphasis), you can also call
+ *      this function with (preccCurrent == CMA_FIRST) to get the
+ *      first selected record core.
+ */
+
+PRECORDCORE cnrhQueryNextSelectedRecord(HWND hwndCnr,
+                                        PRECORDCORE preccCurrent)
+{
+    PRECORDCORE preccReturn = 0;
+    if (preccCurrent)
+    {
+        PRECORDCORE preccNext = (PRECORDCORE)WinSendMsg(hwndCnr,
+                                                        CM_QUERYRECORDEMPHASIS,
+                                                        (MPARAM)preccCurrent,
+                                                        (MPARAM)CRA_SELECTED);
+        if ((preccNext) && ((LONG)preccNext != -1) )
+            preccReturn = preccNext;
+    }
+    return (preccReturn);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Container helpers\Record relations/iteration
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Record relations/iteration                                     *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ cnrhQueryRecordIndex:
+ *      returns the "index" of the specified record
+ *      in the specified container. That is, if the
+ *      specified record is the first record in the
+ *      container, 0 is returned. If it's the second,
+ *      1 is returned, and so on.
+ *
+ *      Returns -1 if precc could not be found.
+ *
+ *      Doesn't work in Tree view.
+ *
+ *@@added V0.9.3 (2000-04-19) [umoeller]
+ */
+
+LONG cnrhQueryRecordIndex(HWND hwndCnr,
+                          PRECORDCORE precc)
+{
+    PRECORDCORE precc2 = (PRECORDCORE)CMA_FIRST;
+    BOOL        fFirstCall = TRUE;
+    LONG        lrc = -1,
+                lCount = 0;
+
+    while (TRUE)
+    {
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                                    CM_QUERYRECORD,
+                                    (MPARAM)precc2,  // ignored on first call
+                                    MPFROM2SHORT(
+                                            (fFirstCall)
+                                                ? CMA_FIRST
+                                                : CMA_NEXT,
+                                            CMA_ITEMORDER)
+                                    );
+        if (     (precc2 == NULL)
+              || (precc2 == (PRECORDCORE)-1)
+           )
+            // error:
+            // return -1
+            break;
+        else
+            if (precc2 == precc)
+            {
+                // same as search record:
+                lrc = lCount;
+                break;
+            }
+
+        // else search on
+        fFirstCall = FALSE;
+        lCount++;
+    }
+
+    return (lrc);
+}
+
+/*
+ *@@ cnrhForAllRecords:
+ *      this monster function calls pfnwpCallback
+ *      for really all the records in the container,
+ *      including child records in tree view.
+ *
+ *      This is extremely useful for cleaning up
+ *      all record cores before a container window
+ *      gets destroyed.
+ *
+ *      This function recurses for child records.
+ *      On the first call, preccParent should be
+ *      NULL; you may however specify a certain
+ *      record, and this function will call the
+ *      callback only for that record and children.
+ *
+ *      pfnwpCallback gets called with the following
+ *      parameters:
+ *
+ *      -- HWND hwnd: hwndCnr, as passed to this func
+ *      -- PRECORDCORE precc: current record core, as
+ *                     determined by this func.
+ *      -- ULONG ulUser: what you have specified here.
+ *
+ *      If the callback returns anything != 0, this
+ *      function stops even before all records have
+ *      been processed. You can use ulUser for a
+ *      pointer to a return value.
+ *
+ *      This always returns the no. of records which
+ *      were processed.
+ *
+ *      If you use this function for deleting record
+ *      cores, you can be sure that you can delete
+ *      every record, because your callback gets called
+ *      for the child records before the parent record.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG cnrhForAllRecords(HWND hwndCnr,
+                        PRECORDCORE preccParent,
+                        PFNCBRECC pfncbRecc,
+                        ULONG ulUser1,
+                        ULONG ulUser2)
+{
+    PRECORDCORE precc2 = preccParent;
+    ULONG       ulrc = 0;
+    USHORT      usQuery;
+    BOOL        fFirstCall = TRUE;
+
+    while (TRUE)
+    {
+        if (fFirstCall)
+        {
+            // first call:
+            if (preccParent)
+                // non-root:
+                usQuery = CMA_FIRSTCHILD;
+            else
+                // NULL == root:
+                usQuery = CMA_FIRST;
+        }
+        else
+            // subsequent calls:
+            usQuery = CMA_NEXT;     // works as CMA_NEXTCHILD also
+
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                                    CM_QUERYRECORD,
+                                    (MPARAM)((fFirstCall)
+                                         // first call (CMA_FIRSTCHILD or CMA_FIRST):
+                                         ? preccParent   // ignored for CMA_FIRST
+                                         // subsequent calls (CMA_NEXTCHILD or CMA_NEXT):
+                                         : precc2),  // what we queried last
+                                    MPFROM2SHORT(
+                                            usQuery,    // set above
+                                            CMA_ITEMORDER)
+                                    );
+
+        if ((precc2) && ((ULONG)precc2 != -1))
+        {
+            // record found:
+            // recurse for that record
+            ulrc += cnrhForAllRecords(hwndCnr,
+                                      precc2,        // new parent to search
+                                      pfncbRecc,
+                                      ulUser1,
+                                      ulUser2);
+
+            // _Pmpf(("Calling callback for %s", precc2->pszIcon));
+
+            // call callback
+            if (pfncbRecc)
+                if ((*pfncbRecc)(hwndCnr, precc2, ulUser1, ulUser2))
+                    // returns something != NULL:
+                    // stop
+                    break;
+            ulrc++;
+        }
+        else
+            // no more records or error: get outta here
+            break;
+
+        fFirstCall = FALSE;
+    }
+
+    return (ulrc);
+}
+
+/*
+ * cnrhForAllChildRecords:
+ *      calls the specified fncbRecc callback for
+ *      the specified recc and all its child records.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+/* VOID cnrhForAllChildRecords(HWND hwndCnr,
+                            PRECORDCORE precc,
+                            PFNCBRECC pfncbRecc,
+                            ULONG ulp1,
+                            ULONG ulp2)
+{
+    PRECORDCORE precc2 = precc;
+    (*pfncbRecc)(precc, ulp1, ulp2);
+    do {
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                    CM_QUERYRECORD,
+                    (MPARAM)precc2,
+                    MPFROM2SHORT(
+                        (precc2 == precc)
+                            ? CMA_FIRSTCHILD : CMA_NEXT,
+                        CMA_ITEMORDER)
+                    );
+        if ((LONG)precc2 == -1)
+            precc2 = NULL;
+        if (precc2)
+            // recurse again
+            cnrhForAllChildRecords(hwndCnr, precc2, pfncbRecc, ulp1, ulp2);
+    } while (precc2);
+} */
+
+/*
+ * cnrhForAllRecords2:
+ *      this is a useful function which calls
+ *      the specified callback function for
+ *      really all records in the container of
+ *      the main window, including child records.
+ *
+ *      xxx
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+/* VOID cnrhForAllRecords2(HWND hwndCnr,
+                        PFNCBRECC pfncbRecc,
+                        ULONG ulp1,
+                        ULONG ulp2)
+{
+    PRECORDCORE precc2 = NULL;
+    do {
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                    CM_QUERYRECORD,
+                    (MPARAM)precc2,
+                    MPFROM2SHORT(
+                        ((precc2 == NULL) ? CMA_FIRST : CMA_NEXT),
+                        CMA_ITEMORDER)
+                    );
+        if ((LONG)precc2 == -1)
+            precc2 = NULL;
+        if (precc2)
+            // recurse again
+            cnrhForAllChildRecords(hwndCnr, precc2, pfncbRecc, ulp1, ulp2);
+    } while (precc2);
+} */
+
+/*
+ * cnrhForAllParentRecords:
+ *      just as above, but climbs up instead.
+ *      The first call of the callback will be
+ *      for the parent record of precc.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+/* VOID cnrhForAllParentRecords(HWND hwndCnr,
+                             PRECORDCORE precc,
+                             PFNCBRECC pfncbRecc,
+                             ULONG ulp1,
+                             ULONG ulp2)
+{
+    PRECORDCORE precc2 = precc;
+    do {
+        precc2 =
+            (PRECORDCORE)WinSendMsg(hwndCnr,
+                                    CM_QUERYRECORD,
+                                    (MPARAM)precc2,
+                                    MPFROM2SHORT(CMA_PARENT,
+                                        CMA_ITEMORDER)
+                                    );
+        if ((LONG)precc2 == -1)
+            precc2 = NULL;
+        if (precc2)
+            (*pfncbRecc)(precc2, ulp1, ulp2);
+    } while (precc2);
+} */
+
+/*
+ *@@category: Helpers\PM helpers\Container helpers\Miscellaneous
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Miscellaneous                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ cnrhQueryCnrFromFrame:
+ *      find the handle of a frame window's container; we do this
+ *      by enumerating all the frame's child windows and comparing
+ *      their window classes to the WC_CONTAINER code.
+ *      This is not terribly fast, so use this func economically.
+ *      Returns NULLHANDLE if not found.
+ */
+
+HWND cnrhQueryCnrFromFrame(HWND hwndFrame)
+{
+    HENUM               henum;
+    CHAR                szClassName[256];
+    HWND                hwndCnr = NULLHANDLE,
+                        hwndTemp = NULLHANDLE;
+
+    if (hwndFrame)
+    {
+        henum = WinBeginEnumWindows(hwndFrame);
+        if (henum)
+        {
+            do
+            {
+                hwndTemp = WinGetNextWindow(henum);
+                if (hwndTemp)
+                {
+                    if (WinQueryClassName(hwndTemp, 250, szClassName))
+                        if (strcmp(szClassName, "#37") == 0)
+                            // unbelievable, this is PM's code for WC_CONTAINER...
+                            hwndCnr = hwndTemp;
+                }
+            } while (hwndTemp);
+        }
+        WinEndEnumWindows(henum);
+    }
+
+    return hwndCnr;
+}
+
+/*
+ *@@ cnrhInitDrag:
+ *      this sets up the necessary structures to begin dragging
+ *      a record core from a container. This helper func should be
+ *      called if your container should support being the source
+ *      window of a direct manipulation.
+ *
+ *      This should get called in three situations:
+ *
+ *      --  the container sends CN_INITDRAG ("real" d'n'd desired by user);
+ *      --  the container sends us CN_PICKUP (Alt+MB2 pressed);
+ *      --  the user has selected "Pickup" from a record core's
+ *          context menu (ID_XSMI_FILETYPES_PICKUP command).
+ *          In that case, you can also call this function with
+ *          usNotifyCode == CN_PICKUP.
+ *
+ *      In all three cases, preccDrag must be the record core
+ *      which is to be dragged.
+ *
+ *      Depending on usNotifyCode, this function, after having allocated
+ *      the necessary data, will do the following:
+ *
+ *      1)  If (usNotifyCode == CN_PICKUP), we will initiate a non-modal
+ *          (lazy) drag (DrgLazyDrag). This function will then return
+ *          after the records have been given "picked" emphasis.
+ *
+ *          Note: You must intercept CN_DROPNOTIFY in your window proc
+ *          to clean up resources later.
+ *
+ *      2)  However, if (usNotifyCode == CN_INITDRAG), we will start
+ *          a regular modal drag here by calling DrgDrag. This function
+ *          will _not_ return until the object has been dropped or d'n'd
+ *          has been cancelled. PM establishes another message loop
+ *          internally for this.
+ *
+ *      This function supports one single record core only. The following
+ *      information will be set in the DRAGITEM structure:
+ *
+ *      --  ulItemID will be set to the preccDrag so that the target
+ *          window can access the dragged record.
+ *      --  hstrSourceName == hstrTargetName gets the RECORDCORE.pszIcon.
+ *
+ *      The drag icon will be a default system file icon.
+ */
+
+PDRAGINFO cnrhInitDrag(HWND hwndCnr,
+                                 // in: source container window
+                       PRECORDCORE preccDrag,
+                                 // in: record to be dragged (only one supported)
+                       USHORT usNotifyCode,
+                                 // in: CN_INITDRAG or CN_PICKUP
+                       PSZ   pszRMF,
+                                 // in: rendering mechanism and format,
+                                 // e.g. "(DRM_MYFILETYPE)x(DRF_UNKNOWN)"
+                       USHORT usSupportedOps)
+                                 // stored in DRAGITEM.fsSupportedOps,
+                                 // one or several of DO_COPYABLE, DO_MOVEABLE, DO_LINKABLE
+{
+    DRAGIMAGE    drgImage;
+    PDRAGINFO    pdrgInfo = NULL;
+
+    memset(&drgImage, 0, sizeof(drgImage));
+    pdrgInfo = DrgAllocDraginfo(1); // one item only
+    if (pdrgInfo)
+    {
+        DRAGITEM drgItem;
+        memset(&drgItem, 0, sizeof(drgItem));
+
+        drgItem.hwndItem  = hwndCnr;
+        drgItem.ulItemID  = (ULONG)preccDrag;
+                    // we use this to store the container rec
+        drgItem.hstrType  = DrgAddStrHandle(DRT_UNKNOWN);
+                    // application defined
+        drgItem.hstrRMF   = DrgAddStrHandle(pszRMF);
+        drgItem.hstrContainerName = 0;
+        drgItem.hstrSourceName = DrgAddStrHandle(preccDrag->pszIcon);
+        drgItem.hstrTargetName = drgItem.hstrSourceName;
+        drgItem.fsSupportedOps = usSupportedOps;
+
+        // set the DRAGITEM struct into the memory
+        // allocated by DrgAllocDraginfo()
+        DrgSetDragitem(pdrgInfo,
+                       &drgItem,
+                       sizeof(DRAGITEM),
+                       0);     // item index
+
+        // fill in the DRAGIMAGE structure
+        drgImage.cb       = sizeof(DRAGIMAGE);
+        drgImage.hImage   = WinQuerySysPointer(HWND_DESKTOP,
+                                               SPTR_FILE,
+                                               FALSE);
+                    // pointer used for dragging; we
+                    // use a dull default file icon
+        drgImage.fl       = DRG_ICON;             // hImage is an HPOINTER
+        drgImage.cxOffset = 0; // 5 * iOffset;          // Image offset from mouse pointer
+        drgImage.cyOffset = 0; // 5 * iOffset;          // Image offset from mouse pointer
+
+        // set source emphasis for the container record
+        WinSendMsg(hwndCnr,
+                   CM_SETRECORDEMPHASIS,
+                   (MPARAM)preccDrag,
+                   MPFROM2SHORT(TRUE,
+                       (usNotifyCode == CN_INITDRAG)
+                           // for dragging: source emphasis
+                           ? CRA_SOURCE
+                           // for lazy drag: picked emphasis
+                           : CRA_PICKED));
+
+        if (usNotifyCode == CN_INITDRAG)
+        {
+            // "real" dragging:
+            // call modal function for drag'n'drop.
+            // This does not return until either
+            // d'n'd has been cancelled or the
+            // record core has been dropped.
+            /* HWND hwndTarget = */ DrgDrag(hwndCnr,
+                                      pdrgInfo,
+                                      &drgImage,
+                                      1,      // drag image count
+                                      VK_ENDDRAG, // system drag button
+                                      NULL);  // reserved
+
+            // since it's the source which is responsible
+            // for cleaning up the resources, this is
+            // what we do now (we had a modal d'n'd)
+            DrgDeleteDraginfoStrHandles(pdrgInfo);
+            DrgFreeDraginfo(pdrgInfo);
+
+            // remove source emphasis for the container record
+            WinSendMsg(hwndCnr,
+                       CM_SETRECORDEMPHASIS,
+                       (MPARAM)preccDrag,
+                       MPFROM2SHORT(FALSE,
+                               CRA_SOURCE));
+        } // end if (usNotifyCode == CN_INITDRAG)
+        else
+        {
+            // "lazy drag" (CN_PICKUP):
+            // initiate lazy drag. This function returns
+            // immediately, since lazy drag is non-modal.
+            // As a result, we cannot clean up the resources
+            // here, but need to do this later (CN_DROPNOTIFY).
+            DrgLazyDrag(hwndCnr,
+                        pdrgInfo,
+                        &drgImage,
+                        1,      // drag image count
+                        0);     // reserved
+        }
+    } // end if (pdrgInfo)
+
+    return (pdrgInfo);
+}
+
+/*
+ *@@ cnrhOwnerDrawRecord:
+ *      this helper func can be called upon receiving
+ *      WM_DRAWITEM for owner-drawing container record
+ *      cores.
+ *
+ *      Presently, this function supports drawing record
+ *      cores in gray color if the record has the CRA_DISABLED
+ *      style (which, AFAIK, PM ignores per default).
+ *
+ *      This returns either TRUE or FALSE as an MPARAM, depending
+ *      on whether we have drawn the item. This return value should
+ *      also be the return value of your window procedure.
+ */
+
+MRESULT cnrhOwnerDrawRecord(MPARAM mp2) // in: mp2 of WM_DRAWITEM (POWNERITEM)
+{
+    MRESULT mrc = 0;
+
+    // get generic DRAWITEM structure
+    POWNERITEM poi = (POWNERITEM)mp2;
+
+    // check if we're to draw the text
+    // (and not the icon)
+    if (poi->idItem == CMA_TEXT)
+    {
+        // get container-specific draw-item struct
+        PCNRDRAWITEMINFO pcdii = (PCNRDRAWITEMINFO)poi->hItem;
+
+        if (((pcdii->pRecord->flRecordAttr) & CRA_DISABLED) == 0)
+        {
+            /*
+            // not disabled == valid WPS class
+            if ((pcdii->pRecord->flRecordAttr) & CRA_SELECTED)
+            {
+                // not disabled, but selected:
+                lBackground = winhQueryPresColor(hwndDlg, PP_HILITEBACKGROUNDCOLOR, SYSCLR_HILITEBACKGROUND);
+                lForeground = winhQueryPresColor(hwndDlg, PP_HILITEFOREGROUNDCOLOR, SYSCLR_HILITEFOREGROUND);
+            }
+            else
+            {
+                // not disabled, not selected:
+                lBackground = winhQueryPresColor(hwndDlg, PP_BACKGROUNDCOLOR, SYSCLR_BACKGROUND);
+                lForeground = winhQueryPresColor(hwndDlg, PP_FOREGROUNDCOLOR, SYSCLR_WINDOWTEXT);
+            } */
+            mrc = FALSE;
+                // let cnr draw the thing
+        }
+        else
+        {
+            // CRA_DISABLED:
+
+            ULONG flCmd = DT_LEFT | DT_TOP | DT_ERASERECT;
+            RECTL rcl2;
+
+            // set draw colors
+            LONG  lBackground,
+                  lForeground;
+
+            // switch to RGB
+            GpiCreateLogColorTable(poi->hps, 0, LCOLF_RGB, 0, 0, NULL);
+
+            if ((pcdii->pRecord->flRecordAttr) & CRA_SELECTED)
+            {
+                // disabled and selected:
+                lBackground = WinQuerySysColor(HWND_DESKTOP,
+                                               SYSCLR_SHADOWTEXT, 0);
+                lForeground = winhQueryPresColor(poi->hwnd,
+                                                 PP_BACKGROUNDCOLOR,
+                                                 FALSE, // no inherit
+                                                 SYSCLR_WINDOW);
+            }
+            else
+            {
+                // disabled and not selected:
+                lBackground = winhQueryPresColor(poi->hwnd,
+                                                 PP_BACKGROUNDCOLOR,
+                                                 FALSE,
+                                                 SYSCLR_WINDOW);
+                lForeground = WinQuerySysColor(HWND_DESKTOP,
+                                               SYSCLR_SHADOWTEXT, 0);
+            }
+
+            memcpy(&rcl2, &(poi->rclItem), sizeof(rcl2));
+            /* WinDrawText(poi->hps,
+                        strlen(pcdii->pRecord->pszText),
+                        pcdii->pRecord->pszText,
+                        &rcl2,
+                        lForeground,  // foreground
+                        lBackground,
+                        flCmd); */
+
+            GpiSetBackColor(poi->hps, lBackground);
+            GpiSetColor(poi->hps, lForeground);
+
+            winhDrawFormattedText(poi->hps,
+                                  &rcl2,
+                                  pcdii->pRecord->pszText,
+                                  flCmd);
+
+            mrc = (MPARAM)TRUE; // tell cnr that we've drawn the item
+        }
+    } else
+        mrc = (MPARAM)FALSE; // tell cnr to draw the item
+
+    return (mrc);
+}
+
+/*
+ *@@ cnrhDateTimeDos2Win:
+ *      this converts the information in a Control Program
+ *      DATETIME structure (as returned by DosGetDateTime)
+ *      to the CDATE and CTIME structures used by containers.
+ *
+ *      If any of the target pointers is NULL, that data is
+ *      not converted.
+ */
+
+BOOL cnrhDateTimeDos2Win(DATETIME* pdt,     // in: Dos format date and time
+                         CDATE* pcdate,     // out: container format date
+                         CTIME* pctime)     // out: container format time
+{
+    if (pdt)
+    {
+        if (pcdate)
+        {
+            pcdate->day = pdt->day;
+            pcdate->month = pdt->month;
+            pcdate->year = pdt->year;
+        }
+        if (pctime)
+        {
+            pctime->seconds = pdt->seconds;
+            pctime->minutes = pdt->minutes;
+            pctime->hours = pdt->hours;
+        }
+        return (TRUE);
+    }
+    else
+        return (FALSE);
+}
+
+/*
+ *@@ cnrhDateDos2Win:
+ *      converts an FDATE structure (Control program)
+ *      to a container CDATE structure.
+ */
+
+BOOL cnrhDateDos2Win(FDATE* pfd,    // in: DOS date
+                     CDATE* pcd)    // out: container date
+{
+    if ((pfd) && (pcd))
+    {
+        pcd->day = pfd->day;
+        pcd->month = pfd->month;
+        pcd->year = pfd->year + 1980;
+        return (TRUE);
+    }
+    else
+        return (FALSE);
+}
+
+/*
+ *@@ cnrhTimeDos2Win:
+ *      converts an FTIME structure (Control program)
+ *      to a container CTIME structure.
+ */
+
+BOOL cnrhTimeDos2Win(FTIME* pft,    // in: DOS time
+                     CTIME* pct)    // out: container time
+{
+    if ((pft) && (pct))
+    {
+        pct->seconds = pft->twosecs * 2;
+        pct->minutes = pft->minutes;
+        pct->hours = pft->hours;
+        pct->ucReserved = 0;
+        return (TRUE);
+    }
+    else
+        return (FALSE);
+}
Index: /trunk/src/helpers/comctl.c
===================================================================
--- /trunk/src/helpers/comctl.c	(revision 8)
+++ /trunk/src/helpers/comctl.c	(revision 8)
@@ -0,0 +1,1215 @@
+
+/*
+ *@@sourcefile comctl.c:
+ *      contains various window procedures for implementing
+ *      common controls. The source file name has nothing to do
+ *      with the Windoze DLL of the same name.
+ *
+ *      Usage: All PM programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  ctl*   common control helper functions
+ *
+ *      The functionality of this code is accessed with the use
+ *      of a special helper function. Sometimes that function
+ *      registers a new window class, sometimes a static control
+ *      needs to be subclassed. See the respective functions for
+ *      details.
+ *
+ *      In detail, we have:
+ *
+ *      --  a "menu button" control, which displays a menu when
+ *          pressed (see ctlMakeMenuButton for details);
+ *
+ *      --  progress bar support (see ctlProgressBarFromStatic for
+ *          details);
+ *
+ *      --  a "chart" control for displaying pie charts (all new
+ *          with V0.9.0; see ctlChartFromStatic for details);
+ *
+ *      --  split windows support (all new with V0.9.0; see
+ *          ctlCreateSplitWindow for details);
+ *
+ *      --  a subclassed static control for enhanced bitmap and
+ *          and icon display (see ctl_fnwpBitmapStatic for details).
+ *          This used to be in animate.c and has been enhanced
+ *          with V0.9.0;
+ *
+ *      --  a "tooltip" control, which shows fly-over ("bubble")
+ *          help over any window, including controls. This is
+ *          largely API-compatible with the Win95 tooltip control.
+ *          See ctl_fnwpTooltip for details;
+ *
+ *      --  a "checkbox container" control, which is a subclassed
+ *          container which uses checkboxes as record icons.
+ *          See ctlMakeCheckboxContainer for details.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\comctl.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINPOINTERS
+#define INCL_WINTRACKRECT
+#define INCL_WINTIMER
+#define INCL_WINSYS
+
+#define INCL_WINRECTANGLES      /// xxx temporary
+
+#define INCL_WINMENUS
+#define INCL_WINSTATICS
+#define INCL_WINBUTTONS
+#define INCL_WINSTDCNR
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPILCIDS
+#define INCL_GPIPATHS
+#define INCL_GPIREGIONS
+#define INCL_GPIBITMAPS             // added V0.9.1 (2000-01-04) [umoeller]: needed for EMX headers
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"             // exception handling
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\winh.h"
+
+#include "helpers\comctl.h"
+
+#pragma hdrstop
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Menu buttons
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   "Menu button" control                                          *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ MENUBUTTONDATA:
+ *      internal data for "menu button"
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+typedef struct _MENUBUTTONDATA
+{
+    PFNWP       pfnwpButtonOriginal;
+    HMODULE     hmodMenu;
+    ULONG       idMenu;
+    BOOL        fMouseCaptured,         // TRUE if WinSetCapture
+                fMouseButton1Down,      // TRUE in between WM_BUTTON1DOWN and WM_BUTTON1UP
+                fButtonSunk;            // toggle state of the button
+    HWND        hwndMenu;
+} MENUBUTTONDATA, *PMENUBUTTONDATA;
+
+/*
+ *@@ ctl_fnwpSubclassedMenuButton:
+ *      subclassed window proc for "menu button".
+ *      See ctlMakeMenuButton for details.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-02-28) [umoeller]: menu was displayed even if button was disabled; fixed
+ */
+
+MRESULT EXPENTRY ctl_fnwpSubclassedMenuButton(HWND hwndButton, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = 0;
+    PMENUBUTTONDATA pmbd = (PMENUBUTTONDATA)WinQueryWindowULong(hwndButton, QWL_USER);
+
+    switch (msg)
+    {
+        /*
+         * WM_BUTTON1DOWN:
+         * WM_BUTTON1UP:
+         *      these show/hide the menu.
+         *
+         *      Showing the menu follows these steps:
+         *          a)  first WM_BUTTON1DOWN hilites the button;
+         *          b)  first WM_BUTTON1UP shows the menu.
+         *
+         *      When the button is pressed again, the open
+         *      menu loses focus, which results in WM_MENUEND
+         *      and destroys the window automatically.
+         */
+
+        case WM_BUTTON1DOWN:
+        case WM_BUTTON1DBLCLK:
+            // only do this if the button is enabled
+            // V0.9.2 (2000-02-28) [umoeller]
+            if (WinIsWindowEnabled(hwndButton))
+            {
+                // _Pmpf(("WM_BUTTON1DOWN"));
+                // since we're not passing the message
+                // to WinDefWndProc, we need to give
+                // ourselves the focus
+                WinSetFocus(HWND_DESKTOP, hwndButton);
+
+                if (!pmbd->fMouseCaptured)
+                {
+                    // capture mouse events while the
+                    // mouse button is down
+                    WinSetCapture(HWND_DESKTOP, hwndButton);
+                    pmbd->fMouseCaptured = TRUE;
+                }
+
+                pmbd->fMouseButton1Down = TRUE;
+
+                if (!pmbd->fButtonSunk)
+                {
+                    // button not hilited yet (first click):
+                    // do it now
+                    // _Pmpf(("  Sinking menu WM_BUTTON1DOWN "));
+                    pmbd->fButtonSunk = TRUE;
+                    WinSendMsg(hwndButton,
+                               BM_SETHILITE,
+                               (MPARAM)TRUE,
+                               (MPARAM)0);
+                }
+
+                // else: the menu has just been destroyed
+                // (WM_MENUEND below)
+            }
+
+            mrc = (MPARAM)TRUE;     // message processed
+        break;
+
+        /*
+         * WM_BUTTON1UP:
+         *
+         */
+
+        case WM_BUTTON1UP:
+            // only do this if the button is enabled
+            // V0.9.2 (2000-02-28) [umoeller]
+            if (WinIsWindowEnabled(hwndButton))
+            {
+                // _Pmpf(("WM_BUTTON1UP sunk %d hwndMenu 0x%lX", pmbd->fButtonSunk, pmbd->hwndMenu));
+
+                // un-capture the mouse first
+                if (pmbd->fMouseCaptured)
+                {
+                    WinSetCapture(HWND_DESKTOP, NULLHANDLE);
+                    pmbd->fMouseCaptured = FALSE;
+                }
+
+                pmbd->fMouseButton1Down = FALSE;
+
+                if (    (pmbd->fButtonSunk)      // set by WM_BUTTON1DOWN above
+                     && (pmbd->hwndMenu)         // menu currently showing
+                   )
+                {
+                    // button currently depressed:
+                    // un-hilite button
+                    // _Pmpf(("  Unsinking menu WM_BUTTON1UP 1"));
+                    pmbd->fButtonSunk = FALSE;
+                    WinSendMsg(hwndButton,
+                               BM_SETHILITE,
+                               (MPARAM)FALSE,
+                               (MPARAM)0);
+                }
+                else
+                {
+                    // first button up:
+                    // show menu
+
+                    if (pmbd->idMenu)
+                    {
+                        // _Pmpf(("  Loading menu hmod %lX id %lX", pmbd->hmodMenu, pmbd->idMenu));
+                        // menu specified: load from
+                        // specified resources
+                        pmbd->hwndMenu = WinLoadMenu(hwndButton,
+                                                     pmbd->hmodMenu,
+                                                     pmbd->idMenu);
+                    }
+                    else
+                    {
+                        HWND    hwndOwner = WinQueryWindow(hwndButton, QW_OWNER);
+                        // _Pmpf(("  Querying menu WM_COMMAND from owner 0x%lX", hwndOwner));
+                        // send WM_COMMAND to owner
+                        pmbd->hwndMenu
+                            = (HWND)WinSendMsg(hwndOwner,
+                                               WM_COMMAND,
+                                               (MPARAM)(ULONG)WinQueryWindowUShort(hwndButton,
+                                                                                   QWS_ID),
+                                               (MPARAM)0);
+                    }
+
+                    // _Pmpf(("  Loaded menu, hwnd: 0x%lX", pmbd->hwndMenu));
+
+                    if (pmbd->hwndMenu)
+                    {
+                        // menu successfully loaded:
+                        // find out where to put it
+                        SWP     swpButton;
+                        POINTL  ptlMenu;
+                        WinQueryWindowPos(hwndButton, &swpButton);
+                        ptlMenu.x = swpButton.x;
+                        ptlMenu.y = swpButton.y;
+
+                        // ptlMenu now has button coordinates
+                        // relative to the button's parent;
+                        // convert this to screen coordinates:
+                        WinMapWindowPoints(WinQueryWindow(hwndButton, QW_PARENT),
+                                           HWND_DESKTOP,
+                                           &ptlMenu,
+                                           1);
+
+                        // now show the menu on top of the button
+                        WinPopupMenu(HWND_DESKTOP,               // menu parent
+                                     hwndButton,                 // owner
+                                     pmbd->hwndMenu,
+                                     (SHORT)(ptlMenu.x),
+                                     (SHORT)(ptlMenu.y + swpButton.cy - 1),
+                                     0,                          // ID
+                                      PU_NONE
+                                         | PU_MOUSEBUTTON1
+                                         | PU_KEYBOARD
+                                         | PU_HCONSTRAIN
+                                         | PU_VCONSTRAIN);
+                    } // end if (pmbd->hwndMenu)
+                    else
+                    {
+                        // menu not loaded:
+                        // _Pmpf(("  Unsinking menu WM_BUTTON1UP 2"));
+                        pmbd->fButtonSunk = FALSE;
+                        WinSendMsg(hwndButton,
+                                   BM_SETHILITE,
+                                   (MPARAM)FALSE,
+                                   (MPARAM)0);
+                    }
+                }
+            }
+
+            mrc = (MPARAM)TRUE;     // message processed
+        break;
+
+        /*
+         * WM_BUTTON1CLICK:
+         *      swallow this
+         */
+
+        case WM_BUTTON1CLICK:
+            mrc = (MPARAM)TRUE;
+        break;
+
+        /*
+         * WM_SETFOCUS:
+         *      swallow this, the button keeps painting
+         *      itself otherwise
+         */
+
+        case WM_SETFOCUS:
+        break;
+
+        /*
+         * WM_MENUEND:
+         *      menu is destroyed; we get this
+         *      because we're the owner
+         */
+
+        case WM_MENUEND:
+        {
+            BOOL fUnHilite = TRUE;
+            // _Pmpf(("WM_MENUEND"));
+            // At this point, the menu has been
+            // destroyed already.
+            // Since WM_BUTTON1UP handles the
+            // default case that the user presses
+            // the menu button a second time while
+            // the menu is open, we only need
+            // to handle the case that the user
+            // c)   presses some key on the menu (ESC, menu selection) or
+            // a)   selects a menu item (which
+            //      dismisses the menu) or
+            // b)   clicks anywhere else.
+
+            // Case a) if mouse button 1 is not currently down
+            if ((WinGetKeyState(HWND_DESKTOP, VK_BUTTON1) & 0x8000) == 0)
+                // button 1 _not_ down:
+                // must be keyboard
+                fUnHilite = TRUE;
+            else
+                // button 1 _is_ down:
+                // query window under mouse pointer
+                ;
+
+            if (fUnHilite)
+            {
+                // _Pmpf(("  Unsinking menu WM_MENUEND"));
+                pmbd->fButtonSunk = FALSE;
+                WinSendMsg(hwndButton,
+                           BM_SETHILITE,
+                           (MPARAM)FALSE,
+                           (MPARAM)0);
+            }
+            pmbd->hwndMenu = NULLHANDLE;
+        break; }
+
+        /*
+         * WM_COMMAND:
+         *      this must be from the menu, so
+         *      forward this to the button's owner
+         */
+
+        case WM_COMMAND:
+            WinPostMsg(WinQueryWindow(hwndButton, QW_OWNER),
+                       msg,
+                       mp1,
+                       mp2);
+        break;
+
+        /*
+         * WM_DESTROY:
+         *      clean up allocated data
+         */
+
+        case WM_DESTROY:
+            mrc = (*(pmbd->pfnwpButtonOriginal))(hwndButton, msg, mp1, mp2);
+            free(pmbd);
+        break;
+
+        default:
+            mrc = (*(pmbd->pfnwpButtonOriginal))(hwndButton, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ ctlMakeMenuButton:
+ *      this turns the specified button into a "menu button",
+ *      which shows a popup menu when the button is depressed.
+ *      This is done by subclassing the menu button with
+ *      ctl_fnwpSubclassedMenuButton.
+ *
+ *      Simply call this function upon any button, and it'll
+ *      turn in to a menu button.
+ *
+ *      When the user presses the button, the specified menu
+ *      is loaded from the resources. The button will then
+ *      be set as the owner, but it will forward all WM_COMMAND
+ *      messages from the menu to its own owner (probably your
+ *      dialog), so you can handle WM_COMMAND messages just as
+ *      if the menu was owned by your dialog.
+ *
+ *      Alternatively, if you don't want to load a menu from
+ *      the resources, you can specify idMenu == 0. In that case,
+ *      when the menu button is pressed, it sends (!) a WM_COMMAND
+ *      message to its owner with its ID in mp1 (as usual). The
+ *      difference is that the return value from that message will
+ *      be interpreted as a menu handle (HWND) to use for the button
+ *      menu.
+ *
+ *      The subclassed button also handles menu destruction etc.
+ *      by itself.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL ctlMakeMenuButton(HWND hwndButton,      // in: button to subclass
+                       HMODULE hmodMenu,     // in: resource module (can be NULLHANDLE for
+                                             // current EXE)
+                       ULONG idMenu)         // in: resource menu ID (or 0)
+{
+    BOOL brc = FALSE;
+    PMENUBUTTONDATA pmbd = (PMENUBUTTONDATA)malloc(sizeof(MENUBUTTONDATA));
+    if (pmbd)
+    {
+        memset(pmbd, 0, sizeof(MENUBUTTONDATA));
+        pmbd->pfnwpButtonOriginal = WinSubclassWindow(hwndButton,
+                                                      ctl_fnwpSubclassedMenuButton);
+        if (pmbd->pfnwpButtonOriginal)
+        {
+            pmbd->hmodMenu = hmodMenu;
+            pmbd->idMenu = idMenu;
+            WinSetWindowULong(hwndButton, QWL_USER, (ULONG)pmbd);
+            brc = TRUE;
+        }
+        else
+            free(pmbd);
+    }
+    return (brc);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Static bitmaps
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Subclassed Static Bitmap Control                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctl_fnwpBitmapStatic:
+ *      subclassed wnd proc for both static controls which
+ *      should display icons and stretched bitmaps.
+ *
+ *      This is not a stand-alone window procedure, but must only
+ *      be used with static controls subclassed by the functions
+ *      listed below.
+ *
+ *      This is now (V0.9.0) used in two contexts:
+ *      --  when subclassed from ctlPrepareStaticIcon,
+ *          this displays transparent icons properly
+ *          (for regular icons or icon animations);
+ *      --  when subclassed from ctlPrepareStretchedBitmap,
+ *          this displays a stretched bitmap.
+ *
+ *      The behavior depends on ANIMATIONDATA.ulFlags,
+ *      which is set by both of these functions (either to
+ *      ANF_ICON or ANF_BITMAP).
+ *
+ *      Only one msg is of interest to the "user" application
+ *      of this control, which is SM_SETHANDLE,
+ *      the normal message sent to a static icon control to change
+ *      its icon or bitmap.
+ *      The new handle (HPOINTER or HBITMAP) is
+ *      in mp1; mp2 must be null.
+ *
+ *      Depending on ANIMATIONDATA.ulFlags, we do the following:
+ *
+ *      --  ANF_ICON:  Unfortunately, the
+ *          standard static control paints garbage if
+ *          the icons contain transparent areas, and the
+ *          display often flickers too.
+ *          We improve this by creating one bitmap from
+ *          the icon that we were given which we can then
+ *          simply copy to the screen in one step in
+ *          WM_PAINT.
+ *
+ *      --  ANF_BITMAP: we create a bitmap which is
+ *          stretched to the size of the static control,
+ *          using gpihStretchBitmap.
+ *
+ *      In any case, a new bitmap is produced internally and
+ *      stored so that it can easily be painted when WM_PAINT
+ *      comes in. The source icon/bitmap can therefore
+ *      be safely destroyed by the caller after sending
+ *      SM_SETHANDLE. This bitmap is automatically deleted when
+ *      the window is destroyed.
+ *
+ *@@changed V0.9.0 [umoeller]: function renamed
+ *@@changed V0.9.0 [umoeller]: added support for stretched bitmaps
+ *@@changed V0.9.0 [umoeller]: added halftoned display for WS_DISABLED
+ *@@changed V0.9.0 [umoeller]: exported gpihIcon2Bitmap function to gpih.c
+ *@@changed V0.9.0 [umoeller]: fixed paint errors when SM_SETHANDLE had NULL argument in mp1
+ */
+
+MRESULT EXPENTRY ctl_fnwpBitmapStatic(HWND hwndStatic, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    PANIMATIONDATA pa = (PANIMATIONDATA)WinQueryWindowULong(hwndStatic, QWL_USER);
+                // animation data which was stored in window words
+    PFNWP   OldStaticProc = NULL;
+    MRESULT mrc = NULL;
+
+    if (pa)
+    {
+        OldStaticProc = pa->OldStaticProc;
+
+        switch(msg)
+        {
+            /*
+             * WM_TIMER:
+             *      this timer is started by the ctl* funcs
+             *      below. Proceed to the next animation step.
+             *
+             *      Note: this timer is only used with
+             *      ANF_ICON, and even then, it
+             *      is not necessarily running.
+             */
+
+            case WM_TIMER: {
+                pa->usAniCurrent++;
+                if (pa->usAniCurrent >= pa->usAniCount)
+                    pa->usAniCurrent = 0;
+
+                WinSendMsg(hwndStatic,
+                        SM_SETHANDLE,
+                        (MPARAM)pa->ahptrAniIcons[pa->usAniCurrent],
+                        (MPARAM)NULL);
+            break; }
+
+            /*
+             * SM_SETHANDLE:
+             *
+             */
+
+            case SM_SETHANDLE:
+            {
+                HDC hdcMem;
+                HPS hpsMem;
+                SIZEL szlPage;
+
+                LONG lBkgndColor = winhQueryPresColor(
+                                WinQueryWindow(hwndStatic, QW_PARENT),
+                                PP_BACKGROUNDCOLOR,
+                                FALSE,
+                                SYSCLR_DIALOGBACKGROUND);
+
+                HPS hps = WinGetPS(hwndStatic);
+
+                // if we have created bitmaps previously, delete them
+                if (pa->hbm)
+                {
+                    GpiDeleteBitmap(pa->hbm);
+                    pa->hbm = NULLHANDLE;
+                }
+                if (pa->hbmHalftoned)
+                {
+                    GpiDeleteBitmap(pa->hbmHalftoned);
+                    pa->hbmHalftoned = NULLHANDLE;
+                }
+
+                // switch to RGB mode
+                gpihSwitchToRGB(hps);
+
+                // create a memory PS
+                szlPage.cx = pa->rclIcon.xRight - pa->rclIcon.xLeft;
+                szlPage.cy = pa->rclIcon.yTop - pa->rclIcon.yBottom;
+                if (gpihCreateMemPS(pa->hab, &szlPage, &hdcMem, &hpsMem))
+                {
+                    // switch the memory PS to RGB mode too
+                    gpihSwitchToRGB(hpsMem);
+
+                    // create a suitable bitmap w/ the size of the
+                    // static control
+                    if ((pa->hbm = gpihCreateBitmap(hpsMem,
+                                                    szlPage.cx,
+                                                    szlPage.cy)))
+                    {
+                        // associate the bit map with the memory PS
+                        if (GpiSetBitmap(hpsMem, pa->hbm) != HBM_ERROR)
+                        {
+                            // fill the bitmap with the current static
+                            // background color
+                            POINTL ptl = {0, 0};
+                            GpiMove(hpsMem, &ptl);
+                            ptl.x = pa->rclIcon.xRight;
+                            ptl.y = pa->rclIcon.yTop;
+                            GpiSetColor(hpsMem,
+                                    lBkgndColor);
+                            GpiBox(hpsMem,
+                                    DRO_FILL, // interior only
+                                    &ptl,
+                                    0, 0);    // no corner rounding
+
+                            /*
+                             * ANF_ICON:
+                             *
+                             */
+
+                            if (pa->ulFlags & ANF_ICON)
+                            {
+                                // store new icon in our own structure
+                                pa->hptr = (HPOINTER)mp1;
+
+                                if (pa->hptr)
+                                    // paint icon into bitmap
+                                    gpihIcon2Bitmap(hpsMem,
+                                                    pa->hptr,
+                                                    lBkgndColor,
+                                                    WinQuerySysValue(HWND_DESKTOP, SV_CXICON));
+
+                            } // end if (pa->ulFlags & ANF_ICON)
+
+                            /*
+                             * ANF_BITMAP:
+                             *
+                             */
+
+                            else if (pa->ulFlags & ANF_BITMAP)
+                            {
+                                // store passed bitmap
+                                HBITMAP     hbmSource = (HBITMAP)mp1;
+
+                                if (hbmSource)
+                                    gpihStretchBitmap(hpsMem,
+                                                      hbmSource,
+                                                      NULL,     // use size of bitmap
+                                                      &(pa->rclIcon),
+                                                      ((pa->ulFlags & ANF_PROPORTIONAL)
+                                                            != 0));
+
+                            } // end if (pa->ulFlags & ANF_BITMAP)
+
+                        } // end if (GpiSetBitmap(...
+                    } // if (pa->hbm = gpihCreateBitmap(hpsMem, &(pa->rclIcon)))
+
+                    // in any case, clean up now
+                    GpiDestroyPS(hpsMem);
+                    DevCloseDC(hdcMem);
+                } // end if (gpihCreateMemPS(hab, &hdcMem, &hpsMem))
+
+                WinReleasePS(hps);
+
+                // enforce WM_PAINT
+                WinInvalidateRect(hwndStatic, NULL, FALSE);
+
+            break; }
+
+            /*
+             * WM_PAINT:
+             *      "normal" paint; this only arrives here if the
+             *      icon needs to be repainted. We simply paint
+             *      the bitmap we created in WM_SETHANDLE.
+             */
+
+            case WM_PAINT: {
+                RECTL rcl;
+                HPS hps = WinBeginPaint(hwndStatic, NULLHANDLE, &rcl);
+                POINTL ptl = {0, 0};
+
+                if (pa->hbm)
+                {
+                    if (WinQueryWindowULong(hwndStatic, QWL_STYLE)
+                            & WS_DISABLED)
+                    {
+                        // if the control is currently disabled,
+                        // draw in half-toned (grayed)
+
+                        LONG lBkgndColor = winhQueryPresColor(
+                                        WinQueryWindow(hwndStatic, QW_PARENT),
+                                        PP_BACKGROUNDCOLOR,
+                                        FALSE,
+                                        SYSCLR_DIALOGBACKGROUND);
+
+                        // 1) check if we created the half-tone
+                        // bitmap already (WinDrawBitmap doesn't
+                        // work with half-toned color bitmaps, so
+                        // here's yet another thing we have to do
+                        // all alone)
+                        if (pa->hbmHalftoned == NULLHANDLE)
+                            pa->hbmHalftoned = gpihCreateHalftonedBitmap(pa->hab,
+                                                                         pa->hbm,
+                                                                         lBkgndColor);
+
+                        if (pa->hbmHalftoned)
+                            WinDrawBitmap(hps,
+                                          pa->hbmHalftoned,
+                                          NULL,     // whole bmp
+                                          &ptl,     // lower left corner
+                                          0, 0,     // no colors
+                                          DBM_NORMAL);
+                    }
+                    else
+                    {
+                        // not disabled: draw regular bitmap
+                        // we created previously
+                        // draw the bitmap that we created previously
+                        WinDrawBitmap(hps,
+                                      pa->hbm,
+                                      NULL,     // whole bmp
+                                      &ptl,     // lower left corner
+                                      0, 0,     // no colors
+                                      DBM_NORMAL);
+                    }
+                }
+
+                WinEndPaint(hps);
+            break; }
+
+            /*
+             * WM_DESTROY:
+             *      clean up.
+             */
+
+            case WM_DESTROY:
+            {
+                // undo subclassing in case more WM_TIMERs come in
+                WinSubclassWindow(hwndStatic, OldStaticProc);
+
+                if (pa->hbm)
+                    GpiDeleteBitmap(pa->hbm);
+                if (pa->hbmHalftoned)
+                    GpiDeleteBitmap(pa->hbm);
+
+                // clean up ANIMATIONDATA struct
+                WinSetWindowULong(hwndStatic, QWL_USER, (ULONG)NULL);
+                free(pa);
+
+                mrc = OldStaticProc(hwndStatic, msg, mp1, mp2);
+            break; }
+
+            default:
+                mrc = OldStaticProc(hwndStatic, msg, mp1, mp2);
+       }
+    }
+    return (mrc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Icon animation                                                 *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctlPrepareStaticIcon:
+ *      turns a static control into one which properly
+ *      displays transparent icons when given a
+ *      SM_SETHANDLE msg.
+ *      This is done by subclassing the static control
+ *      with ctl_fnwpBitmapStatic.
+ *
+ *      This function gets called automatically by
+ *      ctlPrepareAnimation, so you need not call it
+ *      independently for animations. See the notes there
+ *      how this can be done.
+ *
+ *      You can, however, call this function if you
+ *      have some static control with transparent icons
+ *      which is not animated but changes sometimes
+ *      (because you have the same repaint problems there).
+ *
+ *      This func does _not_ start an animation yet.
+ *
+ *      To change the icon being displayed, send the control
+ *      a SM_SETHANDLE msg with the icon handle in (HPOINTER)mp1.
+ *
+ *      Returns a PANIMATIONDATA if subclassing succeeded or
+ *      NULL upon errors. If you only call this function, you
+ *      do not need this structure; it is needed by
+ *      ctlPrepareAnimation though.
+ *
+ *      The subclassed static icon func above automatically
+ *      cleans up resources, so you don't have to worry about
+ *      that either.
+ *
+ *@@changed V0.9.0 [umoeller]: adjusted for ctl_fnwpBitmapStatic changes
+ */
+
+PANIMATIONDATA ctlPrepareStaticIcon(HWND hwndStatic,
+                                    USHORT usAnimCount) // needed for allocating extra memory,
+                                                        // this must be at least 1
+{
+    PANIMATIONDATA pa = NULL;
+    PFNWP OldStaticProc = WinSubclassWindow(hwndStatic, ctl_fnwpBitmapStatic);
+    if (OldStaticProc)
+    {
+        // create the ANIMATIONDATA structure,
+        // initialize some fields,
+        // and store it in QWL_USER of the static control
+        ULONG   cbStruct =   sizeof(ANIMATIONDATA)
+                           + ((usAnimCount-1) * sizeof(HPOINTER));
+        pa = (PANIMATIONDATA)malloc(cbStruct);
+        memset(pa, 0, cbStruct);
+
+        // switch static to icon mode
+        pa->ulFlags = ANF_ICON;
+        pa->OldStaticProc = OldStaticProc;
+        WinQueryWindowRect(hwndStatic, &(pa->rclIcon));
+        pa->hab = WinQueryAnchorBlock(hwndStatic);
+
+        WinSetWindowULong(hwndStatic, QWL_USER, (ULONG)pa);
+    }
+    return (pa);
+}
+
+/*
+ *@@ ctlPrepareAnimation:
+ *      this function turns a regular static icon
+ *      control an animated one. This is the one-shot
+ *      function for animating static icon controls.
+ *
+ *      It calls ctlPrepareStaticIcon first, then uses
+ *      the passed parameters to prepare an animation:
+ *
+ *      pahptr must point to an array of HPOINTERs
+ *      which must contain usAnimCount icon handles.
+ *      If (fStartAnimation == TRUE), the animation
+ *      is started already. Otherwise you'll have
+ *      to call ctlStartAnimation yourself.
+ *
+ *      To create an animated icon, simply create a static
+ *      control with the dialog editor (can be any static,
+ *      e.g. a text control). Then do the following in your code:
+ *
+ *      1) load the dlg box template (using WinLoadDlg);
+ *
+ *      2) load all the icons for the animation in an array of
+ *         HPOINTERs (check xsdLoadAnimation in shutdown.c for an
+ *         example);
+ *
+ *      3) call ctlPrepareAnimation, e.g.:
+ +             ctlPrepareAnimation(WinWindowFromID(hwndDlg, ID_ICON), // get icon hwnd
+ +                             8,                   // no. of icons for the anim
+ +                             &ahptr[0],           // ptr to first icon in the array
+ +                             150,                 // delay
+ +                             TRUE);               // start animation now
+ *
+ *      4) call WinProcessDlg(hwndDlg);
+ *
+ *      5) stop the animation, e.g.:
+ +              ctlStopAnimation(WinWindowFromID(hwndDlg, ID_ICON));
+ *
+ *      6) destroy the dlg window;
+ *
+ *      7) free all the HPOINTERS loaded above (check xsdFreeAnimation
+ *         in shutdown.c for an example).
+ *
+ *      When the icon control is destroyed, the
+ *      subclassed window proc (ctl_fnwpBitmapStatic)
+ *      automatically cleans up resources. However,
+ *      the icons in *pahptr are not freed.
+ */
+
+BOOL ctlPrepareAnimation(HWND hwndStatic,   // icon hwnd
+                         USHORT usAnimCount,         // no. of anim steps
+                         HPOINTER *pahptr,           // array of HPOINTERs
+                         ULONG ulDelay,              // delay per anim step (in ms)
+                         BOOL fStartAnimation)       // TRUE: start animation now
+{
+    PANIMATIONDATA paNew = ctlPrepareStaticIcon(hwndStatic, usAnimCount);
+    if (paNew)
+    {
+        paNew->ulDelay = ulDelay;
+        // paNew->OldStaticProc already set
+        paNew->hptr = NULLHANDLE;
+        // paNew->rclIcon already set
+        paNew->usAniCurrent = 0;
+        paNew->usAniCount = usAnimCount;
+        memcpy(&(paNew->ahptrAniIcons), pahptr,
+                        (usAnimCount * sizeof(HPOINTER)));
+
+        if (fStartAnimation) {
+            WinStartTimer(WinQueryAnchorBlock(hwndStatic), hwndStatic,
+                    1, ulDelay);
+            WinPostMsg(hwndStatic, WM_TIMER, NULL, NULL);
+        }
+    }
+    return (paNew != NULL);
+}
+
+/*
+ *@@ ctlStartAnimation:
+ *      starts an animation that not currently running. You
+ *      must prepare the animation by calling ctlPrepareAnimation
+ *      first.
+ */
+
+BOOL ctlStartAnimation(HWND hwndStatic)
+{
+    BOOL brc = FALSE;
+    PANIMATIONDATA pa = (PANIMATIONDATA)WinQueryWindowULong(hwndStatic, QWL_USER);
+    if (pa)
+        if (WinStartTimer(WinQueryAnchorBlock(hwndStatic), hwndStatic,
+                        1, pa->ulDelay))
+        {
+            brc = TRUE;
+            WinPostMsg(hwndStatic, WM_TIMER, NULL, NULL);
+        }
+    return (brc);
+}
+
+/*
+ *@@ ctlStopAnimation:
+ *      stops an animation that is currently running.
+ *      This does not free any resources.
+ */
+
+BOOL ctlStopAnimation(HWND hwndStatic)
+{
+    return (WinStopTimer(WinQueryAnchorBlock(hwndStatic), hwndStatic, 1));
+}
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Stretched bitmaps
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Bitmap functions                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctlPrepareStretchedBitmap:
+ *      turns a static control into a bitmap control
+ *      which stretches the bitmap to the size of the
+ *      control when given an SM_SETHANDLE msg.
+ *
+ *      If (fPreserveProportions == TRUE), the control
+ *      will size the bitmap proportionally only. Otherwise,
+ *      it will always stretch the bitmap to the whole
+ *      size of the static control (possibly distorting
+ *      the proportions). See gpihStretchBitmap for
+ *      details.
+ *
+ *      This function subclasses the static control
+ *      with ctl_fnwpBitmapStatic, setting the flags as
+ *      necessary. However, this does not set the bitmap
+ *      to display yet.
+ *      To have a bitmap displayed, send the control
+ *      a SM_SETHANDLE message with the bitmap to be
+ *      displayed in (HBITMAP)mp1 after having used this
+ *      function.
+ *
+ *      ctl_fnwpBitmapStatic will then create a private
+ *      copy of that bitmap, stretched to its own size.
+ *      You can therefore delete the "source" bitmap
+ *      after sending SM_SETHANDLE.
+ *
+ *      You can also send SM_SETHANDLE several times.
+ *      The control will then readjust itself and delete
+ *      its old bitmap.
+ *      But note that GpiWCBitBlt is invoked on that new
+ *      bitmap with every new message, so this is not
+ *      terribly fast.
+ *
+ *      Also note that you should not resize the static
+ *      control after creation, because the bitmap will
+ *      _not_ be adjusted.
+ *
+ *      This returns a PANIMATIONDATA if subclassing succeeded or
+ *      NULL upon errors. You do not need to save this structure;
+ *      it is cleaned up automatically when the control is destroyed.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PANIMATIONDATA ctlPrepareStretchedBitmap(HWND hwndStatic,
+                                         BOOL fPreserveProportions)
+{
+    PANIMATIONDATA pa = NULL;
+    PFNWP OldStaticProc = WinSubclassWindow(hwndStatic, ctl_fnwpBitmapStatic);
+    if (OldStaticProc)
+    {
+        // create the ANIMATIONDATA structure,
+        // initialize some fields,
+        // and store it in QWL_USER of the static control
+        ULONG   cbStruct =   sizeof(ANIMATIONDATA);
+        pa = (PANIMATIONDATA)malloc(cbStruct);
+        memset(pa, 0, cbStruct);
+
+        // switch static to bitmap mode
+        pa->ulFlags = ANF_BITMAP;
+        if (fPreserveProportions)
+            pa->ulFlags |= ANF_PROPORTIONAL;
+
+        pa->OldStaticProc = OldStaticProc;
+        WinQueryWindowRect(hwndStatic, &(pa->rclIcon));
+        pa->hab = WinQueryAnchorBlock(hwndStatic);
+
+        WinSetWindowULong(hwndStatic, QWL_USER, (ULONG)pa);
+    }
+    return (pa);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Hotkey entry field
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Hotkey entry field                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ ctl_fnwpHotkeyEntryField:
+ *      this is the window proc for a subclassed entry
+ *      field which displays a description of a keyboard
+ *      combination upon receiving WM_CHAR.
+ *
+ *      Use ctlMakeHotkeyEntryField to turn any existing
+ *      entry field into such an "hotkey entry field".
+ *
+ *      The entry field is deleted on every WM_CHAR that
+ *      comes in.
+ *
+ *      For this to work, this window proc needs the
+ *      cooperation of its owner. Whenever a WM_CHAR
+ *      message is received by the entry field which
+ *      looks like a meaningful key or key combination
+ *      (some filtering is done by this func), then
+ *      the owner of the entry field receives a WM_CONTROL
+ *      message with the new EN_HOTKEY notification code
+ *      (see comctl.h for details).
+ *
+ *      It is then the responsibility of the owner to
+ *      check whether the HOTKEYNOTIFY structure pointed
+ *      to by WM_CONTROL's mp2 is a meaningful keyboard
+ *      combination.
+ *
+ *      If not, the owner must return FALSE, and the
+ *      entry field's contents are deleted.
+ *
+ *      If yes however, the owner must fill the szDescription
+ *      field with a description of the keyboard combination
+ *      (e.g. "Shift+Alt+C") and return TRUE. The entry field
+ *      will then be set to that description.
+ *
+ *@@added V0.9.1 (99-12-19) [umoeller]
+ */
+
+MRESULT EXPENTRY ctl_fnwpObjectHotkeyEntryField(HWND hwndEdit, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = (MPARAM)FALSE; // WM_CHAR not-processed flag
+
+    // get object window data; this was stored in QWL_USER by
+    // WM_INITDLG of obj_fnwpSettingsObjDetails
+    // PXFOBJWINDATA   pWinData = (PXFOBJWINDATA)WinQueryWindowULong(hwndEdit, QWL_USER);
+    PFNWP           pfnwpOrig = (PFNWP)WinQueryWindowULong(hwndEdit, QWL_USER);
+
+    switch (msg)
+    {
+        case WM_CHAR:
+        {
+            /*
+                  #define KC_CHAR                    0x0001
+                  #define KC_VIRTUALKEY              0x0002
+                  #define KC_SCANCODE                0x0004
+                  #define KC_SHIFT                   0x0008
+                  #define KC_CTRL                    0x0010
+                  #define KC_ALT                     0x0020
+                  #define KC_KEYUP                   0x0040
+                  #define KC_PREVDOWN                0x0080
+                  #define KC_LONEKEY                 0x0100
+                  #define KC_DEADKEY                 0x0200
+                  #define KC_COMPOSITE               0x0400
+                  #define KC_INVALIDCOMP             0x0800
+            */
+
+            /*
+                Examples:           usFlags  usKeyCode
+                    F3                02       22
+                    Ctrl+F4           12       23
+                    Ctrl+Shift+F4     1a       23
+                    Ctrl              12       0a
+                    Alt               22       0b
+                    Shift             0a       09
+            */
+
+            // USHORT usCommand;
+            // USHORT usKeyCode;
+            USHORT usFlags    = SHORT1FROMMP(mp1);
+            // UCHAR  ucRepeat   = CHAR3FROMMP(mp1);
+            UCHAR  ucScanCode = CHAR4FROMMP(mp1);
+            USHORT usch       = SHORT1FROMMP(mp2);
+            USHORT usvk       = SHORT2FROMMP(mp2);
+
+            if (
+                    // process only key-down messages
+                    ((usFlags & KC_KEYUP) == 0)
+                    // check flags:
+                    // filter out those ugly composite key (accents etc.)
+               &&   ((usFlags & (KC_DEADKEY | KC_COMPOSITE | KC_INVALIDCOMP)) == 0)
+               )
+            {
+                HOTKEYNOTIFY    hkn;
+                ULONG           flReturned;
+                HWND            hwndOwner = WinQueryWindow(hwndEdit, QW_OWNER);
+
+                usFlags &= (KC_VIRTUALKEY | KC_CTRL | KC_ALT | KC_SHIFT);
+
+                hkn.usFlags = usFlags;
+                hkn.usvk = usvk;
+                hkn.usch = usch;
+                hkn.ucScanCode = ucScanCode;
+
+                if (usFlags & KC_VIRTUALKEY)
+                    hkn.usKeyCode = usvk;
+                else
+                    hkn.usKeyCode = usch;
+
+                hkn.szDescription[0] = 0;
+
+                flReturned = (ULONG)WinSendMsg(hwndOwner,
+                                          WM_CONTROL,
+                                          MPFROM2SHORT(WinQueryWindowUShort(hwndEdit,
+                                                                            QWS_ID),
+                                                       EN_HOTKEY),  // new notification code
+                                          (MPARAM)&hkn);
+                if (flReturned & HEFL_SETTEXT)
+                    WinSetWindowText(hwndEdit, hkn.szDescription);
+                else
+                    WinSetWindowText(hwndEdit, "");
+
+                if (flReturned & HEFL_FORWARD2OWNER)
+                    WinPostMsg(hwndOwner,
+                               WM_CHAR,
+                               mp1, mp2);
+
+                mrc = (MPARAM)TRUE; // WM_CHAR processed flag;
+            }
+        break; }
+
+        default:
+            mrc = pfnwpOrig(hwndEdit, msg, mp1, mp2);
+    }
+    return (mrc);
+}
+
+/*
+ *@@ ctlMakeHotkeyEntryField:
+ *      this turns any entry field into a "hotkey entry field"
+ *      by subclassing it with ctl_fnwpObjectHotkeyEntryField.
+ *      See remarks there for details.
+ *
+ *      Returns FALSE upon errors, TRUE otherwise.
+ *
+ *@@added V0.9.1 (99-12-19) [umoeller]
+ */
+
+BOOL ctlMakeHotkeyEntryField(HWND hwndHotkeyEntryField)
+{
+    PFNWP pfnwpOrig = WinSubclassWindow(hwndHotkeyEntryField,
+                                        ctl_fnwpObjectHotkeyEntryField);
+    if (pfnwpOrig)
+    {
+        WinSetWindowPtr(hwndHotkeyEntryField, QWL_USER, (PVOID)pfnwpOrig);
+        return (TRUE);
+    }
+
+    return (FALSE);
+}
+
Index: /trunk/src/helpers/datetime.c
===================================================================
--- /trunk/src/helpers/datetime.c	(revision 8)
+++ /trunk/src/helpers/datetime.c	(revision 8)
@@ -0,0 +1,239 @@
+
+/*
+ *@@sourcefile datetime.c:
+ *      contains various date and time helper functions.
+ *      Some functions in here are OS/2-specific, others
+ *      are plain C code.
+ *
+ *      Functions marked with (C) Ray Gardner are from
+ *      "scaldate.c":
+ *          scalar date routines    --    public domain by Ray Gardner
+ *          These will work over the range 1/01/01 thru 14699/12/31
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  dat*   date/time helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\datetime.h"
+ *@@added V0.9.0 [umoeller]
+ */
+
+/*
+ *      This file Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#include <os2.h>
+
+#include <stdio.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\datetime.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\C helpers\Date/time helpers
+ */
+
+/*******************************************************************
+ *                                                                 *
+ *  Private declarations                                           *
+ *                                                                 *
+ ******************************************************************/
+
+const char  *pcszFormatTimestamp = "%4u%02u%02u%02u%02u%02u%";
+
+/*
+ *@@ dtGetULongTime:
+ *      this returns the current time as a ULONG value (in milliseconds).
+ *      Useful for stopping how much time the machine has spent in
+ *      a certain function. To do this, simply call this function twice,
+ *      and subtract the two values, which will give you the execution
+ *      time in milliseconds.
+ *
+ *      Warning: this does not handle day information. When called twice
+ *      in between day changes, this returns incorrect information.
+ */
+
+ULONG dtGetULongTime(VOID)
+{
+    DATETIME    dt;
+    DosGetDateTime(&dt);
+    return (10*(dt.hundredths + 100*(dt.seconds + 60*(dt.minutes + 60*(dt.hours)))));
+}
+
+/*
+ *@@ dtCreateFileTimeStamp:
+ *      this creates a time stamp string in pszTimeStamp
+ *      from the given FDATE and FTIME structures (which
+ *      are, for example, used in the FILESTATUS3 structure
+ *      returned by DosQueryPathInfo).
+ *
+ *      The time stamp string is exactly 15 bytes in length
+ *      (including the terminating null byte) and has the
+ *      following format:
+ +          YYYYMMDDhhmmss
+ *      (being year, mondth, day, hours, minutes, seconds).
+ *      Your buffer must be large enough for that, this is
+ *      not checked.
+ *
+ *      This time stamp can be used to compare two dates
+ *      simply by calling strcmp.
+ *
+ *      Note that since FTIME only has a two-seconds resolution,
+ *      the seconds part of the time stamp will have that too.
+ *
+ *      This returns the string length (excluding the null
+ *      terminator), which should be 14.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+int dtCreateFileTimeStamp(PSZ pszTimeStamp,      // out: time stamp
+                            FDATE* pfdate,         // in: date
+                            FTIME* pftime)         // in: time
+{
+    return (sprintf(pszTimeStamp,
+                    pcszFormatTimestamp,
+                    pfdate->year + 1980,
+                    pfdate->month,
+                    pfdate->day,
+                    pftime->hours,
+                    pftime->minutes,
+                    pftime->twosecs * 2));
+}
+
+/*
+ *@@ dtCreateDosTimeStamp:
+ *      just like dtCreateFileTimeStamp,
+ *      except that this takes a DATETIME
+ *      structure as input. The time stamp
+ *      string is exactly the same.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+int dtCreateDosTimeStamp(PSZ pszTimeStamp,
+                         DATETIME* pdt)
+{
+    return (sprintf(pszTimeStamp,
+                    pcszFormatTimestamp,
+                    pdt->year,
+                    pdt->month,
+                    pdt->day,
+                    pdt->hours,
+                    pdt->minutes,
+                    pdt->seconds));
+}
+
+/*
+ *@@ dtIsLeapYear:
+ *      returns TRUE if yr is a leap year.
+ *
+ *      (c) Ray Gardner.
+ */
+
+int dtIsLeapYear(unsigned yr)
+{
+   return (    (yr % 400 == 0)
+            || (    (yr % 4 == 0)
+                 && (yr % 100 != 0)
+               )
+          );
+}
+
+/*
+ *@@ dtMonths2Days:
+ *      returns the no. of days for month.
+ *
+ *      (c) Ray Gardner.
+ */
+
+unsigned dtMonths2Days(unsigned month)
+{
+   return (month * 3057 - 3007) / 100;
+}
+
+/*
+ *@@ dtYears2Days:
+ *      converts a year to the no. of days passed.
+ *
+ *      (c) Ray Gardner.
+ */
+
+long dtYears2Days (unsigned yr)
+{
+   return ( yr * 365L
+          + yr / 4
+          - yr / 100
+          + yr / 400);
+}
+
+/*
+ *@@ dtDate2Scalar:
+ *      returns a scalar (i.e. the no. of days) for
+ *      the given date.
+ *
+ *      (c) Ray Gardner.
+ */
+
+long dtDate2Scalar(unsigned yr,     // in: year     (e.g. 1999)
+                   unsigned mo,     // in: month    (1-12)
+                   unsigned day)    // in: day      (1-31)
+{
+   long scalar;
+   scalar = day + dtMonths2Days(mo);
+   if ( mo > 2 )                         /* adjust if past February */
+      scalar -= dtIsLeapYear(yr) ? 1 : 2;
+   yr--;
+   scalar += dtYears2Days(yr);
+   return scalar;
+}
+
+/*
+ *@@ dtScalar2Date:
+ *
+ *
+ *      (c) Ray Gardner.
+ */
+
+void dtScalar2Date(long scalar,     // in: date scalar
+                   unsigned *pyr,   // out: year    (e.g. 1999)
+                   unsigned *pmo,   // out: month   (1-12)
+                   unsigned *pday)  // out: day     (1-31)
+{
+   unsigned n;                /* compute inverse of dtYears2Days() */
+
+   for ( n = (unsigned)((scalar * 400L) / 146097); dtYears2Days(n) < scalar;)
+      n++;                          /* 146097 == dtYears2Days(400) */
+   *pyr = n;
+   n = (unsigned)(scalar - dtYears2Days(n-1));
+   if ( n > 59 ) {                       /* adjust if past February */
+      n += 2;
+      if ( dtIsLeapYear(*pyr) )
+         n -= n > 62 ? 1 : 2;
+   }
+   *pmo = (n * 100 + 3007) / 3057;  /* inverse of dtMonths2Days() */
+   *pday = n - dtMonths2Days(*pmo);
+}
+
+
Index: /trunk/src/helpers/debug.c
===================================================================
--- /trunk/src/helpers/debug.c	(revision 8)
+++ /trunk/src/helpers/debug.c	(revision 8)
@@ -0,0 +1,1958 @@
+
+/*
+ *@@sourcefile debug.c:
+ *      this file contains debugging functions for the
+ *      exception handlers in except.c.
+ *
+ *      This code is capable of unwinding the stack from
+ *      a given address and trying to get function names
+ *      and source line numbers, either from the respective
+ *      module's debug code (if present) or from a SYM file,
+ *      which is searched for in the directory of the module
+ *      or in ?:\OS2\PDPSI\PMDF\WARP4.
+ *
+ *      This file incorporates code from the following:
+ *      -- Marc Fiammante, John Currier, Kim Rasmussen,
+ *         Anthony Cruise (EXCEPT3.ZIP package for a generic
+ *         exception handling DLL, available at Hobbes).
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@changed V0.9.0 [umoeller]: made some declarations C++-compatible
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: greatly cleaned up this file
+ *
+ *@@header "helpers\debug.h"
+ */
+
+/*
+ *      This file Copyright (C) 1992-99 Ulrich Mller,
+ *                                      Kim Rasmussen,
+ *                                      Marc Fiammante,
+ *                                      John Currier,
+ *                                      Anthony Cruise.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_BASE
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#define DONT_REPLACE_MALLOC
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\debug.h"
+#include "helpers\dosh.h"
+
+#pragma hdrstop
+
+#include <fcntl.h>
+#ifdef __EMX__
+    #include <sys\types.h>      // required for sys\stat.h; UM 99-10-22
+#endif
+#include <sys\stat.h>
+#include <share.h>
+#include <io.h>
+
+#ifndef DWORD
+#define DWORD unsigned long
+#endif
+#ifndef WORD
+#define WORD  unsigned short
+#endif
+
+#pragma stack16(512)
+#define HF_STDERR 2
+
+/*
+ *@@category: Helpers\Control program helpers\Exceptions/debugging
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+// this specifies whether we're dealing with 32-bit code;
+// this gets changed whenever 16-bit count is detected
+static BOOL     f32bit = TRUE;
+
+/*
+ * Global variables for Read32PmDebug:
+ *
+ */
+
+ULONG           func_ofs;
+ULONG           pubfunc_ofs;
+char            func_name[128];
+ULONG           var_ofs = 0;
+
+struct {
+    BYTE            name[128];
+    ULONG           stack_offset;
+    USHORT          type_idx;
+} autovar_def[100];
+
+#pragma pack(1)
+
+BYTE           *type_name[] =
+{
+    "8 bit signed                     ",
+    "16 bit signed                    ",
+    "32 bit signed                    ",
+    "Unknown (0x83)                   ",
+    "8 bit unsigned                   ",
+    "16 bit unsigned                  ",
+    "32 bit unsigned                  ",
+    "Unknown (0x87)                   ",
+    "32 bit real                      ",
+    "64 bit real                      ",
+    "80 bit real                      ",
+    "Unknown (0x8B)                   ",
+    "64 bit complex                   ",
+    "128 bit complex                  ",
+    "160 bit complex                  ",
+    "Unknown (0x8F)                   ",
+    "8 bit boolean                    ",
+    "16 bit boolean                   ",
+    "32 bit boolean                   ",
+    "Unknown (0x93)                   ",
+    "8 bit character                  ",
+    "16 bit characters                ",
+    "32 bit characters                ",
+    "void                             ",
+    "15 bit unsigned                  ",
+    "24 bit unsigned                  ",
+    "31 bit unsigned                  ",
+    "Unknown (0x9B)                   ",
+    "Unknown (0x9C)                   ",
+    "Unknown (0x9D)                   ",
+    "Unknown (0x9E)                   ",
+    "Unknown (0x9F)                   ",
+    "near pointer to 8 bit signed     ",
+    "near pointer to 16 bit signed    ",
+    "near pointer to 32 bit signed    ",
+    "Unknown (0xA3)                   ",
+    "near pointer to 8 bit unsigned   ",
+    "near pointer to 16 bit unsigned  ",
+    "near pointer to 32 bit unsigned  ",
+    "Unknown (0xA7)                   ",
+    "near pointer to 32 bit real      ",
+    "near pointer to 64 bit real      ",
+    "near pointer to 80 bit real      ",
+    "Unknown (0xAB)                   ",
+    "near pointer to 64 bit complex   ",
+    "near pointer to 128 bit complex  ",
+    "near pointer to 160 bit complex  ",
+    "Unknown (0xAF)                   ",
+    "near pointer to 8 bit boolean    ",
+    "near pointer to 16 bit boolean   ",
+    "near pointer to 32 bit boolean   ",
+    "Unknown (0xB3)                   ",
+    "near pointer to 8 bit character  ",
+    "near pointer to 16 bit characters",
+    "near pointer to 32 bit characters",
+    "near pointer to void             ",
+    "near pointer to 15 bit unsigned  ",
+    "near pointer to 24 bit unsigned  ",
+    "near pointer to 31 bit unsigned  ",
+    "Unknown (0xBB)                   ",
+    "Unknown (0xBC)                   ",
+    "Unknown (0xBD)                   ",
+    "Unknown (0xBE)                   ",
+    "Unknown (0xBF)                   ",
+    "far pointer to 8 bit signed      ",
+    "far pointer to 16 bit signed     ",
+    "far pointer to 32 bit signed     ",
+    "Unknown (0xC3)                   ",
+    "far pointer to 8 bit unsigned    ",
+    "far pointer to 16 bit unsigned   ",
+    "far pointer to 32 bit unsigned   ",
+    "Unknown (0xC7)                   ",
+    "far pointer to 32 bit real       ",
+    "far pointer to 64 bit real       ",
+    "far pointer to 80 bit real       ",
+    "Unknown (0xCB)                   ",
+    "far pointer to 64 bit complex    ",
+    "far pointer to 128 bit complex   ",
+    "far pointer to 160 bit complex   ",
+    "Unknown (0xCF)                   ",
+    "far pointer to 8 bit boolean     ",
+    "far pointer to 16 bit boolean    ",
+    "far pointer to 32 bit boolean    ",
+    "Unknown (0xD3)                   ",
+    "far pointer to 8 bit character   ",
+    "far pointer to 16 bit characters ",
+    "far pointer to 32 bit characters ",
+    "far pointer to void              ",
+    "far pointer to 15 bit unsigned   ",
+    "far pointer to 24 bit unsigned   ",
+    "far pointer to 31 bit unsigned   ",
+};
+
+// Thanks to John Currier:
+// Do not call 16 bit code in myHandler function to prevent call
+// to __EDCThunkProlog and problems is guard page exception handling
+// Also reduce the stack size to 1K for true 16 bit calls.
+// 16 bit calls thunk will now only occur on fatal exceptions
+#pragma stack16(1024)
+
+// ------------------------------------------------------------------
+// Last 8 bytes of 16:16 file when CODEVIEW debugging info is present
+#pragma pack(1)
+struct _eodbug
+{
+    unsigned short  dbug;       // 'NB' signature
+    unsigned short  ver;        // version
+    unsigned long   dfaBase;    // size of codeview info
+} G_eodbug;
+
+#define         DBUGSIG         0x424E
+#define         SSTMODULES      0x0101
+#define         SSTPUBLICS      0x0102
+#define         SSTTYPES        0x0103
+#define         SSTSYMBOLS      0x0104
+#define         SSTSRCLINES     0x0105
+#define         SSTLIBRARIES    0x0106
+#define         SSTSRCLINES2    0x0109
+#define         SSTSRCLINES32   0x010B
+
+typedef struct _SYMBASE
+{
+    unsigned short  dbug;       // 'NB' signature
+    unsigned short  ver;        // version
+    unsigned long   lfoDir;     // file offset to dir entries
+} SYMBASE;
+
+typedef struct _SSDIR
+{
+    unsigned short  sst;        // SubSection Type
+    unsigned short  modindex;   // Module index number
+    unsigned long   lfoStart;   // Start of section
+    unsigned short  cb;         // Size of section
+} SSDIR;
+
+typedef struct _SSDIR32
+{
+    unsigned short  sst;        // SubSection Type
+    unsigned short  modindex;   // Module index number
+    unsigned long   lfoStart;   // Start of section
+    unsigned long   cb;         // Size of section
+} SSDIR32;
+
+typedef struct _SSMODULE
+{
+    unsigned short  csBase;     // code segment base
+    unsigned short  csOff;      // code segment offset
+    unsigned short  csLen;      // code segment length
+    unsigned short  ovrNum;     // overlay number
+    unsigned short  indxSS;     // Index into sstLib or 0
+    unsigned short  reserved;
+    char            csize;      // size of prefix string
+} SSMODULE;
+
+typedef struct _SSMOD32
+{
+    unsigned short  csBase;     // code segment base
+    unsigned long   csOff;      // code segment offset
+    unsigned long   csLen;      // code segment length
+    unsigned long   ovrNum;     // overlay number
+    unsigned short  indxSS;     // Index into sstLib or 0
+    unsigned long   reserved;
+    char            csize;      // size of prefix string
+} SSMOD32;
+
+typedef struct _SSPUBLIC
+{
+    unsigned short  offset;
+    unsigned short  segment;
+    unsigned short  type;
+    char            csize;
+} SSPUBLIC;
+
+typedef struct _SSPUBLIC32
+{
+    unsigned long   offset;
+    unsigned short  segment;
+    unsigned short  type;
+    char            csize;
+} SSPUBLIC32;
+
+typedef struct _SSLINEENTRY32
+{
+    unsigned short  LineNum;
+    unsigned short  FileNum;
+    unsigned long   Offset;
+} SSLINEENTRY32;
+
+typedef struct _FIRSTLINEENTRY32
+{
+    unsigned short  LineNum;
+    unsigned char   entry_type;
+    unsigned char   reserved;
+    unsigned short  numlines;
+    unsigned short  segnum;
+} FIRSTLINEENTRY32;
+
+typedef struct _SSFILENUM32
+{
+    unsigned long   first_displayable;  // Not used
+    unsigned long   number_displayable;     // Not used
+    unsigned long   file_count; // number of source files
+} SSFILENUM32;
+
+/*
+ *@@ XDEBUGINFO:
+ *      buffers for Read... funcs.
+ *
+ *@@added V0.9.4 (2000-06-15) [umoeller]
+ */
+
+typedef struct _XDEBUGINFO
+{
+    char            szNrFile[300];      // receives source file
+    char            szNrLine[300];      // receives line number
+    char            szNrPub[300];       // receives function name
+
+    struct new_seg  *pseg;
+    struct o32_obj  *pobj;              // flat .EXE object table entry
+
+    SYMBASE         base;
+
+    SSDIR           *pDirTab;
+    SSDIR32         *pDirTab32;
+    unsigned char   *pEntTab;
+    unsigned long   lfaBase;
+    SSMOD32         ssmod32;
+    SSPUBLIC32      sspub32;
+
+    SSMODULE        ssmod;
+    SSPUBLIC        sspub;
+} XDEBUGINFO, *PXDEBUGINFO;
+
+#pragma pack()
+
+/* ******************************************************************
+ *                                                                  *
+ *   PART 1: ANALYZE DEBUG CODE                                     *
+ *                                                                  *
+ ********************************************************************/
+
+int Read16CodeView(FILE *LogFile, PXDEBUGINFO pxdi, int fh, int TrapSeg, int TrapOff, CHAR *FileName);
+int Read32PmDebug(FILE *LogFile, PXDEBUGINFO pxdi, int fh, int TrapSeg, int TrapOff, CHAR *FileName);
+
+/*
+ *@@ dbgPrintDebugInfo:
+ *      this is the main entry point into analyzing debug
+ *      code.
+ *      This analyzes a given address and tries to find
+ *      debug code descriptions for this address. If found,
+ *      the information is written to the given log file.
+ *
+ *      Gets called from dbgPrintStack.
+ *
+ *      This returns NO_ERROR if the could was successfully
+ *      analyzed or something != 0 if we failed.
+ *
+ *      New with V0.84.
+ */
+
+APIRET dbgPrintDebugInfo(FILE *LogFile,         // out: log file to write to
+                         CHAR *FileName,        // in: EXE/DLL module file name
+                         ULONG Object,          // in: trapping object
+                         ULONG TrapOffset)      // in: trapping address
+{
+    APIRET                  rc = 0;
+    int                     ModuleFile = 0;
+    static struct exe_hdr   OldExeHeader;
+    static struct new_exe   NewExeHeader;
+
+    XDEBUGINFO              xdi;
+    memset(&xdi, 0, sizeof(xdi));
+
+    // open the module file for reading to analyze the code
+    ModuleFile = sopen(FileName, O_RDONLY | O_BINARY, SH_DENYNO);
+
+    if (ModuleFile != -1)
+    {
+        // file found:
+        // read old Exe header
+        if (read(ModuleFile, (void*)&OldExeHeader, 64) == -1L)
+        {
+            fprintf(LogFile, "errno %d reading old exe header\n", errno);
+            close(ModuleFile);
+            return 2;
+        }
+        // seek to new Exe header
+        if (lseek(ModuleFile, (long)E_LFANEW(OldExeHeader), SEEK_SET) == -1L)
+        {
+            fprintf(LogFile, "errno %d seeking to new exe header\n", errno);
+            close(ModuleFile);
+            return 3;
+        }
+        if (read(ModuleFile, (void *)&NewExeHeader, 64) == -1L)
+        {
+            fprintf(LogFile, "errno %d reading new exe header\n", errno);
+            close(ModuleFile);
+            return 4;
+        }
+
+        // check EXE signature
+        if (NE_MAGIC(NewExeHeader) == E32MAGIC)
+        {
+            /*
+             * flat 32 executable:
+             *
+             */
+
+            // do analysis for 32-bit code
+            rc = Read32PmDebug(LogFile,
+                               &xdi,
+                               ModuleFile,
+                               Object + 1,
+                               TrapOffset,
+                               FileName);
+            if (rc == 0)
+            {
+                fprintf(LogFile, "%s", xdi.szNrFile);
+                fprintf(LogFile, "%s", xdi.szNrLine);
+                fprintf(LogFile, "%s", xdi.szNrPub);
+            }                   // endif
+            close(ModuleFile);
+
+            // rc !=0 try with DBG file
+            if (rc != 0)
+            {
+                strcpy(FileName + strlen(FileName) - 3, "DBG");     // Build DBG File name
+                ModuleFile = sopen(FileName, O_RDONLY | O_BINARY, SH_DENYNO);
+                if (ModuleFile != -1)
+                {
+                    rc = Read32PmDebug(LogFile, &xdi, ModuleFile, Object + 1, TrapOffset, FileName);
+                    if (rc == 0)
+                    {
+                        fprintf(LogFile, "%s", xdi.szNrFile);
+                        fprintf(LogFile, "%s", xdi.szNrLine);
+                        fprintf(LogFile, "%s", xdi.szNrPub);
+                    }           // endif
+                    close(ModuleFile);
+                }
+            }                   // endif
+
+            return rc;
+        }
+        else
+        {
+            if (NE_MAGIC(NewExeHeader) == NEMAGIC)
+            {
+                /*
+                 * 16:16 executable:
+                 *
+                 */
+
+                if ((xdi.pseg = (struct new_seg *)calloc(NE_CSEG(NewExeHeader), sizeof(struct new_seg))) == NULL)
+                {
+                    fprintf(LogFile, "Out of memory!");
+                    close(ModuleFile);
+                    return -1;
+                }
+                if (lseek(ModuleFile, E_LFANEW(OldExeHeader) + NE_SEGTAB(NewExeHeader), SEEK_SET) == -1L)
+                {
+                    fprintf(LogFile, "Error %u seeking segment table in %s\n", errno, FileName);
+                    free(xdi.pseg);
+                    close(ModuleFile);
+                    return 9;
+                }
+
+                if (read(ModuleFile, (void *)xdi.pseg, NE_CSEG(NewExeHeader) * sizeof(struct new_seg)) == -1)
+                {
+                    fprintf(LogFile, "Error %u reading segment table from %s\n", errno, FileName);
+                    free(xdi.pseg);
+                    close(ModuleFile);
+                    return 10;
+                }
+                rc = Read16CodeView(LogFile, &xdi, ModuleFile, Object + 1, TrapOffset, FileName);
+                if (rc == 0)
+                {
+                    fprintf(LogFile, "%s", xdi.szNrFile);
+                    fprintf(LogFile, "%s", xdi.szNrLine);
+                    fprintf(LogFile, "%s", xdi.szNrPub);
+                }               // endif
+                free(xdi.pseg);
+                close(ModuleFile);
+
+                // rc !=0 try with DBG file
+                if (rc != 0)
+                {
+                    strcpy(FileName + strlen(FileName) - 3, "DBG");     // Build DBG File name
+                    ModuleFile = sopen(FileName, O_RDONLY | O_BINARY, SH_DENYNO);
+                    if (ModuleFile != -1)
+                    {
+                        rc = Read16CodeView(LogFile, &xdi, ModuleFile, Object + 1, TrapOffset, FileName);
+                        if (rc == 0)
+                        {
+                            fprintf(LogFile, "%s", xdi.szNrFile);
+                            fprintf(LogFile, "%s", xdi.szNrLine);
+                            fprintf(LogFile, "%s", xdi.szNrPub);
+                        }       // endif
+                        close(ModuleFile);
+                    }
+                }               // endif
+                return rc;
+
+            }
+            else
+            {
+                /*
+                 * Unknown executable:
+                 *
+                 */
+
+                fprintf(LogFile, "Error, could not find exe signature");
+                close(ModuleFile);
+                return 11;
+            }
+        }
+    } // end if (ModuleFile != -1)
+    else
+    {
+        fprintf(LogFile, "Error %d opening module file %s", errno, FileName);
+        return 1;
+    }                           // endif
+
+    // return 0;        we never get here
+}
+
+char            fname[128],
+                ModName[80];
+char            ename[128],
+                dummy[128];
+
+#define MAX_USERDEFS 300        // raised from 150 V0.9.1 (2000-01-30) [umoeller]
+#define MAX_POINTERS 300        // raised from 150 V0.9.1 (2000-01-30) [umoeller]
+
+USHORT          userdef_count;
+USHORT          pointer_count;
+
+struct one_userdef_rec
+{
+    USHORT          idx;
+    USHORT          type_index;
+    BYTE            name[33];
+} one_userdef[MAX_USERDEFS];
+
+struct one_pointer_rec
+{
+    USHORT          idx;
+    USHORT          type_index;
+    BYTE            type_qual;
+    BYTE            name[33];
+} one_pointer[MAX_POINTERS];
+
+/*
+ * Read32PmDebug:
+ *      parses 32-bit debug code.
+ *      Called from dbgPrintDebugInfo for 32-bit modules.
+ */
+
+int Read32PmDebug(FILE *LogFile,        // in: text log file to write to
+                  PXDEBUGINFO pxdi,
+                  int ModuleFile,       // in: module file opened with sopen()
+                  int TrapSeg,
+                  int TrapOff,
+                  CHAR *FileName)
+{
+    static unsigned int CurrSymSeg, NrSymbol,
+                        /* offset, */ NrPublic,
+                        NrFile, NrLine, /* NrEntry */
+                        numdir, namelen,
+                        numlines /* , line */;
+    static int      ModIndex;
+    static int      bytesread, i, j;
+    static SSLINEENTRY32 LineEntry;
+    static SSFILENUM32 FileInfo;
+    static FIRSTLINEENTRY32 FirstLine;
+    static BYTE     dump_vars = FALSE;
+    static USHORT   idx;
+    static BOOL     read_types;
+    static LONG     lSize;
+
+    ModIndex = 0;
+    // See if any CODEVIEW info
+    if (lseek(ModuleFile, -8L, SEEK_END) == -1)
+    {
+        fprintf(LogFile, "Error %u seeking CodeView table in %s\n", errno, FileName);
+        return (18);
+    }
+
+    if (read(ModuleFile, (void *)&G_eodbug, 8) == -1)
+    {
+        fprintf(LogFile, "Error %u reading debug info from %s\n", errno, FileName);
+        return (19);
+    }
+    if (G_eodbug.dbug != DBUGSIG)
+    {
+        // fprintf(LogFile,"\nNo CodeView information stored.\n");
+        return (100);
+    }
+
+    if ((pxdi->lfaBase = lseek(ModuleFile, -(LONG)G_eodbug.dfaBase, SEEK_END)) == -1L)
+    {
+        fprintf(LogFile, "Error %u seeking base codeview data in %s\n", errno, FileName);
+        return (20);
+    }
+
+    if (read(ModuleFile, (void *)&pxdi->base, 8) == -1)
+    {
+        fprintf(LogFile, "Error %u reading base codeview data in %s\n", errno, FileName);
+        return (21);
+    }
+
+    if (lseek(ModuleFile, pxdi->base.lfoDir - 8 + 4, SEEK_CUR) == -1)
+    {
+        fprintf(LogFile, "Error %u seeking dir codeview data in %s\n", errno, FileName);
+        return (22);
+    }
+
+    if (read(ModuleFile, (void *)&numdir, 4) == -1)
+    {
+        fprintf(LogFile, "Error %u reading dir codeview data in %s\n", errno, FileName);
+        return (23);
+    }
+
+    // Read dir table into buffer
+    if ((pxdi->pDirTab32 = (SSDIR32*)calloc(numdir,
+                                             sizeof(SSDIR32))
+                     ) == NULL)
+    {
+        fprintf(LogFile, "Out of memory!");
+        return (-1);
+    }
+
+    if (read(ModuleFile, (void *)pxdi->pDirTab32, numdir * sizeof(SSDIR32)) == -1)
+    {
+        fprintf(LogFile, "Error %u reading codeview dir table from %s\n", errno, FileName);
+        free(pxdi->pDirTab32);
+        return (24);
+    }
+
+    i = 0;
+    while (i < numdir)
+    {
+        if (pxdi->pDirTab32[i].sst != SSTMODULES)
+        {
+            i++;
+            continue;
+        }
+        NrPublic = 0x0;
+        NrSymbol = 0;
+        NrLine = 0x0;
+        NrFile = 0x0;
+        CurrSymSeg = 0;
+        // point to subsection
+        lseek(ModuleFile, pxdi->pDirTab32[i].lfoStart + pxdi->lfaBase, SEEK_SET);
+        read(ModuleFile, (void*)&(pxdi->ssmod32.csBase), sizeof(SSMOD32));
+        read(ModuleFile, (void*)ModName, (unsigned)pxdi->ssmod32.csize);
+        ModIndex = pxdi->pDirTab32[i].modindex;
+        ModName[pxdi->ssmod32.csize] = '\0';
+        i++;
+
+        read_types = FALSE;
+
+        while (     (pxdi->pDirTab32[i].modindex == ModIndex)
+                 && (i < numdir)
+              )
+        {
+            // point to subsection
+            lseek(ModuleFile, pxdi->pDirTab32[i].lfoStart + pxdi->lfaBase, SEEK_SET);
+            switch (pxdi->pDirTab32[i].sst)
+            {
+                case SSTPUBLICS:
+                    bytesread = 0;
+                    while (bytesread < pxdi->pDirTab32[i].cb)
+                    {
+                        bytesread += read(ModuleFile, (void *)&pxdi->sspub32.offset, sizeof(pxdi->sspub32));
+                        bytesread += read(ModuleFile, (void *)ename, (unsigned)pxdi->sspub32.csize);
+                        ename[pxdi->sspub32.csize] = '\0';
+                        if (    (pxdi->sspub32.segment == TrapSeg)
+                             && (pxdi->sspub32.offset <= TrapOff)
+                             && (pxdi->sspub32.offset >= NrPublic)
+                           )
+                        {
+                            NrPublic = pubfunc_ofs = pxdi->sspub32.offset;
+                            read_types = TRUE;
+                            sprintf(pxdi->szNrPub, "%s %s (%s, seg %04X : ofs %08X\n",
+                                    (pxdi->sspub32.type == 1) ? " Abs" : " ",
+                                    ename,
+                                    ModName, // ()
+                                    pxdi->sspub32.segment,
+                                    pxdi->sspub32.offset
+                                );
+                            // but continue, because there might be a
+                            // symbol that comes closer
+                        }
+                    }
+                    break;
+
+                // Read symbols, so we can dump the variables on the stack
+                case SSTSYMBOLS:
+                    if (TrapSeg != pxdi->ssmod32.csBase)
+                        break;
+
+                    bytesread = 0;
+                    while (bytesread < pxdi->pDirTab32[i].cb)
+                    {
+                        static USHORT   usLength;
+                        static BYTE     b1,
+                                        b2;
+                        static BYTE     bType;
+                                       // *ptr;
+                        static ULONG    ofs;
+                        // static ULONG    last_addr = 0;
+                        static BYTE     str[256];
+                        static struct symseg_rec symseg;
+                        static struct symauto_rec symauto;
+                        static struct symproc_rec symproc;
+
+                        // Read the length of this subentry
+                        bytesread += read(ModuleFile, &b1, 1);
+                        if (b1 & 0x80)
+                        {
+                            bytesread += read(ModuleFile, &b2, 1);
+                            usLength = ((b1 & 0x7F) << 8) + b2;
+                        }
+                        else
+                            usLength = b1;
+
+                        ofs = tell(ModuleFile);
+
+                        bytesread += read(ModuleFile, &bType, 1);
+
+                        switch (bType)
+                        {
+                            case SYM_CHANGESEG:
+                                read(ModuleFile, &symseg, sizeof(symseg));
+                                CurrSymSeg = symseg.seg_no;
+                                break;
+
+                            case SYM_PROC:
+                            case SYM_CPPPROC:
+                                read(ModuleFile, &symproc, sizeof(symproc));
+                                read(ModuleFile, str, symproc.name_len);
+                                str[symproc.name_len] = 0;
+
+                                if ((CurrSymSeg == TrapSeg) &&
+                                    (symproc.offset <= TrapOff) &&
+                                    (symproc.offset >= NrSymbol))
+                                {
+
+                                    dump_vars = TRUE;
+                                    var_ofs = 0;
+                                    NrSymbol = symproc.offset;
+                                    func_ofs = symproc.offset;
+
+                                    strcpy(func_name, str);
+                                }
+                                else
+                                {
+                                    dump_vars = FALSE;
+                                }
+                                break;
+
+                            case SYM_AUTO:
+                                if (!dump_vars)
+                                    break;
+
+                                read(ModuleFile, &symauto, sizeof(symauto));
+                                read(ModuleFile, str, symauto.name_len);
+                                str[symauto.name_len] = 0;
+
+                                strcpy(autovar_def[var_ofs].name, str);
+                                autovar_def[var_ofs].stack_offset = symauto.stack_offset;
+                                autovar_def[var_ofs].type_idx = symauto.type_idx;
+                                var_ofs++;
+                                break;
+
+                        }
+
+                        bytesread += usLength;
+
+                        lseek(ModuleFile, ofs + usLength, SEEK_SET);
+                    }
+                    break;
+
+                case SSTTYPES:
+                    // if (ModIndex != TrapSeg)
+                    if (!read_types)
+                        break;
+
+                    bytesread = 0;
+                    idx = 0x200;
+                    userdef_count = 0;
+                    pointer_count = 0;
+                    while (bytesread < pxdi->pDirTab32[i].cb)
+                    {
+                        static struct type_rec type;
+                        static struct type_userdefrec udef;
+                        static struct type_pointerrec point;
+                        static ULONG    ofs;
+                        static BYTE     str[256];
+
+                        // Read the length of this subentry
+                        ofs = tell(ModuleFile);
+
+                        read(ModuleFile, &type, sizeof(type));
+                        bytesread += sizeof(type);
+
+                        switch (type.type)
+                        {
+                            case TYPE_USERDEF:
+                                if (userdef_count >= MAX_USERDEFS)
+                                    break;
+
+                                read(ModuleFile, &udef, sizeof(udef));
+                                read(ModuleFile, str, udef.name_len);
+                                str[udef.name_len] = 0;
+
+                                // Insert userdef in table
+                                one_userdef[userdef_count].idx = idx;
+                                one_userdef[userdef_count].type_index = udef.type_index;
+                                memcpy(one_userdef[userdef_count].name,
+                                       str,
+                                       _min(udef.name_len + 1, 32));
+                                one_userdef[userdef_count].name[32] = 0;
+                                userdef_count++;
+                                break;
+
+                            case TYPE_POINTER:
+                                if (pointer_count >= MAX_POINTERS)
+                                    break;
+
+                                read(ModuleFile, &point, sizeof(point));
+                                read(ModuleFile, str, point.name_len);
+                                str[point.name_len] = 0;
+
+                                // Insert userdef in table
+                                one_pointer[pointer_count].idx = idx;
+                                one_pointer[pointer_count].type_index = point.type_index;
+                                memcpy(one_pointer[pointer_count].name,
+                                       str,
+                                       _min(point.name_len + 1, 32));
+                                one_pointer[pointer_count].name[32] = 0;
+                                one_pointer[pointer_count].type_qual = type.type_qual;
+                                pointer_count++;
+                                break;
+                        }
+
+                        ++idx;
+
+                        bytesread += type.length;
+
+                        lseek(ModuleFile, ofs + type.length + 2, SEEK_SET);
+                    }
+                    break;
+
+                case SSTSRCLINES32:
+                    if (TrapSeg != pxdi->ssmod32.csBase)
+                        break;
+
+                    // read first line
+                    do
+                    {
+                        read(ModuleFile, (void *)&FirstLine, sizeof(FirstLine));
+
+                        if (FirstLine.LineNum != 0)
+                        {
+                            fprintf(LogFile, "Missing Line table information\n");
+                            break;
+                        }       // endif
+                        numlines = FirstLine.numlines;
+                        // Other type of data skip 4 more bytes
+                        if (FirstLine.entry_type < 4)
+                        {
+                            read(ModuleFile, (void *)&lSize, 4);
+                            if (FirstLine.entry_type == 3)
+                                lseek(ModuleFile, lSize, SEEK_CUR);
+                        }
+                    }
+                    while (FirstLine.entry_type == 3);
+
+                    for (j = 0; j < numlines; j++)
+                    {
+                        switch (FirstLine.entry_type)
+                        {
+                            case 0:
+                                read(ModuleFile, (void *)&LineEntry, sizeof(LineEntry));
+                                // Changed by Kim Rasmussen 26/06 1996 to ignore linenumber 0
+                                // if (LineEntry.Offset+ssmod32.csOff<=TrapOff && LineEntry.Offset+ssmod32.csOff>=NrLine) {
+                                if (    (LineEntry.LineNum)
+                                     && (LineEntry.Offset + pxdi->ssmod32.csOff
+                                            <= TrapOff)
+                                     && (LineEntry.Offset + pxdi->ssmod32.csOff >= NrLine)
+                                   )
+                                {
+                                    NrLine = LineEntry.Offset;
+                                    NrFile = LineEntry.FileNum;
+                                    /*pOffset =sprintf(szNrLine,"%04X:%08X  line #%hu ",
+                                     * ssmod32.csBase,LineEntry.Offset,
+                                     * LineEntry.LineNum); */
+                                    sprintf(pxdi->szNrLine, "% 6hu", LineEntry.LineNum);
+                                }
+                                break;
+
+                            case 1:
+                                lseek(ModuleFile, sizeof(struct linlist_rec), SEEK_CUR);
+                                break;
+
+                            case 2:
+                                lseek(ModuleFile, sizeof(struct linsourcelist_rec), SEEK_CUR);
+                                break;
+
+                            case 3:
+                                lseek(ModuleFile, sizeof(struct filenam_rec), SEEK_CUR);
+                                break;
+
+                            case 4:
+                                lseek(ModuleFile, sizeof(struct pathtab_rec), SEEK_CUR);
+                                break;
+
+                        }
+                    }
+
+                    if (NrFile != 0)
+                    {
+                        // file found:
+                        read(ModuleFile, (void*)&FileInfo, sizeof(FileInfo));
+                        namelen = 0;
+                        for (j = 1; j <= FileInfo.file_count; j++)
+                        {
+                            namelen = 0;
+                            read(ModuleFile, (void *)&namelen, 1);
+                            read(ModuleFile, (void *)ename, namelen);
+                            if (j == NrFile)
+                                break;
+                        }
+                        ename[namelen] = '\0';
+                        //  pOffset=sprintf(szNrLine+pOffset," (%s) (%s)\n",ename,ModName);
+                        sprintf(pxdi->szNrFile, "%11.11s ", ename);
+                    }
+                    else
+                    {
+                        // strcat(szNrLine,"\n"); avoid new line for empty name fill
+                        strcpy(pxdi->szNrFile, "file?       ");
+                    } // endif
+                    break;
+            } // end switch
+
+            i++;
+        } // end while modindex
+    } // End While i < numdir
+    free(pxdi->pDirTab32);
+    return (0);
+}
+
+/*
+ * Read16CodeView:
+ *      parses 16-bit debug code.
+ *      Called from dbgPrintDebugInfo for 16-bit modules.
+ */
+
+int Read16CodeView(FILE *LogFile,       // in: text log file to write to
+                   PXDEBUGINFO pxdi,
+                   int fh,
+                   int TrapSeg,
+                   int TrapOff,
+                   CHAR *FileName)
+{
+    static unsigned short int offset,
+                    NrPublic, NrLine,
+                    numdir,
+                    namelen, numlines,
+                    line;
+    static int      ModIndex;
+    static int      bytesread, i, j;
+
+    ModIndex = 0;
+    // See if any CODEVIEW info
+    if (lseek(fh, -8L, SEEK_END) == -1)
+    {
+        fprintf(LogFile, "Error %u seeking CodeView table in %s\n", errno, FileName);
+        return (18);
+    }
+
+    if (read(fh, (void *)&G_eodbug, 8) == -1)
+    {
+        fprintf(LogFile, "Error %u reading debug info from %s\n", errno, FileName);
+        return (19);
+    }
+    if (G_eodbug.dbug != DBUGSIG)
+    {
+        // fprintf(LogFile,"\nNo CodeView information stored.\n");
+        return (100);
+    }
+
+    if ((pxdi->lfaBase = lseek(fh, -(LONG)G_eodbug.dfaBase, SEEK_END)) == -1L)
+    {
+        fprintf(LogFile, "Error %u seeking base codeview data in %s\n", errno, FileName);
+        return (20);
+    }
+
+    if (read(fh, (void *)&pxdi->base, 8) == -1)
+    {
+        fprintf(LogFile, "Error %u reading base codeview data in %s\n", errno, FileName);
+        return (21);
+    }
+
+    if (lseek(fh, pxdi->base.lfoDir - 8, SEEK_CUR) == -1)
+    {
+        fprintf(LogFile, "Error %u seeking dir codeview data in %s\n", errno, FileName);
+        return (22);
+    }
+
+    if (read(fh, (void *)&numdir, 2) == -1)
+    {
+        fprintf(LogFile, "Error %u reading dir codeview data in %s\n", errno, FileName);
+        return (23);
+    }
+
+    // Read dir table into buffer
+    if ((pxdi->pDirTab = (SSDIR*)calloc(numdir, sizeof(SSDIR))) == NULL)
+    {
+        fprintf(LogFile, "Out of memory!");
+        return (-1);
+    }
+
+    if (read(fh, (void*)pxdi->pDirTab, numdir * sizeof(SSDIR)) == -1)
+    {
+        fprintf(LogFile, "Error %u reading codeview dir table from %s\n", errno, FileName);
+        free(pxdi->pDirTab);
+        return (24);
+    }
+
+    i = 0;
+    while (i < numdir)
+    {
+        if (pxdi->pDirTab[i].sst != SSTMODULES)
+        {
+            i++;
+            continue;
+        }
+        NrPublic = 0x0;
+        NrLine = 0x0;
+        // point to subsection
+        lseek(fh, pxdi->pDirTab[i].lfoStart + pxdi->lfaBase, SEEK_SET);
+        read(fh, (void *)&pxdi->ssmod.csBase, sizeof(SSMODULE));
+        read(fh, (void *)ModName, (unsigned)pxdi->ssmod.csize);
+        ModIndex = pxdi->pDirTab[i].modindex;
+        ModName[pxdi->ssmod.csize] = '\0';
+        i++;
+        while (pxdi->pDirTab[i].modindex == ModIndex && i < numdir)
+        {
+            // point to subsection
+            lseek(fh, pxdi->pDirTab[i].lfoStart + pxdi->lfaBase, SEEK_SET);
+            switch (pxdi->pDirTab[i].sst)
+            {
+                case SSTPUBLICS:
+                    bytesread = 0;
+                    while (bytesread < pxdi->pDirTab[i].cb)
+                    {
+                        bytesread += read(fh, (void *)&pxdi->sspub.offset, sizeof(pxdi->sspub));
+                        bytesread += read(fh, (void *)ename, (unsigned)pxdi->sspub.csize);
+                        ename[pxdi->sspub.csize] = '\0';
+                        if ((pxdi->sspub.segment == TrapSeg) &&
+                            (pxdi->sspub.offset <= TrapOff) &&
+                            (pxdi->sspub.offset >= NrPublic))
+                        {
+                            NrPublic = pxdi->sspub.offset;
+                            sprintf(pxdi->szNrPub, "%s %s (%s) %04hX:%04hX\n",
+                                    (pxdi->sspub.type == 1) ? " Abs" : " ", ename,
+                                    ModName, // ()
+                                    pxdi->sspub.segment,
+                                    pxdi->sspub.offset
+                                );
+                        }
+                    }
+                    break;
+
+                case SSTSRCLINES2:
+                case SSTSRCLINES:
+                    if (TrapSeg != pxdi->ssmod.csBase)
+                        break;
+                    namelen = 0;
+                    read(fh, (void *)&namelen, 1);
+                    read(fh, (void *)ename, namelen);
+                    ename[namelen] = '\0';
+                    // skip 2 zero bytes
+                    if (pxdi->pDirTab[i].sst == SSTSRCLINES2)
+                        read(fh, (void *)&numlines, 2);
+                    read(fh, (void *)&numlines, 2);
+                    for (j = 0; j < numlines; j++)
+                    {
+                        read(fh, (void *)&line, 2);
+                        read(fh, (void *)&offset, 2);
+                        if (offset <= TrapOff && offset >= NrLine)
+                        {
+                            NrLine = offset;
+                            sprintf(pxdi->szNrFile, "% 12.12s ", ename);
+                            sprintf(pxdi->szNrLine, "% 6hu", line);
+                            /*sprintf(szNrLine,"%04hX:%04hX  line #%hu  (%s) (%s)\n",
+                             * ssmod.csBase,offset,line,ModName,ename); */
+                        }
+                    }
+                    break;
+            }                   // end switch
+            i++;
+        }                       // end while modindex
+    }                           // End While i < numdir
+    free(pxdi->pDirTab);
+    return (0);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   PART 2: ANALYZE VARIABLES                                      *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * var_value:
+ *      writes a description of a variable type to
+ *      the specified buffer, depending on "type".
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: changed prototype to use external buffer
+ */
+
+VOID var_value(void *varptr,        // in: address of the variable on the stack
+               char *pszBuf,        // out: information
+               BYTE type)           // in: type; if >= 32, we'll call DosQueryMem
+{
+    ULONG           Size = 1,
+                    Attr = 0;
+
+    if (DosQueryMem(varptr, &Size, &Attr) != NO_ERROR)
+    {
+        sprintf(pszBuf, "type %d, DosQueryMem failed", type);
+        return;
+    }
+
+    if ((Attr & PAG_READ) == 0)
+    {
+        sprintf(pszBuf, "type %d, read-access to value denied", type);
+        return;
+    }
+
+    if (type == 0)
+        sprintf(pszBuf, "%hd", *(signed char*)varptr);
+    else if (type == 1)
+        sprintf(pszBuf, "%hd", *(signed short*)varptr);
+    else if (type == 2)
+        sprintf(pszBuf, "%ld", *(signed long*)varptr);
+    else if (type == 4)
+        sprintf(pszBuf, "%hu", *(BYTE*) varptr);
+    else if (type == 5)
+        sprintf(pszBuf, "%hu", *(USHORT*)varptr);
+    else if (type == 6)
+        sprintf(pszBuf, "0x%lX (%lu)", *((ULONG*)varptr), *((ULONG*)varptr));
+    else if (type == 8)
+        sprintf(pszBuf, "%f", *(float*)varptr);
+    else if (type == 9)
+        sprintf(pszBuf, "%f", *(double*)varptr);
+    else if (type == 10)
+        sprintf(pszBuf, "%f", *(long double*)varptr);
+    else if (type == 16)
+        sprintf(pszBuf, "%s", *(char*)varptr ? "TRUE" : "FALSE");
+    else if (type == 17)
+        sprintf(pszBuf, "%s", *(short*)varptr ? "TRUE" : "FALSE");
+    else if (type == 18)
+        sprintf(pszBuf, "%s", *(long*)varptr ? "TRUE" : "FALSE");
+    else if (type == 20)
+        sprintf(pszBuf, "%c", *(char*)varptr);
+    else if (type == 21)
+        sprintf(pszBuf, "%lc", *(short*)varptr);
+    else if (type == 22)
+        sprintf(pszBuf, "%lc", *(long*)varptr);
+    else if (type == 23)
+        sprintf(pszBuf, "void");
+    else if (type >= 32)
+    {
+        sprintf(pszBuf, "0x%p", *(ULONG *) varptr);
+        if (Attr & PAG_FREE)
+        {
+            strcat(pszBuf, " unallocated memory");
+        }
+        else
+        {
+            if ((Attr & PAG_COMMIT) == 0x0U)
+            {
+                strcat(pszBuf, " uncommitted");
+            }               // endif
+            if ((Attr & PAG_WRITE) == 0x0U)
+            {
+                strcat(pszBuf, " unwritable");
+            }               // endif
+            if ((Attr & PAG_READ) == 0x0U)
+            {
+                strcat(pszBuf, " unreadable");
+            }               // endif
+        }                   // endif
+    }                       // endif
+    else
+        sprintf(pszBuf, "Unknown type %d", type);
+}
+
+/*
+ * search_userdefs:
+ *      searches the table of userdef's-
+ *      Return TRUE if found.
+ */
+
+BOOL search_userdefs(FILE *LogFile,     // in: text log file to write to
+                     ULONG stackofs,
+                     USHORT var_no)
+{
+    USHORT          pos;
+
+    for (pos = 0;
+         pos < userdef_count;
+         pos++)
+    {
+        if (one_userdef[pos].idx == autovar_def[var_no].type_idx)
+        {
+            if (    (one_userdef[pos].type_index >= 0x80)
+                // &&  (one_userdef[pos].type_index <= 0xDA)
+               )
+            {
+                static char sszVar3[500] = "complex";
+                if (one_userdef[pos].type_index <= 0xDA)
+                    var_value((void*)(stackofs + autovar_def[var_no].stack_offset),
+                              sszVar3,
+                              one_userdef[pos].type_index - 0x80);
+
+                fprintf(LogFile, "     %- 6d %- 20.20s %- 33.33s %s (user)\n",
+                        autovar_def[var_no].stack_offset,       // stack offset
+                        autovar_def[var_no].name,               // identifier
+                        one_userdef[pos].name,                  // type name
+                        sszVar3                                 // composed by var_value
+                       );
+                return TRUE;
+            }
+            else
+                return FALSE;
+        }
+    }
+
+    return FALSE;
+}
+
+/*
+ * search_pointers:
+ *
+ */
+
+BOOL search_pointers(FILE *LogFile,     // in: text log file to write to
+                     ULONG stackofs,
+                     USHORT var_no)
+{
+    USHORT          pos, upos;
+    static BYTE     str[35];
+    static char     sszVar[500];
+
+    // BYTE            type_index;
+
+    for (pos = 0;
+         (   (pos < pointer_count)
+          && (one_pointer[pos].idx != autovar_def[var_no].type_idx)
+         );
+         pos++);
+
+    if (pos < pointer_count)
+    {
+        if (    (one_pointer[pos].type_index >= 0x80)
+             && (one_pointer[pos].type_index <= 0xDA)
+           )
+        {
+            strcpy(str, type_name[one_pointer[pos].type_index - 0x80]);
+            strcat(str, " *");
+            var_value((void*)(stackofs + autovar_def[var_no].stack_offset),
+                      sszVar,
+                      32);
+            fprintf(LogFile, "     %- 6d %- 20.20s %- 33.33s %s (ptr1)\n",
+                    autovar_def[var_no].stack_offset,
+                    autovar_def[var_no].name,
+                    str,
+                    sszVar);
+            return TRUE;
+        }
+        else
+        {
+            // If the result isn't a simple type, look for it in the other lists
+            for (upos = 0;
+                 (   (upos < userdef_count)
+                  && (one_userdef[upos].idx != one_pointer[pos].type_index)
+                 );
+                 upos++)
+                ;
+
+            if (upos < userdef_count)
+            {
+                strcpy(str, one_userdef[upos].name);
+                strcat(str, " *");
+                var_value((void *)(stackofs + autovar_def[var_no].stack_offset),
+                          sszVar,
+                          32);
+                fprintf(LogFile, "     %- 6d %- 20.20s %- 33.33s %s (ptr2)\n",
+                        autovar_def[var_no].stack_offset,
+                        autovar_def[var_no].name,
+                        str,
+                        sszVar);
+                return TRUE;
+            }
+            else
+            {
+                // if it isn't a userdef, for now give up and just print
+                // as much as we know
+                sprintf(str, "Pointer to type 0x%X", one_pointer[pos].type_index);
+
+                var_value((void *)(stackofs + autovar_def[var_no].stack_offset),
+                          sszVar,
+                          32);
+                fprintf(LogFile, "     %- 6d %- 20.20s %- 33.33s %s (ptr3)\n",
+                        autovar_def[var_no].stack_offset,
+                        autovar_def[var_no].name,
+                        str,
+                        sszVar);
+
+                return TRUE;
+            }
+        }
+    }
+
+    return FALSE;
+}
+
+/*
+ *@@ dbgPrintVariables:
+ *      Dumps variables for the specified stack offset
+ *      to the specified log file.
+ *
+ *      New with V0.84.
+ */
+
+void dbgPrintVariables(FILE *LogFile,   // in: text log file to write to
+                       ULONG stackofs)
+{
+    USHORT          n; // , pos;
+    BOOL            AutoVarsFound = FALSE;
+
+    if (/* 1 || */ func_ofs == pubfunc_ofs)
+    {
+        for (n = 0;
+             n < var_ofs;
+             n++)
+        {
+            if (AutoVarsFound == FALSE)
+            {
+                AutoVarsFound = TRUE;
+                fprintf(LogFile, "     List of auto variables at EBP %p in %s:\n", stackofs, func_name);
+                fprintf(LogFile, "     Offset Name                 Type                              Value            \n");
+                fprintf(LogFile, "     ÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ\n");
+            }
+
+            // If it's one of the simple types
+            if (   (autovar_def[n].type_idx >= 0x80)
+                && (autovar_def[n].type_idx <= 0xDA)
+                )
+            {
+                static char sszVar2[500];
+
+                var_value((void *)(stackofs + autovar_def[n].stack_offset),
+                          sszVar2,
+                          autovar_def[n].type_idx - 0x80);
+
+                fprintf(LogFile, "     %- 6d %- 20.20s %- 33.33s %s (simple)\n",
+                        autovar_def[n].stack_offset,
+                        autovar_def[n].name,
+                        type_name[autovar_def[n].type_idx - 0x80],
+                        sszVar2);
+            }
+            else
+            {                   // Complex type, check if we know what it is
+                if (!search_userdefs(LogFile, stackofs, n))
+                {
+                    if (!search_pointers(LogFile, stackofs, n))
+                    {
+                        fprintf(LogFile, "     %- 6d %-20.20s 0x%X (unknown)\n",
+                                autovar_def[n].stack_offset,
+                                autovar_def[n].name,
+                                autovar_def[n].type_idx);
+                    }
+                }
+            }
+        }
+        /* if (AutoVarsFound == FALSE)
+        {
+            fprintf(LogFile, "  No auto variables found in %s.\n", func_name);
+        } */
+        fprintf(LogFile, "\n");
+    }
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   PART 3: ANALYZE SYMBOL (.SYM) FILE                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ dbgPrintSYMInfo:
+ *      this gets called by dbgPrintStack if dbgPrintDebugInfo
+ *      failed (because no debug code was found) to check if
+ *      maybe a SYM file with the same filename exists and try
+ *      to get the info from there.
+ *
+ *      This gets called for every line of the stack
+ *      walk, but only if getting the information from
+ *      the debug code failed, e.g. because no debug code
+ *      was available for an address.
+ *
+ *      The file pointer is in the "Source file" column
+ *      every time this gets called.
+ *
+ *      New with V0.84.
+ *
+ *      Returns 0 if reading the SYM file was successful.
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: added return code; this used to be VOID
+ */
+
+int dbgPrintSYMInfo(FILE *LogFile,      // in: text log file to write to
+                    CHAR *SymFileName,  // in: SYM file name (can be fully q'fied)
+                    ULONG Object,
+                    ULONG TrapOffset)
+{
+    static FILE    *SymFile;
+    static MAPDEF   MapDef;
+    static SEGDEF   SegDef;
+    static SYMDEF32 SymDef32;
+    static SYMDEF16 SymDef16;
+    static char     Buffer[256];
+    static int      SegNum, SymNum, LastVal;
+    static unsigned long int SegOffset,
+                    SymOffset, SymPtrOffset;
+
+    // open .SYM file
+    SymFile = fopen(SymFileName, "rb");
+    if (SymFile == 0)
+        return (2);
+
+    // read in first map definition
+    fread(&MapDef, sizeof(MAPDEF), 1, SymFile);
+
+    SegOffset = SEGDEFOFFSET(MapDef);
+
+    // go thru all segments
+    for (SegNum = 0;
+         SegNum < MapDef.cSegs;
+         SegNum++)
+    {
+        // printf("Scanning segment #%d Offset %4.4hX\n",SegNum+1,SegOffset);
+        if (fseek(SymFile, SegOffset, SEEK_SET))
+            // seek error
+            return (3);
+
+        // read in segment definition
+        fread(&SegDef, sizeof(SEGDEF), 1, SymFile);
+        if (SegNum == Object)
+        {
+            // stack object found:
+            Buffer[0] = 0x00;
+            LastVal = 0;
+
+            // go thru all symbols in this object
+            for (SymNum = 0; SymNum < SegDef.cSymbols; SymNum++)
+            {
+
+                // read in symbol offset USHORT
+                SymPtrOffset = SYMDEFOFFSET(SegOffset, SegDef, SymNum);
+                fseek(SymFile, SymPtrOffset, SEEK_SET);
+                fread(&SymOffset, sizeof(unsigned short int), 1, SymFile);
+
+                // go to symbol definition
+                fseek(SymFile, SymOffset + SegOffset, SEEK_SET);
+
+                if (SegDef.bFlags & 0x01)
+                {
+                    // 32-bit symbol:
+                    fread(&SymDef32, sizeof(SYMDEF32), 1, SymFile);
+                    if (SymDef32.wSymVal > TrapOffset)
+                    {
+                        // symbol found
+                        fprintf(LogFile, "between %s + 0x%X ", Buffer, TrapOffset - LastVal);
+                        fprintf(LogFile, "(ppLineDef: 0x%lX) ",
+                                    LINEDEFOFFSET(SegDef)
+                                    );
+                        fprintf(LogFile, "\n");
+                    }
+
+                    LastVal = SymDef32.wSymVal;
+                    Buffer[0] = SymDef32.achSymName[0];
+                    fread(&Buffer[1], 1, SymDef32.cbSymName, SymFile);
+                    Buffer[SymDef32.cbSymName] = 0x00;
+
+                    if (SymDef32.wSymVal > TrapOffset)
+                    {
+                        // symbol found, as above
+                        fprintf(LogFile, "                                         "
+                                         "and %s - 0x%X ", Buffer, LastVal - TrapOffset);
+                        fprintf(LogFile, "\n");
+                        break;
+                    }
+                    /*printf("32 Bit Symbol <%s> Address %p\n",Buffer,SymDef32.wSymVal); */
+                }
+                else
+                {
+                    // 16-bit symbol:
+                    fread(&SymDef16, sizeof(SYMDEF16), 1, SymFile);
+                    if (SymDef16.wSymVal > TrapOffset)
+                    {
+                        fprintf(LogFile, "between %s + %X\n",
+                                         Buffer,
+                                         TrapOffset - LastVal);
+                    }
+                    LastVal = SymDef16.wSymVal;
+                    Buffer[0] = SymDef16.achSymName[0];
+                    fread(&Buffer[1], 1, SymDef16.cbSymName, SymFile);
+                    Buffer[SymDef16.cbSymName] = 0x00;
+                    if (SymDef16.wSymVal > TrapOffset)
+                    {
+                        fprintf(LogFile, "                                         "
+                                         "and %s - %X\n",
+                                         Buffer,
+                                         LastVal - TrapOffset);
+                        break;
+                    }
+                    /*printf("16 Bit Symbol <%s> Address %p\n",Buffer,SymDef16.wSymVal); */
+                }               // endif
+            }
+            break;
+        }                       // endif
+        SegOffset = NEXTSEGDEFOFFSET(SegDef);
+    }                           // endwhile
+    fclose(SymFile);
+    return (0);         // no error
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   PART 4: dbgPrintStack                                          *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ dbgPrintStackFrame:
+ *      parses and dumps one stack frame.
+ *      Called from excPrintStackFrame.
+ *
+ *      This calls dbgPrintDebugInfo and, if
+ *      that fails, dbgPrintSYMInfo.
+ *
+ *@@added V0.9.2 (2000-03-10) [umoeller]
+ *@@changed V0.9.3 (2000-04-10) [umoeller]: added support for non-Warp 4 SYM files
+ *@@changed V0.9.3 (2000-04-26) [umoeller]: this broke Warp 4 FP 13, fixed
+ */
+
+BOOL dbgPrintStackFrame(FILE *LogFile,
+                        PSZ pszModuleName,  // in: module name (fully q'fied)
+                        ULONG ulObject,
+                        ULONG ulOffset)
+{
+    APIRET arc = 0;
+    // "Source file"... columns
+
+    // first attempt to analyze the debug code
+    arc = dbgPrintDebugInfo(LogFile,
+                            pszModuleName,
+                            ulObject,
+                            ulOffset);
+    // if no debug code is available, analyze
+    // the SYM file instead
+    if (arc != NO_ERROR)
+    {
+        CHAR szSymName[CCHMAXPATH];
+        strcpy(szSymName, pszModuleName);
+        strcpy(szSymName + strlen(szSymName) - 3, "SYM");
+        arc = dbgPrintSYMInfo(LogFile,
+                              szSymName,
+                              ulObject,
+                              ulOffset);
+        if (arc != 0)
+        {
+            // SYM file not found in current directory:
+            // check the SYM files in the \OS2 directory,
+            // depending on the OS/2 version level:
+            CHAR szSymFile2[CCHMAXPATH];
+            PSZ  pszFilename = strrchr(szSymName, '\\');
+            if (pszFilename)
+            {
+                PSZ         pszVersionDir = "WARP4";
+                ULONG       aulBuf[3];
+
+                DosQuerySysInfo(QSV_VERSION_MAJOR,      // 11
+                                QSV_VERSION_MINOR,      // 12
+                                &aulBuf, sizeof(aulBuf));
+                // Warp 3 is reported as 20.30
+                // Warp 4 is reported as 20.40
+                // Aurora is reported as 20.45
+
+                if (aulBuf[0] == 20)
+                {
+                    if (aulBuf[1] == 30)
+                        // Warp 3:
+                        pszVersionDir = "WARP3";
+                    else if (aulBuf[1] >= 40)
+                        // Warp 4 or higher:
+                        // (NOTE: Warp 4 FP 13 now returns 45 also,
+                        // but the SYM files are still in the WARP4 directory...)
+                        // V0.9.3 (2000-04-26) [umoeller]
+                        pszVersionDir = "WARP4";
+                    /* else if (aulBuf[1] == 45)
+                        // Warp Server for e-Business (aka Warp 4.5):
+                        // we use the SYM files for the UNI kernel,
+                        // I have found no way to find out whether
+                        // we're running on an SMP kernel
+                        pszVersionDir = "WARP45_U"; */
+                }
+
+                pszFilename++;
+                sprintf(szSymFile2,
+                        "%c:\\OS2\\PDPSI\\PMDF\\%s\\%s",
+                        doshQueryBootDrive(),
+                        pszVersionDir,
+                        pszFilename);
+                arc = dbgPrintSYMInfo(LogFile,
+                                      szSymFile2,
+                                      ulObject,
+                                      ulOffset);
+
+                // V0.9.3 (2000-04-26) [umoeller]
+                if (    (arc != 0)  // still not found
+                     && (aulBuf[1] == 45) // and running Aurora or Warp 4 FP13?
+                   )
+                {
+                    // Warp Server for e-Business (aka Warp 4.5):
+                    // we use the SYM files for the UNI kernel,
+                    // I have found no way to find out whether
+                    // we're running on an SMP kernel
+                    sprintf(szSymFile2,
+                            "%c:\\OS2\\PDPSI\\PMDF\\%s\\%s",
+                            doshQueryBootDrive(),
+                            "WARP45_U",
+                            pszFilename);
+                    arc = dbgPrintSYMInfo(LogFile,
+                                          szSymFile2,
+                                          ulObject,
+                                          ulOffset);
+                }
+            }
+        }
+
+        if (arc == 2)       // file not found
+            fprintf(LogFile,
+                    "Cannot find symbol file %s\n",
+                    szSymName);
+        else if (arc != 0)
+            fprintf(LogFile,
+                    "Error %d reading symbol file %s\n",
+                    arc,
+                    szSymName);
+    }
+
+    return (arc == NO_ERROR);
+}
+
+/*
+ *@@ dbgPrintStack:
+ *      this takes stack data from the TIB and
+ *      context record data structures and tries
+ *      to analyse what the different stack frames
+ *      point to.
+ *
+ *      For each stack frame, this calls dbgPrintDebugInfo,
+ *      and, if that fails, dbgPrintSYMInfo.
+ *
+ *      New with V0.84.
+ *
+ *@@changed V0.9.2 (2000-03-08) [umoeller]: now searching OS2\PDPSI\PMDF for SYM files also
+ */
+
+VOID dbgPrintStack(FILE *LogFile,           // in: text log file to write to
+                   PUSHORT StackBottom,
+                   PUSHORT StackTop,
+                   PUSHORT Ebp,
+                   PUSHORT ExceptionAddress)
+{
+    PUSHORT         RetAddr = 0;
+    PUSHORT         LastEbp = 0;
+    APIRET          rc = 0;
+    ULONG           Size = 0,
+                    Attr = 0;
+    USHORT          Cs = 0,
+                    Ip = 0,
+                    // Bp,
+                    Sp = 0;
+    static char     Name[CCHMAXPATH];
+    HMODULE         hMod = 0;
+    ULONG           ObjNum = 0;
+    ULONG           Offset = 0;
+    BOOL            fExceptionAddress = TRUE;   // Use Exception Addr 1st time thru
+
+    // Note: we can't handle stacks bigger than 64K for now...
+    Sp = (USHORT) (((ULONG) StackBottom) >> 16);
+    // Bp = ;
+
+    if (!f32bit)
+        Ebp = (PUSHORT) MAKEULONG(((USHORT)(ULONG)Ebp), Sp);
+
+    fprintf(LogFile, "\n\nCall stack:\n");
+    fprintf(LogFile, "                                        Source    Line      Nearest\n");
+    fprintf(LogFile, "   EBP      Address    Module  Obj#      File     Numbr  Public Symbol\n");
+    fprintf(LogFile, " ÄÄÄÄÄÄÄÄ  ÄÄÄÄÄÄÄÄ-  ÄÄÄÄÄÄÄÄ ÄÄÄÄ  ÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄ  ÄÄÄÄÄÄÄÄÄÄÄÄ-\n");
+
+    do
+    {
+        Size = 10;
+        rc = DosQueryMem((PVOID) (Ebp + 2), &Size, &Attr);
+        if (rc != NO_ERROR)
+        {
+            fprintf(LogFile, "Invalid EBP %8.8p (DosQueryMem returned %d)\n", Ebp, rc);
+            break;
+        }
+        if (!(Attr & PAG_COMMIT))
+        {
+            fprintf(LogFile, "Invalid EBP %8.8p (not committed)\n", Ebp);
+            break;
+        }
+        if (Size < 10)
+        {
+            fprintf(LogFile, "Invalid EBP %8.8p (mem block size < 10)\n", Ebp);
+            break;
+        }
+
+        if (fExceptionAddress)
+            RetAddr = ExceptionAddress;
+        else
+            RetAddr = (PUSHORT) (*((PULONG) (Ebp + 2)));
+
+        if (RetAddr == (PUSHORT) 0x00000053)
+        {
+            // For some reason there's a "return address" of 0x53 following
+            // EBP on the stack and we have to adjust EBP by 44 bytes to get
+            // at the real return address.  This has something to do with
+            // thunking from 32bits to 16bits...
+            // Serious kludge, and it's probably dependent on versions of C(++)
+            // runtime or OS, but it works for now!
+            Ebp += 22;
+            RetAddr = (PUSHORT) (*((PULONG) (Ebp + 2)));
+        }
+
+        // Get the (possibly) 16bit CS and IP
+        if (fExceptionAddress)
+        {
+            Cs = (USHORT) (((ULONG) ExceptionAddress) >> 16);
+            Ip = (USHORT) (ULONG) ExceptionAddress;
+        }
+        else
+        {
+            Cs = *(Ebp + 2);
+            Ip = *(Ebp + 1);
+        }
+
+        // if the return address points to the stack then it's really just
+        // a pointer to the return address (UGH!).
+        if (    (USHORT) (((ULONG) RetAddr) >> 16) == Sp
+           )
+            RetAddr = (PUSHORT) (*((PULONG) RetAddr));
+
+        if (Ip == 0 && *Ebp == 0)
+        {
+            // End of the stack so these are both shifted by 2 bytes:
+            Cs = *(Ebp + 3);
+            Ip = *(Ebp + 2);
+        }
+
+        // 16bit programs have on the stack:
+        //   BP:IP:CS
+        //   where CS may be thunked
+        //
+        //         in dump                 swapped
+        //    BP        IP   CS          BP   CS   IP
+        //   4677      53B5 F7D0        7746 D0F7 B553
+        //
+        // 32bit programs have:
+        //   EBP:EIP
+        // and you'd have something like this (with SP added) (not
+        // accurate values)
+        //
+        //         in dump               swapped
+        //      EBP       EIP         EBP       EIP
+        //   4677 2900 53B5 F7D0   0029 7746 D0F7 B553
+        //
+        // So the basic difference is that 32bit programs have a 32bit
+        // EBP and we can attempt to determine whether we have a 32bit
+        // EBP by checking to see if its 'selector' is the same as SP.
+        // Note that this technique limits us to checking stacks < 64K.
+        //
+        // Soooo, if IP (which maps into the same USHORT as the swapped
+        // stack page in EBP) doesn't point to the stack (i.e. it could
+        // be a 16bit IP) then see if CS is valid (as is or thunked).
+        //
+        // Note that there's the possibility of a 16bit return address
+        // that has an offset that's the same as SP so we'll think it's
+        // a 32bit return address and won't be able to successfully resolve
+        // its details.
+        if (Ip != Sp)
+        {
+            if (DOS16SIZESEG(Cs, &Size) == NO_ERROR)
+            {
+                RetAddr = (USHORT * _Seg16) MAKEULONG(Ip, Cs);
+                f32bit = FALSE;
+            }
+            else if (DOS16SIZESEG((Cs << 3) + 7, &Size) == NO_ERROR)
+            {
+                Cs = (Cs << 3) + 7;
+                RetAddr = (USHORT * _Seg16) MAKEULONG(Ip, Cs);
+                f32bit = FALSE;
+            }
+            else
+                f32bit = TRUE;
+        }
+        else
+            f32bit = TRUE;
+
+
+        // "EBP" column
+        if (fExceptionAddress)
+            fprintf(LogFile, " Trap  ->  ");
+        else
+            fprintf(LogFile, " %8.8p  ", Ebp);
+
+        // "Address" column
+        if (f32bit)
+            fprintf(LogFile, ":%8.8p  ", RetAddr);
+        else
+            fprintf(LogFile, "%04.04X:%04.04X  ", Cs, Ip);
+
+        // Version check omitted; the following requires
+        // OS/2 2.10 or later (*UM)
+        // if (Version[0] >= 20 && Version[1] >= 10)
+        {
+            // Make a 'tick' sound to let the user know we're still alive
+            DosBeep(2000, 10);
+
+            Size = 10;    // Inserted by Kim Rasmussen 26/06 1996 to avoid error 87 when Size is 0
+
+            // "Module"/"Object" columns
+            rc = DosQueryMem((PVOID) RetAddr, &Size, &Attr);
+            if (rc != NO_ERROR || !(Attr & PAG_COMMIT))
+            {
+                fprintf(LogFile, "Invalid RetAddr: %8.8p\n", RetAddr);
+                break;          // avoid infinite loops
+            }
+            else
+            {
+                rc = DOSQUERYMODFROMEIP(&hMod,
+                                        &ObjNum,
+                                        sizeof(Name), Name,
+                                        &Offset,
+                                        (PVOID)RetAddr);
+                if (    (rc == NO_ERROR)
+                     // && (ObjNum != -1)
+                   )
+                {
+                    // static char     szJunk[_MAX_FNAME];
+                    static char     szName[_MAX_FNAME];
+
+                    DosQueryModuleName(hMod, sizeof(Name), Name);
+                    // _splitpath(Name, szJunk, szJunk, szName, szJunk);
+
+                    // print module and object
+                    fprintf(LogFile, "%-8s %04X  ", szName, ObjNum + 1);
+
+                    if (strlen(Name) > 3)
+                    {
+                        dbgPrintStackFrame(LogFile,
+                                           Name,
+                                           ObjNum,
+                                           Offset);
+                    }
+                }
+                else
+                    fprintf(LogFile,
+                            "DosQueryModFromEIP failed, returned %d\n",
+                            rc);
+            }
+        }
+
+        if (    ((*Ebp) == 0)
+             && ((*Ebp + 1) == 0)
+           )
+        {
+            fprintf(LogFile, "End of call stack\n");
+            break;
+        }
+
+        if (!fExceptionAddress)
+        {
+            LastEbp = Ebp;
+#if 0
+            Ebp = (PUSHORT) MAKEULONG(Bp, Sp);
+#else // Inserted by Kim Rasmussen 26/06 1996 to allow big stacks
+            if (f32bit)
+                Ebp = (PUSHORT) *(PULONG) LastEbp;
+            else
+                Ebp = (PUSHORT) MAKEULONG((*Ebp), Sp);
+#endif
+            if (f32bit)
+            {
+                dbgPrintVariables(LogFile, (ULONG) Ebp);
+            }                   // endif
+
+            if (Ebp < LastEbp)
+            {
+                fprintf(LogFile, "... lost stack chain - new EBP below previous\n");
+                break;
+            }
+        }
+        else
+            fExceptionAddress = FALSE;
+
+        Size = 4;
+        rc = DosQueryMem((PVOID) Ebp, &Size, &Attr);
+        if ((rc != NO_ERROR) || (Size < 4))
+        {
+            fprintf(LogFile, "... lost stack chain - invalid EBP: %8.8p\n", Ebp);
+            break;
+        }
+    } while (TRUE);
+
+    fprintf(LogFile, "\n");
+}
+
Index: /trunk/src/helpers/dosh.c
===================================================================
--- /trunk/src/helpers/dosh.c	(revision 8)
+++ /trunk/src/helpers/dosh.c	(revision 8)
@@ -0,0 +1,1650 @@
+
+/*
+ *@@sourcefile dosh.c:
+ *      dosh.c contains Control Program helper functions.
+ *
+ *      This file has miscellaneous system functions,
+ *      drive helpers, file helpers, and partition functions.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  dosh*   Dos (Control Program) helper functions
+ *
+ *      These funcs are forward-declared in dosh.h, which
+ *      must be #include'd first.
+ *
+ *      The resulting dosh.obj object file can be linked
+ *      against any application object file. As opposed to
+ *      the code in dosh2.c, it does not require any other
+ *      code from the helpers.
+ *
+ *      dosh.obj can also be used with the VAC subsystem
+ *      library (/rn compiler option).
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\dosh.h"
+ */
+
+/*
+ *      This file Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSDEVIOCTL
+#define INCL_DOSERRORS
+#define INCL_KBD
+#include <os2.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+// #include <ctype.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+
+#pragma hdrstop
+
+const CHAR  G_acDriveLetters[28] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+/*
+ *@@category: Helpers\Control program helpers\Miscellaneous
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Miscellaneous                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshGetChar:
+ *      reads a single character from the keyboard.
+ *      Useful for VIO sessions, since there's great
+ *      confusion between the various C dialects about
+ *      how to use getc(), and getc() doesn't work
+ *      with the VAC subsystem library.
+ *
+ *@@added V0.9.4 (2000-07-27) [umoeller]
+ */
+
+CHAR doshGetChar(VOID)
+{
+    // CHAR    c;
+    // ULONG   ulRead = 0;
+
+    KBDKEYINFO kki;
+    KbdCharIn(&kki,
+              0, // wait
+              0);
+
+    return (kki.chChar);
+}
+
+/*
+ *@@ doshQueryShiftState:
+ *      returns TRUE if any of the SHIFT keys are
+ *      currently pressed. Useful for checks during
+ *      WM_COMMAND messages from menus.
+ *
+ *@@changed V0.9.5 (2000-09-27) [umoeller]: added error checking
+ */
+
+BOOL doshQueryShiftState(VOID)
+{
+    BOOL            brc = FALSE;
+    APIRET          arc = NO_ERROR;
+    HFILE           hfKbd;
+    ULONG           ulAction;
+
+    arc = DosOpen("KBD$", &hfKbd, &ulAction, 0,
+                  FILE_NORMAL, FILE_OPEN,
+                  OPEN_ACCESS_READONLY | OPEN_SHARE_DENYWRITE,
+                  (PEAOP2)NULL);
+    if (arc == NO_ERROR)
+    {
+        SHIFTSTATE      ShiftState;
+        ULONG           cbDataLen = sizeof(ShiftState);
+
+        arc = DosDevIOCtl(hfKbd, IOCTL_KEYBOARD, KBD_GETSHIFTSTATE,
+                          NULL, 0, NULL,      // no parameters
+                          &ShiftState, cbDataLen, &cbDataLen);
+        if (arc == NO_ERROR)
+            brc = ((ShiftState.fsState & 3) != 0);
+
+        DosClose(hfKbd);
+    }
+
+    return brc;
+}
+
+
+/*
+ *@@ doshIsWarp4:
+ *      returns TRUE only if at least OS/2 Warp 4 is running.
+ *
+ *@@changed V0.9.2 (2000-03-05) [umoeller]: reported TRUE on Warp 3 also; fixed
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: patched for speed
+ */
+
+BOOL doshIsWarp4(VOID)
+{
+    static BOOL s_brc = FALSE;
+    static BOOL s_fQueried = FALSE;
+    if (!s_fQueried)
+    {
+        // first call:
+        ULONG       aulBuf[3];
+
+        DosQuerySysInfo(QSV_VERSION_MAJOR,      // 11
+                        QSV_VERSION_MINOR,      // 12
+                        &aulBuf, sizeof(aulBuf));
+        // Warp 3 is reported as 20.30
+        // Warp 4 is reported as 20.40
+        // Aurora is reported as 20.45
+
+        if     (    (aulBuf[0] > 20)        // major > 20; not the case with Warp 3, 4, 5
+                 || (   (aulBuf[0] == 20)   // major == 20 and minor >= 40
+                     && (aulBuf[1] >= 40)
+                    )
+               )
+            s_brc = TRUE;
+
+        s_fQueried = TRUE;
+    }
+
+    return (s_brc);
+}
+
+/*
+ *@@ doshQuerySysErrorMsg:
+ *      this retrieves the error message for a system error
+ *      (APIRET) from the system error message file (OSO001.MSG).
+ *      This file better be on the DPATH (it normally is).
+ *
+ *      This returns the string in the "SYSxxx: blahblah" style,
+ *      which is normally displayed on the command line when
+ *      errors occur.
+ *
+ *      The error message is returned in a newly allocated
+ *      buffer, which should be free()'d afterwards.
+ *
+ *      Returns NULL upon errors.
+ */
+
+PSZ doshQuerySysErrorMsg(APIRET arc)    // in: DOS error code
+{
+    PSZ     pszReturn = 0;
+    CHAR    szDosError[1000];
+    ULONG   cbDosError = 0;
+    DosGetMessage(NULL, 0,       // no string replacements
+                  szDosError, sizeof(szDosError),
+                  arc,
+                  "OSO001.MSG",        // default OS/2 message file
+                  &cbDosError);
+    if (cbDosError > 2)
+    {
+        szDosError[cbDosError - 2] = 0;
+        pszReturn = strdup(szDosError);
+    }
+    return (pszReturn);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Memory management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Memory helpers                                                 *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshAllocSharedMem:
+ *      wrapper for DosAllocSharedMem which has
+ *      a malloc()-like syntax. Just due to my
+ *      lazyness.
+ *
+ *      Note that ulSize is always rounded up to the
+ *      next 4KB value, so don't use this hundreds of times.
+ *
+ *      Returns NULL upon errors. Possible errors include
+ *      that a memory block calle pcszName has already been
+ *      allocated.
+ *
+ *      Use DosFreeMem(pvrc) to free the memory. The memory
+ *      will only be freed if no other process has requested
+ *      access.
+ *
+ *@@added V0.9.3 (2000-04-18) [umoeller]
+ */
+
+PVOID doshAllocSharedMem(ULONG ulSize,      // in: requested mem block size (rounded up to 4KB)
+                         const char* pcszName) // in: name of block ("\\SHAREMEM\\xxx") or NULL
+{
+    PVOID   pvrc = NULL;
+    APIRET  arc = DosAllocSharedMem((PVOID*)(&pvrc),
+                                    (PSZ)pcszName,
+                                    ulSize,
+                                    PAG_COMMIT | PAG_READ | PAG_WRITE);
+    if (arc == NO_ERROR)
+        return (pvrc);
+
+    return (NULL);
+}
+
+/*
+ *@@ doshRequestSharedMem:
+ *      requests access to a block of named shared memory
+ *      allocated by doshAllocSharedMem.
+ *
+ *      Returns NULL upon errors.
+ *
+ *      Use DosFreeMem(pvrc) to free the memory. The memory
+ *      will only be freed if no other process has requested
+ *      access.
+ *
+ *@@added V0.9.3 (2000-04-19) [umoeller]
+ */
+
+PVOID doshRequestSharedMem(const char *pcszName)
+{
+    PVOID pvrc = NULL;
+    APIRET arc = DosGetNamedSharedMem((PVOID*)(pvrc),
+                                      (PSZ)pcszName,
+                                      PAG_READ | PAG_WRITE);
+    if (arc == NO_ERROR)
+        return (pvrc);
+
+    return (NULL);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Drive management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Drive helpers                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshEnumDrives:
+ *      this function enumerates all valid drive letters on
+ *      the system by composing a string of drive letters
+ *      in the buffer pointed to by pszBuffer, which should
+ *      be 27 characters in size to hold information for
+ *      all drives. The buffer will be null-terminated.
+ *
+ *      If (pcszFileSystem != NULL), only drives matching
+ *      the specified file system type (e.g. "HPFS") will
+ *      be enumerated. If (pcszFileSystem == NULL), all
+ *      drives will be enumerated.
+ *
+ *      If (fSkipRemovables == TRUE), removeable drives will
+ *      be skipped. This applies to floppy, CD-ROM, and
+ *      virtual floppy drives. This will start the search
+ *      at drive letter C: so that drives A: and B: will
+ *      never be checked (to avoid the hardware bumps).
+ *
+ *      Otherwise, the search starts at drive A:. Still,
+ *      removeable drives will only be added if valid media
+ *      is inserted.
+ *
+ *@@changed V0.9.4 (2000-07-03) [umoeller]: this stopped at the first invalid drive letter; fixed
+ *@@changed V0.9.4 (2000-07-03) [umoeller]: added fSkipRemoveables
+ */
+
+VOID doshEnumDrives(PSZ pszBuffer,      // out: drive letters
+                    const char *pcszFileSystem,  // in: FS's to match or NULL
+                    BOOL fSkipRemoveables) // in: if TRUE, only non-removeable disks will be returned
+{
+    CHAR szName[5] = "";
+    ULONG ulLogicalDrive = 1, // start with drive A:
+          ulFound = 0;        // found drives count
+    APIRET arc             = NO_ERROR; // return code
+
+    if (fSkipRemoveables)
+        // start with drive C:
+        ulLogicalDrive = 3;
+
+    // go thru the drives, start with C: (== 3), stop after Z: (== 26)
+    while (ulLogicalDrive <= 26)
+    {
+        UCHAR nonRemovable=0;
+        ULONG parmSize=2;
+        ULONG dataLen=1;
+
+        #pragma pack(1)
+        struct
+        {
+            UCHAR dummy,drive;
+        } parms;
+        #pragma pack()
+
+        parms.drive=(UCHAR)(ulLogicalDrive-1);
+        arc = DosDevIOCtl((HFILE)-1,
+                          IOCTL_DISK,
+                          DSK_BLOCKREMOVABLE,
+                          &parms,
+                          parmSize,
+                          &parmSize,
+                          &nonRemovable,
+                          1,
+                          &dataLen);
+
+        /* _Pmpf(("  ul = %d, Drive %c: arc = %d nonRemoveable = %d",
+                    ulLogicalDrive,
+                    G_acDriveLetters[ulLogicalDrive],
+                    arc,
+                    nonRemovable)); */
+
+        if (    // fixed disk and non-removeable
+                ((arc == NO_ERROR) && (nonRemovable))
+                // or network drive:
+             || (arc == ERROR_NOT_SUPPORTED)
+           )
+        {
+            ULONG  ulOrdinal       = 0;     // ordinal of entry in name list
+            BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
+            ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
+            PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;
+
+            szName[0] = G_acDriveLetters[ulLogicalDrive];
+            szName[1] = ':';
+            szName[2] = '\0';
+
+            arc = DosQueryFSAttach(szName,          // logical drive of attached FS
+                                   ulOrdinal,       // ignored for FSAIL_QUERYNAME
+                                   FSAIL_QUERYNAME, // return data for a Drive or Device
+                                   pfsqBuffer,      // returned data
+                                   &cbBuffer);      // returned data length
+
+            if (arc == NO_ERROR)
+            {
+                // The data for the last three fields in the FSQBUFFER2
+                // structure are stored at the offset of fsqBuffer.szName.
+                // Each data field following fsqBuffer.szName begins
+                // immediately after the previous item.
+                CHAR* pszFSDName = (PSZ)&(pfsqBuffer->szName) + (pfsqBuffer->cbName) + 1;
+                if (pcszFileSystem == NULL)
+                {
+                    // enum-all mode: always copy
+                    pszBuffer[ulFound] = szName[0]; // drive letter
+                    ulFound++;
+                }
+                else if (strcmp(pszFSDName, pcszFileSystem) == 0)
+                {
+                    pszBuffer[ulFound] = szName[0]; // drive letter
+                    ulFound++;
+                }
+            }
+        }
+
+        ulLogicalDrive++;
+    } // end while (G_acDriveLetters[ulLogicalDrive] <= 'Z')
+
+    pszBuffer[ulFound] = '\0';
+}
+
+/*
+ *@@ doshQueryBootDrive:
+ *      returns the letter of the boot drive as a
+ *      single (capital) character, which is useful for
+ *      constructing file names using sprintf and such.
+ */
+
+CHAR doshQueryBootDrive(VOID)
+{
+    ULONG ulBootDrive;
+    DosQuerySysInfo(5, 5, &ulBootDrive, sizeof(ulBootDrive));
+    return (G_acDriveLetters[ulBootDrive]);
+}
+
+/*
+ *@@ doshAssertDrive:
+ *      this checks for whether the given drive
+ *      is currently available without provoking
+ *      those ugly white "Drive not ready" popups.
+ *
+ *      This returns (from my testing):
+ *      -- NO_ERROR: drive is available.
+ *      -- ERROR_INVALID_DRIVE (15): drive letter does not exist.
+ *      -- ERROR_NOT_READY (21): drive exists, but is not ready
+ *                  (e.g. CD-ROM drive without CD inserted).
+ *      -- ERROR_NOT_SUPPORTED (50): this is returned by the RAMFS.IFS
+ *                  file system; apparently, the IFS doesn't support
+ *                  DASD opening.
+ *
+ *@@changed V0.9.1 (99-12-13) [umoeller]: rewritten, prototype changed. Now using DosOpen on the drive instead of DosError.
+ *@@changed V0.9.1 (2000-01-08) [umoeller]: DosClose was called even if DosOpen failed, which messed up OS/2 error handling.
+ *@@changed V0.9.1 (2000-02-09) [umoeller]: this didn't work for network drives, including RAMFS; fixed.
+ *@@changed V0.9.3 (2000-03-28) [umoeller]: added check for network drives, which weren't working
+ *@@changed V0.9.4 (2000-08-03) [umoeller]: more network fixes
+ */
+
+APIRET doshAssertDrive(ULONG ulLogicalDrive) // in: 1 for A:, 2 for B:, 3 for C:, ...
+{
+    CHAR    szDrive[3] = "C:";
+    HFILE   hfDrive = 0;
+    ULONG   ulTemp = 0;
+    APIRET  arc;
+
+    szDrive[0] = 'A' + ulLogicalDrive - 1;
+
+    arc = DosOpen(szDrive,   // "C:", "D:", ...
+                  &hfDrive,
+                  &ulTemp,
+                  0,
+                  FILE_NORMAL,
+                  OPEN_ACTION_FAIL_IF_NEW
+                         | OPEN_ACTION_OPEN_IF_EXISTS,
+                  OPEN_FLAGS_DASD
+                         | OPEN_FLAGS_FAIL_ON_ERROR
+                         | OPEN_ACCESS_READONLY
+                         | OPEN_SHARE_DENYNONE,
+                  NULL);
+
+    switch (arc)
+    {
+        case ERROR_NETWORK_ACCESS_DENIED: // 65
+            // added V0.9.3 (2000-03-27) [umoeller];
+            // according to user reports, this is returned
+            // by all network drives, which apparently don't
+            // support DASD DosOpen
+        case ERROR_ACCESS_DENIED: // 5
+            // added V0.9.4 (2000-07-10) [umoeller]
+            // LAN drives still didn't work... apparently
+            // the above only works for NFS drives
+        case ERROR_PATH_NOT_FOUND: // 3
+            // added V0.9.4 (2000-08-03) [umoeller]:
+            // this is returned by some other network types...
+            // sigh...
+        case ERROR_NOT_SUPPORTED: // 50
+        {
+            // this is returned by file systems which don't
+            // support DASD DosOpen;
+            // use some other method then, this isn't likely
+            // to fail -- V0.9.1 (2000-02-09) [umoeller]
+            FSALLOCATE  fsa;
+            arc = DosQueryFSInfo(ulLogicalDrive,
+                                 FSIL_ALLOC,
+                                 &fsa,
+                                 sizeof(fsa));
+        break; }
+
+        case NO_ERROR:
+            DosClose(hfDrive);
+        break;
+    }
+
+    return (arc);
+
+    /* FSALLOCATE  fsa;
+    APIRET arc = NO_ERROR;
+
+    if (fSuppress)
+    {
+        DosError(FERR_DISABLEHARDERR | FERR_DISABLEEXCEPTION);
+        DosEnterCritSec();
+    }
+
+    arc = DosQueryFSInfo(ulLogicalDrive, FSIL_ALLOC, &fsa, sizeof(fsa));
+
+    if (fSuppress)
+    {
+        DosError(FERR_ENABLEHARDERR | FERR_ENABLEEXCEPTION);
+        DosExitCritSec();
+    }
+
+    return (arc); */
+}
+
+/*
+ *@@ doshQueryDiskFree:
+ *       returns the number of bytes remaining on the disk
+ *       specified by the given logical drive, or -1 upon errors.
+ *
+ *       Note: This returns a "double" value, because a ULONG
+ *       can only hold values of some 4 billion, which would
+ *       lead to funny results for drives > 4 GB.
+ *
+ *@@changed V0.9.0 [umoeller]: fixed another > 4 GB bug (thanks to Rdiger Ihle)
+ */
+
+double doshQueryDiskFree(ULONG ulLogicalDrive) // in: 1 for A:, 2 for B:, 3 for C:, ...
+{
+    FSALLOCATE  fsa;
+    double      dbl = -1;
+
+    if (ulLogicalDrive)
+        if (DosQueryFSInfo(ulLogicalDrive, FSIL_ALLOC, &fsa, sizeof(fsa))
+                == NO_ERROR)
+            dbl = ((double)fsa.cSectorUnit * fsa.cbSector * fsa.cUnitAvail);
+                   // ^ fixed V0.9.0
+
+    return (dbl);
+}
+
+/*
+ *@@ doshQueryDiskFSType:
+ *       copies the file-system type of the given disk object
+ *       (HPFS, FAT, CDFS etc.) to pszBuf.
+ *       Returns the DOS error code.
+ *
+ *@@changed V0.9.1 (99-12-12) [umoeller]: added cbBuf to prototype
+ */
+
+APIRET doshQueryDiskFSType(ULONG ulLogicalDrive, // in:  1 for A:, 2 for B:, 3 for C:, ...
+                           PSZ pszBuf,           // out: buffer for FS type
+                           ULONG cbBuf)          // in: size of that buffer
+{
+    APIRET arc = NO_ERROR;
+    CHAR szName[5];
+
+    BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
+    ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
+    PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;
+
+    // compose "D:"-type string from logical drive letter
+    szName[0] = G_acDriveLetters[ulLogicalDrive];
+    szName[1] = ':';
+    szName[2] = '\0';
+
+    arc = DosQueryFSAttach(szName,          // logical drive of attached FS ("D:"-style)
+                           0,               // ulOrdinal, ignored for FSAIL_QUERYNAME
+                           FSAIL_QUERYNAME, // return name for a drive or device
+                           pfsqBuffer,      // buffer for returned data
+                           &cbBuffer);      // sizeof(*pfsqBuffer)
+
+    if (arc == NO_ERROR)
+    {
+        if (pszBuf)
+        {
+            // The data for the last three fields in the FSQBUFFER2
+            // structure are stored at the offset of fsqBuffer.szName.
+            // Each data field following fsqBuffer.szName begins
+            // immediately after the previous item.
+            strcpy(pszBuf,
+                   (CHAR*)(&pfsqBuffer->szName) + pfsqBuffer->cbName + 1);
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshIsFixedDisk:
+ *      checks whether a disk is fixed or removeable.
+ *      ulLogicalDrive must be 1 for drive A:, 2 for B:, ...
+ *      The result is stored in *pfFixed.
+ *      Returns DOS error code.
+ *
+ *      Warning: This uses DosDevIOCtl, which has proved
+ *      to cause problems with some device drivers for
+ *      removeable disks.
+ */
+
+APIRET doshIsFixedDisk(ULONG  ulLogicalDrive,   // in: 1 for A:, 2 for B:, 3 for C:, ...
+                       PBOOL  pfFixed)          // out: TRUE for fixed disks
+{
+    APIRET arc = ERROR_INVALID_DRIVE;
+
+    if (ulLogicalDrive)
+    {
+        // parameter packet
+        #pragma pack(1)
+        struct {
+            UCHAR command, drive;
+        } parms;
+        #pragma pack()
+
+        // data packet
+        UCHAR ucNonRemoveable;
+
+        ULONG ulParmSize = sizeof(parms);
+        ULONG ulDataSize = sizeof(ucNonRemoveable);
+
+        parms.drive = (UCHAR)(ulLogicalDrive-1);
+        arc = DosDevIOCtl((HFILE)-1,
+                          IOCTL_DISK,
+                          DSK_BLOCKREMOVABLE,
+                          &parms,
+                          ulParmSize,
+                          &ulParmSize,
+                          &ucNonRemoveable,
+                          ulDataSize,
+                          &ulDataSize);
+
+        if (arc == NO_ERROR)
+            *pfFixed = (BOOL)ucNonRemoveable;
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshQueryDiskParams:
+ *      this retrieves more information about a given drive,
+ *      which is stored in the specified DRIVEPARAMS structure
+ *      (dosh.h).
+ *
+ *      Warning: This uses DosDevIOCtl, which has proved
+ *      to cause problems with some device drivers for
+ *      removeable disks.
+ *
+ *      This returns the DOS error code of DosDevIOCtl.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshQueryDiskParams(ULONG ulLogicalDrive,        // in:  1 for A:, 2 for B:, 3 for C:, ...
+                           PDRIVEPARAMS pdp)            // out: drive parameters
+{
+    APIRET arc = ERROR_INVALID_DRIVE;
+
+    if (ulLogicalDrive)
+    {
+        #pragma pack(1)
+        // parameter packet
+        struct {
+            UCHAR command, drive;
+        } parms;
+        #pragma pack()
+
+        ULONG ulParmSize = sizeof(parms);
+        ULONG ulDataSize = sizeof(DRIVEPARAMS);
+
+        parms.command = 1; // read currently inserted media
+        parms.drive=(UCHAR)(ulLogicalDrive-1);
+
+        arc = DosDevIOCtl((HFILE)-1,
+                          IOCTL_DISK,
+                          DSK_GETDEVICEPARAMS,
+                          // parameter packet:
+                          &parms, ulParmSize, &ulParmSize,
+                          // data packet: DRIVEPARAMS structure
+                          pdp,    ulDataSize, &ulDataSize);
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshQueryDiskLabel:
+ *      this returns the label of a disk into
+ *      *pszVolumeLabel, which must be 12 bytes
+ *      in size.
+ *
+ *      This function was added because the Toolkit
+ *      information for DosQueryFSInfo is only partly
+ *      correct. On OS/2 2.x, that function does not
+ *      take an FSINFO structure as input, but a VOLUMELABEL.
+ *      On Warp, this does take an FSINFO.
+ *
+ *      DosSetFSInfo is even worse. See doshSetDiskLabel.
+ *
+ *      See http://zebra.asta.fh-weingarten.de/os2/Snippets/Bugi6787.HTML
+ *      for details.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshQueryDiskLabel(ULONG ulLogicalDrive,         // in:  1 for A:, 2 for B:, 3 for C:, ...
+                          PSZ pszVolumeLabel)           // out: volume label (must be 12 chars in size)
+{
+    APIRET      arc;
+
+    #ifdef __OS2V2X__
+        VOLUMELABEL FSInfoBuf;
+    #else
+        FSINFO      FSInfoBuf;
+    #endif
+
+    arc = DosQueryFSInfo(ulLogicalDrive,
+                         FSIL_VOLSER,
+                         &FSInfoBuf,
+                         sizeof(FSInfoBuf)); // depends
+
+    #ifdef __OS2V2X__
+        strcpy(pszVolumeLabel, FSInfoBuf.szVolLabel);
+    #else
+        strcpy(pszVolumeLabel, FSInfoBuf.vol.szVolLabel);
+    #endif
+
+    return (arc);
+}
+
+/*
+ *@@ doshSetDiskLabel:
+ *      this sets the label of a disk.
+ *
+ *      This function was added because the Toolkit
+ *      information for DosSetFSInfo is flat out wrong.
+ *      That function does not take an FSINFO structure
+ *      as input, but a VOLUMELABEL. As a result, using
+ *      that function with the Toolkit's calling specs
+ *      results in ERROR_LABEL_TOO_LONG always.
+ *
+ *      See http://zebra.asta.fh-weingarten.de/os2/Snippets/Bugi6787.HTML
+ *      for details.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshSetDiskLabel(ULONG ulLogicalDrive,        // in:  1 for A:, 2 for B:, 3 for C:, ...
+                        PSZ pszNewLabel)
+{
+    VOLUMELABEL FSInfoBuf;
+
+    // check length; 11 chars plus null byte allowed
+    FSInfoBuf.cch = (BYTE)strlen(pszNewLabel);
+    if (FSInfoBuf.cch < sizeof(FSInfoBuf.szVolLabel))
+    {
+        strcpy(FSInfoBuf.szVolLabel, pszNewLabel);
+
+        return (DosSetFSInfo(ulLogicalDrive,
+                             FSIL_VOLSER,
+                             &FSInfoBuf,
+                             sizeof(FSInfoBuf)));
+    }
+    else
+        return (ERROR_LABEL_TOO_LONG);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\File management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   File helpers                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshGetExtension:
+ *      finds the file name extension of pszFilename,
+ *      which can be a file name only or a fully
+ *      qualified filename.
+ *
+ *      This returns a pointer into pszFilename to
+ *      the character after the last dot.
+ *
+ *      Returns NULL if not found (e.g. if the filename
+ *      has no dot in it).
+ *
+ *@@added V0.9.6 (2000-10-16) [umoeller]
+ */
+
+PSZ doshGetExtension(const char *pcszFilename)
+{
+    PSZ pszExtension = 0;
+
+    if (pcszFilename)
+    {
+        // find filename
+        PSZ p2 = strrchr(pcszFilename + 2, '\\'),
+                    // works on "C:\blah" or "\\unc\blah"
+            p3 = NULL;
+
+        if (!p2)
+            // no backslash found: then this is not qualified...
+            // use start of filename
+            p2 = (PSZ)pcszFilename;
+
+        // find last dot in filename
+        p3 = strrchr(p2 + 1, '.');
+        if (p3)
+            pszExtension = p3 + 1;
+    }
+
+    return (pszExtension);
+}
+
+/*
+ *@@ doshIsFileOnFAT:
+ *      returns TRUE if pszFileName resides on
+ *      a FAT drive. Note that pszFileName must
+ *      be fully qualified (i.e. the drive letter
+ *      must be the first character), or this will
+ *      return garbage.
+ */
+
+BOOL doshIsFileOnFAT(const char* pcszFileName)
+{
+    BOOL brc = FALSE;
+    CHAR szName[5];
+
+    APIRET rc              = NO_ERROR; // return code
+    BYTE   fsqBuffer[sizeof(FSQBUFFER2) + (3 * CCHMAXPATH)] = {0};
+    ULONG  cbBuffer   = sizeof(fsqBuffer);        // Buffer length)
+    PFSQBUFFER2 pfsqBuffer = (PFSQBUFFER2)fsqBuffer;
+
+    szName[0] = pcszFileName[0];    // copy drive letter
+    szName[1] = ':';
+    szName[2] = '\0';
+
+    rc = DosQueryFSAttach(szName,          // logical drive of attached FS
+                          0,               // ulOrdinal, ignored for FSAIL_QUERYNAME
+                          FSAIL_QUERYNAME, // return data for a Drive or Device
+                          pfsqBuffer,      // returned data
+                          &cbBuffer);      // returned data length
+
+    if (rc == NO_ERROR)
+    {
+        // The data for the last three fields in the FSQBUFFER2
+        // structure are stored at the offset of fsqBuffer.szName.
+        // Each data field following fsqBuffer.szName begins
+        // immediately after the previous item.
+        if (strncmp((PSZ)&(pfsqBuffer->szName) + pfsqBuffer->cbName + 1,
+                    "FAT",
+                    3)
+               == 0)
+            brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ doshQueryFileSize:
+ *      returns the size of an already opened file
+ *      or 0 upon errors.
+ *      Use doshQueryPathSize to query the size of
+ *      any file.
+ */
+
+ULONG doshQueryFileSize(HFILE hFile)
+{
+    FILESTATUS3 fs3;
+    if (DosQueryFileInfo(hFile, FIL_STANDARD, &fs3, sizeof(fs3)))
+        return (0);
+    else
+        return (fs3.cbFile);
+}
+
+/*
+ *@@ doshQueryPathSize:
+ *      returns the size of any file,
+ *      or 0 if the file could not be
+ *      found.
+ *      Use doshQueryFileSize instead to query the
+ *      size if you have a HFILE.
+ */
+
+ULONG doshQueryPathSize(PSZ pszFile)
+{
+    FILESTATUS3 fs3;
+    if (DosQueryPathInfo(pszFile, FIL_STANDARD, &fs3, sizeof(fs3)))
+        return (0);
+    else
+        return (fs3.cbFile);
+}
+
+/*
+ *@@ doshQueryPathAttr:
+ *      returns the file attributes of pszFile,
+ *      which can be fully qualified. The
+ *      attributes will be stored in *pulAttr.
+ *      pszFile can also specify a directory,
+ *      although not all attributes make sense
+ *      for directories.
+ *
+ *      fAttr can be:
+ *      --  FILE_ARCHIVED
+ *      --  FILE_READONLY
+ *      --  FILE_SYSTEM
+ *      --  FILE_HIDDEN
+ *
+ *      This returns the APIRET of DosQueryPathAttr.
+ *      *pulAttr is only valid if NO_ERROR is
+ *      returned.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshQueryPathAttr(const char* pcszFile,      // in: file or directory name
+                         PULONG pulAttr)            // out: attributes
+{
+    FILESTATUS3 fs3;
+    APIRET arc = DosQueryPathInfo((PSZ)pcszFile,
+                                  FIL_STANDARD,
+                                  &fs3,
+                                  sizeof(fs3));
+    if (arc == NO_ERROR)
+    {
+        if (pulAttr)
+            *pulAttr = fs3.attrFile;
+        else
+            arc = ERROR_INVALID_PARAMETER;
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshSetPathAttr:
+ *      sets the file attributes of pszFile,
+ *      which can be fully qualified.
+ *      pszFile can also specify a directory,
+ *      although not all attributes make sense
+ *      for directories.
+ *
+ *      fAttr can be:
+ *      --  FILE_ARCHIVED
+ *      --  FILE_READONLY
+ *      --  FILE_SYSTEM
+ *      --  FILE_HIDDEN
+ *
+ *      Note that this simply sets all the given
+ *      attributes; the existing attributes
+ *      are lost.
+ *
+ *      This returns the APIRET of DosQueryPathInfo.
+ */
+
+APIRET doshSetPathAttr(const char* pcszFile,    // in: file or directory name
+                       ULONG ulAttr)            // in: new attributes
+{
+    FILESTATUS3 fs3;
+    APIRET rc = DosQueryPathInfo((PSZ)pcszFile,
+                                 FIL_STANDARD,
+                                 &fs3,
+                                 sizeof(fs3));
+
+    if (rc == NO_ERROR)
+    {
+        fs3.attrFile = ulAttr;
+        rc = DosSetPathInfo((PSZ)pcszFile,
+                            FIL_STANDARD,
+                            &fs3,
+                            sizeof(fs3),
+                            DSPI_WRTTHRU);
+    }
+    return (rc);
+}
+
+/*
+ *@@ doshReadTextFile:
+ *      reads a text file from disk, allocates memory
+ *      via malloc() and sets a pointer to this
+ *      buffer (or NULL upon errors).
+ *
+ *      This returns the APIRET of DosOpen and DosRead.
+ *      If any error occured, no buffer was allocated.
+ *      Otherwise, you should free() the buffer when
+ *      no longer needed.
+ */
+
+APIRET doshReadTextFile(PSZ pszFile,        // in: file name to read
+                        PSZ* ppszContent)   // out: newly allocated buffer with file's content
+{
+    ULONG   ulSize,
+            ulBytesRead = 0,
+            ulAction, ulLocal;
+    HFILE   hFile;
+    PSZ     pszContent = NULL;
+
+    APIRET arc = DosOpen(pszFile,
+                         &hFile,
+                         &ulAction,                      // action taken
+                         5000L,                          // primary allocation size
+                         FILE_ARCHIVED | FILE_NORMAL,    // file attribute
+                         OPEN_ACTION_OPEN_IF_EXISTS,     // open flags
+                         OPEN_FLAGS_NOINHERIT
+                            | OPEN_SHARE_DENYNONE
+                            | OPEN_ACCESS_READONLY,      // read-only mode
+                         NULL);                          // no EAs
+
+    if (arc == NO_ERROR)
+    {
+        ulSize = doshQueryFileSize(hFile);
+        pszContent = (PSZ)malloc(ulSize+1);
+        arc = DosSetFilePtr(hFile,
+                            0L,
+                            FILE_BEGIN,
+                            &ulLocal);
+        arc = DosRead(hFile,
+                      pszContent,
+                      ulSize,
+                      &ulBytesRead);
+        DosClose(hFile);
+        *(pszContent+ulBytesRead) = 0;
+
+        // set output buffer pointer
+        *ppszContent = pszContent;
+    }
+    else
+        *ppszContent = 0;
+
+    return (arc);
+}
+
+/*
+ *@@ doshCreateBackupFileName:
+ *      creates a valid backup filename of pszExisting
+ *      with a numerical file name extension which does
+ *      not exist in the directory where pszExisting
+ *      resides.
+ *      Returns a PSZ to a new buffer which was allocated
+ *      using malloc().
+ *
+ *      <B>Example:</B> returns "C:\CONFIG.002" for input
+ *      "C:\CONFIG.SYS" if "C:\CONFIG.001" already exists.
+ *
+ *@@changed V0.9.1 (99-12-13) [umoeller]: this crashed if pszExisting had no file extension
+ */
+
+PSZ doshCreateBackupFileName(const char* pszExisting)
+{
+    CHAR    szFilename[CCHMAXPATH];
+    PSZ     pszLastDot;
+    ULONG   ulCount = 1;
+    CHAR    szCount[5];
+
+    strcpy(szFilename, pszExisting);
+    pszLastDot = strrchr(szFilename, '.');
+    if (!pszLastDot)
+        // no dot in filename:
+        pszLastDot = szFilename + strlen(szFilename);
+    do
+    {
+        sprintf(szCount, ".%03d", ulCount);
+        strcpy(pszLastDot, szCount);
+        ulCount++;
+    } while (doshQueryPathSize(szFilename) != 0);
+
+    return (strdup(szFilename));
+}
+
+/*
+ *@@ doshWriteTextFile:
+ *      writes a text file to disk; pszFile must contain the
+ *      whole path and filename.
+ *
+ *      An existing file will be backed up if (pszBackup != NULL),
+ *      using doshCreateBackupFileName. In that case, pszBackup
+ *      receives the name of the backup created, so that buffer
+ *      should be CCHMAXPATH in size.
+ *
+ *      If (pszbackup == NULL), an existing file will be overwritten.
+ *
+ *      On success (NO_ERROR returned), *pulWritten receives
+ *      the no. of bytes written.
+ *
+ *@@changed V0.9.3 (2000-05-01) [umoeller]: optimized DosOpen; added error checking; changed prototype
+ *@@changed V0.9.3 (2000-05-12) [umoeller]: added pszBackup
+ */
+
+APIRET doshWriteTextFile(const char* pszFile,        // in: file name
+                         const char* pszContent,     // in: text to write
+                         PULONG pulWritten,          // out: bytes written (ptr can be NULL)
+                         PSZ pszBackup)              // in/out: create-backup?
+{
+    APIRET  arc = NO_ERROR;
+    ULONG   ulWritten = 0;
+
+    if ((!pszFile) || (!pszContent))
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        ULONG   ulAction = 0,
+                ulLocal = 0;
+        HFILE   hFile = 0;
+
+        ULONG ulSize = strlen(pszContent);      // exclude 0 byte
+
+        if (pszBackup)
+        {
+            PSZ     pszBackup2 = doshCreateBackupFileName(pszFile);
+            DosCopy((PSZ)pszFile,
+                    pszBackup2,
+                    DCPY_EXISTING); // delete existing
+            strcpy(pszBackup, pszBackup2);
+            free(pszBackup2);
+        }
+
+        arc = DosOpen((PSZ)pszFile,
+                      &hFile,
+                      &ulAction,                      // action taken
+                      ulSize,                         // primary allocation size
+                      FILE_ARCHIVED | FILE_NORMAL,    // file attribute
+                      OPEN_ACTION_CREATE_IF_NEW
+                         | OPEN_ACTION_REPLACE_IF_EXISTS,  // open flags
+                      OPEN_FLAGS_NOINHERIT
+                         | OPEN_FLAGS_SEQUENTIAL         // sequential, not random access
+                         | OPEN_SHARE_DENYWRITE          // deny write mode
+                         | OPEN_ACCESS_WRITEONLY,        // write mode
+                      NULL);                          // no EAs
+
+        if (arc == NO_ERROR)
+        {
+            arc = DosSetFilePtr(hFile,
+                                0L,
+                                FILE_BEGIN,
+                                &ulLocal);
+            if (arc == NO_ERROR)
+            {
+                arc = DosWrite(hFile,
+                               (PVOID)pszContent,
+                               ulSize,
+                               &ulWritten);
+                if (arc == NO_ERROR)
+                    arc = DosSetFileSize(hFile, ulSize);
+            }
+
+            DosClose(hFile);
+        }
+    } // end if ((pszFile) && (pszContent))
+
+    if (pulWritten)
+        *pulWritten = ulWritten;
+
+    return (arc);
+}
+
+/*
+ *@@ doshOpenLogFile:
+ *      this opens a log file in the root directory of
+ *      the boot drive; it is titled pszFilename, and
+ *      the file handle is returned.
+ */
+
+HFILE doshOpenLogFile(const char* pcszFilename)
+{
+    APIRET  rc;
+    CHAR    szFileName[CCHMAXPATH];
+    HFILE   hfLog;
+    ULONG   ulAction;
+    ULONG   ibActual;
+
+    sprintf(szFileName, "%c:\\%s", doshQueryBootDrive(), pcszFilename);
+    rc = DosOpen(szFileName,
+                 &hfLog,
+                 &ulAction,
+                 0,             // file size
+                 FILE_NORMAL,
+                 OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_OPEN_IF_EXISTS,
+                 OPEN_ACCESS_WRITEONLY | OPEN_SHARE_DENYWRITE,
+                 (PEAOP2)NULL);
+    if (rc == NO_ERROR)
+    {
+        DosSetFilePtr(hfLog, 0, FILE_END, &ibActual);
+        return (hfLog);
+    }
+    else
+        return (0);
+}
+
+/*
+ * doshWriteToLogFile
+ *      writes a string to a log file, adding a
+ *      leading timestamp.
+ */
+
+APIRET doshWriteToLogFile(HFILE hfLog, const char* pcsz)
+{
+    if (hfLog)
+    {
+        DATETIME dt;
+        CHAR szTemp[2000];
+        ULONG   cbWritten;
+        DosGetDateTime(&dt);
+        sprintf(szTemp, "Time: %02d:%02d:%02d %s",
+            dt.hours, dt.minutes, dt.seconds,
+            pcsz);
+        return (DosWrite(hfLog, (PVOID)szTemp, strlen(szTemp), &cbWritten));
+    }
+    else return (ERROR_INVALID_HANDLE);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Directory management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Directory helpers                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshQueryDirExist:
+ *      returns TRUE if the given directory
+ *      exists.
+ */
+
+BOOL doshQueryDirExist(PSZ pszDir)
+{
+    FILESTATUS3 fs3;
+    APIRET arc = DosQueryPathInfo(pszDir, FIL_STANDARD, &fs3, sizeof(fs3));
+    if (arc == NO_ERROR)
+        // file found:
+        return ((fs3.attrFile & FILE_DIRECTORY) != 0);
+    else
+        return FALSE;
+}
+
+/*
+ *@@ doshCreatePath:
+ *      this creates the specified directory.
+ *      As opposed to DosCreateDir, this
+ *      function can create several directories
+ *      at the same time, if the parent
+ *      directories do not exist yet.
+ */
+
+APIRET doshCreatePath(PSZ pszPath,
+                      BOOL fHidden) // in: if TRUE, the new directories will get FILE_HIDDEN
+{
+    APIRET  arc0 = NO_ERROR;
+    CHAR    path[CCHMAXPATH];
+    CHAR    *cp, c;
+    ULONG   cbPath;
+
+    strcpy(path, pszPath);
+    cbPath = strlen(path);
+
+    if (path[cbPath] != '\\')
+    {
+        path[cbPath] = '\\';
+        path[cbPath+1] = 0;
+    }
+
+    cp = path;
+    // advance past the drive letter only if we have one
+    if (*(cp+1) == ':')
+        cp += 3;
+
+    // go!
+    while (*cp != 0)
+    {
+        if (*cp == '\\')
+        {
+            c = *cp;
+            *cp = 0;
+            if (!doshQueryDirExist(path))
+            {
+                APIRET arc = DosCreateDir(path,
+                                          0);   // no EAs
+                if (arc != NO_ERROR)
+                {
+                    arc0 = arc;
+                    break;
+                }
+                else
+                    if (fHidden)
+                    {
+                        // hide the directory we just created
+                        doshSetPathAttr(path, FILE_HIDDEN);
+                    }
+            }
+            *cp = c;
+        }
+        cp++;
+    }
+    return (arc0);
+}
+
+/*
+ *@@ doshQueryCurrentDir:
+ *      writes the current directory into
+ *      the specified buffer, which should be
+ *      CCHMAXPATH in size.
+ *
+ *      As opposed to DosQueryCurrentDir, this
+ *      includes the drive letter.
+ *
+ *@@added V0.9.4 (2000-07-22) [umoeller]
+ */
+
+APIRET doshQueryCurrentDir(PSZ pszBuf)
+{
+    APIRET arc = NO_ERROR;
+    ULONG   ulCurDisk = 0;
+    ULONG   ulMap = 0;
+    arc = DosQueryCurrentDisk(&ulCurDisk, &ulMap);
+    if (arc == NO_ERROR)
+    {
+        ULONG   cbBuf = CCHMAXPATH - 3;
+        *pszBuf = G_acDriveLetters[ulCurDisk];
+        *(pszBuf + 1) = ':';
+        *(pszBuf + 2) = '\\';
+        arc = DosQueryCurrentDir(0, pszBuf + 3, &cbBuf);
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshDeleteDir:
+ *      deletes a directory. As opposed to DosDeleteDir,
+ *      this removes empty subdirectories and/or files
+ *      as well.
+ *
+ *      flFlags can be any combination of the following:
+ *
+ *      -- DOSHDELDIR_RECURSE: recurse into subdirectories.
+ *
+ *      -- DOSHDELDIR_DELETEFILES: delete all regular files
+ *              which are found on the way.
+ *
+ *              THIS IS NOT IMPLEMENTED YET.
+ *
+ *      If 0 is specified, this effectively behaves just as
+ *      DosDeleteDir.
+ *
+ *      If you specify DOSHDELDIR_RECURSE only, only empty
+ *      subdirectories are deleted as well.
+ *
+ *      If you specify DOSHDELDIR_RECURSE | DOSHDELDIR_DELETEFILES,
+ *      this removes an entire directory tree, including all
+ *      subdirectories and files.
+ *
+ *@@added V0.9.4 (2000-07-01) [umoeller]
+ */
+
+APIRET doshDeleteDir(const char *pcszDir,
+                     ULONG flFlags,
+                     PULONG pulDirs,        // out: directories found
+                     PULONG pulFiles)       // out: files found
+{
+    APIRET          arc = NO_ERROR,
+                    arcReturn = NO_ERROR;
+
+    HDIR            hdirFindHandle = HDIR_CREATE;
+    FILEFINDBUF3    ffb3     = {0};      // returned from FindFirst/Next
+    ULONG           ulResultBufLen = sizeof(FILEFINDBUF3);
+    ULONG           ulFindCount    = 1;        // look for 1 file at a time
+
+    CHAR            szFileMask[2*CCHMAXPATH];
+    sprintf(szFileMask, "%s\\*", pcszDir);
+
+    // find files
+    arc = DosFindFirst(szFileMask,
+                       &hdirFindHandle,     // directory search handle
+                       FILE_ARCHIVED | FILE_DIRECTORY | FILE_SYSTEM
+                         | FILE_HIDDEN | FILE_READONLY,
+                                     // search attributes; include all, even dirs
+                       &ffb3,               // result buffer
+                       ulResultBufLen,      // result buffer length
+                       &ulFindCount,        // number of entries to find
+                       FIL_STANDARD);       // return level 1 file info
+
+    if (arc == NO_ERROR)
+    {
+        // keep finding the next file until there are no more files
+        while (arc == NO_ERROR)     // != ERROR_NO_MORE_FILES
+        {
+            if (ffb3.attrFile & FILE_DIRECTORY)
+            {
+                // we found a directory:
+
+                // ignore the pseudo-directories
+                if (    (strcmp(ffb3.achName, ".") != 0)
+                     && (strcmp(ffb3.achName, "..") != 0)
+                   )
+                {
+                    // real directory:
+                    if (flFlags & DOSHDELDIR_RECURSE)
+                    {
+                        // recurse!
+                        CHAR szSubDir[2*CCHMAXPATH];
+                        sprintf(szSubDir, "%s\\%s", pcszDir, ffb3.achName);
+                        arcReturn = doshDeleteDir(szSubDir,
+                                                  flFlags,
+                                                  pulDirs,
+                                                  pulFiles);
+                        // this removes ffb3.achName as well
+                    }
+                    else
+                    {
+                        // directory, but no recursion:
+                        // report "access denied"
+                        // (this is what OS/2 reports with DosDeleteDir as well)
+                        arcReturn = ERROR_ACCESS_DENIED;  // 5
+                        (*pulDirs)++;
+                    }
+                }
+            }
+            else
+            {
+                // it's a file:
+                arcReturn = ERROR_ACCESS_DENIED;
+                (*pulFiles)++;
+            }
+
+            if (arc == NO_ERROR)
+            {
+                ulFindCount = 1;                      // reset find count
+                arc = DosFindNext(hdirFindHandle,      // directory handle
+                                  &ffb3,         // result buffer
+                                  ulResultBufLen,      // result buffer length
+                                  &ulFindCount);       // number of entries to find
+            }
+        } // endwhile
+
+        DosFindClose(hdirFindHandle);    // close our find handle
+    }
+
+    if (arcReturn == NO_ERROR)
+        // success so far:
+        // delete our directory now
+        arc = DosDeleteDir((PSZ)pcszDir);
+
+    return (arcReturn);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Process management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Process helpers                                                *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshExecVIO:
+ *      executes cmd.exe with the /c parameter
+ *      and pcszExecWithArgs. This is equivalent
+ *      to the C library system() function,
+ *      except that an OS/2 error code is returned
+ *      and that this works with the VAC subsystem
+ *      library.
+ *
+ *      This uses DosExecPgm and handles the sick
+ *      argument passing.
+ *
+ *      If NO_ERROR is returned, *plExitCode receives
+ *      the exit code of the process. If the process
+ *      was terminated abnormally, *plExitCode receives:
+ *
+ *      -- -1: hard error halt
+ *      -- -2: 16-bit trap
+ *      -- -3: DosKillProcess
+ *      -- -4: 32-bit exception
+ *
+ *@@added V0.9.4 (2000-07-27) [umoeller]
+ */
+
+APIRET doshExecVIO(const char *pcszExecWithArgs,
+                   PLONG plExitCode)            // out: exit code (ptr can be NULL)
+{
+    APIRET arc = NO_ERROR;
+
+    if (strlen(pcszExecWithArgs) > 255)
+        arc = ERROR_INSUFFICIENT_BUFFER;
+    {
+        CHAR szObj[CCHMAXPATH];
+        RESULTCODES res = {0};
+        CHAR szBuffer[300];
+
+        // DosExecPgm expects two args in szBuffer:
+        // --  cmd.exe\0
+        // --  then the actual argument, terminated by two 0 bytes.
+        memset(szBuffer, 0, sizeof(szBuffer));
+        strcpy(szBuffer, "cmd.exe\0");
+        sprintf(szBuffer + 8, "/c %s",
+                pcszExecWithArgs);
+
+        arc = DosExecPgm(szObj, sizeof(szObj),
+                         EXEC_SYNC,
+                         szBuffer,
+                         0,
+                         &res,
+                         "cmd.exe");
+        if ((arc == NO_ERROR) && (plExitCode))
+        {
+            if (res.codeTerminate == 0)
+                // normal exit:
+                *plExitCode = res.codeResult;
+            else
+                *plExitCode = -((LONG)res.codeTerminate);
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshQuickStartSession:
+ *      this is a shortcut to DosStartSession w/out having to
+ *      deal with all the messy parameters.
+ *
+ *      This one starts pszPath as a child session and passes
+ *      pszParams to it.
+ *
+ *      In usPgmCtl, OR any or none of the following (V0.9.0):
+ *      --  SSF_CONTROL_NOAUTOCLOSE (0x0008): do not close automatically.
+ *              This bit is used only for VIO Windowable apps and ignored
+ *              for all other applications.
+ *      --  SSF_CONTROL_MINIMIZE (0x0004)
+ *      --  SSF_CONTROL_MAXIMIZE (0x0002)
+ *      --  SSF_CONTROL_INVISIBLE (0x0001)
+ *      --  SSF_CONTROL_VISIBLE (0x0000)
+ *
+ *      Specifying 0 will therefore auto-close the session and make it
+ *      visible.
+ *
+ *      If (fWait), this function will create a termination queue
+ *      and not return until the child session has ended. Otherwise
+ *      the function will return immediately, and the SID/PID of
+ *      the child session can be found in *pulSID and *ppid.
+ *
+ *      Returns the error code of DosStartSession.
+ *
+ *      Note: According to CPREF, calling DosStartSession calls
+ *      DosExecPgm in turn for all full-screen, VIO, and PM sessions.
+ *
+ *@@changed V0.9.0 [umoeller]: prototype changed to include usPgmCtl
+ *@@changed V0.9.1 (99-12-30) [umoeller]: queue was sometimes not closed. Fixed.
+ *@@changed V0.9.3 (2000-05-03) [umoeller]: added fForeground
+ */
+
+APIRET doshQuickStartSession(const char *pcszPath,       // in: program to start
+                             const char *pcszParams,     // in: parameters for program
+                             BOOL fForeground,  // in: if TRUE, session will be in foreground
+                             USHORT usPgmCtl,   // in: STARTDATA.PgmControl
+                             BOOL fWait,        // in: wait for termination?
+                             PULONG pulSID,     // out: session ID (req.)
+                             PPID ppid)         // out: process ID (req.)
+{
+    APIRET      arc;
+    // queue stuff
+    const char  *pcszQueueName = "\\queues\\kfgstart.que";
+    HQUEUE      hq = 0;
+    PID         qpid = 0;
+    STARTDATA   SData;
+    CHAR        szObjBuf[CCHMAXPATH];
+
+    if (fWait)
+    {
+        if ((arc = DosCreateQueue(&hq,
+                                  QUE_FIFO | QUE_CONVERT_ADDRESS,
+                                  (PSZ)pcszQueueName))
+                    != NO_ERROR)
+            return (arc);
+
+        if ((arc = DosOpenQueue(&qpid, &hq, (PSZ)pcszQueueName)) != NO_ERROR)
+            return (arc);
+    }
+
+    SData.Length  = sizeof(STARTDATA);
+    SData.Related = SSF_RELATED_CHILD; //INDEPENDENT;
+    SData.FgBg    = (fForeground) ? SSF_FGBG_FORE : SSF_FGBG_BACK;
+            // V0.9.3 (2000-05-03) [umoeller]
+    SData.TraceOpt = SSF_TRACEOPT_NONE;
+
+    SData.PgmTitle = (PSZ)pcszPath;       // title for window
+    SData.PgmName = (PSZ)pcszPath;
+    SData.PgmInputs = (PSZ)pcszParams;
+
+    SData.TermQ = (fWait) ? "\\queues\\kfgstart.que" : NULL;
+    SData.Environment = 0;
+    SData.InheritOpt = SSF_INHERTOPT_PARENT;
+    SData.SessionType = SSF_TYPE_DEFAULT;
+    SData.IconFile = 0;
+    SData.PgmHandle = 0;
+
+    SData.PgmControl = usPgmCtl;
+
+    SData.InitXPos  = 30;
+    SData.InitYPos  = 40;
+    SData.InitXSize = 200;
+    SData.InitYSize = 140;
+    SData.Reserved = 0;
+    SData.ObjectBuffer  = (CHAR*)&szObjBuf;
+    SData.ObjectBuffLen = (ULONG)sizeof(szObjBuf);
+
+    arc = DosStartSession(&SData, pulSID, ppid);
+
+    if (arc == NO_ERROR)
+    {
+        if (fWait)
+        {
+            REQUESTDATA rqdata;
+            ULONG       DataLength = 0;
+            PULONG      DataAddress;
+            BYTE        elpri;
+
+            rqdata.pid = qpid;
+            DosReadQueue(hq,                // in: queue handle
+                         &rqdata,           // out: pid and ulData
+                         &DataLength,       // out: size of data returned
+                         (PVOID*)&DataAddress, // out: data returned
+                         0,                 // in: remove first element in queue
+                         0,                 // in: wait for queue data (block thread)
+                         &elpri,            // out: element's priority
+                         0);                // in: event semaphore to be posted
+        }
+    }
+
+    if (hq)
+        DosCloseQueue(hq);
+
+    return (arc);
+}
+
+
Index: /trunk/src/helpers/dosh2.c
===================================================================
--- /trunk/src/helpers/dosh2.c	(revision 8)
+++ /trunk/src/helpers/dosh2.c	(revision 8)
@@ -0,0 +1,2105 @@
+
+/*
+ *@@sourcefile dosh2.c:
+ *      dosh.c contains more Control Program helper functions.
+ *
+ *      This file is new with V0.9.4 (2000-07-26) [umoeller].
+ *
+ *      As opposed to the functions in dosh.c, these require
+ *      linking against other helpers. As a result, these have
+ *      been separated from dosh.c to allow linking against
+ *      dosh.obj only.
+ *
+ *      Function prefixes:
+ *      --  dosh*   Dos (Control Program) helper functions
+ *
+ *      This has the same header as dosh.c, dosh.h.
+ *
+ *      The partition functions in this file are based on
+ *      code which has kindly been provided by Dmitry A. Steklenev.
+ *      See doshGetPartitionsList for how to use these.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\dosh.h"
+ *@@added V0.9.4 (2000-07-27) [umoeller]
+ */
+
+/*
+ *      This file Copyright (C) 1997-2000 Ulrich Mller,
+ *                                        Dmitry A. Steklenev.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSDEVIOCTL
+#define INCL_DOS
+#define INCL_DOSERRORS
+// #define INCL_GPI
+#include <os2.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+#include "helpers\stringh.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Control program helpers\Miscellaneous
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Miscellaneous                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshIsValidFileName:
+ *      this returns NO_ERROR only if pszFile is a valid file name.
+ *      This may include a full path.
+ *
+ *      If a drive letter is specified, this checks for whether
+ *      that drive is a FAT drive and adjust the checks accordingly,
+ *      i.e. 8+3 syntax (per path component).
+ *
+ *      If no drive letter is specified, this check is performed
+ *      for the current drive.
+ *
+ *      This also checks if pszFileNames contains characters which
+ *      are invalid for the current drive.
+ *
+ *      Note: this performs syntactic checks only. This does not
+ *      check for whether the specified path components exist.
+ *      However, it _is_ checked for whether the given drive
+ *      exists.
+ *
+ *      This func is especially useful to check filenames that
+ *      have been entered by the user in a "Save as" dialog.
+ *
+ *      If an error is found, the corresponding DOS error code
+ *      is returned:
+ *      --  ERROR_INVALID_DRIVE
+ *      --  ERROR_FILENAME_EXCED_RANGE  (on FAT: no 8+3 filename)
+ *      --  ERROR_INVALID_NAME          (invalid character)
+ *      --  ERROR_CURRENT_DIRECTORY     (if fFullyQualified: no full path specified)
+ *
+ *@@changed V0.9.2 (2000-03-11) [umoeller]: added fFullyQualified
+ */
+
+APIRET doshIsValidFileName(const char* pcszFile,
+                           BOOL fFullyQualified)    // in: if TRUE, pcszFile must be fully q'fied
+{
+    CHAR    szPath[CCHMAXPATH+4] = " :";
+    CHAR    szComponent[CCHMAXPATH];
+    PSZ     p1, p2;
+    BOOL    fIsFAT = FALSE;
+    PSZ     pszInvalid;
+
+    if (fFullyQualified)    // V0.9.2 (2000-03-11) [umoeller]
+    {
+        if (    (*(pcszFile + 1) != ':')
+             || (*(pcszFile + 2) != '\\')
+           )
+            return (ERROR_CURRENT_DIRECTORY);
+    }
+
+    // check drive first
+    if (*(pcszFile + 1) == ':')
+    {
+        CHAR cDrive = toupper(*pcszFile);
+        // drive specified:
+        strcpy(szPath, pcszFile);
+        szPath[0] = toupper(*pcszFile);
+        if (doshQueryDiskFree(cDrive - 'A' + 1) == -1)
+            return (ERROR_INVALID_DRIVE);
+    }
+    else
+    {
+        // no drive specified: take current
+        ULONG   ulDriveNum = 0,
+                ulDriveMap = 0;
+        DosQueryCurrentDisk(&ulDriveNum, &ulDriveMap);
+        szPath[0] = ((UCHAR)ulDriveNum) + 'A' - 1;
+        szPath[1] = ':';
+        strcpy(&szPath[2], pcszFile);
+    }
+
+    fIsFAT = doshIsFileOnFAT(szPath);
+
+    pszInvalid = (fIsFAT)
+                    ? "<>|+=:;,\"/[] "  // invalid characters in FAT
+                    : "<>|:\"/";        // invalid characters in IFS's
+
+    // now separate path components
+    p1 = &szPath[2];       // advance past ':'
+
+    do {
+
+        if (*p1 == '\\')
+            p1++;
+
+        p2 = strchr(p1, '\\');
+        if (p2 == NULL)
+            p2 = p1 + strlen(p1);
+
+        if (p1 != p2)
+        {
+            LONG    lDotOfs = -1,
+                    lAfterDot = -1;
+            ULONG   cbFile,
+                    ul;
+            PSZ     pSource = szComponent;
+
+            strncpy(szComponent, p1, p2-p1);
+            szComponent[p2-p1] = 0;
+            cbFile = strlen(szComponent);
+
+            // now check each path component
+            for (ul = 0; ul < cbFile; ul++)
+            {
+                if (fIsFAT)
+                {
+                    // on FAT: only 8 characters allowed before dot
+                    if (*pSource == '.')
+                    {
+                        lDotOfs = ul;
+                        lAfterDot = 0;
+                        if (ul > 7)
+                            return (ERROR_FILENAME_EXCED_RANGE);
+                    }
+                }
+                // and check for invalid characters
+                if (strchr(pszInvalid, *pSource) != NULL)
+                    return (ERROR_INVALID_NAME);
+
+                pSource++;
+
+                // on FAT, allow only three chars after dot
+                if (fIsFAT)
+                    if (lAfterDot != -1)
+                    {
+                        lAfterDot++;
+                        if (lAfterDot > 3)
+                            return(ERROR_FILENAME_EXCED_RANGE);
+                    }
+            }
+
+            // we are still missing the case of a FAT file
+            // name without extension; if so, check whether
+            // the file stem is <= 8 chars
+            if (fIsFAT)
+                if (lDotOfs == -1)  // dot not found:
+                    if (cbFile > 8)
+                        return (ERROR_FILENAME_EXCED_RANGE);
+        }
+
+        // go for next component
+        p1 = p2+1;
+    } while (*p2);
+
+    return (NO_ERROR);
+}
+
+/*
+ *@@ doshMakeRealName:
+ *      this copies pszSource to pszTarget, replacing
+ *      all characters which are not supported by file
+ *      systems with cReplace.
+ *      pszTarget must be at least the same size as pszSource.
+ *      If (fIsFAT), the file name will be made FAT-compliant (8+3).
+ *      Returns TRUE if characters were replaced.
+ *
+ *@@changed V0.9.0 (99-11-06) [umoeller]: now replacing "*" too
+ */
+
+BOOL doshMakeRealName(PSZ pszTarget,    // out: new real name
+                      PSZ pszSource,    // in: filename to translate
+                      CHAR cReplace,    // in: replacement char for invalid
+                                        //     characters (e.g. '!')
+                      BOOL fIsFAT)      // in: make-FAT-compatible flag
+{
+    ULONG ul,
+          cbSource = strlen(pszSource);
+    LONG  lDotOfs = -1,
+          lAfterDot = -1;
+    BOOL  brc = FALSE;
+    PSZ   pSource = pszSource,
+          pTarget = pszTarget,
+          pszInvalid = (fIsFAT)
+                            ? "*<>|+=:;,\"/\\[] "  // invalid characters in FAT
+                            : "*<>|:\"/\\"; // invalid characters in IFS's
+
+    for (ul = 0; ul < cbSource; ul++)
+    {
+        if (fIsFAT)
+        {
+            // on FAT: truncate filename if neccessary
+            if (*pSource == '.')
+            {
+                lDotOfs = ul;
+                lAfterDot = 0;
+                if (ul > 7) {
+                    // only 8 characters allowed before dot,
+                    // so set target ptr to dot pos
+                    pTarget = pszTarget+8;
+                }
+            }
+        }
+        // and replace invalid characters
+        if (strchr(pszInvalid, *pSource) == NULL)
+            *pTarget = *pSource;
+        else
+        {
+            *pTarget = cReplace;
+            brc = TRUE;
+        }
+        pTarget++;
+        pSource++;
+
+        // on FAT, allow only three chars after dot
+        if (fIsFAT)
+            if (lAfterDot != -1)
+            {
+                lAfterDot++;
+                if (lAfterDot > 3)
+                    break;
+            }
+    }
+    *pTarget = '\0';
+
+    if (fIsFAT)
+    {
+        // we are still missing the case of a FAT file
+        // name without extension; if so, check whether
+        // the file stem is <= 8 chars
+        if (lDotOfs == -1)  // dot not found:
+            if (cbSource > 8)
+                *(pszTarget+8) = 0; // truncate
+
+        // convert to upper case
+        strupr(pszTarget);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ doshSetCurrentDir:
+ *      sets the current working directory
+ *      to the given path.
+ *
+ *      As opposed to DosSetCurrentDir, this
+ *      one will change the current drive
+ *      also, if one is specified.
+ */
+
+APIRET doshSetCurrentDir(const char *pcszDir)
+{
+    if (pcszDir)
+    {
+        if (*pcszDir != 0)
+            if (*(pcszDir+1) == ':')
+            {
+                // drive given:
+                CHAR    cDrive = toupper(*(pcszDir));
+                APIRET  arc;
+                // change drive
+                arc = DosSetDefaultDisk( (ULONG)(cDrive - 'A' + 1) );
+                        // 1 = A:, 2 = B:, ...
+                if (arc != NO_ERROR)
+                    return (arc);
+            }
+
+        return (DosSetCurrentDir((PSZ)pcszDir));
+    }
+    return (ERROR_INVALID_PARAMETER);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Environment management
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Environment helpers                                            *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshParseEnvironment:
+ *      this takes one of those ugly environment strings
+ *      as used by DosStartSession and WinStartApp (with
+ *      lots of zero-terminated strings one after another
+ *      and a duplicate zero byte as a terminator) as
+ *      input and splits it into an array of separate
+ *      strings in pEnv.
+ *
+ *      The newly allocated strings are stored in in
+ *      pEnv->papszVars. The array count is stored in
+ *      pEnv->cVars.
+ *
+ *      Each environment variable will be copied into
+ *      one newly allocated string in the array. Use
+ *      doshFreeEnvironment to free the memory allocated
+ *      by this function.
+ *
+ *      Use the following code to browse thru the array:
+ +
+ +          DOSENVIRONMENT Env = {0};
+ +          if (doshParseEnvironment(pszEnv,
+ +                                   &Env)
+ +                  == NO_ERROR)
+ +          {
+ +              if (Env.papszVars)
+ +              {
+ +                  PSZ *ppszThis = Env.papszVars;
+ +                  for (ul = 0;
+ +                       ul < Env.cVars;
+ +                       ul++)
+ +                  {
+ +                      PSZ pszThis = *ppszThis;
+ +                      // pszThis now has something like PATH=C:\TEMP
+ +                      // ...
+ +                      // next environment string
+ +                      ppszThis++;
+ +                  }
+ +              }
+ +              doshFreeEnvironment(&Env);
+ +          }
+ *
+ *@@added V0.9.4 (2000-08-02) [umoeller]
+ */
+
+APIRET doshParseEnvironment(const char *pcszEnv,
+                            PDOSENVIRONMENT pEnv)
+{
+    APIRET arc = NO_ERROR;
+    if (!pcszEnv)
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        PSZ     pszVarThis = (PSZ)pcszEnv;
+        ULONG   cVars = 0;
+        // count strings
+        while (*pszVarThis)
+        {
+            cVars++;
+            pszVarThis += strlen(pszVarThis) + 1;
+        }
+
+        pEnv->cVars = cVars;
+        pEnv->papszVars = 0;
+
+        if (cVars)
+        {
+            PSZ *papsz = (PSZ*)malloc(sizeof(PSZ) * cVars);
+            if (!papsz)
+                arc = ERROR_NOT_ENOUGH_MEMORY;
+            else
+            {
+                PSZ *ppszTarget = papsz;
+                memset(papsz, 0, sizeof(PSZ) * cVars);
+                pszVarThis = (PSZ)pcszEnv;
+                while (*pszVarThis)
+                {
+                    *ppszTarget = strdup(pszVarThis);
+                    ppszTarget++;
+                    pszVarThis += strlen(pszVarThis) + 1;
+                }
+
+                pEnv->papszVars = papsz;
+            }
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshGetEnvironment:
+ *      calls doshParseEnvironment for the current
+ *      process environment, which is retrieved from
+ *      the info blocks.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+APIRET doshGetEnvironment(PDOSENVIRONMENT pEnv)
+{
+    APIRET  arc = NO_ERROR;
+    if (!pEnv)
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        PTIB    ptib = 0;
+        PPIB    ppib = 0;
+        arc = DosGetInfoBlocks(&ptib, &ppib);
+        if (arc == NO_ERROR)
+        {
+            PSZ pszEnv = ppib->pib_pchenv;
+            if (pszEnv)
+                arc = doshParseEnvironment(pszEnv, pEnv);
+            else
+                arc = ERROR_BAD_ENVIRONMENT;
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshFindEnvironmentVar:
+ *      returns the PSZ* in the pEnv->papszVars array
+ *      which specifies the environment variable in pszVarName.
+ *
+ *      With pszVarName, you can either specify the variable
+ *      name only ("VARNAME") or a full environment string
+ *      ("VARNAME=BLAH"). In any case, only the variable name
+ *      is compared.
+ *
+ *      Returns NULL if no such variable name was found in
+ *      the array.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+PSZ* doshFindEnvironmentVar(PDOSENVIRONMENT pEnv,
+                            PSZ pszVarName)
+{
+    PSZ *ppszRet = 0;
+    if (pEnv)
+    {
+        if ((pEnv->papszVars) && (pszVarName))
+        {
+            PSZ     *ppszThis = pEnv->papszVars;
+            PSZ     pszThis;
+            ULONG   ul = 0;
+            ULONG   ulVarNameLen = 0;
+
+            PSZ     pszSearch = NULL; // receives "VARNAME="
+            PSZ     pFirstEqual = strchr(pszVarName, '=');
+            if (pFirstEqual)
+                pszSearch = strhSubstr(pszVarName, pFirstEqual + 1);
+            else
+            {
+                ulVarNameLen = strlen(pszVarName);
+                pszSearch = (PSZ)malloc(ulVarNameLen + 2);
+                memcpy(pszSearch, pszVarName, ulVarNameLen);
+                *(pszSearch + ulVarNameLen) = '=';
+                *(pszSearch + ulVarNameLen + 1) = 0;
+            }
+
+            ulVarNameLen = strlen(pszSearch);
+
+            for (ul = 0;
+                 ul < pEnv->cVars;
+                 ul++)
+            {
+                pszThis = *ppszThis;
+
+                if (strnicmp(*ppszThis, pszSearch, ulVarNameLen) == 0)
+                {
+                    ppszRet = ppszThis;
+                    break;
+                }
+
+                // next environment string
+                ppszThis++;
+            }
+        }
+    }
+
+    return (ppszRet);
+}
+
+/*
+ *@@ doshSetEnvironmentVar:
+ *      sets an environment variable in the specified
+ *      environment, which must have been initialized
+ *      using doshGetEnvironment first.
+ *
+ *      pszNewEnv must be a full environment string
+ *      in the form "VARNAME=VALUE".
+ *
+ *      If "VARNAME" has already been set to something
+ *      in the string array in pEnv, that array item
+ *      is replaced. Otherwise a new item is added to
+ *      the array, and pEnv->cVars is raised by one.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+APIRET doshSetEnvironmentVar(PDOSENVIRONMENT pEnv,
+                             PSZ pszNewEnv)
+{
+    APIRET  arc = NO_ERROR;
+    if (!pEnv)
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        if ((!pEnv->papszVars) || (!pszNewEnv))
+            arc = ERROR_INVALID_PARAMETER;
+        else
+        {
+            PSZ *ppszEnvLine = doshFindEnvironmentVar(pEnv, pszNewEnv);
+            if (ppszEnvLine)
+            {
+                // was set already: replace
+                free(*ppszEnvLine);
+                *ppszEnvLine = strdup(pszNewEnv);
+            }
+            else
+            {
+                PSZ *ppszNew;
+                PSZ *papszNew;
+                // not set already:
+                // append
+                // reallocate array and add new string
+                papszNew = (PSZ*)realloc(pEnv->papszVars, sizeof(PSZ) * (pEnv->cVars + 1));
+                if (!papszNew)
+                    arc = ERROR_NOT_ENOUGH_MEMORY;
+                else
+                {
+                    pEnv->papszVars = papszNew;
+                    ppszNew = pEnv->papszVars + pEnv->cVars;
+                    pEnv->cVars++;
+                    *ppszNew = strdup(pszNewEnv);
+                }
+            }
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshConvertEnvironment:
+ *      converts an environment initialized by doshGetEnvironment
+ *      to the string format required by WinStartApp and DosExecPgm,
+ *      that is, one memory block is allocated in *ppszEnv and all
+ *      strings in pEnv->papszVars are copied to that block. Each
+ *      string is terminated with a null character; the last string
+ *      is terminated with two null characters.
+ *
+ *      Use free() to free the memory block allocated by this
+ *      function in *ppszEnv.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+APIRET doshConvertEnvironment(PDOSENVIRONMENT pEnv,
+                              PSZ *ppszEnv,     // out: environment string
+                              PULONG pulSize)  // out: size of block allocated in *ppszEnv
+{
+    APIRET  arc = NO_ERROR;
+    if (!pEnv)
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        if (!pEnv->papszVars)
+            arc = ERROR_INVALID_PARAMETER;
+        else
+        {
+            // count memory needed for all strings
+            ULONG   cbNeeded = 0,
+                    ul = 0;
+            PSZ     *ppszThis = pEnv->papszVars;
+
+            for (ul = 0;
+                 ul < pEnv->cVars;
+                 ul++)
+            {
+                cbNeeded += strlen(*ppszThis) + 1; // length of string plus null terminator
+
+                // next environment string
+                ppszThis++;
+            }
+
+            cbNeeded++;     // for another null terminator
+
+            *ppszEnv = (PSZ)malloc(cbNeeded);
+            if (!(*ppszEnv))
+                arc = ERROR_NOT_ENOUGH_MEMORY;
+            else
+            {
+                PSZ     pTarget = *ppszEnv;
+                *pulSize = cbNeeded;
+                ppszThis = pEnv->papszVars;
+
+                // now copy each string
+                for (ul = 0;
+                     ul < pEnv->cVars;
+                     ul++)
+                {
+                    PSZ pSource = *ppszThis;
+
+                    while ((*pTarget++ = *pSource++))
+                        ;
+
+                    // *pTarget++ = 0;     // append null terminator per string
+
+                    // next environment string
+                    ppszThis++;
+                }
+
+                *pTarget++ = 0;     // append second null terminator
+            }
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ doshFreeEnvironment:
+ *      frees memory allocated by doshGetEnvironment.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+APIRET doshFreeEnvironment(PDOSENVIRONMENT pEnv)
+{
+    APIRET  arc = NO_ERROR;
+    if (!pEnv)
+        arc = ERROR_INVALID_PARAMETER;
+    else
+    {
+        if (!pEnv->papszVars)
+            arc = ERROR_INVALID_PARAMETER;
+        else
+        {
+            PSZ     *ppszThis = pEnv->papszVars;
+            PSZ     pszThis;
+            ULONG   ul = 0;
+
+            for (ul = 0;
+                 ul < pEnv->cVars;
+                 ul++)
+            {
+                pszThis = *ppszThis;
+                free(pszThis);
+                // *ppszThis = NULL;
+                // next environment string
+                ppszThis++;
+            }
+
+            free(pEnv->papszVars);
+            pEnv->cVars = 0;
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Module handling
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Module handling helpers                                        *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshResolveImports:
+ *      this function loads the module called pszModuleName
+ *      and resolves imports dynamically using DosQueryProcAddress.
+ *
+ *      To specify the functions to be imported, a RESOLVEFUNCTION
+ *      array is used. In each of the array items, specify the
+ *      name of the function and a pointer to a function pointer
+ *      where to store the resolved address.
+ *
+ *@@added V0.9.3 (2000-04-29) [umoeller]
+ */
+
+APIRET doshResolveImports(PSZ pszModuleName,    // in: DLL to load
+                          HMODULE *phmod,       // out: module handle
+                          PRESOLVEFUNCTION paResolves, // in/out: function resolves
+                          ULONG cResolves)      // in: array item count (not array size!)
+{
+    CHAR    szName[CCHMAXPATH];
+    APIRET arc = DosLoadModule(szName,
+                               sizeof(szName),
+                               pszModuleName,
+                               phmod);
+    if (arc == NO_ERROR)
+    {
+        ULONG  ul;
+        for (ul = 0;
+             ul < cResolves;
+             ul++)
+        {
+            arc = DosQueryProcAddr(*phmod,
+                                   0,               // ordinal, ignored
+                                   (PSZ)paResolves[ul].pcszFunctionName,
+                                   paResolves[ul].ppFuncAddress);
+
+            /* _Pmpf(("Resolved %s to 0x%lX, rc: %d",
+                    paResolves[ul].pcszFunctionName,
+                    *paResolves[ul].ppFuncAddress,
+                    arc)); */
+            if (arc != NO_ERROR)
+                break;
+        }
+    }
+
+    return (arc);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Executable info
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   Executable functions                                           *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshExecOpen:
+ *      this opens the specified executable file
+ *      (which can be an .EXE, .COM, .DLL, or
+ *      driver file) for use with the other
+ *      doshExec* functions.
+ *
+ *      If no error occurs, NO_ERROR is returned
+ *      and a pointer to a new EXECUTABLE structure
+ *      is stored in *ppExec. Consider this pointer a
+ *      handle and pass it to doshExecClose when the
+ *      executable is no longer needed to free
+ *      resources.
+ *
+ *      If NO_ERROR is returned, all the fields through
+ *      ulOS are set in EXECUTABLE. The psz* fields
+ *      which follow afterwards require an additional
+ *      call to doshExecQueryBldLevel.
+ *
+ *      If errors occur, this function returns the
+ *      following error codes:
+ *      -- ERROR_NOT_ENOUGH_MEMORY: malloc() failed.
+ *      -- ERROR_INVALID_EXE_SIGNATURE: specified file
+ *              has no DOS EXE header.
+ *      -- ERROR_INVALID_PARAMETER: ppExec is NULL.
+ *
+ *      plus those of DosOpen, DosSetFilePtr, and
+ *      DosRead.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (2000-02-13) [umoeller]: fixed 32-bits flag
+ */
+
+APIRET doshExecOpen(const char* pcszExecutable,
+                    PEXECUTABLE* ppExec)
+{
+    ULONG   ulAction = 0;
+    HFILE   hFile;
+    APIRET  arc = DosOpen((PSZ)pcszExecutable,
+                          &hFile,
+                          &ulAction,                      // out: action taken
+                          0,                              // in: new file (ignored for read-mode)
+                          0,                              // in: new file attribs (ignored)
+                          // open-flags
+                          OPEN_ACTION_FAIL_IF_NEW
+                             | OPEN_ACTION_OPEN_IF_EXISTS,
+                          // open-mode
+                          OPEN_FLAGS_FAIL_ON_ERROR        // report errors to caller
+                             | OPEN_FLAGS_SEQUENTIAL
+                             | OPEN_FLAGS_NOINHERIT
+                             | OPEN_SHARE_DENYNONE
+                             | OPEN_ACCESS_READONLY,      // read-only mode
+                          NULL);                          // no EAs
+
+    if (arc == NO_ERROR)
+    {
+        // file opened successfully:
+        // create EXECUTABLE structure
+
+        if (ppExec)
+        {
+            *ppExec = (PEXECUTABLE)malloc(sizeof(EXECUTABLE));
+            if (*ppExec)
+            {
+                ULONG           ulLocal = 0;
+
+                memset((*ppExec), 0, sizeof(EXECUTABLE));
+
+                // read old DOS EXE header
+                (*ppExec)->pDosExeHeader = (PDOSEXEHEADER)malloc(sizeof(DOSEXEHEADER));
+                if ((*ppExec)->pDosExeHeader == NULL)
+                    arc = ERROR_NOT_ENOUGH_MEMORY;
+                else
+                {
+                    ULONG   ulBytesRead = 0;
+                    arc = DosSetFilePtr(hFile,
+                                        0L,
+                                        FILE_BEGIN,
+                                        &ulLocal);      // out: new offset
+                    arc = DosRead(hFile,
+                                  (*ppExec)->pDosExeHeader,
+                                  sizeof(DOSEXEHEADER),
+                                  &((*ppExec)->cbDosExeHeader));
+                    // now check if we really have a DOS header
+                    if ((*ppExec)->pDosExeHeader->usDosExeID != 0x5a4d)
+                        arc = ERROR_INVALID_EXE_SIGNATURE;
+                    else
+                    {
+                        // we have a DOS header:
+                        if ((*ppExec)->pDosExeHeader->usRelocTableOfs < 0x40)
+                        {
+                            // not LX or PE or NE:
+                            (*ppExec)->ulOS = EXEOS_DOS3;
+                            (*ppExec)->ulExeFormat = EXEFORMAT_OLDDOS;
+                        }
+                        else
+                        {
+                            // either LX or PE or NE:
+                            // read more bytes from position
+                            // specified in header
+                            arc = DosSetFilePtr(hFile,
+                                                (*ppExec)->pDosExeHeader->usNewHeaderOfs,
+                                                FILE_BEGIN,
+                                                &ulLocal);
+
+                            if (arc == NO_ERROR)
+                            {
+                                PBYTE   pbCheckOS = NULL;
+
+                                // read two chars to find out header type
+                                CHAR    achNewHeaderType[2] = "";
+                                arc = DosRead(hFile,
+                                              &achNewHeaderType,
+                                              sizeof(achNewHeaderType),
+                                              &ulBytesRead);
+                                // reset file ptr
+                                DosSetFilePtr(hFile,
+                                              (*ppExec)->pDosExeHeader->usNewHeaderOfs,
+                                              FILE_BEGIN,
+                                              &ulLocal);
+
+                                if (memcmp(achNewHeaderType, "NE", 2) == 0)
+                                {
+                                    // New Executable:
+                                    (*ppExec)->ulExeFormat = EXEFORMAT_NE;
+                                    // read NE header
+                                    (*ppExec)->pNEHeader = (PNEHEADER)malloc(sizeof(NEHEADER));
+                                    DosRead(hFile,
+                                            (*ppExec)->pNEHeader,
+                                            sizeof(NEHEADER),
+                                            &((*ppExec)->cbNEHeader));
+                                    if ((*ppExec)->cbNEHeader == sizeof(NEHEADER))
+                                        pbCheckOS = &((*ppExec)->pNEHeader->bTargetOS);
+                                }
+                                else if (   (memcmp(achNewHeaderType, "LX", 2) == 0)
+                                         || (memcmp(achNewHeaderType, "LE", 2) == 0)
+                                                    // this is used by SMARTDRV.EXE
+                                        )
+                                {
+                                    // OS/2 Linear Executable:
+                                    (*ppExec)->ulExeFormat = EXEFORMAT_LX;
+                                    // read LX header
+                                    (*ppExec)->pLXHeader = (PLXHEADER)malloc(sizeof(LXHEADER));
+                                    DosRead(hFile,
+                                            (*ppExec)->pLXHeader,
+                                            sizeof(LXHEADER),
+                                            &((*ppExec)->cbLXHeader));
+                                    if ((*ppExec)->cbLXHeader == sizeof(LXHEADER))
+                                        pbCheckOS = (PBYTE)(&((*ppExec)->pLXHeader->usTargetOS));
+                                }
+                                else if (memcmp(achNewHeaderType, "PE", 2) == 0)
+                                {
+                                    (*ppExec)->ulExeFormat = EXEFORMAT_PE;
+                                    (*ppExec)->ulOS = EXEOS_WIN32;
+                                    (*ppExec)->f32Bits = TRUE;
+                                }
+                                else
+                                    arc = ERROR_INVALID_EXE_SIGNATURE;
+
+                                if (pbCheckOS)
+                                    // BYTE to check for operating system
+                                    // (NE and LX):
+                                    switch (*pbCheckOS)
+                                    {
+                                        case NEOS_OS2:
+                                            (*ppExec)->ulOS = EXEOS_OS2;
+                                            if ((*ppExec)->ulExeFormat == EXEFORMAT_LX)
+                                                (*ppExec)->f32Bits = TRUE;
+                                        break;
+                                        case NEOS_WIN16:
+                                            (*ppExec)->ulOS = EXEOS_WIN16;
+                                        break;
+                                        case NEOS_DOS4:
+                                            (*ppExec)->ulOS = EXEOS_DOS4;
+                                        break;
+                                        case NEOS_WIN386:
+                                            (*ppExec)->ulOS = EXEOS_WIN386;
+                                            (*ppExec)->f32Bits = TRUE;
+                                        break;
+                                    }
+                            }
+                        }
+                    }
+
+                    // store exec's HFILE
+                    (*ppExec)->hfExe = hFile;
+                }
+
+                if (arc != NO_ERROR)
+                    // error: clean up
+                    doshExecClose(*ppExec);
+
+            } // end if (*ppExec)
+            else
+                arc = ERROR_NOT_ENOUGH_MEMORY;
+        } // end if (ppExec)
+        else
+            arc = ERROR_INVALID_PARAMETER;
+    } // end if (arc == NO_ERROR)
+
+    return (arc);
+}
+
+/*
+ *@@ doshExecClose:
+ *      this closes an executable opened with doshExecOpen.
+ *      Always call this function if NO_ERROR was returned by
+ *      doshExecOpen.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshExecClose(PEXECUTABLE pExec)
+{
+    APIRET arc = NO_ERROR;
+    if (pExec)
+    {
+        if (pExec->pDosExeHeader)
+            free(pExec->pDosExeHeader);
+        if (pExec->pNEHeader)
+            free(pExec->pNEHeader);
+        if (pExec->pLXHeader)
+            free(pExec->pLXHeader);
+
+        if (pExec->pszDescription)
+            free(pExec->pszDescription);
+        if (pExec->pszVendor)
+            free(pExec->pszVendor);
+        if (pExec->pszVersion)
+            free(pExec->pszVersion);
+        if (pExec->pszInfo)
+            free(pExec->pszInfo);
+
+        DosClose(pExec->hfExe);
+
+        free(pExec);
+    }
+    else
+        arc = ERROR_INVALID_PARAMETER;
+
+    return (arc);
+}
+
+/*
+ *@@ doshExecQueryBldLevel:
+ *      this retrieves buildlevel information for an
+ *      LX or NE executable previously opened with
+ *      doshExecOpen.
+ *
+ *      BuildLevel information must be contained in the
+ *      DESCRIPTION field of an executable's module
+ *      definition (.DEF) file. In order to be readable
+ *      by BLDLEVEL.EXE (which ships with OS/2), this
+ *      string must have the following format:
+ *
+ +          Description '@#AUTHOR:VERSION#@ DESCRIPTION'
+ *
+ *      Example:
+ *
+ +          Description '@#Ulrich Mller:0.9.0#@ XWorkplace Sound Support Module'
+ *
+ *      The "Description" entry always ends up as the
+ *      very first entry in the non-resident name table
+ *      in LX and NE executables. So this is what we retrieve
+ *      here.
+ *
+ *      If the first entry in that table exists, NO_ERROR is
+ *      returned and at least the pszDescription field in
+ *      EXECUTABLE is set to that information.
+ *
+ *      If that string is in IBM BLDLEVEL format, the string
+ *      is automatically parsed, and the pszVendor, pszVersion,
+ *      and pszInfo fields are also set. In the above examples,
+ *      this would return the following information:
+ +          pszVendor = "Ulrich Mller"
+ +          pszVersion = "0.9.0"
+ +          pszInfo = "XWorkplace Sound Support Module"
+ *
+ *      If that string is not in BLDLEVEL format, only pszDescription
+ *      will be set. The other fields remain NULL.
+ *
+ *      This returns the following errors:
+ *      -- ERROR_INVALID_PARAMETER: pExec invalid
+ *      -- ERROR_INVALID_EXE_SIGNATURE (191): pExec is not in LX or NE format
+ *      -- ERROR_INVALID_DATA (13): non-resident name table not found.
+ *      -- ERROR_NOT_ENOUGH_MEMORY: malloc() failed.
+ *
+ *      plus the error codes of DosSetFilePtr and DosRead.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.0 (99-10-22) [umoeller]: NE format now supported
+ *@@changed V0.9.1 (99-12-06): fixed memory leak
+ */
+
+APIRET doshExecQueryBldLevel(PEXECUTABLE pExec)
+{
+    APIRET      arc = NO_ERROR;
+    PSZ         pszNameTable = NULL;
+    ULONG       ulNRNTOfs = 0;
+
+    do
+    {
+        ULONG       ulLocal = 0,
+                    ulBytesRead = 0;
+        PSZ         pStartOfAuthor = NULL;
+
+        if (pExec == NULL)
+        {
+            arc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        if (pExec->ulExeFormat == EXEFORMAT_LX)
+        {
+            // OK, LX format:
+            // check if we have a non-resident name table
+            if (pExec->pLXHeader == NULL)
+            {
+                arc = ERROR_INVALID_DATA;
+                break;
+            }
+            if (pExec->pLXHeader->ulNonResdNameTblOfs == 0)
+            {
+                arc = ERROR_INVALID_DATA;
+                break;
+            }
+
+            ulNRNTOfs = pExec->pLXHeader->ulNonResdNameTblOfs;
+        }
+        else if (pExec->ulExeFormat == EXEFORMAT_NE)
+        {
+            // OK, NE format:
+            // check if we have a non-resident name table
+            if (pExec->pNEHeader == NULL)
+            {
+                arc = ERROR_INVALID_DATA;
+                break;
+            }
+            if (pExec->pNEHeader->ulNonResdTblOfs == 0)
+            {
+                arc = ERROR_INVALID_DATA;
+                break;
+            }
+
+            ulNRNTOfs = pExec->pNEHeader->ulNonResdTblOfs;
+        }
+        else
+        {
+            // neither LX nor NE: stop
+            arc = ERROR_INVALID_EXE_SIGNATURE;
+            break;
+        }
+
+        if (ulNRNTOfs == 0)
+        {
+            // shouldn't happen
+            arc = ERROR_INVALID_DATA;
+            break;
+        }
+
+        // move EXE file pointer to offset of non-resident name table
+        // (from LX header)
+        arc = DosSetFilePtr(pExec->hfExe,     // file is still open
+                            ulNRNTOfs,      // ofs determined above
+                            FILE_BEGIN,
+                            &ulLocal);
+        if (arc != NO_ERROR)
+            break;
+
+        // allocate memory as necessary
+        pszNameTable = (PSZ)malloc(2001); // should suffice, because each entry
+                                    // may only be 255 bytes in length
+        if (pszNameTable)
+        {
+            arc = DosRead(pExec->hfExe,
+                          pszNameTable,
+                          2000,
+                          &ulBytesRead);
+            if (arc != NO_ERROR)
+                break;
+            if (*pszNameTable == 0)
+            {
+                // first byte (length byte) is null:
+                arc = ERROR_INVALID_DATA;
+                free (pszNameTable);        // fixed V0.9.1 (99-12-06)
+                break;
+            }
+
+            // now copy the string, which is in Pascal format
+            pExec->pszDescription = (PSZ)malloc((*pszNameTable) + 1);    // addt'l null byte
+            memcpy(pExec->pszDescription,
+                   pszNameTable + 1,        // skip length byte
+                   *pszNameTable);          // length byte
+            // terminate string
+            *(pExec->pszDescription + (*pszNameTable)) = 0;
+
+            // _Pmpf(("pszDescription: %s", pExec->pszDescription));
+
+            // now parse the damn thing:
+            // @#AUTHOR:VERSION#@ DESCRIPTION
+            // but skip the first byte, which has the string length
+            pStartOfAuthor = strstr(pExec->pszDescription, "@#");
+            if (pStartOfAuthor)
+            {
+                PSZ pStartOfInfo = strstr(pStartOfAuthor + 2, "#@");
+                // _Pmpf(("Testing"));
+                if (pStartOfInfo)
+                {
+                    PSZ pEndOfAuthor = strchr(pStartOfAuthor + 2, ':');
+                    // _Pmpf(("pStartOfinfo: %s", pStartOfInfo));
+                    if (pEndOfAuthor)
+                    {
+                        // _Pmpf(("pEndOfAuthor: %s", pEndOfAuthor));
+                        pExec->pszVendor = strhSubstr(pStartOfAuthor + 2, pEndOfAuthor);
+                        pExec->pszVersion = strhSubstr(pEndOfAuthor + 1, pStartOfInfo);
+                        // skip "@#" in info string
+                        pStartOfInfo += 2;
+                        // skip leading spaces in info string
+                        while (*pStartOfInfo == ' ')
+                            pStartOfInfo++;
+                        // and copy until end of string
+                        pExec->pszInfo = strdup(pStartOfInfo);
+                    }
+                }
+            }
+
+            free(pszNameTable);
+        }
+        else
+            arc = ERROR_NOT_ENOUGH_MEMORY;
+    } while (FALSE);
+
+
+    return (arc);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Partitions info
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   Partition functions                                            *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ doshQueryDiskCount:
+ *      returns the no. of physical disks installed
+ *      on the system.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+UINT doshQueryDiskCount(VOID)
+{
+    USHORT count = 0;
+
+    DosPhysicalDisk(INFO_COUNT_PARTITIONABLE_DISKS, &count, 2, 0, 0);
+    return (count);
+}
+
+/*
+ *@@ doshReadSector:
+ *      reads a physical disk sector.
+ *
+ *      If NO_ERROR is returned, the sector contents
+ *      have been stored in *buff.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshReadSector(USHORT disk,      // in: physical disk no. (1, 2, 3, ...)
+                      void *buff,
+                      USHORT head,
+                      USHORT cylinder,
+                      USHORT sector)
+{
+    UINT   arc;
+    HFILE  dh = 0;
+    char   dn[256];
+    // char   ms[256];
+
+    sprintf( dn, "%u:", disk );
+    arc = DosPhysicalDisk(INFO_GETIOCTLHANDLE, &dh, 2, dn, 3);
+
+    if (arc)
+        // error:
+        return (arc);
+    else
+    {
+        TRACKLAYOUT DiskIOParm;
+        ULONG IOCtlDataLength = sizeof(DiskIOParm);
+        ULONG IOCtlParmLength = 512;
+
+        DiskIOParm.bCommand = 0;
+        DiskIOParm.usHead = head;
+        DiskIOParm.usCylinder = cylinder;
+        DiskIOParm.usFirstSector = 0;
+        DiskIOParm.cSectors = 1;
+        DiskIOParm.TrackTable[0].usSectorNumber = sector;
+        DiskIOParm.TrackTable[0].usSectorSize = 512;
+
+        arc = DosDevIOCtl(dh,
+                          IOCTL_PHYSICALDISK, PDSK_READPHYSTRACK,
+                          &DiskIOParm, IOCtlParmLength, &IOCtlParmLength,
+                          buff       , IOCtlDataLength, &IOCtlDataLength);
+
+        if(arc)
+        {
+            // error:
+            DosPhysicalDisk(INFO_FREEIOCTLHANDLE, 0, 0, &dh, 2);
+            return (arc);
+        }
+
+        DosPhysicalDisk(INFO_FREEIOCTLHANDLE, 0, 0, &dh, 2);
+    }
+    return (NO_ERROR);
+}
+
+/*
+ *@@ doshType2FSName:
+ *      this returns a static, zero-terminated string
+ *      for the given FS type. This is always 7 bytes
+ *      in length.
+ *
+ *      Values for operating system indicator:
+ *      --  00h  empty
+ *      --  01h  DOS 12-bit FAT
+ *      --  02h  XENIX root file system
+ *      --  03h  XENIX /usr file system (obsolete)
+ *      --  04h  DOS 16-bit FAT (up to 32M)
+ *      --  05h  DOS 3.3+ extended partition
+ *      --  06h  DOS 3.31+ Large File System (16-bit FAT, over 32M)
+ *      --  07h  QNX
+ *      --  07h  OS/2 HPFS
+ *      --  07h  Windows NT NTFS
+ *      --  07h  Advanced Unix
+ *      --  08h  OS/2 (v1.0-1.3 only)
+ *      --  08h  AIX bootable partition, SplitDrive
+ *      --  08h  Commodore DOS
+ *      --  08h  DELL partition spanning multiple drives
+ *      --  09h  AIX data partition
+ *      --  09h  Coherent filesystem
+ *      --  0Ah  OS/2 Boot Manager
+ *      --  0Ah  OPUS
+ *      --  0Ah  Coherent swap partition
+ *      --  0Bh  Windows95 with 32-bit FAT
+ *      --  0Ch  Windows95 with 32-bit FAT (using LBA-mode INT 13 extensions)
+ *      --  0Eh  logical-block-addressable VFAT (same as 06h but using LBA-mode INT 13)
+ *      --  0Fh  logical-block-addressable VFAT (same as 05h but using LBA-mode INT 13)
+ *      --  10h  OPUS
+ *      --  11h  OS/2 Boot Manager hidden 12-bit FAT partition
+ *      --  12h  Compaq Diagnostics partition
+ *      --  14h  (resulted from using Novell DOS 7.0 FDISK to delete Linux Native part)
+ *      --  14h  OS/2 Boot Manager hidden sub-32M 16-bit FAT partition
+ *      --  16h  OS/2 Boot Manager hidden over-32M 16-bit FAT partition
+ *      --  17h  OS/2 Boot Manager hidden HPFS partition
+ *      --  18h  AST special Windows swap file ("Zero-Volt Suspend" partition)
+ *      --  21h  officially listed as reserved
+ *      --  23h  officially listed as reserved
+ *      --  24h  NEC MS-DOS 3.x
+ *      --  26h  officially listed as reserved
+ *      --  31h  officially listed as reserved
+ *      --  33h  officially listed as reserved
+ *      --  34h  officially listed as reserved
+ *      --  36h  officially listed as reserved
+ *      --  38h  Theos
+ *      --  3Ch  PowerQuest PartitionMagic recovery partition
+ *      --  40h  VENIX 80286
+ *      --  41h  Personal RISC Boot
+ *      --  42h  SFS (Secure File System) by Peter Gutmann
+ *      --  50h  OnTrack Disk Manager, read-only partition
+ *      --  51h  OnTrack Disk Manager, read/write partition
+ *      --  51h  NOVEL
+ *      --  52h  CP/M
+ *      --  52h  Microport System V/386
+ *      --  53h  OnTrack Disk Manager, write-only partition???
+ *      --  54h  OnTrack Disk Manager (DDO)
+ *      --  56h  GoldenBow VFeature
+ *      --  61h  SpeedStor
+ *      --  63h  Unix SysV/386, 386/ix
+ *      --  63h  Mach, MtXinu BSD 4.3 on Mach
+ *      --  63h  GNU HURD
+ *      --  64h  Novell NetWare 286
+ *      --  65h  Novell NetWare (3.11)
+ *      --  67h  Novell
+ *      --  68h  Novell
+ *      --  69h  Novell
+ *      --  70h  DiskSecure Multi-Boot
+ *      --  71h  officially listed as reserved
+ *      --  73h  officially listed as reserved
+ *      --  74h  officially listed as reserved
+ *      --  75h  PC/IX
+ *      --  76h  officially listed as reserved
+ *      --  80h  Minix v1.1 - 1.4a
+ *      --  81h  Minix v1.4b+
+ *      --  81h  Linux
+ *      --  81h  Mitac Advanced Disk Manager
+ *      --  82h  Linux Swap partition
+ *      --  82h  Prime
+ *      --  83h  Linux native file system (ext2fs/xiafs)
+ *      --  84h  OS/2-renumbered type 04h partition (related to hiding DOS C: drive)
+ *      --  86h  FAT16 volume/stripe set (Windows NT)
+ *      --  87h  HPFS Fault-Tolerant mirrored partition
+ *      --  87h  NTFS volume/stripe set
+ *      --  93h  Amoeba file system
+ *      --  94h  Amoeba bad block table
+ *      --  A0h  Phoenix NoteBIOS Power Management "Save-to-Disk" partition
+ *      --  A1h  officially listed as reserved
+ *      --  A3h  officially listed as reserved
+ *      --  A4h  officially listed as reserved
+ *      --  A5h  FreeBSD, BSD/386
+ *      --  A6h  officially listed as reserved
+ *      --  B1h  officially listed as reserved
+ *      --  B3h  officially listed as reserved
+ *      --  B4h  officially listed as reserved
+ *      --  B6h  officially listed as reserved
+ *      --  B7h  BSDI file system (secondarily swap)
+ *      --  B8h  BSDI swap partition (secondarily file system)
+ *      --  C1h  DR DOS 6.0 LOGIN.EXE-secured 12-bit FAT partition
+ *      --  C4h  DR DOS 6.0 LOGIN.EXE-secured 16-bit FAT partition
+ *      --  C6h  DR DOS 6.0 LOGIN.EXE-secured Huge partition
+ *      --  C6h  corrupted FAT16 volume/stripe set (Windows NT)
+ *      --  C7h  Syrinx Boot
+ *      --  C7h  corrupted NTFS volume/stripe set
+ *      --  D8h  CP/M-86
+ *      --  DBh  CP/M, Concurrent CP/M, Concurrent DOS
+ *      --  DBh  CTOS (Convergent Technologies OS)
+ *      --  E1h  SpeedStor 12-bit FAT extended partition
+ *      --  E3h  DOS read-only
+ *      --  E3h  Storage Dimensions
+ *      --  E4h  SpeedStor 16-bit FAT extended partition
+ *      --  E5h  officially listed as reserved
+ *      --  E6h  officially listed as reserved
+ *      --  F1h  Storage Dimensions
+ *      --  F2h  DOS 3.3+ secondary partition
+ *      --  F3h  officially listed as reserved
+ *      --  F4h  SpeedStor
+ *      --  F4h  Storage Dimensions
+ *      --  F6h  officially listed as reserved
+ *      --  FEh  LANstep
+ *      --  FEh  IBM PS/2 IML
+ *      --  FFh  Xenix bad block table
+ *
+ *      Note: for partition type 07h, one should inspect the partition boot record
+ *            for the actual file system type
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+char* doshType2FSName(unsigned char bFSType)  // in: FS type
+{
+    PSZ zFSName = NULL;
+
+    switch (bFSType)
+    {
+        case PAR_UNUSED:
+            zFSName = "UNUSED ";
+            break;
+        case PAR_FAT12SMALL:
+            zFSName = "FAT-12 ";
+            break;
+        case PAR_XENIXROOT:
+            zFSName = "XENIX  ";
+            break;
+        case PAR_XENIXUSER:
+            zFSName = "XENIX  ";
+            break;
+        case PAR_FAT16SMALL:
+            zFSName = "FAT-16 ";
+            break;
+        case PAR_EXTENDED:
+            zFSName = "EXTEND ";
+            break;
+        case PAR_FAT16BIG:
+            zFSName = "BIGDOS ";
+            break;
+        case PAR_HPFS:
+            zFSName = "HPFS   ";
+            break;
+        case PAR_AIXBOOT:
+            zFSName = "AIX    ";
+            break;
+        case PAR_AIXDATA:
+            zFSName = "AIX    ";
+            break;
+        case PAR_BOOTMANAGER:
+            zFSName = "BOOTMNG";
+            break;
+        case PAR_WINDOWS95:
+            zFSName = "WIN95  ";
+            break;
+        case PAR_WINDOWS95LB:
+            zFSName = "WIN95  ";
+            break;
+        case PAR_VFAT16BIG:
+            zFSName = "VFAT   ";
+            break;
+        case PAR_VFAT16EXT:
+            zFSName = "VFAT   ";
+            break;
+        case PAR_OPUS:
+            zFSName = "OPUS   ";
+            break;
+        case PAR_HID12SMALL:
+            zFSName = "FAT-12*";
+            break;
+        case PAR_COMPAQDIAG:
+            zFSName = "COMPAQ ";
+            break;
+        case PAR_HID16SMALL:
+            zFSName = "FAT-16*";
+            break;
+        case PAR_HID16BIG:
+            zFSName = "BIGDOS*";
+            break;
+        case PAR_HIDHPFS:
+            zFSName = "HPFS*  ";
+            break;
+        case PAR_WINDOWSSWP:
+            zFSName = "WINSWAP";
+            break;
+        case PAR_NECDOS:
+            zFSName = "NECDOS ";
+            break;
+        case PAR_THEOS:
+            zFSName = "THEOS  ";
+            break;
+        case PAR_VENIX:
+            zFSName = "VENIX  ";
+            break;
+        case PAR_RISCBOOT:
+            zFSName = "RISC   ";
+            break;
+        case PAR_SFS:
+            zFSName = "SFS    ";
+            break;
+        case PAR_ONTRACK:
+            zFSName = "ONTRACK";
+            break;
+        case PAR_ONTRACKEXT:
+            zFSName = "ONTRACK";
+            break;
+        case PAR_CPM:
+            zFSName = "CP/M   ";
+            break;
+        case PAR_UNIXSYSV:
+            zFSName = "UNIX   ";
+            break;
+        case PAR_NOVELL_64:
+            zFSName = "NOVELL ";
+            break;
+        case PAR_NOVELL_65:
+            zFSName = "NOVELL ";
+            break;
+        case PAR_NOVELL_67:
+            zFSName = "NOVELL ";
+            break;
+        case PAR_NOVELL_68:
+            zFSName = "NOVELL ";
+            break;
+        case PAR_NOVELL_69:
+            zFSName = "NOVELL ";
+            break;
+        case PAR_PCIX:
+            zFSName = "PCIX   ";
+            break;
+        case PAR_MINIX:
+            zFSName = "MINIX  ";
+            break;
+        case PAR_LINUX:
+            zFSName = "LINUX  ";
+            break;
+        case PAR_LINUXSWAP:
+            zFSName = "LNXSWP ";
+            break;
+        case PAR_LINUXFILE:
+            zFSName = "LINUX  ";
+            break;
+        case PAR_FREEBSD:
+            zFSName = "FREEBSD";
+            break;
+        case PAR_BBT:
+            zFSName = "BBT    ";
+            break;
+
+        default:
+            zFSName = "       ";
+            break;
+    }
+    return zFSName;
+}
+
+/*
+ * AppendPartition:
+ *      this appends the given partition information to
+ *      the given partition list. To do this, a new
+ *      PARTITIONINFO structure is created and appended
+ *      in a list (managed thru the PARTITIONINFO.pNext
+ *      items).
+ *
+ *      pppiThis must be a pointer to a pointer to a PARTITIONINFO.
+ *      With each call of this function, this pointer is advanced
+ *      to point to the newly created PARTITIONINFO, so before
+ *      calling this function for the first time,
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET AppendPartition(PARTITIONINFO **pppiFirst,
+                       PARTITIONINFO **pppiThis,    // in/out: partition info; pointer will be advanced
+                       PUSHORT posCount,            // in/out: partition count
+                       BYTE bDisk,                  // in: disk of partition
+                       char *pszBootName,           // in: boot partition name
+                       CHAR cLetter,                // in/out: drive letter
+                       BYTE bFsType,                // in: file system type
+                       BOOL fPrimary,               // in: primary?
+                       BOOL fBootable,
+                       ULONG ulSectors)             // in: no. of sectors
+{
+    APIRET arc = NO_ERROR;
+    PPARTITIONINFO ppiNew = (PPARTITIONINFO)malloc(sizeof(PARTITIONINFO));
+    if (ppiNew)
+    {
+        // store data
+        ppiNew->bDisk = bDisk;
+        if (fBootable)
+        {
+            memcpy(ppiNew->szBootName, pszBootName, 8);
+            ppiNew->szBootName[8] = 0;
+        }
+        else
+            ppiNew->szBootName[0] = 0;
+        ppiNew->cLetter = cLetter;
+        ppiNew->bFSType = bFsType;
+        strcpy(ppiNew->szFSType,
+               doshType2FSName(bFsType));
+        ppiNew->fPrimary = fPrimary;
+        ppiNew->fBootable = fBootable;
+        ppiNew->ulSize = ulSectors / 2048;
+
+        ppiNew->pNext = NULL;
+
+        (*posCount)++;
+
+        if (*pppiFirst == (PPARTITIONINFO)NULL)
+        {
+            // first call:
+            *pppiFirst = ppiNew;
+            *pppiThis = ppiNew;
+        }
+        else
+        {
+            // append to list
+            (**pppiThis).pNext = ppiNew;
+            *pppiThis = ppiNew;
+        }
+    }
+    else
+        arc = ERROR_NOT_ENOUGH_MEMORY;
+
+    return (arc);
+}
+
+// Sector and Cylinder values are actually 6 bits and 10 bits:
+//
+//   1 1 1 1 1 1
+//  Ú5Â4Â3Â2Â1Â0Â9Â8Â7Â6Â5Â4Â3Â2Â1ÂÄ¿
+//  ³c c c c c c c c C c S s s s s s³
+//  ÀÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÁÄÙ
+//
+// The high two bits of the second byte are used as the high bits
+// of a 10-bit value.  This allows for as many as 1024 cylinders
+// and 64 sectors per cylinder.
+
+/*
+ * GetCyl:
+ *      get cylinder number.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+static USHORT GetCyl(USHORT rBeginSecCyl)
+{
+    return ((rBeginSecCyl & 0x00C0) << 2) +
+        ((rBeginSecCyl & 0xFF00) >> 8);
+}
+
+/*
+ * GetSec:
+ *      get sector number.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+static USHORT GetSec(USHORT rBeginSecCyl)
+{
+    return rBeginSecCyl & 0x003F;
+}
+
+/*
+ *@@ doshGetBootManager:
+ *      this goes thru the master boot records on all
+ *      disks to find the boot manager partitions.
+ *
+ *      Returns:
+ *      -- NO_ERROR: boot manager found; in that case,
+ *                   information about the boot manager
+ *                   is written into *pusDisk, *pusPart,
+ *                   *BmInfo. Any of these pointers can
+ *                   be NULL if you're not interested.
+ *      -- ERROR_NOT_SUPPORTED (50): boot manager not installed.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshGetBootManager(USHORT   *pusDisk,    // out: if != NULL, boot manager disk (1, 2, ...)
+                          USHORT   *pusPart,    // out: if != NULL, index of bmgr primary partition (0-3)
+                          PAR_INFO *BmInfo)     // out: if != NULL, boot manager partition info
+{
+    APIRET          arc = NO_ERROR;
+    USHORT          count = doshQueryDiskCount();    // Physical disk number
+    MBR_INFO        MBoot;      // Master Boot
+    USHORT          usDisk;
+
+    if (count > 8)              // Not above 8 disks
+        count = 8;
+
+    for (usDisk = 1; usDisk <= count; usDisk++)
+    {
+        USHORT usPrim = 0;
+
+        // for each disk, read the MBR, which has the
+        // primary partitions
+        if ((arc = doshReadSector(usDisk, &MBoot,
+                                  0, 0, 1)))
+            return (arc);
+
+        // Lookup BootManager partition
+        for (usPrim = 0; usPrim < 4; usPrim++)
+        {
+            if (MBoot.sPrtnInfo[usPrim].bFileSysCode == 0x0A)
+            {
+                if (BmInfo)
+                    *BmInfo = MBoot.sPrtnInfo[usPrim];
+                if (pusPart)
+                    *pusPart = usPrim;
+                if (pusDisk)
+                    *pusDisk = usDisk;
+                return (NO_ERROR);
+            }
+        }
+    }
+
+    return (ERROR_NOT_SUPPORTED);
+}
+
+/*
+ * GetPrimaryPartitions:
+ *      this returns the primary partitions.
+ *
+ *      This gets called from doshGetPartitionsList.
+ *
+ *      Returns 0 upon errors.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET GetPrimaryPartitions(PARTITIONINFO **pppiFirst,
+                            PARTITIONINFO **pppiThis,
+                            PUSHORT posCount,    // in/out: partition count
+                            PCHAR pcLetter,      // in/out: drive letter counter
+                            UINT BmDisk,         // in: physical disk (1, 2, 3, ...) of boot manager or null
+                            PAR_INFO* BmInfo,    // in: info returned by doshGetBootManager or NULL
+                            UINT iDisk)          // in: system's physical disk count
+{
+    APIRET          arc = NO_ERROR;
+    MBR_INFO        MBoot;      // Master Boot
+    SYS_INFO        MName[32];  // Name Space from Boot Manager
+    USHORT          i;
+
+    memset(&MName, 0, sizeof(MName));
+
+    if (BmInfo)
+    {
+        // read boot manager name table
+        if ((arc = doshReadSector(BmDisk, &MName, BmInfo->bBeginHead,
+                                  GetCyl(BmInfo->rBeginSecCyl),
+                                  GetSec(BmInfo->rBeginSecCyl) + 3)))
+            return (arc);
+    }
+
+    // read master boot record
+    if ((arc = doshReadSector(iDisk, &MBoot, 0, 0, 1)))
+        return (arc);
+
+    for (i = 0;
+         i < 4;     // there can be only four primary partitions
+         i++)
+    {
+        // skip unused partition, BootManager or Extended partition
+        if (    (MBoot.sPrtnInfo[i].bFileSysCode)  // skip unused
+            &&  (MBoot.sPrtnInfo[i].bFileSysCode != PAR_BOOTMANAGER) // skip boot manager
+            &&  (MBoot.sPrtnInfo[i].bFileSysCode != PAR_EXTENDED) // skip extended
+           )
+        {
+            BOOL fBootable = (  (BmInfo)
+                             && (MName[(iDisk-1) * 4 + i].bootable & 0x01)
+                             );
+            // store this partition
+            if ((arc = AppendPartition(pppiFirst,
+                                       pppiThis,
+                                       posCount,
+                                       iDisk,
+                                       (fBootable)
+                                         ? (char*)&MName[(iDisk - 1) * 4 + i].name
+                                         : "",
+                                       *pcLetter,
+                                       MBoot.sPrtnInfo[i].bFileSysCode,
+                                       TRUE,        // primary
+                                       fBootable,
+                                       MBoot.sPrtnInfo[i].lTotalSects)))
+                return (arc);
+        }
+    }
+    return (NO_ERROR);
+}
+
+/*
+ * GetLogicalDrives:
+ *      this returns info for the logical drives
+ *      in the extended partition. This gets called
+ *      from GetExtendedPartition.
+ *
+ *      This gets called from GetExtendedPartition.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET GetLogicalDrives(PARTITIONINFO **pppiFirst,
+                        PARTITIONINFO **pppiThis,
+                        PUSHORT posCount,
+                        PCHAR pcLetter,
+                        PAR_INFO* PrInfo,                    // in: MBR entry of extended partition
+                        UINT PrDisk,
+                        PAR_INFO* BmInfo)
+{
+    APIRET          arc = NO_ERROR;
+    EXT_INFO        MBoot;      // Master Boot
+    USHORT          i;
+
+    if ((arc = doshReadSector(PrDisk, &MBoot, PrInfo->bBeginHead,
+                              GetCyl(PrInfo->rBeginSecCyl),
+                              GetSec(PrInfo->rBeginSecCyl))))
+        return (arc);
+
+    for (i = 0; i < 4; i++)
+    {
+        // skip unused partition or BootManager partition
+        if (    (MBoot.sPrtnInfo[i].bFileSysCode)
+             && (MBoot.sPrtnInfo[i].bFileSysCode != PAR_BOOTMANAGER)
+           )
+        {
+            BOOL    fBootable = FALSE;
+            BOOL    fAssignLetter = FALSE;
+
+            // special work around extended partition
+            if (MBoot.sPrtnInfo[i].bFileSysCode == PAR_EXTENDED)
+            {
+                if ((arc = GetLogicalDrives(pppiFirst,
+                                            pppiThis,
+                                            posCount,
+                                            pcLetter,
+                                            &MBoot.sPrtnInfo[i],
+                                            PrDisk,
+                                            BmInfo)))
+                    return (arc);
+
+                continue;
+            }
+
+            // raise driver letter if OS/2 would recognize this drive
+            if (    (MBoot.sPrtnInfo[i].bFileSysCode < PAR_PCIX)
+               )
+                fAssignLetter = TRUE;
+
+            if (fAssignLetter)
+                (*pcLetter)++;
+
+            fBootable = (   (BmInfo)
+                         && ((MBoot.sBmNames[i].bootable & 0x01) != 0)
+                        );
+
+            if ((arc = AppendPartition(pppiFirst,
+                                       pppiThis,
+                                       posCount,
+                                       PrDisk,
+                                       (fBootable)
+                                         ? (char*)&MBoot.sBmNames[i].name
+                                         : "",
+                                       (fAssignLetter)
+                                         ? *pcLetter
+                                         : ' ',
+                                       MBoot.sPrtnInfo[i].bFileSysCode,
+                                       FALSE,        // primary
+                                       fBootable,    // bootable
+                                       MBoot.sPrtnInfo[i].lTotalSects)))
+                return (arc);
+        }
+
+        /* // if BootManager installed and partition is bootable
+        if (BmInfo)
+        {
+            if (MBoot.sBmNames[i].bootable & 0x01)
+            {
+            }
+        }
+
+        // if BootManager not installed
+        else
+        {
+            if (arc = AppendPartition(pppiFirst,
+                                      pppiThis,
+                                      posCount,
+                                      PrDisk,
+                                      "",
+                                      *pcLetter,
+                                      MBoot.sPrtnInfo[i].bFileSysCode,
+                                      FALSE,
+                                      MBoot.sPrtnInfo[i].lTotalSects))
+                return (arc);
+        } */
+    }
+
+    return (NO_ERROR);
+}
+
+/*
+ * GetExtendedPartition:
+ *      this finds the extended partition on the given
+ *      drive and calls GetLogicalDrives in turn.
+ *
+ *      This gets called from doshGetPartitionsList.
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET GetExtendedPartition(PARTITIONINFO **pppiFirst,
+                            PARTITIONINFO **pppiThis,
+                            PUSHORT posCount,
+                            PCHAR pcLetter,
+                            PAR_INFO* BmInfo,
+                            UINT iDisk)                // in: disk to query
+{
+    APIRET          arc = NO_ERROR;
+    MBR_INFO        MBoot;      // Master Boot
+    USHORT          i;
+
+    if ((arc = doshReadSector(iDisk, &MBoot, 0, 0, 1)))
+        return (arc);
+
+    // go thru MBR entries to find extended partition
+    for (i = 0;
+         i < 4;
+         i++)
+    {
+        if (MBoot.sPrtnInfo[i].bFileSysCode == PAR_EXTENDED)
+        {
+            if ((arc = GetLogicalDrives(pppiFirst,
+                                        pppiThis,
+                                        posCount,
+                                        pcLetter,
+                                        &MBoot.sPrtnInfo[i],
+                                        iDisk,
+                                        BmInfo)))
+                return (arc);
+        }
+    }
+
+    return (NO_ERROR);
+}
+
+/*
+ *@@ doshGetPartitionsList:
+ *      this returns lots of information about the
+ *      partitions on all physical disks, which is
+ *      read directly from the MBRs and partition
+ *      tables.
+ *
+ *      If NO_ERROR is returned by this function,
+ *      *ppPartitionInfo points to a linked list of
+ *      PARTITIONINFO structures, which has
+ *      *pusPartitionCount items.
+ *
+ *      In that case, use doshFreePartitionsList to
+ *      free the resources allocated by this function.
+ *
+ *      The linked list starts out with all the primary
+ *      partitions, followed by the logical drives in
+ *      the extended partitions. This function attempts
+ *      to guess the correct drive letters and stores
+ *      these with the PARTITIONINFO items, but there's
+ *      no guarantee that this is correct. We correctly
+ *      ignore Linux partitions here and give all primary
+ *      partitions the C: letter, but I have no idea
+ *      what happens with NTFS partitions, since I have
+ *      none.
+ *
+ *      If an error != NO_ERROR is returned, *pusContext
+ *      will be set to one of the following:
+ *      --  1: boot manager not found
+ *      --  2: primary partitions error
+ *      --  3: secondary partitions error
+ *
+ *      Based on code (C) Dmitry A. Steklenev.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshGetPartitionsList(PPARTITIONINFO *ppPartitionInfo,   // out: partition info array
+                             PUSHORT pusPartitionCount,         // out: count of items in **ppPartitionInfo
+                             PUSHORT pusContext)                // out: error context
+{
+    APIRET          arc = NO_ERROR;
+    PAR_INFO        BmInfo;     // BootManager partition
+    USHORT          usBmDisk;     // BootManager disk
+    USHORT          cDisks = doshQueryDiskCount();    // physical disks count
+    USHORT          i;
+
+    PARTITIONINFO   *pPartitionInfos = NULL, // linked list of all partitions
+                    *ppiTemp = NULL;
+    USHORT          osCount;        // bootable partition count
+    CHAR            cLetter = 'C';  // first drive letter
+
+    if (cDisks > 8)              // Not above 8 disks
+        cDisks = 8;
+
+    // get boot manager disk and info
+    if ((arc = doshGetBootManager(&usBmDisk,
+                                  NULL,
+                                  &BmInfo)) != NO_ERROR)
+    {
+        *pusContext = 1;
+        return (arc);
+    }
+    // on each disk, read primary partitions
+    for (i = 1; i <= cDisks; i++)
+        if ((arc = GetPrimaryPartitions(&pPartitionInfos,
+                                        &ppiTemp,
+                                        &osCount,
+                                        &cLetter,
+                                        usBmDisk,
+                                        usBmDisk ? &BmInfo : 0,
+                                        i)))
+        {
+            *pusContext = 2;
+            return (arc);
+        }
+
+    if (usBmDisk)
+    {
+        // boot manager found:
+        // on each disk, read extended partition
+        // with logical drives
+        for (i = 1; i <= cDisks; i++)
+            if ((arc = GetExtendedPartition(&pPartitionInfos,
+                                            &ppiTemp,
+                                            &osCount,
+                                            &cLetter,
+                                            &BmInfo,
+                                            i)))
+        {
+            *pusContext = 3;
+            return (arc);
+        }
+    }
+
+    *ppPartitionInfo = pPartitionInfos;
+    *pusPartitionCount = osCount;
+
+    return (NO_ERROR);  // 0
+}
+
+/*
+ *@@ doshFreePartitionsList:
+ *      this frees the resources allocated by
+ *      doshGetPartitionsList.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET doshFreePartitionsList(PPARTITIONINFO pPartitionInfo)
+{
+    PPARTITIONINFO ppiThis = NULL;
+    ppiThis = pPartitionInfo;
+    while (ppiThis)
+    {
+        PPARTITIONINFO ppiNext = ppiThis->pNext;
+        free(ppiThis);
+        ppiThis = ppiNext;
+    }
+    return (NO_ERROR);
+}
+
+
Index: /trunk/src/helpers/eah.c
===================================================================
--- /trunk/src/helpers/eah.c	(revision 8)
+++ /trunk/src/helpers/eah.c	(revision 8)
@@ -0,0 +1,1497 @@
+
+/*
+ *@@sourcefile eah.c:
+ *      contains helper functions for handling Extended Attributes.
+ *      See explanations below.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  ea*         EA helper functions
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      This file is new with V0.81 and contains all the EA functions
+ *      that were in helpers.c previously.
+ *
+ *      Most of the functions in here come in pairs: one for working
+ *      on EAs based on a path (eaPath*), one for open HFILEs (eaHFile*).
+ *      Most functions return structures called EABINDING which should
+ *      be freed again later.
+ *
+ *      <B>Example:</B>
+ +          PEABINDING peab = eaPathReadOneByName("C:\Desktop", ".LONGNAME");
+ +          if (peab)
+ +              PSZ pszLongName = peab->pszValue;
+ +          eaFreeBinding(peab);
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\eah.h"
+ */
+
+/*
+ *      Most of the code in this file dealing with Extended Attributes is based
+ *      on code (w) by Chris Hanson (cph@zurich.ai.mit.edu).
+ *      Copyright (c) 1995 Massachusetts Institute of Technology.
+ *
+ *      The original code is available as EALIB.ZIP at Hobbes.
+ *
+ *      This material was developed by the Scheme project at the Massachusetts
+ *      Institute of Technology, Department of Electrical Engineering and
+ *      Computer Science.  Permission to copy this software, to redistribute
+ *      it, and to use it for any purpose is granted, subject to the following
+ *      restrictions and understandings.
+ *
+ *      1. Any copy made of this software must include this copyright notice
+ *      in full.
+ *
+ *      2. Users of this software agree to make their best efforts (a) to
+ *      return to the MIT Scheme project any improvements or extensions that
+ *      they make, so that these may be included in future releases; and (b)
+ *      to inform MIT of noteworthy uses of this software.
+ *
+ *      3. All materials developed as a consequence of the use of this
+ *      software shall duly acknowledge such use, in accordance with the usual
+ *      standards of acknowledging credit in academic research.
+ *
+ *      4. MIT has made no warrantee or representation that the operation of
+ *      this software will be error-free, and MIT is under no obligation to
+ *      provide any services, by way of maintenance, update, or otherwise.
+ *
+ *      5. In conjunction with products arising from the use of this material,
+ *      there shall be no use of the name of the Massachusetts Institute of
+ *      Technology nor of any adaptation thereof in any advertising,
+ *      promotional, or sales literature without prior written consent from
+ *      MIT in each case.
+ *
+ *      Copyright (C) 1995 Massachusetts Institute of Technology.
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\eah.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Control program helpers\Extended attributes
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   Extended Attribute handling                                    *
+ *                                                                  *
+ ********************************************************************/
+
+#define EA_BINDING_FLAGS(binding) ((binding)->bFlags)
+#define EA_BINDING_NAME_LENGTH(binding) ((binding)->bNameLength)
+#define EA_BINDING_VALUE_LENGTH(binding) ((binding)->usValueLength)
+#define EA_BINDING_NAME(binding) ((binding)->pszName)
+#define EA_BINDING_VALUE(binding) ((binding)->pszValue)
+
+#define EA_LIST_BINDING(list) ((list)->peab)
+#define EA_LIST_NEXT(list) ((list)->next)
+
+// forward declarations to helper funcs at bottom
+static PEALIST      ReadEAList(ULONG, PVOID);
+static EABINDING *  ReadEAByIndex(ULONG, PVOID, ULONG);
+static EABINDING *  ReadEAByName(ULONG, PVOID, PSZ);
+static PDENA2       ReadDenaByIndex(ULONG, PVOID, ULONG);
+static PEABINDING   GetEAValue(ULONG, PVOID, PDENA2);
+static void         SetupQueryEAInfo(PDENA2, PEAOP2);
+static PEABINDING   ConvertFeal2Binding(PFEA2LIST);
+static APIRET       WriteEAList(ULONG, PVOID, PEALIST);
+static APIRET       WriteEA(ULONG, PVOID, PEABINDING);
+static PFEA2LIST    ConvertBinding2Feal(PEABINDING);
+
+/*
+ *@@ eaFreeBinding:
+ *      deallocate EA binding memory that was generated
+ *      by the ea...Read... procedures below. These procs
+ *      assume that "malloc" was used for allocation and
+ *      that the "pszName" and "pszValue" fields of each binding
+ *      were also allocated using "malloc". "pszValue" may also
+ *      be NULL.
+ *
+ *@@changed V0.9.0 [umoeller]: added check for (binding != NULL)
+ */
+
+void eaFreeBinding(PEABINDING peab)
+{
+    if (peab)
+    {
+        free(EA_BINDING_NAME(peab));
+        if ((EA_BINDING_VALUE(peab)) != 0)
+            free(EA_BINDING_VALUE(peab));
+        free(peab);
+    }
+}
+
+/*
+ *@@ eaFreeList:
+ *      like eaFreeBinding, but for an EA binding list.
+ *      This calls eaFreeBinding for each list item.
+ *
+ *@@changed V0.9.0 [umoeller]: added check for (list != NULL)
+ */
+
+void eaFreeList(PEALIST list)
+{
+    if (list)
+    {
+        while (list != 0)
+        {
+            PEALIST next = (EA_LIST_NEXT (list));
+            eaFreeBinding(EA_LIST_BINDING (list));
+            free(list);
+            list = next;
+        }
+    }
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Read-EA functions                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * All of the following functions come in two flavors:
+ *
+ *      eaHFile*    operate on an open file handle.
+ *
+ *      eaPath*     operate on any file specified by its
+ *                  filename, which may be fully qualified.
+ */
+
+/*
+ *@@ eaPathQueryTotalSize:
+ *      returns the total size of all EAs for a given file.
+ *      This does not use the other EA functions, but
+ *      accesses the EAs directly, so this is a lot quicker
+ *      if you only want the total EA size without accessing
+ *      the EA data.
+ */
+
+ULONG eaPathQueryTotalSize(PSZ pszPath)
+{
+    APIRET  arc;
+    ULONG   ulTotalEASize = 0;
+    FILEFINDBUF4   ffb4;
+
+    _Pmpf(("eaPathQueryTotalSize %s", pszPath));
+
+    arc = DosQueryPathInfo(pszPath,
+                           FIL_QUERYEASIZE,
+                           &ffb4,
+                           sizeof(FILEFINDBUF4));
+
+    if (arc == NO_ERROR)
+    {
+        // CHAR szFile[CCHMAXPATH];
+        // PBYTE pbBuffer = malloc(ffb4.cbList);
+        BYTE abBuf[2000];
+        LONG lCount = 0;
+        PDENA2 pdena2;
+
+        lCount = -1;
+
+        arc = DosEnumAttribute(ENUMEA_REFTYPE_PATH,
+                               pszPath,
+                               1,
+                               abBuf,
+                               sizeof(abBuf),
+                               (PULONG)&lCount,
+                               ENUMEA_LEVEL_NO_VALUE);
+            // ulCount now contains the EA count
+
+        pdena2 = (PDENA2)abBuf;
+
+        _Pmpf(("  %s: arc = %d, count = %d", pszPath, arc, lCount));
+
+        if (lCount > 0)
+        {
+            ulTotalEASize = pdena2->cbName + 8;
+
+            while (lCount > 0)
+            {
+                ulTotalEASize += (pdena2->cbValue + sizeof(DENA2));
+                lCount--;
+                pdena2 = (PDENA2) (((PBYTE) pdena2) +
+                              pdena2->oNextEntryOffset);
+
+            }
+        }
+    }
+
+    _Pmpf(("    %s: total %d", pszPath, ulTotalEASize));
+
+    return (ulTotalEASize);
+}
+
+/*
+ *@@ eaPathReadAll:
+ *      reads all of the extended attributes into an EALIST.
+ *      Returns NULL if no EAs were found.
+ *      The returned list should be freed using eaFreeList.
+ */
+
+PEALIST eaPathReadAll(PSZ path)
+{
+    return (ReadEAList(ENUMEA_REFTYPE_PATH, path));
+}
+
+/*
+ *@@ eaHFileReadAll:
+ *      like eaPathReadAll, but for an open file handle.
+ */
+
+PEALIST eaHFileReadAll(HFILE hfile)
+{
+    return (ReadEAList(ENUMEA_REFTYPE_FHANDLE, (&hfile)));
+}
+
+/*
+ *@@ eaPathReadOneByIndex:
+ *      returns one EA specified by a given index, counting
+ *      from 1. Returns NULL if the specified index was not
+ *      found, either because the file has no EAs at all or
+ *      the index is too large.
+ *      The returned binding should be freed using eaFreeBinding.
+ */
+
+PEABINDING eaPathReadOneByIndex(PSZ path, ULONG index)
+{
+    return (ReadEAByIndex(ENUMEA_REFTYPE_PATH, path, index));
+}
+
+/*
+ *@@ eaHFileReadOneByIndex:
+ *      like eaPathReadOneByIndex, but for an open file handle.
+ */
+
+PEABINDING eaHFileReadOneByIndex(HFILE hfile, ULONG index)
+{
+    return (ReadEAByIndex(ENUMEA_REFTYPE_FHANDLE, (&hfile), index));
+}
+
+/*
+ *@@ eaPathReadOneByName:
+ *      returns one EA specified by the given EA name (e.g.
+ *      ".LONGNAME"). Returns NULL if not found.
+ *      The returned binding should be freed using eaFreeBinding.
+ */
+
+PEABINDING eaPathReadOneByName(PSZ path, PSZ name)
+{
+    return (ReadEAByName(ENUMEA_REFTYPE_PATH, path, name));
+}
+
+/*
+ *@@ eaHFileReadOneByName:
+ *      like eaPathReadOneByName, but for an open file handle.
+ */
+
+PEABINDING eaHFileReadOneByName(HFILE hfile, PSZ name)
+{
+    return (ReadEAByName(ENUMEA_REFTYPE_FHANDLE, (&hfile), name));
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Write-EA functions                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ eaPathWriteAll:
+ *      writes a list of EAs to a given file. These EAs
+ *      are added to possibly existing EAs on the file;
+ *      existing EAs will be overwritten.
+ *
+ *      A given EA is deleted if its EABINDING.usValueLength
+ *      field is 0; only in that case, the EABINDING.value
+ *      field may also be NULL.
+ */
+
+void eaPathWriteAll(PSZ path, PEALIST list)
+{
+    WriteEAList(ENUMEA_REFTYPE_PATH, path, list);
+}
+
+/*
+ *@@ eaHFileWriteAll:
+ *      like eaPathWriteAll, but for an open file handle.
+ */
+
+void eaHFileWriteAll(HFILE hfile, PEALIST list)
+{
+    WriteEAList(ENUMEA_REFTYPE_FHANDLE, (&hfile), list);
+}
+
+/*
+ *@@ eaPathWriteOne:
+ *      adds one EA to a given file. If the EA exists
+ *      alredy, it is overwritten.
+ *
+ *      A given EA is deleted if its EABINDING.usValueLength
+ *      field is 0; only in that case, the EABINDING.pszValue
+ *      field may also be NULL.
+ *
+ *      To delete an EA, you may also use eaPathDeleteOne.
+ */
+
+void eaPathWriteOne(PSZ path, PEABINDING peab)
+{
+    WriteEA(ENUMEA_REFTYPE_PATH, path, peab);
+}
+
+/*
+ *@@ eaHFileWriteOne:
+ *      like eaPathWriteOne, but for an open file handle.
+ */
+
+void eaHFileWriteOne(HFILE hfile, PEABINDING peab)
+{
+    WriteEA(ENUMEA_REFTYPE_FHANDLE, (&hfile), peab);
+}
+
+/*
+ *@@ eaPathDeleteOne:
+ *      this deletes one EA by constructing a temporary
+ *      empty EABINDING for pszEAName and calling
+ *      eaPathWriteOne.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+void eaPathDeleteOne(PSZ path, PSZ pszEAName)
+{
+    EABINDING eab;
+    eab.bFlags = 0;
+    eab.bNameLength = strlen(pszEAName);
+    eab.pszName = pszEAName;
+    eab.usValueLength = 0;
+    eab.pszValue = 0;
+    eaPathWriteOne(path, &eab);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Extended attributes\Translation helpers
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   Translation funcs                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ eaQueryEAType:
+ *      this returns the type of the EA stored
+ *      in the given EA binding.
+ *      See CPREF for the EAT_* codes which are
+ *      returned here.
+ *      Returns 0 upon errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+USHORT eaQueryEAType(PEABINDING peab)
+{
+    USHORT usReturn = 0;
+    if (peab)
+        if (peab->pszValue)
+            // first USHORT always has EAT_* flag
+            usReturn = *((PUSHORT)(peab->pszValue));
+    return (usReturn);
+}
+
+/*
+ *@@ eaCreatePSZFromBinding:
+ *      this returns a new PSZ for the given EABINDING,
+ *      if this is a string EA (EAT_ASCII).
+ *      This PSZ should be free()'d after use.
+ *      Otherwise, NULL is returned.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-03-12) [umoeller]: added checks for NULl strings
+ */
+
+PSZ eaCreatePSZFromBinding(PEABINDING peab)
+{
+    PSZ pszReturn = NULL;
+
+    if (peab)
+    {
+        if (eaQueryEAType(peab) == EAT_ASCII)
+        {
+            // OK: next USHORT has length of string
+            PUSHORT pusLength = (PUSHORT)(peab->pszValue + 2);
+            if (*pusLength)
+            {
+                pszReturn = (PSZ)malloc(*pusLength + 1);
+                memcpy(pszReturn, peab->pszValue + 4, *pusLength);
+                // add null terminator
+                *(pszReturn + (*pusLength)) = 0;
+            }
+        }
+    }
+
+    return (pszReturn);
+}
+
+/*
+ *@@ eaCreateBindingFromPSZ:
+ *      reverse to eaCreatePSZFromBinding, this creates
+ *      a new EABINDING from the given PSZ, which
+ *      can be used with the write-EA functions.
+ *      This EA is of type EAT_ASCII and will be
+ *      made non-critical (peab->bFlags = 0).
+ *
+ *      If the given string is NULL or empty,
+ *      a binding is created anyway; however, usValueLength
+ *      and pszValue will be set to NULL so that the string
+ *      can be deleted.
+ *
+ *      Returns NULL only upon malloc() errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (2000-01-22) [umoeller]: fixed null-string behavior
+ *@@changed V0.9.2 (2000-02-26) [umoeller]: fixed null-string behaviour AGAIN
+ */
+
+PEABINDING eaCreateBindingFromPSZ(PSZ pszEAName,     // in: EA name (e.g. ".LONGNAME")
+                                  PSZ pszString)     // in: string for EAT_ASCII EA
+{
+    PEABINDING peab = (PEABINDING)malloc(sizeof(EABINDING));
+    if (peab)
+    {
+        SHORT cbString = 0;
+        if (pszString)
+            cbString = strlen(pszString);
+
+        peab->bFlags = 0;
+        peab->bNameLength = strlen(pszEAName);
+        peab->pszName = strdup(pszEAName);
+
+        if (cbString)
+        {
+            // non-null string:
+            peab->usValueLength = cbString + 4;
+            peab->pszValue = (PSZ)malloc(peab->usValueLength);
+            if (peab->pszValue)
+            {
+                // set first USHORT to EAT_ASCII
+                *((PUSHORT)(peab->pszValue)) = EAT_ASCII;
+                // set second USHORT to length of string
+                *((PUSHORT)(peab->pszValue + 2)) = cbString;
+                // copy string to byte 4 (no null-terminator)
+                memcpy(peab->pszValue + 4, pszString, cbString);
+            }
+            else
+            {
+                // malloc error:
+                if (peab->pszName)
+                    free(peab->pszName);
+                free(peab);
+                peab = NULL;
+            }
+        }
+        else
+        {
+            // null string:
+            peab->usValueLength = 0;
+            peab->pszValue = NULL;
+        }
+    }
+
+    return (peab);
+}
+
+/*
+ *@@ eaQueryMVCount:
+ *      this returns the number of subitems in a
+ *      multi-value EA. This works for both EAT_MVMT
+ *      and EAT_MVST (multi-type and single-type) EAs.
+ *      Returns 0 upon errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-03-12) [umoeller]: added more error checks
+ */
+
+USHORT eaQueryMVCount(PEABINDING peab,      // in: EA binding to examine (must be EAT_MVMT or EAT_MVST)
+                      PUSHORT pusCodepage,  // out: codepage found in binding (ptr can be NULL)
+                      PUSHORT pusEAType)    // out: either EAT_MVST or EAT_MVMT (ptr can be NULL)
+{
+    USHORT usReturn = 0;
+
+    USHORT usEAType = eaQueryEAType(peab);
+
+    if (pusEAType)
+        *pusEAType = usEAType;
+
+    if (usEAType == EAT_MVST)
+    {
+        // multi-value single-type:
+        // pszValue is as follows (all USHORTs)
+        // EAT_MVST usCodepage usCount usDataType data ....
+
+        // store codepage
+        if (pusCodepage)
+            *pusCodepage = *((PUSHORT)(peab->pszValue + 2));
+        // find count of entries
+        usReturn = *((PUSHORT)(peab->pszValue + 4));
+    } // end if (*((PUSHORT)(peab->pszValue)) == EAT_MVST)
+    else if (usEAType == EAT_MVMT)
+    {
+        // multi-value multi-type:
+        // pszValue is as follows (all USHORTs)
+        // EAT_MVMT usCodepage usCount (usDataType data... )...
+
+        // store codepage
+        if (pusCodepage)
+            *pusCodepage = *((PUSHORT)(peab->pszValue + 2));
+        // find count of entries
+        usReturn = *((PUSHORT)(peab->pszValue + 4));
+    }
+
+    return (usReturn);
+}
+
+/*
+ *@@ eaQueryMVItem:
+ *      this returns a pointer to the beginning
+ *      of data of a subitem in a multi-value
+ *      (EAT_MVST or EAT_MVMT) EA.
+ *
+ *      Note that this _only_ works if the data
+ *      stored in the multi-value fields has
+ *      length-data right after the EAT_* fields.
+ *      This is true for EAT_ASCII, for example.
+ *
+ *      <B>Example:</B> If the EA value in the
+ *      binding is like this:
+ +          EAT_MVMT  codp  count [DataType   Data] ...
+ +          EAT_MVMT  0000  0002  EAT_ASCII   000A Hello John
+ +                                EAT_BINARY  0003 0x12 0x21 0x34
+ *      calling this function with usindex==1 would return
+ *      a pointer to the 0x12 byte in the EA data, set *pusCodepage
+ *      to 0, and set *pusDataLength to 3.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-03-12) [umoeller]: added more error checks
+ */
+
+PSZ eaQueryMVItem(PEABINDING peab,            // in: binding to examing
+                  USHORT usIndex,             // in: item to search (starting at 0)
+                  PUSHORT pusCodepage,        // out: codepage found in binding (ptr can be NULL)
+                  PUSHORT pusEAType,          // out: EAT_* data type of data that the return value points to (ptr can be NULL)
+                  PUSHORT pusDataLength)      // out: length of data that the return value points to (ptr can be NULL)
+{
+    PSZ pszReturn = NULL;
+
+    USHORT usEAType = eaQueryEAType(peab);
+
+    if (usEAType == EAT_MVST)
+    {
+        // multi-value single-type:
+        // pszValue is as follows (all USHORTs)
+        // EAT_MVST usCodepage usCount usDataType data ....
+        USHORT usCount,
+               usDataType;
+        PUSHORT pusLengthThis;
+        PSZ    pData;
+        // store codepage
+        if (pusCodepage)
+            *pusCodepage = *((PUSHORT)(peab->pszValue + 2));
+        // find count of entries
+        usCount = *((PUSHORT)(peab->pszValue + 4));
+
+        if (usIndex < usCount)
+        {
+            // find data type
+            usDataType = *((PUSHORT)(peab->pszValue + 6));
+            if (    (usDataType == EAT_ASCII)
+                 || (usDataType == EAT_BINARY)
+               )
+            {
+                USHORT us = 0;
+                // find beginning of data (after length word)
+                pData = peab->pszValue + 10;
+
+                while (us < usIndex)
+                {
+                    pusLengthThis = (PUSHORT)(pData - 2);
+                    pData += *pusLengthThis + 2;
+                    us++;
+                }
+
+                // pData now points to the actual data;
+                // pData - 2 has the length of the subvalue
+
+                // return values
+                if (pusEAType)
+                    *pusEAType = usDataType; // same for all MVST subvalues
+                if (pusDataLength)
+                    *pusDataLength = *((PUSHORT)(pData - 2));
+                pszReturn = pData;
+            }
+        }
+    } // end if (usEAType == EAT_MVST)
+    else if (usEAType == EAT_MVMT)
+    {
+        // multi-value multi-type:
+        // pszValue is as follows (all USHORTs)
+        // EAT_MVMT usCodepage usCount (usDataType data... )...
+        USHORT usCount;
+               // usDataType;
+        PUSHORT pusLengthThis;
+        PSZ    pData;
+        // store codepage
+        if (pusCodepage)
+            *pusCodepage = *((PUSHORT)(peab->pszValue + 2));
+        // find count of entries
+        usCount = *((PUSHORT)(peab->pszValue + 4));
+
+        if (usIndex < usCount)
+        {
+            USHORT us = 0;
+            pData = peab->pszValue + 6;
+
+            while (us < usIndex)
+            {
+                PUSHORT pusDataType = (PUSHORT)(pData);
+
+                if (    (*pusDataType == EAT_ASCII)
+                     || (*pusDataType == EAT_BINARY)
+                   )
+                {
+                    pusLengthThis = (PUSHORT)(pData + 2);
+                    pData += *pusLengthThis + 4;
+                    us++;
+                }
+                else
+                {
+                    pData = 0;
+                    break;
+                }
+            }
+
+            // pData now points to the usDataType field
+            // of the subvalue;
+            // pData + 2 is the length of the subvalue;
+            // pData + 4 is the actual data
+
+            // return values
+            if (pData)
+            {
+                if (pusEAType)
+                    *pusEAType = *((PUSHORT)(pData));  // different for each MVMT item
+                if (pusDataLength)
+                    *pusDataLength = *((PUSHORT)(pData + 2));
+                pszReturn = pData + 4;
+            }
+        }
+    } // end if (usEAType == EAT_MVMT)
+
+    return (pszReturn);
+}
+
+/*
+ *@@ eaCreatePSZFromMVBinding:
+ *      this returns a new PSZ for the given EABINDING,
+ *      if this is a multi-value, multi-type EA (EAT_MVMT),
+ *      all of whose subitems are of EAT_ASCII though.
+ *      Note that this does _not_ work for EAT_MVST EAs.
+ *
+ *      This format is used by the .COMMENTS and .KEYPHRASES
+ *      system EAs, for example.
+ *
+ *      The different subitems will be separated by what
+ *      you specify in pszSeparator. You can, for example,
+ *      specify \n\r\0 to have CR-LF separators.
+ *
+ *      This PSZ should be free()'d after use.
+ *      On errors, NULL is returned.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-03-12) [umoeller]: added more error checks
+ */
+
+PSZ eaCreatePSZFromMVBinding(PEABINDING peab,       // in: EAT_MVMT binding
+                             PSZ     pszSeparator,  // in: null-terminated string used as separator
+                             PUSHORT pusCodepage)   // out: codepage found in binding (ptr can be NULL)
+{
+    PSZ     pszTotal = NULL;     // this will hold the whole string
+
+    if (peab)
+    {
+        USHORT  usEAType = 0;
+
+        USHORT usMVCount = eaQueryMVCount(peab,
+                                          pusCodepage,
+                                          &usEAType);
+        if (    (usMVCount)
+                    // items found?
+             && (usEAType == EAT_MVMT)
+                    // rule out EAT_MVST, which we cannot handle here
+           )
+        {
+            // EAT_MVMT items found:
+            // go thru all of them
+            USHORT  us = 0;
+            USHORT  cbComment = 0;
+            USHORT  cbSeparator = strlen(pszSeparator);
+            while (us < usMVCount)
+            {
+                USHORT  usEATypeThis = 0;
+                USHORT  usEALenThis = 0;
+                PSZ pszSubItem = eaQueryMVItem(peab,
+                                               us,    // index
+                                               NULL,  // codepage
+                                               &usEATypeThis,
+                                               &usEALenThis);
+                if (!pszSubItem)
+                    break;
+
+                if (usEATypeThis == EAT_ASCII)
+                {
+                    if (usEALenThis)
+                    {
+                        PSZ pszTemp = pszTotal;
+                        PSZ pTarget;
+                        cbComment += usEALenThis + cbSeparator;
+                        pszTotal = (PSZ)malloc(cbComment);
+                        if (pszTemp)
+                        {
+                            // not first loop: append EA value
+                            USHORT cbCommentOld = strlen(pszTemp);
+                            // copy previous compiled string
+                            strcpy(pszTotal, pszTemp);
+                            // append separator
+                            memcpy(pszTotal + cbCommentOld,
+                                   pszSeparator,
+                                   cbSeparator);
+                            // copy the rest after the separator (below)
+                            pTarget = pszTotal + cbCommentOld + cbSeparator;
+                            free(pszTemp);
+                        }
+                        else
+                            // first loop: copy to beginning
+                            pTarget = pszTotal;
+
+                        // copy EA value
+                        memcpy(pTarget, pszSubItem, usEALenThis);
+                        // append null terminator
+                        *(pTarget + usEALenThis) = 0;
+                    }
+                } // if (usEATypeThis == EAT_ASCII)
+                us++;
+            } // end while (us < usMVCount)
+        } // end if (usMVCount)
+    }
+
+    return (pszTotal);
+}
+
+/*
+ *@@ eaCreateMVBindingFromPSZ:
+ *      reverse to eaCreatePSZFromMVBinding, this
+ *      parses pszInput and creates an EAT_MVMT
+ *      EABINDING, all of whose subvalues are of
+ *      EAT_ASCII. The EABINDING is non-critical
+ *      (peab->bFlags = 0).
+ *
+ *      pszSeparator is used in the analysis of pszInput
+ *      as the separator string (e.g. "\r\n").
+ *
+ *      Returns NULL if the given string is NULL.
+ *
+ *      <B>Example:</B>
+ +          PSZ pszInput = "Line 1\r\nLine2\r\nLine3\r\n";
+ +          PEABINDING peab = eaCreateMVBindingFromPSZ(".KEYPHRASES",
+ +                                                     pszInput,
+ +                                                     "\r\n",
+ +                                                     0);      // codepage
+ *      will create an EABINDING with the following pszValue:
+ +          EAT_MVMT codp usCount (usDataType usLen data...)
+ +          EAT_MVMT   0     3    EAT_ASCII     6   Line 1
+ +                                EAT_ASCII     5   Line2
+ +                                EAT_ASCII     5   Line3
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PEABINDING eaCreateMVBindingFromPSZ(PSZ pszEAName,      // in: EA name (e.g. ".KEYPHRASES")
+                                    PSZ pszInput,       // in: string to parse
+                                    PSZ pszSeparator,   // in: separator used in pszInput
+                                    USHORT usCodepage)  // in: codepage to set in EAT_MVMT
+{
+    PEABINDING peab;
+    if (pszInput)
+    {
+        peab = (PEABINDING)malloc(sizeof(EABINDING));
+        if (peab)
+        {
+            PSZ     p = pszInput,
+                    pSource,
+                    pTarget;
+            USHORT  cbInput = strlen(pszInput),
+                    cbSep = strlen(pszSeparator),
+                    usSepCount = 0,
+                    cbToAlloc = 0,
+                    cbThis,
+                    us;
+
+            peab->bFlags = 0;
+            peab->bNameLength = strlen(pszEAName);
+            peab->pszName = strdup(pszEAName);
+
+            // now count the number of pszSeparators in pszInput
+            while ((p = strstr(p, pszSeparator)))
+            {
+                usSepCount++;
+                p += cbSep;
+            }
+            // usSepCount now has the separator count; that means
+            // we have (usSepCount + 1) data fields
+
+            // allocate the memory we need for the total EA:
+            // EAT_MVMT usCodepage usCount (usDataType data... )...
+
+            cbToAlloc =     6       // six fixed bytes for (EAT_MVMT usCodepage usCount)
+                         +  cbInput - (usSepCount * cbSep)
+                                    // length of input string minus length of all separators
+                         +  ((usSepCount + 1) * 4);
+                                    // for each data field (of which we'll have (usSepCount + 1)),
+                                    // we need an extra four bytes for EAT_ASCII and the length
+            peab->usValueLength = cbToAlloc;
+            peab->pszValue = (PSZ)malloc(cbToAlloc + 1);
+
+            // now initialize the first three fields:
+            *((PUSHORT)(peab->pszValue)) = EAT_MVMT;
+            *((PUSHORT)(peab->pszValue + 2)) = usCodepage;
+            *((PUSHORT)(peab->pszValue + 4)) = (usSepCount + 1);
+
+            // set pointer to first field
+            pSource = pszInput;
+            pTarget = peab->pszValue + 6;
+
+            // now go thru all fields except the last
+            for (us = 0;
+                 us < usSepCount;       // exclude the last field
+                 us++)
+            {
+                // find the next separator
+                PSZ     pNextSep = strstr(pSource, pszSeparator);
+                // calculate the length of the substring
+                cbThis = pNextSep - pSource;
+                // set data type in field
+                *((PUSHORT)(pTarget)) = EAT_ASCII;
+                // set length of this field
+                *((PUSHORT)(pTarget + 2)) = cbThis;
+                // copy data
+                memcpy(pTarget + 4, pSource, cbThis);
+                // advance source to the next (after substring and separator)
+                pSource += cbThis + cbSep;
+                // advance target to the next (after substring and (EAT_ASCII usLen)
+                pTarget += cbThis + 4;
+            }
+
+            // now handle the last field
+            cbThis = strlen(pSource);
+            *((PUSHORT)(pTarget)) = EAT_ASCII;
+            *((PUSHORT)(pTarget + 2)) = cbThis;
+            memcpy(pTarget + 4, pSource, cbThis);
+        } // end if (peab)
+    } // end if (pszInput)
+
+    return (peab);
+}
+
+/********************************************************************
+ *                                                                  *
+ *   EA helper funcs                                                *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *  These get called by the functions above.
+ *
+ */
+
+/*
+ *@@ ReadEAList:
+ *
+ */
+
+static PEALIST ReadEAList(ULONG type, // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                          PVOID pfile)
+{
+    ULONG index = 1;
+    PEALIST head = 0;
+    PEALIST tail = 0;
+
+    while (1)
+    {
+        PEABINDING binding = (ReadEAByIndex(type, pfile, index));
+        if (binding == 0)
+            break;
+        {
+            PEALIST list = (PEALIST)malloc(sizeof(EALIST));
+            (EA_LIST_BINDING (list)) = binding;
+            (EA_LIST_NEXT (list)) = 0;
+            if (head == 0)
+                head = list;
+            else
+                (EA_LIST_NEXT (tail)) = list;
+            tail = list;
+        }
+        index += 1;
+    }
+    return (head);
+}
+
+/*
+ *@@ ReadEAByIndex:
+ *
+ */
+
+static PEABINDING ReadEAByIndex(ULONG type, // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                                PVOID pfile,
+                                ULONG index)
+{
+    PDENA2 dena = (ReadDenaByIndex(type, pfile, index));
+    return ((dena == 0)
+                    ? 0
+                    : (GetEAValue(type, pfile, dena)));
+}
+
+/*
+ *@@ ReadEAByName:
+ *
+ */
+
+static PEABINDING ReadEAByName(ULONG type, // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                               PVOID pfile,
+                               PSZ name)
+{
+    ULONG index = 1;
+    while (1)
+    {
+        PDENA2 dena = ReadDenaByIndex(type, pfile, index);
+        if (dena == 0)
+            return (NULL);
+        if ((strcmp(name, (dena->szName))) == 0)
+            return (GetEAValue(type, pfile, dena));
+        free(dena);
+        index += 1;
+    }
+}
+
+/*
+ *@@ ReadDenaByIndex:
+ *      fills a DENA2 structure by calling DosEnumAttribute.
+ *
+ *      DENA2 is typedef'd to FEA2 in the OS/2 headers.
+ *      FEA2 defines the format for setting the full extended
+ *      attributes in the file.
+ *
+ +         typedef struct _FEA2 {
+ +           ULONG      oNextEntryOffset;  //  Offset to next entry.
+ +           BYTE       fEA;               //  Extended attributes flag.
+ +           BYTE       cbName;            //  Length of szName, not including NULL.
+ +           USHORT     cbValue;           //  Value length.
+ +           CHAR       szName[1];         //  Extended attribute name.
+ +         } FEA2;
+ +         typedef FEA2 *PFEA2;
+ *
+ *      Returns 0 on errors.
+ *
+ *@@changed V0.9.2 (2000-03-12) [umoeller]: added more error checking
+ *@@changed V0.9.4 (2000-08-03) [umoeller]: added even more error checking; this fixed problems with swapper.dat and such
+ */
+
+static PDENA2 ReadDenaByIndex(ULONG type, // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                              PVOID pfile, // in: file handle or name
+                              ULONG index) // in: EA index (>= 1)
+{
+    ULONG count = 1;
+    PDENA2 dena = (PDENA2)malloc(500); // 500 is magic -- IBM doesn't explain.
+    APIRET arc = DosEnumAttribute(type,
+                                  pfile,
+                                  index,
+                                  dena,
+                                  500,
+                                  (&count),
+                                  ENUMEA_LEVEL_NO_VALUE);
+    if (   (arc != NO_ERROR)
+        || (count == 0)     // check error code?
+       )
+    {
+        free(dena);
+        return (0);
+    }
+    else
+        return (dena);
+}
+
+/*
+ *@@ GetEAValue:
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: now returning NULL upon errors
+ */
+
+static PEABINDING GetEAValue(ULONG type,  // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                             PVOID pfile, // in: file handle or path
+                             PDENA2 dena) // in: buffer allocated by ReadDenaByIndex
+{
+    ULONG level = FIL_QUERYEASFROMLIST;
+    EAOP2 eaop;
+    ULONG size = (sizeof(eaop));
+    APIRET arc = NO_ERROR;
+    SetupQueryEAInfo(dena, (&eaop));
+            // this freeds dena
+    if (type == ENUMEA_REFTYPE_FHANDLE)
+        arc = DosQueryFileInfo((* ((PHFILE) pfile)), level, (&eaop), size);
+    else
+        arc = DosQueryPathInfo((PSZ)pfile, level, (&eaop), size);
+                    // changed V0.9.0 (99-11-28) [umoeller]; this now works with C
+    free(eaop.fpGEA2List);
+    if (arc == NO_ERROR)
+        return (ConvertFeal2Binding(eaop.fpFEA2List));
+    else
+        return (NULL);
+}
+
+/*
+ *@@ SetupQueryEAInfo:
+ *      prepares a EAOP2 structure for querying EAs.
+ *      This also frees dena, since it's no longer used.
+ */
+
+static void SetupQueryEAInfo(PDENA2 dena,  // in: buffer allocated by ReadDenaByIndex
+                             PEAOP2 eaop)  // out: EAOP2 to fill
+{
+    unsigned int geal_size = ((sizeof (GEA2LIST)) + (dena->cbName));
+    unsigned int feal_size
+                 = ((sizeof (FEA2LIST)) + (dena->cbName) + (dena->cbValue));
+    (eaop->fpGEA2List) = (GEA2LIST*)(malloc(geal_size));
+    ((eaop->fpGEA2List)->cbList) = geal_size;
+    (eaop->fpFEA2List) = (FEA2LIST*)(malloc(feal_size));
+    ((eaop->fpFEA2List)->cbList) = feal_size;
+    (eaop->oError) = 0;
+    {
+        PGEA2 gea = (&(((eaop->fpGEA2List)->list) [0]));
+        (gea->oNextEntryOffset) = 0;
+        (gea->cbName) = (dena->cbName);
+        strcpy ((gea->szName), (dena->szName));
+    }
+    free(dena);
+}
+
+/*
+ *@@ ConvertFeal2Binding:
+ *      creates a new EABINDING from the given FEA2LIST
+ *      and frees feal. Returns NULL upon errors.
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: now returning NULL upon errors
+ */
+
+static PEABINDING ConvertFeal2Binding(PFEA2LIST feal)
+{
+    PFEA2 fea = (&((feal->list) [0]));
+    PEABINDING binding = (PEABINDING)(malloc(sizeof (EABINDING)));
+    if (binding)
+    {
+        (EA_BINDING_FLAGS (binding)) = (fea->fEA);
+        (EA_BINDING_NAME_LENGTH (binding)) = (fea->cbName);
+        (EA_BINDING_VALUE_LENGTH (binding)) = (fea->cbValue);
+        (EA_BINDING_NAME (binding)) = (PSZ)(malloc((fea->cbName) + 1));
+        strcpy ((EA_BINDING_NAME (binding)), (fea->szName));
+        (EA_BINDING_VALUE (binding)) = (PSZ)(malloc(fea->cbValue));
+        memcpy ((EA_BINDING_VALUE (binding)),
+                (&((fea->szName) [(fea->cbName) + 1])),
+                (fea->cbValue));
+        free(feal);
+    }
+    return (binding);
+}
+
+/*
+ *@@ WriteEAList:
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: now returning APIRET
+ */
+
+static APIRET WriteEAList(ULONG type, // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                          PVOID pfile,
+                          PEALIST list)
+{
+    APIRET arc = NO_ERROR;
+    while (    (list != 0)
+            && (arc == NO_ERROR)
+          )
+    {
+        arc = WriteEA(type, pfile, (EA_LIST_BINDING (list)));
+        list = (EA_LIST_NEXT (list));
+    }
+
+    return (arc);
+}
+
+/*
+ *@@ WriteEA:
+ *
+ *@@changed V0.9.1 (2000-01-30) [umoeller]: now returning APIRET
+ */
+
+static APIRET WriteEA(ULONG type,          // in: ENUMEA_REFTYPE_FHANDLE or  ENUMEA_REFTYPE_PATH
+                      PVOID pfile,
+                      PEABINDING binding)
+{
+    ULONG level = FIL_QUERYEASIZE;
+    EAOP2 eaop;
+    ULONG size = (sizeof (eaop));
+    APIRET arc = NO_ERROR;
+
+    (eaop.fpGEA2List) = 0;
+    (eaop.fpFEA2List) = (ConvertBinding2Feal(binding));
+    (eaop.oError) = 0;
+    if (type == ENUMEA_REFTYPE_FHANDLE)
+        arc = DosSetFileInfo((* ((PHFILE) pfile)), level, (&eaop), size);
+    else
+        arc = DosSetPathInfo((PSZ)pfile, level, (&eaop), size, DSPI_WRTTHRU);
+    free(eaop.fpFEA2List);
+    return (arc);
+}
+
+/*
+ *@@ ConvertBinding2Feal:
+ *
+ */
+
+static PFEA2LIST ConvertBinding2Feal(PEABINDING binding)
+{
+    unsigned int feal_size
+              = ((sizeof (FEA2LIST))
+                 + (EA_BINDING_NAME_LENGTH (binding))
+                 + (EA_BINDING_VALUE_LENGTH (binding)));
+    PFEA2LIST feal = (PFEA2LIST)(malloc(feal_size));
+    PFEA2 fea = (&((feal->list) [0]));
+    (feal->cbList) = feal_size;
+    (fea->oNextEntryOffset) = 0;
+    (fea->fEA) = (EA_BINDING_FLAGS (binding));
+    (fea->cbName) = (EA_BINDING_NAME_LENGTH (binding));
+    (fea->cbValue) = (EA_BINDING_VALUE_LENGTH (binding));
+    strcpy ((fea->szName), (EA_BINDING_NAME (binding)));
+    if ((EA_BINDING_VALUE (binding)) != 0)
+        memcpy ((&((fea->szName) [(fea->cbName) + 1])),
+                (EA_BINDING_VALUE (binding)),
+                (fea->cbValue));
+    return (feal);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Direct plain-string EA handling                                *
+ *                                                                  *
+ ********************************************************************/
+
+#define USE_EAMVMT 1
+
+/*
+ * eahWriteStringEA:
+ *
+ *      (C) Christian Langanke. Moved this here from tmsgfile.c.
+ *
+ *@@added V0.9.3 (2000-05-21) [umoeller]
+ */
+
+APIRET eahWriteStringEA(HFILE hfile,
+                        PSZ pszEaName,
+                        PSZ pszEaValue)
+{
+
+    APIRET          rc = NO_ERROR;
+    PFEA2LIST       pfea2l = NULL;
+
+#ifdef USE_EAMVMT
+    PEAMVMT         peamvmt;
+#else
+    PEASVST         peasvst;
+#endif
+    ULONG           ulEAListLen;
+    ULONG           ulValueLen;
+    EAOP2           eaop2;
+
+    // PSZ             pszValue;
+
+    do
+    {
+        // check parameters
+        if (    (hfile == NULLHANDLE)
+             || (pszEaName == NULL)
+             || (*pszEaName == 0)
+             || (pszEaValue == NULL)
+           )
+        {
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        // write EAs
+        ulValueLen = strlen(pszEaValue) +
+#ifdef USE_EAMVMT
+            sizeof(EAMVMT);
+#else
+            sizeof(EASVST);
+#endif
+
+        ulEAListLen = strlen(pszEaName) +
+                      sizeof(FEA2LIST) +
+                      ulValueLen;
+
+        // get memory for FEA2LIST
+        pfea2l = (FEA2LIST*)malloc(ulEAListLen);
+        if (pfea2l == 0)
+        {
+            rc = ERROR_NOT_ENOUGH_MEMORY;
+            break;
+        }
+
+        // init FEA2LIST
+        eaop2.fpGEA2List = NULL;
+        eaop2.fpFEA2List = pfea2l;
+        memset(pfea2l, 0, ulEAListLen);
+
+        // write timeframe EA
+        pfea2l->cbList = ulEAListLen;
+        pfea2l->list[0].cbName = strlen(pszEaName);
+        strcpy(pfea2l->list[0].szName, pszEaName);
+
+        // delete attribute if value empty
+        if (strlen(pszEaValue) == 0)
+            pfea2l->list[0].cbValue = 0;
+        else
+        {
+            pfea2l->list[0].cbValue = ulValueLen;
+
+#ifdef USE_EAMVMT
+            // multi value multi type
+            peamvmt = (PEAMVMT) NEXTSTR(pfea2l->list[0].szName);
+            peamvmt->usType = EAT_MVMT;
+            peamvmt->usCodepage = 0;
+            peamvmt->usEntries = 1;
+            peamvmt->usEntryType = EAT_ASCII;
+            peamvmt->usEntryLen = strlen(pszEaValue);
+            memcpy(&peamvmt->chEntry[0], pszEaValue, peamvmt->usEntryLen);
+#else
+            // single value single type
+            peasvst = NEXTSTR(pfea2l->list[0].szName);
+            peasvst->usType = EAT_ASCII;
+            peasvst->usEntryLen = strlen(pszEaValue);
+            memcpy(&peasvst->chEntry[0], pszEaValue, peasvst->usEntryLen);
+#endif
+        }
+
+        // set the new EA value
+        rc = DosSetFileInfo(hfile,
+                            FIL_QUERYEASIZE,
+                            &eaop2,
+                            sizeof(eaop2));
+
+    }
+    while (FALSE);
+
+// cleanup
+    if (pfea2l)
+        free(pfea2l);
+    return rc;
+}
+
+/*
+ * eahReadStringEA:
+ *
+ *      (C) Christian Langanke. Moved this here from tmsgfile.c.
+ *
+ *@@added V0.9.3 (2000-05-21) [umoeller]
+ */
+
+APIRET eahReadStringEA(PSZ pszFileName,
+                       PSZ pszEaName,
+                       PSZ pszBuffer,
+                       PULONG pulBuflen)
+{
+
+    APIRET          rc = NO_ERROR;
+    FILESTATUS4     fs4;
+
+    EAOP2           eaop2;
+    PGEA2LIST       pgea2l = NULL;
+    PFEA2LIST       pfea2l = NULL;
+
+    PGEA2           pgea2;
+    PFEA2           pfea2;
+
+    ULONG           ulGea2Len = 0;
+    ULONG           ulFea2Len = 0;
+
+    PEASVST         peasvst;
+    PEAMVMT         peamvmt;
+
+    ULONG           ulRequiredLen;
+
+    do
+    {
+        // check parameters
+        if ((pszFileName == NULL) ||
+            (pszEaName == NULL) ||
+            (*pszEaName == 0) ||
+            (pulBuflen == NULL))
+        {
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        // initialize target buffer
+        if (pszBuffer)
+            memset(pszBuffer, 0, *pulBuflen);
+
+        // get EA size
+        rc = DosQueryPathInfo(pszFileName,
+                              FIL_QUERYEASIZE,
+                              &fs4,
+                              sizeof(fs4));
+        if (rc != NO_ERROR)
+            break;
+
+        // no eas here ?
+        if (fs4.cbList == 0)
+        {
+            pulBuflen = 0;
+            break;
+        }
+
+        // determine required space
+        // - for ulFea2Len use at least 2 * Gea2Len because
+        //   buffer needs at least to be Geal2Len even for an empty
+        //   attribute, otherwise rc == ERROR_BUFFER_OVERFLOW !
+        ulGea2Len = sizeof(GEA2LIST) + strlen(pszEaName);
+        ulFea2Len = 2 * MAX(fs4.cbList, ulGea2Len);
+
+        // get memory for GEA2LIST
+        if ((pgea2l = (GEA2LIST*)malloc(ulGea2Len)) == 0)
+        {
+            rc = ERROR_NOT_ENOUGH_MEMORY;
+            break;
+        }
+        memset(pgea2l, 0, ulGea2Len);
+
+        // get memory for FEA2LIST
+        if ((pfea2l = (FEA2LIST*)malloc(ulFea2Len)) == 0)
+        {
+            rc = ERROR_NOT_ENOUGH_MEMORY;
+            break;
+        }
+        memset(pfea2l, 0, ulFea2Len);
+
+        // init ptrs and do the query
+        memset(&eaop2, 0, sizeof(EAOP2));
+        eaop2.fpGEA2List = pgea2l;
+        eaop2.fpFEA2List = pfea2l;
+        pfea2l->cbList = ulFea2Len;
+        pgea2l->cbList = ulGea2Len;
+
+        pgea2 = &pgea2l->list[0];
+        pfea2 = &pfea2l->list[0];
+
+
+        pgea2->oNextEntryOffset = 0;
+        pgea2->cbName = strlen(pszEaName);
+        strcpy(pgea2->szName, pszEaName);
+
+        rc = DosQueryPathInfo(pszFileName,
+                              FIL_QUERYEASFROMLIST,
+                              &eaop2,
+                              sizeof(eaop2));
+        if (rc != NO_ERROR)
+            break;
+
+        // check first entry only
+        peamvmt = (PEAMVMT) ((PBYTE) pfea2->szName + pfea2->cbName + 1);
+
+        // is it MVMT ? then adress single EA !
+        if (peamvmt->usType == EAT_MVMT)
+        {
+            peasvst = (PEASVST) & peamvmt->usEntryType;
+        }
+        else
+            peasvst = (PEASVST) peamvmt;
+
+
+        // is entry empty ?
+        if (peasvst->usEntryLen == 0)
+        {
+            rc = ERROR_INVALID_EA_NAME;
+            break;
+        }
+
+        // is it ASCII ?
+        if (peasvst->usType != EAT_ASCII)
+        {
+            rc = ERROR_INVALID_DATA;
+            break;
+        }
+
+        // check buffer and hand over value
+        ulRequiredLen = peasvst->usEntryLen + 1;
+        if (*pulBuflen < ulRequiredLen)
+        {
+            *pulBuflen = ulRequiredLen;
+            rc = ERROR_BUFFER_OVERFLOW;
+            break;
+        }
+
+        // hand over len
+        *pulBuflen = ulRequiredLen;
+
+        // hand over value
+        if (pszBuffer)
+            memcpy(pszBuffer, peasvst->chEntry, peasvst->usEntryLen);
+
+    }
+    while (FALSE);
+
+// cleanup
+    if (pgea2l)
+        free(pgea2l);
+    if (pfea2l)
+        free(pfea2l);
+    return rc;
+}
+
+
Index: /trunk/src/helpers/except.c
===================================================================
--- /trunk/src/helpers/except.c	(revision 8)
+++ /trunk/src/helpers/except.c	(revision 8)
@@ -0,0 +1,983 @@
+
+/*
+ *@@sourcefile except.c:
+ *      this file contains powerful exception handlers.
+ *
+ *      Usage: All OS/2 programs, PM or text mode.
+ *
+ *      <B>except.h macros</B>
+ *
+ *      except.h declares a few handy macros to easily install
+ *      exception handling for a section of code.
+ *
+ *      These macros automatically insert code for properly
+ *      registering and deregistering the handlers. You should
+ *      ALWAYS use these macros instead of directly registering
+ *      the handlers to avoid accidentally forgetting to deregister
+ *      them. If you forget to deregister an exception handler, this
+ *      can lead to really strange errors (crashes, hangs) which are
+ *      nearly impossible to debug because the thread's stack space
+ *      might get completely messed up.
+ *
+ *      The general idea of these macros is to define
+ *      TRY / CATCH blocks similar to C++. If an exception
+ *      occurs in the TRY block, execution is transferred
+ *      to the CATCH block. (This works in both C and C++,
+ *      by the way.)
+ *
+ *      In addition, with the TRY statement, you may specify
+ *      an optional "OnKill" function which gets called if
+ *      the thread gets killed while code in the TRY block is
+ *      executed (new with V0.9.0). If you need no such
+ *      function, specify NULL. Details follow.
+ *
+ *      The general usage is like this:
+ *
+ +          int your_protected_func(int ...)
+ +          {
+ +              TRY_LOUD(excptid, OnKillFunc)
+ +              // or: TRY_QUIET(excptid, OnKillFunc)
+ +              // OnKillFunc can be NULL
+ +              {
+ +                  ....        // the stuff in here is protected by
+ +                              // the excHandlerLoud or excHandlerQuiet
+ +                              // exception handler
+ +              }
+ +              CATCH(excptid)
+ +              {
+ +                  ....        // exception occured: react here
+ +              } END_CATCH();  // always needed!
+ +          } // end of your_func
+ *
+ *      TRY_LOUD  is for excHandlerLoud (see below).
+ *      TRY_QUIET is for excHandlerQuiet (see below).
+ *      CATCH / END_CATCH are the same for the two. This
+ *      is where the exception handler jumps to if an
+ *      exception occurs.
+ *      The CATCH block is _required_ even if you do nothing
+ *      in there.
+ *
+ *      "excptid" can be any identifier which is not used in
+ *      your current variable scope, e.g. "excpt1". This
+ *      is used for creating an EXCEPTSTRUCT variable of
+ *      that name on the stack. The "excptid"'s in TRY_* and
+ *      CATCH must match, since this is where the macros
+ *      store the exception handler data.
+ *
+ *      It is possible to nest these handlers though. You
+ *      must use different "excptid"'s only if more than one
+ *      TRY_* block exists in the same variable scope. Avoid
+ *      using gotos to jump between the different CATCH blocks,
+ *      because this might not properly deregister the handlers.
+ *
+ *      Keep in mind that all the code in the TRY_* block is
+ *      protected by the handler, including all functions that
+ *      get called. So if you enclose your main() code in a
+ *      TRY_* block, your entire application is protected.
+ *
+ *      Your "OnKillFunc" must be declared as
+ +          VOID APIENTRY YourOnKillFunc(VOID) {...}.
+ *      This gets called when the thread gets terminated
+ *      while being executed in the TRY block (that is,
+ *      if either the thread gets killed explicitly or
+ *      the entire process ends for some reason). After
+ *      that function got executed, the thread will _always_
+ *      be terminated; the CATCH block will not be executed
+ *      then. You can specify NULL for the "OnKillFunc", if
+ *      you don't need one.
+ *
+ *      To summarize: If an exception occurs in the TRY
+ *      block,
+ *
+ *      a) "OnKillFunc" gets called if the thread got
+ *         killed, and the thread then terminates;
+ *
+ *      b) the CATCH block is executed for other exceptions
+ *         and execution continues.
+ *
+ *      <B>Mutex semaphores</B>
+ *
+ *      Here's how to deal with mutex semaphores: WHENEVER
+ *      you request a mutex semaphore, enclose the block
+ *      with TRY/CATCH in case an error occurs, like this:
+ *
+ +          HMTX hmtx = ...
+ +
+ +          VOID APIENTRY OnKillYourFunc(VOID)
+ +          {
+ +              DosReleaseMutexSem(hmtx);
+ +          }
+ +
+ +          int your_func(int)
+ +          {
+ +              BOOL fSemOwned = FALSE;
+ +              TRY_QUIET(excpt1, OnKillYourFunc) // or TRY_LOUD
+ +              {
+ +                  fSemOwned = (WinRequestMutexSem(hmtx, ...) == NO_ERROR);
+ +                  if (fSemOwned)
+ +                  {       ... // work on your protected data
+ +                  }
+ +              }
+ +              CATCH(excpt1) { } END_CATCH();    // always needed!
+ +
+ +              if (fSemOwned) {
+ +                  // this gets executed always, even if an exception occured
+ +                  DosReleaseMutexSem(hmtx);
+ +                  fSemOwned = FALSE;
+ +              }
+ +          } // end of your_func
+ *
+ *      This way your mutex semaphore gets released even if
+ *      exceptions occur in your code. If you don't do this,
+ *      threads waiting for that semaphore will be blocked
+ *      forever when exceptions occur. As a result, depending
+ *      on the thread, PM will hang, or the application will
+ *      never terminate.
+ *
+ *      <B>Customizing</B>
+ *
+ *      Note: As opposed to versions < 0.9.0, this is now
+ *      completely independent of XWorkplace. This file now
+ *      contains "pure" exception handlers only.
+ *
+ *      However, you can customize these exception handlers by
+ *      calling excRegisterHooks. This is what XWorkplace does now.
+ *      This should be done upon initialization of your application.
+ *      If excRegisterHooks is not called, the following safe
+ *      defaults are used:
+ *          --  the trap log file is TRAP.LOG in the root
+ *              directory of your boot drive.
+ *
+ *      For details on the provided exception handlers, refer
+ *      to excHandlerLoud and excHandlerQuiet.
+ *
+ *      More useful debug information can be found in the "OS/2 Debugging
+ *      Handbook", which is now available in INF format on the IBM
+ *      DevCon site ("http://service2.boulder.ibm.com/devcon/").
+ *      This book shows worked examples of how to unwind a stack dump.
+ *
+ *      This file incorporates code from the following:
+ *      -- Monte Copeland, IBM Boca Ration, Florida, USA (1993)
+ *      -- Roman Stangl, from the Program Commander/2 sources
+ *         (1997-98)
+ *      -- Marc Fiammante, John Currier, Kim Rasmussen,
+ *         Anthony Cruise (EXCEPT3.ZIP package for a generic
+ *         exception handling DLL, available at Hobbes).
+ *
+ *      If not explicitly stated otherwise, the code has been written
+ *      by me, Ulrich Mller.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\except.h"
+ */
+
+/*
+ *      This file Copyright (C) 1992-99 Ulrich Mller,
+ *                                      Monte Copeland,
+ *                                      Roman Stangl,
+ *                                      Kim Rasmussen,
+ *                                      Marc Fiammante,
+ *                                      John Currier,
+ *                                      Anthony Cruise.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSMODULEMGR
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSERRORS
+#include <os2.h>
+
+// C library headers
+#include <stdio.h>              // needed for except.h
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+#include <setjmp.h>             // needed for except.h
+#include <assert.h>             // needed for except.h
+
+#define DONT_REPLACE_MALLOC
+#include "setup.h"                      // code generation and debugging options
+
+// headers in /helpers
+#include "helpers\dosh.h"               // Control Program helper routines
+#include "helpers\except.h"             // exception handling
+#include "helpers\debug.h"              // symbol/debug code analysis
+
+#pragma hdrstop
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+// hooks to be registered using excRegisterHooks
+PFNEXCOPENFILE  G_pfnExcOpenFile = 0;
+PFNEXCHOOK      G_pfnExcHook = 0;
+PFNEXCHOOKERROR G_pfnExcHookError = 0;
+// beep flag for excHandlerLoud
+BOOL            G_fBeepOnException = TRUE;
+
+/*
+ *@@category: Helpers\Control program helpers\Exceptions/debugging
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Exception helper routines                                      *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ excDescribePage:
+ *
+ */
+
+VOID excDescribePage(FILE *file, ULONG ulCheck)
+{
+    APIRET arc;
+    ULONG ulCountPages = 1;
+    ULONG ulFlagsPage = 0;
+    arc = DosQueryMem((PVOID)ulCheck, &ulCountPages, &ulFlagsPage);
+
+    if (arc == NO_ERROR)
+    {
+        fprintf(file, "valid, flags: ");
+        if (ulFlagsPage & PAG_READ)
+            fprintf(file, "read ");
+        if (ulFlagsPage & PAG_WRITE)
+            fprintf(file, "write ");
+        if (ulFlagsPage & PAG_EXECUTE)
+            fprintf(file, "execute ");
+        if (ulFlagsPage & PAG_GUARD)
+            fprintf(file, "guard ");
+        if (ulFlagsPage & PAG_COMMIT)
+            fprintf(file, "committed ");
+        if (ulFlagsPage & PAG_SHARED)
+            fprintf(file, "shared ");
+        if (ulFlagsPage & PAG_FREE)
+            fprintf(file, "free ");
+        if (ulFlagsPage & PAG_BASE)
+            fprintf(file, "base ");
+    }
+    else if (arc == ERROR_INVALID_ADDRESS)
+        fprintf(file, "invalid");
+}
+
+/*
+ *@@ excPrintStackFrame:
+ *      wrapper for dbgPrintStackFrame to format
+ *      output stuff right.
+ *
+ *@@added V0.9.2 (2000-03-10) [umoeller]
+ */
+
+VOID excPrintStackFrame(FILE *file,         // in: output log file
+                        PSZ pszDescription, // in: description for stack frame (should be eight chars)
+                        ULONG ulAddress)    // in: address to debug
+{
+    APIRET  arc = NO_ERROR;
+    HMODULE hmod1 = NULLHANDLE;
+    CHAR    szMod1[2*CCHMAXPATH] = "unknown";
+    ULONG   ulObject = 0,
+            ulOffset = 0;
+    fprintf(file,
+            "    %-8s: %08X ",
+            pszDescription,
+            ulAddress);
+    arc = DosQueryModFromEIP(&hmod1,
+                             &ulObject,
+                             sizeof(szMod1), szMod1,
+                             &ulOffset,
+                             ulAddress);
+
+    if (arc != NO_ERROR)
+    {
+        // error:
+        fprintf(file,
+                " %-8s:%d   Error: DosQueryModFromEIP returned %d\n",
+                szMod1,
+                ulObject,
+                arc);
+    }
+    else
+    {
+        CHAR szFullName[2*CCHMAXPATH];
+
+        fprintf(file,
+                " %-8s:%d   ",
+                szMod1,
+                ulObject);
+
+        DosQueryModuleName(hmod1, sizeof(szFullName), szFullName);
+
+        dbgPrintStackFrame(file,
+                           szFullName,
+                           ulObject,
+                           ulOffset);
+
+        fprintf(file, "\n");
+
+        // make a 'tick' sound to let the user know we're still alive
+        DosBeep(2000, 10);
+    }
+}
+
+/*
+ *@@ excDumpStackFrames:
+ *      called from excExplainException to dump the
+ *      thread's stack frames. This calls excPrintStackFrame
+ *      for each stack frame found.
+ *
+ *@@added V0.9.4 (2000-06-15) [umoeller]
+ */
+
+VOID excDumpStackFrames(FILE *file,                   // in: logfile from fopen()
+                        PTIB ptib,
+                        PCONTEXTRECORD pContextRec)   // in: excpt info
+{
+    PULONG pulStackWord = 0;
+
+    fprintf(file, "\n\nStack frames:\n              Address   Module:Object\n");
+
+    // first the trapping address itself
+    excPrintStackFrame(file,
+                       "CS:EIP  ",
+                       pContextRec->ctx_RegEip);
+
+
+    pulStackWord = (PULONG)pContextRec->ctx_RegEbp;
+    /* if (pContextRec->ctx_RegEbp < pContextRec->ctx_RegEsp)
+        pulStackWord = (PULONG)(pContextRec->ctx_RegEbp & 0xFFFFFFF0);
+    else
+        pulStackWord = (PULONG)(pContextRec->ctx_RegEsp & 0xFFFFFFF0); */
+
+    while (    (pulStackWord != 0)
+            && (pulStackWord < (PULONG)ptib->tib_pstacklimit)
+          )
+    {
+        CHAR szAddress[20];
+
+        if (((ULONG)pulStackWord & 0x00000FFF) == 0x00000000)
+        {
+            // we're on a page boundary: check access
+            ULONG ulCountPages = 0x1000;
+            ULONG ulFlagsPage = 0;
+            APIRET arc = DosQueryMem((void *)pulStackWord,
+                                     &ulCountPages,
+                                     &ulFlagsPage);
+            if (    (arc != NO_ERROR)
+                 || (   (arc == NO_ERROR)
+                      && ( !( ((ulFlagsPage & (PAG_COMMIT|PAG_READ))
+                               == (PAG_COMMIT|PAG_READ)
+                              )
+                            )
+                         )
+                    )
+               )
+            {
+                fprintf(file, "\n    %08X: ", pulStackWord);
+                fprintf(file, "Page inaccessible");
+                pulStackWord += 0x1000;
+                continue; // for
+            }
+        }
+
+        sprintf(szAddress, "%08X",
+                pulStackWord);
+        excPrintStackFrame(file,
+                           szAddress,
+                           *(pulStackWord+1));
+        pulStackWord = (PULONG)*(pulStackWord);
+    } // end while
+}
+
+/*
+ *@@ excExplainException:
+ *      used by the exception handlers below to write
+ *      LOTS of information about the exception into a logfile.
+ *
+ *      This calls excPrintStackFrame for each stack frame.
+ *
+ *      If DEBUG_EXCPT_STACKDUMP is #define'd, this also dumps
+ *      the stack completely, which isn't that useful.
+ *
+ *@@changed V0.9.0 [umoeller]: added support for application hook
+ *@@changed V0.9.0 (99-11-02) [umoeller]: added TID to dump
+ *@@changed V0.9.2 (2000-03-10) [umoeller]: now using excPrintStackFrame
+ *@@changed V0.9.3 (2000-05-03) [umoeller]: fixed crashes
+ */
+
+VOID excExplainException(FILE *file,                   // in: logfile from fopen()
+                         PSZ pszHandlerName,           // in: descriptive string
+                         PEXCEPTIONREPORTRECORD pReportRec, // in: excpt info
+                         PCONTEXTRECORD pContextRec)   // in: excpt info
+{
+    PTIB        ptib = NULL;
+    PPIB        ppib = NULL;
+    HMODULE     hMod1, hMod2;
+    CHAR        szMod1[CCHMAXPATH] = "unknown",
+                szMod2[CCHMAXPATH] = "unknown";
+    ULONG       ulObjNum,
+                ulOffset;
+                /* ulCountPages,
+                ulFlagsPage; */
+    // APIRET      arc;
+    // PULONG      pulStackWord;
+                // pulStackBegin;
+    ULONG       ul;
+
+    ULONG       ulOldPriority = 0x0100; // regular, delta 0
+
+    // raise this thread's priority, because this
+    // might take some time
+    if (DosGetInfoBlocks(&ptib, &ppib) == NO_ERROR)
+        if (ptib)
+            if (ptib->tib_ptib2)
+            {
+                ulOldPriority = ptib->tib_ptib2->tib2_ulpri;
+                DosSetPriority(PRTYS_THREAD,
+                               PRTYC_REGULAR,
+                               PRTYD_MAXIMUM,
+                               0);     // current thread
+            }
+
+    // make some noise
+    if (G_fBeepOnException)
+    {
+        DosBeep( 250, 30);
+        DosBeep( 500, 30);
+        DosBeep(1000, 30);
+        DosBeep(2000, 30);
+        DosBeep(4000, 30);
+        DosBeep(2000, 30);
+        DosBeep(1000, 30);
+        DosBeep( 500, 30);
+        DosBeep( 250, 30);
+    }
+
+    // generic exception info
+    fprintf(file,
+            "\n%s:\n    Exception type: %08X\n    Address:        %08X\n    Params:         ",
+            pszHandlerName,
+            pReportRec->ExceptionNum,
+            pReportRec->ExceptionAddress);
+    for (ul = 0;  ul < pReportRec->cParameters;  ul++)
+    {
+        fprintf(file, "%08X  ",
+                pReportRec->ExceptionInfo[ul]);
+    }
+
+    // now explain the exception in a bit more detail;
+    // depending on the exception, pReportRec->ExceptionInfo
+    // contains some useful data
+    switch (pReportRec->ExceptionNum)
+    {
+        case XCPT_ACCESS_VIOLATION:
+        {
+            fprintf(file, "\nAccess violation: ");
+            if (pReportRec->ExceptionInfo[0] & XCPT_READ_ACCESS)
+                fprintf(file, "Invalid read access from 0x%04X:%08X.\n",
+                        pContextRec->ctx_SegDs, pReportRec->ExceptionInfo[1]);
+            else if (pReportRec->ExceptionInfo[0] & XCPT_WRITE_ACCESS)
+                fprintf(file, "Invalid write access to 0x%04X:%08X.\n",
+                        pContextRec->ctx_SegDs, pReportRec->ExceptionInfo[1]);
+            else if (pReportRec->ExceptionInfo[0] & XCPT_SPACE_ACCESS)
+                fprintf(file, "Invalid space access at 0x%04X.\n",
+                        pReportRec->ExceptionInfo[1]);
+            else if (pReportRec->ExceptionInfo[0] & XCPT_LIMIT_ACCESS)
+                fprintf(file, "Invalid limit access occurred.\n");
+            else if (pReportRec->ExceptionInfo[0] == XCPT_UNKNOWN_ACCESS)
+                fprintf(file, "unknown at 0x%04X:%08X\n",
+                            pContextRec->ctx_SegDs, pReportRec->ExceptionInfo[1]);
+            fprintf(file,
+                    "Explanation: An attempt was made to access a memory object which does\n"
+                    "             not belong to the current process. Most probable causes\n"
+                    "             for this are that an invalid pointer was used, there was\n"
+                    "             confusion with administering memory or error conditions \n"
+                    "             were not properly checked for.\n");
+            break;
+        }
+
+        case XCPT_INTEGER_DIVIDE_BY_ZERO:
+        {
+            fprintf(file, "\nInteger division by zero.\n");
+            fprintf(file,
+                    "Explanation: An attempt was made to divide an integer value by zero,\n"
+                    "             which is not defined.\n");
+            break;
+        }
+
+        case XCPT_ILLEGAL_INSTRUCTION:
+        {
+            fprintf(file, "\nIllegal instruction found.\n");
+            fprintf(file,
+                    "Explanation: An attempt was made to execute an instruction that\n"
+                    "             is not defined on this machine's architecture.\n");
+            break;
+        }
+
+        case XCPT_PRIVILEGED_INSTRUCTION:
+        {
+            fprintf(file, "\nPrivileged instruction found.\n");
+            fprintf(file,
+                    "Explanation: An attempt was made to execute an instruction that\n"
+                    "             is not permitted in the current machine mode or that\n"
+                    "             XFolder had no permission to execute.\n");
+            break;
+        }
+
+        case XCPT_INTEGER_OVERFLOW:
+            fprintf(file, "\nInteger overflow.\n");
+            fprintf(file,
+                    "Explanation: An integer operation generated a carry-out of the most\n"
+                    "             significant bit. This is a sign of an attempt to store\n"
+                    "             a value which does not fit into an integer variable.\n");
+    }
+
+    if (DosGetInfoBlocks(&ptib, &ppib) == NO_ERROR)
+    {
+        /*
+         * process info:
+         *
+         */
+
+        if ((ptib) && (ppib))       // (99-11-01) [umoeller]
+        {
+            if (pContextRec->ContextFlags & CONTEXT_CONTROL)
+            {
+                // get the main module
+                hMod1 = ppib->pib_hmte;
+                DosQueryModuleName(hMod1,
+                                   sizeof(szMod1),
+                                   szMod1);
+
+                // get the trapping module
+                DosQueryModFromEIP(&hMod2,
+                                   &ulObjNum,
+                                   sizeof(szMod2),
+                                   szMod2,
+                                   &ulOffset,
+                                   pContextRec->ctx_RegEip);
+                DosQueryModuleName(hMod2,
+                                   sizeof(szMod2),
+                                   szMod2);
+            }
+
+            fprintf(file,
+                    "\nProcess information:"
+                    "\n    Process ID:      0x%lX"
+                    "\n    Process module:  0x%lX (%s)"
+                    "\n    Trapping module: 0x%lX (%s)"
+                    "\n    Object: %d\n",
+                    ppib->pib_ulpid,
+                    hMod1, szMod1,
+                    hMod2, szMod2,
+                    ulObjNum);
+
+            fprintf(file,
+                    "\nTrapping thread information:"
+                    "\n    Thread ID:       0x%lX (%d)"
+                    "\n    Priority:        0x%lX\n",
+                    ptib->tib_ptib2->tib2_ultid, ptib->tib_ptib2->tib2_ultid,
+                    ulOldPriority);
+        }
+        else
+            fprintf(file, "\nProcess information was not available.");
+
+        /*
+         *  now call the hook, if one has been defined,
+         *  so that the application can write additional
+         *  information to the traplog (V0.9.0)
+         */
+
+        if (G_pfnExcHook)
+        {
+            (*G_pfnExcHook)(file, ptib);
+        }
+
+        // *** registers
+
+        fprintf(file, "\nRegisters:");
+        if (pContextRec->ContextFlags & CONTEXT_INTEGER)
+        {
+            // EAX
+            fprintf(file, "\n    EAX = %08X  ", pContextRec->ctx_RegEax);
+            excDescribePage(file, pContextRec->ctx_RegEax);
+            // EBX
+            fprintf(file, "\n    EBX = %08X  ", pContextRec->ctx_RegEbx);
+            excDescribePage(file, pContextRec->ctx_RegEbx);
+            // ECX
+            fprintf(file, "\n    ECX = %08X  ", pContextRec->ctx_RegEcx);
+            excDescribePage(file, pContextRec->ctx_RegEcx);
+            // EDX
+            fprintf(file, "\n    EDX = %08X  ", pContextRec->ctx_RegEdx);
+            excDescribePage(file, pContextRec->ctx_RegEdx);
+            // ESI
+            fprintf(file, "\n    ESI = %08X  ", pContextRec->ctx_RegEsi);
+            excDescribePage(file, pContextRec->ctx_RegEsi);
+            // EDI
+            fprintf(file, "\n    EDI = %08X  ", pContextRec->ctx_RegEdi);
+            excDescribePage(file, pContextRec->ctx_RegEdi);
+            fprintf(file, "\n");
+        }
+        else
+            fprintf(file, " not available\n");
+
+        if (pContextRec->ContextFlags & CONTEXT_CONTROL)
+        {
+
+            // *** instruction
+
+            fprintf(file, "Instruction pointer (where exception occured):\n    CS:EIP = %04X:%08X  ",
+                    pContextRec->ctx_SegCs,
+                    pContextRec->ctx_RegEip);
+            excDescribePage(file, pContextRec->ctx_RegEip);
+
+            // *** CPU flags
+
+            fprintf(file, "\n    FLG    = %08X", pContextRec->ctx_EFlags);
+
+            /*
+             * stack:
+             *
+             */
+
+            fprintf(file, "\nStack:\n    Base:         %08X\n    Limit:        %08X",
+                   (ptib ? ptib->tib_pstack : 0),
+                   (ptib ? ptib->tib_pstacklimit :0));
+            fprintf(file, "\n    SS:ESP = %04X:%08X  ",
+                    pContextRec->ctx_SegSs, pContextRec->ctx_RegEsp);
+            excDescribePage(file, pContextRec->ctx_RegEsp);
+
+            fprintf(file, "\n    EBP    =      %08X  ", pContextRec->ctx_RegEbp);
+            excDescribePage(file, pContextRec->ctx_RegEbp);
+
+            /*
+             * stack dump:
+             */
+
+            if (ptib != 0)
+                excDumpStackFrames(file, ptib, pContextRec);
+        }
+    }
+    fprintf(file, "\n");
+
+    // reset old priority
+    DosSetPriority(PRTYS_THREAD,
+                   (ulOldPriority & 0x0F00) >> 8,
+                   (UCHAR)ulOldPriority,
+                   0);     // current thread
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Exported routines                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ excRegisterHooks:
+ *      this registers hooks which get called for
+ *      exception handlers. You can set any of the
+ *      hooks to NULL for safe defaults (see top of
+ *      except.c for details). You can set none,
+ *      one, or both of the hooks, and you can call
+ *      this function several times.
+ *
+ *      Both hooks get called whenever an exception
+ *      occurs, so there better be no bugs in these
+ *      routines. ;-) They only get called from
+ *      within excHandlerLoud (because excHandlerQuiet
+ *      writes no trap logs).
+ *
+ *      The hooks are as follows:
+ *
+ *      pfnExcOpenFileNew gets called to open
+ *          the trap log file. This must return a FILE*
+ *          pointer from fopen(). If this is not defined,
+ *          ?:\TRAP.LOG is used. Use this to specify a
+ *          different file and have some notes written
+ *          into it before the actual exception info.
+ *
+ *      pfnExcHookNew gets called while the trap log
+ *          is being written. At this point,
+ *          the following info has been written into
+ *          the trap log already:
+ *          -- exception type/address block
+ *          -- exception explanation
+ *          -- process information
+ *
+ *          _After_ the hook, the exception handler
+ *          continues with the "Registers" information
+ *          and stack dump/analysis.
+ *
+ *          Use this hook to write additional application
+ *          info into the trap log, such as the state
+ *          of your threads.
+ *
+ *      pfnExcHookError gets called when the TRY_* macros
+ *          fail to install an exception handler (when
+ *          DosSetExceptionHandler fails).
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.2 (2000-03-10) [umoeller]: pfnExcHookError added
+ */
+
+VOID excRegisterHooks(PFNEXCOPENFILE pfnExcOpenFileNew,
+                      PFNEXCHOOK pfnExcHookNew,
+                      PFNEXCHOOKERROR pfnExcHookError,
+                      BOOL fBeepOnExceptionNew)
+{
+    // adjust the global variables
+    G_pfnExcOpenFile = pfnExcOpenFileNew;
+    G_pfnExcHook = pfnExcHookNew;
+    G_pfnExcHookError = pfnExcHookError;
+    G_fBeepOnException = fBeepOnExceptionNew;
+}
+
+/*
+ *@@ excHandlerLoud:
+ *      this is the "sophisticated" exception handler;
+ *      which gives forth a loud sequence of beeps thru the
+ *      speaker, writes a trap log and then returns back
+ *      to the thread to continue execution, i.e. the
+ *      default OS/2 exception handler will never get
+ *      called. This requires a setjmp() call on
+ *      EXCEPTIONREGISTRATIONRECORD2.jmpThread before
+ *      being installed.
+ *
+ *      This is best registered thru the TRY_LOUD macro
+ *      (new with V0.84, described in except.c), which
+ *      does the necessary setup.
+ *
+ *      Depending on the type of exception, the following
+ *      happens:
+ *
+ *      <B>a) "real" exceptions</B>
+ *
+ *      --  XCPT_ACCESS_VIOLATION         (traps 0x0d, 0x0e)
+ *      --  XCPT_INTEGER_DIVIDE_BY_ZERO   (trap 0)
+ *      --  XCPT_ILLEGAL_INSTRUCTION      (trap 6)
+ *      --  XCPT_PRIVILEGED_INSTRUCTION
+ *      --  XCPT_INTEGER_OVERFLOW         (trap 4)
+ *
+ *      For these exceptions, we call the functions in debug.c
+ *      to try to find debug code or SYM file information about
+ *      what source code corresponds to the error.
+ *
+ *      Note that to get meaningful debugging information
+ *      in this handler's traplog, you need the following:
+ *      a)  have a MAP file created at link time (/MAP)
+ *      b)  convert the MAP to a SYM file using MAPSYM
+ *      c)  put the SYM file in the same directory of
+ *          the executable. This must have the same
+ *          filestem as the executable.
+ *
+ *      See the "Control Programming Guide and Reference"
+ *      for details.
+ *      All other exceptions are passed to the next handler
+ *      in the exception handler chain. This might be the
+ *      C/C++ compiler handler or the default OS/2 handler,
+ *      which will probably terminate the process.
+ *
+ *      <B>b) thread kills</B>
+ *
+ *      -- XCPT_PROCESS_TERMINATE
+ *      -- XCPT_ASYNC_PROCESS_TERMINATE:
+ *
+ *      If EXCEPTIONREGISTRATIONRECORD2.pfnOnKill is != NULL,
+ *      that function gets called to allow for thread cleanup
+ *      before the thread really terminates. This should be
+ *      used for releasing mutex semaphores.
+ *
+ *@@changed V0.9.0 [umoeller]: added support for thread termination
+ *@@changed V0.9.2 (2000-03-10) [umoeller]: switched date format to ISO
+ */
+
+ULONG _System excHandlerLoud(PEXCEPTIONREPORTRECORD pReportRec,
+                             PEXCEPTIONREGISTRATIONRECORD2 pRegRec2,
+                             PCONTEXTRECORD pContextRec,
+                             PVOID pv)
+{
+    /* From the VAC++3 docs:
+     *     "The first thing an exception handler should do is check the
+     *     exception flags. If EH_EXIT_UNWIND is set, meaning
+     *     the thread is ending, the handler tells the operating system
+     *     to pass the exception to the next exception handler. It does the
+     *     same if the EH_UNWINDING flag is set, the flag that indicates
+     *     this exception handler is being removed.
+     *     The EH_NESTED_CALL flag indicates whether the exception
+     *     occurred within an exception handler. If the handler does
+     *     not check this flag, recursive exceptions could occur until
+     *     there is no stack remaining."
+     * So for all these conditions, we exit immediately.
+     */
+
+    if (pReportRec->fHandlerFlags & EH_EXIT_UNWIND)
+       return (XCPT_CONTINUE_SEARCH);
+    if (pReportRec->fHandlerFlags & EH_UNWINDING)
+       return (XCPT_CONTINUE_SEARCH);
+    if (pReportRec->fHandlerFlags & EH_NESTED_CALL)
+       return (XCPT_CONTINUE_SEARCH);
+
+    switch (pReportRec->ExceptionNum)
+    {
+        case XCPT_PROCESS_TERMINATE:
+        case XCPT_ASYNC_PROCESS_TERMINATE:
+            // thread terminated:
+            // if the handler has been registered to catch
+            // these exceptions, continue;
+            if (pRegRec2->pfnOnKill)
+                // call the "OnKill" function
+                pRegRec2->pfnOnKill();
+            // get outta here, which will kill the thread
+        break;
+
+        case XCPT_ACCESS_VIOLATION:
+        case XCPT_INTEGER_DIVIDE_BY_ZERO:
+        case XCPT_ILLEGAL_INSTRUCTION:
+        case XCPT_PRIVILEGED_INSTRUCTION:
+        case XCPT_INTEGER_OVERFLOW:
+        {
+            // "real" exceptions:
+            FILE *file;
+
+            // open traplog file;
+            if (G_pfnExcOpenFile)
+                // hook defined for this: call it
+                file = (*G_pfnExcOpenFile)();
+            else
+            {
+                CHAR szFileName[100];
+                // no hook defined: open some
+                // default traplog file in root directory of
+                // boot drive
+                sprintf(szFileName, "%c:\\trap.log", doshQueryBootDrive());
+                file = fopen(szFileName, "a");
+
+                if (file)
+                {
+                    DATETIME DT;
+                    DosGetDateTime(&DT);
+                    fprintf(file,
+                            "\nTrap message -- Date: %04d-%02d-%02d, Time: %02d:%02d:%02d\n",
+                            DT.year, DT.month, DT.day,
+                            DT.hours, DT.minutes, DT.seconds);
+                    fprintf(file, "------------------------------------------------\n");
+
+                }
+            }
+
+            // write error log
+            excExplainException(file,
+                                "excHandlerLoud",
+                                pReportRec,
+                                pContextRec);
+            fclose(file);
+
+            // jump back to failing routine
+            /* DosSetPriority(PRTYS_THREAD,
+                           PRTYC_REGULAR,
+                           0,       // delta
+                           0);      // current thread
+                           */
+            longjmp(pRegRec2->jmpThread, pReportRec->ExceptionNum);
+        break; }
+    }
+
+    // not handled
+    return (XCPT_CONTINUE_SEARCH);
+}
+
+/*
+ *@@ excHandlerQuiet:
+ *      "quiet" xcpt handler, which simply suppresses exceptions;
+ *      this is useful for certain error-prone functions, where
+ *      exceptions are likely to appear, for example used by
+ *      cmnCheckObject to implement a fail-safe SOM object check.
+ *
+ *      This does _not_ write an error log and makes _no_ sound.
+ *      This simply jumps back to the trapping thread or
+ *      calls EXCEPTIONREGISTRATIONRECORD2.pfnOnKill.
+ *
+ *      Other than that, this behaves like excHandlerLoud.
+ *
+ *      This is best registered thru the TRY_QUIET macro
+ *      (new with V0.84, described in except.c), which
+ *      does the necessary setup.
+ *
+ *@@changed V0.9.0 [umoeller]: added support for thread termination
+ */
+
+ULONG _System excHandlerQuiet(PEXCEPTIONREPORTRECORD pReportRec,
+                              PEXCEPTIONREGISTRATIONRECORD2 pRegRec2,
+                              PCONTEXTRECORD pContextRec,
+                              PVOID pv)
+{
+    if (pReportRec->fHandlerFlags & EH_EXIT_UNWIND)
+       return (XCPT_CONTINUE_SEARCH);
+    if (pReportRec->fHandlerFlags & EH_UNWINDING)
+       return (XCPT_CONTINUE_SEARCH);
+    if (pReportRec->fHandlerFlags & EH_NESTED_CALL)
+       return (XCPT_CONTINUE_SEARCH);
+
+    switch (pReportRec->ExceptionNum)
+    {
+        case XCPT_PROCESS_TERMINATE:
+        case XCPT_ASYNC_PROCESS_TERMINATE:
+            // thread terminated:
+            // if the handler has been registered to catch
+            // these exceptions, continue;
+            if (pRegRec2->pfnOnKill)
+                // call the "OnKill" function
+                pRegRec2->pfnOnKill();
+            // get outta here, which will kill the thread
+        break;
+
+        case XCPT_ACCESS_VIOLATION:
+        case XCPT_INTEGER_DIVIDE_BY_ZERO:
+        case XCPT_ILLEGAL_INSTRUCTION:
+        case XCPT_PRIVILEGED_INSTRUCTION:
+        case XCPT_INTEGER_OVERFLOW:
+            // write excpt explanation only if the
+            // resp. debugging #define is set (common.h)
+            #ifdef DEBUG_WRITEQUIETEXCPT
+            {
+                FILE *file = excOpenTraplogFile();
+                excExplainException(file,
+                                    "excHandlerQuiet",
+                                    pReportRec,
+                                    pContextRec);
+                fclose(file);
+            }
+            #endif
+
+            // jump back to failing routine
+            longjmp(pRegRec2->jmpThread, pReportRec->ExceptionNum);
+        break;
+
+        default:
+             break;
+    }
+
+    return (XCPT_CONTINUE_SEARCH);
+}
+
+
Index: /trunk/src/helpers/gpih.c
===================================================================
--- /trunk/src/helpers/gpih.c	(revision 8)
+++ /trunk/src/helpers/gpih.c	(revision 8)
@@ -0,0 +1,1692 @@
+
+/*
+ *@@sourcefile gpih.c:
+ *      contains GPI (graphics) helper functions.
+ *
+ *      Usage: All PM programs.
+ *
+ *      A word about GPI rectangles: In general, graphics operations involving
+ *      device coordinates (such as regions, bit maps and bit blts, and window
+ *      management) use inclusive-exclusive rectangles.  All other graphics
+ *      operations, such as GPI functions that define paths, use inclusive-inclusive
+ *      rectangles.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  gpih*   GPI helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\gpih.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSDEVIOCTL
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WIN
+#define INCL_GPI
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\winh.h"
+#include "helpers\gpih.h"
+
+#pragma hdrstop
+
+// array for querying device capabilities (gpihQueryDisplayCaps)
+LONG            DisplayCaps[CAPS_DEVICE_POLYSET_POINTS] = {0};
+BOOL            fCapsQueried = FALSE;
+
+/*
+ *@@category: Helpers\PM helpers\GPI helpers\Devices
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Device helpers                                                 *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ gpihQueryDisplayCaps:
+ *      this returns certain device capabilities of
+ *      the Display device. ulIndex must be one of
+ *      the indices as described in DevQueryCaps.
+ *
+ *      This function will load all the device capabilities
+ *      only once into a global array and re-use them afterwards.
+ */
+
+ULONG gpihQueryDisplayCaps(ULONG ulIndex)
+{
+    if (!fCapsQueried)
+    {
+        HPS hps = WinGetScreenPS(HWND_DESKTOP);
+        HDC hdc = GpiQueryDevice(hps);
+        DevQueryCaps(hdc, 0, CAPS_DEVICE_POLYSET_POINTS, &DisplayCaps[0]);
+    }
+
+    return (DisplayCaps[ulIndex]);
+}
+
+/*
+ *@@category: Helpers\PM helpers\GPI helpers\Colors
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Color helpers                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ gpihManipulateRGB:
+ *      this changes an RGB color value
+ *      by multiplying each color component
+ *      (red, green, blue) with bMultiplier
+ *      and dividing it by bDivisor afterwards.
+ *
+ *      Each color component is treated separately,
+ *      so if overflows occur (because bMultiplier
+ *      is > 1), this does not affect the other
+ *      components.
+ *
+ *      Example: if you pass a brigt red
+ *      (0xFF0000) to this func, this will
+ *      be 0x7F0000 if bMultiplier and
+ *      bDivisor are 1 and 2.
+ */
+
+VOID gpihManipulateRGB(PLONG plColor,       // in/out: RGB color
+                       BYTE bMultiplier,    // in: multiplier
+                       BYTE bDivisor)       // in: divisor
+{
+    PBYTE   pb = (PBYTE)plColor;
+    // in memory, the bytes are blue, green, red, unused
+    *pb++ = (BYTE)((LONG)(*pb) * bMultiplier / bDivisor); // blue
+    *pb++ = (BYTE)((LONG)(*pb) * bMultiplier / bDivisor); // green
+    *pb++ = (BYTE)((LONG)(*pb) * bMultiplier / bDivisor); // red
+}
+
+/*
+ *@@category: Helpers\PM helpers\GPI helpers\Drawing primitives
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Drawing primitives helpers                                     *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ gpihDrawRect:
+ *      this draws a simple rectangle with the current
+ *      color (use GpiSetColor before calling this function).
+ *
+ *      The specified rectangle is inclusive, that is, the top
+ *      right corner specifies the top right pixel to be drawn.
+ *
+ *      This sets the current position to the bottom left corner
+ *      of prcl.
+ *
+ *@added V0.9.0
+ */
+
+VOID gpihDrawRect(HPS hps,      // in: presentation space for output
+                  PRECTL prcl)  // in: rectangle to draw (inclusive)
+{
+    POINTL ptl1;
+
+    ptl1.x = prcl->xLeft;
+    ptl1.y = prcl->yBottom;
+    GpiMove(hps, &ptl1);
+    ptl1.y = prcl->yTop-1;
+    GpiLine(hps, &ptl1);
+    ptl1.x = prcl->xRight-1;
+    GpiLine(hps, &ptl1);
+    ptl1.y = prcl->yBottom;
+    GpiLine(hps, &ptl1);
+    ptl1.x = prcl->xLeft;
+    GpiLine(hps, &ptl1);
+}
+
+/*
+ *@@ gpihBox:
+ *      this is a shortcurt to GpiBox, using the specified
+ *      rectangle.
+ *
+ *      As opposed to WinFillRect, this works with memory
+ *      (bitmap) PS's also.
+ *
+ *      The specified rectangle is exclusive, meaning that
+ *      the top right point lies _outside_ the rectangle
+ *      which is actually drawn. This is the same as with
+ *      WinFillRect.
+ *
+ *      Changes to the HPS:
+ *      --  the current (foreground) color is changed to
+ *          lBackColor;
+ *      --  the current area color is changed to lForeColor;
+ *      --  the current position is moved to the lower left
+ *          corner of *prcl.
+ *
+ *@@changed V0.9.0 [umoeller]: renamed from gpihFillRect
+ *@@changed V0.9.0 [umoeller]: modified function prototype to support lControl
+ */
+
+VOID gpihBox(HPS hps,              // in: presentation space for output
+             LONG lControl,        // in: one of DRO_OUTLINE, DRO_FILL, DRO_OUTLINEFILL
+             PRECTL prcl,          // in: rectangle to draw (exclusive)
+             LONG lColor)          // in: color for outline and/or fill
+{
+    POINTL      ptl;
+
+    GpiSetColor(hps, lColor);
+    ptl.x = prcl->xLeft;
+    ptl.y = prcl->yBottom;
+    GpiMove(hps, &ptl);
+    ptl.x = prcl->xRight;
+    ptl.y = prcl->yTop;
+    GpiBox(hps,
+           lControl,    // DRO_*
+           &ptl,
+           0, 0);       // no corner rounding
+}
+
+/*
+ *@@ gpihMarker:
+ *      this draws a quick marker (a filled
+ *      rectangle) at the specified position.
+ *      The rectangle will be drawn so that
+ *      the specified point is in its center.
+ *
+ *      No PS data is changed.
+ */
+
+VOID gpihMarker(HPS hps,
+                LONG x,             // in: x-center of rectangle
+                LONG y,             // in: y-center of rectangle
+                ULONG ulWidth,      // in: rectangle width and height
+                LONG lColor)        // in: color
+{
+    POINTL  ptlSave;
+    LONG    lColorSave;
+    RECTL   rclTemp;
+    ULONG   ulWidth2 = ulWidth / 2;
+    rclTemp.xLeft = x - ulWidth2;
+    rclTemp.xRight = x + ulWidth2;
+    rclTemp.yBottom = y - ulWidth2;
+    rclTemp.yTop    = y + ulWidth2;
+
+    lColorSave = GpiQueryColor(hps);
+    GpiQueryCurrentPosition(hps, &ptlSave);
+    gpihBox(hps,
+            DRO_FILL,
+            &rclTemp,
+            lColor);
+    GpiMove(hps, &ptlSave);
+    GpiSetColor(hps, lColorSave);
+}
+
+/*
+ *@@ gpihDraw3DFrame:
+ *      this draws a rectangle in 3D style with a given line width
+ *      and the given colors.
+ *
+ *      The specified rectangle is inclusive, that is, the top
+ *      right corner specifies the top right pixel to be drawn.
+ *
+ *      If usWidth > 1, the additional pixels will be drawn towards
+ *      the _center_ of the rectangle. prcl thus always specifies
+ *      the bottom left and top right pixels to be drawn.
+ *
+ *@@changed V0.9.0 [umoeller]: changed function prototype to have colors specified
+ */
+
+VOID gpihDraw3DFrame(HPS hps,
+                     PRECTL prcl,       // in: rectangle
+                     USHORT usWidth,    // in: line width (>= 1)
+                     LONG lColorLeft,   // in: color to use for left and top; e.g. SYSCLR_BUTTONLIGHT
+                     LONG lColorRight)  // in: color to use for right and bottom; e.g. SYSCLR_BUTTONDARK
+{
+    RECTL rcl2 = *prcl;
+    USHORT us;
+    POINTL ptl1;
+
+    for (us = 0; us < usWidth; us++)
+    {
+        GpiSetColor(hps, lColorLeft);
+        ptl1.x = rcl2.xLeft;
+        ptl1.y = rcl2.yBottom;
+        GpiMove(hps, &ptl1);
+        ptl1.y = rcl2.yTop-1;
+        GpiLine(hps, &ptl1);
+        ptl1.x = rcl2.xRight-1;
+        GpiLine(hps, &ptl1);
+        GpiSetColor(hps, lColorRight);
+        ptl1.y = rcl2.yBottom;
+        GpiLine(hps, &ptl1);
+        ptl1.x = rcl2.xLeft;
+        GpiLine(hps, &ptl1);
+
+        rcl2.xLeft++;
+        rcl2.yBottom++;
+        rcl2.xRight--;
+        rcl2.yTop--;
+    }
+}
+
+/*
+ *@@ gpihCharStringPosAt:
+ *      wrapper for GpiCharStringPosAt.
+ *      Since that function is limited to 512 characters
+ *      (according to GPIREF; on my Warp 4 FP13, I actually
+ *      get some 3000 characters... whatever this is),
+ *      this splits the string into 512 byte chunks and
+ *      calls GpiCharStringPosAt accordingly.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+LONG gpihCharStringPosAt(HPS hps,
+                         PPOINTL pptlStart,
+                         PRECTL prclRect,
+                         ULONG flOptions,
+                         LONG lCount,
+                         PCH pchString)
+{
+    LONG    lHits = 0,
+            lCountLeft = lCount;
+    PCH     pchThis = pchString;
+
+    GpiMove(hps, pptlStart);
+
+    if (lCount)
+    {
+        do
+        {
+            LONG    lCountThis = lCountLeft;
+            if (lCountLeft >= 512)
+                lCountThis = 512;
+
+            lHits = GpiCharStringPos(hps,
+                                     prclRect,
+                                     flOptions,
+                                     lCountThis,
+                                     pchThis,
+                                     0);
+
+            pchThis += 512;
+            lCountLeft -= 512;
+        } while (lCountLeft > 0);
+    }
+
+    return (lHits);
+}
+
+/*
+ *@@category: Helpers\PM helpers\GPI helpers\Fonts
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Font helpers                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ gpihSplitPresFont:
+ *      splits a presentation parameter font
+ *      string into the point size and face
+ *      name so that it can be passed to
+ *      gpihFindFont more easily.
+ *
+ *@@added V0.9.1 (2000-02-15) [umoeller]
+ */
+
+BOOL gpihSplitPresFont(PSZ pszFontNameSize,  // in: e.g. "12.Courier"
+                       PULONG pulSize,       // out: integer point size (e.g. 12);
+                                             // ptr must be specified
+                       PSZ *ppszFaceName)    // out: ptr into pszFontNameSize
+                                             // (e.g. "Courier")
+{
+    BOOL brc = FALSE;
+
+    if (pszFontNameSize)
+    {
+        PCHAR   pcDot = strchr(pszFontNameSize, '.');
+        if (pcDot)
+        {
+            // _Pmpf(("Found font PP: %s", pszFontFound));
+            sscanf(pszFontNameSize, "%d", pulSize);
+            *ppszFaceName = pcDot + 1;
+            brc = TRUE;
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ gpihQueryNextLCID:
+ *      returns the next available lcid for the given HPS.
+ *      Actually, it's the next available lcid for the
+ *      entire process, since there can be only 255 altogether.
+ *      Gets called by gpihFindFont automatically.
+ *
+ *      This is possibly the sickest code I've ever written.
+ *
+ *      Warning: This function is _not_ thread-safe.
+ *      We use GpiQueryNumberSetIds to find out the no. of
+ *      LCID's in use by the process. In the worst case,
+ *      between that call and the call to GpiCreateLogFont,
+ *      another font has been created, and you'll get garbage.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+LONG gpihQueryNextFontID(HPS hps)
+{
+    LONG    lcidNext = -1;
+
+    LONG lCount =  GpiQueryNumberSetIds(hps);
+                             //  the number of local identifiers
+                             //  (lcids) currently in use, and
+                             //  therefore the maximum number
+                             //  of objects for which information
+                             //  can be returned
+    if (lCount == 0)
+        // none in use yet:
+        lcidNext = 1;
+    else
+    {
+        #define GQNCL_BLOCK_SIZE 400*sizeof(LONG)
+        PLONG   pBase;
+
+        APIRET  arc;
+
+        // _Pmpf(("gpihQueryNextFontID: calling DosAllocMem"));
+
+        arc = DosAllocMem((PPVOID)(&pBase),
+                          GQNCL_BLOCK_SIZE,
+                                   // space is needed for an array of lCount longs.
+                          PAG_READ |
+                          PAG_WRITE);
+        if (arc == NO_ERROR)
+        {
+            arc = DosSubSetMem(pBase,
+                               DOSSUB_INIT | DOSSUB_SPARSE_OBJ,
+                               GQNCL_BLOCK_SIZE);
+            if (arc == NO_ERROR)
+            {
+                PLONG  alTypes;  // object types
+                PSTR8  aNames;   // font names
+                PLONG  allcids;  // local identifiers
+
+                arc = DosSubAllocMem((PVOID)pBase,
+                                     (PPVOID)(&aNames),
+                                     (ULONG)(lCount*(ULONG)sizeof(STR8)));
+                                             // space is needed for an array of
+                                             // lCount longs
+                if (arc == NO_ERROR)
+                {
+                    arc = DosSubAllocMem((PVOID)pBase,
+                                             (PPVOID)(&allcids),
+                                             (ULONG)lCount*sizeof(LONG));
+                                                // space is needed for an array of
+                                                // lCount longs.
+                    if (arc == NO_ERROR)
+                    {
+                        arc = DosSubAllocMem((PVOID)pBase,
+                                                 (PPVOID)(&alTypes),
+                                                 (ULONG)lCount*sizeof(LONG));
+                                                    // space is needed for an array of
+                                                    // lCount longs.
+                        if (arc == NO_ERROR)
+                        {
+                            if (GpiQuerySetIds(hps,
+                                               lCount,
+                                               alTypes,
+                                               aNames,
+                                               allcids))
+                            {
+                                // FINALLY we have all the lcids in use.
+                                BOOL    fContinue = TRUE;
+                                lcidNext = 1;
+
+                                // now, check if this lcid is in use already:
+                                while (fContinue)
+                                {
+                                    BOOL fFound = FALSE;
+                                    ULONG ul;
+                                    fContinue = FALSE;
+                                    for (ul = 0;
+                                         ul < lCount;
+                                         ul++)
+                                    {
+                                        if (allcids[ul] == lcidNext)
+                                        {
+                                            fFound = TRUE;
+                                            break;
+                                        }
+                                    }
+
+                                    if (fFound)
+                                    {
+                                        // lcid found:
+                                        // try next higher one
+                                        lcidNext++;
+                                        fContinue = TRUE;
+                                    }
+                                    // else: return that one
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            arc = DosFreeMem(pBase);
+        }
+    }
+
+    return (lcidNext);
+}
+
+/*
+ *@@ gpihFindFont:
+ *      this returns a new logical font ID (LCID) for the specified
+ *      font by calling GpiCreateLogFont.
+ *      This function performs the insane "11-step process" to
+ *      match a font, as described in the GPI reference.
+ *
+ *      This function can operate in two modes:
+ *
+ *      -- "Family" mode. In that case, specify the font family name
+ *         with pszName and set fFamily to TRUE. This is useful for
+ *         WYSIWYG text viewing if you need several font faces for
+ *         the same family, such as Courier Bold, Bold Italics, etc.
+ *         You can specify those attributes with usFormat then.
+ *
+ *      -- "Face" mode. In that case, specify the full font face name
+ *         with pszName and set fFamily to FALSE. This is useful for
+ *         font presentation parameters which use the "WarpSans Bold"
+ *         format. In that case, set usFormat to 0.
+ *
+ *      After the font has been created, if (pFontMetrics != NULL),
+ *      *pFontMetrics receives the FONTMETRICS of the font which
+ *      has been created. If an outline font has been created
+ *      (instead of a bitmap font), FONTMETRICS.fsDefn will have
+ *      the FM_DEFN_OUTLINE bit set.
+ *
+ *      To then use the font whose LCID has been returned by this
+ *      function for text output, call:
+ +          GpiSetCharSet(hps, lLCIDReturned);
+ *
+ *      <B>Font Point Sizes:</B>
+ *
+ *      1)  For image (bitmap) fonts, the size is fixed, and
+ *          you can directly draw after the font has been
+ *          selected. GpiSetCharBox has no effect on image
+ *          fonts unless you switch to character mode 2
+ *          (if you care for that: GpiSetCharMode).
+ *
+ *      2)  For outline fonts however, you need to define a
+ *          character box if you want to output text in a
+ *          size other than the default size. This is almost
+ *          as bad a mess as this function, so gpihSetPointSize
+ *          has been provided for this. See remarks there.
+ *
+ *      <B>Example:</B>
+ *
+ *      This example prints text in "24.Courier".
+ *
+ +          PSZ     pszOutput = "Test output";
+ +          FONTMETRICS FontMetrics;
+ +          LONG    lLCID = gpihFindFont(hps,
+ +                                       24,        // point size
+ +                                       FALSE,     // face, not family
+ +                                       "Courier",
+ +                                       0,
+ +                                       &FontMetrics);
+ +          if (lLCID)
+ +          {
+ +              // no error:
+ +              GpiSetCharSet(hps, lLCID);
+ +              if (FontMetrics.fsDefn & FM_DEFN_OUTLINE)
+ +                  // outline font found (should be the case):
+ +                  gpihSetPointSize(hps, 24);
+ +          }
+ +          GpiCharString(hps, strlen(pszOutput), pszOutput);
+ *
+ *      <B>Details:</B>
+ *
+ *      First, GpiQueryFonts is called to enumerate all the fonts on
+ *      the system. We then evaluate the awful FONTMETRICS data
+ *      of those fonts to perform a "real" match.
+ *
+ *      If that fails, we allow GPI to do a "close" match based
+ *      on the input values. This might result in the system
+ *      default font (System Proportional) to be found, in the
+ *      worst case. But even then, a new LCID is returned.
+ *
+ *      The "close match" comes in especially when using the
+ *      font attributes (bold, italics) and we are unable to
+ *      find the correct outline font for that. Unfortunately,
+ *      the information in FONTMETRICS.fsSelection is wrong,
+ *      wrong, wrong for the large majority of fonts. (For
+ *      example, I get the "bold" flag set for regular fonts,
+ *      and vice versa.) So we attempt to use the other fields,
+ *      but this doesn't always help. Not even Netscape gets
+ *      this right.
+ *
+ *      <B>Font faces:</B>
+ *
+ *      This is terribly complicated as well. You need to
+ *      differentiate between "true" emphasis faces (that
+ *      is, bold and italics are implemented thru separate
+ *      font files) and the ugly GPI simulation, which simply
+ *      makes the characters wider or shears them.
+ *
+ *      This function even finds true "bold" and "italic" faces
+ *      for outline fonts. To do this, always specify the "family"
+ *      name as pszFaceName (e.g. "Courier" instead of "Courier
+ *      Bold") and set the flags for usFormat (e.g. FATTR_SEL_BOLD).
+ *      Note that this implies that you need call this function
+ *      twice to create two logical fonts for regular and bold faces.
+ *
+ *      If a "true" emphasis font is not found, the GPI simulation
+ *      is enabled.
+ *
+ *      <B>Remarks:</B>
+ *
+ *      1)  This function _always_ creates a new logical font,
+ *          whose ID is returned, even if the specified font
+ *          was not found or a "close match" was performed by
+ *          GPI. As a result, do not call this function twice
+ *          for the same font specification, because there are
+ *          only 255 logical font IDs for each process.
+ *
+ *      2)  Since this function always creates an LCID,
+ *          you should _always_ free the LCID later.
+ *          This is only valid if the font is no longer selected
+ *          into any presentation space. So use these calls:
+ +              GpiSetCharSet(hps, LCID_DEFAULT);
+ +              GpiDeleteSetId(hps, lLCIDReturnedByThis);
+ *
+ *      3)  Using this function, bitmap fonts will have priority
+ *          over outline fonts of the same face name. This is
+ *          how the WPS does it too. This is most obvious with
+ *          the "Helv" font, which exists as a bitmap font for
+ *          certain point sizes only.
+ *
+ *      4)  <B>Warning: This function is _not_ thread-safe.</B>
+ *          Since logical font IDs are shared across the
+ *          process, this function should _not_ be called
+ *          from several threads at the same time.
+ *
+ *          This calls gpihQueryNextFontID in turn to find the
+ *          next free lcid. See remarks there.
+ *          If your fonts suddenly change to "System Proportional"
+ *          in your application, you probably have a serialization
+ *          problem.
+ *
+ *          To make this thread-safe, create your own wrapper
+ *          function which calls this function while a mutex
+ *          semaphore is held by the wrapper. Then use only
+ *          the wrapper in your code.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.3 (2000-05-06) [umoeller]: didn't work for more than one font; now using gpihQueryNextFontID
+ *@@changed V0.9.3 (2000-05-06) [umoeller]: usFormat didn't work; fixed
+ *@@changed V0.9.4 (2000-08-08) [umoeller]: added fFamily
+ */
+
+LONG gpihFindFont(HPS hps,               // in: HPS for font selection
+                  LONG lSize,            // in: font point size
+                  BOOL fFamily,          // in: if TRUE, pszName specifies font family;
+                                         //     if FALSE, pszName specifies font face
+                  PSZ pszName,           // in: font family or face name (without point size)
+                  USHORT usFormat,       // in: none, one or several of:
+                                         // -- FATTR_SEL_ITALIC
+                                         // -- FATTR_SEL_UNDERSCORE (underline)
+                                         // -- FATTR_SEL_BOLD
+                                         // -- FATTR_SEL_STRIKEOUT
+                                         // -- FATTR_SEL_OUTLINE (hollow)
+                  PFONTMETRICS pFontMetrics) // out: font metrics of created font (optional)
+{
+    LONG    lLCIDReturn = 0;
+    ULONG   ul = 0;
+    FATTRS  FontAttrs;
+
+    // first find out how much memory we need to allocate
+    // for the FONTMETRICS structures
+    LONG    lTemp = 0;
+    LONG    cFonts = GpiQueryFonts(hps,
+                                   QF_PUBLIC | QF_PRIVATE,
+                                   NULL, // pszFaceName,
+                                   &lTemp,
+                                   sizeof(FONTMETRICS),
+                                   NULL);
+    PFONTMETRICS pfm = (PFONTMETRICS)malloc(cFonts * sizeof(FONTMETRICS)),
+                 pfm2 = pfm,
+                 pfmFound = NULL;
+
+    // _Pmpf(("gpihFindFont: enumerating for %s, %d points", pszFaceName, lSize));
+
+    GpiQueryFonts(hps,
+                  QF_PUBLIC | QF_PRIVATE,
+                  NULL, // pszFaceName,
+                  &cFonts,
+                  sizeof(FONTMETRICS),      // length of each metrics structure
+                                            // -- _not_ total buffer size!
+                  pfm);
+    // now we have an array of FONTMETRICS
+    // for EVERY font that is installed on the system...
+    // these things are completely unsorted, so there's
+    // nothing we can rely on, we have to check them all.
+
+    // fill in some default values for FATTRS,
+    // in case we don't find something better
+    // in the loop below; these values will be
+    // applied if
+    // a)   an outline font has been found;
+    // b)   bitmap fonts have been found, but
+    //      none for the current device resolution
+    //      exists;
+    // c)   no font has been found at all.
+    // In all cases, GpiCreateLogFont will do
+    // a "close match" resolution (at the bottom).
+    FontAttrs.usRecordLength = sizeof(FATTRS);
+    FontAttrs.fsSelection = usFormat; // changed later if better font is found
+    FontAttrs.lMatch = 0L;             // closest match
+    strcpy(FontAttrs.szFacename, pszName);
+    FontAttrs.idRegistry = 0;          // default registry
+    FontAttrs.usCodePage = 0;          // default codepage
+    // the following two must be zero, or outline fonts
+    // will not be found; if a bitmap font has been passed
+    // to us, we'll modify these two fields later
+    FontAttrs.lMaxBaselineExt = 0;     // font size (height)
+    FontAttrs.lAveCharWidth = 0;       // font size (width)
+    FontAttrs.fsType = 0;              // default type
+    FontAttrs.fsFontUse = FATTR_FONTUSE_NOMIX;
+
+    // now go thru the array of FONTMETRICS
+    // to check if we have a bitmap font
+    // pszFaceName; the default WPS behavior
+    // is that bitmap fonts appear to take
+    // priority over outline fonts of the
+    // same name, so we check these first
+    pfm2 = pfm;
+    for (ul = 0;
+         ul < cFonts;
+         ul++)
+    {
+        /* _Pmpf(("  Checking font: %s (Fam: %s), %d, %d, %d",
+               pszFaceName,
+               pfm2->szFamilyname,
+               pfm2->sNominalPointSize,
+               pfm2->lMaxBaselineExt,
+               pfm2->lAveCharWidth)); */
+
+        PSZ pszCompare = (fFamily)
+                              ? pfm2->szFamilyname
+                              : pfm2->szFacename;
+
+        if (strcmp(pszCompare, pszName) == 0)
+        {
+            /* _Pmpf(("  Found font %s; slope %d, usWeightClass %d",
+                    pfm2->szFacename,
+                    pfm2->sCharSlope,
+                    pfm2->usWeightClass)); */
+
+            if ((pfm2->fsDefn & FM_DEFN_OUTLINE) == 0)
+            {
+                // image (bitmap) font:
+                // check point size
+                if (pfm2->sNominalPointSize == lSize * 10)
+                {
+                    // OK: check device resolutions, because
+                    // normally, there are always several image
+                    // fonts for different resolutions
+                    // for bitmap fonts, there are always two versions:
+                    // one for low resolutions, one for high resolutions
+                    LONG    alDevRes[2];
+                    DevQueryCaps(GpiQueryDevice(hps),
+                                 CAPS_HORIZONTAL_FONT_RES,
+                                 2L,
+                                 alDevRes);
+                    if (    (pfm2->sXDeviceRes == alDevRes[0])
+                         && (pfm2->sYDeviceRes == alDevRes[1])
+                       )
+                    {
+                        // OK: use this for GpiCreateLogFont
+                        FontAttrs.lMaxBaselineExt = pfm2->lMaxBaselineExt;
+                        FontAttrs.lAveCharWidth = pfm2->lAveCharWidth;
+                        FontAttrs.lMatch = pfm2->lMatch;
+
+                        pfmFound = pfm2;
+                        break;
+                    }
+                }
+            }
+            else
+                // outline font:
+                if (pfmFound == NULL)
+                {
+                    /*
+                        #define FATTR_SEL_ITALIC               0x0001
+                        #define FATTR_SEL_UNDERSCORE           0x0002
+                        #define FATTR_SEL_OUTLINE              0x0008
+                        #define FATTR_SEL_STRIKEOUT            0x0010
+                        #define FATTR_SEL_BOLD                 0x0020
+                     */
+                    // no bitmap font found yet:
+                    if (  (     (   (usFormat & FATTR_SEL_BOLD)
+                                 && (pfm2->usWeightClass == 7) // bold
+                                )
+                            ||  (   ((usFormat & FATTR_SEL_BOLD) == 0)
+                                 && (pfm2->usWeightClass == 5) // regular
+                                )
+                           )
+                        && (    (   (usFormat & FATTR_SEL_ITALIC)
+                                 && (pfm2->sCharSlope != 0) // italics
+                                )
+                            ||  (   ((usFormat & FATTR_SEL_ITALIC) == 0)
+                                 && (pfm2->sCharSlope == 0) // regular
+                                )
+                           )
+                       )
+                    {
+                        // yes, we found a true font for that face:
+                        pfmFound = pfm2;
+                        // use this exact font for GpiCreateLogFont
+                        FontAttrs.lMatch = pfm2->lMatch;
+                        // according to GPIREF, we must also specify
+                        // the full face name... Jesus!
+                        strcpy(FontAttrs.szFacename, pfm2->szFacename);
+                        // unset flag in FATTRS, because this would
+                        // duplicate bold or italic
+                        FontAttrs.fsSelection = 0;
+
+                        // _Pmpf(("    --> using it"));
+                        // but loop on, because we might have a bitmap
+                        // font which should take priority
+                    }
+                }
+        }
+
+        pfm2++;
+    }
+
+    if (pfmFound)
+        // FONTMETRICS found:
+        // copy font metrics?
+        if (pFontMetrics)
+            memcpy(pFontMetrics, pfmFound, sizeof(FONTMETRICS));
+
+    // free the FONTMETRICS array
+    free(pfm);
+
+    // new logical font ID: last used plus one
+    lLCIDReturn = gpihQueryNextFontID(hps);
+
+    GpiCreateLogFont(hps,
+                     NULL,  // don't create "logical font name" (STR8)
+                     lLCIDReturn,
+                     &FontAttrs);
+
+    return (lLCIDReturn);
+}
+
+/*
+ *@@ gpihFindPresFont:
+ *      similar to gpihFindFont, but this one evaluates
+ *      the PP_FONTNAMESIZE presentation parameter of the
+ *      specified window instead. If that one is not set,
+ *      the specified default font is used instead.
+ *
+ *      Note that as opposed to gpihFindFont, this one
+ *      takes a "8.Helv"-type string as input.
+ *
+ *      See gpihFindFont for additional remarks, which
+ *      gets called by this function.
+ *
+ *      Again, if an outline font has been returned, you
+ *      must also set the "character box" for the HPS, or
+ *      your text will always have the same point size.
+ *      Use gpihSetPointSize for that, using the presparam's
+ *      point size, which is returned by this function
+ *      into *plSize, if (plSize != NULL):
+ +
+ +          FONTMETRICS FontMetrics;
+ +          LONG        lPointSize;
+ +          LONG        lLCID = gpihFindPresFont(hwnd, hps, "8.Helv",
+ +                                               &FontMetrics,
+ +                                               &lPointSize);
+ +          GpiSetCharSet(hps, lLCID);
+ +          if (FontMetrics.fsDefn & FM_DEFN_OUTLINE)
+ +              gpihSetPointSize(hps, lPointSize);
+ *
+ *      If errors occur, e.g. if the font string does not
+ *      conform to the "size.face" format, null is returned.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+LONG gpihFindPresFont(HWND hwnd,          // in: window to search for presparam
+                      BOOL fInherit,      // in: search parent windows too?
+                      HPS hps,            // in: HPS for font selection
+                      PSZ pszDefaultFont, // in: default font if not found (i.e. "8.Helv")
+                      PFONTMETRICS pFontMetrics, // out: font metrics of created font (optional)
+                      PLONG plSize)       // out: presparam's point size (optional)
+{
+    CHAR    szPPFont[200] = "";
+    PSZ     pszFontFound = 0;
+    CHAR    szFaceName[300] = "";
+    ULONG   ulFontSize = 0;
+
+    if (WinQueryPresParam(hwnd,
+                          PP_FONTNAMESIZE,          // first PP to query
+                          0,                        // second PP to query
+                          NULL,                     // out: which one is returned
+                          (ULONG)sizeof(szPPFont),  // in: buffer size
+                          (PVOID)&szPPFont,         // out: PP value returned
+                          (fInherit)
+                               ? 0
+                               : QPF_NOINHERIT))
+        // PP found:
+        pszFontFound = szPPFont;
+    else
+        pszFontFound = pszDefaultFont;
+
+    if (pszFontFound)
+    {
+        PCHAR   pcDot = strchr(pszFontFound, '.');
+        if (pcDot)
+        {
+            // _Pmpf(("Found font PP: %s", pszFontFound));
+            sscanf(pszFontFound, "%d", &ulFontSize);
+            if (plSize)
+                *plSize = ulFontSize;
+            strcpy(szFaceName, pcDot + 1);
+            return (gpihFindFont(hps,
+                                 ulFontSize,
+                                 FALSE,         // face, not family name
+                                 szFaceName,
+                                 0,
+                                 pFontMetrics));
+        }
+    }
+    return (0);
+}
+
+/*
+ *@@ gpihSetPointSize:
+ *      this invokes GpiSetCharBox on the given HPS to
+ *      set the correct "character box" with the proper
+ *      parameters.
+ *
+ *      This is necessary for text output with outline
+ *      fonts. Bitmap fonts have a fixed size, and
+ *      calling this function is not necessary for them.
+ *
+ *      Unfortunately, IBM has almost not documented
+ *      how to convert nominal point sizes to the
+ *      correct character cell values. This involves
+ *      querying the output device (DevQueryCaps).
+ *
+ *      I have found one hint for this procedure in GPIREF
+ *      (chapter "Character string primitives", "Using...",
+ *      "Drawing text"), but the example code given
+ *      there is buggy, because it must be "72" instead
+ *      of "720". #%(%!"õ!!.
+ *
+ *      So here we go. If you want to output text in,
+ *      say, "Courier" and 24 points (nominal point size),
+ *      select the font into your HPS and call
+ +          gpihSetPointSize(hps, 24)
+ *      and you're done. See gpihFindFont for a complete
+ *      example.
+ *
+ *      Call this function as many times as needed. This
+ *      consumes no resources at all.
+ *
+ *      This returns the return value of GpiSetCharBox.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL gpihSetPointSize(HPS hps,          // in: presentation space for output
+                      LONG lPointSize)  // in: desired nominal point size
+{
+    SIZEF   box;
+    LONG    alDevRes[2];
+    DevQueryCaps(GpiQueryDevice(hps),       // get the HDC from the HPS
+                 CAPS_HORIZONTAL_FONT_RES,
+                 2L,
+                 alDevRes);
+    box.cx = MAKEFIXED((lPointSize * alDevRes[0]) / 72, 0);
+    box.cy = MAKEFIXED((lPointSize * alDevRes[1]) / 72, 0);
+    return (GpiSetCharBox(hps, &box));
+}
+
+/*
+ *@@ gpihQueryLineSpacing:
+ *      this returns the optimal line spacing for text
+ *      output with the current HPS; this is computed
+ *      by evaluating those incredible FONTMETRICS.
+ *
+ *      This might be helpful if you write text to the
+ *      screen yourself and need the height of a text
+ *      line to advance to the next.
+ */
+
+LONG gpihQueryLineSpacing(HPS hps,
+                          PSZ pszText)      // in:  text to output
+{
+    FONTMETRICS fm;
+    POINTL aptlText[TXTBOX_COUNT];
+    GpiQueryTextBox(hps, strlen(pszText), pszText,
+                    TXTBOX_COUNT, (PPOINTL)&aptlText);
+
+    if (GpiQueryFontMetrics(hps, sizeof(FONTMETRICS), &fm))
+        return ( (  fm.lMaxBaselineExt     // max vertical font space
+                   +fm.lExternalLeading)    // space advised by font designer
+                 // * 12 / 10
+               );
+    else
+        return (15);
+}
+
+/*
+ *@@category: Helpers\PM helpers\GPI helpers\Bitmaps/Icons
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Bitmap helpers                                                 *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ gpihCreateMemPS:
+ *      creates a memory device context and presentation space so
+ *      that they are compatible with the screen device context and
+ *      presentation space. These are stored in *hdcMem and *hpsMem.
+ *
+ *      This is a one-shot function for the standard code that is
+ *      always needed when working with bitmaps in a memory device
+ *      context.
+ *
+ *      psizlPage must point to a SIZEL structure containing the
+ *      width and height for the memory PS. Specify the size of
+ *      the future bitmap here. Specify {0, 0} to get a PS with
+ *      the size of the full screen, which consumes quite a bit
+ *      of memory though.
+ *
+ *      Returns FALSE upon errors. In that case, both hdcMem and
+ *      hpsMem are set to NULLHANDLE.
+ *
+ *      To cleanup after this function has returned TRUE, use the
+ *      following:
+ +          GpiDestroyPS(hpsMem);
+ +          DevCloseDC(hdcMem);
+ *
+ *@@changed V0.9.3 (2000-05-18) [umoeller]: added psiszlPage
+ */
+
+BOOL gpihCreateMemPS(HAB hab,       // in: anchor block
+                     PSIZEL psizlPage, // in: width and height for mem PS
+                     HDC *hdcMem,   // out: memory DC or NULLHANDLE upon errors
+                     HPS *hpsMem)   // out: memory PS or NULLHANDLE upon errors
+{
+    BOOL brc = FALSE;
+    PSZ pszData[4] = { "Display", NULL, NULL, NULL };
+
+    // create new memory DC
+    if ((*hdcMem = DevOpenDC(hab,
+                             OD_MEMORY,      // create memory DC
+                             "*",            // token: do not take INI info
+                             4,              // item count in pszData
+                             (PDEVOPENDATA)pszData,
+                             NULLHANDLE)))    // compatible with screen
+    {
+        // memory DC created successfully:
+        // create compatible PS
+        if ((*hpsMem = GpiCreatePS(hab,
+                                   *hdcMem,      // HDC to associate HPS with (GPIA_ASSOC);
+                                                 // mandatory for GPIT_MICRO
+                                   psizlPage,    // is (0, 0) == screen size
+                                   PU_PELS       // presentation page units: pixels
+                                    | GPIA_ASSOC    // associate with hdcMem (req. for GPIT_MICRO)
+                                    | GPIT_MICRO)))  // micro presentation space
+            brc = TRUE;
+        else
+        {
+            // error (hpsMem == NULLHANDLE):
+            // close memory DC again
+            DevCloseDC(*hdcMem);
+            *hdcMem = NULLHANDLE;
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ gpihCreateBitmap:
+ *      creates a new bitmap for a given memory PS.
+ *      This bitmap will have the cPlanes and bitcount
+ *      which are found in the memory PS.
+ *      For all the mysterious other values, we use
+ *      fixed default values, this doesn't seem to hurt.
+ *
+ *      Note that the bitmap is _not_ yet selected into
+ *      the specified memory PS. You must call
+ +          GpiSetBitmap(hpsMem, hbm)
+ *      to do this.
+ *
+ *      Returns the bitmap handle or NULLHANDLE upon errors.
+ *
+ *@@changed V0.9.0 [umoeller]: function prototype changed to cx and cy
+ */
+
+HBITMAP gpihCreateBitmap(HPS hpsMem,        // in: memory DC
+                         ULONG cx,          // in: width of new bitmap
+                         ULONG cy)          // in: height of new bitmap
+{
+    HBITMAP hbm = NULLHANDLE;
+    LONG alData[2];
+    BITMAPINFOHEADER2 bih2;
+    PBITMAPINFO2 pbmi = NULL;
+
+    // determine the device's plane/bit-count format;
+    // alData[0] then has cPlanes,
+    // alData[1] has cBitCount
+    if (GpiQueryDeviceBitmapFormats(hpsMem, 2, alData))
+    {
+        // set up the BITMAPINFOHEADER2 and BITMAPINFO2 structures
+        bih2.cbFix = (ULONG)sizeof(BITMAPINFOHEADER2);
+        bih2.cx = cx; // (prcl->xRight - prcl->xLeft);       changed V0.9.0
+        bih2.cy = cy; // (prcl->yTop - prcl->yBottom);       changed V0.9.0
+        bih2.cPlanes = alData[0];
+        bih2.cBitCount = alData[1];
+        bih2.ulCompression = BCA_UNCOMP;
+        bih2.cbImage = (    (   (bih2.cx
+                                    * (1 << bih2.cPlanes)
+                                    * (1 << bih2.cBitCount)
+                                ) + 31
+                            ) / 32
+                       ) * bih2.cy;
+        bih2.cxResolution = 70;
+        bih2.cyResolution = 70;
+        bih2.cclrUsed = 2;
+        bih2.cclrImportant = 0;
+        bih2.usUnits = BRU_METRIC;  // measure units for cxResolution/cyResolution: pels per meter
+        bih2.usReserved = 0;
+        bih2.usRecording = BRA_BOTTOMUP;        // scan lines are bottom to top (default)
+        bih2.usRendering = BRH_NOTHALFTONED;    // other algorithms aren't documented anyways
+        bih2.cSize1 = 0;            // parameter for halftoning (undocumented anyways)
+        bih2.cSize2 = 0;            // parameter for halftoning (undocumented anyways)
+        bih2.ulColorEncoding = BCE_RGB;     // only possible value
+        bih2.ulIdentifier = 0;              // application-specific data
+
+        // allocate memory for info header
+        if (DosAllocMem((PPVOID)&pbmi,
+                        sizeof(BITMAPINFO2) +
+                            (sizeof(RGB2)
+                                * (1 << bih2.cPlanes)
+                                * (1 << bih2.cBitCount)
+                            ),
+                        PAG_COMMIT | PAG_READ | PAG_WRITE)
+            == NO_ERROR)
+        {
+            pbmi->cbFix = bih2.cbFix;
+            pbmi->cx = bih2.cx;
+            pbmi->cy = bih2.cy;
+            pbmi->cPlanes = bih2.cPlanes;
+            pbmi->cBitCount = bih2.cBitCount;
+            pbmi->ulCompression = BCA_UNCOMP;
+            pbmi->cbImage = ((bih2.cx+31)/32) * bih2.cy;
+            pbmi->cxResolution = 70;
+            pbmi->cyResolution = 70;
+            pbmi->cclrUsed = 2;
+            pbmi->cclrImportant = 0;
+            pbmi->usUnits = BRU_METRIC;
+            pbmi->usReserved = 0;
+            pbmi->usRecording = BRA_BOTTOMUP;
+            pbmi->usRendering = BRH_NOTHALFTONED;
+            pbmi->cSize1 = 0;
+            pbmi->cSize2 = 0;
+            pbmi->ulColorEncoding = BCE_RGB;
+            pbmi->ulIdentifier = 0;
+
+            // create a bit map that is compatible with the display
+            hbm = GpiCreateBitmap(hpsMem,
+                                  &bih2,
+                                  FALSE,
+                                  NULL,
+                                  pbmi);
+
+            // free the memory we allocated previously; GPI has
+            // allocated all the resources it needs itself, so
+            // we can release this
+            DosFreeMem(pbmi);
+        }
+    }
+
+    return (hbm);
+}
+
+/*
+ *@@ gpihCreateBmpFromPS:
+ *      this creates a new bitmap and copies a screen rectangle
+ *      into it. Consider this a "screen capture" function.
+ *
+ *      The new bitmap (which is returned) is compatible with the
+ *      device associated with hpsScreen. This function calls
+ *      gpihCreateMemPS and gpihCreateBitmap to have it created.
+ *      The memory PS is only temporary and freed again.
+ *
+ *      This returns the handle of the new bitmap,
+ *      which can then be used for WinDrawBitmap and such, or
+ *      NULLHANDLE upon errors.
+ */
+
+HBITMAP gpihCreateBmpFromPS(HAB hab,        // in: anchor block
+                            HPS hpsScreen,  // in: screen PS to copy from
+                            PRECTL prcl)    // in: rectangle to copy
+{
+
+    /* To copy an image from a display screen to a bit map:
+      1. Associate the memory device context with a presentation space.
+      2. Create a bit map.
+      3. Select the bit map into the memory device context by calling GpiSetBitmap.
+      4. Determine the location (in device coordinates) of the image.
+      5. Call GpiBitBlt and copy the image to the bit map. */
+
+    HDC hdcMem;
+    HPS hpsMem;
+    HBITMAP hbm = NULLHANDLE;
+    POINTL aptl[3];
+
+    SIZEL szlPage = {0, 0};
+    if (gpihCreateMemPS(hab, &szlPage, &hdcMem, &hpsMem))
+    {
+        if ((hbm = gpihCreateBitmap(hpsMem,
+                                    prcl->xRight - prcl->xLeft,
+                                    prcl->yTop - prcl->yBottom)))
+        {
+            // Associate the bit map and the memory presentation space.
+            if (GpiSetBitmap(hpsMem, hbm)
+                    != HBM_ERROR)
+            {
+                // Copy the screen to the bit map.
+                aptl[0].x = 0;              // lower-left corner of destination rectangle
+                aptl[0].y = 0;
+                aptl[1].x = prcl->xRight;   // upper-right corner for both
+                aptl[1].y = prcl->yTop;
+                aptl[2].x = prcl->xLeft;    // lower-left corner of source rectangle
+                aptl[2].y = prcl->yBottom;
+
+                if (GpiBitBlt(hpsMem, hpsScreen,
+                              sizeof(aptl) / sizeof(POINTL), // Number of points in aptl
+                              aptl,
+                              ROP_SRCCOPY,
+                              BBO_IGNORE)
+                        == GPI_ERROR)
+                {
+                    // error during bitblt:
+                    GpiDeleteBitmap(hbm);
+                    hbm = NULLHANDLE; // for return code
+                }
+            } else {
+                // error selecting bitmap for hpsMem:
+                GpiDeleteBitmap(hbm);
+                hbm = NULLHANDLE; // for return code
+            }
+        }
+
+        GpiDestroyPS(hpsMem);
+        DevCloseDC(hdcMem);
+    } // end if (hdcMem = DevOpenDC())
+
+    return (hbm);
+}
+
+/*
+ *@@ gpihCreateHalftonedBitmap:
+ *      this creates a half-toned copy of the
+ *      input bitmap by doing the following:
+ *
+ *      1)  create a new bitmap with the size of hbmSource;
+ *      2)  copy hbmSource to the new bitmap (using GpiWCBitBlt);
+ *      3)  overpaint every other pixel with lColorGray.
+ *
+ *      Note that the memory DC is switched to RGB mode, so
+ *      lColorGray better be an RGB color.
+ *
+ *      Note: hbmSource must _not_ be selected into any device
+ *      context, or otherwise GpiWCBitBlt will fail.
+ *
+ *      This returns the new bitmap or NULLHANDLE upon errors.
+ *
+ *@added V0.9.0
+ */
+
+HBITMAP gpihCreateHalftonedBitmap(HAB hab,              // in: anchor block
+                                  HBITMAP hbmSource,    // in: source bitmap
+                                  LONG lColorGray)      // in: color used for gray
+{
+    HBITMAP hbmReturn = NULLHANDLE;
+
+    HDC     hdcMem;
+    HPS     hpsMem;
+    BITMAPINFOHEADER2 bmi;
+    // RECTL   rclSource;
+
+    if (hbmSource)
+    {
+        SIZEL szlPage;
+        // query bitmap info
+        bmi.cbFix = sizeof(bmi);
+        GpiQueryBitmapInfoHeader(hbmSource, &bmi);
+
+        szlPage.cx = bmi.cx;
+        szlPage.cy = bmi.cy;
+        if (gpihCreateMemPS(hab, &szlPage, &hdcMem, &hpsMem))
+        {
+            if ((hbmReturn = gpihCreateBitmap(hpsMem,
+                                              bmi.cx,
+                                              bmi.cy)))
+            {
+                if (GpiSetBitmap(hpsMem, hbmReturn) != HBM_ERROR)
+                {
+                    POINTL  aptl[4];
+
+                    // step 1: copy bitmap
+                    memset(aptl, 0, sizeof(POINTL) * 4);
+                    // aptl[0]: target bottom-left, is all 0
+                    // aptl[1]: target top-right (inclusive!)
+                    aptl[1].x = bmi.cx - 1;
+                    aptl[1].y = bmi.cy - 1;
+                    // aptl[2]: source bottom-left, is all 0
+
+                    // aptl[3]: source top-right (exclusive!)
+                    aptl[3].x = bmi.cx;
+                    aptl[3].y = bmi.cy;
+                    GpiWCBitBlt(hpsMem,     // target HPS (bmp selected)
+                                hbmSource,
+                                4L,             // must always be 4
+                                &aptl[0],       // points array
+                                ROP_SRCCOPY,
+                                BBO_IGNORE);
+                                        // doesn't matter here, because we're not stretching
+
+                    // step 2: overpaint bitmap
+                    // with half-toned pattern
+
+                    gpihSwitchToRGB(hpsMem);
+
+                    GpiMove(hpsMem, &aptl[0]);  // still 0, 0
+                    aptl[0].x = bmi.cx - 1;
+                    aptl[0].y = bmi.cy - 1;
+                    GpiSetColor(hpsMem, lColorGray);
+                    GpiSetPattern(hpsMem, PATSYM_HALFTONE);
+                    GpiBox(hpsMem,
+                           DRO_FILL, // interior only
+                           &aptl[0],
+                           0, 0);    // no corner rounding
+
+                    // unselect bitmap
+                    GpiSetBitmap(hpsMem, NULLHANDLE);
+                } // end if (GpiSetBitmap(hpsMem, hbmReturn) != HBM_ERROR)
+                else
+                {
+                    // error selecting bitmap:
+                    GpiDeleteBitmap(hbmReturn);
+                    hbmReturn = NULLHANDLE;
+                }
+            } // end if (hbmReturn = gpihCreateBitmap...
+
+            GpiDestroyPS(hpsMem);
+            DevCloseDC(hdcMem);
+        } // end if (gpihCreateMemPS(hab, &hdcMem, &hpsMem))
+    } // end if (hbmSource)
+
+    return (hbmReturn);
+}
+
+/*
+ *@@ gpihLoadBitmapFile:
+ *      this loads the specified bitmap file into
+ *      the given HPS. Note that the bitmap is _not_
+ *      yet selected into the HPS.
+ *
+ *      This function can currently only handle OS/2 1.3
+ *      bitmaps.
+ *
+ *      Returns the new bitmap handle or NULL upon errors
+ *      (e.g. if an OS/2 2.0 bitmap was accessed).
+ *
+ *      In the latter case, *pulError is set to one of
+ *      the following:
+ *      --  -1:      file not found
+ *      --  -2:      malloc failed
+ *      --  -3:      the bitmap data could not be read (fopen failed)
+ *      --  -4:      file format not recognized (maybe OS/2 2.0 bitmap)
+ *      --  -5:      GpiCreateBitmap error (maybe file corrupt)
+ *
+ *@@changed V0.9.4 (2000-08-03) [umoeller]: this didn't return NULLHANDLE on errors
+ */
+
+HBITMAP gpihLoadBitmapFile(HPS hps,             // in: HPS for bmp
+                           PSZ pszBmpFile,      // in: bitmap filename
+                           PULONG pulError)     // out: error code if FALSE is returned
+{
+    HBITMAP hbm = NULLHANDLE;
+    PBITMAPFILEHEADER2  pbfh;
+
+    struct stat st;
+    PBYTE       pBmpData;
+    FILE        *BmpFile;
+
+    if (stat(pszBmpFile, &st) == 0)
+    {
+
+        if ((pBmpData = (PBYTE)malloc(st.st_size)))
+        {
+            // open bmp file
+            if ((BmpFile = fopen(pszBmpFile, "rb")))
+            {
+                // read bmp data
+                fread(pBmpData, 1, st.st_size, BmpFile);
+                fclose(BmpFile);
+
+                // check bitmap magic codes
+                if (pBmpData[0] == 'B' && pBmpData[1] == 'M')
+                {
+                    pbfh = (PBITMAPFILEHEADER2)pBmpData;
+                    hbm = GpiCreateBitmap(hps,
+                                          &pbfh->bmp2,
+                                          CBM_INIT,
+                                          (pBmpData + pbfh->offBits),
+                                          (PBITMAPINFO2)&pbfh->bmp2);
+
+                    if (hbm == NULLHANDLE)
+                    {
+                        if (pulError)
+                        *pulError = -5;
+                    }
+                }
+                else if (pulError)
+                        *pulError = -4;
+
+            }
+            else if (pulError)
+                    *pulError = -3;
+
+            free(pBmpData);
+        }
+        else if (pulError)
+                *pulError = -2;
+    }
+    else if (pulError)
+            *pulError = -1;
+
+    return (hbm);
+}
+
+/*
+ *@@ gpihStretchBitmap:
+ *      this copies hbmSource to the bitmap selected
+ *      into hpsTarget, which must be a memory PS.
+ *
+ *      The source size is the whole size of hbmSource,
+ *      the target size is specified in prclTarget
+ *      (which is exclusive, meaning that the top right
+ *      corner of that rectangle lies _outside_ the target).
+ *
+ *      This uses GpiWCBitBlt to stretch the bitmap.
+ *      hbmSource therefore must _not_ be selected
+ *      into any presentation space, or GpiWCBitBlt will
+ *      fail.
+ *
+ *      If (fPropotional == TRUE), the target size is
+ *      modified so that the proportions of the bitmap
+ *      are preserved. The bitmap data will then be
+ *      copied to a subrectangle of the target bitmap:
+ *      there will be extra space either to the left
+ *      and right of the bitmap data or to the bottom
+ *      and top.
+ *      The outside areas of the target bitmap are
+ *      not changed then, so you might want to fill
+ *      the bitmap with some color first.
+ *
+ *      This returns the return value of GpiWCBitBlt,
+ *      which can be:
+ *      --  GPI_OK
+ *      --  GPI_HITS: correlate hits
+ *      --  GPI_ERROR: error occured (probably either hbmSource not free
+ *                     or no bitmap selected into hpsTarget)
+ *
+ *@added V0.9.0
+ */
+
+LONG gpihStretchBitmap(HPS hpsTarget,       // in: memory PS to copy bitmap to
+                       HBITMAP hbmSource,   // in: bitmap to be copied into hpsTarget (must be free)
+                       PRECTL prclSource,   // in: source rectangle -- if NULL, use size of bitmap
+                       PRECTL prclTarget,   // in: target rectangle (req.)
+                       BOOL fProportional)  // in: preserve proportions when stretching?
+{
+    LONG                lHits = 0;
+    BITMAPINFOHEADER2   bih2;
+    POINTL              aptl[4];
+    BOOL                fCalculated = FALSE;
+
+    memset(aptl, 0, sizeof(POINTL) * 4);
+
+    bih2.cbFix = sizeof(bih2);
+    GpiQueryBitmapInfoHeader(hbmSource,
+                             &bih2);
+
+    // aptl[2]: source bottom-left, is all 0
+    // aptl[3]: source top-right (exclusive!)
+    aptl[3].x = bih2.cx;
+    aptl[3].y = bih2.cy;
+
+    if (fProportional)
+    {
+        // proportional mode:
+
+        // 1) find out whether cx or cy is too
+        // large
+
+        ULONG ulPropSource = (bih2.cx * 1000)
+                                    / bih2.cy;
+                // e.g. if the bmp is 200 x 100, we now have 2000
+        ULONG ulPropTarget = ((prclTarget->xRight - prclTarget->xLeft) * 1000)
+                                    / (prclTarget->yTop - prclTarget->yBottom);
+                // case 1: if prclTarget is 300 x 100, we now have 3000 (> ulPropSource)
+                // case 2: if prclTarget is 150 x 100, we now have 1500 (< ulPropSource)
+
+        // case 1:
+        if (ulPropTarget > ulPropSource)
+        {
+            // prclTarget is too wide (horizontally):
+            // decrease width, keep height
+
+            ULONG cx = (prclTarget->xRight - prclTarget->xLeft);
+            ULONG cxNew = (cx * ulPropSource) / ulPropTarget;
+
+            // aptl[0]: target bottom-left
+            // move left right (towards center)
+            aptl[0].x = prclTarget->xLeft + ((cx - cxNew) / 2);
+            aptl[0].y = prclTarget->yBottom;
+
+            // aptl[1]: target top-right (inclusive!)
+            aptl[1].x = aptl[0].x + cxNew;
+            aptl[1].y = prclTarget->yTop;
+
+            fCalculated = TRUE;
+        }
+        else
+        {
+            // prclTarget is too high (vertically):
+            // keep width, decrease height
+
+            ULONG cy = (prclTarget->yTop - prclTarget->yBottom);
+            ULONG cyNew = (cy * ulPropTarget) / ulPropSource;
+
+            // aptl[0]: target bottom-left
+            aptl[0].x = prclTarget->xLeft;
+            // move bottom up (towards center)
+            aptl[0].y = prclTarget->yBottom + ((cy - cyNew) / 2);
+
+            // aptl[1]: target top-right (inclusive!)
+            aptl[1].x = prclTarget->xRight;
+            aptl[1].y = aptl[0].y + cyNew;
+                    // (prclTarget->yTop * ulPropSource) / ulPropTarget;
+
+            fCalculated = TRUE;
+        }
+    } // end if (pa->ulFlags & ANF_PROPORTIONAL)
+
+    if (!fCalculated)
+    {
+        // non-proportional mode or equal proportions:
+        // stretch to whole size of prclTarget
+
+        // aptl[0]: target bottom-left
+        aptl[0].x = prclTarget->xLeft;
+        aptl[0].y = prclTarget->yBottom;
+        // aptl[1]: target top-right (inclusive!)
+        aptl[1].x = prclTarget->xRight;
+        aptl[1].y = prclTarget->yTop;
+    }
+
+    lHits = GpiWCBitBlt(hpsTarget,     // target HPS (bmp selected)
+                        hbmSource,
+                        4L,             // must always be 4
+                        &aptl[0],       // points array
+                        ROP_SRCCOPY,
+                        BBO_IGNORE);
+                                // ignore eliminated rows or
+                                // columns; useful for color
+
+    return (lHits);
+}
+
+/*
+ *@@ gpihIcon2Bitmap:
+ *      this paints the given icon/pointer into
+ *      a bitmap.
+ *
+ *      Returns FALSE upon errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL gpihIcon2Bitmap(HPS hpsMem,         // in: target memory PS with bitmap selected into it
+                     HPOINTER hptr,      // in: source icon
+                     LONG lBkgndColor,   // in: background color for transparent areas
+                     ULONG ulIconSize)   // in: icon size (should be the value of WinQuerySysValue(HWND_DESKTOP, SV_CXICON))
+{
+    BOOL        brc = FALSE;
+    POINTERINFO pi;
+
+    // Each icon consists of two (really three)
+    // bitmaps, which are stored in the POINTERINFO
+    // structure:
+    //   pi.hbmColor    is the actual bitmap to be
+    //                  drawn. The parts that are
+    //                  to be transparent or inverted
+    //                  are black in this image.
+    //   pi.hbmPointer  has twice the height of
+    //                  hbmColor. The upper bitmap
+    //                  contains an XOR mask (for
+    //                  inverting parts), the lower
+    //                  bitmap an AND mask (for
+    //                  transparent parts).
+    if (WinQueryPointerInfo(hptr, &pi))
+    {
+        POINTL  aptl[4];
+        memset(aptl, 0, sizeof(POINTL) * 4);
+
+        // aptl[0]: target bottom-left, is all 0
+
+        // aptl[1]: target top-right (inclusive!)
+        aptl[1].x = ulIconSize;
+        aptl[1].y = ulIconSize;
+
+        // aptl[2]: source bottom-left, is all 0
+
+        // aptl[3]: source top-right (exclusive!)
+        aptl[3].x = ulIconSize + 1;
+        aptl[3].y = ulIconSize + 1;
+
+        GpiSetColor(hpsMem, CLR_WHITE);
+        GpiSetBackColor(hpsMem, CLR_BLACK);
+
+        // GpiErase(hpsMem);
+
+        // work on the AND image
+        GpiWCBitBlt(hpsMem,
+                    pi.hbmPointer,
+                    4L,         // must always be 4
+                    &aptl[0],   // point array
+                    ROP_SRCAND,   // source AND target
+                    BBO_OR);
+
+        // paint the real image
+        if (pi.hbmColor)
+            GpiWCBitBlt(hpsMem,
+                        pi.hbmColor,
+                        4L,         // must always be 4
+                        &aptl[0],   // point array
+                        ROP_SRCPAINT,    // source OR target
+                        BBO_OR);
+
+        GpiSetColor(hpsMem, lBkgndColor);
+        // work on the XOR image
+        aptl[2].y = ulIconSize;
+        aptl[3].y = (ulIconSize * 2) + 1;
+        GpiWCBitBlt(hpsMem,
+                    pi.hbmPointer,
+                    4L,         // must always be 4
+                    &aptl[0],   // point array
+                    ROP_SRCINVERT,
+                    BBO_OR);
+
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
Index: /trunk/src/helpers/level.c
===================================================================
--- /trunk/src/helpers/level.c	(revision 8)
+++ /trunk/src/helpers/level.c	(revision 8)
@@ -0,0 +1,841 @@
+
+/*
+ *@@sourcefile level.c:
+ *      contains helper functions for querying SYSLEVEL files.
+ *
+ *      This is all new with V0.9.2.
+ *
+ *      Function prefixes:
+ *      --  lvl*    syslevel helpers
+ *
+ *@@added V0.9.2 (2000-03-08) [umoeller]
+ *@@header "helpers\level.h"
+ */
+
+/*
+ *      Copyright (C) 1994 Martin Lafaix (EDM/2 2-05).
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSFILEMGR
+#define INCL_NOPMAPI
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "setup.h"
+
+#define SYSLEVEL_PRIVATE
+#include "helpers\level.h"
+
+// redefine the FIELDOFFSET macro; the one
+// in the OS/2 header files doesn't work with C++
+#undef FIELDOFFSET
+#define FIELDOFFSET(type, field)    ((ULONG)&(((type *)0)->field))
+
+/*
+ *@@category: Helpers\Control program helpers\SYSLEVEL parsing
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Interface                                                      *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ lvlOpenLevelFile:
+ *      finds, opens, or creates a SYSLEVEL file. *phFile
+ *      receives the DosOpen file handle, which can then be
+ *      passed to lvlQueryLevelFileData and such. Alternatively,
+ *      you can use DosOpen directly.
+ *
+ *      ulOpenMode can be:
+ *
+ *      -- OLF_OPEN:
+ *              open the specified file. The default behavior is to scan
+ *              the current disk, starting from the current directory.
+ *              It returns the first SYSLEVEL file whose extension
+ *              matches pszName. You can override this by
+ *              combining OLF_OPEN with OLF_SCANDISK or
+ *              OLF_CHECKID.
+ *
+ *      -- OLF_SCANDISKS:
+ *              scans all disks (starting from C:). Use this flag to
+ *              modify the default OLF_OPEN behavior.
+ *
+ *      -- OLF_CHECKID:
+ *              finds file(s) whose ID matches the specified one. Use
+ *              this flag to override the default OLF_OPEN behavior.
+ *
+ *      -- OLF_CREATE:
+ *              creates the specified file in the current directory. A
+ *              valid pszName and ID should be provided.
+ */
+
+APIRET lvlOpenLevelFile(PSZ pszName,        // in: syslevel file name extension
+                        PHFILE phFile,      // out: Dos file handle
+                        ULONG ulOpenMode,   // in: OLF_* flags
+                        PSZ pszCID)         // in: if OLF_CHECKID is specified, the ID
+{
+    APIRET rc = 0;
+    ULONG ulAction;
+    CHAR achFileName[CCHMAXPATHCOMP];
+
+    switch (ulOpenMode)
+    {
+        case OLF_OPEN:
+        case OLF_OPEN | OLF_CHECKID:
+            if (pszName || pszCID)
+            {
+                rc = _findsyslevelfile(pszName, pszCID, achFileName);
+
+                if (rc == 18)
+                    rc = _searchsubdirs(pszName, pszCID, achFileName);
+
+                if (rc == 0)
+                    rc = DosOpen(achFileName,
+                                 phFile,
+                                 &ulAction,
+                                 0,
+                                 FILE_NORMAL,
+                                 FILE_OPEN,
+                                 OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYWRITE,
+                                 0);
+
+                if (rc == 18)
+                    rc = 2;
+            }
+            else
+                rc = ERROR_INVALID_PARAMETER;
+            break;
+
+        case OLF_SCANDISKS:
+        case OLF_SCANDISKS | OLF_CHECKID:
+        case OLF_SCANDISKS | OLF_OPEN:
+        case OLF_SCANDISKS | OLF_OPEN | OLF_CHECKID:
+            if (pszName || pszCID)
+            {
+                ULONG ulDrive, ulDriveMap, ulCurrent, ulFound = 0;
+
+                rc = DosQueryCurrentDisk(&ulDrive, &ulDriveMap);
+                ulCurrent = _firstdrive(ulDriveMap);
+
+                while (ulCurrent && !ulFound)
+                {
+                    rc = _findsyslevelfile(pszName, pszCID, achFileName);
+                    if (rc == 18)
+                        rc = _searchsubdirs(pszName, pszCID, achFileName);
+
+                    if (rc == 0)
+                    {
+                        ulFound = 1;
+
+                        rc = DosOpen(achFileName,
+                                     phFile,
+                                     &ulAction,
+                                     0,
+                                     FILE_NORMAL,
+                                     FILE_OPEN,
+                                     OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYWRITE,
+                                     0);
+                    }
+                    else
+                        ulCurrent = _nextdrive(ulDriveMap, ulCurrent);
+                }
+
+                DosSetDefaultDisk(ulDrive);
+                if (ulFound == 0)
+                    rc = 2;
+            }
+            else
+                rc = ERROR_INVALID_PARAMETER;
+            break;
+
+        case OLF_CREATE:
+            if (pszName && pszCID)
+            {
+                SYSLEVELHEADER slh;
+                SYSLEVELDATA sld;
+
+                strcat(strcpy(achFileName, "syslevel."), pszName);
+
+                rc = DosOpen(achFileName,
+                             phFile,
+                             &ulAction,
+                             0,
+                             FILE_NORMAL,
+                             FILE_CREATE,
+                             OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYWRITE,
+                             0);
+
+                slh.h_magic[0] = slh.h_magic[1] = 0xFF;
+                strcpy((PSZ)slh.h_name, "SYSLEVEL");
+                for (ulAction = 0; ulAction < sizeof(slh.h_reserved1); ulAction++)
+                    slh.h_reserved1[ulAction] = 0;
+                slh.h_updated = 0;
+                for (ulAction = 0; ulAction < sizeof(slh.h_reserved2); ulAction++)
+                    slh.h_reserved2[ulAction] = 0;
+                slh.h_data = sizeof(SYSLEVELHEADER);
+
+                if (rc == 0)
+                    rc = _writesyslevelheader(*phFile, &slh);
+
+                memset(&sld, 0, sizeof(sld));
+                sld.d_kind[0] = SLK_STANDARD;
+                sld.d_reserved3 = sld.d_reserved4 = 0x5F;
+                memcpy(sld.d_cid, pszCID, sizeof(sld.d_cid));
+
+                if (rc == 0)
+                    rc = DosWrite(*phFile, &sld, sizeof(SYSLEVELDATA), &ulAction);
+            }
+            else
+                rc = ERROR_INVALID_PARAMETER;
+            break;
+        default:
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+    }
+
+    return rc;
+}
+
+/*
+ *@@ lvlQueryLevelFile:
+ *      finds all SYSLEVEL files on the system and
+ *      writes them to pBuffer.
+ */
+
+APIRET lvlQueryLevelFile(PSZ pszName,
+                         PSZ pszCID,
+                         PVOID pBuffer,
+                         ULONG ulBufSize)
+{
+    ULONG ulDrive, ulDriveMap, ulCurrent;
+    CHAR achFileName[CCHMAXPATHCOMP];
+    APIRET rc = DosQueryCurrentDisk(&ulDrive, &ulDriveMap);
+
+    strcat(strcpy(achFileName, "syslevel."),
+           (pszName == NULL) ? (PSZ) "???" : pszName);
+    memset(pBuffer, 0, ulBufSize);
+
+    ulCurrent = _firstdrive(ulDriveMap);
+
+    while (ulCurrent && (rc == 0))
+    {
+        rc = _findallsyslevelfiles(achFileName, pszCID, &pBuffer, &ulBufSize);
+
+        if (rc == 0)
+            ulCurrent = _nextdrive(ulDriveMap, ulCurrent);
+    }
+
+    if (rc == 0)
+        rc = DosSetDefaultDisk(ulDrive);
+
+    return rc;
+}
+
+/*
+ *@@ lvlQueryLevelFileData:
+ *      parses syslevel data from a SYSLEVEL file previously
+ *      opened with lvlOpenLevelFile. Alternatively, you can
+ *      open the SYSLEVEL file directly using DosOpen.
+ *
+ *      ulWhat can be:
+ *
+ *      -- QLD_MAJORVERSION
+ *              Query (or update) the major version field. It's a
+ *              one-character field. It should be in the range '0'-'9'.
+ *              The value is placed in (or taken from) the first
+ *              character of pBuffer. (Buffer size should be at least 1.)
+ *
+ *      -- QLD_MINORVERSION
+ *              Query (or update) the minor version field. It's a
+ *              two-character field. It should be in range '00'-'99'. The
+ *              value is placed in (or taken from) the first two chars
+ *              of pBuffer. (Buffer size should be at least 2.)
+ *
+ *      -- QLD_REVISION
+ *              Query (or update) the revision field. It's should fit in
+ *              a character. If it's '0', there's no revision available. It
+ *              can be a letter as well as a digit. The value is placed
+ *              in (or taken from) the first character of pBuffer.
+ *              (Buffer size should be at least 1.)
+ *
+ *      -- QLD_KIND
+ *              Query (or update) the kind field. The value is placed
+ *              in (or taken from) the first character of *pBuffer.
+ *              (Buffer size should be at least 1.)
+ *
+ *      -- QLD_CURRENTCSD
+ *              Query (or update) the current CSD level (when you
+ *              update this field, its old value is copied to the old
+ *              CSD level field). It's a seven-character field, and it
+ *              does not have to be null-terminated. The value is
+ *              placed in (or taken from) the first seven characters of
+ *              pBuffer. (Buffer size should be at least 7.)
+ *
+ *      -- QLD_PREVIOUSCSD
+ *              Query the previous CSD level. You can't update this
+ *              field. The value is placed in the first seven chars of
+ *              pBuffer. (Buffer size should be at least 7.)
+ *              Note: CSD levels are not null-terminated. Be careful
+ *              when using such a returned value.
+ *
+ *      -- QLD_TITLE
+ *              Query (or update) the component title field. It's an
+ *              eighty-character string (required ending null
+ *              included). The value is placed in (or taken from) the
+ *              first eighty characters of pBuffer. On input, the buffer
+ *              size should be at least 80. On output, the buffer size
+ *              can exceed 80, but the written string is truncated
+ *              (and null-terminated) to eighty characters.
+ *
+ *      -- QLD_ID
+ *              Query (or update) the component ID field. It's a
+ *              nine-character field. It does not have to be
+ *              null-terminated. The value is placed in (or taken
+ *              from) the first nine characters of pBuffer. (Buffer size
+ *              should be at least 9.)
+ *              Note: IDs are not null-terminated. Be careful when
+ *              using such a returned value.
+ *
+ *      -- QLD_TYPE
+ *              Query (or update) the component type field.
+ */
+
+APIRET lvlQueryLevelFileData(HFILE hFile,       // in: file handle returned by lvlOpenLevelFile
+                             ULONG ulWhat,      // in: QLD* flags
+                             PVOID pBuffer,     // in/out: data
+                             ULONG ulBufSize,   // in: sizeof(*pBuffer)
+                             PULONG pulSize)    // out: size required if
+                                                // ERROR_INSUFFICIENT_BUFFER is returned;
+                                                // otherwise bytes written
+{
+    APIRET rc = _locatesysleveldata(hFile);
+    PSYSLEVELDATA psld = _allocsysleveldata(hFile);
+
+    if (rc == 0)
+        rc = pBuffer ? rc : ERROR_INVALID_PARAMETER;
+    if (rc == 0)
+        rc = _readsysleveldata(hFile, psld);
+
+    if (rc == 0)
+        switch (ulWhat)
+        {
+            case QLD_MAJORVERSION:
+                if (ulBufSize >= 1)
+                    _achBuffer[0] = '0' + (psld->d_version[0] >> 4);
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(1);
+                break;
+            case QLD_MINORVERSION:
+                if (ulBufSize >= 2)
+                {
+                    _achBuffer[0] = '0' + (psld->d_version[0] & 0x0F);
+                    _achBuffer[1] = '0' + psld->d_version[1];
+                }
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(2);
+                break;
+            case QLD_REVISION:
+                if (ulBufSize >= 1)
+                    _achBuffer[0] = '0' + psld->d_revision[0];
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(1);
+                break;
+            case QLD_KIND:
+                _getfield(d_kind);
+                break;
+            case QLD_CURRENTCSD:
+                _getfield(d_clevel);
+                break;
+            case QLD_PREVIOUSCSD:
+                _getfield(d_plevel);
+                break;
+            case QLD_TITLE:
+                _getfield(d_title);
+                break;
+            case QLD_ID:
+                _getfield(d_cid);
+                break;
+            case QLD_TYPE:
+                if (ulBufSize >= strlen((PSZ)psld->d_type))
+                    memcpy(pBuffer, psld->d_type, strlen((PSZ)psld->d_type));
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(strlen((PSZ)psld->d_type));
+                break;
+            default:
+                rc = ERROR_INVALID_PARAMETER;
+                break;
+        }
+
+    _freesysleveldata(psld);
+
+    return rc;
+}
+
+/*
+ *@@ lvlWriteLevelFileData:
+ *      writes data to a SYSLEVEL file.
+ */
+
+APIRET lvlWriteLevelFileData(HFILE hFile,
+                             ULONG ulWhat,
+                             PVOID pBuffer,
+                             ULONG ulBufSize,
+                             PULONG pulSize)
+{
+    APIRET rc = _locatesysleveldata(hFile);
+    PSYSLEVELDATA psld = _allocsysleveldata(hFile);
+
+    if (rc == 0)
+        rc = pBuffer ? rc : ERROR_INVALID_PARAMETER;
+    if (rc == 0)
+        rc = _readsysleveldata(hFile, psld);
+
+    if (rc == 0)
+        switch (ulWhat)
+        {
+            case QLD_MAJORVERSION:
+                if (ulBufSize >= 1)
+                {
+                    psld->d_version[0] &= 0x0F;
+                    psld->d_version[0] |= (_achBuffer[0] - '0') << 4;
+                }
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(1);
+                break;
+            case QLD_MINORVERSION:
+                if (ulBufSize >= 2)
+                {
+                    psld->d_version[0] &= 0xF0;
+                    psld->d_version[0] |= (_achBuffer[0] - '0') && 0x0F;
+                    psld->d_version[1] = _achBuffer[1] - '0';
+                }
+                else
+                    rc = ERROR_INSUFFICIENT_BUFFER;
+                _setsize(2);
+                break;
+            case QLD_REVISION:
+                _setfield(d_revision);
+                break;
+            case QLD_KIND:
+                _setfield(d_kind);
+                break;
+            case QLD_CURRENTCSD:
+                memcpy(psld->d_plevel, psld->d_clevel, sizeof(psld->d_plevel));
+                _setfield(d_clevel);
+                break;
+            case QLD_TITLE:
+                memcpy(psld->d_title, pBuffer, min(ulBufSize, sizeof(psld->d_title)));
+                _setsize(min(ulBufSize, sizeof(psld->d_title)));
+                break;
+            case QLD_ID:
+                _setfield(d_cid);
+                break;
+            case QLD_TYPE:
+                break;
+            default:
+                rc = ERROR_INVALID_PARAMETER;
+                break;
+        }
+
+    if (rc == 0)
+        rc = _locatesysleveldata(hFile);
+    if (rc == 0)
+        rc = _writesysleveldata(hFile, psld);
+
+    _freesysleveldata(psld);
+
+    return rc;
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Local helper functions                                         *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * _readsysleveldata:
+ *
+ */
+
+static APIRET _readsysleveldata(HFILE hFile,
+                                PSYSLEVELDATA psld)
+{
+    ULONG ulSize;
+
+    return DosRead(hFile, psld, sizeof(SYSLEVELDATA), &ulSize);
+}
+
+/*
+ * _writesysleveldata:
+ *
+ */
+
+static APIRET _writesysleveldata(HFILE hFile,
+                                 PSYSLEVELDATA psld)
+{
+    ULONG ulSize;
+
+    return DosWrite(hFile, psld, sizeof(SYSLEVELDATA), &ulSize);
+}
+
+/*
+ * _locatesysleveldata:
+ *
+ */
+
+static APIRET _locatesysleveldata(HFILE hFile)
+{
+    ULONG ulPos, ulData;
+    APIRET rc = DosSetFilePtr(hFile,
+                              FIELDOFFSET(SYSLEVELHEADER, h_data),
+                              FILE_BEGIN,
+                              &ulPos);
+
+    rc = rc ? rc : DosRead(hFile, &ulData, sizeof(ulData), &ulPos);
+
+    return rc ? rc : DosSetFilePtr(hFile, ulData, FILE_BEGIN, &ulPos);
+}
+
+/*
+ * _allocsysleveldata:
+ *
+ */
+
+static PSYSLEVELDATA _allocsysleveldata(HFILE hFile)
+{
+    return (PSYSLEVELDATA) malloc(sizeof(SYSLEVELDATA));
+}
+
+/*
+ * _freesysleveldata:
+ *
+ */
+
+static VOID _freesysleveldata(PSYSLEVELDATA psld)
+{
+    if (psld)
+        free(psld);
+}
+
+/*
+ * _writesyslevelheader:
+ *
+ */
+
+static APIRET _writesyslevelheader(HFILE hFile,
+                                   PSYSLEVELHEADER pslh)
+{
+    ULONG ulSize;
+
+    return DosWrite(hFile, pslh, sizeof(SYSLEVELHEADER), &ulSize);
+}
+
+/*
+ * _isasyslevelfile:
+ *
+ */
+
+static BOOL _isasyslevelfile(PSZ pszFile,
+                             PSZ pszCID)
+{
+    HFILE hFile;
+    ULONG ulAction;
+    SYSLEVELHEADER slh;
+    APIRET rc = DosOpen(pszFile,
+                        &hFile,
+                        &ulAction,
+                        0,
+                        FILE_NORMAL,
+                        FILE_OPEN,
+                        OPEN_ACCESS_READONLY | OPEN_SHARE_DENYWRITE,
+                        0);
+
+    if (rc == 0)
+        rc = DosRead(hFile, &slh, sizeof(slh), &ulAction);
+
+    ulAction = (slh.h_magic[0] == 0xFF) && (slh.h_magic[1] == 0xFF);
+    if (ulAction)
+        ulAction = strcmp((PSZ)slh.h_name, "SYSLEVEL") == 0;
+    if (ulAction)
+        if (pszCID)
+        {
+            CHAR achCID[9];
+            ULONG ulCID;
+
+            rc = lvlQueryLevelFileData(hFile, QLD_ID, achCID, 9, &ulCID);
+            ulAction = (strcmp(achCID, pszCID) == 0);
+        }
+
+    rc = DosClose(hFile);
+
+    return ulAction;
+}
+
+/*
+ * _getfullname:
+ *
+ */
+
+static APIRET _getfullname(PVOID pBuffer,
+                           PSZ pszFileName)
+{
+    ULONG ulDrive, ulDriveMap, ulBufSize = CCHMAXPATHCOMP;
+    APIRET rc = DosQueryCurrentDisk(&ulDrive, &ulDriveMap);
+
+    _achBuffer[0] = '@' + ulDrive;
+    _achBuffer[1] = ':';
+    _achBuffer[2] = '\\';
+    rc = DosQueryCurrentDir(0, _achBuffer + 3, &ulBufSize);
+    strcat(strcat((PSZ)pBuffer, "\\"), pszFileName);
+
+    return rc;
+}
+
+/*
+ * _findsyslevelfile:
+ *
+ */
+
+static APIRET _findsyslevelfile(PSZ pszName,
+                                PSZ pszCID,
+                                PVOID pBuffer)
+{
+    HDIR hDirHandle = HDIR_CREATE;
+    FILEFINDBUF3 ffb;
+    ULONG ulCount = 1, ulFound = 0;
+    CHAR achFileName[CCHMAXPATHCOMP] = "syslevel.";
+    APIRET rc = 0;
+
+    if (pszName)
+        strcat(achFileName, pszName);
+    else
+        strcat(achFileName, "???");
+
+    rc = DosFindFirst(achFileName,
+                      &hDirHandle,
+                      FILE_NORMAL,
+                      (PVOID) & ffb,
+                      sizeof(ffb),
+                      &ulCount,
+                      FIL_STANDARD);
+
+    while ((!ulFound) && (rc == 0))
+    {
+        if (_isasyslevelfile(ffb.achName, pszCID))
+        {
+            _getfullname(pBuffer, ffb.achName);
+            ulFound = 1;
+            break;
+        }
+
+        rc = DosFindNext(hDirHandle, (PVOID) & ffb, sizeof(ffb), &ulCount);
+    }
+
+    if ((rc == 0) || (rc == 18))
+        rc = DosFindClose(hDirHandle);
+
+    return ulFound ? 0 : 18;
+}
+
+/*
+ * _findallsyslevelfiles:
+ *
+ */
+
+static APIRET _findallsyslevelfiles(PSZ achFileName,
+                                    PSZ pszCID,
+                                    PPVOID ppBuffer,
+                                    PULONG pulBufSize)
+{
+    HDIR hDirHandle = HDIR_CREATE;
+    FILEFINDBUF3 ffb;
+    ULONG ulCount = 1, ulSize;
+    APIRET rc = DosFindFirst(achFileName,
+                             &hDirHandle,
+                             FILE_NORMAL,
+                             (PVOID) & ffb,
+                             sizeof(ffb),
+                             &ulCount,
+                             FIL_STANDARD);
+
+    while (rc == 0)
+    {
+        if (_isasyslevelfile(ffb.achName, pszCID))
+        {
+            CHAR achBuf[CCHMAXPATHCOMP];
+
+            _getfullname(achBuf, ffb.achName);
+            ulSize = strlen(achBuf);
+            if ((*pulBufSize) > ulSize)
+            {
+                *pulBufSize -= ulSize + 1;
+                memcpy(*ppBuffer, achBuf, ulSize);
+                *ppBuffer = ((CHAR *) (*ppBuffer)) + ulSize + 1;
+            }
+            else
+                rc = ERROR_INSUFFICIENT_BUFFER;
+        }
+
+        if (rc == 0)
+            rc = DosFindNext(hDirHandle, (PVOID) & ffb, sizeof(ffb), &ulCount);
+    }
+
+    if ((rc == 0) || (rc == 18))
+    {
+        rc = DosFindClose(hDirHandle);
+        if (rc == 6)
+            rc = 0;
+    }
+
+    hDirHandle = HDIR_CREATE;
+    if (rc == 0)
+        rc = DosFindFirst("*",
+                          &hDirHandle,
+                          MUST_HAVE_DIRECTORY,
+                          (PVOID) & ffb,
+                          sizeof(ffb),
+                          &ulCount,
+                          FIL_STANDARD);
+
+    while (rc == 0)
+    {
+        if (ffb.achName[0] == '.' &&
+            (ffb.achName[1] == 0 || (ffb.achName[1] == '.' && ffb.achName[2] == 0)))
+        {
+        }
+        else
+        {
+            rc = DosSetCurrentDir(ffb.achName);
+
+            if (rc == 0)
+                rc = _findallsyslevelfiles(achFileName, pszCID, ppBuffer, pulBufSize);
+
+            if ((rc == 0) || (rc == 18))
+                rc = DosSetCurrentDir("..");
+        }
+
+        if (rc == 0)
+            rc = DosFindNext(hDirHandle, (PVOID) & ffb, sizeof(ffb), &ulCount);
+    }
+
+    if ((rc == 0) || (rc == 18))
+    {
+        rc = DosFindClose(hDirHandle);
+        if (rc == 6)
+            rc = 0;
+    }
+
+    return rc;
+}
+
+/*
+ * _searchsubdirs:
+ *
+ */
+
+static APIRET _searchsubdirs(PSZ pszName,
+                             PSZ pszCID,
+                             PVOID pBuffer)
+{
+    HDIR hDirHandle = HDIR_CREATE;
+    FILEFINDBUF3 ffb;
+    ULONG ulCount = 1, ulFound = 0;
+    APIRET rc = DosFindFirst("*",
+                             &hDirHandle,
+                             MUST_HAVE_DIRECTORY,
+                             (PVOID) & ffb,
+                             sizeof(ffb),
+                             &ulCount,
+                             FIL_STANDARD);
+
+    while ((!ulFound) && (rc == 0))
+    {
+        if (ffb.achName[0] == '.' &&
+            (ffb.achName[1] == 0 || (ffb.achName[1] == '.' && ffb.achName[2] == 0)))
+        {
+        }
+        else
+        {
+            rc = DosSetCurrentDir(ffb.achName);
+
+            if (rc == 0)
+                rc = _findsyslevelfile(pszName, pszCID, pBuffer);
+
+            if (rc == 18)
+                rc = _searchsubdirs(pszName, pszCID, pBuffer);
+
+            if (rc == 0)
+                ulFound = 1;
+
+            if ((rc == 0) || (rc == 18))
+                rc = DosSetCurrentDir("..");
+        }
+
+        if ((!ulFound) && (rc == 0))
+            rc = DosFindNext(hDirHandle, (PVOID) & ffb, sizeof(ffb), &ulCount);
+    }
+
+    rc = DosFindClose(hDirHandle);
+
+    return ulFound ? 0 : 18;
+}
+
+/*
+ * _firstdrive:
+ *
+ */
+
+static ULONG _firstdrive(ULONG ulDriveMap)
+{
+    return _nextdrive(ulDriveMap, 2);
+}
+
+/*
+ * _nextdrive:
+ *
+ */
+
+static ULONG _nextdrive(ULONG ulDriveMap,
+                        ULONG ulCurrent)
+{
+    ULONG ulNext = ulCurrent + 1;
+
+    while (ulNext < 27)
+        if (ulDriveMap & (0x1 << (ulNext - 1)))
+            break;
+        else
+            ulNext++;
+
+    if (ulNext < 27)
+    {
+        DosSetDefaultDisk(ulNext);
+        DosSetCurrentDir("\\");
+    }
+
+    return (ulNext == 27) ? 0 : ulNext;
+}
Index: /trunk/src/helpers/linklist.c
===================================================================
--- /trunk/src/helpers/linklist.c	(revision 8)
+++ /trunk/src/helpers/linklist.c	(revision 8)
@@ -0,0 +1,998 @@
+
+/*
+ *@@sourcefile linklist.c:
+ *      contains helper functions for maintaining doubly-linked lists.
+ *      See explanations below.
+ *
+ *      This file is new with V0.81 and contains all the functions
+ *      that were in helpers.c previously.
+ *
+ *      Usage: All C programs; not OS/2-specific.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  lst*    linked list helper functions
+ *
+ *      <B>Usage:</B>
+ *
+ *      Each list item is stored in a LISTNODE structure, which in
+ *      turn has a pItemData field, which you can use to get your
+ *      data. So a typical list would be coded like this:
+ *
+ +          // fill the list
+ +          PLINKLIST pll = lstCreate(TRUE or FALSE);
+ +          while ...
+ +          {
+ +              PYOURDATA pYourData = ...
+ +              lstAppendItem(pll, pYourData);  // store the data in a new node
+ +          }
+ +
+ +          ...
+ +
+ +          // now iterate over the list
+ +          PLISTNODE pNode = lstQueryFirstNode(pll);
+ +          while (pNode)
+ +          {
+ +              PYOURDATA pYourData = (PYOURDATA)pNode->pItemData;
+ +              ...
+ +              pNode = pNode->pNext;
+ +          }
+ +
+ +          lstFree(pll);   // this is free the list items too, if TRUE had
+ +                          // been specified with lstCreate
+ *
+ *      If __XWPMEMDEBUG__ is defined, the lstCreate and lstAppendItem funcs will
+ *      automatically be replaced with lstCreateDebug and lstAppendItemDebug,
+ *      and mapper macros are defined in linklist.h.
+ *
+ *      Note: If you frequently need to search your data structures,
+ *      you might prefer the new balanced binary trees in tree.c which
+ *      have been added with V0.9.5.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\linklist.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\linklist.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\C helpers\Linked lists
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   List base functions                                            *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ lstInit:
+ *      this initializes a given linked list.
+ *      This is useful only if you have a static
+ *      LINKLIST structure in your sources and don't
+ *      use lstCreate/lstFree to have lists created
+ *      dynamically. In that case, use this function
+ *      as follows:
+ +
+ +          LINKLIST llWhatever;
+ +          lstInit(&llWhatever);
+ +          ...
+ +          lstClear(&llWhatever);
+ *
+ *      The list which is initialized here must be
+ *      cleaned up using lstClear.
+ *
+ *      If (fItemsFreeable == TRUE), free() will be
+ *      invoked on list items automatically in lstClear,
+ *      lstRemoveNode, and lstRemoveItem.
+ *      Set this to TRUE if you have created the
+ *      list items yourself. Set this to FALSE if
+ *      you're storing other objects, such as numbers
+ *      or other static items.
+ *
+ *      Note: You better call lstInit only once per list,
+ *      because we don't check here if the list
+ *      contains any data. This will lead to memory
+ *      leaks if called upon existing lists, because
+ *      we'll lose track of the allocated items.
+ */
+
+void lstInit(PLINKLIST pList,
+             BOOL fItemsFreeable)   // in: invoke free() on the data
+                                    // item pointers upon destruction?
+{
+    if (pList)
+    {
+        memset(pList, 0, sizeof(LINKLIST));
+        pList->ulMagic = LINKLISTMAGIC;      // set integrity check word
+        pList->fItemsFreeable = fItemsFreeable;
+    }
+}
+
+/*
+ *@@ lstClear:
+ *      this will delete all list items. As opposed to
+ *      lstFree, the "root" of the list will not be
+ *      freed so that items can be added later again.
+ *
+ *      This can be used on static lists initialized with
+ *      lstInit also.
+ *
+ *      If fItemsFreeable had been specified as TRUE
+ *      when the list was initialized (lstInit), free()
+ *      will be invoked on all data item pointers also.
+ *      See the remarks there.
+ *
+ *      Returns FALSE only upon errors, e.g. because
+ *      integrity checks failed.
+ */
+
+BOOL lstClear(PLINKLIST pList)
+{
+    BOOL brc = FALSE;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+        {
+            PLISTNODE  pNode = pList->pFirst,
+                       pNode2;
+
+            while (pNode)
+            {
+                if (pList->fItemsFreeable)
+                    if (pNode->pItemData)
+                        free(pNode->pItemData);
+                pNode2 = pNode->pNext;
+                free(pNode);
+                pNode = pNode2;
+            } // while (pNode);
+
+            lstInit(pList, pList->fItemsFreeable);
+            brc = TRUE;
+        }
+
+    return (brc);
+}
+
+#ifdef __XWPMEMDEBUG__
+
+/*
+ *@@ lstCreateDebug:
+ *      debug version of lstCreate, using _debug_malloc.
+ *
+ *      Calls to lstCreate are automatically mapped to
+ *      this function if __XWPMEMDEBUG__ is defined.
+ *      This makes sure that the source file and line stored
+ *      with the debug memory functions will be that of the
+ *      caller, not the ones in this file.
+ *
+ *@@added V0.9.1 (99-12-18) [umoeller]
+ */
+
+PLINKLIST lstCreateDebug(BOOL fItemsFreeable,
+                         const char *file,
+                         unsigned long line,
+                         const char *function)
+{
+    PLINKLIST pNewList = (PLINKLIST)memdMalloc(sizeof(LINKLIST),
+                                               file,
+                                               line,
+                                               function);
+    lstInit(pNewList, fItemsFreeable);
+    return (pNewList);
+}
+
+#else
+
+/*
+ *@@ lstCreate:
+ *      this creates a new linked list and initializes it
+ *      (using lstInit).
+ *      The LINKLIST structure which defines the "root" of
+ *      the list is returned. This must be passed to all
+ *      other list functions.
+ *
+ *      The list which is created here must be destroyed
+ *      using lstFree.
+ *
+ *      If (fItemsFreeable == TRUE), free() will be
+ *      invoked on list items automatically in lstFree,
+ *      lstRemoveNode, and lstRemoveItem.
+ *      Set this to TRUE if you have created the
+ *      list items yourself. Set this to FALSE if
+ *      you're storing other objects, such as numbers
+ *      or other static items.
+ *
+ *      Returns NULL upon errors.
+ *
+ *      <B>Usage:</B>
+ +          PLINKLIST pllWhatever = lstCreate();
+ +          ...
+ +          lstFree(pllWhatever);
+ */
+
+PLINKLIST lstCreate(BOOL fItemsFreeable)    // in: invoke free() on the data
+                                            // item pointers upon destruction?
+{
+    PLINKLIST pNewList = (PLINKLIST)malloc(sizeof(LINKLIST));
+    if (pNewList)
+        lstInit(pNewList, fItemsFreeable);
+    return (pNewList);
+}
+
+#endif      // __XWPMEMDEBUG__
+
+/*
+ *@@ lstFree:
+ *      this will delete all list items (by calling
+ *      lstClear) and the list "root" itself.
+ *      Returns TRUE if anything was deleted at all
+ *      (even if the list was empty)
+ *      or FALSE upon errors, e.g. because integrity
+ *      checks failed.
+ *
+ *      If fItemsFreeable had been specified as TRUE
+ *      when the list was created (lstCreate), free()
+ *      will be invoked on all data item pointers also.
+ *      See the remarks there.
+ *
+ *      This must only be used with dynamic lists created
+ *      with lstCreate.
+ */
+
+BOOL lstFree(PLINKLIST pList)
+{
+    BOOL brc = FALSE;
+
+    if (lstClear(pList))        // this checks for list integrity
+    {
+        // unset magic word; the pList pointer
+        // will point to invalid memory after
+        // freeing the list, and subsequent
+        // integrity checks must fail
+        pList->ulMagic = 0;
+        free(pList);
+
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ lstCountItems:
+ *      this will return the number of items
+ *      in the given linked list.
+ *      This returns 0 if the list is empty
+ *      or -1 if the list is invalid or corrupt.
+ */
+
+long lstCountItems(PLINKLIST pList)
+{
+    long lCount = -1;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+        {
+            lCount = pList->ulCount;
+        }
+
+    return (lCount);
+}
+
+/*
+ *@@ lstQueryFirstNode:
+ *      this returns the first node of the list,
+ *      or NULL if the list is empty. This is
+ *      preferred over getting the first directly
+ *      from the LINKLIST structure, because this
+ *      one checks for data integrity.
+ *
+ *      The item data can be queried from the node
+ *      as follows:
+ +          PLINKLIST pll = lstCreate();
+ +          ...  // add items here
+ +          PLISTNODE pNode = lstQueryFirstNode(pll);
+ +          PYOURDATA pData = pNode->pItemData;
+ *
+ *      You can iterate over the whole list like this:
+ +          PLISTNODE pNode = lstQueryFirstNode(pll);
+ +          while (pNode)
+ +          {
+ +              PYOURDATA pYourData = (PYOURDATA)pNode->pItemData;
+ +              ...
+ +              pNode = pNode->pNext;
+ +          }
+ */
+
+PLISTNODE lstQueryFirstNode(PLINKLIST pList)
+{
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+            return (pList->pFirst);
+
+    return (0);
+}
+
+/*
+ *@@ lstNodeFromIndex:
+ *      returns the data of list item with a given ulIndex
+ *      (counting from 0), or NULL if ulIndex is too large.
+ *
+ *      This traverses the whole list, so it's not terribly
+ *      fast. See lstQueryFirstNode for how to traverse
+ *      the list yourself.
+ *
+ *      Note: As opposed to lstItemFromIndex, this one
+ *      returns the LISTNODE structure.
+ */
+
+PLISTNODE lstNodeFromIndex(PLINKLIST pList,
+                           unsigned long ulIndex)
+{
+    PLISTNODE pNode = NULL;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+            if (pList->pFirst)
+            {
+                unsigned long ulCount = 0;
+                pNode = pList->pFirst;
+                for (ulCount = 0;
+                     ((pNode) && (ulCount < ulIndex));
+                     ulCount++)
+                {
+                    if (pNode->pNext)
+                        pNode = pNode->pNext;
+                    else
+                        pNode = NULL; // exit
+                }
+            }
+
+    return (pNode);
+}
+
+/*
+ *@@ lstNodeFromItem:
+ *      this finds the list node which has the given
+ *      data pointer or NULL if not found.
+ *
+ *      This traverses the whole list, so it's not terribly
+ *      fast. See lstQueryFirstNode for how to traverse
+ *      the list yourself.
+ */
+
+PLISTNODE lstNodeFromItem(PLINKLIST pList, void* pItemData)
+{
+    PLISTNODE pNode = 0,
+              pNodeFound = 0;
+
+    if (pList)
+        if (    (pList->ulMagic == LINKLISTMAGIC)
+             && (pItemData)
+           )
+        {
+            pNode = pList->pFirst;
+            while (pNode)
+            {
+                if (pNode->pItemData == pItemData)
+                {
+                    pNodeFound = pNode;
+                    break;
+                }
+
+                pNode = pNode->pNext;
+            }
+        }
+
+    return (pNodeFound);
+}
+
+/*
+ *@@ lstItemFromIndex:
+ *      returns the data of the list item with a given ulIndex
+ *      (counting from 0), or NULL if ulIndex is too large.
+ *
+ *      This traverses the whole list, so it's not terribly
+ *      fast. See lstQueryFirstNode for how to traverse
+ *      the list yourself.
+ *
+ *      Note: As opposed to lstNodeFromIndex, this one
+ *      returns the item data directly, not the LISTNODE
+ *      structure.
+ */
+
+void* lstItemFromIndex(PLINKLIST pList, unsigned long ulIndex)
+{
+    PLISTNODE pNode = lstNodeFromIndex(pList, ulIndex);
+    if (pNode)
+        return (pNode->pItemData);
+    else
+        return (0);
+}
+
+#ifdef __XWPMEMDEBUG__
+
+/*
+ *@@ lstAppendItemDebug:
+ *      debug version of lstAppendItem, using _debug_malloc.
+ *
+ *      Calls to lstAppendItem are automatically mapped to
+ *      this function if __XWPMEMDEBUG__ is defined.
+ *      This makes sure that the source file and line stored
+ *      with the debug memory functions will be that of the
+ *      caller, not the ones in this file.
+ *
+ *@@added V0.9.1 (99-12-18) [umoeller]
+ */
+
+PLISTNODE lstAppendItemDebug(PLINKLIST pList,
+                             void* pNewItemData,     // in: data to store in list node
+                             const char *file,
+                             unsigned long line,
+                             const char *function)
+{
+    PLISTNODE pNewNode = 0;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+        {
+            pNewNode = (PLISTNODE)memdMalloc(sizeof(LISTNODE), file, line, function);
+            if (pNewNode)
+            {
+                memset(pNewNode, 0, sizeof(LISTNODE));
+                pNewNode->pItemData = pNewItemData;
+
+                if (pList->pLast)
+                {
+                    // list is not empty: append to tail
+
+                    // 1) make last item point to new node
+                    pList->pLast->pNext = pNewNode;
+                    // 2) make new node point to last item
+                    pNewNode->pPrevious = pList->pLast;
+                    // 3) store new node as new last item
+                    pList->pLast = pNewNode;
+
+                    pList->ulCount++;
+                }
+                else
+                {
+                    // list is empty: insert as first
+                    pList->pFirst
+                        = pList->pLast
+                        = pNewNode;
+
+                    pList->ulCount = 1;
+                }
+            }
+         }
+
+    return (pNewNode);
+}
+
+#else
+
+/*
+ *@@ lstAppendItem:
+ *      this adds a new node to the tail of the list.
+ *      As opposed to lstInsertItemBefore, this is very
+ *      fast, since the list always keeps track of its last item.
+ *
+ *      This returns the LISTNODE of the new list item,
+ *      or NULL upon errors.
+ */
+
+PLISTNODE lstAppendItem(PLINKLIST pList,
+                        void* pNewItemData)     // in: data to store in list node
+{
+    PLISTNODE pNewNode = 0;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+        {
+            pNewNode = (PLISTNODE)malloc(sizeof(LISTNODE));
+            if (pNewNode)
+            {
+                memset(pNewNode, 0, sizeof(LISTNODE));
+                pNewNode->pItemData = pNewItemData;
+
+                if (pList->pLast)
+                {
+                    // list is not empty: append to tail
+
+                    // 1) make last item point to new node
+                    pList->pLast->pNext = pNewNode;
+                    // 2) make new node point to last item
+                    pNewNode->pPrevious = pList->pLast;
+                    // 3) store new node as new last item
+                    pList->pLast = pNewNode;
+
+                    pList->ulCount++;
+                }
+                else
+                {
+                    // list is empty: insert as first
+                    pList->pFirst
+                        = pList->pLast
+                        = pNewNode;
+
+                    pList->ulCount = 1;
+                }
+            }
+         }
+
+    return (pNewNode);
+}
+
+#endif // __XWPMEMDEBUG__
+
+/*
+ *@@ lstInsertItemBefore:
+ *      this inserts a new node to the list. As opposed to
+ *      lstAppendItem, the new node can be appended anywhere
+ *      in the list, that is, it will be appended BEFORE
+ *      the specified index lIndex.
+ *
+ *      So if ulIndex == 0, the new item will be made the first
+ *      item.
+ *
+ *      If ulIndex is larger than the item count on the list,
+ *      the new node will be appended at the tail of the list
+ *      (as with lstAppendItem).
+ *
+ *      This needs to traverse the list to find the indexed
+ *      item, so for larger ulIndex's this function is not
+ *      terribly fast.
+ *
+ *      This returns the LISTNODE of the new list item,
+ *      or NULL upon errors.
+ */
+
+PLISTNODE lstInsertItemBefore(PLINKLIST pList,
+                              void* pNewItemData,     // data to store in list node
+                              unsigned long ulIndex)
+{
+    PLISTNODE pNewNode = 0;
+
+    if (pList)
+        if (pList->ulMagic == LINKLISTMAGIC)
+        {
+            pNewNode = (PLISTNODE)malloc(sizeof(LISTNODE));
+            if (pNewNode)
+            {
+                memset(pNewNode, 0, sizeof(LISTNODE));
+                pNewNode->pItemData = pNewItemData;
+
+                if (ulIndex == 0)
+                {
+                    // insert at beginning:
+                    if (pList->pFirst)
+                        pList->pFirst->pPrevious = pNewNode;
+
+                    pNewNode->pNext = pList->pFirst;
+                    pNewNode->pPrevious = NULL;
+
+                    pList->pFirst = pNewNode;
+
+                    pList->ulCount++;
+                }
+                else
+                {
+                    // insert at a later position:
+                    PLISTNODE pNodeInsertAfter = lstNodeFromIndex(
+                                            pList,
+                                            (ulIndex-1));
+
+                    if (pNodeInsertAfter)
+                    {
+                        // 1) set pointers for new node
+                        pNewNode->pPrevious = pNodeInsertAfter;
+                        pNewNode->pNext = pNodeInsertAfter->pNext;
+
+                        // 2) adjust next item
+                        // so that it points to the new node
+                        if (pNodeInsertAfter->pNext)
+                            pNodeInsertAfter->pNext->pPrevious = pNewNode;
+
+                        // 3) adjust previous item
+                        // so that it points to the new node
+                        pNodeInsertAfter->pNext = pNewNode;
+
+                        // 4) adjust last item, if necessary
+                        if (pList->pLast == pNodeInsertAfter)
+                            pList->pLast = pNewNode;
+
+                        pList->ulCount++;
+                    }
+                    else
+                    {
+                        // item index too large: append instead
+                        free(pNewNode);
+                        pNewNode = lstAppendItem(pList, pNewItemData);
+                    }
+                }
+            }
+        }
+
+    return (pNewNode);
+}
+
+/*
+ *@@ lstRemoveNode:
+ *      this will remove a given pRemoveNode from
+ *      the given linked list.
+ *
+ *      To remove a node according to the item pointer,
+ *      use lstRemoveItem.
+ *      To get the node from a node index, use
+ *      lstNodeFromIndex.
+ *
+ *      Since the LISTNODE is known, this function
+ *      operates very fast.
+ *
+ *      If fItemsFreeable had been specified as TRUE
+ *      when the list was created (lstInit or lstCreate),
+ *      free() will be invoked on the data item pointer also.
+ *      See the remarks there.
+ *
+ *      Returns TRUE if successful, FALSE upon errors.
+ */
+
+BOOL lstRemoveNode(PLINKLIST pList, PLISTNODE pRemoveNode)
+{
+    BOOL fFound = FALSE;
+
+    if (pList)
+        if (    (pList->ulMagic == LINKLISTMAGIC)
+             && (pRemoveNode)
+           )
+        {
+            if (pList->pFirst == pRemoveNode)
+                // item to be removed is first: adjust first
+                pList->pFirst = pRemoveNode->pNext;     // can be NULL
+            if (pList->pLast == pRemoveNode)
+                // item to be removed is last: adjust last
+                pList->pLast = pRemoveNode->pPrevious;  // can be NULL
+
+            if (pRemoveNode->pPrevious)
+                // adjust previous item
+                pRemoveNode->pPrevious->pNext = pRemoveNode->pNext;
+
+            if (pRemoveNode->pNext)
+                // adjust next item
+                pRemoveNode->pNext->pPrevious = pRemoveNode->pPrevious;
+
+            // decrease list count
+            pList->ulCount--;
+
+            // free node data
+            if (pList->fItemsFreeable)
+                if (pRemoveNode->pItemData)
+                    free(pRemoveNode->pItemData);
+            // free node
+            free(pRemoveNode);
+
+            fFound = TRUE;
+        }
+
+    return (fFound);
+}
+
+/*
+ *@@ lstRemoveItem:
+ *      as opposed to lstRemoveNode, this removes a
+ *      node according to an item data pointer.
+ *
+ *      This needs to call lstNodeFromItem first and
+ *      then invokes lstRemoveNode, so this is a lot
+ *      slower.
+ *
+ *      If fItemsFreeable had been specified as TRUE
+ *      when the list was created (lstInit or lstCreate),
+ *      free() will be invoked on the data item pointer also.
+ *      See the remarks there.
+ *
+ *      Returns TRUE if successful, FALSE upon errors.
+ */
+
+BOOL lstRemoveItem(PLINKLIST pList, void* pRemoveItem)
+{
+    BOOL brc = FALSE;
+
+    PLISTNODE pNode = lstNodeFromItem(pList, pRemoveItem);
+
+    if (pNode)
+        brc = lstRemoveNode(pList, pNode);
+
+    return (brc);
+}
+
+/*
+ *@@ lstSwapItems:
+ *      this will swap the items pNode1 and pNode2.
+ *      This is used in the sort routines.
+ *
+ *      Note that it is not really the nodes that are swapped,
+ *      but only the data item pointers. This is a lot quicker
+ *      than what we did in versions < 0.9.0.
+ */
+
+BOOL lstSwapNodes(PLISTNODE pNode1,
+                  PLISTNODE pNode2)
+{
+    BOOL brc = FALSE;
+
+    if ( (pNode1) && (pNode2) )
+    {
+        void* pTemp = pNode1->pItemData;
+        pNode1->pItemData = pNode2->pItemData;
+        pNode2->pItemData = pTemp;
+
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   List sorting                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * lstQuickSort2:
+ *      helper routine for recursing during quicksort (lstQuickSort).
+ */
+
+void lstQuickSort2(PLINKLIST pList,
+                   PFNSORTLIST pfnSort,
+                   void* pStorage,
+                   long lLeft,
+                   long lRight)
+{
+    long ll = lLeft,
+         lr = lRight-1,
+         lPivot = lRight;
+
+    if (lRight > lLeft)
+    {
+        PLISTNODE   pNodeLeft = lstNodeFromIndex(pList, ll),
+                    pNodeRight = lstNodeFromIndex(pList, lr),
+                    pNodePivot = lstNodeFromIndex(pList, lPivot);
+
+        while (TRUE)
+        {
+            // compare left item data to pivot item data
+            while ( (*pfnSort)(pNodeLeft->pItemData,
+                               pNodePivot->pItemData,
+                               pStorage)
+                    < 0 )
+            {
+                ll++;
+                // advance to next
+                pNodeLeft = pNodeLeft->pNext;
+            }
+
+            // compare right item to pivot
+            while (     ( (*pfnSort)(pNodeRight->pItemData,
+                                     pNodePivot->pItemData,
+                                     pStorage)
+                           >= 0 )
+                    && (lr > ll)
+                  )
+            {
+                lr--;
+                // step to previous
+                pNodeRight = pNodeRight->pPrevious;
+            }
+
+            if (lr <= ll)
+                break;
+
+            // swap pNodeLeft and pNodeRight
+            lstSwapNodes(pNodeLeft, pNodeRight);
+        }
+
+        // swap pNodeLeft and pNodePivot
+        lstSwapNodes(pNodeLeft, pNodePivot);
+
+        // recurse!
+        lstQuickSort2(pList, pfnSort, pStorage, lLeft, ll-1);
+        lstQuickSort2(pList, pfnSort, pStorage, ll+1, lRight);
+    }
+}
+
+/*
+ *@@ lstQuickSort:
+ *      this will sort the given linked list using the
+ *      sort function pfnSort, which works similar to those of the
+ *      container sorts, i.e. it must be declared as a
+ +          SHORT fnSort(void* pItem1, void* pItem1, void* pStorage)
+ *
+ *      These sort functions need to return the following:
+ *      --      0:   pItem1 == pItem2
+ *      --     -1:   pItem1 <  pItem2
+ *      --     +1:   pItem1 >  pItem2
+ *
+ *      Note that the comparison function does not get the PLISTNODEs,
+ *      but the item data pointers instead.
+ *
+ *      The pStorage parameter will simply be passed to the comparison
+ *      function. This might be useful for additional data you might need.
+ *
+ *      This returns FALSE upon errors.
+ *
+ *      This function implements the "quick sort" algorithm, which is one
+ *      of the fastest sort algorithms known to mankind. However, this is
+ *      an "instable" sort algorithm, meaning that list items considered
+ *      equal by pfnSort do _not_ retain their position, but might be
+ *      changed. If you need these to remain where they are, use
+ *      lstBubbleSort, which is a lot slower though.
+ *
+ *      This calls lstSwapNodes to swap the list items.
+ */
+
+BOOL lstQuickSort(PLINKLIST pList,
+                  PFNSORTLIST pfnSort,
+                  void* pStorage)
+{
+    BOOL brc = FALSE;
+
+    if (pList)
+        if (    (pList->ulMagic == LINKLISTMAGIC)
+             && (pfnSort)
+           )
+        {
+            long lRight = lstCountItems(pList)-1;
+
+            lstQuickSort2(pList, pfnSort, pStorage,
+                        0,          // lLeft
+                        lRight);
+            brc = TRUE;
+        }
+
+    return (brc);
+}
+
+/*
+ *@@ lstBubbleSort:
+ *      just like lstQuickSort, this will sort a given linked list.
+ *      See lstQuickSort for the parameters.
+ *
+ *      However, this function uses the "bubble sort" algorithm, which
+ *      will cause a lot of calls to pfnSort and which is really
+ *      ineffective for lists with more than 100 items.
+ *      Use only if you absolutely need a stable sort.
+ *
+ *      This calls lstSwapNodes to swap the list items.
+ */
+
+BOOL lstBubbleSort(PLINKLIST pList,
+                   PFNSORTLIST pfnSort,
+                   void* pStorage)
+{
+    BOOL brc = FALSE;
+
+    if (pList)
+        if (    (pList->ulMagic == LINKLISTMAGIC)
+             && (pfnSort)
+           )
+        {
+            long lRight = lstCountItems(pList),
+                 lSorted,
+                 x;
+
+            do {
+                lRight--;
+                lSorted = 0;
+                for (x = 0; x < lRight; x++)
+                {
+                    // ulComp++;
+                    PLISTNODE pNode1 = lstNodeFromIndex(pList, x),
+                              pNode2 = pNode1->pNext;
+                    if ((pNode1) && (pNode2))
+                        if ( (*pfnSort)(pNode1->pItemData,
+                                        pNode2->pItemData,
+                                        pStorage) > 0
+                           )
+                        {
+                            lstSwapNodes(pNode1, pNode2);
+                            lSorted++;
+                        }
+                }
+            } while ( lSorted && (lRight > 1) );
+
+            brc = TRUE;
+        }
+
+    return (brc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   List pseudo-stacks                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ lstPush:
+ *      pushes any data onto a "stack", which is a plain
+ *      LINKLIST being abused as a stack.
+ *      The "stack" is last-in, first-out (LIFO), meaning
+ *      that the last item pushed onto the list will be
+ *      the first returned by lstPop. See lstPop for example
+ *      usage.
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+PLISTNODE lstPush(PLINKLIST pList,
+                  void* pNewItemData)     // in: data to store in list node
+{
+    return (lstAppendItem(pList, pNewItemData));
+}
+
+/*
+ *@@ lstPop:
+ *      returns the last item which has been pushed onto
+ *      a pseudo-stack LIFO LINKLIST using lstPush.
+ *      Note that to really "pop" the item, i.e. remove
+ *      it from the list, you MUST call lstRemoveNode
+ *      with the list node returned from this function,
+ *      or otherwise you keep getting the same last item
+ *      on subsequent calls.
+ *
+ *      Example:
+ *
+ +          LINKLIST ll;
+ +          PLISTNODE pNode;
+ +          lstInit(&ll, FALSE;
+ +
+ +          lstPush(&ll, (PVOID)1);
+ +          lstPush(&ll, (PVOID)2);
+ +
+ +          pNode = lstPop(&ll);     // returns the node containing "2"
+ +          lstRemoveNode(pNode);
+ +          pNode = lstPop(&ll);     // returns the node containing "1"
+ +          lstRemoveNode(pNode);
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+PLISTNODE lstPop(PLINKLIST pList)
+{
+    PLISTNODE pNodeReturn = NULL;
+    unsigned long cItems = lstCountItems(pList);
+    if (cItems)
+    {
+        // list not empty:
+        // get last item on the list
+        pNodeReturn = lstNodeFromIndex(pList,
+                                       cItems - 1);
+    }
+    return (pNodeReturn);
+}
+
Index: /trunk/src/helpers/memdebug.c
===================================================================
--- /trunk/src/helpers/memdebug.c	(revision 8)
+++ /trunk/src/helpers/memdebug.c	(revision 8)
@@ -0,0 +1,1235 @@
+
+/*
+ * memdebug.c:
+ *      memory debugging helpers. Memory debugging is enabled
+ *      if the __XWPMEMDEBUG__ #define is set in setup.h.
+ *
+ *      Several things are in here which might turn out to
+ *      be useful:
+ *
+ *      -- Memory block dumping (memdDumpMemoryBlock).
+ *
+ *      -- Sophisticated heap debugging functions, which
+ *         automatically replace malloc() and free() etc.
+ *         when XWorkplace is compiled with debug code.
+ *         These log every memory allocation made and log
+ *         much more data compared to the VAC++ memory
+ *         debugging functions. See HEAPITEM for details.
+ *         Automatic heap checking is also included, using
+ *         special "magic string" which are checked to
+ *         detect overwrites.
+ *
+ *         To be able to trace memory errors, set the global
+ *         variable G_pMemdLogFunc to a function which can
+ *         write an error string to a meaningful place (the
+ *         screen or a file). WARNING: While that error function
+ *         is executed, the system might be in a global memory
+ *         lock, so DON'T display a message box while in that
+ *         function.
+ *
+ *         At present, malloc(), calloc(), strdup() and free()
+ *         are supported. If you invoke free() on a memory block
+ *         allocated by a function other than the above, you'll
+ *         get a runtime error.
+ *
+ *         These debug functions have been added with V0.9.3
+ *         and should now be compiler-independent.
+ *
+ *      -- A PM heap debugging window which shows the statuzs
+ *         of the heap logging list. See memdCreateMemDebugWindow
+ *         for details.
+ *
+ *      To enable memory debugging, do the following:
+ *
+ *      1) Include at least <stdlib.h> and <string.h>.
+ *
+ *      2) Include memdebug.h AFTER those two. This will remap
+ *         the malloc() etc. calls.
+ *
+ *         If you don't want those replaced, add
+ +              #define DONT_REPLACE_MALLOC
+ *         before including memdebug.h.
+ *
+ *      That's all. XWorkplace's setup.h does this automatically
+ *      if XWorkplace is compiled with debug code.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This program is part of the XWorkplace package.
+ *      This program is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published by
+ *      the Free Software Foundation, in version 2 as it comes in the COPYING
+ *      file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSEXCEPTIONS
+#define INCL_DOSPROCESS
+#define INCL_DOSERRORS
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINCOUNTRY
+#define INCL_WINSYS
+#define INCL_WINMENUS
+#define INCL_WINSTDCNR
+#include <os2.h>
+
+#include <stdio.h>
+#include <string.h>
+// #include <malloc.h>
+#include <setjmp.h>
+
+#define DONT_REPLACE_MALLOC             // we need the "real" malloc in this file
+#include "setup.h"
+
+#ifdef __XWPMEMDEBUG__
+
+#include "helpers\cnrh.h"
+#include "helpers\except.h"
+// #include "helpers\memdebug.h"        // included by setup.h already
+#include "helpers\stringh.h"
+#include "helpers\winh.h"
+
+/*
+ *@@category: Helpers\C helpers\Heap debugging
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+#define MEMBLOCKMAGIC_HEAD     "\210\203`H&cx$&%\254"
+            // size must be a multiple of 4 or dword-alignment will fail;
+            // there's an extra 0 byte at the end, so we have a size of 12
+            // V0.9.3 (2000-04-17) [umoeller]
+#define MEMBLOCKMAGIC_TAIL     "\250\210&%/dfjsk%#,dlhf\223"
+
+/*
+ *@@ HEAPITEM:
+ *      informational structure created for each
+ *      malloc() call by memdMalloc. These are stored
+ *      in a global linked list (G_pHeapItemsRoot).
+ *
+ *      We cannot use the linklist.c functions for
+ *      managing the linked list because these use
+ *      malloc in turn, which would lead to infinite
+ *      loops.
+ *
+ *@@added V0.9.3 (2000-04-11) [umoeller]
+ */
+
+typedef struct _HEAPITEM
+{
+    struct _HEAPITEM    *pNext;     // next item in linked list or NULL if last
+
+    void                *pAfterMagic; // memory pointer returned by memdMalloc;
+                                    // this points to after the magic string
+    unsigned long       ulSize;     // size of *pData
+
+    const char          *pcszSourceFile;    // as passed to memdMalloc
+    unsigned long       ulLine;             // as passed to memdMalloc
+    const char          *pcszFunction;      // as passed to memdMalloc
+
+    DATETIME            dtAllocated;        // system date/time at time of memdMalloc call
+
+    ULONG               ulTID;      // thread ID that memdMalloc was running on
+
+    BOOL                fFreed;     // TRUE only after item has been freed by memdFree
+} HEAPITEM, *PHEAPITEM;
+
+HMTX            G_hmtxMallocList = NULLHANDLE;
+
+PHEAPITEM       G_pHeapItemsRoot = NULL,
+                G_pHeapItemsLast = NULL;
+
+PSZ             G_pszMemCnrTitle = NULL;
+
+PFNCBMEMDLOG    G_pMemdLogFunc = NULL;
+
+ULONG           G_ulItemsReleased = 0,
+                G_ulBytesReleased = 0;
+
+/* ******************************************************************
+ *                                                                  *
+ *   Debug heap management                                          *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ memdLock:
+ *      enables the global memory lock to protect
+ *      the global variables here. Use memdUnlock
+ *      to unlock again, and lock only for the shortest
+ *      possible time. This is only used by the memdebug.c
+ *      functions.
+ *
+ *@@added V0.9.3 (2000-04-10) [umoeller]
+ */
+
+BOOL memdLock(VOID)
+{
+    APIRET arc = NO_ERROR;
+    if (G_hmtxMallocList == NULLHANDLE)
+        // first call:
+        arc = DosCreateMutexSem(NULL,
+                                &G_hmtxMallocList,
+                                0,
+                                FALSE);
+
+    arc = DosRequestMutexSem(G_hmtxMallocList,
+                             SEM_INDEFINITE_WAIT);
+
+    return (arc == NO_ERROR);
+}
+
+/*
+ *@@ memdUnlock:
+ *      the reverse to memdLock.
+ *
+ *@@added V0.9.3 (2000-04-10) [umoeller]
+ */
+
+VOID memdUnlock(VOID)
+{
+    DosReleaseMutexSem(G_hmtxMallocList);
+}
+
+/*
+ *@@ memdMalloc:
+ *      wrapper function for malloc() to trace malloc()
+ *      calls more precisely.
+ *
+ *      If XWorkplace is compiled with debug code, setup.h
+ *      automatically #includes memdebug.h, which maps
+ *      malloc to this function so that the source file
+ *      etc. parameters automatically get passed.
+ *
+ *      For each call, we call the default malloc(), whose
+ *      return value is returned, and create a HEAPITEM
+ *      for remembering the call, which is stored in a global
+ *      linked list.
+ *
+ *@@added V0.9.3 (2000-04-11) [umoeller]
+ */
+
+void* memdMalloc(size_t stSize,
+                 const char *pcszSourceFile, // in: source file name
+                 unsigned long ulLine,       // in: source line
+                 const char *pcszFunction)   // in: function name
+{
+    void *prc = NULL;
+
+    if (stSize == 0)
+        // malloc(0) called: report error
+        if (G_pMemdLogFunc)
+        {
+            CHAR szMsg[1000];
+            sprintf(szMsg,
+                    "Function %s (%s, line %d) called malloc(0).",
+                    pcszFunction,
+                        pcszSourceFile,
+                        ulLine);
+            G_pMemdLogFunc(szMsg);
+        }
+
+    if (memdLock())
+    {
+        // call default malloc(), but with the additional
+        // size of our MEMBLOCKMAGIC strings; we'll return
+        // the first byte after the "front" string so we can
+        // check for string overwrites
+        void *pObj = malloc(stSize
+                            + sizeof(MEMBLOCKMAGIC_HEAD)
+                            + sizeof(MEMBLOCKMAGIC_TAIL));
+        if (pObj)
+        {
+            PHEAPITEM   pHeapItem = (PHEAPITEM)malloc(sizeof(HEAPITEM));
+
+            // store "front" magic string
+            memcpy(pObj,
+                   MEMBLOCKMAGIC_HEAD,
+                   sizeof(MEMBLOCKMAGIC_HEAD));
+            // return address: first byte after "front" magic string
+            prc = ((PBYTE)pObj) + sizeof(MEMBLOCKMAGIC_HEAD);
+            // store "tail" magic string to block which
+            // will be returned plus the size which was requested
+            memcpy(((PBYTE)prc) + stSize,
+                   MEMBLOCKMAGIC_TAIL,
+                   sizeof(MEMBLOCKMAGIC_TAIL));
+
+            if (pHeapItem)
+            {
+                PTIB        ptib;
+                PPIB        ppib;
+
+                pHeapItem->pNext = 0;
+
+                pHeapItem->pAfterMagic = prc;
+                pHeapItem->ulSize = stSize;
+                pHeapItem->pcszSourceFile = pcszSourceFile;
+                pHeapItem->ulLine = ulLine;
+                pHeapItem->pcszFunction = pcszFunction;
+
+                DosGetDateTime(&pHeapItem->dtAllocated);
+
+                pHeapItem->ulTID = 0;
+
+                if (DosGetInfoBlocks(&ptib, &ppib) == NO_ERROR)
+                    if (ptib)
+                        if (ptib->tib_ptib2)
+                            pHeapItem->ulTID = ptib->tib_ptib2->tib2_ultid;
+
+                pHeapItem->fFreed = FALSE;
+
+                // append heap item to linked list
+                if (G_pHeapItemsRoot == NULL)
+                    // first item:
+                    G_pHeapItemsRoot = pHeapItem;
+                else
+                    // we have items already:
+                    if (G_pHeapItemsLast)
+                    {
+                        // last item cached:
+                        G_pHeapItemsLast->pNext = pHeapItem;
+                        G_pHeapItemsLast = pHeapItem;
+                    }
+                    else
+                    {
+                        // not cached: find end of list
+                        PHEAPITEM phi = G_pHeapItemsRoot;
+                        while (phi->pNext)
+                            phi = phi->pNext;
+
+                        phi->pNext = pHeapItem;
+                        G_pHeapItemsLast = pHeapItem;
+                    }
+            }
+        }
+
+        memdUnlock();
+    }
+
+    return (prc);
+}
+
+/*
+ *@@ memdCalloc:
+ *      similar to memdMalloc; this is the wrapper for
+ *      the calloc() call. This is automatically
+ *      remapped also.
+ *
+ *@@added V0.9.3 (2000-04-11) [umoeller]
+ */
+
+void* memdCalloc(size_t num,
+                 size_t stSize,
+                 const char *pcszSourceFile,
+                 unsigned long ulLine,
+                 const char *pcszFunction)
+{
+    void *p = memdMalloc(num * stSize,
+                         pcszSourceFile,
+                         ulLine,
+                         pcszFunction);
+    memset(p, 0, num * stSize);
+    return (p);
+}
+
+/*
+ *@@ memdFree:
+ *      wrapper for the free() call, which is remapped
+ *      by setup.h and memdebug.h like memdMalloc
+ *      and memdCalloc. This searches the memory object
+ *      (p) which was previously allocated on the linked
+ *      list of HEAPITEM's and frees it then by calling
+ *      the default free().
+ *
+ *      The HEAPITEM itself is not freed, but only marked
+ *      as freed. As a result, the linked list can grow
+ *      REALLY large. While memdMalloc does not become
+ *      slower with large HEAPITEM lists because it only
+ *      appends to the end of the list, which is remembered,
+ *      memdFree can become extremely slow because the entire
+ *      list needs to be searched with each call.
+ *      So call memdReleaseFreed from time to time.
+ *
+ *@@added V0.9.3 (2000-04-10) [umoeller]
+ */
+
+void memdFree(void *p,
+              const char *pcszSourceFile,
+              unsigned long ulLine,
+              const char *pcszFunction)
+{
+    BOOL fFound = FALSE;
+    if (memdLock())
+    {
+        // PLISTNODE   pNode = lstQueryFirstNode(&G_llHeapItems);
+        PHEAPITEM pHeapItem = G_pHeapItemsRoot;
+
+        // search the list with the pointer which was
+        // really returned by the original malloc(),
+        // that is, the byte before the magic string
+        void *pBeforeMagic = ((PBYTE)p) - sizeof(MEMBLOCKMAGIC_HEAD);
+
+        while (pHeapItem)
+        {
+            if (pHeapItem->pAfterMagic == p)
+                // the same address may be allocated and freed
+                // several times, so if this address has been
+                // freed, search on
+                if (!pHeapItem->fFreed)
+                {
+                    // found:
+                    ULONG   ulError = 0;
+                    // check magic string
+                    if (memcmp(pBeforeMagic,
+                               MEMBLOCKMAGIC_HEAD,
+                               sizeof(MEMBLOCKMAGIC_HEAD))
+                            != 0)
+                        ulError = 1;
+                    else if (memcmp(((PBYTE)pHeapItem->pAfterMagic) + pHeapItem->ulSize,
+                                    MEMBLOCKMAGIC_TAIL,
+                                    sizeof(MEMBLOCKMAGIC_TAIL))
+                            != 0)
+                        ulError = 2;
+
+                    if (ulError)
+                    {
+                        // magic block has been overwritten:
+                        if (G_pMemdLogFunc)
+                        {
+                            CHAR szMsg[1000];
+                            sprintf(szMsg,
+                                    "Magic string %s memory block at 0x%lX has been overwritten.\n"
+                                    "This was detected by the free() call at %s (%s, line %d).\n"
+                                    "The block was allocated by %s (%s, line %d).",
+                                    (ulError == 1) ? "before" : "after",
+                                    p,
+                                    pcszFunction,
+                                        pcszSourceFile,
+                                        ulLine, // free
+                                    pHeapItem->pcszFunction,
+                                        pHeapItem->pcszSourceFile,
+                                        pHeapItem->ulLine);
+                            G_pMemdLogFunc(szMsg);
+                        }
+                    }
+
+                    free(pBeforeMagic);
+                    pHeapItem->fFreed = TRUE;
+
+                    /* lstRemoveNode(&G_llHeapItems,
+                                  pNode); */
+                    fFound = TRUE;
+                    break;
+                }
+
+            pHeapItem = pHeapItem->pNext;
+        }
+
+        memdUnlock();
+    }
+
+    if (!fFound)
+        if (G_pMemdLogFunc)
+        {
+            CHAR szMsg[1000];
+            sprintf(szMsg,
+                    "free() failed. Called from %s (%s, line %d) for object 0x%lX.",
+                    pcszFunction,
+                        pcszSourceFile,
+                        ulLine,
+                    p);
+            G_pMemdLogFunc(szMsg);
+        }
+}
+
+/*
+ *@@ memdReleaseFreed:
+ *      goes thru the entire global HEAPITEM's list
+ *      and throws out all items which have been freed.
+ *      Call this from time to time in order to keep
+ *      the system usable. See memdFree() for details.
+ *
+ *      Returns the no. of HEAPITEM's that have been
+ *      released.
+ *
+ *@@added V0.9.3 (2000-04-11) [umoeller]
+ */
+
+unsigned long memdReleaseFreed(void)
+{
+    BOOL    ulItemsReleased = 0,
+            ulBytesReleased = 0;
+    if (memdLock())
+    {
+        PHEAPITEM pHeapItem = G_pHeapItemsRoot,
+                  pPrevious = NULL;
+
+        while (pHeapItem)
+        {
+            PHEAPITEM   pNext = pHeapItem->pNext;       // can be NULL
+            if (pHeapItem->fFreed)
+            {
+                // item freed:
+                if (pPrevious == NULL)
+                    // head of list:
+                    G_pHeapItemsRoot = pNext;           // can be NULL
+                else
+                    // somewhere later:
+                    // link next to previous to skip current
+                    pPrevious->pNext = pNext;           // can be NULL
+
+                ulItemsReleased++;
+                ulBytesReleased += pHeapItem->ulSize;
+
+                free(pHeapItem);
+            }
+            else
+                // item still valid:
+                pPrevious = pHeapItem;
+
+            pHeapItem = pNext;
+        }
+
+        G_ulItemsReleased += ulItemsReleased;
+        G_ulBytesReleased += ulBytesReleased;
+
+        memdUnlock();
+    }
+
+    return (ulItemsReleased);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   XFolder debugging helpers                                      *
+ *                                                                  *
+ ********************************************************************/
+
+#ifdef _PMPRINTF_
+    /*
+     *@@ memdDumpMemoryBlock:
+     *      if _PMPRINTF_ has been #define'd before including
+     *      memdebug.h,
+     *      this will dump a block of memory to the PMPRINTF
+     *      output window. Useful for debugging internal
+     *      structures.
+     *      If _PMPRINTF_ has been NOT #define'd,
+     *      no code will be produced at all. :-)
+     */
+
+    void memdDumpMemoryBlock(PBYTE pb,       // in: start address
+                             ULONG ulSize,   // in: size of block
+                             ULONG ulIndent) // in: how many spaces to put
+                                             //     before each output line
+    {
+        PSZ psz = strhCreateDump(pb, ulSize, ulIndent);
+        if (psz)
+        {
+            _Pmpf(("\n%s", psz));
+            free(psz);
+        }
+    }
+#endif
+
+/* ******************************************************************
+ *                                                                  *
+ *   Heap debugging window                                          *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ MEMRECORD:
+ *
+ *@@added V0.9.1 (99-12-04) [umoeller]
+ */
+
+typedef struct _MEMRECORD
+{
+    RECORDCORE  recc;
+
+    ULONG       ulIndex;
+
+    CDATE       cdateAllocated;
+    CTIME       ctimeAllocated;
+
+    PSZ         pszFreed;
+
+    ULONG       ulTID;
+
+    PSZ         pszFunction;    // points to szFunction
+    CHAR        szFunction[400];
+
+    PSZ         pszSource;      // points to szSource
+    CHAR        szSource[CCHMAXPATH];
+
+    ULONG       ulLine;
+
+    PSZ         pszAddress;     // points to szAddress
+    CHAR        szAddress[20];
+
+    ULONG       ulSize;
+
+} MEMRECORD, *PMEMRECORD;
+
+/* ULONG       ulHeapItemsCount1,
+            ulHeapItemsCount2;
+ULONG       ulTotalAllocated,
+            ulTotalFreed;
+PMEMRECORD  pMemRecordThis = NULL;
+PSZ         pszMemCnrTitle = NULL; */
+
+#if 0
+    /*
+     *@@ fncbMemHeapWalkCount:
+     *      callback func for _heap_walk function used for
+     *      fnwpMemDebug.
+     *
+     *@@added V0.9.1 (99-12-04) [umoeller]
+     */
+
+    int fncbMemHeapWalkCount(const void *pObject,
+                             size_t Size,
+                             int useflag,
+                             int status,
+                             const char *filename,
+                             size_t line)
+    {
+        // skip all the items which seem to be
+        // internal to the runtime
+        if ((filename) || (useflag == _FREEENTRY))
+        {
+            ulHeapItemsCount1++;
+            if (useflag == _FREEENTRY)
+                ulTotalFreed += Size;
+            else
+                ulTotalAllocated += Size;
+        }
+        return (0);
+    }
+
+    /*
+     *@@ fncbMemHeapWalkFill:
+     *      callback func for _heap_walk function used for
+     *      fnwpMemDebug.
+     *
+     *@@added V0.9.1 (99-12-04) [umoeller]
+     */
+
+    int fncbMemHeapWalkFill(const void *pObject,
+                            size_t Size,
+                            int useflag,
+                            int status,
+                            const char *filename,
+                            size_t line)
+    {
+        // skip all the items which seem to be
+        // internal to the runtime
+        if ((filename) || (useflag == _FREEENTRY))
+        {
+            ulHeapItemsCount2++;
+            if ((pMemRecordThis) && (ulHeapItemsCount2 < ulHeapItemsCount1))
+            {
+                pMemRecordThis->ulIndex = ulHeapItemsCount2 - 1;
+
+                pMemRecordThis->pObject = pObject;
+                pMemRecordThis->useflag = useflag;
+                pMemRecordThis->status = status;
+                pMemRecordThis->filename = filename;
+
+                pMemRecordThis->pszAddress = pMemRecordThis->szAddress;
+
+                pMemRecordThis->ulSize = Size;
+
+                pMemRecordThis->pszSource = pMemRecordThis->szSource;
+
+                pMemRecordThis->ulLine = line;
+
+                pMemRecordThis = (PMEMRECORD)pMemRecordThis->recc.preccNextRecord;
+            }
+            else
+                return (1);     // stop
+        }
+
+        return (0);
+    }
+
+    /*
+     *@@ memdCreateRecordsVAC:
+     *
+     *@@added V0.9.3 (2000-04-10) [umoeller]
+     */
+
+    VOID memdCreateRecordsVAC(HWND hwndCnr)
+    {
+        // count heap items
+        ulHeapItemsCount1 = 0;
+        ulTotalFreed = 0;
+        ulTotalAllocated = 0;
+        _heap_walk(fncbMemHeapWalkCount);
+
+        pMemRecordFirst = (PMEMRECORD)cnrhAllocRecords(hwndCnr,
+                                                       sizeof(MEMRECORD),
+                                                       ulHeapItemsCount1);
+        if (pMemRecordFirst)
+        {
+            ulHeapItemsCount2 = 0;
+            pMemRecordThis = pMemRecordFirst;
+            _heap_walk(fncbMemHeapWalkFill);
+
+            // the following doesn't work while _heap_walk is running
+            pMemRecordThis = pMemRecordFirst;
+            while (pMemRecordThis)
+            {
+                switch (pMemRecordThis->useflag)
+                {
+                    case _USEDENTRY: pMemRecordThis->pszUseFlag = "Used"; break;
+                    case _FREEENTRY: pMemRecordThis->pszUseFlag = "Freed"; break;
+                }
+
+                switch (pMemRecordThis->status)
+                {
+                    case _HEAPBADBEGIN: pMemRecordThis->pszStatus = "heap bad begin"; break;
+                    case _HEAPBADNODE: pMemRecordThis->pszStatus = "heap bad node"; break;
+                    case _HEAPEMPTY: pMemRecordThis->pszStatus = "heap empty"; break;
+                    case _HEAPOK: pMemRecordThis->pszStatus = "OK"; break;
+                }
+
+                sprintf(pMemRecordThis->szAddress, "0x%lX", pMemRecordThis->pObject);
+                strcpy(pMemRecordThis->szSource,
+                        (pMemRecordThis->filename)
+                            ? pMemRecordThis->filename
+                            : "?");
+
+                pMemRecordThis = (PMEMRECORD)pMemRecordThis->recc.preccNextRecord;
+            }
+
+            cnrhInsertRecords(hwndCnr,
+                              NULL,         // parent
+                              (PRECORDCORE)pMemRecordFirst,
+                              TRUE,
+                              NULL,
+                              CRA_RECORDREADONLY,
+                              ulHeapItemsCount2);
+        }
+    }
+
+#endif
+
+/*
+ *@@ memdCreateRecords:
+ *
+ *@@added V0.9.3 (2000-04-10) [umoeller]
+ */
+
+VOID memdCreateRecords(HWND hwndCnr,
+                       PULONG pulTotalItems,
+                       PULONG pulAllocatedItems,
+                       PULONG pulFreedItems,
+                       PULONG pulTotalBytes,
+                       PULONG pulAllocatedBytes,
+                       PULONG pulFreedBytes)
+{
+    // count heap items
+    ULONG       ulHeapItemsCount1 = 0;
+    PMEMRECORD  pMemRecordFirst;
+
+    if (memdLock())
+    {
+        // PLISTNODE   pNode = lstQueryFirstNode(&G_llHeapItems);
+        PHEAPITEM pHeapItem = G_pHeapItemsRoot;
+
+        *pulTotalItems = 0;
+        *pulAllocatedItems = 0;
+        *pulFreedItems = 0;
+
+        *pulTotalBytes = 0;
+        *pulAllocatedBytes = 0;
+        *pulFreedBytes = 0;
+
+        while (pHeapItem)
+        {
+            ulHeapItemsCount1++;
+            if (pHeapItem->fFreed)
+            {
+                (*pulFreedItems)++;
+                (*pulFreedBytes) += pHeapItem->ulSize;
+            }
+            else
+            {
+                (*pulAllocatedItems)++;
+                (*pulAllocatedBytes) += pHeapItem->ulSize;
+            }
+
+            (*pulTotalBytes) += pHeapItem->ulSize;
+
+            pHeapItem = pHeapItem->pNext;
+        }
+
+        *pulTotalItems = ulHeapItemsCount1;
+
+        pMemRecordFirst = (PMEMRECORD)cnrhAllocRecords(hwndCnr,
+                                                       sizeof(MEMRECORD),
+                                                       ulHeapItemsCount1);
+        if (pMemRecordFirst)
+        {
+            ULONG       ulHeapItemsCount2 = 0;
+            PMEMRECORD  pMemRecordThis = pMemRecordFirst;
+            pHeapItem = G_pHeapItemsRoot;
+            // PLISTNODE   pMemNode = lstQueryFirstNode(&G_llHeapItems);
+
+            while ((pMemRecordThis) && (pHeapItem))
+            {
+                // PHEAPITEM pHeapItem = (PHEAPITEM)pMemNode->pItemData;
+
+                pMemRecordThis->ulIndex = ulHeapItemsCount2++;
+
+                cnrhDateTimeDos2Win(&pHeapItem->dtAllocated,
+                                    &pMemRecordThis->cdateAllocated,
+                                    &pMemRecordThis->ctimeAllocated);
+
+                if (pHeapItem->fFreed)
+                    pMemRecordThis->pszFreed = "yes";
+
+                pMemRecordThis->ulTID = pHeapItem->ulTID;
+
+                strcpy(pMemRecordThis->szSource, pHeapItem->pcszSourceFile);
+                pMemRecordThis->pszSource = pMemRecordThis->szSource;
+
+                pMemRecordThis->ulLine = pHeapItem->ulLine;
+
+                strcpy(pMemRecordThis->szFunction, pHeapItem->pcszFunction);
+                pMemRecordThis->pszFunction = pMemRecordThis->szFunction;
+
+                sprintf(pMemRecordThis->szAddress, "0x%lX", pHeapItem->pAfterMagic);
+                pMemRecordThis->pszAddress = pMemRecordThis->szAddress;
+
+                pMemRecordThis->ulSize = pHeapItem->ulSize;
+
+
+                /* switch (pMemRecordThis->useflag)
+                {
+                    case _USEDENTRY: pMemRecordThis->pszUseFlag = "Used"; break;
+                    case _FREEENTRY: pMemRecordThis->pszUseFlag = "Freed"; break;
+                }
+
+                switch (pMemRecordThis->status)
+                {
+                    case _HEAPBADBEGIN: pMemRecordThis->pszStatus = "heap bad begin"; break;
+                    case _HEAPBADNODE: pMemRecordThis->pszStatus = "heap bad node"; break;
+                    case _HEAPEMPTY: pMemRecordThis->pszStatus = "heap empty"; break;
+                    case _HEAPOK: pMemRecordThis->pszStatus = "OK"; break;
+                }
+
+                sprintf(pMemRecordThis->szAddress, "0x%lX", pMemRecordThis->pObject);
+                strcpy(pMemRecordThis->szSource,
+                        (pMemRecordThis->filename)
+                            ? pMemRecordThis->filename
+                            : "?"); */
+
+                pMemRecordThis = (PMEMRECORD)pMemRecordThis->recc.preccNextRecord;
+                pHeapItem = pHeapItem->pNext;
+            }
+
+            cnrhInsertRecords(hwndCnr,
+                              NULL,         // parent
+                              (PRECORDCORE)pMemRecordFirst,
+                              TRUE,
+                              NULL,
+                              CRA_RECORDREADONLY,
+                              ulHeapItemsCount2);
+        }
+
+        memdUnlock();
+    }
+}
+
+/*
+ *@@ mnu_fnCompareIndex:
+ *
+ *@@added V0.9.1 (99-12-03) [umoeller]
+ */
+
+SHORT EXPENTRY mnu_fnCompareIndex(PMEMRECORD pmrc1, PMEMRECORD  pmrc2, PVOID pStorage)
+{
+    // HAB habDesktop = WinQueryAnchorBlock(HWND_DESKTOP);
+    pStorage = pStorage; // to keep the compiler happy
+    if ((pmrc1) && (pmrc2))
+        if (pmrc1->ulIndex < pmrc2->ulIndex)
+            return (-1);
+        else if (pmrc1->ulIndex > pmrc2->ulIndex)
+            return (1);
+
+    return (0);
+}
+
+/*
+ *@@ mnu_fnCompareSourceFile:
+ *
+ *@@added V0.9.1 (99-12-03) [umoeller]
+ */
+
+SHORT EXPENTRY mnu_fnCompareSourceFile(PMEMRECORD pmrc1, PMEMRECORD  pmrc2, PVOID pStorage)
+{
+    HAB habDesktop = WinQueryAnchorBlock(HWND_DESKTOP);
+    pStorage = pStorage; // to keep the compiler happy
+    if ((pmrc1) && (pmrc2))
+            switch (WinCompareStrings(habDesktop, 0, 0,
+                                      pmrc1->szSource,
+                                      pmrc2->szSource,
+                                      0))
+            {
+                case WCS_LT: return (-1);
+                case WCS_GT: return (1);
+                default:    // equal
+                    if (pmrc1->ulLine < pmrc2->ulLine)
+                        return (-1);
+                    else if (pmrc1->ulLine > pmrc2->ulLine)
+                        return (1);
+
+            }
+
+    return (0);
+}
+
+#define ID_MEMCNR   1000
+
+/*
+ *@@ memd_fnwpMemDebug:
+ *      client window proc for the heap debugger window
+ *      accessible from the Desktop context menu if
+ *      __XWPMEMDEBUG__ is defined. Otherwise, this is not
+ *      compiled.
+ *
+ *      Usage: this is a regular PM client window procedure
+ *      to be used with WinRegisterClass and WinCreateStdWindow.
+ *      See dtpMenuItemSelected, which uses this.
+ *
+ *      This creates a container with all the memory objects
+ *      with the size of the client area in turn.
+ *
+ *@@added V0.9.1 (99-12-04) [umoeller]
+ */
+
+
+MRESULT EXPENTRY memd_fnwpMemDebug(HWND hwndClient, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = 0;
+
+    switch (msg)
+    {
+        case WM_CREATE:
+        {
+            TRY_LOUD(excpt1, NULL)
+            {
+                // PCREATESTRUCT pcs = (PCREATESTRUCT)mp2;
+                HWND hwndCnr;
+                hwndCnr = WinCreateWindow(hwndClient,        // parent
+                                          WC_CONTAINER,
+                                          "",
+                                          WS_VISIBLE | CCS_MINIICONS | CCS_READONLY | CCS_SINGLESEL,
+                                          0, 0, 0, 0,
+                                          hwndClient,        // owner
+                                          HWND_TOP,
+                                          ID_MEMCNR,
+                                          NULL, NULL);
+                if (hwndCnr)
+                {
+                    XFIELDINFO      xfi[11];
+                    PFIELDINFO      pfi = NULL;
+                    PMEMRECORD      pMemRecordFirst;
+                    int             i = 0;
+
+                    ULONG           ulTotalItems = 0,
+                                    ulAllocatedItems = 0,
+                                    ulFreedItems = 0;
+                    ULONG           ulTotalBytes = 0,
+                                    ulAllocatedBytes = 0,
+                                    ulFreedBytes = 0;
+
+                    // set up cnr details view
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, ulIndex);
+                    xfi[i].pszColumnTitle = "No.";
+                    xfi[i].ulDataType = CFA_ULONG;
+                    xfi[i++].ulOrientation = CFA_RIGHT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, cdateAllocated);
+                    xfi[i].pszColumnTitle = "Date";
+                    xfi[i].ulDataType = CFA_DATE;
+                    xfi[i++].ulOrientation = CFA_LEFT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, ctimeAllocated);
+                    xfi[i].pszColumnTitle = "Time";
+                    xfi[i].ulDataType = CFA_TIME;
+                    xfi[i++].ulOrientation = CFA_LEFT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, pszFreed);
+                    xfi[i].pszColumnTitle = "Freed";
+                    xfi[i].ulDataType = CFA_STRING;
+                    xfi[i++].ulOrientation = CFA_CENTER;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, ulTID);
+                    xfi[i].pszColumnTitle = "TID";
+                    xfi[i].ulDataType = CFA_ULONG;
+                    xfi[i++].ulOrientation = CFA_RIGHT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, pszFunction);
+                    xfi[i].pszColumnTitle = "Function";
+                    xfi[i].ulDataType = CFA_STRING;
+                    xfi[i++].ulOrientation = CFA_LEFT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, pszSource);
+                    xfi[i].pszColumnTitle = "Source";
+                    xfi[i].ulDataType = CFA_STRING;
+                    xfi[i++].ulOrientation = CFA_LEFT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, ulLine);
+                    xfi[i].pszColumnTitle = "Line";
+                    xfi[i].ulDataType = CFA_ULONG;
+                    xfi[i++].ulOrientation = CFA_RIGHT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, ulSize);
+                    xfi[i].pszColumnTitle = "Size";
+                    xfi[i].ulDataType = CFA_ULONG;
+                    xfi[i++].ulOrientation = CFA_RIGHT;
+
+                    xfi[i].ulFieldOffset = FIELDOFFSET(MEMRECORD, pszAddress);
+                    xfi[i].pszColumnTitle = "Address";
+                    xfi[i].ulDataType = CFA_STRING;
+                    xfi[i++].ulOrientation = CFA_LEFT;
+
+                    pfi = cnrhSetFieldInfos(hwndCnr,
+                                            &xfi[0],
+                                            i,             // array item count
+                                            TRUE,          // no draw lines
+                                            3);            // return column
+
+                    {
+                        PSZ pszFont = "9.WarpSans";
+                        WinSetPresParam(hwndCnr,
+                                        PP_FONTNAMESIZE,
+                                        strlen(pszFont),
+                                        pszFont);
+                    }
+
+                    memdCreateRecords(hwndCnr,
+                                      &ulTotalItems,
+                                      &ulAllocatedItems,
+                                      &ulFreedItems,
+                                      &ulTotalBytes,
+                                      &ulAllocatedBytes,
+                                      &ulFreedBytes);
+
+                    BEGIN_CNRINFO()
+                    {
+                        CHAR    szCnrTitle[1000];
+                        CHAR    szTotalItems[100],
+                                szAllocatedItems[100],
+                                szFreedItems[100],
+                                szReleasedItems[100];
+                        CHAR    szTotalBytes[100],
+                                szAllocatedBytes[100],
+                                szFreedBytes[100],
+                                szReleasedBytes[100];
+                        sprintf(szCnrTitle,
+                                "Total logs in use: %s items = %s bytes\n"
+                                "    Total in use: %s items = %s bytes\n"
+                                "    Total freed: %s items = %s bytes\n"
+                                "Total logs released: %s items = %s bytes",
+                                strhThousandsDouble(szTotalItems,
+                                                    ulTotalItems,
+                                                    '.'),
+                                strhThousandsDouble(szTotalBytes,
+                                                    ulTotalBytes,
+                                                    '.'),
+                                strhThousandsDouble(szAllocatedItems,
+                                                    ulAllocatedItems,
+                                                    '.'),
+                                strhThousandsDouble(szAllocatedBytes,
+                                                    ulAllocatedBytes,
+                                                    '.'),
+                                strhThousandsDouble(szFreedItems,
+                                                    ulFreedItems,
+                                                    '.'),
+                                strhThousandsDouble(szFreedBytes,
+                                                    ulFreedBytes,
+                                                    '.'),
+                                strhThousandsDouble(szReleasedItems,
+                                                    G_ulItemsReleased,
+                                                    '.'),
+                                strhThousandsDouble(szReleasedBytes,
+                                                    G_ulBytesReleased,
+                                                    '.'));
+                        G_pszMemCnrTitle = strdup(szCnrTitle);
+                        cnrhSetTitle(G_pszMemCnrTitle);
+                        cnrhSetView(CV_DETAIL | CV_MINI | CA_DETAILSVIEWTITLES
+                                        | CA_DRAWICON
+                                    | CA_CONTAINERTITLE | CA_TITLEREADONLY
+                                        | CA_TITLESEPARATOR | CA_TITLELEFT);
+                        cnrhSetSplitBarAfter(pfi);
+                        cnrhSetSplitBarPos(250);
+                    } END_CNRINFO(hwndCnr);
+
+                    WinSetFocus(HWND_DESKTOP, hwndCnr);
+                }
+            }
+            CATCH(excpt1) {} END_CATCH();
+
+            mrc = WinDefWindowProc(hwndClient, msg, mp1, mp2);
+        break; }
+
+        case WM_WINDOWPOSCHANGED:
+        {
+            PSWP pswp = (PSWP)mp1;
+            mrc = WinDefWindowProc(hwndClient, msg, mp1, mp2);
+            if (pswp->fl & SWP_SIZE)
+            {
+                WinSetWindowPos(WinWindowFromID(hwndClient, ID_MEMCNR), // cnr
+                                HWND_TOP,
+                                0, 0, pswp->cx, pswp->cy,
+                                SWP_SIZE | SWP_MOVE | SWP_SHOW);
+            }
+        break; }
+
+        case WM_CONTROL:
+        {
+            USHORT usItemID = SHORT1FROMMP(mp1),
+                   usNotifyCode = SHORT2FROMMP(mp1);
+            if (usItemID == ID_MEMCNR)       // cnr
+            {
+                switch (usNotifyCode)
+                {
+                    case CN_CONTEXTMENU:
+                    {
+                        PMEMRECORD precc = (PMEMRECORD)mp2;
+                        if (precc == NULL)
+                        {
+                            // whitespace
+                            HWND hwndMenu = WinCreateMenu(HWND_DESKTOP,
+                                                          NULL); // no menu template
+                            winhInsertMenuItem(hwndMenu,
+                                               MIT_END,
+                                               1001,
+                                               "Sort by index",
+                                               MIS_TEXT, 0);
+                            winhInsertMenuItem(hwndMenu,
+                                               MIT_END,
+                                               1002,
+                                               "Sort by source file",
+                                               MIS_TEXT, 0);
+                            cnrhShowContextMenu(WinWindowFromID(hwndClient, ID_MEMCNR),
+                                                NULL,       // record
+                                                hwndMenu,
+                                                hwndClient);
+                        }
+                    }
+                }
+            }
+        break; }
+
+        case WM_COMMAND:
+            switch (SHORT1FROMMP(mp1))
+            {
+                case 1001:  // sort by index
+                    WinSendMsg(WinWindowFromID(hwndClient, ID_MEMCNR),
+                               CM_SORTRECORD,
+                               (MPARAM)mnu_fnCompareIndex,
+                               0);
+                break;
+
+                case 1002:  // sort by source file
+                    WinSendMsg(WinWindowFromID(hwndClient, ID_MEMCNR),
+                               CM_SORTRECORD,
+                               (MPARAM)mnu_fnCompareSourceFile,
+                               0);
+                break;
+            }
+        break;
+
+        case WM_CLOSE:
+            WinDestroyWindow(WinWindowFromID(hwndClient, ID_MEMCNR));
+            WinDestroyWindow(WinQueryWindow(hwndClient, QW_PARENT));
+            free(G_pszMemCnrTitle);
+            G_pszMemCnrTitle = NULL;
+        break;
+
+        default:
+            mrc = WinDefWindowProc(hwndClient, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ memdCreateMemDebugWindow:
+ *      creates a heap debugging window which
+ *      is a standard frame with a container,
+ *      listing all heap objects ever allocated.
+ *
+ *      The client of this standard frame is in
+ *      memd_fnwpMemDebug.
+ *
+ *      This thing lists all calls to malloc()
+ *      which were ever made, including the
+ *      source file and source line number which
+ *      made the call. Extreeeemely useful for
+ *      detecting memory leaks.
+ *
+ *      This only works if the memory functions
+ *      have been replaced with the debug versions
+ *      in this file.
+ */
+
+VOID memdCreateMemDebugWindow(VOID)
+{
+    ULONG flStyle = FCF_TITLEBAR | FCF_SYSMENU | FCF_HIDEMAX
+                    | FCF_SIZEBORDER | FCF_SHELLPOSITION
+                    | FCF_NOBYTEALIGN | FCF_TASKLIST;
+    if (WinRegisterClass(WinQueryAnchorBlock(HWND_DESKTOP),
+                         "XWPMemDebug",
+                         memd_fnwpMemDebug, 0L, 0))
+    {
+        HWND hwndClient;
+        HWND hwndMemFrame = WinCreateStdWindow(HWND_DESKTOP,
+                                               0L,
+                                               &flStyle,
+                                               "XWPMemDebug",
+                                               "Allocated XWorkplace Memory Objects",
+                                               0L,
+                                               NULLHANDLE,     // resource
+                                               0,
+                                               &hwndClient);
+        if (hwndMemFrame)
+        {
+            WinSetWindowPos(hwndMemFrame,
+                            HWND_TOP,
+                            0, 0, 0, 0,
+                            SWP_ZORDER | SWP_SHOW | SWP_ACTIVATE);
+        }
+    }
+}
+
+#else
+void memdDummy(void)
+{
+    int i = 0;
+}
+#endif
+
Index: /trunk/src/helpers/prfh.c
===================================================================
--- /trunk/src/helpers/prfh.c	(revision 8)
+++ /trunk/src/helpers/prfh.c	(revision 8)
@@ -0,0 +1,485 @@
+
+/*
+ *@@sourcefile prfh.c:
+ *      contains those Presentation Manager helper functions
+ *      which deal with Profile (Prf*) functions.
+ *      This file is new with V0.82.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes:
+ *      --  prfh*   Prf (profile, INI) helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\prfh.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WIN
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\prfh.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Profile (INI) helpers
+ */
+
+/*
+ *@@ prfhQueryKeysForApp:
+ *      returns the keys list for an INI application. This
+ *      list is copied into a newly allocated buffer, of which
+ *      the address is returned.
+ *
+ *      Returns NULL upon errors.
+ *
+ *      If the return value is != NULL, the PSZ points to a new
+ *      buffer which contains all the keys within the pszApp
+ *      application. Each key name in the list is terminated with
+ *      a null character. The last string in the list is terminated
+ *      with two null characters.
+ *
+ *      The returned buffer should be freed later using free().
+ *
+ *      <B>Example</B> for iterating over a keys list:
+ +          PSZ pszKeysList = prfhQueryKeysForApp(...);
+ +          if (pszKeysList)
+ +          {
+ +              PSZ pKey2 = pszKeysList;
+ +
+ +              while (*pKey2 != 0)
+ +              {
+ +                  ...     // pKey2 has the current key now
+ +                  pKey2 += strlen(pKey2)+1; // next key
+ +              }
+ +              free(pszKeysList);
+ +          }
+ *
+ *      You can also use this function to query the applications
+ *      list for hIni, if you specifiy pszApp as NULL.
+ */
+
+PSZ prfhQueryKeysForApp(HINI hIni,      // in: INI handle (can be HINI_USER or HINI_SYSTEM)
+                        PSZ pszApp)     // in: application to query list for (or NULL for applications list)
+{
+    PSZ     pKeys = NULL;
+    ULONG   ulSizeOfKeysList = 0;
+
+    // get size of keys list for pszApp
+    if (PrfQueryProfileSize(hIni, pszApp, NULL, &ulSizeOfKeysList))
+    {
+        pKeys = (PSZ)malloc(ulSizeOfKeysList);
+        if (!PrfQueryProfileData(hIni, pszApp, NULL, pKeys, &ulSizeOfKeysList))
+        {
+            free(pKeys);
+            pKeys = NULL;
+        }
+    }
+    return (pKeys);
+}
+
+#ifdef __XWPMEMDEBUG__ // setup.h, helpers\memdebug.c
+
+/*
+ *@@ prfhQueryProfileDataDebug:
+ *      debug version of prfhQueryProfileData, which is
+ *      automatically mapped to if __XWPMEMDEBUG__ is defined.
+ *
+ *@@added V0.9.1 (99-12-20) [umoeller]
+ *@@changed V0.9.3 (2000-04-20) [umoeller]: this called malloc(0) if the key existed, but was empty. Fixed.
+ */
+
+PSZ prfhQueryProfileDataDebug(HINI hIni,      // in: INI handle (can be HINI_USER or HINI_SYSTEM)
+                              PSZ pszApp,      // in: application to query
+                              PSZ pszKey,      // in: key to query
+                              PULONG pcbBuf,   // out: size of the returned buffer
+                              const char *file,
+                              unsigned long line,
+                              const char *function)
+{
+    PSZ     pData = NULL;
+    ULONG   ulSizeOfData = 0;
+
+    // get size of data for pszApp/pszKey
+    if (PrfQueryProfileSize(hIni, pszApp, pszKey, &ulSizeOfData))
+    {
+        if (ulSizeOfData)
+        {
+            pData = (PSZ)memdMalloc(ulSizeOfData, file, line, function);
+            if (!PrfQueryProfileData(hIni, pszApp, pszKey, pData, &ulSizeOfData))
+            {
+                free(pData);
+                pData = NULL;
+            }
+        }
+    }
+
+    if (pcbBuf)
+        *pcbBuf = ulSizeOfData;
+
+    return (pData);
+}
+
+#else
+
+/*
+ *@@ prfhQueryProfileData:
+ *      similar to PrfQueryProfileData, but this one copies
+ *      the data into a newly allocated buffer, of which the
+ *      address is returned.
+ *
+ *      Returns NULL upon errors, for example if the specified
+ *      key doesn't exist or doesn't contain any data.
+ *
+ *      If pcbBuf != NULL, this func will write the size of
+ *      the allocated buffer into *pcbBuf.
+ *
+ *      The returned buffer should be freed later using free().
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.3 (2000-04-20) [umoeller]: this called malloc(0) if the key existed, but was empty. Fixed.
+ */
+
+PSZ prfhQueryProfileData(HINI hIni,      // in: INI handle (can be HINI_USER or HINI_SYSTEM)
+                        PSZ pszApp,      // in: application to query
+                        PSZ pszKey,      // in: key to query
+                        PULONG pcbBuf)   // out: size of the returned buffer
+{
+    PSZ     pData = NULL;
+    ULONG   ulSizeOfData = 0;
+
+    // get size of data for pszApp/pszKey
+    if (PrfQueryProfileSize(hIni, pszApp, pszKey, &ulSizeOfData))
+    {
+        if (ulSizeOfData)
+        {
+            pData = (PSZ)malloc(ulSizeOfData);
+            if (!PrfQueryProfileData(hIni, pszApp, pszKey, pData, &ulSizeOfData))
+            {
+                free(pData);
+                pData = NULL;
+            }
+        }
+    }
+
+    if (pcbBuf)
+        *pcbBuf = ulSizeOfData;
+
+    return (pData);
+}
+
+#endif
+
+/*
+ *@@ prfhQueryProfileChar:
+ *      this query the first character of profile data.
+ *      This is mostly useful with the PM country settings
+ *      in OS2.INI:
+ *      -- date separator: "PM_National", "sDate"
+ *      -- time separator: "PM_National", "sTime"
+ */
+
+CHAR prfhQueryProfileChar(HINI hini,        // in: INI handle (can be HINI_USER or HINI_SYSTEM)
+                          PSZ pszApp,       // in: application to query
+                          PSZ pszKey,       // in: key to query
+                          CHAR cDefault)    // in: default to return if not found
+{
+    // CHAR    crc = 0;
+    CHAR    szTemp[5],
+            szDefault[5];
+    szDefault[0] = cDefault;
+    szDefault[1] = 0;
+    PrfQueryProfileString(HINI_USER, pszApp, pszKey,
+                          szDefault,
+                          szTemp, sizeof(szTemp)-1);
+    return (szTemp[0]);
+}
+
+/*
+ *@@ prfhQueryCountrySettings:
+ *      this returns the most frequently used country settings
+ *      all at once into a COUNTRYSETTINGS structure (prfh.h).
+ *      This data corresponds to the user settings in the
+ *      WPS "Country" object (which writes the data in "PM_National"
+ *      in OS2.INI).
+ *
+ *      In case a key cannot be found, the following (English)
+ *      default values are set:
+ *      --  ulDateFormat = 0 (English date format, mm.dd.yyyy);
+ *      --  ulTimeFormat = 0 (12-hour clock);
+ *      --  cDateSep = '/' (date separator);
+ *      --  cTimeSep = ':' (time separator);
+ *      --  cThousands = ',' (thousands separator).
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+VOID prfhQueryCountrySettings(PCOUNTRYSETTINGS pcs)
+{
+    if (pcs)
+    {
+        pcs->ulDateFormat = PrfQueryProfileInt(HINI_USER, "PM_National", "iDate", 0);
+        pcs->ulTimeFormat = PrfQueryProfileInt(HINI_USER, "PM_National", "iTime", 0);
+        pcs->cDateSep = prfhQueryProfileChar(HINI_USER, "PM_National", "sDate", '/');
+        pcs->cTimeSep = prfhQueryProfileChar(HINI_USER, "PM_National", "sTime", ':');
+        pcs->cThousands = prfhQueryProfileChar(HINI_USER, "PM_National", "sThousand", ',');
+    }
+}
+
+/*
+ *@@ prfhQueryColor:
+ *      returns a system color in OS2.INI's PM_Colors as a LONG.
+ */
+
+LONG prfhQueryColor(PSZ pszKeyName, PSZ pszDefault)
+{
+    CHAR szColor[30];
+    LONG r, g, b;
+    PrfQueryProfileString(
+                HINI_USER,
+                "PM_Colors",
+                pszKeyName,
+                pszDefault,
+                szColor,
+                sizeof(szColor)-1);
+    sscanf(szColor, "%d %d %d ", &r, &g, &b);
+    return (r*0x10000 + g*0x100 + b);
+}
+
+/*
+ *@@ prfhCopyKey:
+ *      this copies one key from the given profile and application
+ *      to another one.
+ *
+ *      pszTargetApp may be in the same profile (and must be
+ *      different from pszSourceApp then) or in a different
+ *      profile (and can be the same then).
+ *
+ *      You must specify all parameters. You cannot specify pszKey
+ *      as NULL to have a whole application copied. Use prfhCopyApp
+ *      for that.
+ *      No check is made for this.
+ *
+ *      Returns:
+ *      --  0: no error
+ *      --  PRFERR_DATASIZE: couldn't query data size for key
+ *      --  PRFERR_MEMORY: couldn't allocate memory
+ *      --  PRFERR_READ: couldn't read data from source (PrfQueryProfileData error)
+ *      --  PRFERR_WRITE: couldn't write data to target (PrfWriteProfileData error)
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG prfhCopyKey(HINI hiniSource,       // in: source profile (can be HINI_USER or HINI_SYSTEM)
+                 PSZ pszSourceApp,      // in: source application
+                 PSZ pszKey,            // in: source/target key
+                 HINI hiniTarget,       // in: target profile (can be HINI_USER or HINI_SYSTEM)
+                 PSZ pszTargetApp)      // in: target app
+{
+    ULONG   ulSizeOfData = 0,
+            ulrc = 0;       // return: no error
+
+    if (PrfQueryProfileSize(hiniSource, pszSourceApp, pszKey, &ulSizeOfData))
+    {
+        PSZ pData = 0;
+
+        // copy data
+        if (ulSizeOfData == 0)
+        {
+            // data size == 0: this shouldn't really happen,
+            // but if it does, we'll just create a NULL string.
+            // Users have reported that some INI files seem to
+            // contain those "empty" keys. I don't see how these
+            // can exist, but they seem to...
+            pData = (PSZ)malloc(1);
+            *pData = 0;
+        }
+        else
+            pData = (PSZ)malloc(ulSizeOfData);
+
+        if (pData)
+        {
+            if (PrfQueryProfileData(hiniSource,
+                                    pszSourceApp,
+                                    pszKey,
+                                    pData,
+                                    &ulSizeOfData))
+            {
+                if (!PrfWriteProfileData(hiniTarget,
+                                         pszTargetApp,
+                                         pszKey,
+                                         pData,
+                                         ulSizeOfData))
+                    ulrc = PRFERR_WRITE;
+            }
+            else
+                ulrc = PRFERR_READ;
+
+            free(pData);
+        }
+        else
+            ulrc = ERROR_NOT_ENOUGH_MEMORY;
+    }
+    else
+        ulrc = PRFERR_DATASIZE;
+
+    return (ulrc);
+}
+
+/*
+ *@@ prfhCopyApp:
+ *      this copies one key from the given profile and application
+ *      to another one.
+ *
+ *      You can use this function in several contexts:
+ *      -- copy one application within the same profile
+ *         (i.e. hiniSource == hiniTarget);
+ *         in this case, pszSourceApp must be != pszTargetApp;
+ *      -- copy an application from one profile to another
+ *         (i.e. hiniSource != hiniTarget);
+ *         in this case, pszSourceApp can be == pszTargetApp
+ *         (but can be different also).
+ *
+ *      WARNING: This does _not_ check for whether the target
+ *      application exists already. This has two consequences:
+ *      --  existing data will be overwritten without warning;
+ *      --  if the existing target application has keys that are
+ *          not in the source application, they are not deleted.
+ *          As a result, you might end up with more keys than
+ *          in the source application.
+ *
+ *      So you should delete the target application before
+ *      calling this function, like this:
+ +          PrfWriteProfileString(hiniTarget, pszTargetApp, NULL, NULL);
+ *
+ *      You must specify all parameters. You cannot specify pszApp
+ *      as NULL to have a whole profile copied. Use prfhCopyProfile
+ *      for that.
+ *      No check is made for this.
+ *
+ *      Returns:
+ *      --  0: no error
+ *      --  PRFERR_KEYSLIST: couldn't query keys for pszSourceApp
+ *      --  PRFERR_DATASIZE: couldn't query data size for key
+ *      --  PRFERR_MEMORY: couldn't allocate memory
+ *      --  PRFERR_READ: couldn't read data from source (PrfQueryProfileData error)
+ *      --  PRFERR_WRITE: couldn't write data to target (PrfWriteProfileData error)
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG prfhCopyApp(HINI hiniSource,   // in: source profile (can be HINI_USER or HINI_SYSTEM)
+                  PSZ pszSourceApp,  // in: source application
+                  HINI hiniTarget,   // in: target profile (can be HINI_USER or HINI_SYSTEM)
+                  PSZ pszTargetApp,  // in: name of pszSourceApp in hiniTarget
+                  PSZ pszErrorKey)   // out: failing key in case of error; ptr can be NULL
+{
+    ULONG   ulrc;
+    PSZ pszKeysList;
+
+    if (pszErrorKey)
+        *pszErrorKey = 0;
+
+    pszKeysList = prfhQueryKeysForApp(hiniSource, pszSourceApp);
+    if (pszKeysList)
+    {
+        PSZ pKey2 = pszKeysList;
+
+        while (*pKey2 != 0)
+        {
+            // copy this key
+            ulrc = prfhCopyKey(hiniSource,
+                               pszSourceApp,
+                               pKey2,
+                               hiniTarget,
+                               pszTargetApp);
+            if (ulrc)
+            {
+                // error: copy failing key to buffer
+                if (pszErrorKey)
+                    strcpy(pszErrorKey, pKey2);
+                break;
+            }
+            pKey2 += strlen(pKey2)+1;
+        } // end while (*pKey2 != 0)
+
+        free (pszKeysList);
+    }
+    else
+        ulrc = PRFERR_KEYSLIST;
+
+    return (ulrc);
+}
+
+/*
+ *@@ prfhSetUserProfile:
+ *      calls PrfReset to change the current user
+ *      profile (normally OS2.INI) to the specified
+ *      INI file.
+ *
+ *@@added V0.9.4 (2000-07-19) [umoeller]
+ */
+
+BOOL prfhSetUserProfile(HAB hab,
+                        PSZ pszUserProfile)     // in: new user profile (.INI)
+{
+    BOOL    brc = FALSE;
+    // find out current profile names
+    PRFPROFILE Profiles;
+    Profiles.cchUserName = Profiles.cchSysName = 0;
+    // first query their file name lengths
+    if (PrfQueryProfile(hab, &Profiles))
+    {
+        // allocate memory for filenames
+        Profiles.pszUserName  = (PSZ)malloc(Profiles.cchUserName);
+        Profiles.pszSysName  = (PSZ)malloc(Profiles.cchSysName);
+
+        if ((Profiles.pszSysName) && (Profiles.pszUserName))
+        {
+            // get filenames
+            if (PrfQueryProfile(hab, &Profiles))
+            {
+                _Pmpf(("Old user profile: %s", Profiles.pszUserName));
+
+                // change INIs
+                free(Profiles.pszUserName);
+                Profiles.pszUserName = pszUserProfile;
+                Profiles.cchUserName = strlen(pszUserProfile) + 1;
+                brc = PrfReset(hab, &Profiles);
+                free(Profiles.pszSysName);
+            }
+        }
+    }
+
+    return (brc);
+}
+
+
Index: /trunk/src/helpers/prfh2.c
===================================================================
--- /trunk/src/helpers/prfh2.c	(revision 8)
+++ /trunk/src/helpers/prfh2.c	(revision 8)
@@ -0,0 +1,641 @@
+
+/*
+ *@@sourcefile prfh2.c:
+ *      contains more Presentation Manager helper functions
+ *      which deal with Profile (Prf*) functions.
+ *
+ *      This file is new with V0.9.4 (2000-07-26) [umoeller].
+ *
+ *      As opposed to the functions in prfh.c, these require
+ *      linking against other helpers. As a result, these have
+ *      been separated from prfh.c to allow linking against
+ *      prfh.obj only.
+ *
+ *      Function prefixes:
+ *      --  prfh*   Prf (profile, INI) helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\prfh.h"
+ *@@added V0.9.4 (2000-07-27) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WIN
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+#include "helpers\stringh.h"
+
+#include "helpers\prfh.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Profile (INI) helpers
+ */
+
+/*
+ * prfhINIError:
+ *      this is called whenever an error occurs saving a
+ *      profile. This will compose an error string and
+ *      all the callback func fncbError.
+ *
+ *@@changed V0.9.0 [umoeller]: changed prototype's logfile to stdio FILE*
+ */
+
+ULONG prfhINIError(ULONG ulOptions,
+                   FILE* fLog,
+                   PFNWP fncbError,
+                   PSZ pszErrorString)
+{
+    ULONG ulrc;
+    CHAR szError2[2000];
+
+    if (fncbError)
+    {
+        if (ulOptions == MB_ABORTRETRYIGNORE)
+            sprintf(szError2, "%s"
+                              "\nPress 'Abort' to abort saving the INI files. This will restart the WPS to avoid loss of data."
+                              "\nPress 'Retry' to attempt saving this INI file again. "
+                              "\nPress 'Ignore' to ignore this error. This might help, but "
+                              "also lead to more errors or loss of the currently processed data.",
+                              pszErrorString);
+        else
+            sprintf(szError2, "%s\nPress 'Cancel' to abort shutdown. ", pszErrorString);
+        ulrc = ( (ULONG)(*fncbError)(0, 0, szError2, (MPARAM)ulOptions) );
+
+    }
+    else
+        ulrc = (MBID_ABORT);
+
+    if (fLog)
+    {
+        fprintf(fLog, "    Error occured: %s\n    Return code: %s (%d)\n",
+                pszErrorString,
+                    (ulrc == MBID_ABORT) ? "MBID_ABORT"
+                    : (ulrc == MBID_IGNORE) ? "MBID_IGNORE"
+                    : (ulrc == MBID_RETRY) ? "MBID_RETRY"
+                    : "unknown",
+                ulrc);
+        fflush(fLog);
+    }
+
+    return (ulrc);
+}
+
+/*
+ * prfhINIError2:
+ *      like the previous func, but with additional info
+ *      for the error string.
+ *
+ *@@changed V0.9.0 [umoeller]: changed prototype's logfile to stdio FILE*
+ */
+
+ULONG prfhINIError2(ULONG ulOptions,
+                    PSZ pszINI,
+                    FILE* fLog,
+                    PFNWP fncbError,
+                    PSZ pszErrorString)
+{
+    CHAR szError2[2000];
+    sprintf(szError2, "An error occured copying the profile %s: \n%s",
+            pszINI, pszErrorString);
+    return (prfhINIError(ulOptions, fLog, fncbError, szError2));
+}
+
+/*
+ *@@ prfhCopyProfile:
+ *      this function copies a given profile entirely into a
+ *      new file. hOld is the handle, pszOld the filename
+ *      of the profile to be copied (e.g. HINI_USERPROFILE
+ *      and "?:\OS2\OS2.INI"; pszNew must point to a
+ *      buffer which will contain the new filename ending
+ *      in ".XFL" after prfhCopyProfile returns
+ *      (e.g. "?:\OS2\OS2.XFL").
+ *
+ *      You may specify a callback procedure which gets
+ *      called upon every copied application in the INI
+ *      file; this way, you can provide a progress bar.
+ *      The second callback, fncbError, gets called upon
+ *      errors.
+ *
+ *@@changed V0.9.0 [umoeller]: this now calls prfhCopyApp.
+ *@@changed V0.9.0 [umoeller]: fixed crashes with lower-case INI file names.
+ *@@changed V0.9.0 [umoeller]: changed prototype's logfile to stdio FILE*
+ *@@changed V0.9.4 (2000-07-15) [umoeller]: this never reported any errors, causing 0-byte INI files without warning. Fixed.
+ */
+
+BOOL prfhCopyProfile(HAB hab,               // in:  anchor block
+                     FILE* fLog,            // in:  stdio logfile or NULL for no log
+                     HINI hOld,             // in:  HINI to copy
+                     PSZ pszOld,            // in:  fully qualif. filename of hOld
+                     PSZ pszNew,            // out: new filename
+                     PFNWP fncbUpdate,      // in:  progress callback
+                     HWND hwnd, ULONG msg, ULONG ulCount, ULONG ulMax,
+                                            // in:  passed to fncbUpdate
+                     PFNWP fncbError)       // in:  error callback
+{
+    // BOOL    rc = TRUE;
+    HINI    hNew;
+    PSZ     pApps, // pKeys,
+            pApp2; // , pKey2;
+    ULONG   ulSizeOfAppsList; // , ulSizeOfKeysList; // , ulSizeOfData;
+    // CHAR    szLog[1000];     // removed V0.9.0
+    #define MBID_NOERROR 999
+    ULONG   ulErrorStatus = MBID_NOERROR;
+    PSZ     p_;
+    CHAR    szError2[2000];
+
+    if (fLog)
+    {
+        fprintf(fLog, "    Entering prfhCopyProfile, params: "
+                      "hiniOld 0x%lX, pszOld %s, pfnwpCallback 0x%lX, "
+                      "hwnd 0x%lX, msg 0x%lX, ulCount 0x%lX, ulMax 0x%lX\n",
+                hOld, pszOld, fncbUpdate, hwnd, msg, ulCount, ulMax);
+        fflush(fLog);
+    }
+
+    if ( (!pszOld) || (!pszNew) )
+        ulErrorStatus = prfhINIError2(MB_CANCEL, pszOld, fLog, fncbError,
+                                      "Invalid name buffers given.");
+
+    if (ulErrorStatus == MBID_NOERROR)
+    {
+        strupr(pszOld);
+        strcpy(pszNew, pszOld);
+        p_ = strhistr(pszNew, ".INI");
+        if (!p_)
+            ulErrorStatus = prfhINIError2(MB_CANCEL, pszNew, fLog, fncbError,
+                                          "Error composing new filename.");
+    }
+
+    if (ulErrorStatus == MBID_NOERROR)
+    {
+        strcpy(p_, ".XFL");
+
+        // delete new profile, in case something's left
+        if (fLog)
+            fprintf(fLog, "    DEL %s\n", pszNew);
+
+        DosDelete(pszNew);
+
+        // open new profile
+        if (fLog)
+            fprintf(fLog, "    PrfOpenProfile %s\n", pszNew);
+
+        hNew = PrfOpenProfile(hab, pszNew);
+        if (!hNew)
+            ulErrorStatus = prfhINIError2(MB_CANCEL, pszNew, fLog, fncbError,
+                                          "Error creating temporary profile.");
+    }
+
+    // get size of applications list
+    if (ulErrorStatus == MBID_NOERROR)
+    {
+        if (!PrfQueryProfileSize(hOld, NULL, NULL, &ulSizeOfAppsList))
+            ulErrorStatus = prfhINIError2(MB_CANCEL, pszNew, fLog, fncbError,
+                    "Error querying applications list size.");
+        else
+        if (ulSizeOfAppsList == 0)
+            ulErrorStatus = prfhINIError2(MB_CANCEL, pszNew, fLog, fncbError,
+                "Size of applications list cannot be 0.");
+    }
+
+    if (ulErrorStatus == MBID_NOERROR)
+    {
+        // do-while loop in case of MBID_RETRY
+        do
+        {
+            ulErrorStatus = MBID_NOERROR; // if we have a retry in the do-while loop
+
+            // get applications list
+            pApps = (PSZ)malloc(ulSizeOfAppsList);
+            pApp2 = pApps;
+            if (!PrfQueryProfileData(hOld,
+                                     NULL,
+                                     NULL,
+                                     pApps,
+                                     &ulSizeOfAppsList))
+                ulErrorStatus = prfhINIError2(MB_CANCEL, pszNew, fLog, fncbError,
+                                              "Could not query application list.");
+
+            // applications loop
+
+            while (   (*pApp2 != 0)
+                   && (ulErrorStatus == MBID_NOERROR)
+                  )
+            {
+                CHAR szErrorKey[1000];
+                // copy application (this will call prfhCopyKey in turn)
+                ULONG ulrc = prfhCopyApp(hOld,
+                                         pApp2,
+                                         hNew,
+                                         pApp2,
+                                         szErrorKey);
+
+                ulErrorStatus = MBID_ABORT;
+
+                switch (ulrc)
+                {
+                    case 0: // no error
+                        ulErrorStatus = MBID_NOERROR;
+                    break;
+
+                    case PRFERR_KEYSLIST:
+                        // couldn't query keys list
+                        sprintf(szError2,
+                                "Error querying keys list for \"%s\".",
+                                pApp2);
+                    break;
+
+                    case PRFERR_DATASIZE:
+                        // couldn't query data size for key
+                        sprintf(szError2,
+                                "Error querying data size for \"%s\"/\"%s\".",
+                                pApp2, szErrorKey);
+                    break;
+
+                    case ERROR_NOT_ENOUGH_MEMORY:
+                        // couldn't allocate memory
+                        sprintf(szError2,
+                                "Error allocating memory for copying \"%s\"/\"%s\".",
+                                pApp2, szErrorKey);
+                    break;
+
+                    case PRFERR_READ:
+                        // couldn't read data from source (PrfQueryProfileData error)
+                        sprintf(szError2,
+                                "Error reading data from app \"%s\", key \"%s\".",
+                                pApp2, szErrorKey);
+                    break;
+
+                    case PRFERR_WRITE:
+                        // couldn't write data to target (PrfWriteProfileData error)
+                        sprintf(szError2,
+                                "Error writing data to app \"%s\", key \"%s\".",
+                                pApp2, szErrorKey);
+                    break;
+                }
+
+                if (ulErrorStatus != MBID_NOERROR) // V0.9.4 (2000-07-15) [umoeller]
+                {
+                    ulErrorStatus = prfhINIError2(MB_ABORTRETRYIGNORE, pszNew, fLog, fncbError,
+                                                  szError2);
+                }
+
+                if (fncbUpdate)
+                {
+                    ULONG ulNow2, ulMax2;
+                    ulNow2 = ((1000*(pApp2-pApps))/ulSizeOfAppsList) + (ulCount*1000);
+                    ulMax2 = (ulMax+1)*1000; // ulAppsSize;
+                    (*fncbUpdate)(hwnd, msg, (MPARAM)ulNow2, (MPARAM)ulMax2);
+                }
+
+                // go for next app
+                pApp2 += strlen(pApp2)+1;
+
+                if (ulErrorStatus == MBID_IGNORE)
+                    // error occured, but user pressed "Ignore":
+                    // skip this app
+                    ulErrorStatus = MBID_NOERROR;
+
+            } // end while (*pApp2 != 0) && MBID_NOERROR
+
+            if (pApps)
+                free(pApps);
+
+            if (fLog)
+                fprintf(fLog, "    Done copying apps\n");
+
+        } while (ulErrorStatus == MBID_RETRY);
+    } // end if (ulErrorOccured == MBID_NOERROR)
+
+    if (fLog)
+        fprintf(fLog, "    PrfCloseProfile %s\n", pszNew);
+
+    PrfCloseProfile(hNew);
+    if (fncbUpdate)
+        (*fncbUpdate)(hwnd, msg, (MPARAM)(ulCount+1), (MPARAM)(ulMax+1));
+
+    if (fLog)
+    {
+        fprintf(fLog, "    Leaving prfhCopyProfile.\n");
+        fflush(fLog);
+    }
+
+    return (ulErrorStatus == MBID_NOERROR); // FALSE if error occured
+}
+
+/*
+ *@@ prfhSaveINIs:
+ *      this will enforce saving of OS2.INI and OS2SYS.INI
+ *      by calling prfhCopyProfile (above) on them; the therefrom
+ *      resulting ".XFL" files will replace the original
+ *      ".INI" files.
+ *      Specify fncbUpdate and fncbError like in prfhCopyProfile.
+ *
+ *@@changed V0.9.0 [umoeller]: fixed crashes with lower-case INI file names.
+ *@@changed V0.9.0 [umoeller]: changed prototype's logfile to stdio FILE*
+ */
+
+APIRET prfhSaveINIs(HAB hab,               // in:  anchor block
+                    FILE* fLog,            // in:  stdio logfile or NULL for no log
+                    PFNWP fncbUpdate,      // in:  progress callback
+                    HWND hwnd, ULONG msg,  // in:  params passed to fncbUpdate
+                    PFNWP fncbError)       // in:  error callback
+{
+    PRFPROFILE Profiles;
+    APIRET  arc = NO_ERROR, arc2;
+    BOOL    brc = TRUE;
+
+    // FILESTATUS3 fs3;
+    CHAR    szSysNew[CCHMAXPATH],
+            szUserNew[CCHMAXPATH],
+            szSysBackup[CCHMAXPATH],
+            szUserBackup[CCHMAXPATH];
+
+    // the following flag may be one of the following:
+    //        MBID_NOERROR--- everything's fine, continue
+    //        MBID_IGNORE --- error occured, but ignore
+    //        MBID_RETRY  --- error occured, but retry
+    //        MBID_ABORT  --- error occured, abort saving
+    ULONG   ulErrorOccured = MBID_IGNORE;
+
+    if (fLog)
+        fprintf(fLog, "  Entering prfhSaveINIs...\n");
+
+    /*
+     * get system profiles:
+     *
+     */
+
+    Profiles.cchUserName = Profiles.cchSysName = 0;
+    brc = PrfQueryProfile(hab, &Profiles);
+
+    if (!brc)
+        ulErrorOccured = prfhINIError(MB_CANCEL, fLog, fncbError,
+                                      "Error querying system profiles size.");
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        Profiles.pszUserName  = (PSZ)malloc(Profiles.cchUserName);
+        Profiles.pszSysName  = (PSZ)malloc(Profiles.cchSysName);
+        if (    (Profiles.pszSysName == NULL)
+             || (Profiles.pszUserName == NULL)
+           )
+            ulErrorOccured = prfhINIError(MB_CANCEL, fLog, fncbError,
+                    "Error allocating memory (1).");
+    }
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        if (!PrfQueryProfile(hab, &Profiles))
+            ulErrorOccured = prfhINIError(MB_CANCEL, fLog, fncbError,
+                    "Error querying profiles (2).");
+    }
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        if (fLog)
+            fprintf(fLog, "  System profiles are %s, %s\n",
+                    Profiles.pszUserName, Profiles.pszSysName);
+
+        /*
+         * create new profile names:
+         *
+         */
+
+        strcpy(szSysBackup, Profiles.pszSysName);
+        strcpy(strhistr(szSysBackup, ".INI"), ".BAK");
+        strcpy(szUserBackup, Profiles.pszUserName);
+        strcpy(strhistr(szUserBackup, ".INI"), ".BAK");
+
+        /*
+         * create OS2SYS.XFL:
+         *
+         */
+
+        if (fLog)
+            fprintf(fLog, "  Storing %s in *.XFL\n",
+                    Profiles.pszSysName);
+
+        if (!prfhCopyProfile(hab,
+                             fLog,
+                             HINI_SYSTEMPROFILE,
+                             Profiles.pszSysName,   // old filename
+                             szSysNew,              // new filename
+                             fncbUpdate, hwnd, msg, 0, 1,
+                             fncbError))
+        {
+            // abort, since prfhCopyProfile already has error handling
+            ulErrorOccured = MBID_ABORT;
+        }
+    }
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        // create OS2.XFL
+
+        if (fLog)
+            fprintf(fLog, "  Storing %s in *.XFL\n",
+                    Profiles.pszUserName);
+
+        if (!prfhCopyProfile(hab,
+                             fLog,
+                             HINI_USERPROFILE,
+                             Profiles.pszUserName, szUserNew,
+                             fncbUpdate, hwnd, msg, 1, 1,
+                             fncbError))
+        {
+            // abort, since prfhCopyProfile already has error handling
+            ulErrorOccured = MBID_ABORT;
+        }
+    }
+
+    /*
+     * renaming stuff for OS2SYS.INI
+     *
+     */
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        do
+        {
+            ulErrorOccured = MBID_IGNORE; // if we have a retry in the do-while loop
+
+            if (ulErrorOccured == MBID_IGNORE)
+            {
+                // attrib -r -s -h -a OS2SYS.BAK
+                if (fLog)
+                    fprintf(fLog, "  ATTRIB -R -S -H -A %s\n",
+                            szSysBackup);
+
+                arc2 = doshSetPathAttr(szSysBackup, FILE_NORMAL);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // delete OS2SYS.BAK
+                if (fLog)
+                    fprintf(fLog, "  DEL %s\n",
+                            szSysBackup);
+
+                arc2 = DosDelete(szSysBackup);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // attrib -r -s -h -a OS2SYS.INI
+                if (fLog)
+                    fprintf(fLog, "  ATTRIB -R -S -H -A %s\n",
+                            Profiles.pszSysName);
+
+                arc2 = doshSetPathAttr(Profiles.pszSysName, FILE_NORMAL);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // move OS2SYS.INI OS2SYS.BAK
+                if (fLog)
+                    fprintf(fLog, "  MOVE %s %s\n",
+                            Profiles.pszSysName, szSysBackup);
+                arc = DosMove(Profiles.pszSysName, szSysBackup);
+                if (fLog)
+                    fprintf(fLog, "    rc: %d\n", arc);
+
+                if (arc)
+                    ulErrorOccured = prfhINIError(MB_ABORTRETRYIGNORE, fLog, fncbError, "Error moving original system profile to backup.");
+            }
+
+            if (ulErrorOccured == MBID_IGNORE)
+            {
+                if (fLog)
+                    fprintf(fLog, "  MOVE %s %s\n",
+                            szSysNew, Profiles.pszSysName);
+                arc = DosMove(szSysNew, Profiles.pszSysName);
+                if (fLog)
+                    fprintf(fLog, "    rc: %d\n", arc);
+                if (arc)
+                    ulErrorOccured = prfhINIError(MB_ABORTRETRYIGNORE, fLog, fncbError, "Error moving newly created profile to system profile.");
+            }
+        } while (ulErrorOccured == MBID_RETRY);
+    }
+
+    /*
+     * renaming stuff for OS2.INI
+     *
+     */
+
+    if (ulErrorOccured == MBID_IGNORE)
+    {
+        do
+        {
+            ulErrorOccured = MBID_IGNORE; // if we have a retry in the do-while loop
+
+            if (ulErrorOccured == MBID_IGNORE)
+            {
+                // attrib -r -s -h -a OS2.BAK
+                if (fLog)
+                    fprintf(fLog, "  ATTRIB -R -S -H -A %s\n",
+                            szUserBackup);
+
+                arc2 = doshSetPathAttr(szUserBackup, FILE_NORMAL);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // delete OS2.BAK
+                if (fLog)
+                    fprintf(fLog,"  DEL %s\n",
+                            szUserBackup);
+
+                arc2 = DosDelete(szUserBackup);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // attrib -r -s -h -a OS2.INI
+                if (fLog)
+                    fprintf(fLog, "  ATTRIB -R -S -H -A %s\n",
+                            Profiles.pszUserName);
+                arc2 = doshSetPathAttr(Profiles.pszUserName, FILE_NORMAL);
+                if (fLog)
+                    fprintf(fLog, "    rc2: %d\n", arc2);
+
+                // move OS2.INI OS2.BAK
+                if (fLog)
+                    fprintf(fLog, "  MOVE %s %s\n",
+                            Profiles.pszUserName, szUserBackup);
+                arc = DosMove(Profiles.pszUserName, szUserBackup);
+                if (fLog)
+                    fprintf(fLog, "    rc: %d\n", arc);
+
+                if (arc)
+                    ulErrorOccured = prfhINIError(MB_ABORTRETRYIGNORE, fLog, fncbError, "Error moving original user profile to backup.");
+            }
+
+            if (ulErrorOccured == MBID_IGNORE)
+            {
+                // move OS2.XFL OS2.INI
+                if (fLog)
+                    fprintf(fLog, "  MOVE %s %s\n",
+                            szUserNew, Profiles.pszUserName);
+
+                arc = DosMove(szUserNew, Profiles.pszUserName);
+                if (fLog)
+                    fprintf(fLog, "    rc: %d\n", arc);
+
+                if (arc)
+                    ulErrorOccured = prfhINIError(MB_ABORTRETRYIGNORE, fLog, fncbError, "Error moving newly created profile to user profile.");
+            }
+        } while (ulErrorOccured == MBID_RETRY);
+    }
+
+    // DosExitCritSec();
+
+    if (ulErrorOccured != MBID_IGNORE)
+    {
+        DosMove(szSysBackup, Profiles.pszSysName);
+        DosMove(szUserBackup, Profiles.pszUserName);
+    }
+
+    if (    (Profiles.pszSysName)
+         && (Profiles.pszUserName)
+       )
+    {
+        free(Profiles.pszSysName);
+        free(Profiles.pszUserName);
+    }
+
+    if (fLog)
+        fprintf(fLog, "  Done with prfhSaveINIs\n");
+
+    if (ulErrorOccured != MBID_IGNORE)
+        return (999);
+    else
+        return (NO_ERROR);
+}
+
+
Index: /trunk/src/helpers/procstat.c
===================================================================
--- /trunk/src/helpers/procstat.c	(revision 8)
+++ /trunk/src/helpers/procstat.c	(revision 8)
@@ -0,0 +1,681 @@
+
+/*
+ *@@sourcefile procstat.c:
+ *      functions for querying process information.
+ *      This is an easy-to-use interface to the
+ *      messy 16-bit DosQProcStatus function.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  prc*   Query Process helper functions
+ *
+ *      Based on Kai Uwe Rommel's "dosqproc" package
+ *      available at Hobbes:
+ *      Kai Uwe Rommel - Wed 25-Mar-1992
+ *                       Sat 13-Aug-1994
+ *
+ *      Note: If you link against procstat.obj, you
+ *      need to import the following in your .DEF file:
+ *
+ +          IMPORTS
+ +              DOSQPROCSTATUS = DOSCALLS.154
+ +              DosQuerySysState = DOSCALLS.368
+ *
+ *      or linking will fail.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\procstat.h"
+ */
+
+/*
+ *      Copyright (C) 1992-1994 Kai Uwe Rommel.
+ *      Copyright (C) 1998-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_DOSMODULEMGR
+#include <os2.h>
+
+#include <stdlib.h>      // already #include'd
+#include <string.h>      // already #include'd
+#include <stdio.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\procstat.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Control program helpers\Process status\16-bit DosQProcStat
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   DosQProcStat (16-bit) interface                                *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ prc16GetInfo:
+ *      nifty interface to DosQProcStat (16-bit).
+ *      This returns the head of a newly
+ *      allocated buffer which has plenty
+ *      of pointers for subsequent browsing.
+ *
+ *      Use prc16FreeInfo to free the buffer.
+ *
+ *@@added V0.9.3 (2000-05-05) [umoeller]
+ */
+
+PQPROCSTAT16 prc16GetInfo(APIRET *parc)     // out: error, ptr can be NULL
+{
+    APIRET arc = NO_ERROR;
+    PQPROCSTAT16 pps = (PQPROCSTAT16)malloc(0x8000);
+    if (!pps)
+        arc = ERROR_NOT_ENOUGH_MEMORY;
+    else
+    {
+        arc = DosQProcStatus(pps, 0x8000);
+        if (arc != NO_ERROR)
+            free(pps);
+    }
+
+    if (parc)
+        *parc = arc;
+
+    return (pps);
+}
+
+/*
+ *@@ prc16FreeInfo:
+ *      frees memory allocated by prc16GetInfo.
+ *
+ *@@added V0.9.3 (2000-05-05) [umoeller]
+ */
+
+VOID prc16FreeInfo(PQPROCSTAT16 pInfo)
+{
+    free(pInfo);
+}
+
+/*
+ *@@ prc16FindProcessFromName:
+ *      searches the specified buffer for a process
+ *      with the specified name and returns a pointer
+ *      to its data within pInfo.
+ *
+ *      Returns NULL if not found.
+ *
+ *@@added V0.9.3 (2000-05-05) [umoeller]
+ */
+
+PQPROCESS16 prc16FindProcessFromName(PQPROCSTAT16 pInfo,    // in: from prc16GetInfo
+                                     const char *pcszName)  // in: e.g. "pmshell.exe"
+{
+    PQPROCESS16 pProcess,
+                pReturn = NULL;
+    for ( pProcess = (PQPROCESS16)PTR(pInfo->ulProcesses, 0);
+          pProcess->ulType != 3;
+          pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                                      pProcess->usThreads * sizeof(QTHREAD16))
+        )
+    {
+        CHAR    szModuleName[CCHMAXPATH];
+        if (DosQueryModuleName(pProcess->usHModule,
+                               sizeof(szModuleName),
+                               szModuleName)
+                == NO_ERROR)
+        {
+            // the module name is fully qualified, so find the
+            // file name (after the last backslash)
+            PSZ pLastBackslash = strrchr(szModuleName, '\\');
+            if (pLastBackslash)
+                // found:
+                if (stricmp(pLastBackslash + 1, pcszName) == 0)
+                {
+                    // matches:
+                    pReturn = pProcess;
+                    break;
+                }
+        }
+    }
+
+    return (pReturn);
+}
+
+/*
+ *@@ prc16FindProcessFromPID:
+ *      searches the specified buffer for a process
+ *      with the specified PID and returns a pointer
+ *      to its data within pInfo.
+ *
+ *      Returns NULL if not found.
+ *
+ *V0.9.5 (2000-09-29) [umoeller]
+ */
+
+PQPROCESS16 prc16FindProcessFromPID(PQPROCSTAT16 pInfo, // in: from prc16GetInfo
+                                    ULONG ulPID)        // in: PID
+{
+    PQPROCESS16 pProcess,
+                pReturn = NULL;
+    for ( pProcess = (PQPROCESS16)PTR(pInfo->ulProcesses, 0);
+          pProcess->ulType != 3;
+          pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                                      pProcess->usThreads * sizeof(QTHREAD16))
+        )
+    {
+        if (pProcess->usPID == ulPID)
+        {
+            pReturn = pProcess;
+            break;
+        }
+    }
+
+    return (pReturn);
+}
+
+/********************************************************************
+ *                                                                  *
+ *   DosQProcStat (16-bit) helpers                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * prcReport16:
+ *      fill PRCPROCESS structure
+ */
+
+VOID prcReport16(PQPROCESS16 pProcess, PPRCPROCESS pprcp)
+{
+    // USHORT usPriority = 0;
+    PQTHREAD16 pThread;
+    int i;
+
+    DosQueryModuleName(pProcess->usHModule,
+                       sizeof(pprcp->szModuleName),
+                       pprcp->szModuleName);
+    // DosGetPrty(PRTYS_PROCESS, &(pprcp->usPriority), pProcess->usPID);
+
+    // sum up CPU time for process
+    for (pprcp->ulCPU = 0,
+                i = 0,
+                pThread = (PQTHREAD16)PTR(pProcess->ulThreadList, 0);
+         i < pProcess->usThreads;
+         i++, pThread++ )
+    {
+        pprcp->ulCPU += (pThread->ulSysTime + pThread->ulUserTime);
+    }
+
+    pprcp->usPID            = pProcess->usPID;
+    pprcp->usParentPID      = pProcess->usParentPID;
+    pprcp->usThreads        = pProcess->usThreads;
+    pprcp->ulSID            = pProcess->ulSID;
+    pprcp->ulSessionType    = pProcess->ulSessionType;
+    pprcp->ulStatus         = pProcess->ulStatus;
+}
+
+/*
+ *@@ prc16QueryProcessInfo:
+ *      this searches for a given process ID (usPID) and
+ *      fills a given PRCPROCESS structure with lots of
+ *      information about this process.
+ *      Returns FALSE upon errors, e.g. if no process
+ *      of that ID is found.
+ */
+
+BOOL prc16QueryProcessInfo(USHORT usPID,  // in: PID to query
+                           PPRCPROCESS pprcp) // out: process info
+{
+    BOOL rc = FALSE;
+    PQPROCESS16 pProcess;
+    PQPROCSTAT16 pps = (PQPROCSTAT16)malloc(0x8000);
+    DosQProcStatus(pps, 0x8000);
+
+    for ( pProcess = (PQPROCESS16)PTR(pps->ulProcesses, 0);
+          pProcess->ulType != 3;
+          pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                                      pProcess->usThreads * sizeof(QTHREAD16))
+        )
+    {
+        if (pProcess->usPID == usPID)
+        {
+            prcReport16(pProcess, pprcp);
+            rc = TRUE;
+            break;
+        }
+    }
+
+    free(pps);
+    return (rc);
+}
+
+/*
+ *@@ prc16ForEachProcess:
+ *      this calls a given callback func for each running
+ *      process. The callback must be a FNWP, which will be
+ *      passed the following parameters for each call:
+ *      --  HWND hwnd:       like hwnd passed to this func
+ *      --  ULONG msg:       like msg passed to this func
+ *      --  MPARAM mp1:      like mp1 passed to this func
+ *      --  PPRCPROCESS: mp2 pointer to a PRCPROCESS struct for each process
+ *
+ *      This function returns the number of running processes on the
+ *      system. If pfnwpCallback is NULL, only this number will be
+ *      returned, so you can use this as a process counter too.
+ */
+
+ULONG prc16ForEachProcess(PFNWP pfnwpCallback, HWND hwnd, ULONG ulMsg, MPARAM mp1)
+{
+    ULONG ulrc = 0;
+    PQPROCESS16 pProcess;
+    PRCPROCESS prcp;
+    PQPROCSTAT16 pps = (PQPROCSTAT16)malloc(0x8000);
+    DosQProcStatus(pps, 0x8000);
+
+    for ( pProcess = (PQPROCESS16)PTR(pps->ulProcesses, 0);
+          pProcess->ulType != 3;
+          pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                     pProcess->usThreads * sizeof(QTHREAD16))
+        )
+    {
+        if (pfnwpCallback)
+        {
+            prcReport16(pProcess, &prcp);
+            (*pfnwpCallback)(hwnd, ulMsg, mp1, &prcp);
+        }
+        ulrc++;
+    }
+
+    free(pps);
+    return (ulrc);
+}
+
+/*
+ *@@ prc16QueryThreadCount:
+ *      returns the total number of running threads
+ *      in the given process. If pid == 0, the
+ *      total thread count for the system is returned.
+ */
+
+ULONG prc16QueryThreadCount(USHORT usPID)
+{
+    ULONG       ulrc = 0;
+    PQPROCSTAT16  pps = (PQPROCSTAT16)malloc(0x8000);
+    DosQProcStatus(pps, 0x8000);
+
+    if (usPID)
+    {
+        // process query:
+        PQPROCESS16 pProcess;
+        for ( pProcess = (PQPROCESS16)PTR(pps->ulProcesses, 0);
+              pProcess->ulType != 3;
+              pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                                          pProcess->usThreads * sizeof(QTHREAD16))
+            )
+        {
+            if (pProcess->usPID == usPID)
+            {
+                ulrc = pProcess->usThreads;
+                break;
+            }
+        }
+    }
+    else
+    {
+        // global query:
+        PQGLOBAL16   pg;
+        pg = (PQGLOBAL16)PTR(pps->ulGlobal, 0);
+        ulrc = pg->ulThreads;
+    }
+
+    free(pps);
+    return (ulrc);
+}
+
+/*
+ *@@ prc16QueryThreadInfo:
+ *      this searches for a given thread in a given process
+ *      and fills a given PRCTHREAD structure with lots of
+ *      information about that thread.
+ *
+ *      Returns FALSE upon errors.
+ *
+ *      Note: This function loops thru all processes which
+ *      are currently running and is therefore not terribly
+ *      fast. Use economically.
+ */
+
+BOOL prc16QueryThreadInfo(USHORT usPID, USHORT usTID, PPRCTHREAD pprct)
+{
+    BOOL        brc = FALSE;
+    PQPROCSTAT16  pps = (PQPROCSTAT16)malloc(0x8000);
+    PQPROCESS16 pProcess;
+    DosQProcStatus(pps, 0x8000);
+
+    // find process:
+    for ( pProcess = (PQPROCESS16)PTR(pps->ulProcesses, 0);
+          pProcess->ulType != 3;
+          pProcess = (PQPROCESS16)PTR(pProcess->ulThreadList,
+                                      pProcess->usThreads * sizeof(QTHREAD16))
+        )
+    {
+        if (pProcess->usPID == usPID)
+        {
+            PQTHREAD16 pThread;
+            int i;
+            // process found: find thread
+            for ( i = 0, pThread = (PQTHREAD16)PTR(pProcess->ulThreadList, 0);
+                  i < pProcess->usThreads;
+                  i++, pThread++ )
+            {
+                if (pThread->usTID == usTID)
+                {
+                    // thread found:
+                    pprct->usTID          = pThread->usTID;
+                    pprct->usThreadSlotID = pThread->usThreadSlotID;
+                    pprct->ulBlockID      = pThread->ulBlockID;
+                    pprct->ulPriority     = pThread->ulPriority;
+                    pprct->ulSysTime      = pThread->ulSysTime;
+                    pprct->ulUserTime     = pThread->ulUserTime;
+                    pprct->ucStatus       = pThread->ucStatus;
+
+                    brc = TRUE;
+
+                    break; // thread-for loop
+                }
+            } // end for thread
+            break; // process-for loop
+        }
+    } // end for process
+
+    free(pps);
+    return (brc);
+}
+
+/*
+ *@@ prcQueryPriority:
+ *      shortcut to prc16QueryThreadInfo if you want the priority only.
+ *
+ *      Returns -1 upon errors.
+ *
+ *      Note: This function loops thru all processes which
+ *      are currently running and is therefore not terribly
+ *      fast. Use economically.
+ */
+
+ULONG prc16QueryThreadPriority(USHORT usPID,
+                               USHORT usTID)
+{
+    PRCTHREAD prct;
+    ULONG ulrc = -1;
+    if (prc16QueryThreadInfo(usPID, usTID, &prct))
+        ulrc = prct.ulPriority;
+    return (ulrc);
+}
+
+/*
+ *@@category: Helpers\Control program helpers\Process status\32-bit DosQuerySysState
+ */
+
+/********************************************************************
+ *                                                                  *
+ *   DosQuerySysState (32-bit) interface                            *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ prc32GetInfo:
+ *      nifty interface to DosQuerySysState,
+ *      the 32-bit version of DosQProcStat.
+ *      This returns the head of a newly
+ *      allocated buffer which has plenty
+ *      of pointers for subsequent browing.
+ *
+ *      Use prc32FreeInfo to free the buffer.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ *@@changed V0.9.3 (2000-05-01) [umoeller]: now using DosAllocMem
+ */
+
+PQTOPLEVEL32 prc32GetInfo(APIRET *parc)     // out: error, ptr can be NULL
+{
+    #define BUFSIZE 128000l
+    PCHAR pBuf = NULL; // (PCHAR)malloc(BUFSIZE);
+
+    if (DosAllocMem((PVOID*)&pBuf,
+                    BUFSIZE,
+                    PAG_READ | PAG_WRITE | PAG_COMMIT | OBJ_TILE)
+            == NO_ERROR)
+        if (pBuf)
+        {
+            APIRET arc = DosQuerySysState(0x1f,
+                                          0, 0, 0,
+                                          (PCHAR)pBuf,
+                                          BUFSIZE);
+            if (parc)
+                *parc = arc;
+
+            if (arc == NO_ERROR)
+                return ((PQTOPLEVEL32)pBuf);
+            else
+                DosFreeMem(pBuf);
+        }
+
+    return (NULL);
+}
+
+/*
+ *@@ prc32FreeInfo:
+ *      frees the memory allocated by prc32GetInfo.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ *@@changed V0.9.3 (2000-05-01) [umoeller]: now using DosAllocMem
+ */
+
+VOID prc32FreeInfo(PQTOPLEVEL32 pInfo)
+{
+    DosFreeMem(pInfo);;
+}
+
+/*
+ *@@ prc32FindProcessFromName:
+ *
+ *@@added V0.9.2 (2000-03-05) [umoeller]
+ */
+
+PQPROCESS32 prc32FindProcessFromName(PQTOPLEVEL32 pInfo,
+                                     const char *pcszName) // in: e.g. "pmshell.exe"
+{
+    PQPROCESS32 pProcThis = pInfo->pProcessData;
+    while (pProcThis && pProcThis->rectype == 1)
+    {
+        int i;
+        PQTHREAD32  t = pProcThis->pThreads;
+        PQMODULE32 pModule = prc32FindModule(pInfo,
+                                             pProcThis->usHModule);
+
+        if (pModule)
+        {
+            // the module name is fully qualified, so find the
+            // file name (after the last backslash)
+            if (pModule->pcName)
+            {
+                PSZ pLastBackslash = strrchr(pModule->pcName, '\\');
+                if (pLastBackslash)
+                    // found:
+                    if (stricmp(pLastBackslash + 1, pcszName) == 0)
+                        // matches:
+                        break;
+            }
+        }
+
+        // for next process, skip the threads info;
+        // the next process block comes after the
+        // threads
+        for (i=0;
+             i < pProcThis->usThreadCount;
+             i++,t++)
+            ;
+
+        pProcThis = (PQPROCESS32)t;
+    }
+
+    if (pProcThis->rectype == 1)
+        return (pProcThis);
+    else
+        return (NULL);
+}
+
+/*
+ *@@ prc32FindSem16:
+ *      attempts to find the specified 16-bit semaphore
+ *      in the specified info buffer.
+ *
+ *      The return value points into the pInfo buffer.
+ *      Returns NULL if not found.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+PQSEMA32 prc32FindSem16(PQTOPLEVEL32 pInfo,     // in: as returned by prc32GetInfo
+                          USHORT usSemID)       // in: as in QPROCESS32.pausSem16
+{
+    PQSEM16STRUC32  pSemData = pInfo->pSem16Data;
+    PQSEMA32        pSemThis = &pSemData->sema;
+    ULONG           i = 0;
+
+    while (pSemThis)
+    {
+        _Pmpf(("prc32FindSem16: found usIndex 0x%lX", pSemThis->usIndex));
+        if (/* pSemThis->usIndex */ i == usSemID)
+            return (pSemThis);
+
+        i++;
+        pSemThis = pSemThis->pNext;
+    }
+
+    return (NULL);
+}
+
+/*
+ *@@ prc32FindSem32:
+ *      attempts to find the specified 32-bit semaphore
+ *      in the specified info buffer. This might fail
+ *      because the data isn't always complete.
+ *
+ *      The return value points into the pInfo buffer.
+ *      Returns NULL if not found.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+PQSEM32STRUC32 prc32FindSem32(PQTOPLEVEL32 pInfo,     // in: as returned by prc32GetInfo
+                              USHORT usSemID)         // in: as in QPROCESS32.pausSem16
+{
+    PQSEM32STRUC32  pSemThis = pInfo->pSem32Data;
+
+    /* while (pSemThis)
+    {
+        if (pSemThis->usIndex == usSemID)
+            return (pSemThis);
+
+        pSemThis = pSemThis->pNext;
+    } */
+
+    return (NULL);
+}
+
+/*
+ *@@ prc32FindShrMem:
+ *      attempts to find the specified shared memory
+ *      block description.
+ *
+ *      The return value points into the pInfo buffer.
+ *      Returns NULL if not found.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+PQSHRMEM32 prc32FindShrMem(PQTOPLEVEL32 pInfo,  // in: as returned by prc32GetInfo
+                           USHORT usShrMemID)   // in: as in QPROCESS32.pausShrMems
+{
+    PQSHRMEM32 pShrMem = pInfo->pShrMemData;
+    while (pShrMem)
+    {
+        if (pShrMem->usHandle == usShrMemID)
+            return (pShrMem);
+        pShrMem = pShrMem->pNext;
+    }
+
+    return (NULL);
+}
+
+/*
+ *@@ prc32FindModule:
+ *      attempts to find the specified module description.
+ *
+ *      The return value points into the pInfo buffer.
+ *      Returns NULL if not found.
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+PQMODULE32 prc32FindModule(PQTOPLEVEL32 pInfo,  // in: as returned by prc32GetInfo
+                           USHORT usHModule)
+{
+    PQMODULE32 pModule = pInfo->pModuleData;
+    while (pModule)
+    {
+        if (pModule->usHModule == usHModule)
+            return (pModule);
+        pModule = pModule->pNext;
+    }
+
+    return (NULL);
+}
+
+/*
+ *@@ prc32FindFileData:
+ *
+ *
+ *@@added V0.9.1 (2000-02-12) [umoeller]
+ */
+
+PQFILEDATA32 prc32FindFileData(PQTOPLEVEL32 pInfo,  // in: as returned by prc32GetInfo
+                               USHORT usFileID)     // in: as in QPROCESS32.pausFds
+{
+    PQFILEDATA32 pFile = pInfo->pFileData;
+    while (     (pFile)
+             && (pFile->rectype == 8)  // this is necessary, we'll crash otherwise!!
+          )
+    {
+            if (pFile->filedata->sfn == usFileID)
+                return (pFile);
+
+        pFile = pFile->pNext;
+    }
+
+    return (NULL);
+}
+
+
Index: /trunk/src/helpers/resh.c
===================================================================
--- /trunk/src/helpers/resh.c	(revision 8)
+++ /trunk/src/helpers/resh.c	(revision 8)
@@ -0,0 +1,134 @@
+
+/*
+ *@@sourcefile resh.c:
+ *      resh.c contains Resource helper functions.
+ *
+ *      Function prefixes:
+ *      --  resh*   Resource helper functions
+ *
+ *      These functions are forward-declared in resh.h, which
+ *      must be #include'd first.
+ *
+ *@@header "helpers\resh.h"
+ *@@added V0.9.4 (2000-07-27) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Christoph Schulte Mnting.
+ *
+ *      This program is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published by
+ *      the Free Software Foundation, in version 2 as it comes in the COPYING
+ *      file of this distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *      GNU General Public License for more details.
+ */
+
+// OS2 includes
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSERRORS
+#define INCL_DOS
+#define INCL_DOS
+#include <os2.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\resh.h"
+
+/*
+ *@@category: Helpers\Control program helpers\Resources
+ */
+
+/*
+ *@@ reshWriteResourceToFile:
+ *      get the indicated resource from the current module (usually the
+ *      executable) and write it to a file. If pulBytesWritten is not NULL,
+ *      the number of written bytes is stored to that location.
+ *
+ *      Returns:
+ *      -- NO_ERROR: no error, resource successfully read and written.
+ *      -- ERROR_INVALID_DATA: no. of bytes written != no. of bytes in
+ *                             resource. Shouldn't happen.
+ *
+ *      plus the error codes from DosQueryResourceSize, DosOpen,
+ *      DosGetResource, DosWrite.
+ *
+ *      <B>Warning:</B> If the given file exists, it will be
+ *      overwritten without asking for confirmation!
+ *
+ *@@added V0.9.4 [csm]
+ *@@changed V0.9.4 (2000-07-24) [umoeller]: added hmod, changed return values
+ */
+
+APIRET reshWriteResourceToFile(HMODULE hmod,        // in: module handle or NULLHANDLE for current EXE
+                               ULONG  ulTypeID,        // in:  type of the resource
+                               ULONG  ulNameID,        // in:  ID of the resource
+                               const char *pcszFilename, // in:  name of file to write to
+                               PULONG pulBytesWritten) // out: number of bytes written
+{
+    ULONG   arc = 0;
+    ULONG ulResourceSize;
+    ULONG ulBytesWritten = 0;
+
+    arc = DosQueryResourceSize(hmod,
+                               ulTypeID,
+                               ulNameID,
+                               &ulResourceSize);
+    if (arc == NO_ERROR)
+    {
+        PVOID pvResourceData;
+
+        HFILE hFile;
+        ULONG ulAction = 0;
+
+        // Open file for writing, replace if exists
+        arc = DosOpen((PSZ)pcszFilename,
+                      &hFile,
+                      &ulAction,                           // action taken
+                      ulResourceSize,                      // primary allocation size
+                      FILE_ARCHIVED | FILE_NORMAL,         // file attribute
+                      OPEN_ACTION_CREATE_IF_NEW
+                        | OPEN_ACTION_REPLACE_IF_EXISTS,   // open flags
+                      OPEN_FLAGS_NOINHERIT
+                        | OPEN_FLAGS_SEQUENTIAL            // sequential, not random access
+                        | OPEN_SHARE_DENYREADWRITE         // deny r/w share
+                        | OPEN_ACCESS_WRITEONLY,           // write mode
+                      NULL);                               // no EAs
+
+        // If successful: get resource, write it and close file
+        if (arc == NO_ERROR)
+        {
+            arc = DosGetResource(hmod,
+                                 ulTypeID,
+                                 ulNameID,
+                                 &pvResourceData);
+            if (arc == NO_ERROR)
+            {
+                arc = DosWrite(hFile,
+                               pvResourceData,
+                               ulResourceSize,
+                               &ulBytesWritten);
+                if (arc == NO_ERROR)
+                    if (ulResourceSize != ulBytesWritten)
+                        arc = ERROR_INVALID_DATA;
+
+                DosFreeResource(pvResourceData);
+            }
+
+            DosClose(hFile);
+        }
+    }
+
+    if (pulBytesWritten)
+        *pulBytesWritten = ulBytesWritten;
+
+    return (arc);
+}
+
Index: /trunk/src/helpers/shapewin.c
===================================================================
--- /trunk/src/helpers/shapewin.c	(revision 8)
+++ /trunk/src/helpers/shapewin.c	(revision 8)
@@ -0,0 +1,1707 @@
+
+/*
+ *@@sourcefile shapewin.c:
+ *      contains helper functions for "shaped" PM windows which
+ *      can be non-rectangular in shape and have transparency.
+ *
+ *      This file is new with V0.85.
+ *
+ *      Usage: All PM programs.
+ *
+ *      Function prefixes:
+ *      --  shp*    shape window functions
+ *
+ *      This file consists of two parts:
+ *
+ *  *** Part 1 ***
+ *
+ *  The code in Part 1 implements the "Shape" window. This is used
+ *  to display a bitmap on the screen with transparency.
+ *
+ *  Really, it is not the "Shape" window which displays the bitmap.
+ *  Since PM can only handle rectangular windows, as many subwindows
+ *  as required are created, which are only managed by the (invisible)
+ *  "Shape" window.
+ *
+ *  See shp_fnwpShapeMgr for a window hierarchy.
+ *
+ *  If you are not using the frame window functions in Part 2, the
+ *  shape window class must be registered like this (the declarations
+ *  are in shapewin.h):
+ *
+ +      WinRegisterClass(
+ +                     hab,                // anchor block handle
+ +                     SHAPE_WINDOW_CLASS, // class name (shapewin.h)
+ +                     shp_fnwpShapeMgr,    // window procedure (in this file)
+ +                     0L,                 // default window style
+ +                     sizeof(PVOID)) ;    // class data size
+ *
+ *  You can then create shape windows using WinCreateWindow():
+ +      hwndShape = WinCreateWindow(
+ +                     hwndParent,      // see remark (2)
+ +                     SHAPE_WINDOW_CLASS, // class name (shapewin.h)
+ +                     pszName,         // doesn't matter
+ +                     flStyle,         // can be 0
+ +                     x, y, cx, cy,    // can all be 0; shape window
+ +                                      // size depends on bitmap
+ +                     hwndOwner,       // see remark (2)
+ +                     hwndInsertBehind, // should be HWND_TOP
+ +                     id,
+ +                     pCtlData,        // *SHPCTLDATA, see remark (1)
+ +                     pPresParams);    // NULL
+ *
+ *  Remarks:
+ *
+ *  1)  Shape windows _require_ a SHPCTLDATA structure for input with
+ *      the pCtlData parameter of WinCreateWindow. This structure
+ *      must contain the following:
+ +          SHORT   cx, cy ;       // bitmap size
+ +          HPS     hpsMask;       // HPS with bitmap selected into
+ +          HPS     hpsDraw;       // HPS used for drawing the bitmap
+ *
+ *      Note that the bitmap in hpsMask determines the drawing mask
+ *      of the bitmap, while the bitmap in hpsDraw determines the
+ *      bitmap to be drawn. You can use the same HPS (and thus
+ *      the same bitmap) for both, but you can also use different
+ *      ones.
+ *
+ *      You _must_ select the bitmap(s) to be displayed into the HPS's,
+ *      using GpiSetBitmap, or nothing will work. shpLoadBitmap
+ *      in Part 2 of this file offers you a one-shot function for
+ *      loading a bitmap either from resources or a PM 1.3 *.BMP
+ *      file.
+ *
+ *      Transparency is determined according to the most lower-left
+ *      pixel of the bitmap in hpsMask. That is, all hpsDraw bitmap
+ *      pixels which have the same color as hpsMask pixel (0, 0)
+ *      will be made transparent.
+ *
+ *      During window creation, the shape window analyzes the bitmap
+ *      which was selected into hpsMask and creates lots of rectangular
+ *      PM windows as neccessary. This is dependent of the
+ *      "transparency" pixels of the bitmap.
+ *
+ *      There is no limit for the size of the bitmap (in the HPS's).
+ *      But you should be very careful with large bitmaps, because
+ *      this can block the system forever. Shape windows are real
+ *      CPU hogs. The number of PM subwindows created depends on the
+ *      size of the bitmap and on the transparency. The calculations
+ *      are line-based:
+ *      --  each line is examined, and a PM window is created for
+ *          each line of the input bitmap;
+ *      --  if this line contains "transparency" pixels, more PM
+ *          windows are created accordingly for that line.
+ *      --  However, if several vertically adjacent subwindows have
+ *          the same left and right coordinates, they are combined
+ *          into one window.
+ *
+ *      As a result, the more single transparent pixels you have,
+ *      the more windows need to be created. The more rectangular
+ *      transparent areas you have, the less windows need to be
+ *      created.
+ *
+ *  2)  The "Shape" window requires both a parent and an owner window.
+ *      As always, the parent window determines the visibility, while
+ *      the owner window requires a more abstract relationship. With
+ *      shape windows, the owner window is important because all the
+ *      input messages (for mouse and keyboard input) are forwarded
+ *      to the owner.
+ *
+ *      So it's best to set the parent to HWND_DESKTOP and specify
+ *      a frame window for the owner. If you don't already have a
+ *      frame window in your application, you can create an invisible
+ *      frame window just for message handling. That's what the shp*
+ *      functions in Part 2 of this file do (Akira Hatakeyama calls
+ *      this the "Voodoo" feature).
+ *
+ *      Never use the shape window itself as a top-level window,
+ *      but one of your own windows instead, which own the shape
+ *      window.
+ *
+ *      Note that the keyboard and mouse input messages which are
+ *      forwarded to the owner contain mouse coordinates relative
+ *      to the rectangular shape subwindow which actually received
+ *      the message, not relative to the frame window. So you better
+ *      determine the real mouse position using WinQueryPointerPos().
+ *
+ *  Note that the size of the shape window is defined once upon
+ *  creation and cannot be changed later because this would conflict
+ *  with all the subwindows which were created. For the same reason,
+ *  you cannot alter the bitmap of the shape window after creation.
+ *  You must create a second shape bitmap for that.
+ *  Thus all the size parameters in WinSetWindowPos are swallowed and
+ *  ignored.
+ *
+ *  *** Part 2 ***
+ *
+ *  This implements a proper invisible frame window for displaying
+ *  shaped bitmap windows directly on the desktop.
+ *  See shpCreateWindows for a working example.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\shapewin.h"
+ */
+
+/*
+ *  Credits:
+ *      1)  Functions for shaped windows.
+ *          This is based on the ShapeWin library 1.01 (SHAPEWIN.ZIP at
+ *          Hobbes), (C) 1998, 1999 Software Research Associates, Inc.
+ *          This has been written by Akira Hatakeyama (akira@sra.co.jp).
+ *          Updated by Takayuki Suwa (jjsuwa@ibm.net).
+ *          The original was placed under the GPL.
+ *
+ *          Changes made by Ulrich Mller (February 1999):
+ *          --  some speedup (marked with *UM)
+ *          --  renamed and commented out almost all of the functions
+ *              and structures to make their use more lucid.
+ *
+ *      2)  Functions for easily creating shape frame windows from a
+ *          given HBITMAP. This has been written by me, after getting
+ *          some inspiration from the sample programs in SHAPEWIN.ZIP.
+ *
+ *      Copyright (C) 1998-2000
+ *                  Ulrich Mller,
+ *                  Akira Hatakeyama,
+ *                  Takayuki Suwa.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ *
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WIN
+#define INCL_WINSYS
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPIPRIMITIVES
+#define INCL_GPIBITMAPS
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\gpih.h"
+#include "helpers\shapewin.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\Shaped windows (transparency)
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+BOOL    G_ShapeRegisteredRegion = FALSE;
+
+/* ******************************************************************
+ *                                                                  *
+ *   Part 1: Shape window functions                                 *
+ *                                                                  *
+ ********************************************************************/
+
+// forward declarations for structures, because
+// the structures are referenced from each other
+typedef struct  _SHPWINCTLDATA  *PSHPWINCTLDATA;
+typedef struct  _SHPREGION  *PSHPREGION;
+
+/*
+ * SHPWINCTLDATA:
+ *      internal shape window control data.
+ *      This is the shape-window-internal structure
+ *      to allow the shape window to manage itself.
+ *      This is created in WM_CREATE with the data
+ *      passed in the mp1 and mp2 parameters
+ *      (CREATESTRUCT and SHPCTLDATA).
+ *      This is stored in a shape window's window words.
+ */
+
+typedef struct _SHPWINCTLDATA
+{
+    // common window parameters
+    // (copied from CREATESTRUCT in WM_CREATE)
+    HWND        hwndShape ;    // main shape window handle
+    HWND        hwndParent;    // shape window's parent (should be HWND_DESKTOP)
+    HWND        hwndOwner ;    // shape window's owner, to whom we'll forward all
+                               // all the input messages
+    USHORT      id        ;    // window ID
+    SHORT       x         ;    // x - horz. position
+    SHORT       y         ;    // y - vert. position
+    SHORT       cx        ;    // horz. size
+    SHORT       cy        ;    // vert. size
+    PSZ         pszText   ;    // window text
+
+    // additional window parameters
+    HPS         hpsDraw;       // the input HPS with the bitmap
+    ULONG       nRegion;       // number of "regions"
+    PSHPREGION  aRegion;       // array of "regions"
+
+} SHPWINCTLDATA;
+
+/*
+ * SHPREGION:
+ *      this is the structure for shape window
+ *      "regions". Note that has nothing to do
+ *      with GPI regions, but is simply a structure
+ *      for each rectangular area in the shape
+ *      window's bitmap which can be displayed
+ *      as a rectangular PM window.
+ */
+
+typedef struct _SHPREGION
+{
+    PSHPWINCTLDATA   pCtrl;   // link to parent
+    HWND        hwnd ;         // drawing window
+    RECTL       rect ;         // rectangle of sub-bitmap
+} SHPREGION;
+
+/*
+ * SHPSPAN:
+ *      this is a temporary structure used during
+ *      the creation of SHPREGION's from the
+ *      input bitmap.
+ */
+
+typedef struct _SHPSPAN {
+    ULONG   nSize   ;
+    ULONG   nUsed   ;
+    RECTL   aSpan[1];
+} SHPSPAN, *PSHPSPAN;
+
+// the following defines how many SHPSPAN structures
+// will be initially created and afterwards for span
+// expansions by spanExpand
+#define SPAN_ALLOC_STEP    1024
+
+/*
+ * spanFree:
+ *
+ */
+
+void spanFree(PSHPSPAN pSpan)
+{
+    if (pSpan != NULL)
+        free(pSpan);
+}
+
+/*
+ * spanCreate:
+ *      this creates SPAN_ALLOC_STEP SHPSPAN structures.
+ */
+
+PSHPSPAN spanCreate(void)
+{
+    PSHPSPAN pSpan;
+    int     len  ;
+
+    len = sizeof(SHPSPAN) + sizeof(RECTL) * SPAN_ALLOC_STEP;
+
+    if ((pSpan = (PSHPSPAN)malloc(len)) == NULL)
+        return NULL;
+
+    memset(pSpan, 0, len);
+    pSpan->nSize = SPAN_ALLOC_STEP;
+    pSpan->nUsed = 0   ;
+    return pSpan;
+}
+
+/*
+ * spanExpand:
+ *      this expands the memory used for the SHPSPAN
+ *      structures if more spans need to be appended.
+ *      Another SPAN_ALLOC_STEP SHPSPAN structures are created.
+ */
+
+PSHPSPAN spanExpand(PSHPSPAN pOld)
+{
+    PSHPSPAN pNew;
+    int     len ;
+
+    // TRACE("spanExpand\n");
+
+    len = sizeof(SHPSPAN) + sizeof(RECTL) * (pOld->nSize + SPAN_ALLOC_STEP);
+
+    if ((pNew = (PSHPSPAN)malloc(len)) == NULL)
+        return NULL;
+
+    memset(pNew, 0, len);
+    pNew->nSize = pOld->nSize + SPAN_ALLOC_STEP;
+    pNew->nUsed = pOld->nUsed;
+    memcpy(pNew->aSpan, pOld->aSpan, sizeof(RECTL) * pOld->nUsed);
+    spanFree(pOld);
+
+    return pNew;
+}
+
+/*
+ * spanAppend:
+ *      this adds a new SHPSPAN to the list of spans.
+ *
+ */
+
+PSHPSPAN spanAppend(PSHPSPAN pSpan,
+                    int y,       // bottom y; top y = y+1
+                    int x1,      // left x
+                    int x2)      // right x
+{
+    int     i;
+    PRECTL  p;
+    PSHPSPAN pNew;
+
+    // TRACE("spanAppend %d (%d %d)\n", y, x1, x2);
+
+    /*
+     * check if continuous one
+     */
+
+    for (i = 0; i < pSpan->nUsed; i++) {
+        p = &pSpan->aSpan[i];
+        if (p->yTop == y && p->xLeft == x1 && p->xRight == x2)
+        {
+            p->yTop += 1;
+            return pSpan;      // merged
+        }
+    }
+
+    /*
+     * if not enough space, expand
+     */
+
+    if ((pSpan->nUsed + 1) >= pSpan->nSize)
+    {
+        if ((pNew = spanExpand(pSpan)) == NULL)
+            return NULL;
+
+        pSpan = pNew;
+    }
+
+    /*
+     * append a rectangle
+     */
+
+    pSpan->aSpan[pSpan->nUsed].yTop = y + 1;
+    pSpan->aSpan[pSpan->nUsed].yBottom = y ;
+    pSpan->aSpan[pSpan->nUsed].xLeft   = x1;
+    pSpan->aSpan[pSpan->nUsed].xRight  = x2;
+    pSpan->nUsed += 1;
+
+    return pSpan;
+}
+
+#ifdef  SHPDEBUG
+
+    /*
+     * ptisin:
+     *
+     */
+
+    BOOL ptisin(PSHPSPAN pSpan, int x, int y)
+    {
+        int     i;
+
+        for (i = 0; i < pSpan->nUsed; i++)
+        {
+            if (y >= pSpan->aSpan[i].yTop) {
+                continue;
+            }
+            if (y < pSpan->aSpan[i].yBottom) {
+                continue;
+            }
+            if (x < pSpan->aSpan[i].xLeft) {
+                continue;
+            }
+            if (x >= pSpan->aSpan[i].xRight) {
+                continue;
+            }
+            return TRUE;
+        }
+        return FALSE;
+    }
+
+    /*
+     * dumpSpan:
+     *
+     */
+
+    void dumpSpan(PSHPSPAN pSpan)
+    {
+        int     i, maxx, maxy, x, y;
+
+        TRACE("dumpSpan %d\n", pSpan->nUsed);
+
+        maxx = maxy = 0;
+
+        for (i = 0; i < pSpan->nUsed; i++)
+        {
+            if (pSpan->aSpan[i].yTop > maxy)
+            {
+                maxy = pSpan->aSpan[i].yTop;
+            }
+            if (pSpan->aSpan[i].xRight > maxx)
+            {
+                maxx = pSpan->aSpan[i].xRight;
+            }
+        }
+
+        for (y = maxy - 1; y >= 0; y--)
+        {
+            printf("%03d : ", y);
+            for (x = 0; x < maxx; x++)
+            {
+                if (ptisin(pSpan, x, y))
+                    printf("#");
+                else
+                    printf("_");
+            }
+            printf("\n");
+        }
+    }
+
+#endif  // SHPDEBUG
+
+/*
+ * shprgnDraw:
+ *      this gets called upon receiving WM_PAINT in
+ *      shp_fnwpShapeRegion. We simply draw the
+ *      subrectangle from the shape window's bitmap
+ *      which corresponds to our region rectangle.
+ */
+
+void shprgnDraw(HPS hps, PSHPREGION pRgn)
+{
+    POINTL      apt[3];
+
+    apt[0].x = 0;
+    apt[0].y = 0;
+    apt[1].x = (pRgn->rect.xRight - pRgn->rect.xLeft);
+    apt[1].y = (pRgn->rect.yTop - pRgn->rect.yBottom);
+    apt[2].x = pRgn->rect.xLeft  ;
+    apt[2].y = pRgn->rect.yBottom;
+    GpiBitBlt(hps, pRgn->pCtrl->hpsDraw, 3, apt, ROP_SRCCOPY, 0);
+}
+
+/*
+ *@@ shp_fnwpShapeRegion:
+ *      this is the window procedure for each of the
+ *      shape window's rectangular subwindows.
+ *
+ *      See shp_fnwpShapeMgr for a window hierarchy.
+ */
+
+MRESULT EXPENTRY shp_fnwpShapeRegion(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    PSHPREGION   pRgn;
+    HPS         hps ;
+
+    pRgn = (PSHPREGION) WinQueryWindowPtr(hwnd, 0);
+
+    switch (msg)
+    {
+        /*
+         * WM_CREATE:
+         *      store the SHPREGION pointer in our
+         *      window words.
+         */
+
+        case WM_CREATE:
+            // TRACE("WM_CREATE\n");
+            pRgn = (PSHPREGION)PVOIDFROMMP(mp1);
+            WinSetWindowPtr(hwnd, 0, (PVOID) pRgn);
+        return (MRESULT) FALSE;
+
+        /*
+         * WM_PAINT:
+         *      draw our subrectangle of the main bitmap.
+         */
+
+        case WM_PAINT:
+            // TRACE("WM_PAINT (%d %d) (%d %d)\n", pRgn->rect.yBottom, pRgn->rect.xLeft, pRgn->rect.yTop, pRgn->rect.xRight);
+            hps = WinBeginPaint(hwnd, NULLHANDLE, NULL);
+            shprgnDraw(hps, pRgn);
+            WinEndPaint(hps)  ;
+        return (MRESULT) 0;
+
+        case WM_MOUSEMOVE    :
+        case WM_BUTTON1DOWN  :
+        case WM_BUTTON1UP    :
+        case WM_BUTTON1CLICK :
+        case WM_BUTTON1DBLCLK:
+        case WM_BUTTON2DOWN  :
+        case WM_BUTTON2UP    :
+        case WM_BUTTON2CLICK :
+        case WM_BUTTON2DBLCLK:
+        case WM_BUTTON3DOWN  :
+        case WM_BUTTON3UP    :
+        case WM_BUTTON3CLICK :
+        case WM_BUTTON3DBLCLK:
+        case WM_CHAR         :
+        case WM_VIOCHAR      :
+        case WM_BEGINDRAG    :
+        case WM_ENDDRAG      :
+        case WM_SINGLESELECT :
+        case WM_OPEN         :
+        case WM_CONTEXTMENU  :
+        case WM_CONTEXTHELP  :
+        case WM_TEXTEDIT     :
+        case WM_BEGINSELECT  :
+        case WM_ENDSELECT    :
+            // forward all these to the shape window
+            return WinSendMsg(pRgn->pCtrl->hwndShape, msg, mp1, mp2);
+    }
+    return WinDefWindowProc(hwnd, msg, mp1, mp2);
+}
+
+/*
+ * shpmgrWMAdjustWindowPos:
+ *      adjust the region windows' positions, visibility etc.
+ *      by going thru the whole region windows list and
+ *      repositioning all the windows.
+ *      This gets called upon receiving WM_ADJUSTWINDOWPOS
+ *      in shp_fnwpShapeMgr.
+ */
+
+int shpmgrWMAdjustWindowPos(PSHPWINCTLDATA pCtrl, PSWP pSwp)
+{
+    int         i   ;
+    PSHPREGION   pRgn;
+    ULONG       fl  ;
+    // HPS         hps ;
+
+    PSWP pswpArray;
+
+    // TRACE("shpmgrWMAdjustWindowPos - %d, %08x\n", pCtrl->nRegion, pSwp->fl);
+
+    if (pCtrl->nRegion == 0 || pCtrl->aRegion == NULL)
+    {
+        // TRACE("ShpDrawRegion - no region to open\n");
+        return -1;
+    }
+
+    if ((fl = pSwp->fl) & SWP_MOVE)
+    {
+        pCtrl->x = pSwp->x;
+        pCtrl->y = pSwp->y;
+    }
+
+    pswpArray = (PSWP) malloc(sizeof(SWP) * pCtrl->nRegion);
+
+    // go thru all the "regions" and adjust their sizes
+    for (i = 0, pRgn = pCtrl->aRegion;
+         i < pCtrl->nRegion;
+         i++, pRgn++)
+    {
+        pswpArray[i].fl = fl;
+        pswpArray[i].cy = (pRgn->rect.yTop - pRgn->rect.yBottom);
+        pswpArray[i].cx = (pRgn->rect.xRight - pRgn->rect.xLeft);
+        pswpArray[i].y = (pCtrl->y + pRgn->rect.yBottom);
+        pswpArray[i].x = (pCtrl->x + pRgn->rect.xLeft);
+        pswpArray[i].hwndInsertBehind = pSwp->hwndInsertBehind;
+        pswpArray[i].hwnd = pRgn->hwnd;
+    }
+
+    // set all window positions at once
+    WinSetMultWindowPos(WinQueryAnchorBlock(pCtrl->hwndShape),
+                        &pswpArray[0],
+                        pCtrl->nRegion);
+    free((void*) pswpArray);
+
+    // calling WinInvalidateRect is not neccessary,
+    // because the windows' update regions are already
+    // properly set by WinSetMultWindowPos (*UM)
+
+    /* for (i = 0, pRgn = pCtrl->aRegion;
+         i < pCtrl->nRegion;
+         i++, pRgn++)
+    {
+        if (fl & (SWP_MOVE | SWP_ZORDER)) {
+            WinInvalidateRect(pRgn->hwnd, NULL, FALSE);
+        }
+    } */
+    return 0;
+}
+
+/*
+ * shpmgrUpdateRegions:
+ *      this gets called by shp_fnwpShapeMgr upon receiving
+ *      SHAPEWIN_MSG_UPDATE.
+ */
+
+int shpmgrUpdateRegions(PSHPWINCTLDATA pCtrl, PRECTL pRect)
+{
+    RECTL       rect, intern;
+    int         i   ;
+    PSHPREGION   pRgn;
+    HAB         hab ;
+    // HPS         hps ;
+
+    // TRACE("shpmgrUpdateRegions\n");
+
+    hab = WinQueryAnchorBlock(pCtrl->hwndShape);
+
+    if (pRect == NULL) {
+        rect.xLeft   = 0       ;
+        rect.yBottom = 0       ;
+        rect.xRight = pCtrl->cx;
+        rect.yTop   = pCtrl->cy;
+        pRect = &rect;
+    }
+    for (i = 0, pRgn = pCtrl->aRegion; i < pCtrl->nRegion; i++, pRgn++)
+    {
+        if (WinIntersectRect(hab, &intern, pRect, &pRgn->rect) == FALSE) {
+            continue;
+        } else {
+            WinInvalidateRect(pRgn->hwnd, NULL, FALSE);
+        }
+    }
+    return 0;
+}
+
+/*
+ * shpmgrParseBitmap:
+ *      this gets called from shpmgrWMCreate_Bitmap2Regions when the
+ *      shape region windows need to be created.
+ *      This function does the actual bitmap analysis and
+ *      creates a large number of SHPSPAN structures according
+ *      to the bitmaps, which can later be turned into
+ *      PM windows.
+ *      This happens during the processing of WM_CREATE in
+ *      shp_fnwpShapeMgr.
+ *      CPU usage: relatively low. Maybe 5% of the whole creation
+ *      procedure.
+ *
+ *@@changed V0.9.1 (99-12-03): fixed memory leak
+ */
+
+PSHPSPAN shpmgrParseBitmap(HPS hps, PBITMAPINFOHEADER2 bmih2)
+{
+    int             blen, hlen;
+    PUCHAR          buf;
+    PBITMAPINFO2    pbmi;
+    PSHPSPAN        pSpan, pNew;
+    int             x, y, k;
+    LONG            first, color;
+    BOOL            inspan;
+    int             left;
+
+    // TRACE("shpmgrParseBitmap\n");
+
+    hlen = sizeof(BITMAPINFO2) + sizeof(RGB) * 256;
+    blen = ((bmih2->cBitCount * bmih2->cx + 31) / 32) * bmih2->cPlanes * 4;
+
+    pbmi = (PBITMAPINFO2)malloc(hlen);
+    buf = (PUCHAR)malloc(blen);
+
+    if (pbmi == NULL || buf == NULL)
+    {
+        // TRACE("shpmgrParseBitmap - failed to alloc %d %d\n", hlen, blen);
+        if (pbmi)
+            free(pbmi);
+        if (buf)
+            free(buf);
+        return NULL;
+    }
+    memcpy(pbmi, bmih2, sizeof(BITMAPINFOHEADER2));
+
+    if ((pSpan = spanCreate()) == NULL)
+    {
+        // TRACE("shpmgrParseBitmap - failed to make\n");
+        free(pbmi);
+        free(buf);
+        return NULL;
+    }
+
+    first = -1;
+
+    for (y = 0; y < bmih2->cy; y++)
+    {
+
+        // TRACE("shpmgrParseBitmap - scan line %d\n", y); fflush(stdout);
+
+        GpiQueryBitmapBits(hps, y, 1, (PBYTE)buf, pbmi);
+
+        for (x = 0, inspan = FALSE; x < bmih2->cx; x++)
+        {
+            k = x * 3;
+            color = ((buf[k] << 16) | (buf[k+1] << 8) | buf[k+2]);
+
+            if (first < 0)
+            {
+                // first iteration: get the very first color (that
+                // is the pixel at (0, 0)), with which we will
+                // compare the others to determine the mask
+                first = color;
+            }
+
+            if (inspan == FALSE && color != first)
+            {
+                inspan = TRUE;
+                left = x;
+            }
+            else if (inspan == TRUE && color == first)
+            {
+                // transparent color found:
+                // create new span
+                inspan = FALSE;
+                if ((pNew = spanAppend(pSpan, y, left, x)) != NULL)
+                {
+                    pSpan = pNew;
+                }
+                else
+                {
+                    // TRACE("shpmgrParseBitmap - failed to extend\n");
+                    break;
+                }
+            }
+        }
+        if (inspan == TRUE)
+        {
+            if ((pNew = spanAppend(pSpan, y, left, x)) != NULL)
+                pSpan = pNew;
+            else
+                // TRACE("shpmgrParseBitmap - failed to extend\n");
+                break;
+        }
+    }
+
+#ifdef  SHPDEBUG
+    dumpSpan(pSpan);
+#endif
+
+    // fixed these memory leaks V0.9.1 (99-12-03)
+    if (pbmi)
+        free(pbmi);
+    if (buf)
+        free(buf);
+
+    return pSpan;
+}
+
+/*
+ * shpmgrWMCreate_Bitmap2Regions:
+ *      this gets called from shpmgrWMCreate (WM_CREATE) to have a
+ *      drawing region created from the bitmap which has been
+ *      selected into the given HPS.
+ *
+ *      First step in WM_CREATE.
+ */
+
+int shpmgrWMCreate_Bitmap2Regions(PSHPWINCTLDATA pCtrl,  // in: shape control data
+                                  HPS hpsMask)      // in: HPS with selected bitmap
+{
+    HAB             hab;
+    HDC             hdc;
+    HPS             hps;
+    SIZEL           siz;
+    HBITMAP         hbm,
+                    hbmPrevious;
+    BITMAPINFOHEADER2   bmi;
+    POINTL          apt[3];
+    PSHPSPAN        pSpan;
+    PSHPREGION      pRegn;
+    int             i;
+
+    // TRACE("shpmgrWMCreate_Bitmap2Regions\n");
+
+    hab = WinQueryAnchorBlock(pCtrl->hwndShape);
+
+    /*
+     * Create Memory DC & HPS
+     */
+
+    hdc = DevOpenDC(hab, OD_MEMORY, "*", 0, NULL, NULLHANDLE);
+    siz.cx = siz.cy = 0;
+    hps = GpiCreatePS(hab, hdc, &siz,
+            PU_PELS | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC);
+
+    /*
+     * Create Bitmap and relate to memory PS
+     */
+
+    memset(&bmi, 0, sizeof(bmi));
+
+    bmi.cbFix = sizeof(BITMAPINFOHEADER2);
+    bmi.cx = pCtrl->cx;
+    bmi.cy = pCtrl->cy;
+    bmi.cPlanes       = 1 ;
+    bmi.cBitCount     = 24;
+    bmi.ulCompression = 0 ;
+    bmi.cclrUsed      = 0 ;
+    bmi.cclrImportant = 0 ;
+
+    hbm = GpiCreateBitmap(hps, &bmi, 0, NULL, NULL);
+
+    hbmPrevious = GpiSetBitmap(hps, hbm);
+
+    /*
+     * Copy in Mask Pattern
+     */
+
+    // TRACE("shpmgrWMCreate_Bitmap2Regions - copyin %d x %d mask pattern\n", bmi.cx, bmi.cy);
+
+    apt[0].x = 0;          // Target
+    apt[0].y = 0;
+    apt[1].x = bmi.cx;
+    apt[1].y = bmi.cy;
+    apt[2].x = 0;          // Source
+    apt[2].y = 0;
+
+    if (GpiBitBlt(hps, pCtrl->hpsDraw, 3, apt, ROP_SRCCOPY, 0) == GPI_ERROR) {
+        // TRACE("MakeRect - BitBlt Failed %08x, hdc %08x, hps %08x, hbm %08x\n",
+                // WinGetLastError(hab), hdc, hps, hbm);
+    }
+
+    /*
+     * Parse Mask Pattern
+     */
+
+    // call shpmgrParseBitmap; this does the actual creation of the
+    // "spans" by comparing all bitmap pixels to the (0, 0)
+    // pixel
+    if ((pSpan = shpmgrParseBitmap(hps, &bmi)) != NULL)
+    {
+        if ((pRegn = (PSHPREGION) malloc(sizeof(SHPREGION) * pSpan->nUsed)) == NULL)
+        {
+        // TRACE("shpmgrWMCreate_Bitmap2Regions - failed to alloc\n");
+        }
+        else
+        {
+            // mask created successfully:
+            pCtrl->nRegion = pSpan->nUsed;
+            pCtrl->aRegion = pRegn;
+
+            for (i = 0; i < pSpan->nUsed; i++)
+            {
+                pRegn[i].pCtrl    = pCtrl;
+                pRegn[i].hwnd = NULLHANDLE;
+                pRegn[i].rect.xLeft   = pSpan->aSpan[i].xLeft  ;
+                pRegn[i].rect.xRight  = pSpan->aSpan[i].xRight ;
+                pRegn[i].rect.yTop    = pSpan->aSpan[i].yTop   ;
+                pRegn[i].rect.yBottom = pSpan->aSpan[i].yBottom;
+            }
+        }
+        spanFree(pSpan);
+    }
+
+    /*
+     * dispose local resources
+     */
+
+    GpiSetBitmap(hps, hbmPrevious); // (*UM)
+    GpiDeleteBitmap(hbm);
+    GpiDestroyPS(hps);
+    DevCloseDC(hdc);
+
+    return 0;
+}
+
+/*
+ * shpmgrWMCreate_Regions2Windows:
+ *      this gets called from shpmgrWMCreate (WM_CREATE) to convert
+ *      all the "regions" that have been created by shpmgrWMCreate_Bitmap2Regions
+ *      in pCtrl to individual rectangular PM windows.
+ *
+ *      CPU usage: extremely high, because all the dull PM functions
+ *      take so damn long. Calculating the "regions" isn't really
+ *      difficult, but it's the PM windows which cause the CPU hog.
+ *
+ *      Second step in WM_CREATE.
+ */
+
+int shpmgrWMCreate_Regions2Windows(PSHPWINCTLDATA pCtrl)
+{
+    int         i   ;
+    PSHPREGION   pRgn;
+    ULONG       flStyle;
+
+    // TRACE("shpmgrWMCreate_Regions2Windows %d regions\n", pCtrl->nRegion);
+
+    // check if any "regions" exist at all
+    if (pCtrl->nRegion == 0 || pCtrl->aRegion == NULL)
+    {
+        // TRACE("shpmgrWMCreate_Regions2Windows - no region to open\n");
+        return -1;
+    }
+
+    if (G_ShapeRegisteredRegion == FALSE)
+    {
+        // first call: register "shape region" class
+        WinRegisterClass(WinQueryAnchorBlock(pCtrl->hwndShape),
+                         WC_SHAPE_REGION,
+                         shp_fnwpShapeRegion,
+                         CS_PARENTCLIP
+                             // use parent's clipping region instead
+                             // of the one of the region window
+                            | CS_CLIPSIBLINGS
+                             // don't allow the subwindows to paint
+                             // over siblings (ie. windows with the
+                             // same parent); this is neccessary if
+                             // HWND_DESKTOP is the parent
+                            | CS_SYNCPAINT,
+                             // paint immediately
+                         sizeof(PVOID));
+        G_ShapeRegisteredRegion = TRUE;
+    }
+
+    flStyle = 0;
+
+    // now go thru the "regions" list
+    for (i = 0, pRgn = pCtrl->aRegion;
+         i < pCtrl->nRegion;
+         i++, pRgn++)
+    {
+        // and create a window for each "region"
+        pRgn->hwnd = WinCreateWindow(
+                pCtrl->hwndParent,      // copy parent window from shape window
+                            // changed (*UM)
+                        // HWND_DESKTOP,           // Parent Window
+                WC_SHAPE_REGION,        // window class
+                NULL,                   // window text
+                flStyle,                // window style
+                (pCtrl->x + pRgn->rect.xLeft),              // x
+                (pCtrl->y + pRgn->rect.yBottom),            // y
+                (pRgn->rect.xRight - pRgn->rect.xLeft),     // cx
+                (pRgn->rect.yTop - pRgn->rect.yBottom),     // cy
+                pCtrl->hwndOwner,       // Owner Window
+                HWND_TOP,               // Z-Order
+                i,                      // Window ID
+                pRgn,                   // Control Data
+                NULL);                  // Pres. Param.
+    }
+    return 0;
+}
+
+/*
+ * shpmgrFreeRegion:
+ *      cleanup during WM_DESTROY.
+ */
+
+int shpmgrFreeRegion(PSHPWINCTLDATA pCtrl)
+{
+    int         i   ;
+    PSHPREGION   pRgn;
+
+    for (i = 0, pRgn = pCtrl->aRegion;
+         i < pCtrl->nRegion;
+         i++, pRgn++)
+        WinDestroyWindow(pRgn->hwnd);
+
+    free(pCtrl->aRegion);
+
+    return 0;
+}
+
+/*
+ * shpmgrWMCreate:
+ *      this initializes the "shape window".
+ *      This gets called upon receiving WM_CREATE in shp_fnwpShapeMgr.
+ *      The procedure is as follows:
+ *      1)  the bitmap in pData->hpsDraw is analyzed pixel by
+ *          pixel to create a number of "regions" from it
+ *          (which are not GPI regions, but an array of SHPREGION
+ *          structures. At least one of these structures is
+ *          created for each line. This is done by calling
+ *          shpmgrWMCreate_Bitmap2Regions.
+ *      2)  We then call shpmgrWMCreate_Regions2Windows to actually create
+ *          PM windows from all these structures.
+ */
+
+PSHPWINCTLDATA shpmgrWMCreate(HWND hwnd, // in: shape window
+                              PCREATESTRUCT pWin,    // in: create struct of WM_CREATE
+                              PSHPCTLDATA pData)       // in: SHPCTLDATA struct (WM_CREATE mp1)
+{
+    PSHPWINCTLDATA   pCtrl;
+
+    if (pData->hpsDraw == NULLHANDLE || pData->hpsMask == NULLHANDLE) {
+        return NULL;
+    }
+
+    // create new PSHPWINCTLDATA structure
+    if ((pCtrl = (PSHPWINCTLDATA) malloc(sizeof(SHPWINCTLDATA))) == NULL) {
+        return NULL;
+    }
+
+    /*
+     * Setup Common Window Parameters
+     */
+
+    pCtrl->hwndShape  = hwnd;
+    pCtrl->hwndParent = pWin->hwndParent;
+    pCtrl->hwndOwner  = pWin->hwndOwner ;
+    pCtrl->id         = pWin->id;
+    pCtrl->x          = pWin->x ;
+    pCtrl->y          = pWin->y ;
+    pCtrl->cx         = pWin->cx;
+    pCtrl->cy         = pWin->cy;
+
+    /*
+     * Setup Image Window's Control Data
+     */
+
+    pCtrl->cx = pData->cx;
+    pCtrl->cy = pData->cy;
+    pCtrl->hpsDraw = pData->hpsDraw;
+
+    // now create "regions" from bitmap;
+    // this will store the "regions" in pCtrl
+    shpmgrWMCreate_Bitmap2Regions(pCtrl, pData->hpsMask);
+
+    // now create as many rectangular PM
+    // windows as we have "regions" in pCtrl
+    shpmgrWMCreate_Regions2Windows(pCtrl);
+
+    return pCtrl;
+}
+
+/*
+ * shpmgrWMDestroy:
+ *      this cleans up the shape's resources.
+ *      Gets called upon receiving WM_DESTROY in
+ *      shp_fnwpShapeMgr.
+ */
+
+void shpmgrWMDestroy(PSHPWINCTLDATA pCtrl)
+{
+    if (pCtrl == NULL)
+        return;
+
+    shpmgrFreeRegion(pCtrl);
+    free(pCtrl);
+}
+
+/*
+ *@@ shp_fnwpShapeMgr:
+ *      this is the window procedure for the "shape window manager".
+ *      Register this procedure with WinRegisterWindowClass.
+ *
+ *      This does the transformation of the bitmap into many
+ *      rectangular subwindows upon WM_CREATE, each of which
+ *      will use shp_fnwpShapeRegion as its window proc.
+ *
+ *      There should be the following window hierarchy (the
+ *      lines signify ownership):
+ *
+ +      Your owner window
+ +         |
+ +         +-- owns "shape manager" window (this window proc)
+ +               |
+ +               +-- lots of "region" windows (shp_fnwpShapeRegion)
+ +               +-- ...
+ *
+ *      The "region" windows are owned by the "shape manager" window,
+ *      but have the same parent as the "shape manager" window.
+ *      Normally, this should be set to HWND_DESKTOP when creating the
+ *      "shape" window.
+ *
+ *      This window procedure forwards the following messages
+ *      to its owner:
+ *      --  WM_MOUSEMOVE
+ *      --  all WM_BUTTONxxx messages
+ *      --  WM_CHAR
+ *      --  WM_VIOCHAR
+ *      --  WM_BEGINDRAG
+ *      --  WM_ENDDRAG
+ *      --  WM_SINGLESELECT
+ *      --  WM_OPEN
+ *      --  WM_CONTEXTMENU
+ *      --  WM_CONTEXTHELP
+ *      --  WM_TEXTEDIT
+ *      --  WM_BEGINSELECT
+ *      --  WM_ENDSELECT
+ */
+
+MRESULT EXPENTRY shp_fnwpShapeMgr(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    PSHPWINCTLDATA   pCtrl;
+    PSWP        pswp ;
+    // SHORT       sx; // , sy;
+
+    pCtrl = (PSHPWINCTLDATA) WinQueryWindowPtr(hwnd, 0);
+
+    switch (msg)
+    {
+
+        /*
+         * WM_CREATE:
+         *      shape window is being created.
+         *      Parameters:
+         *          PSHPCTLDATA mp1
+         *          PCREATESTRUCT mp2
+         *      Shaped windows must have a SHPCTLDATA
+         *      structure in mp1.
+         */
+
+        case WM_CREATE:
+            // TRACE("WM_CREATE\n");
+            pCtrl = shpmgrWMCreate(hwnd,
+                                  (PCREATESTRUCT)PVOIDFROMMP(mp2),
+                                  (PSHPCTLDATA)PVOIDFROMMP(mp1));
+
+            if (pCtrl == NULL)
+                return (MRESULT) TRUE;
+
+            // store control data in window words
+            WinSetWindowPtr(hwnd, 0, (PVOID) pCtrl);
+            return (MRESULT) FALSE;
+
+        /*
+         * WM_DESTROY:
+         *      clean up.
+         */
+
+        case WM_DESTROY:
+            // TRACE("WM_DESTORY\n");
+            shpmgrWMDestroy(pCtrl);
+            return (MRESULT) 0;
+
+        /*
+         * WM_ADJUSTWINDOWPOS:
+         *      this needs to be manipulated to
+         *      adjust the positions of all the
+         *      subwindows instead.
+         */
+
+        case WM_ADJUSTWINDOWPOS:
+            // TRACE("WM_ADJUSTWINDOWPOS\n");
+            pswp = (PSWP) PVOIDFROMMP(mp1);
+            // DUMPSWP(pswp);
+
+            // enforce the size which we were given
+            // in the beginning, because we cannot
+            // change the size later
+            pswp->cx = pCtrl->cx;
+            pswp->cy = pCtrl->cy;
+
+            // adjust the sub-windows
+            shpmgrWMAdjustWindowPos(pCtrl, pswp);
+
+            // never show ourselves
+            pswp->fl &= ~SWP_SHOW;
+            return (MRESULT) 0;
+
+        /*
+         * SHAPEWIN_MSG_UPDATE:
+         *
+         */
+
+        case SHAPEWIN_MSG_UPDATE:
+            shpmgrUpdateRegions(pCtrl, (PRECTL) PVOIDFROMMP(mp1));
+            return (MRESULT) 0;
+
+        /*
+         * WM_QUERYDLGCODE:
+         *
+         */
+
+        case WM_QUERYDLGCODE:
+            // TRACE("WM_QUERYDLGCODE\n");
+            return (MRESULT) DLGC_STATIC;
+
+        /*
+         * WM_PAINT:
+         *      we just swallow this message because
+         *      there's no "shape window" to be painted;
+         *      instead, all the region windows get
+         *      their own WM_PAINT message
+         */
+
+        case WM_PAINT:
+        {
+            // TRACE("WM_PAINT\n");
+            /* HPS hps = WinBeginPaint(hwnd, NULLHANDLE, NULL);
+
+            POINTL apt[3];
+            RECTL rclWin;
+            WinQueryWindowRect(hwnd, &rclWin);
+
+            // lower left of target
+            apt[0].x = 0;
+            apt[0].y = 0;
+            // upper right of target
+            apt[1].x = rclWin.xRight;
+            apt[1].y = rclWin.yTop;
+            // lower left of source
+            apt[2].x = 0;
+            apt[2].y = 0;
+            GpiBitBlt(hps,
+                      pCtrl->hpsDraw,
+                      3,
+                      apt,
+                      ROP_SRCCOPY,
+                      0); */
+
+            // delete update region to stop further
+            // WM_PAINTs for this window;
+            // this is faster than WiNBeginPaint
+            WinValidateRect(hwnd, NULL, FALSE);
+            // WinEndPaint(hps);
+            return (MRESULT) 0;
+        }
+
+        /*
+         * all input-related messages:
+         *      forward these to the owner
+         */
+
+        case WM_MOUSEMOVE    :
+        case WM_BUTTON1DOWN  :
+        case WM_BUTTON1UP    :
+        case WM_BUTTON1CLICK :
+        case WM_BUTTON1DBLCLK:
+        case WM_BUTTON2DOWN  :
+        case WM_BUTTON2UP    :
+        case WM_BUTTON2CLICK :
+        case WM_BUTTON2DBLCLK:
+        case WM_BUTTON3DOWN  :
+        case WM_BUTTON3UP    :
+        case WM_BUTTON3CLICK :
+        case WM_BUTTON3DBLCLK:
+        case WM_CHAR         :
+        case WM_VIOCHAR      :
+        case WM_BEGINDRAG    :
+        case WM_ENDDRAG      :
+        case WM_SINGLESELECT :
+        case WM_OPEN         :
+        case WM_CONTEXTMENU  :
+        case WM_CONTEXTHELP  :
+        case WM_TEXTEDIT     :
+        case WM_BEGINSELECT  :
+        case WM_ENDSELECT    :
+            return WinSendMsg(pCtrl->hwndOwner, msg, mp1, mp2);
+
+    } // end switch (msg)
+    return WinDefWindowProc(hwnd, msg, mp1, mp2);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Part 2: Shape frame functions                                  *
+ *                                                                  *
+ ********************************************************************/
+
+MRESULT EXPENTRY fnwpShapeFrame(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2);
+
+/*
+ *@@ shpLoadBitmap:
+ *      this creates a memory device context/presentation space
+ *      and loads and selects a bitmap into it.
+ *
+ *      If pszBitmap == NULL, the bitmap is loaded from the
+ *      specified resources, otherwise we attempt to load the
+ *      bitmap file specified in pszBitmap (using gpihLoadBitmapFile).
+ *
+ *      Output in the given SHAPEFRAME structure:
+ *      --  hab:                as passed to this func
+ *      --  hdc:                device context handle
+ *      --  hps:                presentation space handle
+ *      --  hbm:                bitmap handle
+ *      --  bmi:                BITMAPINFOHEADER of hbmp
+ *      --  ptlLowerLeft:       lower left corner is set so that
+ *                              bitmap is centered on screen
+ *
+ *      All other fields are neither read nor written to.
+ *      Returns TRUE if everything went OK.
+ *
+ *      You can call this function directly before calling
+ *      shpCreateWindows.
+ *
+ *      Pass the SHAPEFRAME structure to shpFreeBitmap to clean up.
+ *
+ *@@changed V0.9.0 [umoeller]: added default window positioning
+ *@@changed V0.9.0 [umoeller]: removed GpiSetBitmap here
+ */
+
+BOOL shpLoadBitmap(HAB hab, // in: anchor block
+                   PSZ pszBitmapFile, // in: OS/2 1.3 bmp file or NULL
+                   HMODULE hmodResource, // in: module handle from where the
+                            // resources should be loaded if
+                            // pszBitmapFile == NULL. This can be 0 for
+                            // the current EXE
+                   ULONG idResource, // in: bitmap resource ID in that module
+                   PSHAPEFRAME psb) // out: bitmap info
+{
+    SIZEL       siz;
+    BOOL        brc;
+
+    psb->hab = hab;
+
+    psb->hdc = DevOpenDC(hab, OD_MEMORY, "*", 0, NULL, NULLHANDLE);
+    if (psb->hdc)
+    {
+        siz.cx = siz.cy = 0;
+        psb->hps = GpiCreatePS(hab, psb->hdc, &siz,
+                PU_PELS | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC);
+
+        if (psb->hps)
+        {
+            if (pszBitmapFile)
+            {
+                // load bitmap file
+                ULONG ulError;
+                psb->hbm = gpihLoadBitmapFile(psb->hps,
+                                              pszBitmapFile,
+                                              &ulError);
+            } else
+                // load resource bitmap
+                psb->hbm = GpiLoadBitmap(psb->hps,
+                                             hmodResource,
+                                             idResource,
+                                             0, 0);
+            if (psb->hbm)
+            {
+                SWP     swpScreen;
+                // store bitmap info in structure
+                psb->bmi.cbFix = sizeof(psb->bmi);
+                GpiQueryBitmapInfoHeader(psb->hbm, &psb->bmi);
+
+                // set ptlLowerLeft so that the bitmap
+                // is centered on the screen
+                WinQueryWindowPos(HWND_DESKTOP, &swpScreen);
+                psb->ptlLowerLeft.x = (swpScreen.cx - psb->bmi.cx) / 2;
+                psb->ptlLowerLeft.y = (swpScreen.cy - psb->bmi.cy) / 2;
+
+                brc = TRUE;
+            }
+        }
+    }
+    return (brc);
+}
+
+/*
+ *@@ shpFreeBitmap:
+ *      this cleans up resources allocated by shpLoadBitmap:
+ *      delete the bitmap, destroy the HPS and HDC.
+ *
+ *@@changed V0.9.3 (2000-04-11) [umoeller]: fixed major resource leak; the bitmap was never freed
+ */
+
+VOID shpFreeBitmap(PSHAPEFRAME psb)
+{
+    if (psb->hbm != NULLHANDLE)
+    {
+        // unset bitmap in HPS; it cannot be deleted otherwise...
+        GpiSetBitmap(psb->hps, NULLHANDLE);     // V0.9.3 (2000-04-11) [umoeller]
+        GpiDeleteBitmap(psb->hbm);
+        psb->hbm = NULLHANDLE;
+    }
+    if (psb->hps != NULLHANDLE)
+    {
+        GpiDestroyPS(psb->hps);
+        psb->hps = NULLHANDLE;
+    }
+    if (psb->hdc != NULLHANDLE)
+    {
+        DevCloseDC(psb->hdc);
+        psb->hdc = NULLHANDLE;
+    }
+}
+
+/*
+ *@@ shpCreateWindows:
+ *      this is a one-shot function for creating a
+ *      shaped window from a bitmap.
+ *
+ *      Required fields in SHAPEFRAME for input (those
+ *      are all properly set by shpLoadBitmap):
+ *      --  hab:                anchor block
+ *      --  hps:                presentation space handle
+ *      --  hbm:                bitmap handle
+ *      --  bmi:                BITMAPINFOHEADER of hbmp
+ *      --  ptlLowerLeft:       lower left corner of where
+ *                              to position the shape window (new with V0.9.0).
+ *
+ *      All other fields are ignored for input and only
+ *      set for output (and for the shape window later).
+ *
+ *      You can use shpLoadBitmap to have all the fields
+ *      initialized, which will also set ptlLowerLeft
+ *      so that the shape window gets centered on the
+ *      Desktop.
+ *
+ *      Otherwise you must initialize the above fields for
+ *      yourself.
+ *
+ *      Obviously, this function uses the same HPS (and
+ *      thus bitmap) for both transparency and drawing.
+ *      If you wish to use two different bitmaps (one for
+ *      transparency, one for drawing), you cannot use
+ *      this function.
+ *
+ *      Note that the windows are now (V0.9.0) also positioned
+ *      on the screen.
+ *
+ *      Output in that structure:
+ *      --  hwndShapeFrame:     invisible rectangular frame
+ *                              window for the shape window
+ *                              (fnwpShapeFrame, created here)
+ *      --  hwndShape:          "shape" window
+ *                              (shp_fnwpShapeMgr, created here)
+ *      --  pfnFrame:           original window procedure of
+ *                              WC_FRAME (hwndShapeFrame),
+ *                              which is subclassed here
+ *                              using fnwpShapeFrame
+ *      --  shpctrl:            control data for hwndShape
+ *
+ *      Returns TRUE if everything went OK.
+ *
+ *      <B>Example</B> for creating a shaped window and
+ *      centering it on the screen:
+ +          SHAPEFRAME sb;
+ +          if (shpLoadBitmap(hab...,
+ +                              szBitmapFile...,
+ +                              0, 0,
+ +                              &sb))
+ +          {
+ +              // create shape (transparent) windows
+ +              if (shpCreateWindows(habQuickThread, &sb))
+ +              {
+ *                  ...
+ +              }
+ +          }
+ +          ...
+ +          // cleanup
+ +              shpFreeBitmap(&sb);
+ +              WinDestroyWindow(sb.hwndShapeFrame) ;
+ +              WinDestroyWindow(sb.hwndShape);
+ *
+ *@@changed V0.9.0 [umoeller]: removed hab from function prototype
+ *@@changed V0.9.0 [umoeller]: added window positioning
+ *@@changed V0.9.0 [umoeller]: added GpiSetBitmap here (always forget that)
+ */
+
+BOOL shpCreateWindows(PSHAPEFRAME psb)
+{
+    BOOL        brc = FALSE;
+
+    if (psb->hbm)
+    {
+        // bitmap seems to be valid:
+        FRAMECDATA  fcd;
+
+        GpiSetBitmap(psb->hps, psb->hbm);
+
+        memset(&fcd, 0, sizeof(fcd));
+        fcd.cb = sizeof(fcd);
+
+        // create invisible frame
+        psb->hwndShapeFrame = WinCreateWindow(
+                HWND_DESKTOP,           // Parent window handle
+                WC_FRAME,               // Frame Window Class
+                "XWorkplaceLogoShape",  // title
+                0,                      // Window Style
+                0, 0, 0, 0,             // Position & size
+                NULLHANDLE,             // Owner Window
+                HWND_TOP,               // Z-Order
+                0,                      // Window ID
+                &fcd,                   // Control Data
+                NULL);                  // Presentation Parameter
+
+        if (psb->hwndShapeFrame)
+        {
+            // store the SHAPEFRAME structure in the frame's window words
+            // so that the subclassed frame can access the data
+            WinSetWindowULong(psb->hwndShapeFrame, QWL_USER, (ULONG)psb);
+
+            // subclass the frame window; store the original wnd proc in
+            // the SHAPEFRAME structure
+            psb->pfnFrame = WinSubclassWindow(psb->hwndShapeFrame, fnwpShapeFrame);
+
+            if ((psb->hwndShapeFrame) && (psb->hps))
+            {
+                // OK, no errors so far
+
+                // register shape window class; if this is already
+                // registered, this won't hurt
+                WinRegisterClass(psb->hab,
+                                 WC_SHAPE_WINDOW,
+                                 shp_fnwpShapeMgr,
+                                 0L,        // class style flags
+                                 sizeof(PVOID));
+
+                // create shape manager window
+                psb->shpctrl.cx = psb->bmi.cx;
+                psb->shpctrl.cy = psb->bmi.cy;
+                psb->shpctrl.hpsDraw = psb->hps;
+                psb->shpctrl.hpsMask = psb->hps;
+
+                // create the "shape" window
+                psb->hwndShape = WinCreateWindow(
+                                  HWND_DESKTOP,           // Parent Window
+                                  WC_SHAPE_WINDOW,        // Window Class
+                                  NULL,                   // Window Text
+                                  0,                      // Window Style;
+                                                          // no clip siblings!
+                                  0, 0, 0, 0,             // Pos & Size
+                                  psb->hwndShapeFrame,    // Owner Window
+                                  HWND_TOP,               // Z-Order
+                                  0,                      // Window ID
+                                  &psb->shpctrl,          // Control Data
+                                  NULL);                  // Pres. Param.
+
+                if (psb->hwndShape)
+                {
+                    // no error: only then return TRUE
+                    brc = TRUE;
+
+                    // and position the windows
+                    WinSetWindowPos(psb->hwndShapeFrame, NULLHANDLE,
+                            psb->ptlLowerLeft.x, psb->ptlLowerLeft.y,
+                            psb->bmi.cx, psb->bmi.cy,
+                            (SWP_MOVE | SWP_SIZE | SWP_HIDE));
+                    WinSetWindowPos(psb->hwndShape, NULLHANDLE,
+                            psb->ptlLowerLeft.x, psb->ptlLowerLeft.y,
+                            psb->bmi.cx, psb->bmi.cy,
+                            (SWP_MOVE | SWP_SIZE | SWP_SHOW));
+                }
+            }
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ fnwpShapeFrame:
+ *      this is the window proc for subclassing the shape frame window
+ *      (shpCreateWindows above).
+ *      The shaped window proc (shp_fnwpShapeMgr) keeps forwarding messages
+ *      to its owner (which is the frame here), so we better handle
+ *      those messages.
+ */
+
+MRESULT EXPENTRY fnwpShapeFrame(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc;
+
+    // get the SHAPEFRAME structure from the window words,
+    // which has been stored there by shpCreateWindows
+    PSHAPEFRAME psb = (PSHAPEFRAME)WinQueryWindowULong(hwnd, QWL_USER);
+
+    // _Pmpf(("frame %08x %08x %08x", msg, mp1, mp2));
+
+    switch (msg)
+    {
+
+        /*
+         * WM_ADJUSTWINDOWPOS:
+         *      forward the data we get here to the shape
+         *      window (which will in turn reposition all
+         *      the region windows);
+         *      afterwards, always delete SHOW and set HIDE
+         *      for ourselves
+         */
+
+        case WM_ADJUSTWINDOWPOS: {
+            PSWP pswp = (PSWP) PVOIDFROMMP(mp1);
+            POINTL ptl;
+            WinQueryPointerPos(HWND_DESKTOP, &ptl);
+            WinSetWindowPos(psb->hwndShape,
+                            pswp->hwndInsertBehind,
+                            // use current mouse position instead
+                            // of the original ones
+                            ptl.x,
+                            ptl.y,
+                            // pswp->x,
+                            // pswp->y,
+                            pswp->cx,
+                            pswp->cy,
+                            pswp->fl);
+            pswp->fl &= ~SWP_SHOW;
+            pswp->fl |=  SWP_HIDE;
+            mrc = (*psb->pfnFrame)(hwnd, msg, mp1, mp2);
+        break; }
+
+        /* the shape window forwards these messages to us:
+            WM_MOUSEMOVE
+            WM_BUTTON1DOWN
+            WM_BUTTON1UP
+            WM_BUTTON1CLICK
+            WM_BUTTON1DBLCLK
+            WM_BUTTON2DOWN
+            WM_BUTTON2UP
+            WM_BUTTON2CLICK
+            WM_BUTTON2DBLCLK
+            WM_BUTTON3DOWN
+            WM_BUTTON3UP
+            WM_BUTTON3CLICK
+            WM_BUTTON3DBLCLK
+            WM_CHAR
+            WM_VIOCHAR
+            WM_BEGINDRAG
+            WM_ENDDRAG
+            WM_SINGLESELECT
+            WM_OPEN
+            WM_CONTEXTMENU
+            WM_CONTEXTHELP
+            WM_TEXTEDIT
+            WM_BEGINSELECT
+            WM_ENDSELECT */
+
+        /*
+         * WM_SINGLESELECT:
+         *      if we are being clicked upon, bring the
+         *      shape window to top instead
+         */
+
+        case WM_SINGLESELECT:
+            WinSetWindowPos(psb->hwndShape, HWND_TOP, 0, 0, 0, 0, SWP_ZORDER);
+            mrc = 0;
+        break;
+
+        /*
+         * WM_BEGINDRAG:
+         *      if we are being dragged with MB2, forward
+         *      this to the shape window
+         */
+
+        case WM_BEGINDRAG:
+            WinSendMsg(psb->hwndShapeFrame, WM_TRACKFRAME,
+                MPFROMSHORT(TF_MOVE /* | TF_SETPOINTERPOS*/ ), NULL);
+            mrc = 0;
+        break;
+
+        default:
+            mrc = (*psb->pfnFrame)(hwnd, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ shp2RegisterClasses:
+ *
+ *@@added V0.9.3 (2000-05-03) [umoeller]
+ */
+
+BOOL shp2RegisterClasses(HAB hab)
+{
+    // register shape window class; if this is already
+    // registered, this won't hurt
+    return(WinRegisterClass(hab,
+                            WC_SHAPE_WINDOW,
+                            shp_fnwpShapeMgr,
+                            0L,        // class style flags
+                            sizeof(PVOID)));
+}
+
+
Index: /trunk/src/helpers/stringh.c
===================================================================
--- /trunk/src/helpers/stringh.c	(revision 8)
+++ /trunk/src/helpers/stringh.c	(revision 8)
@@ -0,0 +1,2761 @@
+
+/*
+ *@@sourcefile stringh.c:
+ *      contains string/text helper functions. These are good for
+ *      parsing/splitting strings and other stuff used throughout XWorkplace.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  strh*       string helper functions.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\stringh.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      Parts Copyright (C) 1991-1999 iMatix Corporation.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_WINSHELLDATA
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <math.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\stringh.h"
+#include "helpers\xstring.h"            // extended string helpers
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\C helpers\String management
+ */
+
+/*
+ *@@ strhdup:
+ *      like strdup, but this one
+ *      doesn't crash if pszSource is NULL,
+ *      but returns NULL also.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhdup(const char *pszSource)
+{
+    if (pszSource)
+        return (strdup(pszSource));
+    else
+        return (0);
+}
+
+/*
+ *@@ strhistr:
+ *      like strstr, but case-insensitive.
+ *
+ *@@changed V0.9.0 [umoeller]: crashed if null pointers were passed, thanks Rdiger Ihle
+ */
+
+PSZ strhistr(const char *string1, const char *string2)
+{
+    PSZ prc = NULL;
+
+    if ((string1) && (string2))
+    {
+        PSZ pszSrchIn = strdup(string1);
+        PSZ pszSrchFor = strdup(string2);
+
+        if ((pszSrchIn) && (pszSrchFor))
+        {
+            strupr(pszSrchIn);
+            strupr(pszSrchFor);
+
+            prc = strstr(pszSrchIn, pszSrchFor);
+            if (prc)
+            {
+                // prc now has the first occurence of the string,
+                // but in pszSrchIn; we need to map this
+                // return value to the original string
+                prc = (prc-pszSrchIn) // offset in pszSrchIn
+                      + (PSZ)string1;
+            }
+        }
+        if (pszSrchFor)
+            free(pszSrchFor);
+        if (pszSrchIn)
+            free(pszSrchIn);
+    }
+    return (prc);
+}
+
+/*
+ *@@ strhncpy0:
+ *      like strncpy, but always appends a 0 character.
+ */
+
+ULONG strhncpy0(PSZ pszTarget,
+                const char *pszSource,
+                ULONG cbSource)
+{
+    ULONG ul = 0;
+    PSZ pTarget = pszTarget,
+        pSource = (PSZ)pszSource;
+
+    for (ul = 0; ul < cbSource; ul++)
+        if (*pSource)
+            *pTarget++ = *pSource++;
+        else
+            break;
+    *pTarget = 0;
+
+    return (ul);
+}
+
+/*
+ * strhCount:
+ *      this counts the occurences of c in pszSearch.
+ */
+
+ULONG strhCount(const char *pszSearch,
+                CHAR c)
+{
+    PSZ         p = (PSZ)pszSearch;
+    ULONG       ulCount = 0;
+    while (TRUE)
+    {
+        p = strchr(p, c);
+        if (p)
+        {
+            ulCount++;
+            p++;
+        }
+        else
+            break;
+    }
+    return (ulCount);
+}
+
+/*
+ *@@ strhIsDecimal:
+ *      returns TRUE if psz consists of decimal digits only.
+ */
+
+BOOL strhIsDecimal(PSZ psz)
+{
+    PSZ p = psz;
+    while (*p != 0)
+    {
+        if (isdigit(*p) == 0)
+            return (FALSE);
+        p++;
+    }
+
+    return (TRUE);
+}
+
+/*
+ *@@ strhSubstr:
+ *      this creates a new PSZ containing the string
+ *      from pBegin to pEnd, excluding the pEnd character.
+ *      The new string is null-terminated.
+ *
+ *      Example:
+ +              "1234567890"
+ +                ^      ^
+ +                p1     p2
+ +          strhSubstr(p1, p2)
+ *      would return a new string containing "2345678".
+ */
+
+PSZ strhSubstr(const char *pBegin, const char *pEnd)
+{
+    ULONG cbSubstr = (pEnd - pBegin);
+    PSZ pszSubstr = (PSZ)malloc(cbSubstr + 1);
+    strhncpy0(pszSubstr, pBegin, cbSubstr);
+    return (pszSubstr);
+}
+
+/*
+ *@@ strhExtract:
+ *      searches pszBuf for the cOpen character and returns
+ *      the data in between cOpen and cClose, excluding
+ *      those two characters, in a newly allocated buffer
+ *      which you must free() afterwards.
+ *
+ *      Spaces and newlines/linefeeds are skipped.
+ *
+ *      If the search was successful, the new buffer
+ *      is returned and, if (ppEnd != NULL), *ppEnd points
+ *      to the first character after the cClose character
+ *      found in the buffer.
+ *
+ *      If the search was not successful, NULL is
+ *      returned, and *ppEnd is unchanged.
+ *
+ *      If another cOpen character is found before
+ *      cClose, matching cClose characters will be skipped.
+ *      You can therefore nest the cOpen and cClose
+ *      characters.
+ *
+ *      This function ignores cOpen and cClose characters
+ *      in C-style comments and strings surrounded by
+ *      double quotes.
+ *
+ *      Example:
+ +          PSZ pszBuf = "KEYWORD { --blah-- } next",
+ +              pEnd;
+ +          strhExtract(pszBuf,
+ +                      '{', '}',
+ +                      &pEnd)
+ *      would return a new buffer containing " --blah-- ",
+ *      and ppEnd would afterwards point to the space
+ *      before "next" in the static buffer.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhExtract(PSZ pszBuf,     // in: search buffer
+                CHAR cOpen,     // in: opening char
+                CHAR cClose,    // in: closing char
+                PSZ *ppEnd)     // out: if != NULL, receives first character after closing char
+{
+    PSZ pszReturn = NULL;
+
+    if (pszBuf)
+    {
+        PSZ pOpen = strchr(pszBuf, cOpen);
+        if (pOpen)
+        {
+            // opening char found:
+            // now go thru the whole rest of the buffer
+            PSZ     p = pOpen+1;
+            LONG    lLevel = 1;        // if this goes 0, we're done
+            while (*p)
+            {
+                if (*p == cOpen)
+                    lLevel++;
+                else if (*p == cClose)
+                {
+                    lLevel--;
+                    if (lLevel <= 0)
+                    {
+                        // matching closing bracket found:
+                        // extract string
+                        pszReturn = strhSubstr(pOpen+1,  // after cOpen
+                                               p);          // excluding cClose
+                        if (ppEnd)
+                            *ppEnd = p+1;
+                        break;      // while (*p)
+                    }
+                }
+                else if (*p == '\"')
+                {
+                    // beginning of string:
+                    PSZ p2 = p+1;
+                    // find end of string
+                    while ((*p2) && (*p2 != '\"'))
+                        p2++;
+
+                    if (*p2 == '\"')
+                        // closing quote found:
+                        // search on after that
+                        p = p2;     // raised below
+                    else
+                        break;      // while (*p)
+                }
+
+                p++;
+            }
+        }
+    }
+
+    return (pszReturn);
+}
+
+/*
+ *@@ strhQuote:
+ *      similar to strhExtract, except that
+ *      opening and closing chars are the same,
+ *      and therefore no nesting is possible.
+ *      Useful for extracting stuff between
+ *      quotes.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhQuote(PSZ pszBuf,
+              CHAR cQuote,
+              PSZ *ppEnd)
+{
+    PSZ pszReturn = NULL,
+        p1 = NULL;
+    if ((p1 = strchr(pszBuf, cQuote)))
+    {
+        PSZ p2 = strchr(p1+1, cQuote);
+        if (p2)
+        {
+            pszReturn = strhSubstr(p1+1, p2);
+            if (ppEnd)
+                // store closing char
+                *ppEnd = p2 + 1;
+        }
+    }
+
+    return (pszReturn);
+}
+
+/*
+ *@@ strhStrip:
+ *      removes all double spaces.
+ *      This copies within the "psz" buffer.
+ *      If any double spaces are found, the
+ *      string will be shorter than before,
+ *      but the buffer is _not_ reallocated,
+ *      so there will be unused bytes at the
+ *      end.
+ *
+ *      Returns the number of spaces removed.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG strhStrip(PSZ psz)         // in/out: string
+{
+    PSZ     p;
+    ULONG   cb = strlen(psz),
+            ulrc = 0;
+
+    for (p = psz; p < psz+cb; p++)
+    {
+        if ((*p == ' ') && (*(p+1) == ' '))
+        {
+            PSZ p2 = p;
+            while (*p2)
+            {
+                *p2 = *(p2+1);
+                p2++;
+            }
+            cb--;
+            p--;
+            ulrc++;
+        }
+    }
+    return (ulrc);
+}
+
+/*
+ * strhWords:
+ *      returns the no. of words in "psz".
+ *      A string is considered a "word" if
+ *      it is surrounded by spaces only.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG strhWords(PSZ psz)
+{
+    PSZ p;
+    ULONG cb = strlen(psz),
+          ulWords = 0;
+    if (cb > 1)
+    {
+        ulWords = 1;
+        for (p = psz; p < psz+cb; p++)
+            if (*p == ' ')
+                ulWords++;
+    }
+    return (ulWords);
+}
+
+/*
+ *@@ strhThousandsULong:
+ *      converts a ULONG into a decimal string, while
+ *      inserting thousands separators into it. Specify
+ *      the separator char in cThousands.
+ *      Returns pszTarget so you can use it directly
+ *      with sprintf and the "%s" flag.
+ *      For cThousands, you should use the data in
+ *      OS2.INI ("PM_National" application), which is
+ *      always set according to the "Country" object.
+ *      Use strhThousandsDouble for "double" values.
+ */
+
+PSZ strhThousandsULong(PSZ pszTarget,       // out: decimal as string
+                       ULONG ul,            // in: decimal to convert
+                       CHAR cThousands)     // in: separator char (e.g. '.')
+{
+    USHORT ust, uss, usc;
+    CHAR   szTemp[40];
+    sprintf(szTemp, "%d", ul);
+
+    ust = 0;
+    usc = strlen(szTemp);
+    for (uss = 0; uss < usc; uss++)
+    {
+        if (uss)
+            if (((usc - uss) % 3) == 0)
+            {
+                pszTarget[ust] = cThousands;
+                ust++;
+            }
+        pszTarget[ust] = szTemp[uss];
+        ust++;
+    }
+    pszTarget[ust] = '\0';
+
+    return (pszTarget);
+}
+
+/*
+ *@@ strhThousandsDouble:
+ *      like strhThousandsULong, but for a "double"
+ *      value. Note that after-comma values are truncated.
+ */
+
+PSZ strhThousandsDouble(PSZ pszTarget, double dbl, CHAR cThousands)
+{
+    USHORT ust, uss, usc;
+    CHAR   szTemp[40];
+    sprintf(szTemp, "%.0f", floor(dbl));
+
+    ust = 0;
+    usc = strlen(szTemp);
+    for (uss = 0; uss < usc; uss++)
+    {
+        if (uss)
+            if (((usc - uss) % 3) == 0)
+            {
+                pszTarget[ust] = cThousands;
+                ust++;
+            }
+        pszTarget[ust] = szTemp[uss];
+        ust++;
+    }
+    pszTarget[ust] = '\0';
+
+    return (pszTarget);
+}
+
+/*
+ *@@ strhFileDate:
+ *      converts file date data to a string (to pszBuf).
+ *      You can pass any FDATE structure to this function,
+ *      which are returned in those FILEFINDBUF* or
+ *      FILESTATUS* structs by the Dos* functions.
+ *
+ *      ulDateFormat is the PM setting for the date format,
+ *      as set in the "Country" object, and can be queried using
+ +              PrfQueryProfileInt(HINI_USER, "PM_National", "iDate", 0);
+ *
+ *      meaning:
+ *      --  0   mm.dd.yyyy  (English)
+ *      --  1   dd.mm.yyyy  (e.g. German)
+ *      --  2   yyyy.mm.dd  (Japanese, ISO)
+ *      --  3   yyyy.dd.mm
+ *
+ *      cDateSep is used as a date separator (e.g. '.').
+ *      This can be queried using:
+ +              prfhQueryProfileChar(HINI_USER, "PM_National", "sDate", '/');
+ *
+ *      Alternatively, you can query all the country settings
+ *      at once using prfhQueryCountrySettings (prfh.c, new with V0.9.0).
+ *
+ *@@changed (99-11-07) [umoeller]: now calling strhDateTime
+ */
+
+VOID strhFileDate(PSZ pszBuf,           // out: string returned
+                  FDATE *pfDate,        // in: date information
+                  ULONG ulDateFormat,   // in: date format (0-3)
+                  CHAR cDateSep)        // in: date separator (e.g. '.')
+{
+    DATETIME dt;
+    dt.day = pfDate->day;
+    dt.month = pfDate->month;
+    dt.year = pfDate->year + 1980;
+
+    strhDateTime(pszBuf,
+                 NULL,          // no time
+                 &dt,
+                 ulDateFormat,
+                 cDateSep,
+                 0, 0);         // no time
+}
+
+/*
+ *@@ strhFileTime:
+ *      converts file time data to a string (to pszBuf).
+ *      You can pass any FTIME structure to this function,
+ *      which are returned in those FILEFINDBUF* or
+ *      FILESTATUS* structs by the Dos* functions.
+ *
+ *      ulTimeFormat is the PM setting for the time format,
+ *      as set in the "Country" object, and can be queried using
+ +              PrfQueryProfileInt(HINI_USER, "PM_National", "iTime", 0);
+ *      meaning:
+ *      --  0   12-hour clock
+ *      --  >0  24-hour clock
+ *
+ *      cDateSep is used as a time separator (e.g. ':').
+ *      This can be queried using:
+ +              prfhQueryProfileChar(HINI_USER, "PM_National", "sTime", ':');
+ *
+ *      Alternatively, you can query all the country settings
+ *      at once using prfhQueryCountrySettings (prfh.c, new with V0.9.0).
+ *
+ *@@changed 99-03-15 fixed 12-hour crash
+ *@@changed (99-11-07) [umoeller]: now calling strhDateTime
+ */
+
+VOID strhFileTime(PSZ pszBuf,           // out: string returned
+                  FTIME *pfTime,        // in: time information
+                  ULONG ulTimeFormat,   // in: 24-hour time format (0 or 1)
+                  CHAR cTimeSep)        // in: time separator (e.g. ':')
+{
+    DATETIME dt;
+    dt.hours = pfTime->hours;
+    dt.minutes = pfTime->minutes;
+    dt.seconds = pfTime->twosecs * 2;
+
+    strhDateTime(NULL,          // no date
+                 pszBuf,
+                 &dt,
+                 0, 0,          // no date
+                 ulTimeFormat,
+                 cTimeSep);
+}
+
+/*
+ *@@ strhDateTime:
+ *      converts Control Programe DATETIME info
+ *      to two strings. See strhFileDate and strhFileTime
+ *      for more detailed parameter descriptions.
+ *
+ *@@added V0.9.0 (99-11-07) [umoeller]
+ */
+
+VOID strhDateTime(PSZ pszDate,          // out: date string returned (can be NULL)
+                  PSZ pszTime,          // out: time string returned (can be NULL)
+                  DATETIME *pDateTime,  // in: date/time information
+                  ULONG ulDateFormat,   // in: date format (0-3); see strhFileDate
+                  CHAR cDateSep,        // in: date separator (e.g. '.')
+                  ULONG ulTimeFormat,   // in: 24-hour time format (0 or 1); see strhFileTime
+                  CHAR cTimeSep)        // in: time separator (e.g. ':')
+{
+    if (pszDate)
+    {
+        switch (ulDateFormat)
+        {
+            case 0:  // mm.dd.yyyy  (English)
+                sprintf(pszDate, "%02d%c%02d%c%04d",
+                    pDateTime->month,
+                        cDateSep,
+                    pDateTime->day,
+                        cDateSep,
+                    pDateTime->year);
+            break;
+
+            case 1:  // dd.mm.yyyy  (e.g. German)
+                sprintf(pszDate, "%02d%c%02d%c%04d",
+                    pDateTime->day,
+                        cDateSep,
+                    pDateTime->month,
+                        cDateSep,
+                    pDateTime->year);
+            break;
+
+            case 2: // yyyy.mm.dd  (Japanese)
+                sprintf(pszDate, "%04d%c%02d%c%02d",
+                    pDateTime->year,
+                        cDateSep,
+                    pDateTime->month,
+                        cDateSep,
+                    pDateTime->day);
+            break;
+
+            default: // yyyy.dd.mm
+                sprintf(pszDate, "%04d%c%02d%c%02d",
+                    pDateTime->year,
+                        cDateSep,
+                    pDateTime->day,
+                        cDateSep,
+                    pDateTime->month);
+            break;
+        }
+    }
+
+    if (pszTime)
+    {
+        if (ulTimeFormat == 0)
+        {
+            // for 12-hour clock, we need additional INI data
+            CHAR szAMPM[10] = "err";
+
+            if (pDateTime->hours > 12)
+            {
+                // > 12h: PM.
+
+                // Note: 12:xx noon is 12 AM, not PM (even though
+                // AM stands for "ante meridiam", but English is just
+                // not logical), so that's handled below.
+
+                PrfQueryProfileString(HINI_USER,
+                    "PM_National",
+                    "s2359",        // key
+                    "PM",           // default
+                    szAMPM, sizeof(szAMPM)-1);
+                sprintf(pszTime, "%02d%c%02d%c%02d %s",
+                    // leave 12 == 12 (not 0)
+                    pDateTime->hours % 12,
+                        cTimeSep,
+                    pDateTime->minutes,
+                        cTimeSep,
+                    pDateTime->seconds,
+                    szAMPM);
+            }
+            else
+            {
+                // <= 12h: AM
+                PrfQueryProfileString(HINI_USER,
+                                      "PM_National",
+                                      "s1159",        // key
+                                      "AM",           // default
+                                      szAMPM, sizeof(szAMPM)-1);
+                sprintf(pszTime, "%02d%c%02d%c%02d %s",
+                    pDateTime->hours,
+                        cTimeSep,
+                    pDateTime->minutes,
+                        cTimeSep,
+                    pDateTime->seconds,
+                    szAMPM);
+            }
+        }
+        else
+            // 24-hour clock
+            sprintf(pszTime, "%02d%c%02d%c%02d",
+                pDateTime->hours,
+                    cTimeSep,
+                pDateTime->minutes,
+                    cTimeSep,
+                pDateTime->seconds);
+    }
+}
+
+/*
+ *@@ strhGetWord:
+ *      finds word boundaries.
+ *
+ *      *ppszStart is used as the beginning of the
+ *      search.
+ *
+ *      If a word is found, *ppszStart is set to
+ *      the first character of the word which was
+ *      found and *ppszEnd receives the address
+ *      of the first character _after_ the word,
+ *      which is probably a space or a \n or \r char.
+ *      We then return TRUE.
+ *
+ *      The search is stopped if a null character
+ *      is found or pLimit is reached. In that case,
+ *      FALSE is returned.
+ *
+ *@@added V0.9.1 (2000-02-13) [umoeller]
+ */
+
+BOOL strhGetWord(PSZ *ppszStart,        // in: start of search,
+                                        // out: start of word (if TRUE is returned)
+                 const char *pLimit,    // in: ptr to last char after *ppszStart to be
+                                        // searched; if the word does not end before
+                                        // or with this char, FALSE is returned
+                 const char *pcszBeginChars, // stringh.h defines STRH_BEGIN_CHARS
+                 const char *pcszEndChars, // stringh.h defines STRH_END_CHARS
+                 PSZ *ppszEnd)          // out: first char _after_ word
+                                        // (if TRUE is returned)
+{
+    // characters after which a word can be started
+    // const char *pcszBeginChars = "\x0d\x0a ";
+    // const char *pcszEndChars = "\x0d\x0a /-";
+
+    PSZ pStart = *ppszStart;
+
+    // find start of word
+    while (     (pStart < (PSZ)pLimit)
+             && (strchr(pcszBeginChars, *pStart))
+          )
+        // if char is a "before word" char: go for next
+        pStart++;
+
+    if (pStart < (PSZ)pLimit)
+    {
+        // found a valid "word start" character
+        // (which is not in pcszBeginChars):
+
+        // find end of word
+        PSZ  pEndOfWord = pStart;
+        while (     (pEndOfWord <= (PSZ)pLimit)
+                 && (strchr(pcszEndChars, *pEndOfWord) == 0)
+              )
+            // if char is not an "end word" char: go for next
+            pEndOfWord++;
+
+        if (pEndOfWord <= (PSZ)pLimit)
+        {
+            // whoa, got a word:
+            *ppszStart = pStart;
+            *ppszEnd = pEndOfWord;
+            return (TRUE);
+        }
+    }
+
+    return (FALSE);
+}
+
+/*
+ *@@ strhFindWord:
+ *      searches for pszSearch in pszBuf, which is
+ *      returned if found (or NULL if not).
+ *
+ *      As opposed to strstr, this finds pszSearch
+ *      only if it is a "word". A search string is
+ *      considered a word if the character _before_
+ *      it is in pcszBeginChars and the char _after_
+ *      it is in pcszEndChars.
+ *
+ *      Example:
+ +          strhFindWord("This is an example.", "is");
+ +          returns ...........^ this, but not the "is" in "This".
+ *
+ *      The algorithm here uses strstr to find pszSearch in pszBuf
+ *      and performs additional "is-word" checks for each item found.
+ *      With VAC++ 3.0, this is still much faster than searching
+ *      words first and then comparing each word with pszSearch.
+ *      I've tried it that way too, and that took nearly double as
+ *      long. Apparently, the VAC++ runtime library uses some
+ *      optimized search algorithm here, so we better use that one.
+ *
+ *@@added V0.9.0 (99-11-08) [umoeller]
+ *@@changed (99-11-10) [umoeller]: tried second algorithm, reverted to original...
+ */
+
+PSZ strhFindWord(const char *pszBuf,
+                 const char *pszSearch,
+                 const char *pcszBeginChars,    // suggestion: "\x0d\x0a ()/\\-,."
+                 const char *pcszEndChars)      // suggestion: "\x0d\x0a ()/\\-,.:;"
+{
+    PSZ     pszReturn = 0;
+    ULONG   cbBuf = strlen(pszBuf),
+            cbSearch = strlen(pszSearch);
+
+    if ((cbBuf) && (cbSearch))
+    {
+        const char *p = pszBuf;
+
+        /*  // go thru all characters
+        while (*p)
+        {
+            // check if current character is either the
+            // very first or a "begin word" character
+            if (    (p == pszBuf)
+                 || (strchr(pcszBeginChars, *p) == 0)
+               )
+            {
+                // yes: go for next
+                if (*(++p))
+                {
+                    // compare with search string
+                    if (strcmp(p, pszSearch) == 0)
+                    {
+                        // is the same:
+                        // check if still in buffer
+                        if (p < pszBuf + cbBuf)
+                        {
+                            CHAR    cAfterEndOfWord = *(p + cbSearch);
+                            if (cAfterEndOfWord == 0)
+                            {
+                                // end of string:
+                                // that's ok
+                                pszReturn = (PSZ)p;
+                                break;
+                            }
+                            else
+                            {
+                                // check if in "end of word" list
+                                char *pc2 = strchr(pcszEndChars, cAfterEndOfWord);
+                                if (pc2)
+                                    // OK, is end char: avoid doubles of that char,
+                                    // but allow spaces
+                                    if (    (cAfterEndOfWord+1 != *pc2)
+                                         || (cAfterEndOfWord+1 == ' ')
+                                         || (cAfterEndOfWord+1 == 0)
+                                       )
+                                    {
+                                        // end of string:
+                                        // that's ok
+                                        pszReturn = (PSZ)p;
+                                        break;
+                                    }
+                            }
+                        }
+                    }
+                }
+                else
+                    // end of string:
+                    break;
+            }
+
+            ++p;
+        } // end while
+        */
+
+        do  // while p
+        {
+            p = strstr(p, pszSearch);
+            if (p)
+            {
+                // string found:
+                // check if that's a word
+
+                // check previous char
+                if (    (p == pszBuf)
+                     || (strchr(pcszBeginChars, *(p-1)))
+                   )
+                {
+                    // OK, valid begin char:
+                    BOOL    fEndOK = FALSE;
+                    // check end char
+                    CHAR    cNextChar = *(p + cbSearch);
+                    if (cNextChar == 0)
+                        fEndOK = TRUE;
+                    else
+                    {
+                        char *pc = strchr(pcszEndChars, cNextChar);
+                        if (pc)
+                            // OK, is end char: avoid doubles of that char,
+                            // but allow spaces
+                            if (    (cNextChar+1 != *pc)
+                                 || (cNextChar+1 == ' ')
+                                 || (cNextChar+1 == 0)
+                               )
+                                fEndOK = TRUE;
+                    }
+
+                    if (fEndOK)
+                    {
+                        // valid end char:
+                        pszReturn = (PSZ)p;
+                        break;
+                    }
+                }
+                p += cbSearch;
+            }
+        } while (p);
+
+    }
+    return (pszReturn);
+}
+
+/*
+ *@@ strhFindEOL:
+ *      returns a pointer to the next \r, \n or null character
+ *      following pszSearchIn. Stores the offset in *pulOffset.
+ *
+ *      This should never return NULL because at some point,
+ *      there will be a null byte in your string.
+ *
+ *@@added V0.9.4 (2000-07-01) [umoeller]
+ */
+
+PSZ strhFindEOL(PSZ pszSearchIn,        // in: where to search
+                PULONG pulOffset)       // out: offset (ptr can be NULL)
+{
+    PSZ     p = pszSearchIn,
+            prc = NULL;
+    while (TRUE)
+    {
+        if ( (*p == '\r') || (*p == '\n') || (*p == 0) )
+        {
+            prc = p;
+            break;
+        }
+        p++;
+    }
+
+    if (pulOffset)
+        *pulOffset = prc - pszSearchIn;
+    return (prc);
+}
+
+/*
+ *@@ strhFindNextLine:
+ *      like strhFindEOL, but this returns the character
+ *      _after_ \r or \n. Note that this might return
+ *      a pointer to terminating NULL character also.
+ */
+
+PSZ strhFindNextLine(PSZ pszSearchIn, PULONG pulOffset)
+{
+    PSZ pEOL = strhFindEOL(pszSearchIn, NULL);
+    // pEOL now points to the \r char or the terminating 0 byte;
+    // if not null byte, advance pointer
+    PSZ pNextLine = pEOL;
+    if (*pNextLine == '\r')
+        pNextLine++;
+    if (*pNextLine == '\n')
+        pNextLine++;
+    if (pulOffset)
+        *pulOffset = pNextLine - pszSearchIn;
+    return (pNextLine);
+}
+
+/*
+ *@@ strhFindKey:
+ *      finds pszKey in pszSearchIn; similar to strhistr,
+ *      but this one makes sure the key is at the beginning
+ *      of a line. Spaces before the key are tolerated.
+ *      Returns NULL if the key was not found.
+ *      Used by strhGetParameter/strhSetParameter; useful
+ *      for analyzing CONFIG.SYS settings.
+ *
+ *@@changed V0.9.0 [umoeller]: fixed bug in that this would also return something if only the first chars matched
+ *@@changed V0.9.0 [umoeller]: fixed bug which could cause character before pszSearchIn to be examined
+ */
+
+PSZ strhFindKey(PSZ pszSearchIn,     // in: text buffer to search
+                PSZ pszKey,          // in: key to search for
+                PBOOL pfIsAllUpperCase) // out: TRUE if key is completely in upper case;
+                                     // can be NULL if not needed
+{
+    PSZ     p = NULL,
+            pReturn = NULL;
+    // BOOL    fFound = FALSE;
+
+    p = pszSearchIn;
+    do {
+        p = strhistr(p, pszKey);
+
+        if ((p) && (p >= pszSearchIn))
+        {
+            // make sure the key is at the beginning of a line
+            // by going backwards until we find a char != " "
+            PSZ p2 = p;
+            while (     (*p2 == ' ')
+                     && (p2 > pszSearchIn)
+                  )
+                p2--;
+
+            // if previous char is an EOL sign, go on
+            if (    (p2 == pszSearchIn)     // order fixed V0.9.0, Rdiger Ihle
+                 || (*(p2-1) == '\r')
+                 || (*(p2-1) == '\n')
+               )
+            {
+                // now check whether the char after the search
+                // is a "=" char
+                // ULONG cbKey = strlen(pszKey);
+
+                // tolerate spaces before "="
+                /* PSZ p3 = p;
+                while (*(p3+cbKey) == ' ')
+                    p3++;
+
+                if (*(p3+cbKey) == '=') */
+                {
+                    // found:
+                    pReturn = p; // go on, p contains found key
+
+                    // test for all upper case?
+                    if (pfIsAllUpperCase)
+                    {
+                        ULONG cbKey2 = strlen(pszKey),
+                              ul = 0;
+                        *pfIsAllUpperCase = TRUE;
+                        for (ul = 0; ul < cbKey2; ul++)
+                            if (islower(*(p+ul)))
+                            {
+                                *pfIsAllUpperCase = FALSE;
+                                break; // for
+                            }
+                    }
+
+                    break; // do
+                }
+            } // else search next key
+
+            p++; // search on after this key
+        }
+    } while ((!pReturn) && (p != NULL) && (p != pszSearchIn));
+
+    return (pReturn);
+}
+
+/*
+ *@@ strhGetParameter:
+ *      searches pszSearchIn for the key pszKey; if found, it
+ *      returns a pointer to the following characters in pszSearchIn
+ *      and, if pszCopyTo != NULL, copies the rest of the line to
+ *      that buffer, of which cbCopyTo specified the size.
+ *      If the key is not found, NULL is returned.
+ *      String search is done by calling strhFindKey.
+ *      This is useful for querying CONFIG.SYS settings.
+ *
+ *      <B>Example:</B> this would return "YES" if you searched
+ *      for "PAUSEONERROR=", and "PAUSEONERROR=YES" existed in pszSearchIn.
+ */
+
+PSZ strhGetParameter(PSZ pszSearchIn,   // in: text buffer to search
+                     PSZ pszKey,        // in: key to search for
+                     PSZ pszCopyTo,     // out: key value
+                     ULONG cbCopyTo)    // out: sizeof(*pszCopyTo)
+{
+    PSZ p = strhFindKey(pszSearchIn, pszKey, NULL),
+        prc = NULL;
+    if (p)
+    {
+        prc = p + strlen(pszKey);
+        if (pszCopyTo)
+        // copy to pszCopyTo
+        {
+            ULONG cb;
+            PSZ pEOL = strhFindEOL(prc, &cb);
+            if (pEOL)
+            {
+                if (cb > cbCopyTo)
+                    cb = cbCopyTo-1;
+                strhncpy0(pszCopyTo, prc, cb);
+            }
+        }
+    }
+
+    return (prc);
+}
+
+/*
+ *@@ strhSetParameter:
+ *      searches *ppszSearchIn for the key pszKey; if found, it
+ *      replaces the characters following this key up to the
+ *      end of the line with pszParam. If pszKey is not found in
+ *      pszSearchIn, it is appended to the file in a new line.
+ *
+ *      If any changes are made, *ppszSearchIn is re-allocated.
+ *
+ *      This function searches w/out case sensitivity.
+ *
+ *      Returns a pointer to the new parameter inside the buffer.
+ *
+ *@@changed V0.9.0 [umoeller]: changed function prototype to PSZ* ppszSearchIn
+ */
+
+PSZ strhSetParameter(PSZ* ppszSearchIn,    // in: text buffer to search
+                     PSZ pszKey,         // in: key to search for
+                     PSZ pszNewParam,    // in: new parameter to set for key
+                     BOOL fRespectCase)  // in: if TRUE, pszNewParam will
+                             // be converted to upper case if the found key is
+                             // in upper case also. pszNewParam should be in
+                             // lower case if you use this.
+{
+    BOOL fIsAllUpperCase = FALSE;
+    PSZ pKey = strhFindKey(*ppszSearchIn, pszKey, &fIsAllUpperCase),
+        prc = NULL;
+
+    if (pKey)
+    {
+        // key found in file:
+        // replace existing parameter
+        PSZ pOldParam = pKey + strlen(pszKey);
+        prc = pOldParam;
+        // pOldParam now has the old parameter, which we
+        // will overwrite now
+
+        if (pOldParam)
+        {
+            ULONG cbOldParam;
+            PSZ pEOL = strhFindEOL(pOldParam, &cbOldParam);
+            // pEOL now has first end-of-line after the parameter
+
+            if (pEOL)
+            {
+                PSZ pszOldCopy = (PSZ)malloc(cbOldParam+1);
+                strncpy(pszOldCopy, pOldParam, cbOldParam);
+                pszOldCopy[cbOldParam] = '\0';
+
+                // check for upper case desired?
+                if (fRespectCase)
+                    if (fIsAllUpperCase)
+                        strupr(pszNewParam);
+
+                xstrrpl(ppszSearchIn, 0, pszOldCopy, pszNewParam, NULL);
+
+                free(pszOldCopy);
+            }
+        }
+    }
+    else
+    {
+        PSZ pszNew = (PSZ)malloc(strlen(*ppszSearchIn)
+                              + strlen(pszKey)
+                              + strlen(pszNewParam)
+                              + 5);     // 2 * \r\n + null byte
+        // key not found: append to end of file
+        sprintf(pszNew, "%s\r\n%s%s\r\n",
+                *ppszSearchIn, pszKey, pszNewParam);
+        free(*ppszSearchIn);
+        *ppszSearchIn = pszNew;
+    }
+
+    return (prc);
+}
+
+/*
+ *@@ strhDeleteLine:
+ *      this deletes the line in pszSearchIn which starts with
+ *      the key pszKey. Returns TRUE if the line was found and
+ *      deleted.
+ *
+ *      This copies within pszSearchIn.
+ */
+
+BOOL strhDeleteLine(PSZ pszSearchIn,        // in: buffer to search
+                    PSZ pszKey)             // in: key to find
+{
+    BOOL fIsAllUpperCase = FALSE;
+    PSZ pKey = strhFindKey(pszSearchIn, pszKey, &fIsAllUpperCase);
+    BOOL brc = FALSE;
+
+    if (pKey) {
+        PSZ pEOL = strhFindEOL(pKey, NULL);
+        // pEOL now has first end-of-line after the key
+        if (pEOL)
+        {
+            // delete line by overwriting it with
+            // the next line
+            strcpy(pKey, pEOL+2);
+        }
+        else
+        {
+            // EOL not found: we must be at the end of the file
+            *pKey = '\0';
+        }
+        brc = TRUE;
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ strhBeautifyTitle:
+ *      replaces all line breaks (0xd, 0xa) with spaces.
+ */
+
+BOOL strhBeautifyTitle(PSZ psz)
+{
+    BOOL rc = FALSE;
+    CHAR *p;
+    while ((p = strchr(psz, 0xa)))
+    {
+        *p = ' ';
+        rc = TRUE;
+    }
+    while ((p = strchr(psz, 0xd)))
+    {
+        *p = ' ';
+        rc = TRUE;
+    }
+    return (rc);
+}
+
+/*
+ * strhFindAttribValue:
+ *      searches for pszAttrib in pszSearchIn; if found,
+ *      returns the first character after the "=" char.
+ *      If "=" is not found, a space, \r, and \n are
+ *      also accepted. This function searches without
+ *      respecting case.
+ *
+ *      <B>Example:</B>
+ +          strhFindAttribValue("<PAGE BLAH="data">, "BLAH")
+ +
+ +          returns ....................... ^ this address.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.3 (2000-05-19) [umoeller]: some speed optimizations
+ */
+
+PSZ strhFindAttribValue(const char *pszSearchIn, const char *pszAttrib)
+{
+    PSZ     prc = 0;
+    PSZ     pszSearchIn2 = (PSZ)pszSearchIn,
+            p,
+            p2;
+    ULONG   cbAttrib = strlen(pszAttrib);
+
+    // 1) find space char
+    while ((p = strchr(pszSearchIn2, ' ')))
+    {
+        CHAR c;
+        p++;
+        c = *(p+cbAttrib);     // V0.9.3 (2000-05-19) [umoeller]
+        // now check whether the p+strlen(pszAttrib)
+        // is a valid end-of-tag character
+        if (    (memicmp(p, (PVOID)pszAttrib, cbAttrib) == 0)
+             && (   (c == ' ')
+                 || (c == '>')
+                 || (c == '=')
+                 || (c == '\r')
+                 || (c == '\n')
+                 || (c == 0)
+                )
+           )
+        {
+            // yes:
+            CHAR c2;
+            p2 = p + cbAttrib;
+            c2 = *p2;
+            while (     (   (c2 == ' ')
+                         || (c2 == '=')
+                         || (c2 == '\n')
+                         || (c2 == '\r')
+                        )
+                    &&  (c2 != 0)
+                  )
+                c2 = *++p2;
+            prc = p2;
+            break; // first while
+        }
+        pszSearchIn2++;
+    }
+    return (prc);
+}
+
+/*
+ * strhGetNumAttribValue:
+ *      stores the numerical parameter value of an HTML-style
+ *      tag in *pl.
+ *
+ *      Returns the address of the tag parameter in the
+ *      search buffer, if found, or NULL.
+ *
+ *      <B>Example:</B>
+ +          strhGetNumAttribValue("<PAGE BLAH=123>, "BLAH", &l);
+ *
+ *      stores 123 in the "l" variable.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhGetNumAttribValue(const char *pszSearchIn,       // in: where to search
+                          const char *pszTag,            // e.g. "INDEX"
+                          PLONG pl)              // out: numerical value
+{
+    PSZ pParam;
+    if ((pParam = strhFindAttribValue(pszSearchIn, pszTag)))
+        sscanf(pParam, "%d", pl);
+
+    return (pParam);
+}
+
+/*
+ * strhGetTextAttr:
+ *      retrieves the attribute value of a textual HTML-style tag
+ *      in a newly allocated buffer, which is returned,
+ *      or NULL if attribute not found.
+ *      If an attribute value is to contain spaces, it
+ *      must be enclosed in quotes.
+ *
+ *      The offset of the attribute data in pszSearchIn is
+ *      returned in *pulOffset so that you can do multiple
+ *      searches.
+ *
+ *      This returns a new buffer, which should be free()'d after use.
+ *
+ *      <B>Example:</B>
+ +          ULONG   ulOfs = 0;
+ +          strhGetTextAttr("<PAGE BLAH="blublub">, "BLAH", &ulOfs)
+ +                           ............^ ulOfs
+ *
+ *      returns a new string with the value "blublub" (without
+ *      quotes) and sets ulOfs to 12.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhGetTextAttr(const char *pszSearchIn,
+                    const char *pszTag,
+                    PULONG pulOffset)       // out: offset where found
+{
+    PSZ     pParam,
+            pParam2,
+            prc = NULL;
+    ULONG   ulCount = 0;
+    LONG    lNestingLevel = 0;
+
+    if ((pParam = strhFindAttribValue(pszSearchIn, pszTag)))
+    {
+        // determine end character to search for: a space
+        CHAR cEnd = ' ';
+        if (*pParam == '\"')
+        {
+            // or, if the data is enclosed in quotes, a quote
+            cEnd = '\"';
+            pParam++;
+        }
+
+        if (pulOffset)
+            // store the offset
+            (*pulOffset) = pParam - (PSZ)pszSearchIn;
+
+        // now find end of attribute
+        pParam2 = pParam;
+        while (*pParam)
+        {
+            if (*pParam == cEnd)
+                // end character found
+                break;
+            else if (*pParam == '<')
+                // yet another opening tag found:
+                // this is probably some "<" in the attributes
+                lNestingLevel++;
+            else if (*pParam == '>')
+            {
+                lNestingLevel--;
+                if (lNestingLevel < 0)
+                    // end of tag found:
+                    break;
+            }
+            ulCount++;
+            pParam++;
+        }
+
+        // copy attribute to new buffer
+        if (ulCount)
+        {
+            prc = (PSZ)malloc(ulCount+1);
+            memcpy(prc, pParam2, ulCount);
+            *(prc+ulCount) = 0;
+        }
+    }
+    return (prc);
+}
+
+/*
+ * strhFindEndOfTag:
+ *      returns a pointer to the ">" char
+ *      which seems to terminate the tag beginning
+ *      after pszBeginOfTag.
+ *
+ *      If additional "<" chars are found, we look
+ *      for additional ">" characters too.
+ *
+ *      Note: You must pass the address of the opening
+ *      '<' character to this function.
+ *
+ *      Example:
+ +          PSZ pszTest = "<BODY ATTR=\"<BODY>\">";
+ +          strhFindEndOfTag(pszTest)
+ +      returns.................................^ this.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PSZ strhFindEndOfTag(const char *pszBeginOfTag)
+{
+    PSZ     p = (PSZ)pszBeginOfTag,
+            prc = NULL;
+    LONG    lNestingLevel = 0;
+
+    while (*p)
+    {
+        if (*p == '<')
+            // another opening tag found:
+            lNestingLevel++;
+        else if (*p == '>')
+        {
+            // closing tag found:
+            lNestingLevel--;
+            if (lNestingLevel < 1)
+            {
+                // corresponding: return this
+                prc = p;
+                break;
+            }
+        }
+        p++;
+    }
+
+    return (prc);
+}
+
+/*
+ * strhGetBlock:
+ *      this complex function searches the given string
+ *      for a pair of opening/closing HTML-style tags.
+ *
+ *      If found, this routine returns TRUE and does
+ *      the following:
+ *
+ *          1)  allocate a new buffer, copy the text
+ *              enclosed by the opening/closing tags
+ *              into it and set *ppszBlock to that
+ *              buffer;
+ *
+ *          2)  if the opening tag has any attributes,
+ *              allocate another buffer, copy the
+ *              attributes into it and set *ppszAttrs
+ *              to that buffer; if no attributes are
+ *              found, *ppszAttrs will be NULL;
+ *
+ *          3)  set *pulOffset to the offset from the
+ *              beginning of *ppszSearchIn where the
+ *              opening tag was found;
+ *
+ *          4)  advance *ppszSearchIn to after the
+ *              closing tag, so that you can do
+ *              multiple searches without finding the
+ *              same tags twice.
+ *
+ *      All buffers should be freed using free().
+ *
+ *      This returns the following:
+ *      --  0: no error
+ *      --  1: tag not found at all (doesn't have to be an error)
+ *      --  2: begin tag found, but no corresponding end tag found. This
+ *             is a real error.
+ *      --  3: begin tag is not terminated by "&gt;" (e.g. "&lt;BEGINTAG whatever")
+ *
+ *      <B>Example:</B>
+ +          PSZ pSearch = "&lt;PAGE INDEX=1&gt;This is page 1.&lt;/PAGE&gt;More text."
+ +          PSZ pszBlock, pszAttrs;
+ +          ULONG ulOfs;
+ +          strhGetBlock(&pSearch, "PAGE", &pszBlock, &pszAttrs, &ulOfs)
+ *
+ *      would do the following:
+ *
+ *      1)  set pszBlock to a new string containing "This is page 1."
+ *          without quotes;
+ *
+ *      2)  set pszAttrs to a new string containing "&lt;PAGE INDEX=1&gt;";
+ *
+ *      3)  set ulOfs to 0, because "&lt;PAGE" was found at the beginning;
+ *
+ *      4)  pSearch would be advanced to point to the "More text"
+ *          string in the original buffer.
+ *
+ *      Hey-hey. A one-shot function, fairly complicated, but indispensable
+ *      for HTML parsing.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (2000-01-03) [umoeller]: fixed heap overwrites (thanks to string debugging)
+ *@@changed V0.9.1 (2000-01-06) [umoeller]: changed prototype
+ *@@changed V0.9.3 (2000-05-06) [umoeller]: NULL string check was missing
+ */
+
+ULONG strhGetBlock(const char *pszSearchIn, // in: buffer to search
+                   PULONG pulSearchOffset, // in/out: offset where to start search (0 for beginning)
+                   PSZ pszTag,
+                   PSZ *ppszBlock,      // out: block enclosed by the tags
+                   PSZ *ppszAttribs,    // out: attributes of the opening tag
+                   PULONG pulOfsBeginTag, // out: offset from pszSearchIn where opening tag was found
+                   PULONG pulOfsBeginBlock) // out: offset from pszSearchIn where beginning of block was found
+{
+    ULONG   ulrc = 1;
+    PSZ     pszBeginTag = (PSZ)pszSearchIn + *pulSearchOffset,
+            pszSearch2 = pszBeginTag,
+            pszClosingTag;
+    ULONG   cbTag = strlen(pszTag);
+
+    // go thru the block and check all tags if it's the
+    // begin tag we're looking for
+    while ((pszBeginTag = strchr(pszBeginTag, '<')))
+    {
+        if (memicmp(pszBeginTag+1, pszTag, strlen(pszTag)) == 0)
+            // yes: stop
+            break;
+        else
+            pszBeginTag++;
+    }
+
+    if (pszBeginTag)
+    {
+        // we found <TAG>:
+        ULONG   ulNestingLevel = 0;
+
+        PSZ     pszEndOfBeginTag = strhFindEndOfTag(pszBeginTag);
+                                    // strchr(pszBeginTag, '>');
+        if (pszEndOfBeginTag)
+        {
+            // does the caller want the attributes?
+            if (ppszAttribs)
+            {
+                // yes: then copy them
+                ULONG   ulAttrLen = pszEndOfBeginTag - pszBeginTag;
+                PSZ     pszAttrs = (PSZ)malloc(ulAttrLen + 1);
+                strncpy(pszAttrs, pszBeginTag, ulAttrLen);
+                // add terminating 0
+                *(pszAttrs + ulAttrLen) = 0;
+
+                *ppszAttribs = pszAttrs;
+            }
+
+            // output offset of where we found the begin tag
+            if (pulOfsBeginTag)
+                *pulOfsBeginTag = pszBeginTag - (PSZ)pszSearchIn;
+
+            // now find corresponding closing tag (e.g. "</BODY>"
+            pszBeginTag = pszEndOfBeginTag+1;
+            // now we're behind the '>' char of the opening tag
+            // increase offset of that too
+            if (pulOfsBeginBlock)
+                *pulOfsBeginBlock = pszBeginTag - (PSZ)pszSearchIn;
+
+            // find next closing tag;
+            // for the first run, pszSearch2 points to right
+            // after the '>' char of the opening tag
+            pszSearch2 = pszBeginTag;
+            while (     (pszSearch2)        // fixed V0.9.3 (2000-05-06) [umoeller]
+                    &&  (pszClosingTag = strstr(pszSearch2, "<"))
+                  )
+            {
+                // if we have another opening tag before our closing
+                // tag, we need to have several closing tags before
+                // we're done
+                if (memicmp(pszClosingTag+1, pszTag, cbTag) == 0)
+                    ulNestingLevel++;
+                else
+                {
+                    // is this ours?
+                    if (    (*(pszClosingTag+1) == '/')
+                         && (memicmp(pszClosingTag+2, pszTag, cbTag) == 0)
+                       )
+                    {
+                        // we've found a matching closing tag; is
+                        // it ours?
+                        if (ulNestingLevel == 0)
+                        {
+                            // our closing tag found:
+                            // allocate mem for a new buffer
+                            // and extract all the text between
+                            // open and closing tags to it
+                            ULONG ulLen = pszClosingTag - pszBeginTag;
+                            if (ppszBlock)
+                            {
+                                PSZ pNew = (PSZ)malloc(ulLen + 1);
+                                strhncpy0(pNew, pszBeginTag, ulLen);
+                                *ppszBlock = pNew;
+                            }
+
+                            // raise search offset to after the closing tag
+                            *pulSearchOffset = (pszClosingTag + cbTag + 1) - (PSZ)pszSearchIn;
+
+                            ulrc = 0;
+
+                            break;
+                        } else
+                            // not our closing tag:
+                            ulNestingLevel--;
+                    }
+                }
+                // no matching closing tag: search on after that
+                pszSearch2 = strhFindEndOfTag(pszClosingTag);
+            } // end while (pszClosingTag = strstr(pszSearch2, "<"))
+
+            if (!pszClosingTag)
+                // no matching closing tag found:
+                // return 2 (closing tag not found)
+                ulrc = 2;
+        } // end if (pszBeginTag)
+        else
+            // no matching ">" for opening tag found:
+            ulrc = 3;
+    }
+
+    return (ulrc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Miscellaneous                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ strhArrayAppend:
+ *      this appends a string to a "string array".
+ *
+ *      A string array is considered a sequence of
+ *      zero-terminated strings in memory. That is,
+ *      after each string's null-byte, the next
+ *      string comes up.
+ *
+ *      This is useful for composing a single block
+ *      of memory from, say, list box entries, which
+ *      can then be written to OS2.INI in one flush.
+ *
+ *      To append strings to such an array, call this
+ *      function for each string you wish to append.
+ *      This will re-allocate *ppszRoot with each call,
+ *      and update *pcbRoot, which then contains the
+ *      total size of all strings (including all null
+ *      terminators).
+ *
+ *      Pass *pcbRoot to PrfSaveProfileData to have the
+ *      block saved.
+ *
+ *      Note: On the first call, *ppszRoot and *pcbRoot
+ *      _must_ be both NULL, or this crashes.
+ */
+
+VOID strhArrayAppend(PSZ *ppszRoot,         // in: root of array
+                     PSZ pszNew,            // in: string to append
+                     PULONG pcbRoot)        // in/out: size of array
+{
+    ULONG cbNew = strlen(pszNew);
+    PSZ pszTemp = (PSZ)malloc(*pcbRoot
+                              + cbNew
+                              + 1);    // two null bytes
+    if (*ppszRoot)
+    {
+        // not first loop: copy old stuff
+        memcpy(pszTemp,
+               *ppszRoot,
+               *pcbRoot);
+        free(*ppszRoot);
+    }
+    // append new string
+    strcpy(pszTemp + *pcbRoot,
+           pszNew);
+    // update root
+    *ppszRoot = pszTemp;
+    // update length
+    *pcbRoot += cbNew + 1;
+}
+
+/*
+ *@@ strhCreateDump:
+ *      this dumps a memory block into a string
+ *      and returns that string in a new buffer.
+ *
+ *      You must free() the returned PSZ after use.
+ *
+ *      The output looks like the following:
+ *
+ +          0000:  FE FF 0E 02 90 00 00 00   ........
+ +          0008:  FD 01 00 00 57 50 46 6F   ....WPFo
+ +          0010:  6C 64 65 72 00 78 01 34   lder.x.4
+ *
+ *      Each line is terminated with a newline (\n)
+ *      character only.
+ *
+ *@@added V0.9.1 (2000-01-22) [umoeller]
+ */
+
+PSZ strhCreateDump(PBYTE pb,            // in: start address of buffer
+                   ULONG ulSize,        // in: size of buffer
+                   ULONG ulIndent)      // in: indentation of every line
+{
+    PSZ     pszReturn = NULL;
+    CHAR    szTemp[1000];
+
+    PBYTE   pbCurrent = pb;                 // current byte
+    ULONG   ulCount = 0,
+            ulCharsInLine = 0;              // if this grows > 7, a new line is started
+    CHAR    szLine[400] = "",
+            szAscii[30] = "         ";      // ASCII representation; filled for every line
+    PSZ     pszLine = szLine,
+            pszAscii = szAscii;
+
+    for (pbCurrent = pb;
+         ulCount < ulSize;
+         pbCurrent++, ulCount++)
+    {
+        if (ulCharsInLine == 0)
+        {
+            memset(szLine, ' ', ulIndent);
+            pszLine += ulIndent;
+        }
+        pszLine += sprintf(pszLine, "%02lX ", *pbCurrent);
+
+        if ( (*pbCurrent > 31) && (*pbCurrent < 127) )
+            // printable character:
+            *pszAscii = *pbCurrent;
+        else
+            *pszAscii = '.';
+        pszAscii++;
+
+        ulCharsInLine++;
+        if (    (ulCharsInLine > 7)         // 8 bytes added?
+             || (ulCount == ulSize-1)       // end of buffer reached?
+           )
+        {
+            // if we haven't had eight bytes yet,
+            // fill buffer up to eight bytes with spaces
+            ULONG   ul2;
+            for (ul2 = ulCharsInLine;
+                 ul2 < 8;
+                 ul2++)
+                pszLine += sprintf(pszLine, "   ");
+
+            sprintf(szTemp, "%04lX:  %s  %s\n",
+                            (ulCount & 0xFFFFFFF8),  // offset in hex
+                            szLine,         // bytes string
+                            szAscii);       // ASCII string
+            xstrcat(&pszReturn, szTemp);
+
+            // restart line buffer
+            pszLine = szLine;
+
+            // clear ASCII buffer
+            strcpy(szAscii, "         ");
+            pszAscii = szAscii;
+
+            // reset line counter
+            ulCharsInLine = 0;
+        }
+    }
+
+    return (pszReturn);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Wildcard matching                                              *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *  The following code has been taken from "fnmatch.zip".
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+/* In OS/2 and DOS styles, both / and \ separate components of a path.
+ * This macro returns true iff C is a separator. */
+
+#define IS_OS2_COMP_SEP(C)  ((C) == '/' || (C) == '\\')
+
+
+/* This macro returns true if C is at the end of a component of a
+ * path. */
+
+#define IS_OS2_COMP_END(C)  ((C) == 0 || IS_OS2_COMP_SEP (C))
+
+/*
+ *@@ skip_comp_os2:
+ *      Return a pointer to the next component of the path SRC, for OS/2
+ *      and DOS styles.  When the end of the string is reached, a pointer
+ *      to the terminating null character is returned.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static const unsigned char* skip_comp_os2(const unsigned char *src)
+{
+    /* Skip characters until hitting a separator or the end of the
+     * string. */
+
+    while (!IS_OS2_COMP_END(*src))
+        ++src;
+
+    /* Skip the separator if we hit a separator. */
+
+    if (*src != 0)
+        ++src;
+    return src;
+}
+
+/*
+ * has_colon:
+ *      returns true iff the path P contains a colon.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static int has_colon(const unsigned char *p)
+{
+    while (*p != 0)
+        if (*p == ':')
+            return 1;
+        else
+            ++p;
+    return 0;
+}
+
+/*
+ * match_comp_os2:
+ *      Compare a single component (directory name or file name) of the
+ *      paths, for OS/2 and DOS styles.  MASK and NAME point into a
+ *      component of the wildcard and the name to be checked, respectively.
+ *      Comparing stops at the next separator.  The FLAGS argument is the
+ *      same as that of fnmatch().  HAS_DOT is true if a dot is in the
+ *      current component of NAME.  The number of dots is not restricted,
+ *      even in DOS style.  Return FNM_MATCH iff MASK and NAME match.
+ *      Note that this function is recursive.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static int match_comp_os2(const unsigned char *mask,
+                          const unsigned char *name,
+                          unsigned flags,
+                          int has_dot)
+{
+    int             rc;
+
+    for (;;)
+        switch (*mask)
+        {
+            case 0:
+
+                /* There must be no extra characters at the end of NAME when
+                 * reaching the end of MASK unless _FNM_PATHPREFIX is set:
+                 * in that case, NAME may point to a separator. */
+
+                if (*name == 0)
+                    return FNM_MATCH;
+                if ((flags & _FNM_PATHPREFIX) && IS_OS2_COMP_SEP(*name))
+                    return FNM_MATCH;
+                return FNM_NOMATCH;
+
+            case '/':
+            case '\\':
+
+                /* Separators match separators. */
+
+                if (IS_OS2_COMP_SEP(*name))
+                    return FNM_MATCH;
+
+                /* If _FNM_PATHPREFIX is set, a trailing separator in MASK
+                 * is ignored at the end of NAME. */
+
+                if ((flags & _FNM_PATHPREFIX) && mask[1] == 0 && *name == 0)
+                    return FNM_MATCH;
+
+                /* Stop comparing at the separator. */
+
+                return FNM_NOMATCH;
+
+            case '?':
+
+                /* A question mark matches one character.  It does not match
+                 * a dot.  At the end of the component (and before a dot),
+                 * it also matches zero characters. */
+
+                if (*name != '.' && !IS_OS2_COMP_END(*name))
+                    ++name;
+                ++mask;
+                break;
+
+            case '*':
+
+                /* An asterisk matches zero or more characters.  In DOS
+                 * mode, dots are not matched. */
+
+                do
+                {
+                    ++mask;
+                }
+                while (*mask == '*');
+                for (;;)
+                {
+                    rc = match_comp_os2(mask, name, flags, has_dot);
+                    if (rc != FNM_NOMATCH)
+                        return rc;
+                    if (IS_OS2_COMP_END(*name))
+                        return FNM_NOMATCH;
+                    if (*name == '.' && (flags & _FNM_STYLE_MASK) == _FNM_DOS)
+                        return FNM_NOMATCH;
+                    ++name;
+                }
+
+            case '.':
+
+                /* A dot matches a dot.  It also matches the implicit dot at
+                 * the end of a dot-less NAME. */
+
+                ++mask;
+                if (*name == '.')
+                    ++name;
+                else if (has_dot || !IS_OS2_COMP_END(*name))
+                    return FNM_NOMATCH;
+                break;
+
+            default:
+
+                /* All other characters match themselves. */
+
+                if (flags & _FNM_IGNORECASE)
+                {
+                    if (tolower(*mask) != tolower(*name))
+                        return FNM_NOMATCH;
+                }
+                else
+                {
+                    if (*mask != *name)
+                        return FNM_NOMATCH;
+                }
+                ++mask;
+                ++name;
+                break;
+        }
+}
+
+/*
+ * match_comp:
+ *      compare a single component (directory name or file name) of the
+ *      paths, for all styles which need component-by-component matching.
+ *      MASK and NAME point to the start of a component of the wildcard and
+ *      the name to be checked, respectively.  Comparing stops at the next
+ *      separator.  The FLAGS argument is the same as that of fnmatch().
+ *      Return FNM_MATCH iff MASK and NAME match.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static int match_comp(const unsigned char *mask,
+                      const unsigned char *name,
+                      unsigned flags)
+{
+    const unsigned char *s;
+
+    switch (flags & _FNM_STYLE_MASK)
+    {
+        case _FNM_OS2:
+        case _FNM_DOS:
+
+            /* For OS/2 and DOS styles, we add an implicit dot at the end of
+             * the component if the component doesn't include a dot. */
+
+            s = name;
+            while (!IS_OS2_COMP_END(*s) && *s != '.')
+                ++s;
+            return match_comp_os2(mask, name, flags, *s == '.');
+
+        default:
+            return FNM_ERR;
+    }
+}
+
+/* In Unix styles, / separates components of a path.  This macro
+ * returns true iff C is a separator. */
+
+#define IS_UNIX_COMP_SEP(C)  ((C) == '/')
+
+
+/* This macro returns true if C is at the end of a component of a
+ * path. */
+
+#define IS_UNIX_COMP_END(C)  ((C) == 0 || IS_UNIX_COMP_SEP (C))
+
+/*
+ * match_unix:
+ *      match complete paths for Unix styles.  The FLAGS argument is the
+ *      same as that of fnmatch().  COMP points to the start of the current
+ *      component in NAME.  Return FNM_MATCH iff MASK and NAME match.  The
+ *      backslash character is used for escaping ? and * unless
+ *      FNM_NOESCAPE is set.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static int match_unix(const unsigned char *mask,
+                      const unsigned char *name,
+                      unsigned flags,
+                      const unsigned char *comp)
+{
+    unsigned char   c1, c2;
+    char            invert, matched;
+    const unsigned char *start;
+    int             rc;
+
+    for (;;)
+        switch (*mask)
+        {
+            case 0:
+
+                /* There must be no extra characters at the end of NAME when
+                 * reaching the end of MASK unless _FNM_PATHPREFIX is set:
+                 * in that case, NAME may point to a separator. */
+
+                if (*name == 0)
+                    return FNM_MATCH;
+                if ((flags & _FNM_PATHPREFIX) && IS_UNIX_COMP_SEP(*name))
+                    return FNM_MATCH;
+                return FNM_NOMATCH;
+
+            case '?':
+
+                /* A question mark matches one character.  It does not match
+                 * the component separator if FNM_PATHNAME is set.  It does
+                 * not match a dot at the start of a component if FNM_PERIOD
+                 * is set. */
+
+                if (*name == 0)
+                    return FNM_NOMATCH;
+                if ((flags & FNM_PATHNAME) && IS_UNIX_COMP_SEP(*name))
+                    return FNM_NOMATCH;
+                if (*name == '.' && (flags & FNM_PERIOD) && name == comp)
+                    return FNM_NOMATCH;
+                ++mask;
+                ++name;
+                break;
+
+            case '*':
+
+                /* An asterisk matches zero or more characters.  It does not
+                 * match the component separator if FNM_PATHNAME is set.  It
+                 * does not match a dot at the start of a component if
+                 * FNM_PERIOD is set. */
+
+                if (*name == '.' && (flags & FNM_PERIOD) && name == comp)
+                    return FNM_NOMATCH;
+                do
+                {
+                    ++mask;
+                }
+                while (*mask == '*');
+                for (;;)
+                {
+                    rc = match_unix(mask, name, flags, comp);
+                    if (rc != FNM_NOMATCH)
+                        return rc;
+                    if (*name == 0)
+                        return FNM_NOMATCH;
+                    if ((flags & FNM_PATHNAME) && IS_UNIX_COMP_SEP(*name))
+                        return FNM_NOMATCH;
+                    ++name;
+                }
+
+            case '/':
+
+                /* Separators match only separators.  If _FNM_PATHPREFIX is
+                 * set, a trailing separator in MASK is ignored at the end
+                 * of NAME. */
+
+                if (!(IS_UNIX_COMP_SEP(*name)
+                      || ((flags & _FNM_PATHPREFIX) && *name == 0
+                      && (mask[1] == 0
+                          || (!(flags & FNM_NOESCAPE) && mask[1] == '\\'
+                  && mask[2] == 0)))))
+                    return FNM_NOMATCH;
+
+                ++mask;
+                if (*name != 0)
+                    ++name;
+
+                /* This is the beginning of a new component if FNM_PATHNAME
+                 * is set. */
+
+                if (flags & FNM_PATHNAME)
+                    comp = name;
+                break;
+
+            case '[':
+
+                /* A set of characters.  Always case-sensitive. */
+
+                if (*name == 0)
+                    return FNM_NOMATCH;
+                if ((flags & FNM_PATHNAME) && IS_UNIX_COMP_SEP(*name))
+                    return FNM_NOMATCH;
+                if (*name == '.' && (flags & FNM_PERIOD) && name == comp)
+                    return FNM_NOMATCH;
+
+                invert = 0;
+                matched = 0;
+                ++mask;
+
+                /* If the first character is a ! or ^, the set matches all
+                 * characters not listed in the set. */
+
+                if (*mask == '!' || *mask == '^')
+                {
+                    ++mask;
+                    invert = 1;
+                }
+
+                /* Loop over all the characters of the set.  The loop ends
+                 * if the end of the string is reached or if a ] is
+                 * encountered unless it directly follows the initial [ or
+                 * [-. */
+
+                start = mask;
+                while (!(*mask == 0 || (*mask == ']' && mask != start)))
+                {
+                    /* Get the next character which is optionally preceded
+                     * by a backslash. */
+
+                    c1 = *mask++;
+                    if (!(flags & FNM_NOESCAPE) && c1 == '\\')
+                    {
+                        if (*mask == 0)
+                            break;
+                        c1 = *mask++;
+                    }
+
+                    /* Ranges of characters are written as a-z.  Don't
+                     * forget to check for the end of the string and to
+                     * handle the backslash.  If the character after - is a
+                     * ], it isn't a range. */
+
+                    if (*mask == '-' && mask[1] != ']')
+                    {
+                        ++mask; /* Skip the - character */
+                        if (!(flags & FNM_NOESCAPE) && *mask == '\\')
+                            ++mask;
+                        if (*mask == 0)
+                            break;
+                        c2 = *mask++;
+                    }
+                    else
+                        c2 = c1;
+
+                    /* Now check whether this character or range matches NAME. */
+
+                    if (c1 <= *name && *name <= c2)
+                        matched = 1;
+                }
+
+                /* If the end of the string is reached before a ] is found,
+                 * back up to the [ and compare it to NAME. */
+
+                if (*mask == 0)
+                {
+                    if (*name != '[')
+                        return FNM_NOMATCH;
+                    ++name;
+                    mask = start;
+                    if (invert)
+                        --mask;
+                }
+                else
+                {
+                    if (invert)
+                        matched = !matched;
+                    if (!matched)
+                        return FNM_NOMATCH;
+                    ++mask;     /* Skip the ] character */
+                    if (*name != 0)
+                        ++name;
+                }
+                break;
+
+            case '\\':
+                ++mask;
+                if (flags & FNM_NOESCAPE)
+                {
+                    if (*name != '\\')
+                        return FNM_NOMATCH;
+                    ++name;
+                }
+                else if (*mask == '*' || *mask == '?')
+                {
+                    if (*mask != *name)
+                        return FNM_NOMATCH;
+                    ++mask;
+                    ++name;
+                }
+                break;
+
+            default:
+
+                /* All other characters match themselves. */
+
+                if (flags & _FNM_IGNORECASE)
+                {
+                    if (tolower(*mask) != tolower(*name))
+                        return FNM_NOMATCH;
+                }
+                else
+                {
+                    if (*mask != *name)
+                        return FNM_NOMATCH;
+                }
+                ++mask;
+                ++name;
+                break;
+        }
+}
+
+/*
+ * _fnmatch_unsigned:
+ *      Check whether the path name NAME matches the wildcard MASK.
+ *
+ *      Return:
+ *      --   0 (FNM_MATCH) if it matches,
+ *      --   _FNM_NOMATCH if it doesn't,
+ *      --   FNM_ERR on error.
+ *
+ *      The operation of this function is controlled by FLAGS.
+ *      This is an internal function, with unsigned arguments.
+ *
+ *      (c) 1994-1996 by Eberhard Mattes.
+ */
+
+static int _fnmatch_unsigned(const unsigned char *mask,
+                             const unsigned char *name,
+                             unsigned flags)
+{
+    int             m_drive, n_drive,
+                    rc;
+
+    /* Match and skip the drive name if present. */
+
+    m_drive = ((isalpha(mask[0]) && mask[1] == ':') ? mask[0] : -1);
+    n_drive = ((isalpha(name[0]) && name[1] == ':') ? name[0] : -1);
+
+    if (m_drive != n_drive)
+    {
+        if (m_drive == -1 || n_drive == -1)
+            return FNM_NOMATCH;
+        if (!(flags & _FNM_IGNORECASE))
+            return FNM_NOMATCH;
+        if (tolower(m_drive) != tolower(n_drive))
+            return FNM_NOMATCH;
+    }
+
+    if (m_drive != -1)
+        mask += 2;
+    if (n_drive != -1)
+        name += 2;
+
+    /* Colons are not allowed in path names, except for the drive name,
+     * which was skipped above. */
+
+    if (has_colon(mask) || has_colon(name))
+        return FNM_ERR;
+
+    /* The name "\\server\path" should not be matched by mask
+     * "\*\server\path".  Ditto for /. */
+
+    switch (flags & _FNM_STYLE_MASK)
+    {
+        case _FNM_OS2:
+        case _FNM_DOS:
+
+            if (IS_OS2_COMP_SEP(name[0]) && IS_OS2_COMP_SEP(name[1]))
+            {
+                if (!(IS_OS2_COMP_SEP(mask[0]) && IS_OS2_COMP_SEP(mask[1])))
+                    return FNM_NOMATCH;
+                name += 2;
+                mask += 2;
+            }
+            break;
+
+        case _FNM_POSIX:
+
+            if (name[0] == '/' && name[1] == '/')
+            {
+                int             i;
+
+                name += 2;
+                for (i = 0; i < 2; ++i)
+                    if (mask[0] == '/')
+                        ++mask;
+                    else if (mask[0] == '\\' && mask[1] == '/')
+                        mask += 2;
+                    else
+                        return FNM_NOMATCH;
+            }
+
+            /* In Unix styles, treating ? and * w.r.t. components is simple.
+             * No need to do matching component by component. */
+
+            return match_unix(mask, name, flags, name);
+    }
+
+    /* Now compare all the components of the path name, one by one.
+     * Note that the path separator must not be enclosed in brackets. */
+
+    while (*mask != 0 || *name != 0)
+    {
+
+        /* If _FNM_PATHPREFIX is set, the names match if the end of MASK
+         * is reached even if there are components left in NAME. */
+
+        if (*mask == 0 && (flags & _FNM_PATHPREFIX))
+            return FNM_MATCH;
+
+        /* Compare a single component of the path name. */
+
+        rc = match_comp(mask, name, flags);
+        if (rc != FNM_MATCH)
+            return rc;
+
+        /* Skip to the next component or to the end of the path name. */
+
+        mask = skip_comp_os2(mask);
+        name = skip_comp_os2(name);
+    }
+
+    /* If we reached the ends of both strings, the names match. */
+
+    if (*mask == 0 && *name == 0)
+        return FNM_MATCH;
+
+    /* The names do not match. */
+
+    return FNM_NOMATCH;
+}
+
+/*
+ *@@ strhMatchOS2:
+ *      this matches wildcards, similar to what DosEditName does.
+ *      However, this does not require a file to be present, but
+ *      works on strings only.
+ */
+
+BOOL strhMatchOS2(const unsigned char* pcszMask,     // in: mask (e.g. "*.txt")
+                  const unsigned char* pcszName)     // in: string to check (e.g. "test.txt")
+{
+    return ((BOOL)(_fnmatch_unsigned(pcszMask,
+                                     pcszName,
+                                     _FNM_OS2 | _FNM_IGNORECASE)
+                   == FNM_MATCH)
+           );
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Fast string searches                                           *
+ *                                                                  *
+ ********************************************************************/
+
+#define ASSERT(a)
+
+/*
+ *      The following code has been taken from the "Standard
+ *      Function Library", file sflfind.c, and only slightly
+ *      modified to conform to the rest of this file.
+ *
+ *      Written:    96/04/24  iMatix SFL project team <sfl@imatix.com>
+ *      Revised:    98/05/04
+ *
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *
+ *      The SFL Licence allows incorporating SFL code into other
+ *      programs, as long as the copyright is reprinted and the
+ *      code is marked as modified, so this is what we do.
+ */
+
+/*
+ *@@ strhfind:
+ *      searches for a pattern in a string using the Boyer-Moore-
+ *      Horspool-Sunday algorithm.  The string and pattern are null-terminated
+ *      strings.  Returns a pointer to the pattern if found within the string,
+ *      or NULL if the pattern was not found.  If you repeatedly scan for the
+ *      same pattern, use the repeat_find argument.  If this is TRUE, the
+ *      function does not re-parse the pattern.  You must of course call the
+ *      function with repeat_find equal to FALSE the first time.  This function
+ *      is meant to handle  character data, and is most effective when you work
+ *      with large strings.  To search binary data use strhmemfind().  Will not work
+ *      on multibyte characters.
+ *
+ *      Examples:
+ +      char *result;
+ +
+ +      result = strhfind ("abracadabra", "cad", FALSE);
+ +      if (result)
+ +          puts (result);
+ +
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+char* strhfind (const char *string,            //  String containing data
+                const char *pattern,           //  Pattern to search for
+                BOOL repeat_find)              //  Same pattern as last time
+{
+    static size_t
+        searchbuf [256];                //  Fixed search buffer
+
+    ASSERT (string);                    //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  fall through if not debugging
+
+    return (char *) strhmemfind_rb (string,    strlen (string),
+                                pattern,   strlen (pattern),
+                                searchbuf, &repeat_find);
+}
+
+/*
+ *@@ strhfind_r:
+ *      searches for a pattern in a string using the Boyer-Moore-
+ *      Horspool-Sunday algorithm.  The string and pattern are null-terminated
+ *      strings.  Returns a pointer to the pattern if found within the string,
+ *      or NULL if the pattern was not found.  This function is meant to handle
+ *      character data, and is most effective when you work with large strings.
+ *      To search binary data use strhmemfind().  Will not work on multibyte
+ *      characters.  Reentrant.
+ *
+ *      Examples:
+ +      char *result;
+ +
+ +      result = strhfind_r ("abracadabra", "cad");
+ +      if (result)
+ +          puts (result);
+ *
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+char* strhfind_r (const char *string,          //  String containing data
+                  const char *pattern)         //  Pattern to search for
+{
+    size_t
+        searchbuf [256];                //  One-time search buffer
+    BOOL
+        secondtime = FALSE;             //  Search buffer init needed
+
+    ASSERT (string);                    //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  fall through if not debugging
+
+    return (char *) strhmemfind_rb (string,    strlen (string),
+                                    pattern,   strlen (pattern),
+                                    searchbuf, &secondtime);
+}
+
+/*
+ *@@ strhfind_rb:
+ *      searches for a pattern in a string using the Boyer-Moore-
+ *      Horspool-Sunday algorithm.  The string and pattern are null-terminated
+ *      strings.  Returns a pointer to the pattern if found within the string,
+ *      or NULL if the pattern was not found.  Supports more efficient repeat
+ *      searches (for the same pattern), through a supplied search buffer.  The
+ *      search buffer must be long enough to contain 256 (2**8) size_t entries.
+ *      On the first call repeat_find must be set to FALSE.  After the search
+ *      buffer has been initialised, repeat_find will be set to TRUE by the
+ *      function, avoiding the search buffer initialisation on later calls.
+ *
+ *      This function is most effective when repeated searches are made for
+ *      the same pattern in one or more strings.  This function is meant to
+ *      handle  character data, and is most effective when you work with
+ *      large strings.  To search binary data use strhmemfind().  Will not work
+ *      on multibyte characters.  Reentrant.
+ *
+ *      Examples:
+ +      char   *result;
+ +      BOOL   repeat_search = FALSE;
+ +      size_t searchbuf[256];
+ +
+ +      result = strhfind_rb ("abracadabra", "cad", searchbuf, &repeat_search);
+ +      if (result)
+ +        {
+ +          puts (result);
+ +          result = strhfind_rb ("cad/cam", "cad", searchbuf, &repeat_search);
+ +          if (result)
+ +              puts (result);
+ +        }
+ *
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+char* strhfind_rb (const char *string,         //  String containing data
+                   const char *pattern,        //  Pattern to search for
+                   size_t     *shift,          //  Working buffer between searches
+                   BOOL       *repeat_find)    //  Flag for first/later search
+{
+    ASSERT (string);                    //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  fall through if not debugging
+    ASSERT (shift);
+    ASSERT (repeat_find);
+
+    return (char *) strhmemfind_rb (string,    strlen (string),
+                                    pattern,   strlen (pattern),
+                                    shift,     repeat_find);
+}
+
+/*
+ *@@ strhmemfind:
+ *      searches for a pattern in a block of memory using the Boyer-
+ *      Moore-Horspool-Sunday algorithm.  The block and pattern may contain any
+ *      values; you must explicitly provide their lengths.  Returns a pointer to
+ *      the pattern if found within the block, or NULL if the pattern was not
+ *      found.  If you repeatedly scan for the same pattern, use the repeat_find
+ *      argument.  If this is TRUE, the function does not re-parse the pattern.
+ *      This function is meant to handle binary data.  If you need to search
+ *      strings, use the strhfind_r or strhfind_rb() functions.  Non-Reentrant.
+ *
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+void* strhmemfind (const void  *block,            //  Block containing data
+                   size_t       block_size,       //  Size of block in bytes
+                   const void  *pattern,          //  Pattern to search for
+                   size_t       pattern_size,     //  Size of pattern block
+                   BOOL         repeat_find)      //  Same pattern as last time
+{
+    static size_t
+        searchbuf [256];                //  Static shared search buffer
+
+    ASSERT (block);                     //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  full through if not debugging
+
+    return strhmemfind_rb (block, block_size, pattern, pattern_size,
+                       searchbuf, &repeat_find);
+}
+
+/*
+ *@@ strhmemfind_r:
+ *      searches for a pattern in a block of memory using the Boyer-
+ *      Moore-Horspool-Sunday algorithm.  The block and pattern may contain any
+ *      values; you must explicitly provide their lengths.  Returns a pointer to
+ *      the pattern if found within the block, or NULL if the pattern was not
+ *      found.
+ *
+ *      This function is meant to handle binary data, for a single search for
+ *      a given pattern.  If you need to search strings, use the strhfind_r()
+ *      or strhfind_rb() functions.  If you want to do efficient repeated searches
+ *      for one pattern, use strhmemfind_rb().  Reentrant.
+ *
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+void* strhmemfind_r (const void  *block,          //  Block containing data
+                     size_t       block_size,     //  Size of block in bytes
+                     const void  *pattern,        //  Pattern to search for
+                     size_t       pattern_size)   //  Size of pattern block
+{
+    size_t
+        searchbuf [256];                //  One-time search buffer
+    BOOL
+        secondtime  = FALSE;
+
+    ASSERT (block);                     //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  full through if not debugging
+
+    return strhmemfind_rb (block, block_size, pattern, pattern_size,
+                           searchbuf, &secondtime);
+}
+
+/*
+ *@@ strhmemfind_rb:
+ *      searches for a pattern in a block of memory using the Boyer-
+ *      Moore-Horspool-Sunday algorithm.  The block and pattern may contain any
+ *      values; you must explicitly provide their lengths.  Returns a pointer to
+ *      the pattern if found within the block, or NULL if the pattern was not
+ *      found.  On the first search with a given pattern, *repeat_find should
+ *      be FALSE.  It will be set to TRUE after the shift table is initialised,
+ *      allowing the initialisation phase to be skipped on subsequent searches.
+ *      shift must point to an array big enough to hold 256 (8**2) size_t values.
+ *
+ *      This function is meant to handle binary data, for repeated searches
+ *      for the same pattern.  If you need to search strings, use the
+ *      strhfind_r() or strhfind_rb() functions.  If you wish to search for a
+ *      pattern only once consider using strhmemfind_r(). Reentrant.
+ *
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+void* strhmemfind_rb (const void  *in_block,      //  Block containing data
+                      size_t       block_size,    //  Size of block in bytes
+                      const void  *in_pattern,    //  Pattern to search for
+                      size_t       pattern_size,  //  Size of pattern block
+                      size_t      *shift,         //  Shift table (search buffer)
+                      BOOL        *repeat_find)   //  TRUE: search buffer already init
+{
+    size_t
+        byte_nbr,                       //  Distance through block
+        match_size;                     //  Size of matched part
+    const unsigned char
+        *match_base = NULL,             //  Base of match of pattern
+        *match_ptr  = NULL,             //  Point within current match
+        *limit      = NULL;             //  Last potiental match point
+    const unsigned char
+        *block   = (unsigned char *) in_block,   //  Concrete pointer to block data
+        *pattern = (unsigned char *) in_pattern; //  Concrete pointer to search value
+
+    ASSERT (block);                     //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  fail gracefully if not debugging
+    ASSERT (shift);                     //  NULL repeat_find => is false
+    if (block == NULL || pattern == NULL || shift == NULL)
+        return (NULL);
+
+    //  Pattern must be smaller or equal in size to string
+    if (block_size < pattern_size)
+        return (NULL);                  //  Otherwise it's not found
+
+    if (pattern_size == 0)              //  Empty patterns match at start
+        return ((void *)block);
+
+    //  Build the shift table unless we're continuing a previous search
+
+    //  The shift table determines how far to shift before trying to match
+    //  again, if a match at this point fails.  If the byte after where the
+    //  end of our pattern falls is not in our pattern, then we start to
+    //  match again after that byte; otherwise we line up the last occurence
+    //  of that byte in our pattern under that byte, and try match again.
+
+    if (!repeat_find || !*repeat_find)
+      {
+        for (byte_nbr = 0; byte_nbr < 256; byte_nbr++)
+            shift [byte_nbr] = pattern_size + 1;
+        for (byte_nbr = 0; byte_nbr < pattern_size; byte_nbr++)
+            shift [(unsigned char) pattern [byte_nbr]] = pattern_size - byte_nbr;
+
+        if (repeat_find)
+            *repeat_find = TRUE;
+      }
+
+    //  Search for the block, each time jumping up by the amount
+    //  computed in the shift table
+
+    limit = block + (block_size - pattern_size + 1);
+    ASSERT (limit > block);
+
+    for (match_base = block;
+         match_base < limit;
+         match_base += shift [*(match_base + pattern_size)])
+      {
+        match_ptr  = match_base;
+        match_size = 0;
+
+        //  Compare pattern until it all matches, or we find a difference
+        while (*match_ptr++ == pattern [match_size++])
+          {
+            ASSERT (match_size <= pattern_size &&
+                    match_ptr == (match_base + match_size));
+
+            //  If we found a match, return the start address
+            if (match_size >= pattern_size)
+              return ((void*)(match_base));
+
+          }
+      }
+    return (NULL);                      //  Found nothing
+}
+
+/*
+ *@@ strhtxtfind:
+ *      searches for a case-insensitive text pattern in a string
+ *      using the Boyer-Moore-Horspool-Sunday algorithm.  The string and
+ *      pattern are null-terminated strings.  Returns a pointer to the pattern
+ *      if found within the string, or NULL if the pattern was not found.
+ *      Will match strings irrespective of case.  To match exact strings, use
+ *      strhfind().  Will not work on multibyte characters.
+ *
+ *      Examples:
+ +      char *result;
+ +
+ +      result = strhtxtfind ("AbracaDabra", "cad");
+ +      if (result)
+ +          puts (result);
+ +
+ *      Taken from the "Standard Function Library", file sflfind.c.
+ *      Copyright:  Copyright (c) 1991-99 iMatix Corporation.
+ *      Slightly modified.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+char* strhtxtfind (const char *string,            //  String containing data
+                   const char *pattern)           //  Pattern to search for
+{
+    size_t
+        shift [256];                    //  Shift distance for each value
+    size_t
+        string_size,
+        pattern_size,
+        byte_nbr,                       //  Index into byte array
+        match_size;                     //  Size of matched part
+    const char
+        *match_base = NULL,             //  Base of match of pattern
+        *match_ptr  = NULL,             //  Point within current match
+        *limit      = NULL;             //  Last potiental match point
+
+    ASSERT (string);                    //  Expect non-NULL pointers, but
+    ASSERT (pattern);                   //  fail gracefully if not debugging
+    if (string == NULL || pattern == NULL)
+        return (NULL);
+
+    string_size  = strlen (string);
+    pattern_size = strlen (pattern);
+
+    //  Pattern must be smaller or equal in size to string
+    if (string_size < pattern_size)
+        return (NULL);                  //  Otherwise it cannot be found
+
+    if (pattern_size == 0)              //  Empty string matches at start
+        return (char *) string;
+
+    //  Build the shift table
+
+    //  The shift table determines how far to shift before trying to match
+    //  again, if a match at this point fails.  If the byte after where the
+    //  end of our pattern falls is not in our pattern, then we start to
+    //  match again after that byte; otherwise we line up the last occurence
+    //  of that byte in our pattern under that byte, and try match again.
+
+    for (byte_nbr = 0; byte_nbr < 256; byte_nbr++)
+        shift [byte_nbr] = pattern_size + 1;
+
+    for (byte_nbr = 0; byte_nbr < pattern_size; byte_nbr++)
+        shift [(unsigned char) tolower (pattern [byte_nbr])] = pattern_size - byte_nbr;
+
+    //  Search for the string.  If we don't find a match, move up by the
+    //  amount we computed in the shift table above, to find location of
+    //  the next potiental match.
+
+    limit = string + (string_size - pattern_size + 1);
+    ASSERT (limit > string);
+
+    for (match_base = string;
+         match_base < limit;
+         match_base += shift [(unsigned char) tolower (*(match_base + pattern_size))])
+      {
+        match_ptr  = match_base;
+        match_size = 0;
+
+        //  Compare pattern until it all matches, or we find a difference
+        while (tolower (*match_ptr++) == tolower (pattern [match_size++]))
+          {
+            ASSERT (match_size <= pattern_size &&
+                    match_ptr == (match_base + match_size));
+
+            //  If we found a match, return the start address
+            if (match_size >= pattern_size)
+                return ((char *)(match_base));
+          }
+      }
+    return (NULL);                      //  Found nothing
+}
+
Index: /trunk/src/helpers/syssound.c
===================================================================
--- /trunk/src/helpers/syssound.c	(revision 8)
+++ /trunk/src/helpers/syssound.c	(revision 8)
@@ -0,0 +1,701 @@
+
+/*
+ *@@sourcefile syssound.c:
+ *      this has code for querying and manipulating the
+ *      system sounds, which are stored in MMPM.INI in
+ *      the MMOS2 directory. Also, we have support for
+ *      manipulating Warp 4 sound schemes in here too.
+ *      This code works on Warp 3 also.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Some of this code used to be in XWorkplace's main\common.c,
+ *      as far as the regular system sounds were concerned.
+ *      However, all this code has been greatly reworked
+ *      and extended with V0.9.0.
+ *
+ *      <B>About system sound configuration data</B>
+ *
+ *      The current system sounds are stored in ?:\MMOS2\MMPM.INI.
+ *
+ *      These are the general flags in the "MMPM2_AlarmSoundsData"
+ *      application:
+ *      --  If "EnableSounds" is FALSE, all system sounds are disabled.
+ *          This defaults to TRUE (tested).
+ *      --  If "ApplyVolumeToAll" is TRUE, the same volume is used for
+ *          all sounds. This defaults to FALSE (tested).
+ *      --  If ApplyVolumeToAll is TRUE, "Volume" is used for the
+ *          global volume. Otherwise, the individual sound volumes
+ *          (below) will be used.
+ *
+ *      "MMPM2_AlarmSounds" then has all the system sounds. The keys
+ *      in that application are numerical indices, which are listed
+ *      in syssound.h.
+ *
+ *      Each sound data block in there consists of three elements:
+ +          soundfile#description#volume
+ *      where "description" is what is listed in the "Sound" object.
+ *      "volume" is only used when "ApplyVolumeToAll" (above) is FALSE.
+ *
+ *      We have functions in this code file for decoding/setting this
+ *      (sndParseSoundData, sndQuerySystemSound, sndWriteSoundData,
+ *      sndSetSystemSound).
+ *
+ *      By contrast (and for no real reason), Warp 4 stores the
+ *      "sound schemes" in OS2SYS.INI. I stuck with that for compatibility,
+ *      although this prevents several users from having different
+ *      sound schemes. What the heck.
+ *
+ *      Anyways, the "PM_SOUND_SCHEMES_LIST" application is a directory of
+ *      schemes, which in turn point to other applications in OS2SYS.INI
+ *      which have the actual sound scheme data. All these applications
+ *      appear to start with "PM_SOUND_xxx" by convention.
+ *
+ *      Note that as opposed to the sound data in MMPM.INI, only the
+ *      sound file name is stored here (not the three elements as
+ *      described above). We slightly extend that mechanism to add an
+ *      additional volume data field after the first null byte. This
+ *      allows the default WPSound object to still be able to read
+ *      the sound scheme data (it apparently uses PrfQueryProfileString)
+ *      while the XWorkplace sound object replacement (XWPSound) can
+ *      still store volume data as well.
+ *
+ *      I have created more functions in this code file to easily load and
+ *      store sound schemes (sndDoesSchemeExist, sndCreateSoundScheme,
+ *      sndLoadSoundScheme, sndDestroySoundScheme).
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\syssound.h"
+ *@@added V0.9.0 [umoeller]
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSERRORS
+#define INCL_WINSHELLDATA
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+#include "helpers\prfh.h"
+
+#include "helpers\syssound.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Profile (INI) helpers\System sounds
+ */
+
+/*
+ *@@ sndParseSoundData:
+ *      this helper func splits the system sound data
+ *      passed to it into three buffers.
+ *      Each key data in there has the following format:
+ +          soundfile#description#volume.
+ *
+ *      This is copied into the three specified buffers.
+ *      You can set any buffer pointer to NULL if you're
+ *      not interested in that data.
+ *
+ *      Returns the number of items successfully parsed,
+ *      which should be 3.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG sndParseSoundData(PSZ pszSoundData,  // in: INI data from MMPM.INI
+                        PSZ pszDescr,    // out: sound description, as displayed
+                               // in the "Sound" object (ptr may be NULL)
+                        PSZ pszFile,     // out: sound file to (ptr may be NULL)
+                        PULONG pulVolume)  // out: sound volume (0-100). Note:
+                               // this always returns the individual sound volume,
+                               // even if "Global volume" is set in MMPM.INI.
+{
+    PSZ         p1 = pszSoundData, p2;
+    ULONG       ulrc = 0;
+    // get sound file
+    p2 = strchr(p1, '#');
+    if (p2)
+    {
+        ulrc++;
+        if (pszFile)
+        {
+            strncpy(pszFile, p1, p2-p1);
+            pszFile[p2-p1] = '\0';
+        }
+        p1 = p2+1;
+
+        // get sound description
+        p2 = strchr(p1, '#');
+        if (p2)
+        {
+            ulrc++;
+            if (pszDescr)
+            {
+                strncpy(pszDescr, p1, p2-p1);
+                pszDescr[p2-p1] = '\0';
+            }
+            p1 = p2+1;
+
+            // get volume (0-100)
+            if (pulVolume)
+            {
+                // individual volume settings per sound
+                sscanf(p1, "%d", pulVolume);
+                ulrc++;
+            }
+        }
+    }
+
+    return (ulrc);
+}
+
+/*
+ *@@ sndOpenMmpmIni:
+ *      this opens \MMOS2\MMPM.INI on the
+ *      boot drive and returns the profile
+ *      handle (or NULLHANDLE upon errors).
+ *      Use PrfCloseProfile to close the
+ *      profile again.
+ *
+ *@@added V0.9.1 (99-12-19) [umoeller]
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: now using MMBASE environment variable
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: added proper HAB param
+ */
+
+HINI sndOpenMmpmIni(HAB hab)
+{
+    HAB     habDesktop = WinQueryAnchorBlock(HWND_DESKTOP);
+    CHAR    szMMPM[CCHMAXPATH];
+    HINI    hini = NULLHANDLE;
+
+    PSZ     pszMMPMPath = getenv("MMBASE"); // V0.9.6 (2000-10-16) [umoeller]
+    if (pszMMPMPath)
+    {
+        // variable set:
+        PSZ p;
+        sprintf(szMMPM, "%s\\MMPM.INI", pszMMPMPath);
+        // kill semicolon if present
+        p = strchr(szMMPM, ';');
+        if (p)
+           *p = 0;
+    }
+    else
+        // variable not set (shouldn't happen): try boot drive
+        sprintf(szMMPM, "%c:\\MMOS2\\MMPM.INI", doshQueryBootDrive());
+    hini = PrfOpenProfile(habDesktop, szMMPM);
+    return (hini);
+}
+
+/*
+ *@@ sndQuerySystemSound:
+ *      this gets a system sound from the MMPM.INI file.
+ *      usIndex must be the sound to query. The following
+ *      MMSOUND_* IDs are declared in syssound.h:
+ *
+ *      Default system sounds (syssound.h):
+ *      --  MMSOUND_WARNING         0
+ *      --  MMSOUND_INFORMATION     1
+ *      --  MMSOUND_ERROR           2
+ *      --  MMSOUND_ANIMATEOPEN     3
+ *      --  MMSOUND_ANIMATECLOSE    4
+ *      --  MMSOUND_DRAG            5
+ *      --  MMSOUND_DROP            6
+ *      --  MMSOUND_SYSTEMSTARTUP   7
+ *      --  MMSOUND_SHUTDOWN        8
+ *      --  MMSOUND_SHREDDER        9
+ *      --  MMSOUND_LOCKUP          10
+ *      --  MMSOUND_ALARMCLOCK      11
+ *      --  MMSOUND_PRINTERROR      12
+ *
+ *      BTW, these values match those of the WinAlarm call
+ *      (WA_* values), but only the first three are documented
+ *      in PMREF and pmwin.h (WA_WARNING, WA_NOTE, WA_ERROR).
+ *
+ *      New XWorkplace system sounds:
+ *      --  MMSOUND_XFLD_SHUTDOWN   555
+ *      --  MMSOUND_XFLD_RESTARTWPS 556
+ *      --  MMSOUND_XFLD_CTXTOPEN   558
+ *      --  MMSOUND_XFLD_CTXTSELECT 559
+ *      --  MMSOUND_XFLD_CNRDBLCLK  560
+ *
+ *      The string buffers are recommended to be at least
+ *      CCHMAXPATH in size.
+ *
+ *@@changed V0.9.0 [umoeller]: this used to be cmnQuerySystemSound
+ *@@changed V0.9.0 [umoeller]: exported stuff to sndParseSoundData
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: added proper HAB param
+ */
+
+BOOL sndQuerySystemSound(HAB hab,           // in: caller's anchor block
+                         USHORT usIndex,    // in: sound index to query
+                         PSZ pszDescr,      // out: sound description, as displayed
+                                // in the "Sound" object (ptr may be NULL)
+                         PSZ pszFile,       // out: sound file to (ptr may be NULL)
+                         PULONG pulVolume)  // out: sound volume (0-100).
+                                // If the "Global volume" flag is
+                                // set in MMPM.INI, this will return the global
+                                // volume instead. Ptr may be NULL also.
+{
+    BOOL    rc = FALSE;
+    HINI    hiniMMPM = sndOpenMmpmIni(hab);
+    if (hiniMMPM)
+    {
+        CHAR szData[1000];
+        CHAR szData2[100];
+        CHAR szKey[10];
+        sprintf(szKey, "%d", usIndex);
+        PrfQueryProfileString(hiniMMPM,
+                              MMINIKEY_SOUNDSETTINGS, "EnableSounds",
+                              "TRUE",    // default string
+                              szData2, sizeof(szData2));
+        #ifdef DEBUG_SOUNDS
+            _Pmpf(("  sounds enabled: %s", szData2));
+        #endif
+        if (strcmp(szData2, "TRUE") == 0)
+            // sounds enabled at all?
+            if (PrfQueryProfileString(hiniMMPM,
+                                      MMINIKEY_SYSSOUNDS, szKey,
+                                      ".",
+                                      szData, sizeof(szData)-1) > 3)
+            {
+                sndParseSoundData(szData,
+                                  pszDescr,
+                                  pszFile,
+                                  pulVolume);
+
+                // if "global volume" has been enabled,
+                // we do not return the value specified
+                // here, but the global value
+                PrfQueryProfileString(hiniMMPM,
+                                      MMINIKEY_SOUNDSETTINGS, "ApplyVolumeToAll",
+                                      "FALSE",
+                                      szData2, sizeof(szData2));
+                if (strcmp(szData2, "FALSE") != 0)
+                {
+                    // global volume setting for all sounds
+                    PrfQueryProfileString(hiniMMPM,
+                                          MMINIKEY_SOUNDSETTINGS, "Volume",
+                                          "100",
+                                          szData2, sizeof(szData2));
+                    sscanf(szData2, "%d", pulVolume);
+                }
+
+                rc = TRUE;
+            }
+
+        PrfCloseProfile(hiniMMPM);
+    }
+
+    return (rc);
+}
+
+/*
+ *@@ sndWriteSoundData:
+ *      this sets a system sound in MMPM.INI.
+ *      Gets called by sndSetSystemSound. As opposed
+ *      to that function, this needs the profile handle
+ *      of MMPM.INI.
+ *
+ *      If (pszDescr == NULL), that sound entry is removed.
+ *
+ *      Returns the return value of PrfWriteProfileString.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (99-12-30) [umoeller]: added delete support
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: added MMPM/2 notify
+ */
+
+BOOL sndWriteSoundData(HINI hiniMMPM,       // in: MMPM.INI handle (from sndOpenMmpmIni)
+                       USHORT usIndex,      // in: sound index
+                       PSZ pszDescr,        // in: sound name or NULL for removal
+                       PSZ pszFile,         // in: sound file
+                       ULONG ulVolume)      // in: sound volume
+{
+    BOOL    brc = FALSE;
+    CHAR    szKey[10];
+
+    sprintf(szKey, "%d", usIndex);
+    if (pszDescr)
+    {
+        CHAR szData[1000];
+        // format: soundfile#description#volume
+        sprintf(szData, "%s#%s#%d", pszFile, pszDescr, ulVolume);
+        brc = PrfWriteProfileString(hiniMMPM,
+                                    MMINIKEY_SYSSOUNDS, szKey,
+                                    szData);
+    }
+    else
+        // pszDescr == NULL:
+        // delete entry
+        brc = PrfWriteProfileString(hiniMMPM,
+                                    MMINIKEY_SYSSOUNDS, szKey,
+                                    NULL);
+
+    if (brc)
+        // success:
+        if (usIndex < 100)
+            // one of the default OS/2 sounds has changed:
+            // we then need to notify MMPM/2...
+            // this is done by calling WinAlarm with 1000+index!
+            // sick stuff..
+            WinAlarm(HWND_DESKTOP, usIndex+1000); // V0.9.6 (2000-10-16) [umoeller]
+
+    return (brc);
+}
+
+/*
+ *@@ sndSetSystemSound:
+ *      this sets a system sound in MMPM.INI by
+ *      calling sndWriteSoundData.
+ *      Returns FALSE if an error occured.
+ *
+ *      See sndQuerySystemSound for the parameters.
+ *
+ *@@changed V0.9.0 [umoeller]: this used to be cmnSetSystemSound
+ *@@changed V0.9.0 [umoeller]: exported stuff to sndWriteSoundData
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: added proper HAB param
+ */
+
+BOOL sndSetSystemSound(HAB hab,
+                       USHORT usIndex,
+                       PSZ pszDescr,
+                       PSZ pszFile,
+                       ULONG ulVolume)
+{
+    BOOL    brc = FALSE;
+    HINI    hiniMMPM = sndOpenMmpmIni(hab);
+    if (hiniMMPM)
+    {
+        brc = sndWriteSoundData(hiniMMPM, usIndex, pszDescr, pszFile, ulVolume);
+        PrfCloseProfile(hiniMMPM);
+    }
+    return (brc);
+}
+
+/*
+ *@@ sndDoesSchemeExist:
+ *      returns TRUE if pszScheme already exists
+ *      in OS2SYS.INI.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL sndDoesSchemeExist(PSZ pszScheme)
+{
+    // check in OS2SYS.INI's scheme list whether that
+    // scheme exists already
+    PSZ pszExisting = prfhQueryProfileData(HINI_SYSTEM,
+                                           MMINIKEY_SOUNDSCHEMES,  // "PM_SOUND_SCHEMES_LIST"
+                                           pszScheme,
+                                           NULL);
+    if (pszExisting)
+    {
+        free(pszExisting);
+        return (TRUE);
+    }
+
+    return (FALSE);
+}
+
+/*
+ *@@ sndCreateSoundScheme:
+ *      this creates a new sound scheme and copies
+ *      the current data in MMPM.INI into it.
+ *      No check is made for whether that sound
+ *      scheme exists already (use sndDoesSchemeExist
+ *      for that). Data is overwritten without further
+ *      discussion.
+ *
+ *      Returns:
+ *      --  NO_ERROR
+ *      --  ERROR_INVALID_HANDLE: hiniMMPM invalid
+ *      --  ERROR_NO_DATA: scheme not found.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET sndCreateSoundScheme(HINI hiniMMPM,      // in: MMPM.INI handle (from sndOpenMmpmIni)
+                            PSZ pszNewScheme)   // in: name of new scheme
+{
+    APIRET  arc = NO_ERROR;
+    CHAR    szNewAppName[200] = "PM_SOUNDS_";
+
+    // create a unique new application name for
+    // this scheme in OS2SYS.INI (this is how
+    // Warp 4 apparently does it)
+    strcat(szNewAppName, pszNewScheme);  // PM_SOUNDS_blahblah
+    strupr(szNewAppName);               // PM_SOUNDS_BLAHBLAH
+
+    if (hiniMMPM)
+    {
+        // get applications list for sounds list in MMPM.INI
+        PSZ pszKeysList = prfhQueryKeysForApp(hiniMMPM,
+                                              MMINIKEY_SYSSOUNDS); // "MMPM2_AlarmSounds"
+        if (pszKeysList)
+        {
+            PSZ     pKey2 = pszKeysList;
+
+            CHAR    szFile[CCHMAXPATH+50];
+            ULONG   ulVolume,
+                    ulKeyLen;
+
+            while (*pKey2 != 0)
+            {
+                // now copy this key to the new sound scheme;
+                // however, we can't just copy the whole key,
+                // but need to extract the file name and
+                // volume first.
+                // Warp 4 normally _only_ stores the file name
+                // in the sound schemes. Since we want the
+                // volume also, we add a null char after the
+                // file name and append the volume...
+
+                PSZ pSoundData = prfhQueryProfileData(hiniMMPM,
+                                                      MMINIKEY_SYSSOUNDS, // "MMPM2_AlarmSounds"
+                                                      pKey2,
+                                                      NULL);
+                if (pSoundData)
+                {
+                    sndParseSoundData(pSoundData,
+                                      NULL,     // we don't need the description
+                                      szFile,
+                                      &ulVolume);
+                    ulKeyLen = strlen(szFile)+1;    // go beyond null byte
+                    ulKeyLen += sprintf(szFile+ulKeyLen,
+                                "%d",
+                                ulVolume) + 1;
+                    // and write to OS2SYS.INI
+                    PrfWriteProfileData(HINI_SYSTEM,
+                                        szNewAppName,
+                                        pKey2,
+                                        szFile,
+                                        ulKeyLen);
+
+                    free(pSoundData);
+                } // end if (pSoundData)
+
+                pKey2 += strlen(pKey2)+1;
+            } // end while (*pKey2 != 0)
+
+            free (pszKeysList);
+
+            // finally, store new scheme in schemes list
+            PrfWriteProfileString(HINI_SYSTEM,
+                                  MMINIKEY_SOUNDSCHEMES,  // "PM_SOUND_SCHEMES_LIST"
+                                  pszNewScheme,      // key is scheme name
+                                  szNewAppName);    // data is new OS2SYS.INI application
+        }
+        else
+            arc = ERROR_NO_DATA;
+    }
+    else
+        arc = ERROR_INVALID_HANDLE;
+
+    return (arc);
+}
+
+/*
+ *@@ sndLoadSoundScheme:
+ *      this loads the data in pszScheme (OS2SYS.INI)
+ *      into MMPM.INI. Existing sound data
+ *      in that profile will be overwritten.
+ *
+ *      Note: Only those sounds in MMPM.INI will be
+ *      overwritten for which a corresponding entry
+ *      in the sound scheme exists. If it doesn't,
+ *      the data in MMPM.INI for _that_ sound only
+ *      will _not_ be changed. In other words, existing
+ *      sound data will be merged with the scheme data.
+ *
+ *      If you don't like this, delete the whole
+ *      "MMPM2_AlarmSounds" application in MMPM.INI
+ *      before calling this function.
+ *
+ *      Returns:
+ *      --  NO_ERROR
+ *      --  ERROR_INVALID_HANDLE: hiniMMPM invalid
+ *      --  ERROR_NO_DATA: scheme not found.
+ *      --  ERROR_BAD_FORMAT: error in MMPM.INI data.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (99-12-20) [umoeller]: fixed memory leak
+ *@@changed V0.9.6 (2000-10-16) [umoeller]: added MMPM/2 notify
+ */
+
+APIRET sndLoadSoundScheme(HINI hiniMMPM,      // in: HINI of ?:\MMOS2\MMPM.INI (PrfOpenProfile)
+                          PSZ pszScheme)      // in: scheme name
+{
+    APIRET arc = NO_ERROR;
+
+    _Pmpf(("Entering sndLoadSoundScheme"));
+
+    if (hiniMMPM)
+    {
+        // check in OS2SYS.INI's scheme list whether that
+        // scheme exists already
+        PSZ pszSchemeAppName = prfhQueryProfileData(HINI_SYSTEM,
+                                    MMINIKEY_SOUNDSCHEMES,  // "PM_SOUND_SCHEMES_LIST"
+                                    pszScheme,
+                                    NULL);
+        _Pmpf(("    pszSchemeAppName: %s", pszSchemeAppName));
+
+        if (pszSchemeAppName)
+        {
+            // now copy keys from OS2SYS.INI to MMPM.INI;
+            // since OS2SYS.INI _only_ has the file name
+            // (and _maybe_ the individual volume, if it
+            // was us who created the sound scheme --
+            // see sndCreateSoundScheme above), we need
+            // to go thru the MMPM.INI (!) sounds lists
+            // and merge the data in there with the
+            // corresponding sound data for the current
+            // scheme...
+
+            // get applications list for sounds list in MMPM.INI
+            PSZ pszMMPMKeysList = prfhQueryKeysForApp(hiniMMPM,
+                                                  MMINIKEY_SYSSOUNDS); // "MMPM2_AlarmSounds"
+            if (pszMMPMKeysList)
+            {
+                PSZ     pMMPMKey2 = pszMMPMKeysList,
+                        pMMPMSoundData,
+                        pSchemeSoundData;
+
+                CHAR    szDescription[300];         // from MMPM.INI
+                CHAR    szFile[CCHMAXPATH+50];      // from OS2SYS.INI
+                ULONG   ulVolume;                   // from MMPM.INI _or_ OS2SYS.INI
+                CHAR    szData[1000];
+
+                // go thru keys (numbers)
+                while (*pMMPMKey2 != 0)
+                {
+                    ULONG   cbSchemeSoundData = 0,
+                            cbSchemeSoundFile = 0;
+
+                    pMMPMSoundData = prfhQueryProfileData(hiniMMPM,
+                                                          MMINIKEY_SYSSOUNDS, // "MMPM2_AlarmSounds"
+                                                          pMMPMKey2,
+                                                          NULL);
+                    pSchemeSoundData = prfhQueryProfileData(HINI_SYSTEM,
+                                                            pszSchemeAppName,
+                                                            pMMPMKey2,
+                                                            &cbSchemeSoundData);
+
+                    if ((pMMPMSoundData) && (pSchemeSoundData))
+                    {
+                        sndParseSoundData(pMMPMSoundData,
+                                          szDescription,
+                                          NULL,                // we don't need the file
+                                          &ulVolume);
+                        // now overwrite this data with scheme data
+                        strcpy(szFile,
+                               pSchemeSoundData);       // up to first null byte
+                        cbSchemeSoundFile = strlen(pSchemeSoundData)+1;
+                        if (cbSchemeSoundData > cbSchemeSoundFile)
+                            // this means we have an additional volume string
+                            // after the null byte (sndCreateSoundScheme);
+                            // copy it
+                            sscanf(pSchemeSoundData + cbSchemeSoundFile,
+                                   "%d",
+                                   &ulVolume);
+
+                        // and write data to MMPM.INI
+                        // format: soundfile#description#volume
+                        sprintf(szData, "%s#%s#%d", szFile, szDescription, ulVolume);
+                        if (PrfWriteProfileString(hiniMMPM,
+                                                  MMINIKEY_SYSSOUNDS, // "MMPM2_AlarmSounds"
+                                                  pMMPMKey2,    // key (decimal number)
+                                                  szData))
+                        {
+                            // success:
+                            USHORT usIndex = atoi(pMMPMKey2);
+                            if (usIndex < 100)
+                                // this was one of the default OS/2 sounds:
+                                // notify MMPM/2 of the change... see sndWriteSoundData
+                                // V0.9.6 (2000-10-16) [umoeller]
+                                WinAlarm(HWND_DESKTOP, usIndex+1000);
+                        }
+                    }
+
+                    if (pMMPMSoundData)
+                        free(pMMPMSoundData);
+                    if (pSchemeSoundData)
+                        free(pSchemeSoundData);
+
+                    pMMPMKey2 += strlen(pMMPMKey2)+1;
+                } // end while (*pMMPMKey2 != 0)
+
+                free (pszMMPMKeysList);
+            }
+            else
+                arc = ERROR_BAD_FORMAT;
+
+            free(pszSchemeAppName);
+        } // end if (pszSchemeAppName)
+        else
+            arc = ERROR_NO_DATA;
+    }
+    else
+        arc = ERROR_INVALID_HANDLE;
+
+    _Pmpf(("End of sndLoadSoundScheme, arc: %d", arc));
+    return (arc);
+}
+
+/*
+ *@@ sndDestroySoundScheme:
+ *      destroys pszScheme in OS2SYS.INI;
+ *      returns TRUE is pszScheme was found
+ *      and deleted.
+ *
+ *      See xsound.c for explanations.
+ *
+ *      Returns:
+ *      --  NO_ERROR
+ *      --  ERROR_NO_DATA: scheme not found.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+APIRET sndDestroySoundScheme(PSZ pszScheme)
+{
+    APIRET arc = NO_ERROR;
+
+    // check in OS2SYS.INI's scheme list whether that
+    // scheme exists already
+    PSZ pszExisting = prfhQueryProfileData(HINI_SYSTEM,
+                                           MMINIKEY_SOUNDSCHEMES,  // "PM_SOUND_SCHEMES_LIST"
+                                           pszScheme,
+                                           NULL);
+    if (pszExisting)
+    {
+        // delete whole existing PM_SOUNDS_BLAHBLAH application
+        PrfWriteProfileString(HINI_SYSTEM,
+                              pszExisting,      // application
+                              NULL,
+                              NULL);
+        // and delete entry in sound schemes list
+        PrfWriteProfileString(HINI_SYSTEM,
+                              MMINIKEY_SOUNDSCHEMES,  // "PM_SOUND_SCHEMES_LIST"
+                              pszScheme,
+                              NULL);
+        free(pszExisting);
+    }
+    else
+        arc = ERROR_NO_DATA;
+
+    return (arc);
+}
+
+
Index: /trunk/src/helpers/textv_html.c
===================================================================
--- /trunk/src/helpers/textv_html.c	(revision 8)
+++ /trunk/src/helpers/textv_html.c	(revision 8)
@@ -0,0 +1,2195 @@
+
+/*
+ *@@sourcefile textv_html.c:
+ *      this code converts HTML code to escape sequences for the
+ *      XTextView control (textview.c).
+ *
+ *      This code is in part ugly spaghetti, but this is intentional to
+ *      make this HTML parser FAST. In general, you get about double or
+ *      triple the speed compared to Netscape 4.6 on OS/2. This code
+ *      doesn't understand all of HTML though, but you get most of HTML 2.
+ *      There's no tables or frames at this point.
+ *
+ *      The entry point into this mess is txvConvertFromHTML, which
+ *      is easy to use.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\textv_html.h"
+ *
+ *@@added V0.9.3 (2000-05-10) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This program is part of the XWorkplace package.
+ *      This program is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published by
+ *      the Free Software Foundation, in version 2 as it comes in the COPYING
+ *      file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\linklist.h"
+#include "helpers\stringh.h"
+#include "helpers\textview.h"
+
+#include "helpers\textv_html.h"
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\XTextView control\HTML conversion
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Declarations                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ LISTDESC:
+ *      structure stored in COPYTARGET to
+ *      hold list information (UL, OL, ... tags).
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+typedef struct _LISTDESC
+{
+    ULONG   ulListType;         // 0: unordered (UL)
+                                // 1: ordered (OL)
+                                // 2: definition lists (DL)
+    ULONG   ulItem;             // list enumeration; 1 on first item,
+                                // 2 on next, ...
+} LISTDESC, *PLISTDESC;
+
+/*
+ *@@ COPYTARGET:
+ *      monster structure which holds the current
+ *      status of the HTML converter while conversion
+ *      is taking place. This stores input/output pointers
+ *      and various flags to avoid duplicate line breaks
+ *      and such.
+ *
+ *      One instance of this is created in txvConvertFromHTML
+ *      on the stack and then passed to all the sub-function
+ *      calls.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+typedef struct _COPYTARGET
+{
+    PSZ     pSource;        // ptr into source string;
+                            // valid ONLY while we're in a tag handler
+    PSZ     pNewSource;     // can be set by tag handler to skip characters;
+                            // this is set to NULL before calling a tag
+                            // handler; if this is still NULL, default
+                            // processing occurs
+
+    // new string:
+    PSZ     pszNew;         // memory buffer
+    ULONG   cbNew;          // size of buffer (reallocated)
+    PSZ     pTarget;        // current char ptr into pszNew
+
+    // saved character while tag handler is being called
+    CHAR    cSaved;
+
+    PXHTMLDATA pxhtml;      // ptr to XHTMLDATA passed to txvConvertFromHTML
+
+    // formatting flags while going through the text
+    BOOL    fSkipNextSpace;
+                // if TRUE, subsequent spaces are skipped
+    BOOL    fNeedsLinebreak;
+                // if TRUE, \n is inserted before any other character
+    BOOL    fSkipNextLinebreak;
+                // if TRUE, subsequent linebreaks are skipped
+    BOOL    fPRE;
+                // are we currently in a PRE tag?
+    BOOL    fInLink;
+                // are we currently in a A HREF= tag?
+
+    // arguments (attributes) for tag handlers
+    PSZ     pszAttributes;  // != NULL while a tag handler is being called
+                            // and attributes exist for the tag
+
+    // anchors count
+    USHORT  usAnchorIndex;  // start with 1
+
+    // list maintenance
+    ULONG   ulListLevel;    // if > 0, we're in a UL or OL block;
+                            // raised for each block
+    ULONG   ulUnorderedListLevel; // raised with each UL block to keep track
+                                // of bullets
+    ULONG   ulOrderedListLevel; // raised with each UL block to keep track
+                                // of 1), 2), a), b)... numbering
+    ULONG   ulCurrentListType;  // current list type (from highest LISTDESC)
+    BOOL    fInDT;          // TRUE if we're currently in a DT tag
+    LINKLIST llLists;       // stack of LISTDESC items
+} COPYTARGET, *PCOPYTARGET;
+
+typedef VOID FNPROCESSTAG(PCOPYTARGET pct);
+typedef FNPROCESSTAG *PFNPROCESSTAG;
+
+/* ******************************************************************
+ *                                                                  *
+ *   Global variables                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/* ******************************************************************
+ *                                                                  *
+ *   Append-char helpers                                            *
+ *                                                                  *
+ ********************************************************************/
+
+#define COPYTARGETALLOC     100000
+
+/*
+ *@@ AppendChar:
+ *      helper for txvConvertFromHTML to
+ *      append a char to the target string
+ *      in COPYTARGET.
+ *      This performs a few additional checks
+ *      and manages memory.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+VOID AppendChar(PCOPYTARGET pct,  // in/out: formatting buffer
+                CHAR c)
+{
+    // calculate ofs where to store next char
+    ULONG   cbOfsNext = pct->pTarget - pct->pszNew;
+    if (cbOfsNext >= pct->cbNew)      // have we reached the buffer size yet?
+    {
+        // more mem needed:
+        pct->cbNew += COPYTARGETALLOC;
+        pct->pszNew = (PSZ)realloc(pct->pszNew, pct->cbNew);
+                    // if first call, pszNew is NULL, and realloc
+                    // behaves just like malloc
+        // adjust target, because ptr might have changed
+        pct->pTarget = pct->pszNew + cbOfsNext;
+    }
+
+    // append character
+    *pct->pTarget++ = c;
+}
+
+/*
+ *@@ AppendString:
+ *      appends the characters in *ach,
+ *      which must be null-terminated.
+ *      Does NOT append a null character though.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+VOID AppendString(PCOPYTARGET pct,  // in/out: formatting buffer
+                  char *ach)
+{
+    ULONG cbAppend = strlen(ach);
+    ULONG ul;
+    PSZ pSource;
+
+    // calculate ofs where to store next char
+    ULONG   cbOfsNext = pct->pTarget - pct->pszNew;
+    while (cbOfsNext + cbAppend >= pct->cbNew)
+    {
+        // more mem needed:
+        pct->cbNew += COPYTARGETALLOC;
+        pct->pszNew = (PSZ)realloc(pct->pszNew, pct->cbNew);
+                    // if first call, pszNew is NULL, and realloc
+                    // behaves just like malloc
+        // adjust target, because ptr might have changed
+        pct->pTarget = pct->pszNew + cbOfsNext;
+    }
+
+    // append characters
+    pSource = ach;
+    for (ul = 0;
+         ul < cbAppend;
+         ul++)
+        *pct->pTarget++ = *pSource++;
+}
+
+/*
+ *@@ AppendLinebreakCheck:
+ *      checks if a linebreak is needed and
+ *      inserts one if so.
+ *
+ *@@added V0.9.3 (2000-05-17) [umoeller]
+ */
+
+VOID AppendLinebreakCheck(PCOPYTARGET pct)
+{
+    if ((!pct->fPRE) && (pct->fNeedsLinebreak))
+    {
+        // yes: insert linebreak; this resets pct->fNeedsLinebreak
+        if (!pct->fSkipNextLinebreak)
+        {
+            AppendChar(pct, '\n');
+
+            if ((pct->ulListLevel) && (!pct->fInDT))
+                // if we're in a list, add a tab also,
+                // because we'll have a negative first-line margin
+                AppendString(pct, TXVESC_TAB);
+        }
+        pct->fNeedsLinebreak = FALSE;
+    }
+}
+
+/*
+ *@@ AppendEscapeWithDecimal:
+ *      appends the specified escape code
+ *      with a three-digit decimal parameter.
+ *      Calls AppendString in turn.
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+VOID AppendEscapeWith3Decimals(PCOPYTARGET pct,  // in/out: formatting buffer
+                               char *ach,
+                               USHORT us)
+{
+    CHAR szDecimal[10];
+    if (us > 999)
+        us = 999;
+    sprintf(szDecimal, "%03d", us);
+    // append escape
+    AppendString(pct, ach);
+    AppendString(pct, szDecimal);
+}
+
+/*
+ *@@ AppendEscapeWith4Decimals:
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+VOID AppendEscapeWith4Decimals(PCOPYTARGET pct,  // in/out: formatting buffer
+                               char *ach,
+                               USHORT us)
+{
+    CHAR szDecimal[10];
+    if (us > 9999)
+        us = 9999;
+    sprintf(szDecimal, "%04d", us);
+    // append escape
+    AppendString(pct, ach);
+    AppendString(pct, szDecimal);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Tag converter functions                                        *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ StartList:
+ *      starts a list (UL or OL).
+ *      This uses a linked list in COPYTARGET
+ *      to keep a pseudo-stack for nested lists.
+ *
+ *@@added V0.9.3 (2000-05-08) [umoeller]
+ */
+
+VOID StartList(PCOPYTARGET pct,     // in/out: formatting buffer
+               ULONG ulListType)    // list type:
+                                    // 0: unordered (UL)
+                                    // 1: ordered (OL)
+                                    // 2: definition lists (DL)
+{
+    PLISTDESC pListDesc;
+
+    // raise list level
+    pct->ulListLevel++;
+
+    if (ulListType == 0)
+        // unordered:
+        pct->ulUnorderedListLevel++;
+    else if (ulListType == 1)
+        // ordered:
+        pct->ulOrderedListLevel++;
+
+    // create LISTDESC and store it on stack
+    pListDesc = (PLISTDESC)malloc(sizeof(LISTDESC));
+    pListDesc->ulListType
+        = pct->ulCurrentListType
+        = ulListType;
+    pListDesc->ulItem = 1;
+
+    lstAppendItem(&pct->llLists,
+                  pListDesc);
+
+    AppendEscapeWith4Decimals(pct,
+                              TXVESC_LEFTMARGIN,
+                              pct->ulListLevel * 5);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_FIRSTLINEMARGIN_LEFT,
+                              (ulListType == 2)
+                                ? 5     // for definition lists
+                                : 3);       // negative!
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+/*
+ *@@ StopList:
+ *      stops a list (UL or OL).
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+VOID StopList(PCOPYTARGET pct)
+{
+    if (pct->ulListLevel)
+    {
+        PLISTNODE pNode;
+
+        // lower list level
+        pct->ulListLevel--;
+        AppendEscapeWith4Decimals(pct,
+                                  TXVESC_LEFTMARGIN,
+                                  pct->ulListLevel * 5);
+        AppendEscapeWith3Decimals(pct,
+                                  TXVESC_FIRSTLINEMARGIN_LEFT,
+                                  (pct->ulListLevel)
+                                        ? 3 // we still have a list level (nested)
+                                        : 0);
+        pct->fNeedsLinebreak = TRUE;
+
+        // remove the LISTDESC from the stack
+        pNode = lstNodeFromIndex(&pct->llLists,
+                                 pct->ulListLevel); // this has been lowered already
+        if (pNode)
+        {
+            PLISTDESC pListDesc = (PLISTDESC)pNode->pItemData;
+            if (pListDesc->ulListType == 0)
+                // was unordered:
+                pct->ulUnorderedListLevel--;
+            else if (pListDesc->ulListType == 1)
+                // was ordered:
+                pct->ulOrderedListLevel--;
+
+            lstRemoveNode(&pct->llLists, pNode);
+
+            // update COPYTARGET with previous list level
+            if (pct->ulListLevel)
+            {
+                // we're still in a list (nested lists):
+                PLISTDESC pListDesc2 = (PLISTDESC)lstItemFromIndex(&pct->llLists,
+                                                         pct->ulListLevel - 1);
+                if (pListDesc2)
+                    pct->ulCurrentListType = pListDesc2->ulListType;
+            }
+        }
+    }
+    // else: buggy HTML code, ignore
+}
+
+/*
+ *@@ TagTITLE:
+ *
+ *@@added V0.9.3 (2000-05-19) [umoeller]
+ */
+
+VOID TagTITLE(PCOPYTARGET pct)
+{
+    // pSource currently points to <TITLE tag
+    PSZ pSource = pct->pSource + strlen(pct->pSource);
+        // points to temporary null byte in main buffer now
+    *pSource = pct->cSaved;
+
+    pSource = strchr(pct->pSource, '>');
+    if (pSource)
+    {
+        PSZ pNextOpen = strchr(pSource, '<');
+        if (pNextOpen)
+        {
+            // extract title
+            pct->pxhtml->pszTitle = strhSubstr(pSource + 1, pNextOpen);
+
+            if (strnicmp(pNextOpen + 1, "/TITLE", 6) == 0)
+            {
+                // closing /TITLE tag found:
+                // search on after that
+                pct->pNewSource = strchr(pNextOpen, '>');
+                if (pct->pNewSource)
+                    pct->pNewSource++;
+            }
+        }
+    }
+}
+
+/*
+ *@@ TagP:
+ *
+ */
+
+VOID TagP(PCOPYTARGET pct)
+{
+    // append newline:
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+
+    /* if (pct->ulListLevel)
+    {
+        // if we are currently in a list, we must also
+        // add a tab escape, because we have set
+        // the first line margin to the left of the
+        // left margin
+        AppendString(pct,
+                     TXVESC_TAB);
+    } */
+}
+
+VOID TagBR(PCOPYTARGET pct)
+{
+    AppendChar(pct,
+               '\r');
+
+    if (pct->ulListLevel)
+    {
+        // if we are currently in a list, we must also
+        // add a tab escape, because we have set
+        // the first line margin to the left of the
+        // left margin
+        AppendString(pct,
+                     TXVESC_TAB);
+    }
+    if (!pct->fPRE)
+        pct->fSkipNextSpace = TRUE;
+}
+
+VOID TagPRE(PCOPYTARGET pct)
+{
+    // start of PRE tag:
+    // add \n before any other character
+    // pct->fNeedsLinebreak = TRUE;
+    AppendChar(pct, '\n');
+    pct->fNeedsLinebreak = FALSE;
+    /* AppendString(pct,
+                 TXVESC_PRE_BEGIN); */
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_SET_FONT,
+                              1);   // monospaced font
+    AppendEscapeWith4Decimals(pct,
+                              TXVESC_SPACEBEFORE,
+                              0);   // no spacing before
+    AppendEscapeWith4Decimals(pct,
+                              TXVESC_SPACEAFTER,
+                              0);   // no spacing after
+    // disable word-wrapping
+    AppendString(pct,
+                 TXVESC_WORDWRAP "0");
+    pct->fPRE = TRUE;
+    pct->fSkipNextSpace = FALSE;
+}
+
+VOID TagXPRE(PCOPYTARGET pct)
+{
+    pct->fPRE = FALSE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_SET_FONT,
+                              0);   // standard font
+    AppendString(pct, TXVESC_SPACEBEFORE);
+    AppendString(pct, "####");  // reset to default
+    AppendString(pct, TXVESC_SPACEAFTER);
+    AppendString(pct, "####");  // reset to default
+    // re-enable word-wrapping
+    AppendString(pct,
+                 TXVESC_WORDWRAP "1"
+                 "\n");         // force line break
+    pct->fNeedsLinebreak = FALSE;
+    // refuse to add \n even if we have another "p" coming up
+    pct->fSkipNextLinebreak = TRUE;
+    pct->fSkipNextSpace = TRUE;
+}
+
+VOID TagH1(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              200);   // double size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH1(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // regular size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagH2(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              175);   // size in percent of regular point size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH2(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // regular size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagH3(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              150);   // size in percent of regular point size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH3(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // size in percent of regular point size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagH4(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              125);   // size in percent of regular point size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH4(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // regular size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagH5(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // size in percent of regular point size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH5(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // regular size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagH6(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              80 );   // size in percent of regular point size
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXH6(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_POINTSIZE_REL,
+                              100);   // regular size
+    // add \n before any other character
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagUL(PCOPYTARGET pct)
+{
+    StartList(pct,
+              0);       // unordered
+}
+
+VOID TagXUL(PCOPYTARGET pct)
+{
+    StopList(pct);
+}
+
+VOID TagOL(PCOPYTARGET pct)
+{
+    StartList(pct,
+              1);       // ordered
+}
+
+VOID TagXOL(PCOPYTARGET pct)
+{
+    StopList(pct);
+}
+
+VOID TagLI(PCOPYTARGET pct)
+{
+    PLISTDESC pListDesc;
+    CHAR     szMarker[20] = TXVESC_MARKER "\x01";
+
+    if (pct->ulListLevel)
+    {
+        // we're in a list:
+        pListDesc = (PLISTDESC)lstItemFromIndex(&pct->llLists,
+                                                pct->ulListLevel - 1);
+        if (pListDesc)
+            if (pListDesc->ulListType == 1)
+                // is ordered list:
+                sprintf(szMarker, "%d.", (pListDesc->ulItem)++);
+            else if (pListDesc->ulListType == 0)
+                // is unordered list:
+                // set bullet type according to unordered nesting
+                szMarker[2] = pct->ulUnorderedListLevel;
+    }
+
+    // add \n before any other character
+    // pct->fNeedsLinebreak = TRUE;
+    // if (pct->fNeedsLinebreak)
+    {
+        AppendChar(pct, '\n');
+        pct->fNeedsLinebreak = FALSE;
+    }
+
+    AppendString(pct, szMarker);
+    AppendString(pct, TXVESC_TAB);
+}
+
+VOID TagDL(PCOPYTARGET pct)
+{
+    StartList(pct,
+              2);       // definition list
+}
+
+VOID TagXDL(PCOPYTARGET pct)
+{
+    StopList(pct);
+    pct->fInDT = FALSE;
+}
+
+VOID TagDT(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    pct->fInDT = TRUE;
+}
+
+VOID TagDD(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+    AppendString(pct, TXVESC_TAB);
+    if (!pct->fPRE)
+        pct->fSkipNextSpace = TRUE;
+    pct->fInDT = FALSE;
+}
+
+VOID TagTR(PCOPYTARGET pct)
+{
+    pct->fNeedsLinebreak = TRUE;
+}
+
+VOID TagB(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_BEGIN);
+}
+
+VOID TagXB(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_BOLD_END);
+}
+
+VOID TagI(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_ITALICS_BEGIN);
+}
+
+VOID TagXI(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_ITALICS_END);
+}
+
+VOID TagU(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_UNDERLINE_BEGIN);
+}
+
+VOID TagXU(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_UNDERLINE_END);
+}
+
+VOID TagSTRIKE(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_STRIKE_BEGIN);
+}
+
+VOID TagXSTRIKE(PCOPYTARGET pct)
+{
+    AppendString(pct,
+                 TXVESC_STRIKE_END);
+}
+
+VOID TagCODE(PCOPYTARGET pct)
+{
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_SET_FONT,
+                              1);   // monospaced font
+}
+
+VOID TagXCODE(PCOPYTARGET pct)
+{
+    AppendEscapeWith3Decimals(pct,
+                              TXVESC_SET_FONT,
+                              0);   // regular font
+}
+
+VOID TagA(PCOPYTARGET pct)
+{
+    CHAR    szAnchor[10];
+
+    pct->fInLink = FALSE;
+
+    if ((pct->pszAttributes) && (pct->pxhtml))     // points into main source buffer
+    {
+        // we have attributes:
+        PSZ pszClosingTag = strchr(pct->pszAttributes, '>');
+        if (pszClosingTag)
+        {
+            ULONG ulOfs = 0;
+
+            /*
+             * HREF attribute:
+             *
+             */
+
+            PSZ pHREF = strhGetTextAttr(pct->pszAttributes, "HREF", &ulOfs),
+                pNAME = 0;
+
+            // replace '>' with null char to mark end of search
+            *pszClosingTag = 0;
+
+            if (pHREF)
+            {
+                // OK, we got a link target:
+                // create a link item and append it to the output list
+                PXHTMLLINK pNewLink = (PXHTMLLINK)malloc(sizeof(XHTMLLINK));
+                memset(pNewLink, 0, sizeof(XHTMLLINK));
+
+                pct->fInLink = TRUE;
+
+                // this starts with anchor 1
+                pNewLink->usLinkIndex = ++pct->usAnchorIndex;
+                pNewLink->pszTargetFile = pHREF;
+                            // do not free
+                lstAppendItem(&pct->pxhtml->llLinks, pNewLink);
+            }
+
+            /*
+             * NAME attribute:
+             *
+             */
+
+            pNAME = strhGetTextAttr(pct->pszAttributes, "NAME", &ulOfs);
+            if (pNAME)
+            {
+                AppendString(pct,
+                             TXVESC_ANCHORNAME);
+                AppendString(pct,
+                             pNAME);
+                // must be terminated with 0xFF
+                AppendChar(pct, 0xFF);
+                free(pNAME);
+            }
+            // restore '>'
+            *pszClosingTag = '>';
+        }
+    }
+
+    if (pct->fInLink)
+    {
+        sprintf(szAnchor, "%04lX", pct->usAnchorIndex);
+        AppendString(pct,
+                     TXVESC_LINK);
+        AppendString(pct,
+                     szAnchor);
+    }
+}
+
+VOID TagXA(PCOPYTARGET pct)
+{
+    if (pct->fInLink)
+    {
+        AppendString(pct,
+                     TXVESC_LINK "####");
+        pct->fInLink = FALSE;
+    }
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Tag helpers                                                    *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ FindTagProcessor:
+ *      returns the Tag* function which handles the
+ *      given tag or NULL if there's none.
+ *
+ *@@added V0.9.4 (2000-06-10) [umoeller]
+ */
+
+PFNPROCESSTAG FindTagProcessor(PSZ pszTag)
+{
+    PFNPROCESSTAG pProcessor = NULL;
+
+    CHAR    c0,
+            c1;
+
+    BOOL fEndOfTag = FALSE;
+
+    PSZ pCheck = pszTag,
+        p2;
+    if (*pCheck == '/')
+    {
+        // end of tag:
+        fEndOfTag = TRUE;
+        pCheck++;
+    }
+
+    c0 = *pCheck;
+    c1 = *(pCheck + 1);
+
+    p2 = pCheck + 2;
+
+    switch (c0)
+    {
+        case 'A':
+        case 'a':
+            switch (c1)
+            {
+                case 0: // A
+                    if (!fEndOfTag)
+                        return TagA;
+                    else
+                        return TagXA;
+                case 'D': // ADDRESS
+                case 'd': // ADDRESS
+                    if (stricmp(p2, "DRESS") == 0)
+                        if (!fEndOfTag)
+                            return TagI;
+                        else
+                            return TagXI;
+            }
+        break;
+
+        case 'B':
+        case 'b':
+            switch (c1)
+            {
+                case 0:
+                    if (!fEndOfTag)
+                        return TagB;
+                    else
+                        return TagXB;
+
+                case 'R':   // BR
+                case 'r':   // BR
+                    if (*p2 == 0)
+                        if (!fEndOfTag)
+                            return TagBR;
+            }
+        break;
+
+        case 'C':
+        case 'c':
+            switch (c1)
+            {
+                case 'I': // CITE
+                case 'i': // CITE
+                    if (stricmp(p2, "TE") == 0)
+                    {
+                        if (!fEndOfTag)
+                            return TagI;
+                        else
+                            return TagXI;
+                    }
+                break;
+
+                case 'O':
+                case 'o':
+                    if (stricmp(p2, "DE") == 0)
+                        if (!fEndOfTag)
+                            return TagCODE;
+                        else
+                            return TagXCODE;
+                break;
+            }
+        break;
+
+        case 'D':
+        case 'd':
+            switch (c1)
+            {
+                case 'D': // DD
+                case 'd': // DD
+                    if ((*p2 == 0) && (!fEndOfTag))
+                        return (TagDD);
+                break;
+
+                case 'I': // DIR
+                case 'i': // DIR
+                    if (*p2 == 'R')
+                        if (*(pCheck + 3) == 0)
+                            if (!fEndOfTag)
+                                return TagUL;
+                            else
+                                return TagXUL;
+                break;
+
+                case 'L': // DL
+                case 'l': // DL
+                    if (*p2 == 0)
+                        if (!fEndOfTag)
+                            return TagDL;
+                        else
+                            return TagXDL;
+                break;
+
+                case 'T': // DT
+                case 't': // DT
+                    if ((*p2 == 0) && (!fEndOfTag))
+                        return TagDT;
+                break;
+            }
+        break;
+
+        case 'E':
+        case 'e':
+            if ( (c1 == 'M') || (c1 == 'm') )  // EM
+                if (*p2 == 0)
+                    if (!fEndOfTag)
+                        return TagI;
+                    else
+                        return TagXI;
+        break;
+
+        case 'H':
+        case 'h':
+            if (c1)
+                if (*p2 == 0)
+                    switch (c1)
+                    {
+                        case '1':
+                            if (!fEndOfTag)
+                                return TagH1;
+                            else
+                                return TagXH1;
+                        case '2':
+                            if (!fEndOfTag)
+                                return TagH2;
+                            else
+                                return TagXH2;
+                        case '3':
+                            if (!fEndOfTag)
+                                return TagH3;
+                            else
+                                return TagXH3;
+                        case '4':
+                            if (!fEndOfTag)
+                                return TagH4;
+                            else
+                                return TagXH4;
+                        case '5':
+                            if (!fEndOfTag)
+                                return TagH5;
+                            else
+                                return TagXH5;
+                        case '6':
+                            if (!fEndOfTag)
+                                return TagH6;
+                            else
+                                return TagXH6;
+                    }
+        break;
+
+        case 'I':
+        case 'i':
+            if (c1 == 0)
+                if (!fEndOfTag)
+                    return TagI;
+                else
+                    return TagXI;
+        break;
+
+        case 'L':
+        case 'l':
+            if ((c1 == 'I') || (c1 == 'i'))
+                if (*p2 == 0)
+                    return TagLI;
+        break;
+
+        case 'M':
+        case 'm':
+            if (stricmp(p2, "NU") == 0)
+                if (!fEndOfTag)
+                    return TagUL;
+                else
+                    return TagXUL;
+        break;
+
+        case 'O':
+        case 'o':
+            if ((c1 == 'L') || (c1 == 'l'))
+                if (*p2 == 0)
+                    if (!fEndOfTag)
+                        return TagOL;
+                    else
+                        return TagXOL;
+        break;
+
+        case 'P':
+        case 'p':
+            switch (c1)
+            {
+                case 0:
+                    if (!fEndOfTag)
+                        return TagP;
+                break;
+
+                case 'R': // PRE
+                case 'r': // PRE
+                    if ((*p2 == 'E') || (*p2 == 'e'))
+                        if (*(pCheck + 3) == 0)
+                            if (!fEndOfTag)
+                                return TagPRE;
+                            else
+                                return TagXPRE;
+                break;
+            }
+        break;
+
+        case 'S':
+        case 's':
+            switch (c1)
+            {
+                case 'T': // STRONG
+                case 't': // STRONG
+                    if (stricmp(p2, "RONG") == 0)
+                        if (!fEndOfTag)
+                            return TagB;
+                        else
+                            return TagXB;
+                    else if (stricmp(p2, "RIKE") == 0)
+                        if (!fEndOfTag)
+                            return TagSTRIKE;
+                        else
+                            return TagXSTRIKE;
+                break;
+
+                case 'A':
+                case 'a':
+                    if (stricmp(p2, "MP") == 0)
+                        if (!fEndOfTag)
+                            return TagCODE;
+                        else
+                            return TagXCODE;
+                break;
+            }
+        break;
+
+        case 'T':
+        case 't':
+            switch (c1)
+            {
+                case 'R':
+                case 'r':
+                    if (*p2 == 0)
+                        return TagTR;
+                break;
+
+                case 'I':
+                case 'i':
+                    if (stricmp(p2, "TLE") == 0)
+                        return TagTITLE;
+                break;
+
+                case 'T': // TT
+                case 't':
+                    if (*p2 == 0)
+                        if (!fEndOfTag)
+                            return TagCODE;
+                        else
+                            return TagXCODE;
+                break;
+            }
+        break;
+
+        case 'U':
+        case 'u':
+            switch (c1)
+            {
+                case 0:
+                    if (!fEndOfTag)
+                        return TagU;
+                    else
+                        return TagXU;
+
+                case 'L':
+                case 'l':
+                    if (*p2 == 0)
+                        if (!fEndOfTag)
+                            return TagUL;
+                        else
+                            return TagXUL;
+                break;
+            }
+        break;
+
+        case 'V':
+        case 'v':
+            if (stricmp(p2, "R") == 0)
+            {
+                if (!fEndOfTag)
+                    return TagI;
+                else
+                    return TagXI;
+            }
+        break;
+
+        case 'X':
+        case 'x':
+            if (stricmp(p2, "MP") == 0) // XMP
+            {
+                if (!fEndOfTag)
+                    return TagPRE;
+                else
+                    return TagXPRE;
+            }
+        break;
+    }
+
+    return (pProcessor);
+}
+
+/*
+ *@@ HandleTag:
+ *      called by txvConvertFromHTML when a "<" character
+ *      is found in the source buffer. This calls
+ *      FindTagProcessor in turn to find the Tag*
+ *      function which handles the tag.
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+VOID HandleTag(PCOPYTARGET pct)
+{
+    PSZ     pStartOfTag = pct->pSource;
+    // '<' == begin of tag:
+
+    // is it a comment?  <!-- ... -->
+    if (strncmp(pStartOfTag + 1, "!--", 3) == 0)
+    {
+        // start of comment:
+        // find end of comment
+        PSZ pEnd = strstr(pStartOfTag, "-->");
+        if (pEnd)
+            // found:
+            // search on after end of comment
+            pct->pSource = pEnd + 3;
+        else
+        {
+            // end of comment not found:
+            // stop formatting...
+            pct->pSource++;
+            return;
+        }
+    }
+    else
+    {
+        // no comment:
+        // find end of tag
+        PSZ     p2 = pStartOfTag + 1,
+                pNextClose = 0,     // receives first '>' after '<'
+                pNextSpace = 0;     // receives first ' ' after '<'
+        BOOL    fCont = TRUE;
+        while (fCont)
+        {
+            switch (*p2)
+            {
+                case ' ':
+                case '\r':
+                case '\n':
+                    // store first space after '<'
+                    if (!pNextSpace)
+                        pNextSpace = p2;
+                    // overwrite line breaks with spaces;
+                    // otherwise we cannot handle tags which go across
+                    // several lines, which is valid HTML
+                    *p2 = ' ';
+                break;
+
+                case '>':   // end of tag found:
+                    pNextClose = p2;
+                    fCont = FALSE;
+                break;
+
+                case '<':
+                    // another opening tag:
+                    // that's an HTML error
+                    AppendChar(pct,
+                               *pct->pSource++);
+                    fCont = FALSE;
+                break;
+
+                case 0:
+                    fCont = FALSE;
+                break;
+            }
+            p2++;
+        }
+
+        if (pNextClose)
+        {
+            // end of tag found:
+            ULONG cbTag;
+            PSZ pStartOfAttrs = 0;
+
+            if ((pNextSpace) && (pNextSpace < pNextClose))
+            {
+                // we have attributes:
+                cbTag = pNextSpace - (pStartOfTag + 1);
+                pStartOfAttrs = pNextSpace;
+            }
+            else
+                cbTag = pNextClose - (pStartOfTag + 1);
+
+            if (!cbTag)
+            {
+                // happens if we have a "<>" in the text:
+                // just insert the '<>' and go on, we have no tag here
+                AppendChar(pct,
+                           *pct->pSource++);
+                AppendChar(pct,
+                           *pct->pSource++);
+            }
+            else
+            {
+                PFNPROCESSTAG pTagProcessor;
+
+                pct->cSaved = *(pStartOfTag + cbTag + 1);
+                // add a null terminator
+                *(pStartOfTag + cbTag + 1) = 0;
+
+                // find corresponding tag converter function
+                // from G_TagProcessors map
+                pTagProcessor = FindTagProcessor(pStartOfTag + 1); // pszTag);
+
+                // restore char under null terminator
+                *(pStartOfTag + cbTag + 1) = pct->cSaved;
+
+                // reset new source ptr; the tag handler
+                // can modify this
+                pct->pNewSource = NULL;
+
+                if (pTagProcessor)
+                {
+                    // tag understood:
+
+                    // terminate string after closing tag
+                    pct->cSaved = *(pNextClose + 1); // can be null byte!
+                    *(pNextClose + 1) = 0;
+
+                    // did we have attributes?
+                    if (pNextSpace)
+                        pct->pszAttributes = pNextSpace;
+
+                    // finally, call the tag handler
+                    (pTagProcessor)    // function
+                        (pct);          // argument
+
+                    *(pNextClose + 1) = pct->cSaved;
+                }
+
+                if (pct->pNewSource == NULL)
+                    // tag handler needs no special processing:
+                    // skip '>' too
+                    pct->pSource = pNextClose + 1;
+                else
+                    // tag handler has skipped something:
+                    pct->pSource = pct->pNewSource;
+            }
+        }
+    }
+}
+
+/*
+ *@@ ConvertEscape:
+ *      called by HandleEscape to find the ANSI (CP 1004)
+ *      character for the given escape sequence (pszTag).
+ *
+ *      pszTag contains the stuff between "&" and ";".
+ *
+ *      This is really ugly spaghetti, but it's the fastest
+ *      way to do it.
+ *
+ *@@added V0.9.4 (2000-06-10) [umoeller]
+ */
+
+CHAR ConvertEscape(PSZ pszTag)
+{
+    CHAR c0, c1;
+    CHAR crc = 0;
+
+    PSZ p2 = pszTag + 2;
+
+    c0 = *pszTag;
+    c1 = *(pszTag + 1);
+
+    switch (c0)
+    {
+        case 'a':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0)
+                        return 225;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0)
+                        return 226;
+                    else if (strcmp(p2, "ute") == 0)
+                        return 180;
+                break;
+
+                case 'e':
+                    if (strcmp(p2, "lig") == 0)
+                        return 230;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0)
+                        return 224;
+                break;
+
+                case 'm':
+                    if (strcmp(p2, "p") == 0)
+                        return '&';
+                break;
+
+                case 'r':
+                    if (strcmp(p2, "ing") == 0)
+                        return 229;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "ilde") == 0)
+                        return 227;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0)
+                        return 228;
+                break;
+            }
+        break;
+
+        case 'b':
+            if (strcmp(pszTag + 1, "rvbar") == 0)
+                return 166;
+        break;
+
+        case 'c':
+            switch (c1)
+            {
+                case 'c':
+                    if (strcmp(p2, "edil") == 0)
+                        return 231;
+                break;
+
+                case 'e':
+                    if (strcmp(p2, "dil") == 0)
+                        return 184;
+                    else if (strcmp(p2, "nt") == 0)
+                        return 162;
+                break;
+
+                case 'o':
+                    if (strcmp(p2, "py") == 0)
+                        return 169;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "rren") == 0)
+                        return 164;
+            }
+        break;
+
+        case 'd':
+            switch (c1)
+            {
+                case 'e':
+                    if (strcmp(p2, "g") == 0) return 176;
+                break;
+
+                case 'i':
+                    if (strcmp(p2, "vide") == 0) return 247;
+                break;
+            }
+        break;
+
+        case 'e':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 233;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 234;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 232;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "h") == 0) return 240;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 235;
+                break;
+            }
+        break;
+
+        case 'f':
+            switch (c1)
+            {
+                case 'r':
+                    if (strcmp(p2, "ac14") == 0) return 188;
+                    if (strcmp(p2, "ac12") == 0) return 189;
+                    if (strcmp(p2, "ac34") == 0) return 190;
+                break;
+            }
+        break;
+
+        case 'g':
+            switch (c1)
+            {
+                case 't':
+                    if (*p2 == 0) return '>';
+            }
+        break;
+
+        case 'i':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 237;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 238;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 236;
+                break;
+
+                case 'e':
+                    if (strcmp(p2, "xcl") == 0) return 161;
+                break;
+
+                case 'q':
+                    if (strcmp(p2, "uest") == 0) return 191;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 239;
+            }
+        break;
+
+        case 'l':
+            switch (c1)
+            {
+                case 't':
+                    if (*p2 == 0)
+                        return '<';
+                break;
+
+                case 'a':
+                    if (strcmp(p2, "quo") == 0) return 171;
+            }
+        break;
+
+        case 'm':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cr") == 0) return 175;
+                break;
+
+                case 'i':
+                    if (strcmp(p2, "cro") == 0) return 181;
+                    if (strcmp(p2, "ddot") == 0) return 183;
+                break;
+            }
+        break;
+
+        case 'n':
+            switch (c1)
+            {
+                case 'b':
+                    if (strcmp(p2, "sp") == 0) return 160;
+                break;
+
+                case 'o':
+                    if (strcmp(p2, "t") == 0) return 172;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "ilde") == 0) return 241;
+            }
+        break;
+
+        case 'o':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 243;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 244;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 242;
+                break;
+
+                case 'r':
+                    if (strcmp(p2, "df") == 0) return 170;
+                    if (strcmp(p2, "dm") == 0) return 186;
+                break;
+
+                case 's':
+                    if (strcmp(p2, "lash") == 0) return 248;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "ilde") == 0) return 245;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 246;
+            }
+        break;
+
+        case 'p':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "ra") == 0) return 182;
+                break;
+
+                case 'l':
+                    if (strcmp(p2, "usmn") == 0) return 177;
+                break;
+
+                case 'o':
+                    if (strcmp(p2, "und") == 0) return 163;
+            }
+        break;
+
+        case 'q':
+            if (strcmp(pszTag, "quot") == 0) return  '"';
+        break;
+
+        case 'r':
+            if (strcmp(pszTag, "raquo") == 0) return 187;
+            if (strcmp(pszTag, "reg") == 0) return 174;
+        break;
+
+        case 's':
+            switch (c1)
+            {
+                case 'z':
+                    if (strcmp(p2, "lig") == 0) return 223;
+                break;
+
+                case 'e':
+                    if (strcmp(p2, "ct") == 0) return 167;
+                break;
+
+                case 'h':
+                    if (strcmp(p2, "y") == 0) return 173;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "p1") == 0) return 185;
+                    if (strcmp(p2, "p2") == 0) return 178;
+                    if (strcmp(p2, "p3") == 0) return 179;
+            }
+        break;
+
+        case 't':
+            if (strcmp(pszTag, "thorn") == 0) return 254;
+            if (strcmp(pszTag, "times") == 0) return 215;
+        break;
+
+        case 'u':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 250;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 251;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 249;
+                break;
+
+                case 'm':
+                    if (strcmp(p2, "l") == 0) return 168;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 252;
+            }
+        break;
+
+        case 'y':
+            if (strcmp(pszTag, "yacute") == 0) return 253;
+            if (strcmp(pszTag, "yen") == 0) return 165;
+            if (strcmp(pszTag, "yuml") == 0) return 255;
+        break;
+
+        case 'A':
+            switch (c1)
+            {
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 196;
+                break;
+
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 193;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 194;
+                break;
+
+                case 'E':
+                    if (strcmp(p2, "lig") == 0) return 198;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 192;
+                break;
+
+                case 'r':
+                    if (strcmp(p2, "ing") == 0) return 197;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "ilde") == 0) return 195;
+            }
+        break;
+
+        case 'C':
+            if (strcmp(pszTag, "Ccedil") == 0) return 199;
+        break;
+
+        case 'E':
+            if (strcmp(pszTag, "Ecirc") == 0) return 202;
+            if (strcmp(pszTag, "Eacute") == 0) return 201;
+            if (strcmp(pszTag, "Egrave") == 0) return 200;
+            if (strcmp(pszTag, "ETH") == 0) return 208;
+            if (strcmp(pszTag, "Euml") == 0) return 203;
+        break;
+
+        case 'I':
+            if (strcmp(pszTag, "Icirc") == 0) return 206;
+            if (strcmp(pszTag, "Iacute") == 0) return 205;
+            if (strcmp(pszTag, "Igrave") == 0) return 204;
+            if (strcmp(pszTag, "Iuml") == 0) return 207;
+        break;
+
+        case 'N':
+            if (strcmp(pszTag, "Ntilde") == 0) return 209;
+        break;
+
+        case 'O':
+            switch (c1)
+            {
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 214;
+                break;
+
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 211;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 212;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 210;
+                break;
+
+                case 't':
+                    if (strcmp(p2, "ilde") == 0) return 213;
+                break;
+
+                case 's':
+                    if (strcmp(p2, "lash") == 0) return 216;
+            }
+        break;
+
+        case 'U':
+            switch (c1)
+            {
+                case 'a':
+                    if (strcmp(p2, "cute") == 0) return 218;
+                break;
+
+                case 'c':
+                    if (strcmp(p2, "irc") == 0) return 219;
+                break;
+
+                case 'g':
+                    if (strcmp(p2, "rave") == 0) return 217;
+                break;
+
+                case 'u':
+                    if (strcmp(p2, "ml") == 0) return 220;
+            }
+        break;
+
+        case 'T':
+            if (strcmp(pszTag, "THORN") == 0) return 222;
+        break;
+
+        case 'Y':
+            if (strcmp(pszTag, "Yacute") == 0) return 221;
+        break;
+    }
+
+    return (crc);
+}
+
+/*
+ *@@ HandleEscape:
+ *      called by txvConvertFromHTML when a "&" character
+ *      is found in the source buffer. This calls
+ *      ConvertEscape in turn.
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+VOID HandleEscape(PCOPYTARGET pct)
+{
+    // ampersand:
+    // replace special characters
+    PSZ     pStartOfTag = pct->pSource;
+    // find end of tag
+    PSZ     p2 = pStartOfTag,
+            pNextClose = 0,
+            pNextSpace = 0;
+    BOOL fCont = TRUE;
+    while (fCont)
+    {
+        switch (*p2)
+        {
+            case 0:
+                fCont = FALSE;
+            break;
+
+            case ';':
+                pNextClose = p2;
+                fCont = FALSE;
+            break;
+
+            case ' ':
+                if (!pNextSpace)
+                    pNextSpace = p2;
+            break;
+        }
+        p2++;
+    }
+
+    if (!pNextClose)
+        // no closing tag found:
+        // just insert the '&' and go on, we have no tag here
+        AppendChar(pct,
+                   *pct->pSource++);
+    else
+    {
+        if ((pNextSpace) && (pNextSpace < pNextClose))
+            // space before ';':
+            // just insert the '&' and go on, we have no tag here
+            AppendChar(pct,
+                       *pct->pSource++);
+        else if ((!pNextClose) || (pNextClose <= pStartOfTag + 1))
+            AppendChar(pct,
+                       *pct->pSource++);
+        else
+        {
+            ULONG ulCode = 0;
+
+            // create substring with tag
+            PSZ pszTag = pStartOfTag + 1;
+            *pNextClose = 0;
+
+            if (*pszTag == '#')
+            {
+                // latin-1 or Unicode encoding (&#000;)
+                ulCode = atoi(pszTag + 1);
+
+                // next input: char after ';'
+                pct->pSource = pNextClose + 1;
+            }
+            else
+            {
+                // named entity:
+                // find char code corresponding to escape
+                // from G_EscapeProcessors map
+                ulCode = ConvertEscape(pszTag);
+                if (ulCode)
+                    // tag supported:
+                    pct->pSource = pNextClose + 1;
+                else
+                    // tag not supported:
+                    ulCode = *pct->pSource++;
+            }
+
+            // restore closing tag which we overwrote
+            *pNextClose = ';';
+
+            if (ulCode)
+            {
+                AppendLinebreakCheck(pct);
+
+                AppendChar(pct,
+                           (CHAR)ulCode);
+                pct->fSkipNextSpace = FALSE;
+            }
+        }
+    }
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Entry points                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ txvConvertFromHTML:
+ *      this modifies the given text string (which should
+ *      be the complete BODY block of any HTML file) so
+ *      that all HTML tags are removed and replaced with
+ *      escape sequences that the XTextView control understands.
+ *
+ *      The buffer gets reallocated by this function, so it
+ *      must be free()'able.
+ *
+ *      So, to have the XTextView control display an HTML file,
+ *      do this:
+ *
+ *      1)  Load an HTML file into a buffer allocated by malloc().
+ *
+ *      2)  Call txvConvertFromHTML.
+ *
+ *      3)  Call WinSetWindowText on the XTextView control with
+ *          the modified buffer.
+ *
+ *      This understands the following limited subset of HTML:
+ *
+ *      Paragraph tags:
+ *
+ *      -- P, BR
+ *      -- PRE, /PRE
+ *      -- UL, /UL, OL, /OL, LI
+ *      -- DL, /DL, DT, DD
+ *      -- H1, /H1 thru H6, /H6
+ *      -- Comments (<!-- .... -->)
+ *
+ *      Character tags:
+ *
+ *      -- B, /B, STRONG, /STRONG
+ *      -- I, /I, EM, /EM, VAR, /VAR, CITE, /CITE
+ *      -- CODE, /CODE, SAMP, /SAMP, KBD, /KBD, TT, /TT
+ *      -- U, /U
+ *      -- STRIKE, /STRIKE
+ *      -- CODE, /CODE
+ *
+ *      The most obvious limitation is that neither tables
+ *      nor frames are supported. Also forget about CSS
+ *      and JavaScript, of course.
+ *
+ *      All the ampersand (&amp; something) sequences defined
+ *      in HTML 3 are properly translated.
+ *
+ *      Note: Those are translated to the ANSI (MS-Windows,
+ *      OS/2 codepage 1004) character set. This has the
+ *      following characteristics:
+ *
+ *      -- Codes 0-127 are identical to ASCII and thus
+ *         ISO 8559-1 ("Latin 1") also.
+ *
+ *      -- Codes 160-255 are identical to ISO 8559-1 ("Latin 1").
+ *
+ *      -- Codes 128-159 are NOT defined in ISO 8559-1, but
+ *         Netscape treats those as ANSI as well, so we do too.
+ *
+ *      As a result, consider the output to be in OS/2 codepage
+ *      1004. Either set your codepage to that (WinSetCp)
+ *      or translate the output (WinCpTranslateString).
+ *
+ *      &#xxx; tags (with xxx being a decimal) are considered
+ *      ANSI codes as well. Even though HTML 4.0 allows Unicode
+ *      characters > 255 to be inserted this way, we ignore
+ *      those. Unicode chars from 0 to 255 are identical to
+ *      ANSI, so for &#000; to &#255;, we are HTML-compliant.
+ *
+ *      All other tags are completely thrown out.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+BOOL txvConvertFromHTML(char **ppszText,
+                        PVOID pxhtml,           // out: various config data (PXHTMLDATA)
+                        PULONG pulProgress,     // out: progress (ptr can be NULL)
+                        PBOOL pfCancel)         // in: cancel flag (ptr can be NULL)
+{
+    BOOL    brc = TRUE;
+
+    PSZ     pszNew,
+            pTarget;
+    ULONG   cbSource = strlen(*ppszText);
+
+    COPYTARGET  ct = {0};
+
+    lstInit(&ct.llLists,
+            TRUE);      // free items
+
+    ct.pSource = *ppszText;
+    // skip leading spaces
+    ct.fSkipNextSpace = TRUE;
+    ct.pxhtml = (PXHTMLDATA)pxhtml;
+
+    // step 2:
+    // actual tags formatting
+
+    while (TRUE)
+    {
+        CHAR    c = *ct.pSource;
+
+        if (pfCancel)
+            if (*pfCancel)
+            {
+                brc = FALSE;
+                break;
+            }
+
+        if (!c)
+            // null terminator reached:
+            break;
+
+        // calculate progress
+        if (pulProgress)
+            *pulProgress = ((ct.pSource - *ppszText)     // characters done
+                            * 100
+                            / cbSource);              // characters total
+
+        switch (c)
+        {
+            case '<':
+                HandleTag(&ct);
+            break;
+
+            case '&':
+                HandleEscape(&ct);
+            break;
+
+            case '\r':
+                // skip
+                if (!ct.fSkipNextSpace)
+                {
+                    AppendChar(&ct,
+                               ' ');
+                    // ct.fNeedsLinebreak = FALSE;
+                    // but skip leading spaces which might follow
+                    if (!ct.fPRE)
+                        ct.fSkipNextSpace = TRUE;
+                }
+                ct.pSource++;
+            break;
+
+            case '\t':
+            {
+                if (ct.fPRE)
+                {
+                    ULONG ul;
+                    for (ul = 0;
+                         ul < 8;
+                         ul++)
+                        AppendChar(&ct,
+                                   ' ');
+                }
+                else
+                {
+                    // not in PRE block:
+                    if (    (!ct.fSkipNextSpace)
+                         // && (!ct.fNeedsLinebreak)
+                       )
+                        // last was not space: copy
+                        AppendChar(&ct,
+                                   ' ');
+
+                    ct.fSkipNextSpace = TRUE;
+                }
+
+                // skip the tab
+                ct.pSource++;
+            break; }
+
+            case '\n':
+            {
+                // newline char:
+                if (!ct.fPRE)
+                {
+                    // if not in PRE mode, replace with space
+                    if (!ct.fSkipNextSpace)
+                    {
+                        AppendChar(&ct,
+                                   ' ');
+                        // ct.fNeedsLinebreak = FALSE;
+                        // but skip leading spaces which might follow
+                        ct.fSkipNextSpace = TRUE;
+                    }
+                }
+                else
+                    // in PRE mode, preserve line breaks
+                    AppendChar(&ct, '\n'); // ct.fNeedsLinebreak = TRUE;
+
+                ct.pSource++;
+            break; }
+
+            case '\xFF':
+            {
+                AppendChar(&ct,
+                           ' ');
+                ct.pSource++;
+            break; }
+
+            case ' ':
+                if (!ct.fPRE)
+                {
+                    // is space, and not in PRE block:
+                    if (    (!ct.fSkipNextSpace)
+                         // && (!ct.fNeedsLinebreak)
+                       )
+                        // last was not space: copy
+                        AppendChar(&ct,
+                                   ' ');
+
+                    ct.fSkipNextSpace = TRUE;
+                }
+                else
+                    // in PRE, always add all spaces
+                    AppendChar(&ct,
+                               ' ');
+                ct.pSource++;
+            break;
+
+            default:
+                // if we're not inserting escapes or anything,
+                // check if a linebreak is needed
+                AppendLinebreakCheck(&ct);
+
+                AppendChar(&ct,
+                           *ct.pSource++);
+                ct.fSkipNextSpace = FALSE;
+                ct.fSkipNextLinebreak = FALSE;
+
+        } // end switch (*pSource);
+    } // end while (*pSource)
+    AppendChar(&ct,
+               '\n');
+    // append null-terminator
+    AppendChar(&ct,
+               0);
+
+    free(*ppszText);
+    *ppszText = ct.pszNew;
+
+    lstClear(&ct.llLists);
+
+    return (brc);
+}
+
+
Index: /trunk/src/helpers/textview.c
===================================================================
--- /trunk/src/helpers/textview.c	(revision 8)
+++ /trunk/src/helpers/textview.c	(revision 8)
@@ -0,0 +1,3643 @@
+
+/*
+ *@@sourcefile textview.c:
+ *      all-new XTextView control as well as device-independent
+ *      text formatting and printing. Whoa.
+ *
+ *      <B>Text view control</B>
+ *
+ *      This is a read-only control to display any given text
+ *      (PSZ) in any given font. As opposed to a multi-line entry
+ *      field (MLE), this can handle multiple fonts and character
+ *      and paragraph formatting. Also, this thing sets its scroll
+ *      bars right, which is one of the most annoying bugs in the
+ *      MLE control.
+ *
+ *      This is currently in the process of turning into a full-fledged
+ *      "rich text" control. For example, the WarpIN "Readme" pages
+ *      use this control.
+ *
+ *      This is all new with V0.9.1. Great changes have been made
+ *      with V0.9.3.
+ *
+ *      To use the text view control, you must call txvRegisterTextView
+ *      in your application first. This registers the WC_XTEXTVIEW
+ *      window class with PM.
+ *
+ *      Then create your XTextView using WinCreateWindow. The
+ *      XTextView control has an XTEXTVIEWCDATA control data
+ *      structure which optionally can be passed to WinCreateWindow
+ *      like this:
+ *
+ +          XTEXTVIEWCDATA xtxvCData;
+ +          memset(&xtxvCData, 0, sizeof(xtxvCData));
+ +          xtxvCData.cbData = sizeof(xtxvCData);
+ +          xtxvCData.flStyle  = XTXF_VSCROLL;
+ +          xtxvCData.ulXBorder = 20;
+ +          xtxvCData.ulYBorder = 20;
+ +          G_hwndProcView = WinCreateWindow(hwndClient,        // parent
+ +                                           WC_XTEXTVIEW,      // class
+ +                                           "",                // title, always ignored
+ +                                           WS_VISIBLE,        // style flags
+ +                                           0, 0, 100, 100,    // pos and size
+ +                                           hwndClient,        // owner
+ +                                           HWND_TOP,          // z-order
+ +                                           ID_PROCINFO,       // win ID
+ +                                           &xtxvCData,        // control data
+ +                                           0);                // presparams
+ +
+ *      <B>Setting the text to be displayed</B>
+ *
+ *      The text to be displayed must be passed to the control using
+ *      the standard WinSetWindowText function (upon which PM sends a
+ *      WM_SETWINDOWPARMS message to the control), which is then
+ *      automatically formatted and painted.
+ *
+ *      However, since the XTextView control is extremely capable,
+ *      a few things need to be noted:
+ *
+ *      -- The text view assumes that lines terminate with \n ONLY.
+ *         The \r char is used for soft line breaks (start new line
+ *         in the same paragraph, similar to the HTML BR tag). If
+ *         you give the control the usual OS/2 \r\n sequence, you
+ *         get large spacings. Use txvStripLinefeeds to strip the
+ *         \r characters before setting the text.
+ *
+ *         In short, to give the control any text, do this:
+ +
+ +              PSZ psz = ...  // whatever, load string
+ +              txvStripLinefeeds(&psz);        // reallocates
+ +              WinSetWindowText(hwndTextView, psz);
+ *
+ *      -- The control uses the \xFF (255) character internally as
+ *         an escape code for formatting commands. See "Escape codes"
+ *         below. If your text contains this character, you should
+ *         overwrite all occurences with spaces, or they will be
+ *         considered an escape code, which will cause problems.
+ *
+ *      -- If you don't care about learning all the escape codes,
+ *         you can automatically have HTML code converted to the
+ *         XTextView format using txvConvertFromHTML, which will
+ *         automatically insert all the codes right from plain
+ *         HTML. In the above code, use txvConvertFromHTML instead
+ *         of txvStripLinefeeds.
+ *
+ *      <B>Code page support</B>
+ *
+ *      The XTextView control assumes that the text given to it uses
+ *      the same codepage as the message queue (thread) on which
+ *      the control is running. So if you need codepage support,
+ *      issue WinSetCp before creating the text view control.
+ *
+ *      <B>Text formatting</B>
+ *
+ *      The XTextView control has a default paragraph format which
+ *      determines how text is formatted. If you don't change this
+ *      format, the control performs no word-wrapping and displays
+ *      all text "as is", that is, practically no formatting is
+ *      performed.
+ *
+ *      You can change the default paragraph format by sending
+ *      TXM_SETPARFORMAT to the control. This takes a XFMTPARAGRAPH
+ *      structure for input.
+ *
+ *      To quickly enable word-wrapping only, we have the extra
+ *      TXM_SETWORDWRAP message. This changes the word-wrapping flag
+ *      in the default paragraph format only so you don't have to
+ *      mess with all the rest.
+ *
+ *      The XTextView control is extremely fast in formatting. It
+ *      does pre-calculations once so that resizing the text
+ *      window does not perform a full reformat, but a quick
+ *      format based on the pre-calculations.
+ *
+ *      Presently, formatting is done synchronously. It is planned
+ *      to put formatting into a separate thread. Performance is
+ *      acceptable already now unless very large texts (> 200 KB)
+ *      are formatted (tested on a PII-400 machine).
+ *
+ *      <B>Presentation Parameters</B>
+ *
+ *      The XTextView control recognizes the following presentation
+ *      parameters:
+ *
+ *      -- PP_BACKGROUNDCOLOR; if not set, SYSCLR_DIALOGBACKGROUND
+ *         (per default gray) is used.
+ *
+ *      -- PP_FOREGROUNDCOLOR: if not set, SYSCLR_WINDOWSTATICTEXT
+ *         (per default blue) is used to signify that the text
+ *         cannot be worked on.
+ *
+ *      -- PP_FONTNAMESIZE: default font. This is the system font,
+ *         if not set.
+ *
+ *      This implies that fonts and colors can be dropped on the
+ *      control in the normal way. Font changes will cause a reformat.
+ *
+ *      Changing those presentation parameters is equivalent to
+ *      changing the corresponding fields in the default paragraph
+ *      format using TXM_SETPARFORMAT.
+ *
+ *      <B>Escape codes</B>
+ *
+ *      All XTextView escape codes start with a \xFF (255) character,
+ *      followed by at least one more character. The escape sequences
+ *      are variable in length and can have parameters. For details,
+ *      see textview.h where all these are listed.
+ *
+ *      Escape codes are evaluated by txvFormatText during formatting.
+ *
+ *      If you choose to give the text view control a text which
+ *      contains escape codes, you better make sure that you get the
+ *      exact codes right, or the text view control can crash. The
+ *      control has been optimized for speed, so no checking is done
+ *      on escape sequences.
+ *
+ *      <B>Device-independent text formatting</B>
+ *
+ *      If the features of the XTextView control satisfy your needs,
+ *      there's not much to worry about. However, if you're interested
+ *      in formatting the text yourself, here's more:
+ *
+ *      This file has the txvFormatText function, which is capable
+ *      of formatting an input string into any HPS. This works for
+ *      windows (used by the text view control) and printers (used
+ *      by txvPrint). Word-wrapping is supported. This is used by
+ *      the XTextView control internally whenever (re)formatting is
+ *      needed: either when the text is set or the formatting parameters
+ *      (fonts, margins, etc.) have changed.
+ *
+ *      These functions are designed to be used in a two-step process:
+ *      first format the text (using txvFormatText), then paint it
+ *      (using txvPaintText) for viewing or printing.
+ *      This speeds up painting dramatically, because formatting
+ *      might take some time.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\textview.h"
+ *
+ *@@added V0.9.1 (2000-02-13) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This program is part of the XWorkplace package.
+ *      This program is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published by
+ *      the Free Software Foundation, in version 2 as it comes in the COPYING
+ *      file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_WINWINDOWMGR
+#define INCL_WINFRAMEMGR
+#define INCL_WINMESSAGEMGR
+#define INCL_WININPUT
+#define INCL_WINRECTANGLES
+#define INCL_WINPOINTERS
+#define INCL_WINSYS
+#define INCL_WINSCROLLBARS
+#define INCL_WINSTDFONT
+#define INCL_WINCOUNTRY
+
+#define INCL_DEV
+#define INCL_SPL
+#define INCL_SPLDOSPRINT
+
+#define INCL_GPIPRIMITIVES
+#define INCL_GPILCIDS
+#define INCL_GPILOGCOLORTABLE
+#define INCL_GPITRANSFORMS
+#define INCL_GPIREGIONS
+
+#define INCL_ERRORS
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\comctl.h"
+#include "helpers\gpih.h"
+#include "helpers\linklist.h"
+#include "helpers\stringh.h"
+#include "helpers\winh.h"
+#include "helpers\xstring.h"            // extended string helpers
+
+#include "helpers\textview.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\PM helpers\Window classes\XTextView control
+ *      See textview.c.
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Device-independent functions                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ txvInitFormat:
+ *
+ */
+
+VOID txvInitFormat(PXFORMATDATA pxfd)
+{
+    memset(pxfd, 0, sizeof(XFORMATDATA));
+    lstInit(&pxfd->llRectangles,
+            TRUE);      // auto-free items
+    lstInit(&pxfd->llWords,
+            TRUE);      // auto-free items
+}
+
+/*
+ *@@ SetSubFont:
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+VOID SetSubFont(HPS hps,
+                PXFMTFONT pFont,
+                ULONG ulPointSize,
+                PSZ pszFaceName,
+                ULONG flFormat)
+{
+    CHAR    ac[256];
+    ULONG   ul;
+    POINTL  ptlStart = {0, 0},
+            aptl[257];
+
+    if (pFont->lcid)
+    {
+        // font already loaded:
+        if (GpiQueryCharSet(hps) == pFont->lcid)
+            // font currently selected:
+            GpiSetCharSet(hps, LCID_DEFAULT);
+        GpiDeleteSetId(hps, pFont->lcid);
+
+    }
+
+    if (pszFaceName)
+        pFont->lcid = gpihFindFont(hps,
+                                   ulPointSize,
+                                   TRUE,        // family, not face name
+                                   pszFaceName,
+                                   flFormat,
+                                   &pFont->FontMetrics);
+    else
+        pFont->lcid = LCID_DEFAULT; // 0
+
+    GpiSetCharSet(hps, pFont->lcid);
+    if (pFont->FontMetrics.fsDefn & FM_DEFN_OUTLINE)
+        // is outline font:
+        gpihSetPointSize(hps, ulPointSize);
+
+    for (ul = 0;
+         ul < 256;
+         ul++)
+        ac[ul] = ul;
+
+    GpiQueryCharStringPosAt(hps,
+                            &ptlStart,
+                            0,
+                            254,        // starting at one
+                            ac + 1,     // starting at one
+                            NULL,
+                            aptl);
+    // now compute width of every char
+    for (ul = 1;
+         ul < 256;
+         ul++)
+    {
+        pFont->alCX[ul] = aptl[ul+1].x - aptl[ul].x;
+    }
+}
+
+/*
+ *@@ txvSetFormatFont:
+ *      creates logical fonts from the specified
+ *      font information.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+VOID txvSetFormatFont(HPS hps,           // in: HPS to select default font into
+                      PXFMTCHARACTER pxfmtc, // in/out: format data
+                      ULONG ulPointSize, // in: font point size (e.g. 12) or 0
+                      PSZ pszFaceName)   // in: font face name (e.g. "Courier") or NULL
+{
+    pxfmtc->lPointSize = ulPointSize;
+
+    // regular
+    SetSubFont(hps,
+               &pxfmtc->fntRegular,
+               ulPointSize,
+               pszFaceName,
+               0);
+
+    // bold
+    SetSubFont(hps,
+               &pxfmtc->fntBold,
+               ulPointSize,
+               pszFaceName,
+               FATTR_SEL_BOLD);
+
+    // italics
+    SetSubFont(hps,
+               &pxfmtc->fntItalics,
+               ulPointSize,
+               pszFaceName,
+               FATTR_SEL_ITALIC);
+
+    // bold italics
+    SetSubFont(hps,
+               &pxfmtc->fntBoldItalics,
+               ulPointSize,
+               pszFaceName,
+               FATTR_SEL_BOLD | FATTR_SEL_ITALIC);
+}
+
+/*
+ *@@ AppendCharNoCheck:
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+VOID AppendCharNoCheck(char **ppszNew,
+                       PULONG pcbNew,
+                       char **ppTarget,
+                       char c)
+{
+    ULONG   cbSizeThis = *ppTarget - *ppszNew;
+    if (cbSizeThis >= *pcbNew)
+    {
+        // more mem needed:
+        *pcbNew += 10000;
+        *ppszNew = (PSZ)realloc(*ppszNew, *pcbNew);
+                    // if first call, pszNew is NULL, and realloc
+                    // behaves just like malloc
+        // adjust target, because ptr might have changed
+        *ppTarget = *ppszNew + cbSizeThis;
+    }
+
+    **ppTarget = c;
+    (*ppTarget)++;
+}
+
+/*
+ *@@ txvStripLinefeeds:
+ *      this removes all linefeeds (\r) from
+ *      the specified string to prepare it
+ *      for display with the text view control.
+ *
+ *      This also replaces tabs (\t) with ulTabSize spaces.
+ *
+ *      The buffer gets reallocated by this function, so it
+ *      must be free()'able.
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+VOID txvStripLinefeeds(char **ppszText,
+                       ULONG ulTabSize)
+{
+    PSZ pSource = *ppszText;
+    ULONG cbNew = 1000;
+    PSZ pszNew = (PSZ)malloc(cbNew);
+    PSZ pTarget = pszNew;
+
+    while (*pSource)
+    {
+        if (*pSource == '\r')
+            pSource++;
+        else if (*pSource == '\t')
+        {
+            ULONG ul;
+            for (ul = 0;
+                 ul < ulTabSize;
+                 ul++)
+                AppendCharNoCheck(&pszNew,
+                                  &cbNew,
+                                  &pTarget,
+                                  ' ');
+
+            // skip the tab
+            pSource++;
+        }
+        else
+            AppendCharNoCheck(&pszNew,
+                              &cbNew,
+                              &pTarget,
+                              *pSource++);
+    }
+    AppendCharNoCheck(&pszNew,
+                      &cbNew,
+                      &pTarget,
+                      '\n');
+    AppendCharNoCheck(&pszNew,
+                      &cbNew,
+                      &pTarget,
+                      0);
+
+    free(*ppszText);
+    *ppszText = pszNew;
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Device-independent text formatting                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ strhFindEOL2:
+ *      finds the end of a line.
+ *
+ *      An "end of line" means the next \r, \n, or \0 character
+ *      after *ppszSearchIn.
+ *
+ *      This returns the pointer to that exact character, which
+ *      can be equal or higher than *ppszSearchIn.
+ *      This should never return NULL because at some point,
+ *      there will be a null byte in your string (unless you have
+ *      a heap problem).
+ *
+ *      If the EOL character is not null (\0), *ppszSearchIN is
+ *      advanced to the first character of the _next_ line. This
+ *      can be the EOL pointer plus one if you have a UNIX-style
+ *      string (\n only at the end of each line) or EOL + 2 for
+ *      DOS and OS/2-style EOLs (which have \r\n at the end of
+ *      each line).
+ *
+ *@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+PSZ strhFindEOL2(PSZ *ppszSearchIn,        // in: where to search
+                 PULONG pulOffset)       // out: offset (ptr can be NULL)
+{
+    PSZ     pThis = *ppszSearchIn,
+            prc = NULL;
+    while (TRUE)
+    {
+        if ( (*pThis == '\r') || (*pThis == '\n') || (*pThis == 0) )
+        {
+            prc = pThis;
+            break;
+        }
+        pThis++;
+    }
+
+    // before modifying pointer, store offset
+    if (pulOffset)
+        *pulOffset = prc - *ppszSearchIn;
+
+    if (*prc == 0)
+    {
+        // null byte (end of string):
+        *ppszSearchIn = prc;
+    }
+    else
+    {
+        // not null byte (end of string):
+        // skip following newline characters
+        if (*prc == '\r')
+        {
+            if ( *(prc+1) == '\n')
+                // we have a \r char next,
+                // that's the DOS and OS/2 format (\r\n):
+                // skip that too
+                *ppszSearchIn = prc + 2;
+            else
+                *ppszSearchIn = prc + 1;
+        }
+        else if (*prc == '\n')
+            // UNIX format (used by HTML formatter also):
+            *ppszSearchIn = prc + 1;
+    }
+
+    // now:
+    // 1)   prc points to the \r, \n, or \0 character (EOL)
+    // 2)   *ppszSearchIn has been advanced to the first character
+    //      of the next line or points to the \0 character
+
+    return (prc);
+}
+
+/* #define TXVFRECTF_EMPTY                  0x0001
+#define TXVFRECTF_PARAGRAPHDONE          0x0002
+#define TXVFRECTF_WORDSLEFT              0x0004
+#define TXVFRECTF_STOPPEDONESCAPE        0x0008
+#define TXVFRECTF_ENDOFTEXT              0x0010
+   */
+/*
+ *@@ FORMATLINEBUF:
+ *      worker structure to store various data
+ *      in txvFormatText in between txvCreateWord
+ *      calls. This has been created for speed
+ *      so we don't have to pass all these on
+ *      the stack all the time.
+ *
+ *@@added V0.9.3 (2000-05-06) [umoeller]
+ */
+
+typedef struct _FORMATLINEBUF
+{
+    PSZ             pLastChar;  // ptr to null terminator in text
+
+    // formatting data; this is set by txvFormatText according
+    // to escape characters and read by txvCreateRectangle
+    XFMTPARAGRAPH   fmtp;
+    PXFMTCHARACTER  pfmtc;      // pointer to character formatting data
+    PXFMTFONT       pfmtf;      // pointer to font to use
+
+    BOOL            fPre,
+                    fBold,
+                    fItalics;
+
+    // current anchor
+    USHORT          usCurrentAnchor;
+                        // this is > 0 if we're currently in an anchor block
+
+    // data copied to TXVWORD
+    LONG            lcid;
+    LONG            lPointSize;
+    ULONG           flOptions;  // any combination of CHS_UNDERSCORE and CHS_STRIKEOUT
+
+    // counters, ...
+    LONG            lXCurrent;  // current X position while adding words to rectangle
+} FORMATLINEBUF, *PFORMATLINEBUF;
+
+/*
+ *@@ txvCreateWord:
+ *
+ *      --  If the word ends with one or several spaces,
+ *          ppStartOfWord is set to the beginning of the
+ *          next word (non-space character).
+ *          pWord->ulFlags is set to 0.
+ *
+ *      --  If the word ends with an escape character,
+ *          ppStartOfWord is set to point to the escape,
+ *          which must be handled by the caller.
+ *          pWord->ulFlags is set to TXVWORDF_GLUEWITHNEXT.
+ *
+ *      --  If the word ends with a \n or \r,
+ *          ppStartOfWord is set to the beginning of the
+ *          next line (first char after \n or \r). This
+ *          may be another \n or \r, but the first one
+ *          is skipped.
+ *          pWord->ulFlags is set to TXVWORDF_LINEBREAK or
+ *          TXVWORDF_LINEFEED.
+ *
+ *@@added V0.9.3 (2000-05-14) [umoeller]
+ */
+
+PTXVWORD txvCreateWord(HPS hps,
+                        PSZ *ppStartOfWord,
+                        PFORMATLINEBUF pflbuf)
+{
+    PTXVWORD   pWord = NULL;
+
+    // find next word:
+    if (**ppStartOfWord)
+    {
+        PSZ         pWordStart = *ppStartOfWord,
+                    pWordEnd = NULL;
+        PSZ         pCheck = *ppStartOfWord;
+        ULONG       cChars = 0,
+                    cCheck = 0;
+
+        pWord = (PTXVWORD)malloc(sizeof(TXVWORD));
+        memset(pWord, 0, sizeof(TXVWORD));
+                // this includes fIsEscapeSequence = FALSE;
+        pWord->pStart = pWordStart;
+
+        // initially, this has pWordStart pointing
+        // to *ppStartOfWord. If a word is found,
+        // pWordStart is set to the first char of
+        // the word and pWordEnd receives the
+        // pointer to the first character after the word (probably space)
+        if (strhGetWord(&pWordStart, // in/out
+                        pflbuf->pLastChar,
+                        " ",
+                        "\x0d\x0a \xFF", // in: end chars; includes our escape!
+                        &pWordEnd))  // out: first char after word
+        {
+            // whoa, found a word:
+            while (*pWordEnd == ' ')
+                pWordEnd++;
+
+            cChars = (pWordEnd - *ppStartOfWord);
+        }
+
+        if (cChars)
+        {
+            POINTL      aptlText[TXTBOX_COUNT];
+            // cChars is != 0 if strhGetWord succeeded AND the
+            // line is not empty, so go on
+            cCheck = cChars;
+
+            // advance input pointer
+            *ppStartOfWord = pWordEnd;
+
+            GpiQueryTextBox(hps,
+                            // no. of chars since start of word:
+                            cChars,
+                            // first char:
+                            pCheck,
+                            TXTBOX_COUNT,
+                            (PPOINTL)&aptlText);
+
+            pWord->cChars = cChars;
+            pWord->ulFlags = 0;
+
+            if (cChars)
+                pWord->ulCXWithSpaces = aptlText[TXTBOX_TOPRIGHT].x;
+            else
+                pWord->ulCXWithSpaces = 0;
+
+            pWord->ulCY = aptlText[TXTBOX_TOPRIGHT].y
+                          - aptlText[TXTBOX_BOTTOMRIGHT].y;
+            // store base line ofs; aptlText[TXTBOX_BOTTOMRIGHT].y is negative
+            // if the string has any characters drawn below the base line, e.g.
+            // for the "g" and "y" characters
+            pWord->ulBaseLineOfs = -aptlText[TXTBOX_BOTTOMRIGHT].y;
+        }
+        else
+        {
+            // no word found or empty line:
+            pWord->ulCY = pflbuf->pfmtf->FontMetrics.lMaxBaselineExt;
+        }
+
+        switch (**ppStartOfWord)
+        {
+            case TXVESC_CHAR: // '\xFF':
+                pWord->ulFlags = TXVWORDF_GLUEWITHNEXT;
+            break;
+
+            case '\n':
+                pWord->ulFlags = TXVWORDF_LINEBREAK;
+                (*ppStartOfWord)++;     // skip \n
+            break;
+
+            case '\r':
+                pWord->ulFlags = TXVWORDF_LINEFEED;
+                (*ppStartOfWord)++;     // skip \r
+            break;
+        }
+
+        pWord->lcid = pflbuf->pfmtf->lcid;
+        pWord->lPointSize = pflbuf->lPointSize;
+        pWord->flOptions = pflbuf->flOptions;
+
+        pWord->usAnchor = pflbuf->usCurrentAnchor; // 0 if none
+    }
+
+    return (pWord);
+}
+
+/*
+ *@@ ProcessEscapes:
+ *      gets called when txvFormatText stops on an
+ *      escape character (\xFF). This evaluates the
+ *      escape sequence, reacts accordingly, and
+ *      advances *ppCurrent to after the escape
+ *      sequence so that regular processing can
+ *      continue.
+ *
+ *      There are two types of escape sequences:
+ *
+ *      -- Those which are only relevant during word processing,
+ *         such as character formatting attributes (bold, italics,
+ *         font, size, ...). Those affect the TXVWORD structure
+ *         directly and are thus never evaluated in step 2,
+ *         rectangles correlation.
+ *
+ *      -- Those which affect spacings, margins, etc. (paragraph
+ *         formatting). These need to be re-evaluated even during
+ *         "quick" format, without words being recalculated, because
+ *         those spacings affect the output rectangles.
+ *
+ *         If one of those sequences is encountered, this function
+ *         appends a special TXVWORD structure to XFORMATDATA.llWords.
+ *
+ *@@added V0.9.3 (2000-05-07) [umoeller]
+ */
+
+PTXVWORD ProcessEscapes(char **ppCurrent,          // in/out: current position; initially points to esc char
+                        PXFORMATDATA pxfd,         // in/out: formatting data
+                        PFORMATLINEBUF pflbuf,     // in/out: formatting buffer
+                        BOOL fWordsProcessed)      // FALSE during step 1 (words processing),
+                                                   // TRUE during step 2 (rectangles correlation)
+{
+    PTXVWORD pEscapeWord = NULL;
+
+    // this is set to TRUE either above or by txvCreateRectangle if
+    // an escape character was found; txvCreateRectangle
+    // then sets pCurrent to the escape character (\xFF)
+    CHAR    cCode1 = *((*ppCurrent)+1);
+    CHAR    cCode2 = *((*ppCurrent)+2);
+    ULONG   ulSkip = 3; // per default, skip \xFF plus two
+    CHAR    szDecimal[10];
+    LONG    lDecimal;
+
+    BOOL    fCreateWord = FALSE,
+            fPaintEscapeWord = FALSE;
+
+    switch (cCode1)
+    {
+        case 1:     // change font:
+            // three decimals follow specifying the font
+            memcpy(szDecimal, (*ppCurrent)+2, 3);
+            szDecimal[3] = 0;
+            lDecimal = atoi(szDecimal);
+            if (lDecimal == 0)
+                pflbuf->pfmtc = &pxfd->fmtcStandard;
+            else if (lDecimal == 1)
+                pflbuf->pfmtc = &pxfd->fmtcCode;
+            ulSkip = 5;
+        break;
+
+        case 2:     // B or /B
+            if (cCode2 == 1)
+                pflbuf->fBold = TRUE;
+            else
+                pflbuf->fBold = FALSE;
+        break;
+
+        case 3:     // I or /I
+            if (cCode2 == 1)
+                pflbuf->fItalics = TRUE;
+            else
+                pflbuf->fItalics = FALSE;
+        break;
+
+        case 4:     // U or /U
+            if (cCode2 == 1)
+                pflbuf->flOptions |= CHS_UNDERSCORE;
+            else
+                pflbuf->flOptions &= ~CHS_UNDERSCORE;
+        break;
+
+        case 5:     // STRIKE or /STRIKE
+            if (cCode2 == 1)
+                pflbuf->flOptions |= CHS_STRIKEOUT;
+            else
+                pflbuf->flOptions &= ~CHS_STRIKEOUT;
+        break;
+
+        case 6:     // A or /A HREF= (link)
+        {
+            // four characters with hex anchor index (>=1)
+            // or ####
+            if (  *( (*ppCurrent)+2 )
+                  == '#'
+               )
+                pflbuf->usCurrentAnchor = 0;
+            else
+            {
+                PSZ pEnd;
+                memcpy(szDecimal, (*ppCurrent)+2, 4);
+                szDecimal[4] = 0;
+                lDecimal = strtol(szDecimal, &pEnd, 16);
+                pflbuf->usCurrentAnchor = lDecimal;
+            }
+
+            ulSkip = 6;
+        break; }
+
+        case 7:     // A NAME= (anchor name)
+        {
+            // this is variable in length and terminated with
+            // another 0xFF char; we completely ignore this
+            // here and just skip the anchor name, this is
+            // only used with TXM_JUMPTOANCHORNAME, which then
+            // searches the buffer
+            PSZ pEnd = strchr((*ppCurrent)+2, 0xFF);
+            if (pEnd)
+            {
+                ulSkip = pEnd - *ppCurrent + 1;
+                // store this with the other words so we can
+                // find this word later
+                fCreateWord = TRUE;
+                // and store this with the rectangles
+                fPaintEscapeWord = TRUE;
+            }
+        break; }
+
+        case 0x10:  // relative point size in percent
+            // three characters follow specifying the
+            // percentage
+            memcpy(szDecimal, (*ppCurrent)+2, 3);
+            szDecimal[3] = 0;
+            lDecimal = atoi(szDecimal);
+
+            pflbuf->lPointSize = pflbuf->pfmtc->lPointSize * lDecimal / 100;
+            ulSkip = 5;
+        break;
+
+        case 0x20: // left margin changed:
+            memcpy(szDecimal, (*ppCurrent)+2, 4);   // four decimals xxxx
+            szDecimal[4] = 0;
+            lDecimal = atoi(szDecimal);
+
+            // this is based on the current average font width, so
+            // find this:
+            pflbuf->fmtp.lLeftMargin = (lDecimal
+                                        * pflbuf->lPointSize);
+            ulSkip = 6;
+            fCreateWord = TRUE;     // for rectangle correlation
+        break;
+
+        case 0x21: // first line margin changed:
+            memcpy(szDecimal, (*ppCurrent)+2, 4);   // +xxx, -xxx
+            szDecimal[4] = 0;
+            lDecimal = atoi(szDecimal);
+
+            // this is based on the current average font width, so
+            // find this:
+            pflbuf->fmtp.lFirstLineMargin = (lDecimal
+                                             * pflbuf->lPointSize);
+            ulSkip = 6;
+            fCreateWord = TRUE;     // for rectangle correlation
+        break;
+
+        case 0x22: // tab: forward current X to left margin
+            pflbuf->lXCurrent = pflbuf->fmtp.lLeftMargin;
+
+            ulSkip = 2;
+            fCreateWord = TRUE;     // for rectangle correlation
+        break;
+
+        case 0x23: // marker: store this in output, this needs
+                   // to be painted
+            fCreateWord = TRUE;
+            fPaintEscapeWord = TRUE;
+            ulSkip = 3;
+        break;
+
+        case 0x30:  // spacing before paragraph:
+            // four chars follow with either "####" or decimal spacing
+            memcpy(szDecimal, (*ppCurrent)+2, 4);
+            szDecimal[4] = 0;
+            if (memcmp(szDecimal, "####", 4) == 0)
+                // reset to default:
+                pflbuf->fmtp.lSpaceBefore = pxfd->fmtpStandard.lSpaceBefore;
+            else
+            {
+                lDecimal = atoi(szDecimal);
+                pflbuf->fmtp.lSpaceBefore = lDecimal;
+            }
+            ulSkip = 6;
+            fCreateWord = TRUE;     // for rectangle correlation
+        break;
+
+        case 0x31:  // spacing before paragraph:
+            // four chars follow with either "####" or decimal spacing
+            memcpy(szDecimal, (*ppCurrent)+2, 4);
+            szDecimal[4] = 0;
+            if (memcmp(szDecimal, "####", 4) == 0)
+                // reset to default:
+                pflbuf->fmtp.lSpaceAfter = pxfd->fmtpStandard.lSpaceAfter;
+            else
+            {
+                lDecimal = atoi(szDecimal);
+                pflbuf->fmtp.lSpaceAfter = lDecimal;
+            }
+            ulSkip = 6;
+            fCreateWord = TRUE;     // for rectangle correlation
+        break;
+
+        case 0x32:  // word-wrapping:
+            // here follows a single char being "0" or "1"
+            if (    *((*ppCurrent) + 2) == '0')
+                pflbuf->fmtp.fWordWrap = FALSE;
+            else
+                pflbuf->fmtp.fWordWrap = TRUE;
+            fCreateWord = TRUE;     // for rectangle correlation
+    }
+
+    if (fCreateWord)        // append for rectangle correlation?
+        if (!fWordsProcessed)    // are we processing words still (step 1)?
+        {
+            // yes: append to list for rectangle correlation later
+            pEscapeWord = (PTXVWORD)malloc(sizeof(TXVWORD));
+            memset(pEscapeWord, 0, sizeof(TXVWORD));
+            // mark as escape sequence
+            pEscapeWord->pStart = *ppCurrent;
+            pEscapeWord->cChars = ulSkip;
+            pEscapeWord->cEscapeCode = *(*ppCurrent + 1);
+            pEscapeWord->fPaintEscapeWord = fPaintEscapeWord;
+            pEscapeWord->usAnchor = pflbuf->usCurrentAnchor; // 0 if none
+            if (fPaintEscapeWord)
+            {
+                pEscapeWord->lX = pflbuf->lXCurrent;
+                pEscapeWord->lcid = pflbuf->pfmtf->lcid;
+                pEscapeWord->lPointSize = pflbuf->lPointSize;
+                pEscapeWord->flOptions = pflbuf->flOptions;
+            }
+            lstAppendItem(&pxfd->llWords, pEscapeWord);
+        }
+
+    if (!fWordsProcessed)
+        // if we're still processing words, advance
+        // current pointer by the escape length
+        *ppCurrent += ulSkip;
+
+    return (pEscapeWord);
+}
+
+/*
+ *@@ txvFormatText:
+ *      this is the core function to text formatting, which
+ *      must be done before the text can be painted into an
+ *      HPS. See the top of textview.c for details.
+ *
+ *      Even though this function does not seem to have a
+ *      lot of parameters, it is extremely powerful. This
+ *      function handles paragraph and character formatting
+ *      automatically. See XFMTPARAGRAPH and XFMTCHARACTER
+ *      for possible formatting attributes, which are part
+ *      of the XFORMATDATA structure passed to this function.
+ *
+ *      "Formatting" means splitting up any zero-terminated
+ *      string (XFORMATDATA.pszViewText) into a possibly
+ *      large list of TXVRECTANGLE structures, which each
+ *      hold a rectangle to be painted. This allows for
+ *      extremely fast painting.
+ *
+ *      Each TXVRECTANGLE in turn holds several "words" to
+ *      be painted. A word consists of a TXVWORD structure
+ *      and is normally a sequence of characters between
+ *      spaces, \n and \r characters. As an exception, if
+ *      escape sequences come up, such a "word" is split up
+ *      into several words because character formatting
+ *      (font, size, ...) is done on a per-word basis when painting.
+ *
+ *      This approach allows for quicker word-wrapping when only
+ *      the output (paint) rectangle is changed because we don't
+ *      have to re-calculate all the character widths (TXVWORD) once we
+ *      got the words. Instead, when re-formatting, we just recompose
+ *      the rectangles based on the words we calculated already.
+ *      Of course, when character widths change (e.g. because
+ *      fonts are changed), everything has to be redone.
+ *
+ *      Processing depends on the current formatting settings
+ *      of the XFORMATDATA structure passed to this func and
+ *      can become quite complicated:
+ *
+ *      --  In the simplest possible formatting mode, that is, if
+ *          word wrapping is disabled, each such TXVRECTANGLE
+ *          structure will hold one paragraph from the text
+ *          (that is, the text between two \n chars).
+ *
+ *      --  If word wrapping is enabled, each paragraph in the text
+ *          can consist of several such rectangles if the paragraph
+ *          does not fit into one line. In that case, we create
+ *          one XFMTRECTANGLE for each line which is needed to
+ *          display the paragraph word-wrapped.
+ *
+ *      This uses an XFORMATDATA structure for input and output
+ *      (besides the other parameters).
+ *
+ *      On input, specify the following:
+ *
+ *      -- hps: window or printer HPS. This is used for
+ *         formatting only, but nothing is painted.
+ *
+ *      -- XFORMATDATA.pszViewText: the text to be formatted.
+ *         This must follow certain conventions; the \xFF,
+ *         \r, and \n characters have a special meaning.
+ *         See the top of textview.c for details.
+ *
+ *      -- XFORMATDATA.fmtpStandard, fmtcStandard, fmtcCode:
+ *         paragraph and character formatting attributes.
+ *         For the simplest possible formatting, memset
+ *         all these to 0. Word-wrapping depends on
+ *         the paragraph formats.
+ *
+ *      -- prclView: rectangle for which formatting should take
+ *         place. When this is called for a screen window,
+ *         this should be the visible area of the window
+ *         (WinQueryWindowRect).
+ *         When this is called with a printer PS, this should
+ *         be the size of a printer page.
+ *
+ *      This function updates the following:
+ *
+ *      -- XFORMATDATA.llWords: list of TXVWORD structures,
+ *         holding all the "words" in the text as described
+ *         above. This list can grow very long, but only needs
+ *         to be recalculated when fonts change.
+ *
+ *      -- XFORMATDATA.llRectangles: list of TXVRECTANGLE
+ *         structures, correlating the words on the words list
+ *         to paint rectangles.
+ *
+ *      -- XFORMATDATA.ulViewportCX, ulViewportCY: total width
+ *         and height of the "viewport", i.e. the total space
+ *         needed to display the text (in pels). This might
+ *         be smaller, the same, or larger than prclView,
+ *         depending on whether the text fits into prclView.
+ *
+ *         When displaying text, you should display scroll bars
+ *         if the viewport is larger than the window (prclView).
+ *
+ *         When printing, if the viewport is larger than the
+ *         printer page (prclView), you will need to call
+ *         txvPaintText several times for each page.
+ *
+ *      All coordinates are in world space (PU_PELS).
+ *
+ *@@changed V0.9.3 (2000-05-06) [umoeller]: largely rewritten; now handling paragraph and character formats
+ */
+
+VOID txvFormatText(HPS hps,             // in: HPS whose font is used for
+                                        // calculating text dimensions
+                   PXFORMATDATA pxfd,   // in: formatting data
+                   PRECTL prclView,     // in: rectangle to format for (window or printer page)
+                   BOOL fFullRecalc)    // in: re-calculate word list too? (must be TRUE on the first call)
+{
+    /* ULONG   ulWinCX = (prclView->xRight - prclView->xLeft),
+            ulWinCY = (prclView->yTop - prclView->yBottom); */
+
+    lstClear(&pxfd->llRectangles);
+    if (fFullRecalc)
+        lstClear(&pxfd->llWords);
+
+    pxfd->ulViewportCX = 0;
+    pxfd->ulViewportCY = 0;
+
+    if (pxfd->pszViewText)
+    {
+        ULONG   ulTextLen = strlen(pxfd->pszViewText);
+
+        FORMATLINEBUF   flbuf;
+        LONG            lcidLast = -99,
+                        lPointSizeLast = -99;
+
+        memset(&flbuf, 0, sizeof(flbuf));
+        // copy default paragraph formatting
+        memcpy(&flbuf.fmtp, &pxfd->fmtpStandard, sizeof(flbuf.fmtp));
+        // set font
+        flbuf.pfmtc = &pxfd->fmtcStandard;
+        flbuf.lPointSize = pxfd->fmtcStandard.lPointSize;
+        flbuf.pLastChar = pxfd->pszViewText + ulTextLen;
+
+        if (ulTextLen)
+        {
+            ULONG   cWords = 0;
+
+            if (fFullRecalc)
+            {
+                /*
+                 * step 1: create words
+                 *
+                 */
+
+                PSZ     pCurrent = pxfd->pszViewText;
+
+                // loop until null terminator
+                while (*pCurrent)
+                {
+                    PTXVWORD pWord;
+
+                    if (flbuf.fBold)
+                    {
+                        if (flbuf.fItalics)
+                            flbuf.pfmtf = &flbuf.pfmtc->fntBoldItalics;
+                        else
+                            flbuf.pfmtf = &flbuf.pfmtc->fntBold;
+                    }
+                    else
+                        if (flbuf.fItalics)
+                            flbuf.pfmtf = &flbuf.pfmtc->fntItalics;
+                        else
+                            flbuf.pfmtf = &flbuf.pfmtc->fntRegular;
+
+                    // set font for subsequent calculations,
+                    // if changed (this includes the first call)
+                    if (lcidLast != flbuf.pfmtf->lcid)
+                    {
+                        GpiSetCharSet(hps, flbuf.pfmtf->lcid);
+                        lcidLast = flbuf.pfmtf->lcid;
+                        // force recalc of point size
+                        lPointSizeLast = -99;
+                    }
+
+                    if (lPointSizeLast != flbuf.lPointSize)
+                    {
+                        if (flbuf.pfmtf->FontMetrics.fsDefn & FM_DEFN_OUTLINE)
+                            // is outline font:
+                            gpihSetPointSize(hps, flbuf.lPointSize);
+                        lPointSizeLast = flbuf.lPointSize;
+                    }
+
+                    pWord = txvCreateWord(hps,
+                                          &pCurrent, // advanced to next word
+                                          &flbuf);
+                    if (pWord)
+                    {
+                        lstAppendItem(&pxfd->llWords, pWord);
+
+                        /* {
+                            CHAR szWord[3000];
+                            strhncpy0(szWord, pWord->pStart, min(pWord->cChars, sizeof(szWord)));
+                            _Pmpf(("Found word '%s'", szWord));
+                        } */
+
+                        cWords++;
+
+                        while (*pCurrent == TXVESC_CHAR) // '\xFF')
+                        {
+                            // handle escapes;
+                            // this advances pCurrent depending on the
+                            // escape sequence length and might append
+                            // another "word" for the escape sequence
+                            // if it's relevant for rectangle correlation
+                            ProcessEscapes(&pCurrent,
+                                           pxfd,
+                                           &flbuf,
+                                           FALSE); // fWordsProcessed
+                        }
+                    }
+                    else
+                        break;
+                }
+            } // end if (fFullRecalc)
+            else
+                cWords = lstCountItems(&pxfd->llWords);
+
+            /*
+             * step 2: create rectangles
+             *
+             */
+
+            if (cWords)
+            {
+                PLISTNODE pWordNode = lstQueryFirstNode(&pxfd->llWords);
+
+                LONG        lCurrentYTop = prclView->yTop,
+                            lOrigYTop = lCurrentYTop;
+
+                BOOL        fRects2Go = TRUE;
+
+                // space before paragraph; this is reset
+                // to 0 if we start a new rectangle for
+                // the same paragraph
+                ULONG       ulYPre = flbuf.fmtp.lSpaceBefore;
+
+                // rectangles loop
+                while (fRects2Go)
+                {
+                    BOOL    fWords2Go = TRUE;
+                    ULONG   ulWordsInThisRect = 0;
+
+                    // maximum height of words in this rect
+                    ULONG   lWordsMaxCY = 0;
+
+                    // start a new rectangle:
+                    PTXVRECTANGLE pRect = (PTXVRECTANGLE)malloc(sizeof(TXVRECTANGLE));
+                    lstInit(&pRect->llWords,
+                            FALSE);     // no auto-free; the words are stored in the main
+                                        // list also, which is freed
+                    // rectangle's xLeft;
+                    // xRight will be set when we're done with this rectangle
+                    pRect->rcl.xLeft = prclView->xLeft + flbuf.fmtp.lLeftMargin;
+                    if (ulYPre)
+                        // starting new paragraph:
+                        // add first-line offset also
+                        pRect->rcl.xLeft += flbuf.fmtp.lFirstLineMargin;
+
+                    // current X pos: start with left of rectangle
+                    flbuf.lXCurrent = pRect->rcl.xLeft;
+
+                    // max baseline ofs: set to 0, this will be raised
+                    pRect->ulMaxBaseLineOfs = 0;
+
+                    // words-per-rectangle loop;
+                    // we keep adding words to the rectangle until
+                    // a) words no longer fit and word-wrapping is on;
+                    // b) a newline or line feed is found;
+                    // c) the last word has been reached;
+                    while (fWords2Go)
+                    {
+                        PTXVWORD pWordThis = (PTXVWORD)pWordNode->pItemData;
+/*
+    #define TXVWORDF_GLUEWITHNEXT       1       // escape
+    #define TXVWORDF_LINEBREAK          2       // \n
+    #define TXVWORDF_LINEFEED           4       // \r
+*/
+                        BOOL    fNextWord = FALSE;
+
+                        if (pWordThis->cEscapeCode)
+                        {
+                            // pseudo-word for escape sequence:
+                            // process...
+                            ProcessEscapes((PSZ*)&pWordThis->pStart,
+                                           pxfd,
+                                           &flbuf,
+                                           TRUE);
+
+                            // append this sequence only if it's needed
+                            // for painting (list markers etc.)
+                            if (pWordThis->fPaintEscapeWord)
+                            {
+                                pWordThis->lX = flbuf.lXCurrent;
+                                pWordThis->pvRectangle = (PVOID)pRect;
+                                lstAppendItem(&pRect->llWords, pWordThis);
+                                ulWordsInThisRect++;
+                            }
+
+                            fNextWord = TRUE;
+                        }
+                        else
+                        {
+                            BOOL    fWordWrapped = FALSE;
+
+                            // not escape sequence, but real word: format...
+                            // is word-wrapping on?
+                            if (flbuf.fmtp.fWordWrap)
+                            {
+                                // yes: check if the word still fits
+                                if (    (flbuf.lXCurrent + pWordThis->ulCXWithSpaces // ###
+                                             > prclView->xRight)
+                                             // > ulWinCX)
+                                        // but always add the first word in the rectangle,
+                                        // because otherwise we get infinite loops
+                                     && (ulWordsInThisRect > 0)
+                                   )
+                                    // no:
+                                    fWordWrapped = TRUE;
+                            }
+
+                            if (fWordWrapped)
+                                // start a new rectangle with the current word:
+                                fWords2Go = FALSE;
+                                        // and do _not_ advance to the next word,
+                                        // but start with this word for the next
+                                        // rectangle...
+                            else
+                            {
+                                // add this word to the rectangle:
+
+                                // store current X pos in word
+                                pWordThis->lX = flbuf.lXCurrent;
+
+                                // increase current X pos by word width
+                                flbuf.lXCurrent += pWordThis->ulCXWithSpaces;
+
+                                // store word in rectangle
+                                pWordThis->pvRectangle = (PVOID)pRect;
+                                lstAppendItem(&pRect->llWords, pWordThis);
+                                ulWordsInThisRect++;
+
+                                // store highest word width found for this rect
+                                if (pWordThis->ulCY > lWordsMaxCY)
+                                    lWordsMaxCY = pWordThis->ulCY;
+
+                                // store highest base line ofs found for this rect
+                                if (pWordThis->ulBaseLineOfs > pRect->ulMaxBaseLineOfs)
+                                    pRect->ulMaxBaseLineOfs = pWordThis->ulBaseLineOfs;
+
+                                // go for next word in any case
+                                fNextWord = TRUE;
+                            } // end if (!fBreakThisWord)
+
+                            // now check: add more words to this rectangle?
+                            if (    (pWordThis->ulFlags == TXVWORDF_LINEBREAK)
+                                        // no if linebreak found
+                                 || (pWordThis->ulFlags == TXVWORDF_LINEFEED)
+                                        // no if linefeed found
+                                 || (!fWords2Go)
+                                        // no if we're out of words or
+                                        // word-break was forced
+                               )
+                            {
+                                // no: finish up this rectangle...
+
+                                // xLeft has been set on top
+                                pRect->rcl.xRight = flbuf.lXCurrent;
+                                pRect->rcl.yTop = lCurrentYTop - ulYPre;
+                                pRect->rcl.yBottom = pRect->rcl.yTop - lWordsMaxCY;
+
+                                // decrease current y top for next line
+                                lCurrentYTop = pRect->rcl.yBottom;
+                                if (!fRects2Go)
+                                    // we're done completely:
+                                    // add another one
+                                    lCurrentYTop -= lWordsMaxCY;
+
+                                if (fWordWrapped)
+                                    // starting with wrapped word in next line:
+                                    ulYPre = 0;
+                                else
+                                    if (pWordThis->ulFlags == TXVWORDF_LINEFEED)
+                                        ulYPre = 0;
+                                    else if (pWordThis->ulFlags == TXVWORDF_LINEBREAK)
+                                    {
+                                        // line break:
+                                        // set y-pre for next loop
+                                        ulYPre = flbuf.fmtp.lSpaceBefore;
+                                        // and add paragraph post-y
+                                        lCurrentYTop -= flbuf.fmtp.lSpaceAfter;
+                                    }
+
+                                // update x extents
+                                if (pRect->rcl.xRight > pxfd->ulViewportCX)
+                                    pxfd->ulViewportCX = pRect->rcl.xRight;
+
+                                // and quit the inner loop
+                                fWords2Go = FALSE;
+                            } // end finish up rectangle
+                        } // end else if (pWordThis->fIsEscapeSequence)
+
+                        if (fNextWord)
+                        {
+                            pWordNode = pWordNode->pNext;
+                            if (!pWordNode)
+                            {
+                                // no more to go:
+                                // quit
+                                fWords2Go = FALSE;
+                                fRects2Go = FALSE;
+                            }
+                        }
+                    } // end while (fWords2Go)
+
+                    // store rectangle
+                    lstAppendItem(&pxfd->llRectangles, pRect);
+                }
+
+                // lCurrentYTop now has the bottommost point we've used;
+                // store this as viewport (this might be negative)
+                pxfd->ulViewportCY = lOrigYTop - lCurrentYTop;
+            }
+        }
+    }
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Device-independent text painting                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ DrawListMarker:
+ *
+ *@@added V0.9.3 (2000-05-17) [umoeller]
+ */
+
+VOID DrawListMarker(HPS hps,
+                    PRECTL prclLine,        // current line rectangle
+                    PTXVWORD pWordThis,    // current word
+                    LONG lViewXOfs)         // in: x offset to paint; 0 means rightmost
+{
+    POINTL ptl;
+
+    ULONG ulBulletSize = pWordThis->lPointSize * 2 / 3; // 2/3 of point size
+
+    ARCPARAMS   arcp = {1, 1, 0, 0};
+
+    // pWordThis->pStart points to the \xFF character;
+    // next is the "marker" escape (\x23),
+    // next is the marker type
+    CHAR cBulletType =  *((pWordThis->pStart) + 2) ;
+
+    switch (cBulletType)
+    {
+        case 2:     // square (filled box)
+            ptl.x = pWordThis->lX - lViewXOfs;
+            // center bullet vertically
+            ptl.y = prclLine->yBottom
+                    +   (   (prclLine->yTop - prclLine->yBottom) // height
+                             - ulBulletSize
+                        ) / 2;
+
+            GpiMove(hps, &ptl);
+            ptl.x += ulBulletSize;
+            ptl.y += ulBulletSize;
+            GpiBox(hps, DRO_FILL, &ptl, 0, 0);
+        break;
+
+        default: // case 1:     // disc (filled circle)
+            ptl.x = pWordThis->lX - lViewXOfs;
+            // center bullet vertically;
+            // the arc is drawn with the current position in its center
+            ptl.y = prclLine->yBottom
+                    +   (   (prclLine->yTop - prclLine->yBottom) // height
+                            / 2
+                        );
+
+            GpiSetArcParams(hps, &arcp);
+            GpiMove(hps, &ptl);
+            GpiFullArc(hps,
+                       (cBulletType == 3)
+                            ? DRO_OUTLINE
+                            : DRO_FILL,
+                       MAKEFIXED(ulBulletSize / 2,      // radius!
+                                 0));
+        break;
+
+    }
+}
+
+/*
+ *@@ txvPaintText:
+ *      device-independent function for painting.
+ *      This can only be called after the text has
+ *      been formatted (using txvFormatText).
+ *
+ *      This only paints rectangles which are within
+ *      prcl2Paint.
+ *
+ *      -- For WM_PAINT, set this to the
+ *         update rectangle, and set fPaintHalfLines
+ *         to TRUE.
+ *
+ *      -- For printing, set this to the page rectangle,
+ *         and set fPaintHalfLines to FALSE.
+ *
+ *      All coordinates are in world space (PU_PELS).
+ *
+ *@@changed V0.9.3 (2000-05-05) [umoeller]: fixed wrong visible lines calculations; great speedup painting!
+ *@@changed V0.9.3 (2000-05-06) [umoeller]: now using gpihCharStringPosAt
+ */
+
+BOOL txvPaintText(HAB hab,
+                  HPS hps,             // in: window or printer PS
+                  PXFORMATDATA pxfd,
+                  PRECTL prcl2Paint,   // in: invalid rectangle to be drawn,
+                                       // can be NULL to paint all
+                  LONG lViewXOfs,      // in: x offset to paint; 0 means rightmost
+                  PLONG plViewYOfs,    // in: y offset to paint; 0 means _top_most;
+                                       // out: y offset which should be passed to next call
+                                       // (if TRUE is returned and fPaintHalfLines == FALSE)
+                  BOOL fPaintHalfLines, // in: if FALSE, lines which do not fully fit on
+                                       // the page are dropped (useful for printing)
+                  PULONG pulLineIndex) // in: line to start painting with;
+                                       // out: next line to paint, if any
+                                       // (if TRUE is returned and fPaintHalfLines == FALSE)
+{
+    BOOL    brc = FALSE,
+            fAnyLinesPainted = FALSE;
+    ULONG   ulCurrentLineIndex = *pulLineIndex;
+    // LONG    lViewYOfsSaved = *plViewYOfs;
+    PLISTNODE pRectNode = lstNodeFromIndex(&pxfd->llRectangles,
+                                           ulCurrentLineIndex);
+
+    LONG    lcidLast = -99;
+    LONG    lPointSizeLast = -99;
+
+    while (pRectNode)
+    {
+        PTXVRECTANGLE   pLineRcl = (PTXVRECTANGLE)pRectNode->pItemData;
+        BOOL            fPaintThis = FALSE;
+
+        // compose rectangle to draw for this line
+        RECTL           rclLine;
+        rclLine.xLeft = pLineRcl->rcl.xLeft - lViewXOfs;
+        rclLine.xRight = pLineRcl->rcl.xRight - lViewXOfs;
+        rclLine.yBottom = pLineRcl->rcl.yBottom + *plViewYOfs;
+        rclLine.yTop = pLineRcl->rcl.yTop + *plViewYOfs;
+
+        /* if (pmpf)
+        {
+            CHAR szTemp[100];
+            ULONG cb = min(pLineRcl->cLineChars, 99);
+            strhncpy0(szTemp, pLineRcl->pStartOfLine, cb);
+
+            _Pmpf(("Checking line %d: '%s'",
+                    ulCurrentLineIndex,
+                    szTemp));
+
+            _Pmpf(("    (yB stored %d -> in HPS %d against win yB %d)",
+                    pLineRcl->rcl.yBottom,
+                    rclLine.yBottom,
+                    prcl2Paint->yBottom));
+        } */
+
+        if (prcl2Paint == NULL)
+            // draw all:
+            fPaintThis = TRUE;
+        else
+        {
+            BOOL fBottomInPaint = (    (rclLine.yBottom >= prcl2Paint->yBottom)
+                                    && (rclLine.yBottom <= prcl2Paint->yTop)
+                                  );
+            BOOL fTopInPaint =    (    (rclLine.yTop >= prcl2Paint->yBottom)
+                                    && (rclLine.yTop <= prcl2Paint->yTop)
+                                  );
+
+            if ((fBottomInPaint) && (fTopInPaint))
+                // both in update rect:
+                fPaintThis = TRUE;
+            else
+                if (fPaintHalfLines)
+                {
+                    if ((fBottomInPaint) || (fTopInPaint))
+                    // only one in update rect:
+                        fPaintThis = TRUE;
+                    else
+                        // now, for very small update rectangles,
+                        // especially with slow scrolling,
+                        // we can have the case that the paint rectangle
+                        // is only a few pixels high so that the top of
+                        // the line is above the repaint, and the bottom
+                        // of the line is below it!
+                        if (    (rclLine.yTop >= prcl2Paint->yTop)
+                             && (rclLine.yBottom <= prcl2Paint->yBottom)
+                           )
+                            fPaintThis = TRUE;
+                }
+        }
+
+        if (fPaintThis)
+        {
+            // rectangle invalid: paint this rectangle
+            // by going thru the member words
+            PLISTNODE   pWordNode = lstQueryFirstNode(&pLineRcl->llWords);
+
+            POINTL ptlStart;
+
+            while (pWordNode)
+            {
+                PTXVWORD pWordThis = (PTXVWORD)pWordNode->pItemData;
+                ULONG flOptions = pWordThis->flOptions;
+
+                if (pWordThis->usAnchor)
+                    flOptions |= CHS_UNDERSCORE;
+
+                // x start: this word's X coordinate
+                ptlStart.x = pWordThis->lX - lViewXOfs;
+                // y start: bottom line of rectangle plus highest
+                // base line offset found in all words (format step 2)
+                ptlStart.y = rclLine.yBottom + pLineRcl->ulMaxBaseLineOfs;
+                         // pWordThis->ulBaseLineOfs;
+
+                // set font for subsequent calculations,
+                // if changed (this includes the first call)
+                if (lcidLast != pWordThis->lcid)
+                {
+                    GpiSetCharSet(hps, pWordThis->lcid);
+                    lcidLast = pWordThis->lcid;
+                    // force recalc of point size
+                    lPointSizeLast = -99;
+                }
+
+                if (lPointSizeLast != pWordThis->lPointSize)
+                {
+                    if (pWordThis->lPointSize)
+                        // is outline font:
+                        gpihSetPointSize(hps, pWordThis->lPointSize);
+                    lPointSizeLast = pWordThis->lPointSize;
+                }
+
+                if (!pWordThis->cEscapeCode)
+                    // regular word:
+                    gpihCharStringPosAt(hps,
+                                        &ptlStart,
+                                        &rclLine,
+                                        flOptions,
+                                        pWordThis->cChars,
+                                        (PSZ)pWordThis->pStart);
+                else
+                {
+                    // check escape code
+                    switch (pWordThis->cEscapeCode)
+                    {
+                        case 0x23:
+                            // escape to be painted:
+                            DrawListMarker(hps,
+                                           &rclLine,
+                                           pWordThis,
+                                           lViewXOfs);
+                        break;
+                    }
+                }
+
+                // ptlStart.x += pWordThis->ulCXWithSpaces;
+
+                fAnyLinesPainted = TRUE;
+                pWordNode = pWordNode->pNext;
+            }
+
+            /* {
+                LONG lColor = GpiQueryColor(hps);
+                POINTL ptl2;
+                GpiSetColor(hps, RGBCOL_RED);
+                ptl2.x = rclLine.xLeft;
+                ptl2.y = rclLine.yBottom;
+                GpiMove(hps, &ptl2);
+                ptl2.x = rclLine.xRight;
+                ptl2.y = rclLine.yTop;
+                GpiBox(hps,
+                       DRO_OUTLINE,
+                       &ptl2,
+                       0, 0);
+                GpiSetColor(hps, lColor);
+            } */
+
+        }
+        else
+        {
+            // this line is no longer fully visible:
+
+            if (fAnyLinesPainted)
+            {
+                // we had painted lines already:
+                // this means that all the following lines are
+                // too far below the window, so quit
+                /* if (pmpf)
+                    _Pmpf(("Quitting with line %d (xL = %d yB = %d)",
+                            ulCurrentLineIndex, rclLine.xLeft, rclLine.yBottom)); */
+
+                *pulLineIndex = ulCurrentLineIndex;
+                if (pRectNode->pNext)
+                {
+                    // another line to paint:
+                    PTXVRECTANGLE   pLineRcl2 = (PTXVRECTANGLE)pRectNode->pNext->pItemData;
+                    // return TRUE
+                    brc = TRUE;
+                    // and set *plViewYOfs to the top of
+                    // the next line, which wasn't visible
+                    // on the page any more
+                    *plViewYOfs = pLineRcl2->rcl.yTop + *plViewYOfs;
+                }
+                break;
+            }
+            // else no lines painted yet:
+            // go for next node, because we're still above the visible window
+        }
+
+        // next line
+        pRectNode = pRectNode->pNext;
+        // raise index to return
+        ulCurrentLineIndex++;
+    }
+
+    if (!fAnyLinesPainted)
+        brc = FALSE;
+
+    return (brc);
+}
+
+/*
+ *@@ txvFindWordFromPoint:
+ *      returns the list node of the word under the
+ *      given point. The list node is from the global
+ *      words list in pxfd.
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+PLISTNODE txvFindWordFromPoint(PXFORMATDATA pxfd,
+                               PPOINTL pptl)
+{
+    PLISTNODE pWordNodeFound = NULL;
+
+    PLISTNODE pRectangleNode = lstQueryFirstNode(&pxfd->llRectangles);
+    while ((pRectangleNode) && (!pWordNodeFound))
+    {
+        PTXVRECTANGLE prclThis = (PTXVRECTANGLE)pRectangleNode->pItemData;
+        if (    (pptl->x >= prclThis->rcl.xLeft)
+             && (pptl->x <= prclThis->rcl.xRight)
+             && (pptl->y >= prclThis->rcl.yBottom)
+             && (pptl->y <= prclThis->rcl.yTop)
+           )
+        {
+            // cool, we found the rectangle:
+            // now go thru the words in this rectangle
+            PLISTNODE pWordNode = lstQueryFirstNode(&prclThis->llWords);
+            while (pWordNode)
+            {
+                PTXVWORD pWordThis = (PTXVWORD)pWordNode->pItemData;
+
+                if (    (pptl->x >= pWordThis->lX)
+                     && (pptl->x <= pWordThis->lX + pWordThis->ulCXWithSpaces)
+                   )
+                {
+                    pWordNodeFound = pWordNode;
+                    break;
+                }
+                pWordNode = pWordNode->pNext;
+            }
+        }
+        pRectangleNode = pRectangleNode->pNext;
+    }
+
+    return (pWordNodeFound);
+}
+
+/*
+ *@@ txvFindWordFromAnchor:
+ *      returns the list node from the global words list
+ *      BEFORE the word which represents the escape sequence
+ *      containing the specified anchor name.
+ *
+ *@@added V0.9.4 (2000-06-12) [umoeller]
+ */
+
+PLISTNODE txvFindWordFromAnchor(PXFORMATDATA pxfd,
+                                const char *pszAnchorName)
+{
+    PLISTNODE pNodeFound = NULL;
+
+    ULONG cbAnchorName = strlen(pszAnchorName);
+
+    PLISTNODE pWordNode = lstQueryFirstNode(&pxfd->llWords);
+    while ((pWordNode) && (!pNodeFound))
+    {
+        PTXVWORD pWordThis = (PTXVWORD)pWordNode->pItemData;
+        if (pWordThis->cEscapeCode == 7)
+        {
+            // this word is an anchor escape sequence:
+            if (strnicmp(pszAnchorName, (pWordThis->pStart + 2), cbAnchorName) == 0)
+            {
+                // matches: check length
+                if (*(pWordThis->pStart + 2 + cbAnchorName) == (char)0xFF)
+                    // OK:
+                    pNodeFound = pWordNode;
+            }
+        }
+
+        pWordNode = pWordNode ->pNext;
+    }
+
+    if (pNodeFound)
+    {
+        // anchor found:
+        // go backwords in word list until we find a "real" word
+        // which is no escape sequence
+        while (pNodeFound)
+        {
+            PTXVWORD pWordThis = (PTXVWORD)pNodeFound->pItemData;
+            if (pWordThis->cEscapeCode)
+                pNodeFound = pNodeFound->pPrevious;
+            else
+                break;
+        }
+    }
+
+    return (pNodeFound);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Window-dependent functions                                     *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ TEXTVIEWWINDATA:
+ *      view control-internal structure, stored in
+ *      QWL_USER at fnwpTextView.
+ *      This is device-dependent on the text view
+ *      window.
+ */
+
+typedef struct _TEXTVIEWWINDATA
+{
+    HAB     hab;                // anchor block (for speed)
+
+    HDC     hdc;
+    HPS     hps;
+
+    LONG    lBackColor,
+            lForeColor;
+
+    XTEXTVIEWCDATA cdata;       // control data, as passed to WM_CREATE
+
+    XFORMATDATA xfd;
+
+    HWND    hwndVScroll,        // vertical scroll bar
+            hwndHScroll;        // horizontal scroll bar
+
+    BOOL    fVScrollVisible,    // TRUE if vscroll is currently used
+            fHScrollVisible;    // TRUE if hscroll is currently used
+
+    RECTL   rclViewReal,        // window rect as returned by WinQueryWindowRect
+                                // (top right point is inclusive!!)
+            rclViewPaint,       // same as rclViewReal, but excluding scroll bars
+            rclViewText;        // same as rclViewPaint, but excluding cdata borders
+
+    LONG    lViewXOfs,          // pixels that we have scrolled to the RIGHT; 0 means very left
+            lViewYOfs;          // pixels that we have scrolled to the BOTTOM; 0 means very top
+
+    BOOL    fAcceptsPresParamsNow; // TRUE after first WM_PAINT
+
+    // anchor clicking
+    PLISTNODE   pWordNodeFirstInAnchor;  // points to first word which belongs to anchor
+    USHORT      usLastAnchorClicked;    // last anchor which was clicked (1-0xFFFF)
+} TEXTVIEWWINDATA, *PTEXTVIEWWINDATA;
+
+#define ID_VSCROLL      100
+#define ID_HSCROLL      101
+
+/*
+ *@@ UpdateTextViewPresData:
+ *      called from WM_CREATE and WM_PRESPARAMCHANGED
+ *      in fnwpTextView to update the TEXTVIEWWINDATA
+ *      from the window's presparams. This calls
+ *      txvSetDefaultFormat in turn.
+ */
+
+VOID UpdateTextViewPresData(HWND hwndTextView,
+                            PTEXTVIEWWINDATA ptxvd)
+{
+    PSZ pszFont;
+    ptxvd->lBackColor = winhQueryPresColor(hwndTextView,
+                                           PP_BACKGROUNDCOLOR,
+                                           FALSE,             // no inherit
+                                           SYSCLR_DIALOGBACKGROUND);
+    ptxvd->lForeColor = winhQueryPresColor(hwndTextView,
+                                           PP_FOREGROUNDCOLOR,
+                                           FALSE,             // no inherit
+                                           SYSCLR_WINDOWSTATICTEXT);
+
+    if ((pszFont = winhQueryWindowFont(hwndTextView)))
+    {
+        ULONG ulSize;
+        PSZ pszFaceName;
+        // _Pmpf(("font: %s", pszFont));
+        if (gpihSplitPresFont(pszFont,
+                              &ulSize,
+                              &pszFaceName))
+        {
+            txvSetFormatFont(ptxvd->hps,
+                             &ptxvd->xfd.fmtcStandard,
+                             ulSize,
+                             pszFaceName);
+        }
+        free(pszFont);
+    }
+}
+
+/*
+ *@@ AdjustViewRects:
+ *      updates the internal size-dependent structures
+ *      and positions the scroll bars.
+ *
+ *      This is device-dependent for the text view
+ *      control and must be called before FormatText2Screen
+ *      so that the view rectangles get calculated right.
+ *
+ *      Required input in TEXTVIEWWINDATA:
+ *
+ *      -- rclViewReal: the actual window dimensions.
+ *
+ *      -- cdata: control data.
+ *
+ *      Output from this function in TEXTVIEWWINDATA:
+ *
+ *      -- rclViewPaint: the paint subrectangle (which
+ *         is rclViewReal minus scrollbars, if any).
+ *
+ *      -- rclViewText: the text subrectangle (which
+ *         is rclViewPaint minus borders).
+ */
+
+VOID AdjustViewRects(HWND hwndTextView,
+                     PTEXTVIEWWINDATA ptxvd)
+{
+    ULONG ulScrollCX = WinQuerySysValue(HWND_DESKTOP, SV_CXVSCROLL),
+          ulScrollCY = WinQuerySysValue(HWND_DESKTOP, SV_CYHSCROLL),
+          ulOfs;
+
+    // calculate rclViewPaint:
+    // 1) left
+    ptxvd->rclViewPaint.xLeft = ptxvd->rclViewReal.xLeft;
+    // 2) bottom
+    ptxvd->rclViewPaint.yBottom = ptxvd->rclViewReal.yBottom;
+    if (ptxvd->fHScrollVisible)
+        // if we have a horizontal scroll bar at the bottom,
+        // raise bottom by its height
+        ptxvd->rclViewPaint.yBottom += ulScrollCY;
+    // 3) right
+    ptxvd->rclViewPaint.xRight = ptxvd->rclViewReal.xRight;
+    if (ptxvd->fVScrollVisible)
+        // if we have a vertical scroll bar at the right,
+        // subtract its width from the right
+        ptxvd->rclViewPaint.xRight -= ulScrollCX;
+    ptxvd->rclViewPaint.yTop = ptxvd->rclViewReal.yTop;
+
+    // calculate rclViewText from that
+    ptxvd->rclViewText.xLeft = ptxvd->rclViewPaint.xLeft + ptxvd->cdata.ulXBorder;
+    ptxvd->rclViewText.yBottom = ptxvd->rclViewPaint.yBottom + ptxvd->cdata.ulYBorder;
+    ptxvd->rclViewText.xRight = ptxvd->rclViewPaint.xRight - ptxvd->cdata.ulXBorder;
+    ptxvd->rclViewText.yTop = ptxvd->rclViewPaint.yTop - ptxvd->cdata.ulXBorder;
+
+    // now reposition scroll bars; their sizes may change
+    // if either the vertical or horizontal scroll bar has
+    // popped up or been hidden
+    if (ptxvd->cdata.flStyle & XTXF_VSCROLL)
+    {
+        // vertical scroll bar enabled:
+        ulOfs = 0;
+        if (ptxvd->fHScrollVisible)
+            ulOfs = ulScrollCX;
+        WinSetWindowPos(ptxvd->hwndVScroll,
+                        HWND_TOP,
+                        ptxvd->rclViewReal.xRight - ulScrollCX,
+                        ulOfs,          // y
+                        ulScrollCX,     // cx
+                        ptxvd->rclViewReal.yTop - ulOfs,     // cy
+                        SWP_MOVE | SWP_SIZE);
+    }
+
+    if (ptxvd->cdata.flStyle & XTXF_HSCROLL)
+    {
+        ulOfs = 0;
+        if (ptxvd->fVScrollVisible)
+            ulOfs = ulScrollCX;
+        WinSetWindowPos(ptxvd->hwndHScroll,
+                        HWND_TOP,
+                        0,
+                        0,
+                        ptxvd->rclViewReal.xRight - ulOfs,     // cx
+                        ulScrollCY, // cy
+                        SWP_MOVE | SWP_SIZE);
+    }
+}
+
+/*
+ *@@ FormatText2Screen:
+ *      device-dependent version of text formatting
+ *      for the text view window. This calls txvFormatText
+ *      in turn and updates the view's scroll bars.
+ *
+ *@@changed V0.9.3 (2000-05-05) [umoeller]: fixed buggy vertical scroll bars
+ */
+
+VOID FormatText2Screen(HWND hwndTextView,
+                       PTEXTVIEWWINDATA ptxvd,
+                       BOOL fAlreadyRecursing,  // in: set this to FALSE when calling
+                       BOOL fFullRecalc)
+{
+    ULONG   ulWinCX,
+            ulWinCY;
+
+    // call device-independent formatter with the
+    // window presentation space
+    txvFormatText(ptxvd->hps,
+                  &ptxvd->xfd,
+                  &ptxvd->rclViewText,
+                  fFullRecalc);
+
+    ulWinCY = (ptxvd->rclViewText.yTop - ptxvd->rclViewText.yBottom);
+
+    if (ptxvd->lViewYOfs < 0)
+        ptxvd->lViewYOfs = 0;
+    if (ptxvd->lViewYOfs > ((LONG)ptxvd->xfd.ulViewportCY - ulWinCY))
+        ptxvd->lViewYOfs = (LONG)ptxvd->xfd.ulViewportCY - ulWinCY;
+
+    // vertical scroll bar enabled at all?
+    if (ptxvd->cdata.flStyle & XTXF_VSCROLL)
+    {
+        BOOL fEnabled = winhUpdateScrollBar(ptxvd->hwndVScroll,
+                                            ulWinCY,
+                                            ptxvd->xfd.ulViewportCY,
+                                            ptxvd->lViewYOfs,
+                                            (ptxvd->cdata.flStyle & XTXF_AUTOVHIDE));
+        // is auto-hide on?
+        if (ptxvd->cdata.flStyle & XTXF_AUTOVHIDE)
+        {
+            // yes, auto-hide on: did visibility change?
+            if (fEnabled != ptxvd->fVScrollVisible)
+                // visibility changed:
+                // if we're not already recursing,
+                // force calling ourselves again
+                if (!fAlreadyRecursing)
+                {
+                    ptxvd->fVScrollVisible = fEnabled;
+                    AdjustViewRects(hwndTextView,
+                                    ptxvd);
+                    FormatText2Screen(hwndTextView,
+                                      ptxvd,
+                                      TRUE,   // fAlreadyRecursing
+                                      FALSE);   // quick format
+                }
+        }
+    }
+
+    ulWinCX = (ptxvd->rclViewText.xRight - ptxvd->rclViewText.xLeft);
+
+    // horizontal scroll bar enabled at all?
+    if (ptxvd->cdata.flStyle & XTXF_HSCROLL)
+    {
+        BOOL fEnabled = winhUpdateScrollBar(ptxvd->hwndHScroll,
+                                            ulWinCX,
+                                            ptxvd->xfd.ulViewportCX,
+                                            ptxvd->lViewXOfs,
+                                            (ptxvd->cdata.flStyle & XTXF_AUTOHHIDE));
+        // is auto-hide on?
+        if (ptxvd->cdata.flStyle & XTXF_AUTOHHIDE)
+        {
+            // yes, auto-hide on: did visibility change?
+            if (fEnabled != ptxvd->fHScrollVisible)
+                // visibility changed:
+                // if we're not already recursing,
+                // force calling ourselves again (at the bottom)
+                if (!fAlreadyRecursing)
+                {
+                    ptxvd->fHScrollVisible = fEnabled;
+                    AdjustViewRects(hwndTextView,
+                                    ptxvd);
+                }
+        }
+    }
+
+    WinInvalidateRect(hwndTextView, NULL, FALSE);
+}
+
+/*
+ *@@ PaintViewText2Screen:
+ *      device-dependent version of text painting
+ *      for the text view window. This calls txvPaintText
+ *      in turn and updates the view's scroll bars.
+ */
+
+VOID PaintViewText2Screen(PTEXTVIEWWINDATA ptxvd,
+                          PRECTL prcl2Paint)  // in: invalid rectangle, can be NULL == paint all
+{
+    ULONG   ulLineIndex = 0;
+    LONG    lYOfs = ptxvd->lViewYOfs;
+    txvPaintText(ptxvd->hab,
+                 ptxvd->hps,        // paint PS: screen
+                 &ptxvd->xfd,       // formatting data
+                 prcl2Paint,        // update rectangle given to us
+                 ptxvd->lViewXOfs,  // current X scrolling offset
+                 &lYOfs,            // current Y scrolling offset
+                 TRUE,              // draw even partly visible lines
+                 &ulLineIndex);
+}
+
+/*
+ *@@ PaintViewFocus:
+ *      paint a focus rectangle.
+ */
+
+VOID PaintViewFocus(HPS hps,
+                    PTEXTVIEWWINDATA ptxvd,
+                    BOOL fFocus)
+{
+    POINTL  ptl;
+    HRGN    hrgn;
+    GpiSetClipRegion(hps,
+                     NULLHANDLE,
+                     &hrgn);
+    GpiSetColor(hps,
+                (fFocus)
+                    ? WinQuerySysColor(HWND_DESKTOP, SYSCLR_HILITEBACKGROUND, 0)
+                    : ptxvd->lBackColor);
+    GpiSetLineType(hps, LINETYPE_DOT);
+    ptl.x = ptxvd->rclViewPaint.xLeft;
+    ptl.y = ptxvd->rclViewPaint.yBottom;
+    GpiMove(hps, &ptl);
+    ptl.x = ptxvd->rclViewPaint.xRight - 1;
+    ptl.y = ptxvd->rclViewPaint.yTop - 1;
+    GpiBox(hps,
+           DRO_OUTLINE,
+           &ptl,
+           0, 0);
+}
+
+/*
+ *@@ RepaintWord:
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+VOID RepaintWord(PTEXTVIEWWINDATA ptxvd,
+                 PTXVWORD pWordThis,
+                 LONG lColor)
+{
+    POINTL ptlStart;
+    ULONG flOptions = pWordThis->flOptions;
+    PTXVRECTANGLE pLineRcl = (PTXVRECTANGLE)pWordThis->pvRectangle;
+
+    RECTL           rclLine;
+    rclLine.xLeft = pLineRcl->rcl.xLeft - ptxvd->lViewXOfs;
+    rclLine.xRight = pLineRcl->rcl.xRight - ptxvd->lViewXOfs;
+    rclLine.yBottom = pLineRcl->rcl.yBottom + ptxvd->lViewYOfs;
+    rclLine.yTop = pLineRcl->rcl.yTop + ptxvd->lViewYOfs;
+
+    if (pWordThis->usAnchor)
+        flOptions |= CHS_UNDERSCORE;
+
+    // x start: this word's X coordinate
+    ptlStart.x = pWordThis->lX - ptxvd->lViewXOfs;
+    // y start: bottom line of rectangle plus highest
+    // base line offset found in all words (format step 2)
+    ptlStart.y = rclLine.yBottom + pLineRcl->ulMaxBaseLineOfs;
+             // pWordThis->ulBaseLineOfs;
+
+    GpiSetCharSet(ptxvd->hps, pWordThis->lcid);
+    if (pWordThis->lPointSize)
+        // is outline font:
+        gpihSetPointSize(ptxvd->hps, pWordThis->lPointSize);
+
+    GpiSetColor(ptxvd->hps,
+                lColor);
+
+    if (!pWordThis->cEscapeCode)
+        gpihCharStringPosAt(ptxvd->hps,
+                            &ptlStart,
+                            &rclLine,
+                            flOptions,
+                            pWordThis->cChars,
+                            (PSZ)pWordThis->pStart);
+    else
+        // escape to be painted:
+        DrawListMarker(ptxvd->hps,
+                       &rclLine,
+                       pWordThis,
+                       ptxvd->lViewXOfs);
+}
+
+/*
+ *@@ RepaintAnchor:
+ *
+ *@@added V0.9.3 (2000-05-18) [umoeller]
+ */
+
+VOID RepaintAnchor(PTEXTVIEWWINDATA ptxvd,
+                   LONG lColor)
+{
+    PLISTNODE pNode = ptxvd->pWordNodeFirstInAnchor;
+    USHORT usAnchor = 0;
+    while (pNode)
+    {
+        PTXVWORD pWordThis = (PTXVWORD)pNode->pItemData;
+        if (usAnchor == 0)
+            // first loop:
+            usAnchor = pWordThis->usAnchor;
+        else
+            if (pWordThis->usAnchor != usAnchor)
+                // first word with different anchor:
+                break;
+
+        RepaintWord(ptxvd,
+                    pWordThis,
+                    lColor);
+        pNode = pNode->pNext;
+    }
+}
+
+/*
+ *@@ fnwpTextView:
+ *      window procedure for the text view control. This is
+ *      registered with the WC_XTEXTVIEW class in txvRegisterTextView.
+ *      We have a TEXTVIEWWINDATA structure in QWL_USER where we
+ *      store all information we need.
+ *
+ *      The text view control is not a subclassed whatever control,
+ *      but a control implemented from scratch. As a result, we
+ *      had to implement all messages which are usually recognized
+ *      by a control. In detail, we have:
+ *
+ *      -- WM_WINDOWPOSCHANGED: if the control is resized, the
+ *         text is reformatted and the scroll bars are readjusted.
+ *         See AdjustViewRects and txvFormatText.
+ *
+ *      -- WM_PRESPARAMCHANGED: if fonts or colors are dropped
+ *         on the control, we reformat the text also.
+ *
+ *      -- WM_HSCROLL and WM_VSCROLL: this calls winhHandleScrollMsg
+ *         to scroll the window contents.
+ *
+ *      -- WM_BUTTON1DOWN: this sets the focus to the control.
+ *
+ *      -- WM_SETFOCUS: if we receive the focus, we draw a fine
+ *         dotted line in the "selection" color around the text
+ *         window.
+ *
+ *      -- WM_CHAR: if we have the focus, the user can move the
+ *         visible part within the viewport using the usual
+ *         cursor and HOME/END keys.
+ *
+ *      -- WM_MOUSEMOVE: this sends WM_CONTROLPOINTER to the
+ *         owner so the owner can change the mouse pointer.
+ *
+ *      <B>Painting</B>
+ *
+ *      The text view control creates a micro presentation space
+ *      from the window's device context upon WM_CREATE, which is
+ *      stored in TEXTVIEWWINDATA. We do not use WinBeginPaint in
+ *      WM_PAINT, but only the PS we created ourselves. This saves
+ *      us from resetting and researching all the fonts etc., which
+ *      should be speedier.
+ *
+ *@@changed V0.9.3 (2000-05-05) [umoeller]: removed TXM_NEWTEXT; now supporting WinSetWindowText
+ *@@changed V0.9.3 (2000-05-07) [umoeller]: crashed if create param was NULL; fixed
+ */
+
+MRESULT EXPENTRY fnwpTextView(HWND hwndTextView, ULONG msg, MPARAM mp1, MPARAM mp2)
+{
+    MRESULT mrc = 0;
+
+    PTEXTVIEWWINDATA ptxvd = (PTEXTVIEWWINDATA)WinQueryWindowPtr(hwndTextView, QWL_USER);
+
+    switch (msg)
+    {
+        /*
+         * WM_CREATE:
+         *
+         */
+
+        case WM_CREATE:
+        {
+            PXTEXTVIEWCDATA pcd = (PXTEXTVIEWCDATA)mp1;
+                        // can be NULL
+            PCREATESTRUCT pcs = (PCREATESTRUCT)mp2;
+            SBCDATA sbcd;
+
+            mrc = (MPARAM)TRUE;     // error
+
+            // allocate TEXTVIEWWINDATA for QWL_USER
+            ptxvd = (PTEXTVIEWWINDATA)malloc(sizeof(TEXTVIEWWINDATA));
+            if (ptxvd)
+            {
+                SIZEL   szlPage = {0, 0};
+                BOOL    fShow = FALSE;
+                // LONG    lcid = 0;
+
+                // query message queue
+                HMQ hmq = WinQueryWindowULong(hwndTextView, QWL_HMQ);
+                // get codepage of message queue
+                ULONG ulCodepage = WinQueryCp(hmq);
+
+                memset(ptxvd, 0, sizeof(TEXTVIEWWINDATA));
+                WinSetWindowPtr(hwndTextView, QWL_USER, ptxvd);
+
+                ptxvd->hab = WinQueryAnchorBlock(hwndTextView);
+
+                ptxvd->hdc = WinOpenWindowDC(hwndTextView);
+                ptxvd->hps = GpiCreatePS(ptxvd->hab,
+                                         ptxvd->hdc,
+                                         &szlPage, // use same page size as device
+                                         PU_PELS | GPIT_MICRO | GPIA_ASSOC);
+
+                gpihSwitchToRGB(ptxvd->hps);
+
+                // set codepage; GPI defaults this to
+                // the process codepage
+                GpiSetCp(ptxvd->hps, ulCodepage);
+
+                txvInitFormat(&ptxvd->xfd);
+
+                // copy control data, if present
+                if (pcd)
+                    memcpy(&ptxvd->cdata, pcd, pcd->cbData);
+
+                // check values which might cause null divisions
+                if (ptxvd->cdata.ulVScrollLineUnit == 0)
+                    ptxvd->cdata.ulVScrollLineUnit = 15;
+                if (ptxvd->cdata.ulHScrollLineUnit == 0)
+                    ptxvd->cdata.ulHScrollLineUnit = 15;
+
+                ptxvd->fAcceptsPresParamsNow = FALSE;
+
+                // copy window dimensions from CREATESTRUCT
+                ptxvd->rclViewReal.xLeft = 0;
+                ptxvd->rclViewReal.yBottom = 0;
+                ptxvd->rclViewReal.xRight = pcs->cx;
+                ptxvd->rclViewReal.yTop = pcs->cy;
+
+                sbcd.cb = sizeof(SBCDATA);
+                sbcd.sHilite = 0;
+                sbcd.posFirst = 0;
+                sbcd.posLast = 100;
+                sbcd.posThumb = 30;
+                sbcd.cVisible = 50;
+                sbcd.cTotal = 50;
+
+                ptxvd->hwndVScroll = WinCreateWindow(hwndTextView,
+                                                     WC_SCROLLBAR,
+                                                     "",
+                                                     SBS_VERT | SBS_THUMBSIZE | WS_VISIBLE,
+                                                     10, 10,
+                                                     20, 100,
+                                                     hwndTextView,     // owner
+                                                     HWND_TOP,
+                                                     ID_VSCROLL,
+                                                     &sbcd,
+                                                     0);
+                fShow = ((ptxvd->cdata.flStyle & XTXF_VSCROLL) != 0);
+                WinShowWindow(ptxvd->hwndVScroll, fShow);
+                ptxvd->fVScrollVisible = fShow;
+
+                ptxvd->hwndHScroll = WinCreateWindow(hwndTextView,
+                                                     WC_SCROLLBAR,
+                                                     "",
+                                                     SBS_THUMBSIZE | WS_VISIBLE,
+                                                     10, 10,
+                                                     20, 100,
+                                                     hwndTextView,     // owner
+                                                     HWND_TOP,
+                                                     ID_HSCROLL,
+                                                     &sbcd,
+                                                     0);
+                fShow = ((ptxvd->cdata.flStyle & XTXF_HSCROLL) != 0);
+                WinShowWindow(ptxvd->hwndHScroll, fShow);
+                ptxvd->fHScrollVisible = fShow;
+
+                // set "code" format
+                txvSetFormatFont(ptxvd->hps,
+                                 &ptxvd->xfd.fmtcCode,
+                                 6,
+                                 "System VIO");
+
+                // get colors from presparams/syscolors
+                UpdateTextViewPresData(hwndTextView, ptxvd);
+
+                AdjustViewRects(hwndTextView,
+                                ptxvd);
+
+                mrc = (MPARAM)FALSE;        // OK
+            }
+        break; }
+
+        /*
+         * WM_SETWINDOWPARAMS:
+         *      this message sets the window parameters,
+         *      most importantly, the window text.
+         *
+         *      This updates the control.
+         */
+
+        case WM_SETWINDOWPARAMS:
+        {
+            WNDPARAMS *pwndParams = (WNDPARAMS *)mp1;
+            if (pwndParams)
+            {
+                if (pwndParams->fsStatus & WPM_TEXT)
+                {
+                    xstrcpy(&ptxvd->xfd.pszViewText, pwndParams->pszText);
+                    ptxvd->lViewXOfs = 0;
+                    ptxvd->lViewYOfs = 0;
+                    /* ptxvd->fVScrollVisible = FALSE;
+                    ptxvd->fHScrollVisible = FALSE; */
+                    AdjustViewRects(hwndTextView,
+                                    ptxvd);
+                    FormatText2Screen(hwndTextView,
+                                      ptxvd,
+                                      FALSE,
+                                      TRUE);        // full format
+                }
+            }
+        break; }
+
+        /*
+         * WM_WINDOWPOSCHANGED:
+         *
+         */
+
+        case WM_WINDOWPOSCHANGED:
+        {
+            // this msg is passed two SWP structs:
+            // one for the old, one for the new data
+            // (from PM docs)
+            PSWP pswpNew = (PSWP)(mp1);
+            // PSWP pswpOld = pswpNew + 1;
+
+            // resizing?
+            if (pswpNew->fl & SWP_SIZE)
+            {
+                if (ptxvd)
+                {
+                    WinQueryWindowRect(hwndTextView,
+                                       &ptxvd->rclViewReal);
+                    AdjustViewRects(hwndTextView,
+                                    ptxvd);
+                    FormatText2Screen(hwndTextView,
+                                      ptxvd,
+                                      FALSE,
+                                      FALSE);   // quick format
+                }
+            }
+        break; }
+
+        /*
+         * WM_PAINT:
+         *
+         */
+
+        case WM_PAINT:
+        {
+            HRGN    hrgnOldClip;
+
+            /* HPS hps = WinBeginPaint(hwndTextView,
+                                    ptxvd->hps,
+                                    &rcl2Paint);  // store invalid rectangle here
+               */
+
+            if (ptxvd)
+            {
+                RECTL   rclClip;
+                RECTL   rcl2Update;
+
+                // get update rectangle
+                WinQueryUpdateRect(hwndTextView,
+                                   &rcl2Update);
+                // since we're not using WinBeginPaint,
+                // we must validate the update region,
+                // or we'll get bombed with WM_PAINT msgs
+                WinValidateRect(hwndTextView,
+                                NULL,
+                                FALSE);
+
+                // reset clip region to "all"
+                GpiSetClipRegion(ptxvd->hps,
+                                 NULLHANDLE,
+                                 &hrgnOldClip);        // out: old clip region
+                // reduce clip region to update rectangle
+                GpiIntersectClipRectangle(ptxvd->hps,
+                                          &rcl2Update);
+
+                // draw little box at the bottom right
+                // (in between scroll bars) if we have
+                // both vertical and horizontal scroll bars
+                if (    (ptxvd->cdata.flStyle & (XTXF_VSCROLL | XTXF_HSCROLL))
+                        == (XTXF_VSCROLL | XTXF_HSCROLL)
+                     && (ptxvd->fVScrollVisible)
+                     && (ptxvd->fHScrollVisible)
+                   )
+                {
+                    RECTL   rclBox;
+                    rclBox.xLeft = ptxvd->rclViewPaint.xRight;
+                    rclBox.yBottom = 0;
+                    rclBox.xRight = rclBox.xLeft + WinQuerySysValue(HWND_DESKTOP, SV_CXVSCROLL);
+                    rclBox.yTop = WinQuerySysValue(HWND_DESKTOP, SV_CYHSCROLL);
+                    WinFillRect(ptxvd->hps,
+                                &rclBox,
+                                WinQuerySysColor(HWND_DESKTOP,
+                                                 SYSCLR_DIALOGBACKGROUND,
+                                                 0));
+                }
+
+                // paint "view paint" rectangle white;
+                // this can be larger than "view text"
+                WinFillRect(ptxvd->hps,
+                            &ptxvd->rclViewPaint,
+                            ptxvd->lBackColor);
+
+                // now reduce clipping rectangle to "view text" rectangle
+                rclClip.xLeft = ptxvd->rclViewText.xLeft;
+                rclClip.xRight = ptxvd->rclViewText.xRight - 1;
+                rclClip.yBottom = ptxvd->rclViewText.yBottom;
+                rclClip.yTop = ptxvd->rclViewText.yTop - 1;
+                GpiIntersectClipRectangle(ptxvd->hps,
+                                          &rclClip);
+                // finally, draw text lines in invalid rectangle;
+                // this subfunction is smart enough to redraw only
+                // the lines which intersect with rcl2Update
+                GpiSetColor(ptxvd->hps, ptxvd->lForeColor);
+                PaintViewText2Screen(ptxvd,
+                                     &rcl2Update);
+
+                if (WinQueryFocus(HWND_DESKTOP) == hwndTextView)
+                {
+                    // we have the focus:
+                    // reset clip region to "all"
+                    GpiSetClipRegion(ptxvd->hps,
+                                     NULLHANDLE,
+                                     &hrgnOldClip);        // out: old clip region
+                    PaintViewFocus(ptxvd->hps,
+                                   ptxvd,
+                                   TRUE);
+                }
+
+                ptxvd->fAcceptsPresParamsNow = TRUE;
+            }
+
+            // WinEndPaint(hps);
+        break; }
+
+        /*
+         * WM_PRESPARAMCHANGED:
+         *
+         *      Changing the color or font settings
+         *      is equivalent to changing the default
+         *      paragraph format. See TXM_SETFORMAT.
+         */
+
+        case WM_PRESPARAMCHANGED:
+            mrc = WinDefWindowProc(hwndTextView, msg, mp1, mp2);
+            if (ptxvd)
+            {
+                LONG    lPPIndex = (LONG)mp1;
+                switch (lPPIndex)
+                {
+                    case 0:     // layout palette thing dropped
+                    case PP_BACKGROUNDCOLOR:
+                    case PP_FOREGROUNDCOLOR:
+                    case PP_FONTNAMESIZE:
+                        // re-query our presparams
+                        UpdateTextViewPresData(hwndTextView, ptxvd);
+                }
+
+                if (ptxvd->fAcceptsPresParamsNow)
+                    FormatText2Screen(hwndTextView,
+                                      ptxvd,
+                                      FALSE,
+                                      TRUE);    // full reformat
+            }
+        break;
+
+        /*
+         * WM_VSCROLL:
+         *
+         */
+
+        case WM_VSCROLL:
+        {
+            if (ptxvd->fVScrollVisible)
+            {
+                winhHandleScrollMsg(hwndTextView,
+                                    ptxvd->hwndVScroll,
+                                    &ptxvd->lViewYOfs,
+                                    &ptxvd->rclViewText,
+                                    ptxvd->xfd.ulViewportCY,
+                                    ptxvd->cdata.ulVScrollLineUnit,
+                                    msg,
+                                    mp2);
+            }
+        break; }
+
+        /*
+         * WM_HSCROLL:
+         *
+         */
+
+        case WM_HSCROLL:
+        {
+            if (ptxvd->fHScrollVisible)
+            {
+                winhHandleScrollMsg(hwndTextView,
+                                    ptxvd->hwndHScroll,
+                                    &ptxvd->lViewXOfs,
+                                    &ptxvd->rclViewText,
+                                    ptxvd->xfd.ulViewportCX,
+                                    ptxvd->cdata.ulHScrollLineUnit,
+                                    msg,
+                                    mp2);
+            }
+        break; }
+
+        /*
+         * WM_SETFOCUS:
+         *
+         */
+
+        case WM_SETFOCUS:
+        {
+            HPS hps = WinGetPS(hwndTextView);
+            gpihSwitchToRGB(hps);
+            PaintViewFocus(hps,
+                           ptxvd,
+                           (mp2 != 0));
+            WinReleasePS(hps);
+        break; }
+
+        /*
+         * WM_MOUSEMOVE:
+         *      send WM_CONTROLPOINTER to owner.
+         */
+
+        case WM_MOUSEMOVE:
+        {
+            HWND    hwndOwner = WinQueryWindow(hwndTextView, QW_OWNER);
+            if (hwndOwner)
+            {
+                HPOINTER hptrSet
+                    = (HPOINTER)WinSendMsg(hwndOwner,
+                                           WM_CONTROLPOINTER,
+                                           (MPARAM)(LONG)WinQueryWindowUShort(hwndTextView,
+                                                                              QWS_ID),
+                                           (MPARAM)WinQuerySysPointer(HWND_DESKTOP,
+                                                                      SPTR_ARROW,
+                                                                      FALSE));
+                WinSetPointer(HWND_DESKTOP, hptrSet);
+            }
+        break; }
+
+        /*
+         * WM_BUTTON1DOWN:
+         *
+         */
+
+        case WM_BUTTON1DOWN:
+        {
+            POINTL ptlPos;
+            PLISTNODE pWordNodeClicked = NULL;
+
+            ptlPos.x = SHORT1FROMMP(mp1) + ptxvd->lViewXOfs;
+            ptlPos.y = SHORT2FROMMP(mp1) - ptxvd->lViewYOfs;
+
+            if (hwndTextView != WinQueryFocus(HWND_DESKTOP))
+                WinSetFocus(HWND_DESKTOP, hwndTextView);
+
+            ptxvd->usLastAnchorClicked = 0;
+
+            pWordNodeClicked = txvFindWordFromPoint(&ptxvd->xfd,
+                                                    &ptlPos);
+
+            if (pWordNodeClicked)
+            {
+                PTXVWORD pWordClicked = (PTXVWORD)pWordNodeClicked->pItemData;
+
+                // store anchor (can be 0)
+                ptxvd->usLastAnchorClicked = pWordClicked->usAnchor;
+
+                if (pWordClicked->usAnchor)
+                {
+                    // word has an anchor:
+                    PLISTNODE   pNode = pWordNodeClicked;
+
+                    // reset first word of anchor
+                    ptxvd->pWordNodeFirstInAnchor = NULL;
+
+                    // go back to find the first word which has this anchor,
+                    // because we need to repaint them all
+                    while (pNode)
+                    {
+                        PTXVWORD pWordThis = (PTXVWORD)pNode->pItemData;
+                        if (pWordThis->usAnchor == pWordClicked->usAnchor)
+                        {
+                            // still has same anchor:
+                            // go for previous
+                            ptxvd->pWordNodeFirstInAnchor = pNode;
+                            pNode = pNode->pPrevious;
+                        }
+                        else
+                            // different anchor:
+                            // pNodeFirst points to first node with same anchor now
+                            break;
+                    }
+
+                    RepaintAnchor(ptxvd,
+                                  RGBCOL_RED);
+                }
+            }
+
+            WinSetCapture(HWND_DESKTOP, hwndTextView);
+            mrc = (MPARAM)TRUE;
+        break; }
+
+        /*
+         * WM_BUTTON1UP:
+         *
+         */
+
+        case WM_BUTTON1UP:
+        {
+            POINTL      ptlPos;
+            // PTXVWORD    pWordClicked = NULL;
+            HWND        hwndOwner = NULLHANDLE;
+
+            ptlPos.x = SHORT1FROMMP(mp1) + ptxvd->lViewXOfs;
+            ptlPos.y = SHORT2FROMMP(mp1) - ptxvd->lViewYOfs;
+            WinSetCapture(HWND_DESKTOP, NULLHANDLE);
+
+            if (ptxvd->usLastAnchorClicked)
+            {
+                RepaintAnchor(ptxvd,
+                              ptxvd->lForeColor);
+
+                // nofify owner
+                hwndOwner = WinQueryWindow(hwndTextView, QW_OWNER);
+                if (hwndOwner)
+                    WinPostMsg(hwndOwner,
+                               WM_CONTROL,
+                               MPFROM2SHORT(WinQueryWindowUShort(hwndTextView,
+                                                                 QWS_ID),
+                                            TXVN_LINK),
+                               (MPARAM)(ULONG)(ptxvd->usLastAnchorClicked));
+            }
+
+            mrc = (MPARAM)TRUE;
+        break; }
+
+        /*
+         * WM_CHAR:
+         *
+         */
+
+        case WM_CHAR:
+        {
+            BOOL    fDefProc = TRUE;
+            USHORT usFlags    = SHORT1FROMMP(mp1);
+            // USHORT usch       = SHORT1FROMMP(mp2);
+            USHORT usvk       = SHORT2FROMMP(mp2);
+
+            if (usFlags & KC_VIRTUALKEY)
+            {
+                ULONG   ulMsg = 0;
+                USHORT  usID = ID_VSCROLL;
+                SHORT   sPos = 0;
+                SHORT   usCmd = 0;
+                fDefProc = FALSE;
+
+                switch (usvk)
+                {
+                    case VK_UP:
+                        ulMsg = WM_VSCROLL;
+                        usCmd = SB_LINEUP;
+                    break;
+
+                    case VK_DOWN:
+                        ulMsg = WM_VSCROLL;
+                        usCmd = SB_LINEDOWN;
+                    break;
+
+                    case VK_RIGHT:
+                        ulMsg = WM_HSCROLL;
+                        usCmd = SB_LINERIGHT;
+                    break;
+
+                    case VK_LEFT:
+                        ulMsg = WM_HSCROLL;
+                        usCmd = SB_LINELEFT;
+                    break;
+
+                    case VK_PAGEUP:
+                        ulMsg = WM_VSCROLL;
+                        if (usFlags & KC_CTRL)
+                        {
+                            sPos = 0;
+                            usCmd = SB_SLIDERPOSITION;
+                        }
+                        else
+                            usCmd = SB_PAGEUP;
+                    break;
+
+                    case VK_PAGEDOWN:
+                        ulMsg = WM_VSCROLL;
+                        if (usFlags & KC_CTRL)
+                        {
+                            sPos = ptxvd->xfd.ulViewportCY;
+                            usCmd = SB_SLIDERPOSITION;
+                        }
+                        else
+                            usCmd = SB_PAGEDOWN;
+                    break;
+
+                    case VK_HOME:
+                        if (usFlags & KC_CTRL)
+                            // vertical:
+                            ulMsg = WM_VSCROLL;
+                        else
+                            ulMsg = WM_HSCROLL;
+
+                        sPos = 0;
+                        usCmd = SB_SLIDERPOSITION;
+                    break;
+
+                    case VK_END:
+                        if (usFlags & KC_CTRL)
+                        {
+                            // vertical:
+                            ulMsg = WM_VSCROLL;
+                            sPos = ptxvd->xfd.ulViewportCY;
+                        }
+                        else
+                        {
+                            ulMsg = WM_HSCROLL;
+                            sPos = ptxvd->xfd.ulViewportCX;
+                        }
+
+                        usCmd = SB_SLIDERPOSITION;
+                    break;
+
+                    default:
+                        // other:
+                        fDefProc = TRUE;
+                }
+
+                if (    ((usFlags & KC_KEYUP) == 0)
+                     && (ulMsg)
+                   )
+                    WinSendMsg(hwndTextView,
+                               ulMsg,
+                               MPFROMSHORT(usID),
+                               MPFROM2SHORT(sPos,
+                                            usCmd));
+            }
+
+            if (fDefProc)
+                mrc = WinDefWindowProc(hwndTextView, msg, mp1, mp2);
+                        // sends to owner
+            else
+                mrc = (MPARAM)TRUE;
+        break; }
+
+        /*
+         *@@ TXM_QUERYPARFORMAT:
+         *      this msg can be sent to the text view control
+         *      to retrieve the paragraph format with the
+         *      index specified in mp1.
+         *
+         *      Parameters:
+         *      -- ULONG mp1: index of format to query.
+         *              Must be 0 currently for the standard
+         *              paragraph format.
+         *      -- PXFMTPARAGRAPH mp2: pointer to buffer
+         *              which is to receive the formatting
+         *              data.
+         *
+         *      Returns TRUE if copying was successful.
+         *
+         *@@added V0.9.3 (2000-05-06) [umoeller]
+         */
+
+        case TXM_QUERYPARFORMAT:
+        {
+            PXFMTPARAGRAPH pFmt = NULL; // source
+
+            mrc = (MPARAM)FALSE;
+
+            if (mp1 == 0)
+                pFmt = &ptxvd->xfd.fmtpStandard;
+            /* else if ((ULONG)mp1 == 1)
+                pFmt = &ptxvd->xfd.fmtpCode; */
+
+            if ((pFmt) && (mp2))
+            {
+                memcpy(mp2, pFmt, sizeof(XFMTPARAGRAPH));
+                mrc = (MPARAM)TRUE;
+            }
+        break; }
+
+        /*
+         *@@ TXM_SETPARFORMAT:
+         *      reverse to TXM_QUERYPARFORMAT, this sets a
+         *      paragraph format (line spacings, margins
+         *      and such).
+         *
+         *      Parameters:
+         *      -- ULONG mp1: index of format to set.
+         *              Must be 0 currently for the standard
+         *              paragraph format.
+         *      -- PXFMTPARAGRAPH mp2: pointer to buffer
+         *              from which to copy formatting data.
+         *              If this pointer is NULL, the format
+         *              is reset to the default.
+         *
+         *      This reformats the control.
+         *
+         *@@added V0.9.3 (2000-05-06) [umoeller]
+         */
+
+        case TXM_SETPARFORMAT:
+        {
+            PXFMTPARAGRAPH pFmt = NULL;     // target
+
+            mrc = (MPARAM)FALSE;
+
+            if (mp1 == 0)
+                pFmt = &ptxvd->xfd.fmtpStandard;
+            /* else if ((ULONG)mp1 == 1)
+                pFmt = &ptxvd->xfd.fmtpCode; */
+
+            if (pFmt)
+            {
+                if (mp2)
+                    // copy
+                    memcpy(pFmt, mp2, sizeof(XFMTPARAGRAPH));
+                else
+                    // default:
+                    memset(pFmt, 0, sizeof(XFMTPARAGRAPH));
+
+                FormatText2Screen(hwndTextView,
+                                  ptxvd,
+                                  FALSE,
+                                  TRUE);    // full reformat
+
+                mrc = (MPARAM)TRUE;
+            }
+        break; }
+
+        /*
+         *@@ TXM_SETWORDWRAP:
+         *      this text view control msg quickly changes
+         *      the word-wrapping style of the default
+         *      paragraph formatting.
+         *      (BOOL)mp1 determines whether word wrapping
+         *      should be turned on or off.
+         */
+
+        case TXM_SETWORDWRAP:
+        {
+            BOOL    ulOldFlFormat = ptxvd->xfd.fmtpStandard.fWordWrap;
+            ptxvd->xfd.fmtpStandard.fWordWrap = (BOOL)mp1;
+            if (ptxvd->xfd.fmtpStandard.fWordWrap != ulOldFlFormat)
+                FormatText2Screen(hwndTextView,
+                                  ptxvd,
+                                  FALSE,
+                                  FALSE);       // quick format
+        break; }
+
+        /*
+         *@@ TXM_QUERYCDATA:
+         *      copies the current XTEXTVIEWCDATA
+         *      into the specified buffer. This must
+         *      be sent to the control.
+         *
+         *      Parameters:
+         *      -- PXTEXTVIEWCDATA mp1: target buffer.
+         *         Before calling this, you MUST specify
+         *         XTEXTVIEWCDATA.cbData.
+         */
+
+        case TXM_QUERYCDATA:
+            if (mp1)
+            {
+                PXTEXTVIEWCDATA pTarget = (PXTEXTVIEWCDATA)mp1;
+                memcpy(pTarget, &ptxvd->cdata, pTarget->cbData);
+            }
+        break;
+
+        /*
+         *@@ TXM_SETCDATA:
+         *      updates the current XTEXTVIEWCDATA
+         *      with the data from the specified buffer.
+         *      This must be sent to the control.
+         *
+         *      Parameters:
+         *      -- PXTEXTVIEWCDATA mp1: source buffer.
+         *         Before calling this, you MUST specify
+         *         XTEXTVIEWCDATA.cbData.
+         */
+
+        case TXM_SETCDATA:
+            if (mp1)
+            {
+                PXTEXTVIEWCDATA pSource = (PXTEXTVIEWCDATA)mp1;
+                memcpy(&ptxvd->cdata, pSource, pSource->cbData);
+            }
+        break;
+
+        /*
+         *@@ TXM_JUMPTOANCHORNAME:
+         *      scrolls the XTextView control contents so that
+         *      the text marked with the specified anchor name
+         *      (TXVESC_ANCHORNAME escape) appears at the top
+         *      of the control.
+         *
+         *      This must be sent, not posted to the control
+         *
+         *      Parameters:
+         *      -- PSZ mp1: anchor name (e.g. "anchor1").
+         *
+         *@@added V0.9.4 (2000-06-12) [umoeller]
+         */
+
+        case TXM_JUMPTOANCHORNAME:
+        {
+            if (mp1)
+            {
+                PLISTNODE pWordNode = txvFindWordFromAnchor(&ptxvd->xfd,
+                                                            (const char*)mp1);
+                if (pWordNode)
+                {
+                    // found:
+                    PTXVWORD pWord = (PTXVWORD)pWordNode->pItemData;
+                    if (pWord)
+                    {
+                        PTXVRECTANGLE pRect = (PTXVRECTANGLE)pWord->pvRectangle;
+                        ULONG ulWinCY = (ptxvd->rclViewText.yTop - ptxvd->rclViewText.yBottom);
+
+                        // now we need to scroll the window so that this rectangle is on top.
+                        // Since rectangles start out with the height of the window (e.g. +768)
+                        // and then have lower y coordinates down to way in the negatives,
+                        // to get the y offset, we must...
+                        ptxvd->lViewYOfs = (-pRect->rcl.yTop) - ulWinCY;
+
+                        if (ptxvd->lViewYOfs < 0)
+                            ptxvd->lViewYOfs = 0;
+                        if (ptxvd->lViewYOfs > ((LONG)ptxvd->xfd.ulViewportCY - ulWinCY))
+                            ptxvd->lViewYOfs = (LONG)ptxvd->xfd.ulViewportCY - ulWinCY;
+
+                        // vertical scroll bar enabled at all?
+                        if (ptxvd->cdata.flStyle & XTXF_VSCROLL)
+                        {
+                            BOOL fEnabled = winhUpdateScrollBar(ptxvd->hwndVScroll,
+                                                                ulWinCY,
+                                                                ptxvd->xfd.ulViewportCY,
+                                                                ptxvd->lViewYOfs,
+                                                                (ptxvd->cdata.flStyle & XTXF_AUTOVHIDE));
+                            WinInvalidateRect(hwndTextView, NULL, FALSE);
+                        }
+                    }
+                }
+            }
+        break; }
+
+        /*
+         * WM_DESTROY:
+         *      clean up.
+         */
+
+        case WM_DESTROY:
+            if (ptxvd->xfd.pszViewText)
+                free(ptxvd->xfd.pszViewText);
+            lstClear(&ptxvd->xfd.llRectangles);
+            lstClear(&ptxvd->xfd.llWords);
+            free(ptxvd);
+            GpiDestroyPS(ptxvd->hps);
+            mrc = WinDefWindowProc(hwndTextView, msg, mp1, mp2);
+        break;
+
+        default:
+            mrc = WinDefWindowProc(hwndTextView, msg, mp1, mp2);
+    }
+
+    return (mrc);
+}
+
+/*
+ *@@ txvRegisterTextView:
+ *      registers the Text View class with PM. Required
+ *      before the text view control can be used.
+ */
+
+BOOL txvRegisterTextView(HAB hab)
+{
+    return (WinRegisterClass(hab,
+                             WC_XTEXTVIEW,
+                             fnwpTextView,
+                             0,
+                             sizeof(PVOID)));     // QWL_USER
+}
+
+/*
+ *@@ txvReplaceWithTextView:
+ *      replaces any window with a text view control.
+ *      You must call txvRegisterTextView beforehand.
+ *
+ *@@added V0.9.1 (2000-02-13) [umoeller]
+ */
+
+HWND txvReplaceWithTextView(HWND hwndParentAndOwner,
+                            USHORT usID,
+                            ULONG flWinStyle,
+                            ULONG flStyle,
+                            USHORT usBorder)
+{
+    HWND hwndMLE = WinWindowFromID(hwndParentAndOwner, usID),
+         hwndTextView = NULLHANDLE;
+    if (hwndMLE)
+    {
+        ULONG           ul,
+                        // attrFound,
+                        abValue[32];
+        SWP             swpMLE;
+        XTEXTVIEWCDATA  xtxCData;
+        PSZ             pszFont = winhQueryWindowFont(hwndMLE);
+        LONG            lBackClr = -1,
+                        lForeClr = -1;
+
+        if ((ul = WinQueryPresParam(hwndMLE,
+                                    PP_BACKGROUNDCOLOR,
+                                    0,
+                                    NULL,
+                                    (ULONG)sizeof(abValue),
+                                    (PVOID)&abValue,
+                                    QPF_NOINHERIT)))
+            lBackClr = abValue[0];
+
+        if ((ul = WinQueryPresParam(hwndMLE,
+                                    PP_FOREGROUNDCOLOR,
+                                    0,
+                                    NULL,
+                                    (ULONG)sizeof(abValue),
+                                    (PVOID)&abValue,
+                                    QPF_NOINHERIT)))
+            lForeClr = abValue[0];
+
+        WinQueryWindowPos(hwndMLE, &swpMLE);
+
+        WinDestroyWindow(hwndMLE);
+        memset(&xtxCData, 0, sizeof(xtxCData));
+        xtxCData.cbData = sizeof(xtxCData);
+        xtxCData.flStyle = flStyle;
+        xtxCData.ulXBorder = usBorder;
+        xtxCData.ulYBorder = usBorder;
+        hwndTextView = WinCreateWindow(hwndParentAndOwner,
+                                       WC_XTEXTVIEW,
+                                       "",
+                                       flWinStyle,
+                                       swpMLE.x,
+                                       swpMLE.y,
+                                       swpMLE.cx,
+                                       swpMLE.cy,
+                                       hwndParentAndOwner,
+                                       HWND_TOP,
+                                       usID,
+                                       &xtxCData,
+                                       0);
+        if (pszFont)
+        {
+            winhSetWindowFont(hwndTextView, pszFont);
+            free(pszFont);
+        }
+
+        if (lBackClr != -1)
+            WinSetPresParam(hwndTextView,
+                            PP_BACKGROUNDCOLOR,
+                            sizeof(ULONG),
+                            &lBackClr);
+        if (lForeClr != -1)
+            WinSetPresParam(hwndTextView,
+                            PP_FOREGROUNDCOLOR,
+                            sizeof(ULONG),
+                            &lForeClr);
+    }
+    return (hwndTextView);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Printer-dependent functions                                    *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ prthQueryQueues:
+ *      returns a buffer containing all print queues
+ *      on the system.
+ *
+ *      This is usually the first step before printing.
+ *      After calling this function, show a dlg to the
+ *      user, allow him to select the printer queue
+ *      to be used. This can then be passed to
+ *      prthCreatePrinterDC.
+ *
+ *      Use prthFreeBuf to free the returned buffer.
+ */
+
+PRQINFO3* prthEnumQueues(PULONG pulReturned)    // out: no. of queues found
+{
+    SPLERR  rc;
+    ULONG   cTotal;
+    ULONG   cbNeeded = 0;
+    PRQINFO3 *pprq3 = NULL;
+
+    // count queues & get number of bytes needed for buffer
+    rc = SplEnumQueue(NULL,     // default computer
+                      3,        // detail level
+                      NULL,     // pbuf
+                      0L,       // cbBuf
+                      pulReturned, // out: entries returned
+                      &cTotal,  // out: total entries available
+                      &cbNeeded,
+                      NULL);    // reserved
+
+    if (cbNeeded)
+    {
+        pprq3 = (PRQINFO3*)malloc(cbNeeded);
+        if (pprq3)
+        {
+            // enum the queues
+            rc = SplEnumQueue(NULL,
+                              3,
+                              pprq3,
+                              cbNeeded,
+                              pulReturned,
+                              &cTotal,
+                              &cbNeeded,
+                              NULL);
+        }
+    }
+
+    return (pprq3);
+}
+
+/*
+ *@@ prthFreeBuf:
+ *
+ */
+
+VOID prthFreeBuf(PVOID pprq3)
+{
+    if (pprq3)
+        free(pprq3);
+}
+
+/*
+ *@@ prthCreatePrinterDC:
+ *      creates a device context for the printer
+ *      specified by the given printer queue.
+ *
+ *      As a nifty feature, this returns printer
+ *      device resolution automatically in the
+ *      specified buffer.
+ *
+ *      Returns NULLHANDLE (== DEV_ERROR) on errors.
+ *
+ *      Use DevCloseDC to destroy the DC.
+ *
+ *      Based on print sample by Peter Fitzsimmons, Fri  95-09-29 02:47:16am.
+ */
+
+HDC prthCreatePrinterDC(HAB hab,
+                        PRQINFO3 *pprq3,
+                        PLONG palRes)  // out: 2 longs holding horizontal and vertical
+                                       // printer resolution in pels per inch
+{
+    HDC     hdc = NULLHANDLE;
+    DEVOPENSTRUC dos;
+    PSZ     p;
+
+    memset(&dos, 0, sizeof(dos));
+    p = strrchr(pprq3->pszDriverName, '.');
+    if (p)
+        *p = 0;                         // del everything after '.'
+
+    dos.pszLogAddress = pprq3->pszName;
+    dos.pszDriverName = pprq3->pszDriverName;
+    dos.pdriv = pprq3->pDriverData;
+    dos.pszDataType = "PM_Q_STD";
+    hdc = DevOpenDC(hab,
+                    OD_QUEUED,
+                    "*",
+                    4L,     // count of items in next param
+                    (PDEVOPENDATA)&dos,
+                    0);     // compatible DC
+
+    if (hdc)
+        DevQueryCaps(hdc,
+                     CAPS_HORIZONTAL_FONT_RES,
+                     2,
+                     palRes);   // buffer
+
+    return (hdc);
+}
+
+/*
+ *@@ prthQueryForms:
+ *      returns a buffer containing all forms
+ *      supported by the specified printer DC.
+ *
+ *      Use prthFreeBuf to free the returned
+ *      buffer.
+ *
+ *      HCINFO uses different model spaces for
+ *      the returned info. See PMREF for details.
+ */
+
+HCINFO* prthQueryForms(HDC hdc,
+                       PULONG pulCount)
+{
+    HCINFO  *pahci = NULL;
+
+    LONG    cForms;
+
+    // get form count
+    cForms = DevQueryHardcopyCaps(hdc, 0L, 0L, NULL); // phci);
+    if (cForms)
+    {
+        pahci = (HCINFO*)malloc(cForms * sizeof(HCINFO));
+        if (pahci)
+        {
+            *pulCount = DevQueryHardcopyCaps(hdc, 0, cForms, pahci);
+        }
+    }
+
+    return (pahci);
+}
+
+/*
+ *@@ prthCreatePS:
+ *      creates a "normal" presentation space from the specified
+ *      printer device context (which can be opened thru
+ *      prthCreatePrinterDC).
+ *
+ *      Returns NULLHANDLE on errors.
+ *
+ *      Based on print sample by Peter Fitzsimmons, Fri  95-09-29 02:47:16am.
+ */
+
+HPS prthCreatePS(HAB hab,       // in: anchor block
+                 HDC hdc,       // in: printer device context
+                 ULONG ulUnits) // in: one of:
+                                // -- PU_PELS
+                                // -- PU_LOMETRIC
+                                // -- PU_HIMETRIC
+                                // -- PU_LOENGLISH
+                                // -- PU_HIENGLISH
+                                // -- PU_TWIPS
+{
+    SIZEL   sizel;
+
+    sizel.cx = 0;
+    sizel.cy = 0;
+    return (GpiCreatePS(hab,
+                        hdc,
+                        &sizel,
+                        ulUnits | GPIA_ASSOC | GPIT_NORMAL));
+}
+
+/*
+ *@@ prthStartDoc:
+ *      calls DevEscape with DEVESC_STARTDOC.
+ *      This must be called before any painting
+ *      into the HDC's HPS. Any GPI calls made
+ *      before this are ignored.
+ *
+ *      pszDocTitle appears in the spooler.
+ */
+
+VOID prthStartDoc(HDC hdc,
+                  PSZ pszDocTitle)
+{
+    DevEscape(hdc,
+              DEVESC_STARTDOC,
+              strlen(pszDocTitle),
+              pszDocTitle,
+              0L,
+              0L);
+}
+
+/*
+ *@@ prthNextPage:
+ *      calls DevEscape with DEVESC_NEWFRAME.
+ *      Signals when an application has finished writing to a page and wants to
+ *      start a new page. It is similar to GpiErase processing for a screen device
+ *      context, and causes a reset of the attributes. This escape is used with a
+ *      printer device to advance to a new page.
+ */
+
+VOID prthNextPage(HDC hdc)
+{
+    DevEscape(hdc,
+              DEVESC_NEWFRAME,
+              0,
+              0,
+              0,
+              0);
+}
+
+/*
+ *@@ prthEndDoc:
+ *      calls DevEscape with DEVESC_ENDDOC
+ *      and disassociates the HPS from the HDC.
+ *      Call this right before doing
+ +          GpiDestroyPS(hps);
+ +          DevCloseDC(hdc);
+ */
+
+VOID prthEndDoc(HDC hdc,
+                HPS hps)
+{
+    DevEscape(hdc, DEVESC_ENDDOC, 0L, 0L, 0, NULL);
+    GpiAssociate(hps, NULLHANDLE);
+}
+
+/*
+ *@@ txvPrint:
+ *      this does the actual printing.
+ */
+
+BOOL txvPrint(HAB hab,
+              HDC hdc,          // in: printer device context
+              HPS hps,          // in: printer presentation space (using PU_PELS)
+              PSZ pszViewText,  // in: text to print
+              ULONG ulSize,     // in: default font point size
+              PSZ pszFaceName,  // in: default font face name
+              HCINFO  *phci,    // in: hardcopy form to use
+              PSZ pszDocTitle,  // in: document title (appears in spooler)
+              FNPRINTCALLBACK *pfnCallback)
+{
+    RECTL       rclPageDevice,
+                rclPageWorld;
+    XFORMATDATA xfd;
+    BOOL        fAnotherPage = FALSE;
+    ULONG       ulCurrentLineIndex = 0,
+                ulCurrentPage = 1;
+    LONG        lCurrentYOfs = 0;
+
+    /* MATRIXLF    matlf;
+    POINTL      ptlCenter;
+    FIXED       scalars[2]; */
+
+    // important: we must do a STARTDOC before we use the printer HPS.
+    prthStartDoc(hdc,
+                 pszDocTitle);
+
+    // the PS is in TWIPS, but our world coordinate
+    // space is in pels, so we need to transform
+    /* GpiQueryViewingTransformMatrix(hps,
+                                   1L,
+                                   &matlf);
+    ptlCenter.x = 0;
+    ptlCenter.y = 0;
+    scalars[0] =  MAKEFIXED(2,0);
+    scalars[1] =  MAKEFIXED(3,0);
+
+    GpiScale (hps,
+              &matlf,
+              TRANSFORM_REPLACE,
+              scalars,
+              &ptlCenter); */
+
+    // initialize format with font from window
+    txvInitFormat(&xfd);
+
+    /* txvSetFormatFont(hps,
+                     &xfd,
+                     ulSize,
+                     pszFaceName); */
+
+    // use text from window
+    xfd.pszViewText = pszViewText;
+
+    // setup page
+    GpiQueryPageViewport(hps,
+                         &rclPageDevice);
+    // this is in device units; convert this
+    // to the world coordinate space of the printer PS
+    memcpy(&rclPageWorld, &rclPageDevice, sizeof(RECTL));
+    GpiConvert(hps,
+               CVTC_DEVICE,     // source
+               CVTC_WORLD,
+               2,               // 2 points, it's a rectangle
+               (PPOINTL)&rclPageWorld);
+
+    // left and bottom margins are in millimeters...
+    /* rclPage.xLeft = 100;        // ###
+    rclPage.yBottom = 100;
+    rclPage.xRight = rclPage.xLeft + phci->xPels;
+    rclPage.yTop = rclPage.yBottom + phci->yPels; */
+
+    txvFormatText(hps,
+                  &xfd,          // in: ptxvd->rclViewText
+                  &rclPageWorld,
+                  TRUE);
+
+    do
+    {
+        _Pmpf(("---- printing page %d",
+                ulCurrentPage));
+
+        fAnotherPage = txvPaintText(hab,
+                                    hps,
+                                    &xfd,
+                                    &rclPageWorld,
+                                    0,
+                                    &lCurrentYOfs,
+                                    FALSE,      // draw only fully visible lines
+                                    &ulCurrentLineIndex); // in/out: line to start with
+        if (fAnotherPage)
+        {
+            prthNextPage(hdc);
+
+            if (pfnCallback(ulCurrentPage++, 0) == FALSE)
+                fAnotherPage = FALSE;
+        }
+    } while (fAnotherPage);
+
+    prthEndDoc(hdc, hps);
+
+    return (TRUE);
+}
+
+/*
+ *@@ txvPrintWindow:
+ *      one-shot function which prints the contents
+ *      of the specified XTextView control to the
+ *      default printer, using the default form.
+ *
+ *      Returns a nonzero value upon errors.
+ *
+ *      Based on print sample by Peter Fitzsimmons, Fri  95-09-29 02:47:16am.
+ */
+
+int txvPrintWindow(HWND hwndTextView,
+                   PSZ pszDocTitle,  // in: document title (appears in spooler)
+                   FNPRINTCALLBACK *pfnCallback)
+{
+    int     irc = 0;
+
+    PTEXTVIEWWINDATA ptxvd = (PTEXTVIEWWINDATA)WinQueryWindowPtr(hwndTextView, QWL_USER);
+
+    if (!ptxvd)
+        irc = 1;
+    else
+    {
+        ULONG       cReturned = 0;
+        PRQINFO3    *pprq3 = prthEnumQueues(&cReturned);
+        HDC         hdc = NULLHANDLE;
+        LONG        caps[2];
+
+        // find default queue
+        if (pprq3)
+        {
+            ULONG   i;
+            // search for default queue;
+            for (i = 0; i < cReturned; i++)
+                if (pprq3[i].fsType & PRQ3_TYPE_APPDEFAULT)
+                {
+                    hdc = prthCreatePrinterDC(ptxvd->hab,
+                                              &pprq3[i],
+                                              caps);
+
+                    break;
+                }
+            prthFreeBuf(pprq3);
+        }
+
+        if (!hdc)
+            irc = 2;
+        else
+        {
+            // OK, we got a printer DC:
+            HPS     hps;
+            ULONG   cForms = 0;
+            HCINFO  *pahci,
+                    *phciSelected = 0;
+
+            // find default form
+            pahci = prthQueryForms(hdc,
+                                   &cForms);
+            if (pahci)
+            {
+                HCINFO  *phciThis = pahci;
+                ULONG   i;
+                for (i = 0;
+                     i < cForms;
+                     i++, phciThis++)
+                {
+                    if (phciThis->flAttributes & HCAPS_CURRENT)
+                    {
+                        phciSelected = phciThis;
+                    }
+                }
+            }
+
+            if (!phciSelected)
+                irc = 3;
+            else
+            {
+                // create printer PS
+                hps = prthCreatePS(ptxvd->hab,
+                                   hdc,
+                                   PU_PELS);
+
+                if (hps == GPI_ERROR)
+                    irc = 4;
+                else
+                {
+                    PSZ pszFont;
+                    ULONG ulSize = 0;
+                    PSZ pszFaceName = 0;
+
+                    if ((pszFont = winhQueryWindowFont(hwndTextView)))
+                        gpihSplitPresFont(pszFont,
+                                          &ulSize,
+                                          &pszFaceName);
+                    txvPrint(ptxvd->hab,
+                             hdc,
+                             hps,
+                             ptxvd->xfd.pszViewText,
+                             ulSize,
+                             pszFaceName,
+                             phciSelected,
+                             pszDocTitle,
+                             pfnCallback);
+
+                    if (pszFont)
+                        free(pszFont);
+
+                    GpiDestroyPS(hps);
+                }
+            }
+            DevCloseDC(hdc);
+        }
+    }
+
+    return (irc);
+}
+
+
Index: /trunk/src/helpers/threads.c
===================================================================
--- /trunk/src/helpers/threads.c	(revision 8)
+++ /trunk/src/helpers/threads.c	(revision 8)
@@ -0,0 +1,437 @@
+
+/*
+ *@@sourcefile threads.c:
+ *      contains helper functions for creating, destroying, and
+ *      synchronizing threads, including PM threads with a
+ *      message queue which is created automatically.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  thr*        Thread helper functions
+ *
+ *      This file is new with V0.81 and contains all the thread
+ *      functions that used to be in helpers.c.
+ *
+ *      Use thrCreate() to start a thread.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\threads.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSPROCESS
+#define INCL_DOSSEMAPHORES
+#define INCL_DOSERRORS
+#include <os2.h>
+
+#include <string.h>
+#include <stdlib.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\threads.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Control program helpers\Thread management
+ */
+
+/*
+ *@@ thr_fntGeneric:
+ *      generic thread function used by thrCreate.
+ *      This in turn calls the actual thread function
+ *      specified with thrCreate.
+ *
+ *@@added V0.9.2 (2000-03-05) [umoeller]
+ */
+
+VOID _Optlink thr_fntGeneric(PVOID ptiMyself)
+{
+    PTHREADINFO pti = (PTHREADINFO)ptiMyself;
+
+    if (pti)
+    {
+        if (pti->pfRunning)
+            // set "running" flag
+            *(pti->pfRunning) = TRUE;
+
+        if (pti->flFlags & THRF_WAIT)
+            // "Wait" flag set: thrCreate is then
+            // waiting on the wait event sem posted
+            DosPostEventSem(pti->hevRunning);
+
+        if (pti->flFlags & THRF_PMMSGQUEUE)
+        {
+            // create msg queue
+            if ((pti->hab = WinInitialize(0)))
+            {
+                if ((pti->hmq = WinCreateMsgQueue(pti->hab, 4000)))
+                {
+                    // run thread func
+                    ((PTHREADFUNC)pti->pThreadFunc)(pti);
+
+                    WinDestroyMsgQueue(pti->hmq);
+
+                }
+                WinTerminate(pti->hab);
+            }
+        }
+        else
+            // no msgqueue:
+            ((PTHREADFUNC)pti->pThreadFunc)(pti);
+
+        // thread func returns:
+        pti->fExitComplete = TRUE;
+        pti->tid = NULLHANDLE;
+
+        if (pti->flFlags & THRF_WAIT)
+            // "Wait" flag set: delete semaphore
+            DosCloseEventSem(pti->hevRunning);
+
+        if (pti->pfRunning)
+            // clear "running" flag
+            *(pti->pfRunning) = FALSE;
+    }
+}
+
+/*
+ *@@ thrCreate:
+ *      this function fills a THREADINFO structure in *pti
+ *      and starts a new thread using _beginthread.
+ *
+ *      You must pass the thread function in pfn, which will
+ *      then be executed. The thread will be passed a pointer
+ *      to the THREADINFO structure as its thread parameter.
+ *      The ulData field in that structure is set to ulData
+ *      here. Use whatever you like.
+ *
+ *      The thread function must be declared like this:
+ *
+ +          void _Optlink fntWhatever(PTHREADINFO ptiMyself)
+ *
+ *      You should manually specify _Optlink because if the
+ *      function is prototyped somewhere, VAC will automatically
+ *      modify the function's linkage, and you'll run into
+ *      compiler warnings.
+ *
+ *      ptiMyself is then a pointer to the THREADINFO structure.
+ *      ulData may be obtained like this:
+ +          ULONG ulData = ((PTHREADINFO)ptiMyself)->ulData;
+ *
+ *      thrCreate does not call your thread func directly,
+ *      but only through the thr_fntGeneric wrapper to
+ *      provide additional functionality. As a consequence,
+ *      in your own thread function, NEVER call _endthread
+ *      explicitly, because this would skip the exit processing
+ *      (cleanup) in thr_fntGeneric. Instead, just fall out of
+ *      your thread function.
+ *
+ *      This function does NOT check whether a thread is
+ *      already running in *pti. If it is, that information
+ *      will be lost.
+ *
+ *      flFlags can be any combination of the following:
+ *
+ *      -- THRF_PMMSGQUEUE: creates a PM message queue on the
+ *         thread. Your thread function will find the HAB and
+ *         the HMQ in its THREADINFO. These are automatically
+ *         destroyed when the thread terminates.
+ *
+ *      -- THRF_WAIT: if this is set, thrCreate does not
+ *         return to the caller until your thread function
+ *         has successfully started running. This is done by
+ *         waiting on an event semaphore which is automatically
+ *         posted by thr_fntGeneric. This is useful for the
+ *         typical PM "Worker" thread where you need to disable
+ *         menu items on thread 1 while the thread is running.
+ *
+ *@@changed V0.9.0 [umoeller]: default stack size raised for Watcom (thanks, Rdiger Ihle)
+ *@@changed V0.9.0 [umoeller]: _beginthread is now only called after all variables have been set (thanks, Rdiger Ihle)
+ *@@changed V0.9.2 (2000-03-04) [umoeller]: added stack size parameter
+ *@@changed V0.9.2 (2000-03-06) [umoeller]: now using thr_fntGeneric; thrGoodbye is no longer needed
+ *@@changed V0.9.3 (2000-04-29) [umoeller]: removed stack size param; added fCreateMsgQueue
+ *@@changed V0.9.3 (2000-05-01) [umoeller]: added pbRunning and flFlags
+ *@@changed V0.9.5 (2000-08-26) [umoeller]: now using PTHREADINFO
+ */
+
+BOOL thrCreate(PTHREADINFO pti,     // out: THREADINFO data
+               PTHREADFUNC pfn,     // in: _Optlink thread function
+               PBOOL pfRunning,     // out: variable set to TRUE while thread is running;
+                                    // ptr can be NULL
+               ULONG flFlags,       // in: THRF_* flags
+               ULONG ulData)        // in: user data to be stored in THREADINFO
+{
+    BOOL            rc = FALSE;
+
+    if (pti)
+    {
+        // we arrive here if *ppti was NULL or (*ppti->tid == NULLHANDLE),
+        // i.e. the thread is not already running.
+        // _beginthread is contained both in the VAC++ and EMX
+        // C libraries with this syntax.
+        memset(pti, 0, sizeof(THREADINFO));
+        pti->cbStruct = sizeof(THREADINFO);
+        pti->pThreadFunc = (PVOID)pfn;
+        pti->pfRunning = pfRunning;
+        pti->flFlags = flFlags;
+        pti->ulData = ulData;
+
+        rc = TRUE;
+
+        if (flFlags & THRF_WAIT)
+            // "Wait" flag set: create an event semaphore which
+            // will be posted by thr_fntGeneric
+            if (DosCreateEventSem(NULL,     // unnamed
+                                  &pti->hevRunning,
+                                  0,        // unshared
+                                  FALSE)    // not posted (reset)
+                    != NO_ERROR)
+                rc = FALSE;
+
+        pti->fExit = FALSE;
+
+        if (rc)
+        {
+            pti->tid = _beginthread(        // moved, V0.9.0 (hint: Rdiger Ihle)
+                                    thr_fntGeneric, // changed V0.9.2 (2000-03-06) [umoeller]
+                                    0,      // unused compatibility param
+                                    3*96000, // plenty of stack
+                                    pti);   // parameter passed to thread
+            rc = (pti->tid != 0);
+
+            if (rc)
+                if (flFlags & THRF_WAIT)
+                {
+                    // "Wait" flag set: wait on event semaphore
+                    // posted by thr_fntGeneric
+                    DosWaitEventSem(pti->hevRunning,
+                                    SEM_INDEFINITE_WAIT);
+                }
+        }
+    }
+
+    return (rc);
+}
+
+/*
+ *@@ thrRunSync:
+ *      runs the specified thread function synchronously.
+ *
+ *      This is a wrapper around thrCreate. However, this
+ *      function does not return until the thread function
+ *      finishes. This creates a modal message loop on the
+ *      calling thread so that the PM message queue is not
+ *      blocked while the thread is running. Naturally this
+ *      implies that the calling thread has a message queue.
+ *
+ *      As a special requirement, your thread function (pfn)
+ *      must post WM_USER to THREADINFO.hwndNotify just before
+ *      exiting. The mp1 value of WM_USER will then be returned
+ *      by this function.
+ *
+ *@@added V0.9.5 (2000-08-26) [umoeller]
+ */
+
+ULONG thrRunSync(HAB hab,               // in: anchor block of calling thread
+                 PTHREADFUNC pfn,       // in: thread function
+                 ULONG ulData)          // in: data for thread function
+{
+    ULONG ulrc = 0;
+    QMSG qmsg;
+    BOOL fQuit = FALSE;
+    HWND hwndNotify = WinCreateWindow(HWND_OBJECT,
+                                      WC_BUTTON,
+                                      (PSZ)"",
+                                      0,
+                                      0,0,0,0,
+                                      0,
+                                      HWND_BOTTOM,
+                                      0,
+                                      0,
+                                      NULL);
+    THREADINFO  ti = {0};
+    thrCreate(&ti,
+              pfn,
+              NULL,
+              THRF_PMMSGQUEUE,
+              ulData);
+    ti.hwndNotify = hwndNotify;
+
+    while (WinGetMsg(hab,
+                     &qmsg, 0, 0, 0))
+    {
+        // current message for our object window?
+        if (    (qmsg.hwnd == hwndNotify)
+             && (qmsg.msg == WM_USER)
+           )
+        {
+            fQuit = TRUE;
+            ulrc = (ULONG)qmsg.mp1;
+        }
+
+        WinDispatchMsg(hab, &qmsg);
+        if (fQuit)
+            break;
+    }
+
+    // we must wait for the thread to finish, or
+    // otherwise THREADINFO is deleted from the stack
+    // before the thread exits... will crash!
+    thrWait(&ti);
+
+    WinDestroyWindow(hwndNotify);
+    return (ulrc);
+}
+
+/*
+ *@@ thrClose:
+ *      this functions sets the "fExit" flag in
+ *      THREADINFO to TRUE.
+ *
+ *      The thread should monitor this flag
+ *      periodically and then terminate itself.
+ */
+
+BOOL thrClose(PTHREADINFO pti)
+{
+    if (pti)
+    {
+        pti->fExit = TRUE;
+        return (TRUE);
+    }
+    return (FALSE);
+}
+
+/*
+ *@@ thrWait:
+ *      this function waits for a thread to end by calling
+ *      DosWaitThread. Note that this blocks the calling
+ *      thread, so only use this function when you're sure
+ *      the thread will actually terminate.
+ *
+ *      Returns FALSE if the thread wasn't running or TRUE
+ *      if it was and has terminated.
+ *
+ *@@changed V0.9.0 [umoeller]: now checking for whether pti->tid is still != 0
+ */
+
+BOOL thrWait(PTHREADINFO pti)
+{
+    if (pti)
+        if (pti->tid)
+        {
+            DosWaitThread(&(pti->tid), DCWW_WAIT);
+            pti->tid = NULLHANDLE;
+            return (TRUE);
+        }
+    return (FALSE);
+}
+
+/*
+ *@@ thrFree:
+ *      this is a combination of thrClose and
+ *      thrWait, i.e. this func does not return
+ *      until the specified thread has ended.
+ */
+
+BOOL thrFree(PTHREADINFO pti)
+{
+    if (pti->tid)
+    {
+        thrClose(pti);
+        thrWait(pti);
+    }
+    return (TRUE);
+}
+
+/*
+ *@@ thrKill:
+ *      just like thrFree, but the thread is
+ *      brutally killed, using DosKillThread.
+ *
+ *      Note: DO NOT USE THIS. DosKillThread
+ *      cannot clean up the C runtime. In the
+ *      worst case, this hangs the system
+ *      because the runtime hasn't released
+ *      a semaphore or something like that.
+ */
+
+BOOL thrKill(PTHREADINFO pti)
+{
+    if (pti->tid)
+    {
+        DosResumeThread(pti->tid);
+            // this returns an error if the thread
+            // is not suspended, but otherwise the
+            // system might hang
+        DosKillThread(pti->tid);
+    }
+    return (TRUE);
+}
+
+/*
+ *@@ thrQueryID:
+ *      returns thread ID or NULLHANDLE if
+ *      the specified thread is not or no
+ *      longer running.
+ */
+
+TID thrQueryID(const THREADINFO* pti)
+{
+    if (pti)
+        if (!(pti->fExitComplete))
+            return (pti->tid);
+
+    return (NULLHANDLE);
+}
+
+/*
+ *@@ thrQueryPriority:
+ *      returns the priority of the calling thread.
+ *      The low byte of the low word is a hexadecimal value
+ *      representing a rank (value 0 to 31) within a priority class.
+ *      Class values, found in the high byte of the low word, are
+ *      as follows:
+ *      --  0x01  idle
+ *      --  0x02  regular
+ *      --  0x03  time-critical
+ *      --  0x04  server
+ *
+ *      Note: This cannot retrieve the priority of a
+ *      thread other than the one on which this function
+ *      is running. Use prc16QueryThreadInfo for that.
+ */
+
+ULONG thrQueryPriority(VOID)
+{
+    PTIB    ptib;
+    PPIB    ppib;
+    if (DosGetInfoBlocks(&ptib, &ppib) == NO_ERROR)
+        if (ptib)
+            if (ptib->tib_ptib2)
+                return (ptib->tib_ptib2->tib2_ulpri);
+    return (0);
+}
+
+
Index: /trunk/src/helpers/tmsgfile.c
===================================================================
--- /trunk/src/helpers/tmsgfile.c	(revision 8)
+++ /trunk/src/helpers/tmsgfile.c	(revision 8)
@@ -0,0 +1,881 @@
+
+/*
+ *@@sourcefile tmsgfile.c:
+ *      contains a "text message file" helper function,
+ *      which works similar to DosGetMessage. See
+ *      tmfGetMessage for details.
+ *
+ *      tmfGetMessage operates on plain-text ".TMF" files.
+ *      The file is "compiled" into the file's extended
+ *      attributes for faster access after the first access.
+ *      When the file's contents change, the EAs are recompiled
+ *      automatically.
+ *
+ *      This has the following advantages over DosGetMessage:
+ *
+ *      1)  No external utility is necessary to change message
+ *          files. Simply edit the text, and on the next call,
+ *          the file is recompiled at run-time.
+ *
+ *      2)  To identify messages, any string can be used, not
+ *          only numerical IDs.
+ *
+ *      This file is entirely new with V0.9.0.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes:
+ *      --  tmf*   text message file functions
+ *
+ *      This code uses only C standard library functions
+ *      which are also part of the subsystem libraries.
+ *      It does _not_ use the fopen etc. functions, but
+ *      DosOpen etc. instead.
+ *      You can therefore use this code with your software
+ *      which uses the subsystem libraries only.
+ *
+ *      This code was kindly provided by Christian Langanke.
+ *      Modifications April 1999 by Ulrich Mller. Any
+ *      substantial changes (other than comments and
+ *      formatting) are marked with (*UM).
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\tmsgfile.h"
+ *@@added V0.9.0 [umoeller]
+ */
+
+/*
+ *      Copyright (C) 1999 Christian Langanke.
+ *      Copyright (C) 1999-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_ERRORS
+#include <os2.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\eah.h"
+#include "helpers\tmsgfile.h"
+
+/*
+ *@@category: Helpers\Control program helpers\Text message files (TMF)
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Declarations                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+// extended attribute used for timestamp; written to .TMF file
+#define EA_TIMESTAMP "TMF.FILEINFO"
+// extended attribute used for compiled text file; written to .TMF file
+#define EA_MSGTABLE  "TMF.MSGTABLE"
+
+#define NEWLINE "\n"
+
+// start-of-msgid marker
+#define MSG_NAME_START   "\r\n<--"
+// end-of-msgid marker
+#define MSG_NAME_END     "-->:"       // fixed: the space had to be removed, because
+                                      // if a newline was the first character after ":",
+                                      // we got complete garbage (crashes) (99-10-23) [umoeller]
+// comment marker
+#define MSG_COMMENT_LINE "\r\n;"
+
+// internal prototypes
+APIRET          CompileMsgTable(PSZ pszMessageFile, PBYTE * ppbTableData);
+APIRET          GetTimeStamp(PFILESTATUS3 pfs3, PSZ pszBuffer, ULONG ulBufferlen);
+
+/* ******************************************************************
+ *                                                                  *
+ *   Text Message File Code                                         *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ tmfGetMessage:
+ *      just like DosGetMessage, except that this does not
+ *      take a simple message number for the message in the
+ *      given message file, but a PSZ message identifier
+ *      (pszMessageName).
+ *
+ *      This function automatically compiles the given .TMF
+ *      file into the file's extended attributes, if we find
+ *      that this is necessary, by calling CompileMsgTable.
+ *
+ *      Note that for compatibily, this function performs the
+ *      same brain-dead string processing as DosGetMessage, that
+ *      is, the return string is _not_ null-terminated, and
+ *      trailing newline characters are _not_ cut off.
+ *
+ *      If you don't like this, call tmfGetMessageExt, which
+ *      calls this function in turn.
+ *
+ *      <B>Returns:</B>
+ *      --  ERROR_INVALID_PARAMETER
+ *      --  ERROR_BUFFER_OVERFLOW
+ *      --  ERROR_MR_MID_NOT_FOUND
+ *      --  ERROR_MR_INV_MSGF_FORMAT
+ *      --  ERROR_BUFFER_OVERFLOW
+ *
+ *      plus the error codes of DosOpen, DosRead, DosClose.
+ */
+
+APIRET tmfGetMessage(PCHAR* pTable,    // in: pointer table for string insertion
+                     ULONG cTable,     // in: number of pointers in *pTable
+                     PBYTE pbBuffer,   // out: returned string
+                     ULONG cbBuffer,   // out: sizeof(*pbBuffer)
+                     PSZ pszMessageName,  // in: message identifier
+                     PSZ pszFile,      // in: message file (.TMF extension proposed)
+                     PULONG pcbMsg)    // out: bytes written to *pbBuffer
+{
+    // fixed UM 99-10-22: now initializing all vars to 0,
+    // because ulBytesRead might be returned from this func
+    APIRET          rc = NO_ERROR;
+    CHAR            szMessageFile[_MAX_PATH];
+    PBYTE           pbTableData = NULL;
+    PSZ             pszEntry = 0;
+    PSZ             pszEndOfEntry = 0;
+    ULONG           ulMessagePos = 0;
+    ULONG           ulMessageLen = 0;
+
+    HFILE           hfile = NULLHANDLE;
+    ULONG           ulFilePtr = 0;
+    ULONG           ulAction = 0;
+    ULONG           ulBytesToRead = 0;
+    ULONG           ulBytesRead = 0;
+
+    do
+    {
+        // check parms
+        if ((!pbBuffer) ||
+            (!cbBuffer) ||
+            (!pszMessageName) ||
+            (!*pszMessageName) ||
+            (!*pszFile))
+        {
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        if (cbBuffer < 2)
+        {
+            rc = ERROR_BUFFER_OVERFLOW;
+            break;
+        }
+
+        // search file
+        if ((strchr(pszFile, ':')) ||
+            (strchr(pszFile, '\\')) ||
+            (strchr(pszFile, '/')))
+            // drive and/or path given: no search in path
+            strcpy(szMessageFile, pszFile);
+        else
+        {
+            // only filename, search in current dir and DPATH
+            rc = DosSearchPath(SEARCH_IGNORENETERRS
+                               | SEARCH_ENVIRONMENT
+                               | SEARCH_CUR_DIRECTORY,
+                               "DPATH",
+                               pszFile,
+                               szMessageFile,
+                               sizeof(szMessageFile));
+            if (rc != NO_ERROR)
+                break;
+        }
+
+        // _Pmpf(("tmfGetMessage: Found %s", szMessageFile));
+
+        // compile table if neccessary
+        rc = CompileMsgTable(szMessageFile, &pbTableData);
+        if (rc != NO_ERROR)
+            break;
+
+        // _Pmpf(("tmfGetMessage: Now searching compiled file"));
+
+        // search the name
+        pszEntry = strstr(pbTableData, pszMessageName);
+        if (!pszEntry)
+        {
+            rc = ERROR_MR_MID_NOT_FOUND;
+            break;
+        }
+        else
+            pszEntry += strlen(pszMessageName) + 1;
+
+        // isolate entry
+        pszEndOfEntry = strchr(pszEntry, '\n');
+        if (pszEndOfEntry)
+            *pszEndOfEntry = 0;
+
+        // get numbers
+        ulMessagePos = atol(pszEntry);
+        if (ulMessagePos == 0)
+            if (!pszEntry)
+            {
+                rc = ERROR_MR_INV_MSGF_FORMAT;
+                break;
+            }
+
+        pszEntry = strchr(pszEntry, ' ');
+        if (!pszEntry)
+        {
+            rc = ERROR_MR_INV_MSGF_FORMAT;
+            break;
+        }
+        ulMessageLen = atol(pszEntry);
+
+        // determine maximum read len
+        ulBytesToRead = _min(ulMessageLen, cbBuffer);
+
+        // open file and read message
+        rc = DosOpen(pszFile,
+                     &hfile,
+                     &ulAction,
+                     0, 0,
+                     OPEN_ACTION_FAIL_IF_NEW
+                     | OPEN_ACTION_OPEN_IF_EXISTS,
+                     OPEN_FLAGS_FAIL_ON_ERROR
+                     | OPEN_SHARE_DENYWRITE
+                     | OPEN_ACCESS_READONLY,
+                     NULL);
+        if (rc != NO_ERROR)
+            break;
+
+        rc = DosSetFilePtr(hfile, ulMessagePos, FILE_BEGIN, &ulFilePtr);
+        if ((rc != NO_ERROR) || (ulFilePtr != ulMessagePos))
+            break;
+
+        rc = DosRead(hfile,
+                     pbBuffer,
+                     ulBytesToRead,
+                     &ulBytesRead);
+        if (rc != NO_ERROR)
+            break;
+
+        // report "buffer too small" here
+        if (ulBytesToRead < ulMessageLen)
+            rc = ERROR_BUFFER_OVERFLOW;
+
+    }
+    while (FALSE);
+
+    // replace string placeholders ("%1" etc.) (*UM)
+    if (rc == NO_ERROR)     // added (99-10-24) [umoeller]
+        if (cTable)
+        {
+            // create a temporary buffer for replacements
+            PSZ             pszTemp = (PSZ)malloc(cbBuffer); // ### memory leak here!
+
+            if (!pszTemp)
+                rc = ERROR_NOT_ENOUGH_MEMORY;
+            else
+            {
+                // two pointers for copying
+                PSZ             pSource = pbBuffer,
+                                pTarget = pszTemp;
+
+                CHAR            szMsgNum[5] = "0";
+                ULONG           ulTableIndex = 0;
+
+                ULONG           ulCount = 0, ulTargetWritten = 0;
+
+                // 1) copy the stuff we've read to the
+                // new temporary buffer
+                for (ulCount = 0;
+                     ulCount < ulBytesRead;
+                     ulCount++)
+                    *pTarget++ = *pSource++;
+
+                // 2) now go thru the source string
+                // (which we've read above) and
+                // look for "%" characters; we do
+                // this while copying the stuff
+                // back to pbBuffer
+
+                pSource = pszTemp;
+                pTarget = pbBuffer;
+
+                for (ulCount = 0;
+                     ulCount < ulBytesRead;
+                     ulCount++)
+                {
+                    if (*pSource == '%')
+                    {
+                        // copy the index (should be > 0)
+                        szMsgNum[0] = *(pSource + 1);
+                        // szMsgNum[1] is still 0
+
+                        // Pmpf(("      Found %s", szMsgNum));
+
+                        ulTableIndex = atoi(szMsgNum);
+                        // _Pmpf(("        --> %d", ulTableIndex));
+                        if ((ulTableIndex != 0)
+                            && (ulTableIndex <= cTable)
+                            )
+                        {
+                            // valid index:
+                            // insert stuff from table
+                            PSZ            *ppTableThis = (PSZ*)(pTable + (ulTableIndex - 1));
+
+                            // (pTable) if 1, (pTable+1) if 2, ...
+                            PSZ             pTableSource = *ppTableThis;
+
+                            // copy string from table
+                            // and increase the target pointer
+                            while ((*pTarget++ = *pTableSource++))
+                                ulTargetWritten++;
+
+                            // increase source pointer to point
+                            // behind the "%x"
+                            pSource += 2;
+                            // decrease target again, because
+                            // we've copied a null-byte
+                            pTarget--;
+
+                            ulCount++;
+                            // next for
+                            continue;
+                        }
+                        // else invalid index: just copy
+                    }
+
+                    // regular character: copy
+                    *pTarget++ = *pSource++;
+                    ulTargetWritten++;
+                }                   // end for (ulCount = 0) ...
+
+                ulBytesRead = ulTargetWritten;
+            }
+        }                           // end if (cTable)
+
+    // report bytes written (*UM)
+    if (pcbMsg)
+        *pcbMsg = ulBytesRead;
+
+    // cleanup
+    if (hfile)
+        DosClose(hfile);
+    if (pbTableData)
+        free(pbTableData);
+
+    return (rc);
+}
+
+/*
+ *@@ tmfGetMessageExt:
+ *      just like tmfGetMessage, but this func is smart
+ *      enough to return a zero-terminated string and
+ *      strip trailing newlines.
+ *
+ *      See tmfGetMessage for details.
+ */
+
+APIRET tmfGetMessageExt(PCHAR* pTable,    // in: pointer table for string insertion
+                        ULONG cTable,     // in: number of pointers in *pTable
+                        PBYTE pbBuffer,   // out: returned string
+                        ULONG cbBuffer,   // out: sizeof(*pbBuffer)
+                        PSZ pszMessageName,  // in: message identifier
+                        PSZ pszFile,      // in: message file (.TMF extension proposed)
+                        PULONG pcbMsg)    // out: bytes written to *pbBuffer
+{
+    APIRET  arc = NO_ERROR;
+    ULONG   cbReturned = 0;
+    arc = tmfGetMessage(pTable, cTable, pbBuffer, cbBuffer, pszMessageName, pszFile,
+                        &cbReturned);
+
+    if (arc == NO_ERROR)
+    {
+        PSZ p = pbBuffer;
+
+        // terminate string
+        if (cbReturned < cbBuffer)
+            *(pbBuffer+cbReturned) = 0;
+        else
+            *(pbBuffer+cbBuffer-1) = 0;
+
+        // remove leading spaces (99-10-24) [umoeller]
+        while (*p == ' ')
+            p++;
+        if (p != pbBuffer)
+        {
+            // we have leading spaces:
+            strcpy(pbBuffer, p);
+            // decrease byte count
+            cbReturned -= (p - pbBuffer);
+        }
+
+        // remove trailing newlines
+        while (TRUE)
+        {
+            p = pbBuffer + strlen(pbBuffer) - 1;
+            if (    (*p == '\n')
+                 || (*p == '\r')
+               )
+            {
+                *p = '\0';
+            } else
+                break; // while (TRUE)
+        }
+    }
+
+    if (pcbMsg)
+        *pcbMsg = cbReturned;
+
+    return (arc);
+}
+
+/*
+ *@@ CompileMsgTable:
+ *      this gets called from tmfGetMessage.
+ *      Here we check for whether the given
+ *      message file has been compiled to the
+ *      EAs yet or if the compilation is
+ *      outdated.
+ *
+ *      If we need recompilation, we do this here.
+ *      Memory for the compiled message table is
+ *      allocated here using malloc(), and the
+ *      buffer is stored in ppbTableData. You
+ *      must free() this buffer after using this
+ *      function.
+ *
+ *      <B>Returns:</B>
+ *      --  ERROR_INVALID_PARAMETER
+ *      --  ERROR_BUFFER_OVERFLOW
+ *      --  ERROR_NOT_ENOUGH_MEMORY
+ *      --  ERROR_READ_FAULT
+ *      --  ERROR_INVALID_DATA
+ *
+ *@@changed V0.9.1 (99-12-12) [umoeller]: fixed heap overwrites which caused irregular crashes
+ *@@changed V0.9.1 (99-12-12) [umoeller]: fixed realloc failure
+ *@@changed V0.9.1 (99-12-12) [umoeller]: file last-write date is now reset when EAs are written
+ *@@changed V0.9.1 (2000-02-01) [umoeller]: now skipping leading spaces in msg
+ */
+
+APIRET CompileMsgTable(PSZ pszMessageFile,     // in: file to compile
+                       PBYTE *ppbTableDataReturn)  // out: compiled message table
+{
+    APIRET          rc = NO_ERROR;
+    // CHAR            szMessageFile[_MAX_PATH];
+
+    FILESTATUS3     fs3MessageFile;
+
+    // fixed UM 99-10-22: initializing all vars to 0
+    ULONG           ulStampLength = 0;
+    PBYTE           pbFileData = NULL;
+    ULONG           ulFileDataLength = 0;
+
+    CHAR            szFileStampOld[18] = "";     // yyyymmddhhmmssms.
+
+    CHAR            szFileStampCurrent[18] = "";
+
+    PBYTE           pbTableData = NULL;
+    ULONG           cbTableDataAllocated = 0;
+    ULONG           cbTableDataUsed = 0;
+
+    HFILE           hfileMessageFile = NULLHANDLE;
+    ULONG           ulAction = 0;
+    ULONG           ulBytesRead = 0;
+
+    COUNTRYCODE     cc = {0, 0};
+
+    ULONG           cbOpenTag = strlen(MSG_NAME_START);
+    ULONG           cbClosingTag = strlen(MSG_NAME_END);
+
+    do
+    {
+        PSZ             pCurrentNameStart = 0;
+        PSZ             pCurrentNameEnd = 0;
+        ULONG           ulCurrentMessagePos = 0;
+        ULONG           ulCurrentMessageLen = 0;
+        // BOOL            fError = FALSE;
+
+        // check parms
+        if ((!pszMessageFile) ||
+            (!ppbTableDataReturn))
+        {
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        // get length and timestamp of file
+        rc = DosQueryPathInfo(pszMessageFile,
+                              FIL_STANDARD,
+                              &fs3MessageFile,
+                              sizeof(fs3MessageFile));
+        if (rc != NO_ERROR)
+            break;
+        ulFileDataLength = fs3MessageFile.cbFile;
+
+        // determine current timestamp
+        GetTimeStamp(&fs3MessageFile, szFileStampCurrent, sizeof(szFileStampCurrent));
+
+        _Pmpf(("    Timestamp for %s: '%s'", pszMessageFile, szFileStampCurrent));
+
+        // determine saved timestamp
+        ulStampLength = sizeof(szFileStampOld);
+        rc = eahReadStringEA(pszMessageFile,
+                          EA_TIMESTAMP,
+                          szFileStampOld,
+                          &ulStampLength);
+
+        _Pmpf(("    Saved timestamp for %s: '%s'", pszMessageFile, szFileStampOld));
+
+        // compare timestamps
+        if ((rc == NO_ERROR)
+            && (ulStampLength == (strlen(szFileStampCurrent) + 1))
+            && (!strcmp(szFileStampCurrent, szFileStampOld))
+            )
+        {
+
+            // read table out of EAs
+            do
+            {
+                // get ea length of table
+                rc = eahReadStringEA(pszMessageFile,
+                                  EA_MSGTABLE,
+                                  NULL, &cbTableDataAllocated);
+                if (rc != ERROR_BUFFER_OVERFLOW)
+                    break;
+
+                // get memory
+                if ((pbTableData = (PBYTE)malloc(cbTableDataAllocated)) == NULL)
+                {
+                    rc = ERROR_NOT_ENOUGH_MEMORY;
+                    break;
+                }
+
+                // read table
+                rc = eahReadStringEA(pszMessageFile,
+                                  EA_MSGTABLE,
+                                  pbTableData, &cbTableDataAllocated);
+
+            }
+            while (FALSE);
+
+            // if no error occurred, we are finished
+            if (rc == NO_ERROR)
+            {
+                _Pmpf(("      --> using precompiled table"));
+                break;
+            }
+        } // end if
+
+        _Pmpf(("      --> recompiling table"));
+
+        // recompilation needed:
+        // get memory for file data
+        if ((pbFileData = (PBYTE)malloc(ulFileDataLength + 1)) == NULL)
+        {
+            rc = ERROR_NOT_ENOUGH_MEMORY;
+            break;
+        }
+        *(pbFileData + ulFileDataLength) = 0;
+
+        // get memory for table data
+        cbTableDataAllocated = ulFileDataLength / 2;
+        if ((pbTableData = (PBYTE)malloc(cbTableDataAllocated)) == NULL)
+        {
+            rc = ERROR_NOT_ENOUGH_MEMORY;
+            break;
+        }
+
+        *pbTableData = 0;
+                // fixed V0.9.1 (99-12-12);
+                // this was
+                //      *(pbTableData + cbTableDataAllocated) = 0;
+                // since we're using "strcat" below to write into
+                // the table data, this wasn't such a good idea
+
+        // open file and read it
+        rc = DosOpen(pszMessageFile,
+                     &hfileMessageFile,
+                     &ulAction,
+                     0, 0,
+                     OPEN_ACTION_FAIL_IF_NEW
+                     | OPEN_ACTION_OPEN_IF_EXISTS,
+                     OPEN_FLAGS_FAIL_ON_ERROR
+                     | OPEN_SHARE_DENYWRITE
+                     | OPEN_ACCESS_READWRITE,   // needed for EA attachement
+                      NULL);
+        if (rc != NO_ERROR)
+            break;
+
+        rc = DosRead(hfileMessageFile,
+                     pbFileData,
+                     ulFileDataLength,
+                     &ulBytesRead);
+        if (rc != NO_ERROR)
+            break;
+        if (ulBytesRead != ulFileDataLength)
+        {
+            rc = ERROR_READ_FAULT;
+            break;
+        }
+
+        // search first message name
+        // this will automatically skip comments
+        // at the beginning
+        pCurrentNameStart = strstr(pbFileData, MSG_NAME_START);
+
+        if (!pCurrentNameStart)
+        {
+            rc = ERROR_INVALID_DATA;
+            break;
+        }
+        else
+            pCurrentNameStart += cbOpenTag;
+                // this points to message ID string after "<--" now
+
+        // is first name complete ?
+        pCurrentNameEnd = strstr(pCurrentNameStart, MSG_NAME_END);
+        if (!pCurrentNameEnd)
+        {
+            rc = ERROR_INVALID_DATA;
+            break;
+        }
+
+        // scan through all names
+        while (     (pCurrentNameStart)
+                 && (*pCurrentNameStart)
+              )
+        {
+            PSZ     pNextNameStart = 0,
+                    pNextCommentStart = 0,
+                    pEndOfMsg = 0;
+            CHAR    szEntry[500] = "";
+
+            // search end of name, if not exist, skip end of file
+            pCurrentNameEnd = strstr(pCurrentNameStart, MSG_NAME_END);
+                // this points to the closing "-->" now
+            if (!pCurrentNameEnd)
+                break;
+
+            // search next name (ID), if none, use end of string
+            pNextNameStart = strstr(pCurrentNameEnd, MSG_NAME_START);
+                    // points to next "<--" now
+            if (!pNextNameStart)
+                // not found:
+                // use terminating null byte
+                pNextNameStart = pCurrentNameStart + strlen(pCurrentNameStart);
+            else
+                // found:
+                // point to next message ID string
+                pNextNameStart += cbOpenTag;
+
+            pEndOfMsg = pNextNameStart - cbOpenTag;
+
+            // search next comment (99-11-28) [umoeller]
+            pNextCommentStart = strstr(pCurrentNameEnd, MSG_COMMENT_LINE);
+            if (pNextCommentStart)
+                // another comment found:
+                if (pNextCommentStart < pNextNameStart)
+                    // if it's even before the next key, use that
+                    // for the end of the current string; otherwise
+                    // the comment would appear in the message too
+                    pEndOfMsg = pNextCommentStart;
+
+            // now we have:
+            // -- pCurrentNameStart: points to message ID after "<--"
+            // -- pCurrentNameEnd: points to "-->"
+            // -- pNextNameStart: points to _next_ message ID after "<--"
+            // -- pNextCommentStart: points to _next_ comment
+            // -- pEndOfMsg: either pNextNameStart-cbOpenTag
+            //               or pNextCommentStart,
+            //               whichever comes first
+
+            // calculate table entry data
+            *pCurrentNameEnd = 0;
+            ulCurrentMessagePos = (pCurrentNameEnd - pbFileData) + cbClosingTag;
+            // this points to the first character after the <-- --> tag now;
+            // if this points to a space character, take next char
+            // (V0.9.1 (2000-02-13) [umoeller])
+            while (     (*(pbFileData + ulCurrentMessagePos))
+                     && (*(pbFileData + ulCurrentMessagePos) == ' ')
+                  )
+                ulCurrentMessagePos++;
+
+            ulCurrentMessageLen = (pEndOfMsg - pbFileData) - ulCurrentMessagePos;
+                    // pszNextNameStart - pbFileData - ulCurrentMessagePos - 1;
+
+            // determine entry
+            sprintf(szEntry, "%s %u %u" NEWLINE,
+                    pCurrentNameStart,
+                    ulCurrentMessagePos,
+                    ulCurrentMessageLen);
+
+            _Pmpf(("Found %s at %d, length %d",
+                    pCurrentNameStart,
+                    ulCurrentMessagePos,
+                    ulCurrentMessageLen));
+
+            // need more space ?
+            if ((cbTableDataUsed + strlen(szEntry) + 1) > cbTableDataAllocated)
+            {
+                PBYTE           pbTmp;
+                _Pmpf(("  Re-allocating!!"));
+
+                cbTableDataAllocated += ulFileDataLength / 2;
+                pbTmp = (PBYTE)realloc(pbTableData, cbTableDataAllocated);
+                if (!pbTmp)
+                {
+                    rc = ERROR_NOT_ENOUGH_MEMORY;
+                    break;
+                }
+                else
+                    pbTableData = pbTmp;
+            }
+
+            // add entry
+            strcat(pbTableData, szEntry);
+            cbTableDataUsed += strlen(szEntry);
+                    // added V0.9.1 (99-12-12);
+                    // this was 0 all the time and never raised
+
+            // adress next entry
+            pCurrentNameStart = pNextNameStart;
+
+        }  // while (pszCurrentNameStart)
+
+        // write new timestamp and table
+        // ### handle 64 kb limit here !!!
+        if (rc == NO_ERROR)
+        {
+            FILESTATUS3 fs3Tmp;
+
+            rc = eahWriteStringEA(hfileMessageFile, EA_TIMESTAMP, szFileStampCurrent);
+            rc = eahWriteStringEA(hfileMessageFile, EA_MSGTABLE, pbTableData);
+
+            if (rc == NO_ERROR)
+            {
+                // fixed V0.9.1 (99-12-12): we need to reset the file date to
+                // the old date,
+                // because writing EAs updates the "last write date" also;
+                // without this, we'd recompile every single time
+                rc = DosQueryFileInfo(hfileMessageFile,
+                                      FIL_STANDARD,
+                                      &fs3Tmp,
+                                      sizeof(fs3Tmp));
+                if (rc == NO_ERROR)
+                {
+                    // restore original "last write" date
+                    _Pmpf(("Resetting file date"));
+                    fs3Tmp.fdateLastWrite = fs3MessageFile.fdateLastWrite;
+                    fs3Tmp.ftimeLastWrite = fs3MessageFile.ftimeLastWrite;
+                    DosSetFileInfo(hfileMessageFile,
+                                   FIL_STANDARD,
+                                   &fs3Tmp,
+                                   sizeof(fs3Tmp));
+                }
+            }
+        }
+    } while (FALSE);
+
+
+    if (rc == NO_ERROR)
+    {
+        // hand over result
+        *ppbTableDataReturn = pbTableData;
+
+        // make text uppercase
+        rc = DosMapCase(cbTableDataAllocated, &cc, pbTableData);
+    }
+    else
+    {
+        // error occured:
+        if (pbTableData)
+            free(pbTableData);
+    }
+
+    // cleanup
+    if (pbFileData)
+        free(pbFileData);
+    if (hfileMessageFile)
+        DosClose(hfileMessageFile);
+
+    return rc;
+}
+
+/*
+ * GetTimeStamp:
+ *      this returns the "last write date" timestamp
+ *      of the file specified in pfs3.
+ *
+ *      <B>Usage:</B> called from CompileMsgTable
+ *      to check whether the compilation is
+ *      outdated.
+ *
+ *      Returns:
+ *      --  NO_ERROR
+ *      --  ERROR_INVALID_PARAMETER
+ *      --  ERROR_BUFFER_OVERFLOW: pszBuffer too small for timestamp
+ */
+
+APIRET GetTimeStamp(PFILESTATUS3 pfs3,     // in: file to check
+                    PSZ pszBuffer,    // out: timestamp
+                    ULONG ulBufferlen)    // in: sizeof(*pszBuffer)
+{
+
+    APIRET          rc = NO_ERROR;
+    CHAR            szTimeStamp[15];
+    static PSZ      pszFormatTimestamp = "%4u%02u%02u%02u%02u%02u%";
+
+    do
+    {
+        // check parms
+        if ((!pfs3) ||
+            (!pszBuffer))
+        {
+            rc = ERROR_INVALID_PARAMETER;
+            break;
+        }
+
+        // create stamp
+        sprintf(szTimeStamp,
+                pszFormatTimestamp,
+                pfs3->fdateLastWrite.year + 1980,
+                pfs3->fdateLastWrite.month,
+                pfs3->fdateLastWrite.day,
+                pfs3->ftimeLastWrite.hours,
+                pfs3->ftimeLastWrite.minutes,
+                pfs3->ftimeLastWrite.twosecs * 2);
+
+        // check bufferlen
+        if (strlen(szTimeStamp) + 1 > ulBufferlen)
+        {
+            rc = ERROR_BUFFER_OVERFLOW;
+            break;
+        }
+
+        // hand over result
+        strcpy(pszBuffer, szTimeStamp);
+    }
+    while (FALSE);
+
+    return rc;
+}
+
Index: /trunk/src/helpers/tree.c
===================================================================
--- /trunk/src/helpers/tree.c	(revision 8)
+++ /trunk/src/helpers/tree.c	(revision 8)
@@ -0,0 +1,981 @@
+
+/*
+ *@@sourcefile tree.c:
+ *      contains helper functions for maintaining 'Red-Black' balanced
+ *      binary trees.
+ *      See explanations below.
+ *
+ *      This file is all new with V0.9.5 (2000-09-29) [umoeller].
+ *
+ *      Usage: All C programs; not OS/2-specific.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  tree*    tree helper functions
+ *
+ *      This has been taken from the Standard Function Library (SFL)
+ *      by iMatix Corporation and changed to user the "id" member for
+ *      tree sorting/comparison.
+ *
+ *      <B>Using binary trees</B>
+ *
+ *      Each TREE node does not only contain data, but also an
+ *      "id" field. The order of nodes in the tree is determined
+ *      by calling a node comparison function provided by the caller
+ *      (which you must write). This takes two "id" values and must
+ *      return:
+ *
+ +           0: id1 == id2
+ +          -1: id1 < id2
+ +          +1: id1 > id2
+ *
+ *      Initialize the root of the tree with treeInit(). Then
+ *      add nodes to the tree with treeInsertID() and remove nodes
+ *      with treeDelete().
+ *
+ *      You can test whether a tree is empty by comparing its
+ *      root with TREE_NULL.
+ *
+ *      Compared to linked lists (as implemented by linklist.c),
+ *      trees allow for much faster searching.
+ *
+ *      Assuming a linked list contains N items, then searching the
+ *      list for an item will take an average of N/2 comparisons
+ *      and even N comparisons if the item cannot be found (unless
+ *      you keep the list sorted, but linklist.c doesn't do this).
+ *
+ *      According to "Algorithms in C", a search in a balanced
+ *      "red-black" binary tree takes about lg N comparisons on
+ *      average, and insertions take less than one rotation on
+ *      average.
+ *
+ *      Example: You need to build a list of files, and you
+ *      will search the list frequently according to the file
+ *      handles. This would make the handle an ideal "id" field.
+ *
+ *      Differences compared to linklist.c:
+ *
+ *      -- As opposed to a LISTNODE, the TREE structure (which
+ *         represents a tree node) does not contain a data pointer.
+ *         Instead, all tree nodes are assumed to contain the
+ *         data themselves. As a result, you must define your
+ *         own structures which start with a TREE structure.
+ *
+ *         See treeInsertID() for samples.
+ *
+ *      -- You must supply a comparison function to allow the
+ *         tree functions to sort the tree.
+ *
+ *@@added V0.9.5 (2000-09-29) [umoeller]
+ *@@header "helpers\tree.h"
+ */
+
+/*
+ *      Written:    97/11/18  Jonathan Schultz <jonathan@imatix.com>
+ *      Revised:    98/12/08  Jonathan Schultz <jonathan@imatix.com>
+ *
+ *      Copyright (C) 1991-99 iMatix Corporation.
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+/*
+ *@@category: Helpers\C helpers\Red-black balanced binary trees
+ */
+
+#include "setup.h"
+#include "helpers\tree.h"
+
+//  Constants
+TREE    TREE_EMPTY = {TREE_NULL, TREE_NULL, NULL, BLACK};
+
+//  Internal function prototypes
+static void insert_fixup(TREE **root, TREE *tree);
+static void rotate_left(TREE **root, TREE *tree);
+static void rotate_right(TREE **root, TREE *tree);
+static void delete_fixup(TREE **root, TREE *tree);
+
+/*
+ *@@ treeInit:
+ *      initializes an empty tree. The data on the
+ *      tree will be invalid, and no memory will be
+ *      freed.
+ *
+ *      Usage:
+ +          TREE *TreeRoot;
+ +          treeInit(&TreeRoot);
+ */
+
+void treeInit(TREE **root)
+{
+    *root = TREE_NULL;
+}
+
+/*
+ *@@ treeInsertID:
+ *      inserts a node into an existing tree.
+ *
+ *      Note: A tree node MUST contain a TREE structure
+ *      at the beginning for the tree functions to work.
+ *      So to create a tree node with usable data, do this:
+ *
+ +          typedef _MYTREENODE
+ +          {
+ +              // TREE must be at beginning
+ +              TREE        Tree;
+ +              // now use whatever you want
+ +              CHAR        szMyExtraData[100];
+ +          } MYTREENODE, *PMYTREENODE;
+ *
+ *      When calling the tree functions, manually cast your
+ *      MYTREENODE pointers to (TREE*).
+ *
+ *      This function initialises the node pointers and colour
+ *      in the TREE structure to correct values, so the caller
+ *      does not have to worry about those.
+ *
+ *      However you must initialize the TREE.id member correctly
+ *      so that your comparison function can compare on that
+ *      to find the correct place in the tree to insert the node.
+ *
+ *      Usage:
+ +          TREE *TreeRoot;
+ +          treeInit(&TreeRoot);
+ +
+ +          PMYTREENODE pTreeItem = malloc(sizeof(MYTREENODE));
+ +          pTreeItem->Tree.id = 1;
+ +
+ +          treeInsertID(&TreeRoot,
+ +                     (TREE*)pTreeItem,
+ +                     fnCompare,       // your comparison function
+ +                     FALSE);
+ *
+ *      Returns:
+ *
+ *      -- TREE_OK: OK, item inserted.
+ *
+ *      -- TREE_DUPLICATE: if (fAllowDuplicates == FALSE), this is
+ *          returned if a tree item with the specified ID already
+ *          exists.
+ */
+
+int treeInsertID(TREE **root,             // in: root of tree
+                 TREE *tree,              // in: new tree node
+                 FNTREE_COMPARE_IDS *comp,      // in: comparison function
+                 BOOL fAllowDuplicates)   // in: whether duplicates with the same ID are allowed
+{
+    TREE
+       *current,
+       *parent;
+    int
+        last_comp = 0;
+
+    // find where node belongs
+    current = *root;
+    parent  = NULL;
+    while (current != TREE_NULL)
+    {
+        parent  = current;
+        last_comp = comp(tree->id, current->id);
+        switch (last_comp)
+        {
+            case -1: current = current->left;  break;
+            case  1: current = current->right; break;
+            default: if (fAllowDuplicates)
+                         current = current->left;
+                     else
+                         return TREE_DUPLICATE;
+
+        }
+    }
+
+    // setup new node
+    ((TREE *) tree)->parent = parent;
+    ((TREE *) tree)->left   = TREE_NULL;
+    ((TREE *) tree)->right  = TREE_NULL;
+    ((TREE *) tree)->colour = RED;
+
+    // insert node in tree
+    if (parent)
+        switch (last_comp)
+        {
+            case  1: parent->right = tree;  break;
+            default: parent->left  = tree;
+        }
+    else
+        *root = tree;
+
+    insert_fixup(root, tree);
+    return(TREE_OK);
+}
+
+/*
+ *@@ treeInsertNode:
+ *      similar to treeInsertID, but this uses
+ *      a comparision function which compares
+ *      nodes.
+ */
+
+int treeInsertNode(TREE **root,             // in: root of tree
+                   TREE *tree,              // in: new tree node
+                   FNTREE_COMPARE_NODES *comp,  // in: comparison function
+                   BOOL fAllowDuplicates)   // in: whether duplicates with the same ID are allowed
+{
+    TREE
+       *current,
+       *parent;
+    int
+        last_comp = 0;
+
+    // find where node belongs
+    current = *root;
+    parent  = NULL;
+    while (current != TREE_NULL)
+    {
+        parent  = current;
+        last_comp = comp(tree, current);
+        switch (last_comp)
+        {
+            case -1: current = current->left;  break;
+            case  1: current = current->right; break;
+            default: if (fAllowDuplicates)
+                         current = current->left;
+                     else
+                         return TREE_DUPLICATE;
+
+        }
+    }
+
+    // setup new node
+    ((TREE *) tree)->parent = parent;
+    ((TREE *) tree)->left   = TREE_NULL;
+    ((TREE *) tree)->right  = TREE_NULL;
+    ((TREE *) tree)->colour = RED;
+
+    // insert node in tree
+    if (parent)
+        switch (last_comp)
+        {
+            case  1: parent->right = tree;  break;
+            default: parent->left  = tree;
+        }
+    else
+        *root = tree;
+
+    insert_fixup(root, tree);
+    return(TREE_OK);
+}
+
+/*
+ * insert_fixup:
+ *      maintains the Red-Black tree balance after a node has been inserted.
+ *
+ *      Private function.
+ */
+
+static void insert_fixup(TREE **root,
+                         TREE *tree)
+{
+    TREE *uncle;
+
+    // check red-black properties
+    while ((tree != *root)
+       &&  (tree->parent->colour == RED))
+    {
+        // we have a violation
+        if (tree->parent == tree->parent->parent->left)
+        {
+            uncle = tree->parent->parent->right;
+            if (uncle->colour == RED)
+            {
+                // uncle is RED
+                tree ->parent->colour = BLACK;
+                uncle->colour = BLACK;
+                tree ->parent->parent->colour = RED;
+
+                tree = tree->parent->parent;
+            }
+            else
+            {
+                // uncle is BLACK
+                if (tree == tree->parent->right)
+                {
+                    // make tree a left child
+                    tree = tree->parent;
+                    rotate_left (root, tree);
+                }
+
+                // recolor and rotate
+                tree->parent->colour = BLACK;
+                tree->parent->parent->colour = RED;
+                rotate_right (root, tree->parent->parent);
+            }
+        }
+        else
+        {
+            // mirror image of above code
+            uncle = tree->parent->parent->left;
+            if (uncle->colour == RED)
+            {
+                // uncle is RED
+                tree ->parent->colour = BLACK;
+                uncle->colour = BLACK;
+                tree ->parent->parent->colour = RED;
+
+                tree = tree->parent->parent;
+            }
+            else
+            {
+                // uncle is BLACK
+                if (tree == tree->parent->left)
+                {
+                    tree = tree->parent;
+                    rotate_right (root, tree);
+                }
+                tree->parent->colour = BLACK;
+                tree->parent->parent->colour = RED;
+                rotate_left (root, tree->parent->parent);
+            }
+        }
+    }
+    (*root)->colour = BLACK;
+}
+
+/*
+ * rotate_left:
+ *      rotates tree to left.
+ *
+ *      Private function.
+ */
+
+static void rotate_left(TREE **root,
+                        TREE *tree)
+{
+    TREE *other = tree->right;
+
+    // establish tree->right link
+    tree->right = other->left;
+    if (other->left != TREE_NULL)
+        other->left->parent = tree;
+
+    // establish other->parent link
+    if (other != TREE_NULL)
+        other->parent = tree->parent;
+
+    if (tree->parent)
+    {
+        if (tree == tree->parent->left)
+            tree->parent->left  = other;
+        else
+            tree->parent->right = other;
+    }
+    else
+        *root = other;
+
+    // link tree and other
+    other->left = tree;
+    if (tree != TREE_NULL)
+        tree->parent = other;
+}
+
+/*
+ * rotate_right:
+ *      rotates tree to right.
+ *
+ *      Private function.
+ */
+
+static void rotate_right(TREE **root,
+                         TREE *tree)
+{
+    TREE *other;
+
+    other = tree->left;
+
+    // establish tree->left link
+    tree->left = other->right;
+    if (other->right != TREE_NULL)
+        other->right->parent = tree;
+
+    // establish other->parent link
+    if (other != TREE_NULL)
+        other->parent = tree->parent;
+
+    if (tree->parent)
+    {
+        if (tree == tree->parent->right)
+            tree->parent->right = other;
+        else
+            tree->parent->left  = other;
+    }
+    else
+        *root = other;
+
+    // link tree and other
+    other->right = tree;
+    if (tree != TREE_NULL)
+        tree->parent = other;
+}
+
+/*
+ *@@ treeDelete:
+ *      deletes a node from a tree. Does not deallocate any memory.
+ *
+ *      Returns:
+ *
+ *      -- TREE_OK: node deleted.
+ *      -- TREE_INVALID_NODE: tree node not found.
+ */
+
+int treeDelete(TREE **root,     // in: root of tree
+               TREE *tree)      // in: tree node to delete
+{
+    int irc = TREE_OK;
+
+    TREE
+       *youngest, *descendent;
+    TREE_COLOUR
+        colour;
+
+    if (    (!tree)
+         || (tree == TREE_NULL)
+       )
+        return TREE_INVALID_NODE;
+
+    if (    (((TREE *) tree)->left  == TREE_NULL)
+         || (((TREE *) tree)->right == TREE_NULL)
+       )
+        // descendent has a TREE_NULL node as a child
+        descendent = tree;
+    else
+  {
+        // find tree successor with a TREE_NULL node as a child
+        descendent = ((TREE *) tree)->right;
+        while (descendent->left != TREE_NULL)
+            descendent = descendent->left;
+  }
+
+    // youngest is descendent's only child, if there is one, else TREE_NULL
+    if (descendent->left != TREE_NULL)
+        youngest = descendent->left;
+    else
+        youngest = descendent->right;
+
+    // remove descendent from the parent chain
+    if (youngest != TREE_NULL)
+        youngest->parent = descendent->parent;
+    if (descendent->parent)
+        if (descendent == descendent->parent->left)
+            descendent->parent->left  = youngest;
+        else
+            descendent->parent->right = youngest;
+    else
+        *root = youngest;
+
+    colour = descendent->colour;
+
+    if (descendent != (TREE *) tree)
+  {
+        // Conceptually what we are doing here is moving the data from
+        // descendent to tree.  In fact we do this by linking descendent
+        // into the structure in the place of tree.
+        descendent->left   = ((TREE *) tree)->left;
+        descendent->right  = ((TREE *) tree)->right;
+        descendent->parent = ((TREE *) tree)->parent;
+        descendent->colour = ((TREE *) tree)->colour;
+
+        if (descendent->parent)
+      {
+            if (tree == descendent->parent->left)
+                descendent->parent->left  = descendent;
+            else
+                descendent->parent->right = descendent;
+      }
+        else
+            *root = descendent;
+
+        if (descendent->left != TREE_NULL)
+            descendent->left->parent = descendent;
+
+        if (descendent->right != TREE_NULL)
+            descendent->right->parent = descendent;
+    }
+
+    if (    (youngest != TREE_NULL)
+        &&  (colour   == BLACK))
+        delete_fixup (root, youngest);
+
+    return (irc);
+}
+
+/*
+ *@@ delete_fixup:
+ *      maintains Red-Black tree balance after deleting a node.
+ *
+ *      Private function.
+ */
+
+static void delete_fixup(TREE **root,
+                         TREE *tree)
+{
+    TREE
+       *sibling;
+
+    while (tree != *root && tree->colour == BLACK)
+    {
+        if (tree == tree->parent->left)
+        {
+            sibling = tree->parent->right;
+            if (sibling->colour == RED)
+            {
+                sibling->colour = BLACK;
+                tree->parent->colour = RED;
+                rotate_left (root, tree->parent);
+                sibling = tree->parent->right;
+            }
+            if ((sibling->left->colour == BLACK)
+            &&  (sibling->right->colour == BLACK))
+            {
+                sibling->colour = RED;
+                tree = tree->parent;
+            }
+            else
+            {
+                if (sibling->right->colour == BLACK)
+                {
+                    sibling->left->colour = BLACK;
+                    sibling->colour = RED;
+                    rotate_right (root, sibling);
+                    sibling = tree->parent->right;
+                }
+                sibling->colour = tree->parent->colour;
+                tree->parent->colour = BLACK;
+                sibling->right->colour = BLACK;
+                rotate_left (root, tree->parent);
+                tree = *root;
+            }
+        }
+        else
+        {
+            sibling = tree->parent->left;
+            if (sibling->colour == RED)
+            {
+                sibling->colour = BLACK;
+                tree->parent->colour = RED;
+                rotate_right (root, tree->parent);
+                sibling = tree->parent->left;
+            }
+            if ((sibling->right->colour == BLACK)
+            &&  (sibling->left->colour == BLACK))
+            {
+                sibling->colour = RED;
+                tree = tree->parent;
+            }
+            else
+            {
+                if (sibling->left->colour == BLACK)
+                {
+                    sibling->right->colour = BLACK;
+                    sibling->colour = RED;
+                    rotate_left (root, sibling);
+                    sibling = tree->parent->left;
+                }
+                sibling->colour = tree->parent->colour;
+                tree->parent->colour = BLACK;
+                sibling->left->colour = BLACK;
+                rotate_right (root, tree->parent);
+                tree = *root;
+            }
+        }
+    }
+    tree->colour = BLACK;
+}
+
+/*
+ *@@ treeFindEQID:
+ *      finds a node with ID exactly matching that provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree node IDs.
+ */
+
+void* treeFindEQID(TREE **root,
+                   unsigned long id,
+                   FNTREE_COMPARE_IDS *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current->id, id))
+        {
+            case -1: current = current->right; break;
+            case  1: current = current->left;  break;
+            default: found = current;           //  In case of duplicates,
+                     current = current->left;  //  get the first one.
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindGEID:
+ *      finds a node with ID greater than or equal to provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree node IDs.
+ */
+
+void* treeFindGEID(TREE **root,
+                   unsigned long idFind,
+                   FNTREE_COMPARE_IDS *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current->id, idFind))
+        {
+            case -1: current = current->right; break;
+            default: found = current;
+                     current = current->left;
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindEQNode:
+ *      finds a node with ID exactly matching that provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree nodes.
+ */
+
+void* treeFindEQNode(TREE **root,
+                     TREE *nodeFind,
+                     FNTREE_COMPARE_NODES *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current, nodeFind))
+        {
+            case -1: current = current->right; break;
+            case  1: current = current->left;  break;
+            default: found = current;           //  In case of duplicates,
+                     current = current->left;  //  get the first one.
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindGENode:
+ *      finds a node with ID greater than or equal to provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree nodes.
+ */
+
+void* treeFindGENode(TREE **root,
+                     TREE *nodeFind,
+                     FNTREE_COMPARE_NODES *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current, nodeFind))
+        {
+            case -1: current = current->right; break;
+            default: found = current;
+                     current = current->left;
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindLTNode:
+ *      finds a node with Node less than provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree nodes.
+ */
+
+void* treeFindLTNode(TREE **root,
+                     TREE *nodeFind,
+                     FNTREE_COMPARE_NODES *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current, nodeFind))
+        {
+            case -1: found = current;
+                     current = current->right; break;
+            default: current = current->left;
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindLENode:
+ *      finds a node with Node less than or equal to provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree nodes.
+ */
+
+void* treeFindLENode(TREE **root,
+                     TREE *nodeFind,
+                     FNTREE_COMPARE_NODES *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current, nodeFind))
+        {
+            case 1 : current = current->left;  break;
+            default: found = current;
+                     current = current->right;
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindGTNode:
+ *      finds a node with Node greater than provided.
+ *      To find a tree node, your comparison function must
+ *      compare the tree nodes.
+ */
+
+void* treeFindGTNode(TREE **root,
+                     TREE *nodeFind,
+                     FNTREE_COMPARE_NODES *comp)
+{
+    TREE
+       *current = *root,
+       *found;
+
+    found = NULL;
+    while (current != TREE_NULL)
+        switch (comp(current, nodeFind))
+        {
+            case 1 : found = current;
+                     current = current->left; break;
+            default: current = current->right;
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeFindEQID:
+ *      finds a node with data exactly matching that provided.
+ *      To find a tree node, your comparison function must
+ *      compare a tree member with external data.
+ *
+ *      This is useful for finding a tree item from a string ID.
+ *
+ *      Make sure to use treeInsertNode and compare according
+ *      to a string member, and then write a second compare
+ *      function for this function which compares the string
+ *      member to an external string.
+ */
+
+void* treeFindEQData(TREE **root,
+                     void *pData,
+                     FNTREE_COMPARE_DATA *comp)
+{
+    TREE    *current = *root,
+            *found = NULL;
+
+    while (current != TREE_NULL)
+        switch (comp(current, pData))
+        {
+            case -1: current = current->right; break;
+            case  1: current = current->left;  break;
+            default: found = current;           //  In case of duplicates,
+                     current = current->left;  //  get the first one.
+        }
+
+    return found;
+}
+
+/*
+ *@@ treeTraverse:
+ *      traverses the specified tree, calling a processing function
+ *      for each tree node.
+ *
+ *      The processing function ("process") must be declared as
+ *      follows:
+ *
+ +          void fnProcess(TREE *t,         // current tree node
+ +                         void *pUser);    // user data
+ *
+ *      and will receive the "pUser" parameter, which you can use
+ *      as a data pointer to some structure for whatever you like.
+ */
+
+void treeTraverse(TREE *tree,
+                  TREE_PROCESS *process,
+                  void *pUser,
+                  int method)
+{
+    if ((!tree)
+    ||  (tree == TREE_NULL))
+        return;
+
+    if (method == 1)
+    {
+        process(tree, pUser);
+        treeTraverse (((TREE *) tree)->left,  process, pUser, method);
+        treeTraverse (((TREE *) tree)->right, process, pUser, method);
+    }
+    else if (method == 2)
+    {
+        treeTraverse (((TREE *) tree)->left,  process, pUser, method);
+        treeTraverse (((TREE *) tree)->right, process, pUser, method);
+        process(tree, pUser);
+    }
+    else
+    {
+        treeTraverse (((TREE *) tree)->left,  process, pUser, method);
+        process(tree, pUser);
+        treeTraverse (((TREE *) tree)->right, process, pUser, method);
+    }
+}
+
+/*
+ *@@ treeFirst:
+ *      finds and returns the first node in a (sub-)tree.
+ */
+
+void* treeFirst(TREE *tree)
+{
+    TREE
+       *current;
+
+    if ((!tree)
+    ||  (tree == TREE_NULL))
+        return NULL;
+
+    current = tree;
+    while (current->left != TREE_NULL)
+        current = current->left;
+
+    return current;
+}
+
+/*
+ *@@ treeLast:
+ *      finds and returns the last node in a (sub-)tree.
+ */
+
+void* treeLast(TREE *tree)
+{
+    TREE
+       *current;
+
+    if ((!tree)
+    ||  (tree == TREE_NULL))
+        return NULL;
+
+    current = tree;
+    while (current->right != TREE_NULL)
+        current = current->right;
+
+    return current;
+}
+
+/*
+ *@@ treeNext:
+ *      finds and returns the next node in a tree.
+ */
+
+void* treeNext(TREE *tree)
+{
+    TREE
+       *current,
+       *child;
+
+    if ((!tree)
+    ||  (tree == TREE_NULL))
+        return NULL;
+
+    current = tree;
+    if (current->right != TREE_NULL)
+        return treeFirst (current->right);
+    else
+    {
+        current = tree;
+        child   = TREE_NULL;
+        while ((current->parent)
+           &&  (current->right == child))
+        {
+            child = current;
+            current = current->parent;
+        }
+        if (current->right != child)
+            return current;
+        else
+            return NULL;
+    }
+}
+
+/*
+ *@@ treePrev:
+ *      finds and returns the previous node in a tree.
+ */
+
+void* treePrev(TREE *tree)
+{
+    TREE
+       *current,
+       *child;
+
+    if ((!tree)
+    ||  (tree == TREE_NULL))
+        return NULL;
+
+    current = tree;
+    if (current->left != TREE_NULL)
+        return treeLast (current->left);
+    else
+    {
+        current = tree;
+        child   = TREE_NULL;
+        while ((current->parent)
+           &&  (current->left == child))
+        {
+            child = current;
+            current = current->parent;
+        }
+        if (current->left != child)
+            return current;
+        else
+            return NULL;
+    }
+}
+
+
Index: /trunk/src/helpers/winh.c
===================================================================
--- /trunk/src/helpers/winh.c	(revision 8)
+++ /trunk/src/helpers/winh.c	(revision 8)
@@ -0,0 +1,3941 @@
+
+/*
+ *@@sourcefile winh.c:
+ *      contains Presentation Manager helper functions that are
+ *      independent of a single application, i.e. these can be
+ *      used w/out the rest of the XWorkplace source in any PM
+ *      program.
+ *
+ *      Usage: All PM programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  winh*   Win (Presentation Manager) helper functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\winh.h"
+ */
+
+/*
+ *      Copyright (C) 1997-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOSDEVIOCTL
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WIN
+#define INCL_GPI
+
+// spooler #include's
+#define INCL_BASE
+#define INCL_SPL
+#define INCL_SPLDOSPRINT
+#define INCL_SPLERRORS
+
+#include <os2.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+#include "helpers\winh.h"
+#include "helpers\prfh.h"
+#include "helpers\gpih.h"
+#include "helpers\stringh.h"
+#include "helpers\undoc.h"
+#include "helpers\xstring.h"            // extended string helpers
+
+/*
+ *@@category: Helpers\PM helpers\Menu helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Menu helpers                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhInsertMenuItem:
+ *      this inserts one one menu item into a given menu.
+ *
+ *      Returns the return value of the MM_INSERTITEM msg:
+ *      --  MIT_MEMERROR:    space allocation for menu item failed
+ *      --  MIT_ERROR:       other error
+ *      --  other:           zero-based index of new item in menu.
+ */
+
+SHORT winhInsertMenuItem(HWND hwndMenu,     // in:  menu to insert item into
+                         SHORT iPosition,   // in:  zero-based index of where to
+                                            //      insert or MIT_END
+                         SHORT sItemId,     // in:  ID of new menu item
+                         PSZ pszItemTitle,  // in:  title of new menu item
+                         SHORT afStyle,
+                            // in:  MIS_* style flags.
+                            // Valid menu item styles are:
+                            // --  MIS_SUBMENU
+                            // --  MIS_SEPARATOR
+                            // --  MIS_BITMAP: the display object is a bit map.
+                            // --  MIS_TEXT: the display object is a text string.
+                            // --  MIS_BUTTONSEPARATOR:
+                            //          The item is a menu button. Any menu can have zero,
+                            //          one, or two items of this type.  These are the last
+                            //          items in a menu and are automatically displayed after
+                            //          a separator bar. The user cannot move the cursor to
+                            //          these items, but can select them with the pointing
+                            //          device or with the appropriate key.
+                            // --  MIS_BREAK: the item begins a new row or column.
+                            // --  MIS_BREAKSEPARATOR:
+                            //          Same as MIS_BREAK, except that it draws a separator
+                            //          between rows or columns of a pull-down menu.
+                            //          This style can only be used within a submenu.
+                            // --  MIS_SYSCOMMAND:
+                            //          menu posts a WM_SYSCOMMAND message rather than a
+                            //          WM_COMMAND message.
+                            // --  MIS_OWNERDRAW:
+                            //          WM_DRAWITEM and WM_MEASUREITEM notification messages
+                            //          are sent to the owner to draw the item or determine its size.
+                            // --  MIS_HELP:
+                            //          menu posts a WM_HELP message rather than a
+                            //          WM_COMMAND message.
+                            // --  MIS_STATIC
+                            //          This type of item exists for information purposes only.
+                            //          It cannot be selected with the pointing device or
+                            //          keyboard.
+                         SHORT afAttr)
+                            // in:  MIA_* attribute flags
+                            // Valid menu item attributes (afAttr) are:
+                            // --  MIA_HILITED: if and only if, the item is selected.
+                            // --  MIA_CHECKED: a check mark appears next to the item (submenu only).
+                            // --  MIA_DISABLED: item is disabled and cannot be selected.
+                            //         The item is drawn in a disabled state (gray).
+                            // --  MIA_FRAMED: a frame is drawn around the item (top-level menu only).
+                            // --  MIA_NODISMISS:
+                            //          if the item is selected, the submenu remains down. A menu
+                            //          with this attribute is not hidden until the  application
+                            //          or user explicitly does so, for example by selecting either
+                            //          another menu on the action bar or by pressing the escape key.
+{
+    MENUITEM mi;
+    SHORT    src = MIT_ERROR;
+
+    mi.iPosition = iPosition;
+    mi.afStyle = afStyle;
+    mi.afAttribute = afAttr;
+    mi.id = sItemId;
+    mi.hwndSubMenu = 0;
+    mi.hItem = 0;
+    src = SHORT1FROMMR(WinSendMsg(hwndMenu,
+                                  MM_INSERTITEM,
+                                  (MPARAM)&mi,
+                                  (MPARAM)pszItemTitle));
+    return (src);
+}
+
+/*
+ *@@ winhInsertSubmenu:
+ *      this inserts a submenu into a given menu and, if
+ *      sItemId != 0, inserts one item into this new submenu also.
+ *
+ *      See winhInsertMenuItem for valid menu item styles and
+ *      attributes.
+ *
+ *      Returns the HWND of the new submenu.
+ */
+
+HWND winhInsertSubmenu(HWND hwndMenu,       // in: menu to add submenu to
+                       ULONG iPosition,     // in: index where to add submenu or MIT_END
+                       SHORT sMenuId,       // in: menu ID of new submenu
+                       PSZ pszSubmenuTitle, // in: title of new submenu
+                       USHORT afMenuStyle,  // in: MIS* style flags for submenu;
+                                            // MIS_SUBMENU will always be added
+                       SHORT sItemId,       // in: ID of first item to add to submenu;
+                                            // if 0, no first item is inserted
+                       PSZ pszItemTitle,    // in: title of this item
+                                            // (if sItemID != 0)
+                       USHORT afItemStyle,  // in: style flags for this item, e.g. MIS_TEXT
+                                            // (this is ignored if sItemID == 0)
+                       USHORT afAttribute)  // in: attributes for this item, e.g. MIA_DISABLED
+                                            // (this is ignored if sItemID == 0)
+{
+    MENUITEM mi;
+    SHORT    src = MIT_ERROR;
+    HWND     hwndNewMenu;
+
+    // create new, empty menu
+    hwndNewMenu = WinCreateMenu(hwndMenu,
+                                NULL); // no menu template
+    if (hwndNewMenu)
+    {
+        // add "submenu item" to this empty menu;
+        // for some reason, PM always needs submenus
+        // to be a menu item
+        mi.iPosition = iPosition;
+        mi.afStyle = afMenuStyle | MIS_SUBMENU;
+        mi.afAttribute = 0;
+        mi.id = sMenuId;
+        mi.hwndSubMenu = hwndNewMenu;
+        mi.hItem = 0;
+        src = SHORT1FROMMR(WinSendMsg(hwndMenu, MM_INSERTITEM, (MPARAM)&mi, (MPARAM)pszSubmenuTitle));
+        if (    (src != MIT_MEMERROR)
+            &&  (src != MIT_ERROR)
+           )
+        {
+            // set the new menu's ID to the same as the
+            // submenu item
+            WinSetWindowUShort(hwndNewMenu, QWS_ID, sMenuId);
+
+            if (sItemId)
+            {
+                // item id given: insert first menu item also
+                mi.iPosition = 0;
+                mi.afStyle = afItemStyle;
+                mi.afAttribute = afAttribute;
+                mi.id = sItemId;
+                mi.hwndSubMenu = 0;
+                mi.hItem = 0;
+                WinSendMsg(hwndNewMenu,
+                           MM_INSERTITEM,
+                           (MPARAM)&mi,
+                           (MPARAM)pszItemTitle);
+            }
+        }
+    }
+    return (hwndNewMenu);
+}
+
+/*
+ *@@ winhInsertMenuSeparator:
+ *      this inserts a separator into a given menu at
+ *      the given position (which may be MIT_END);
+ *      returns the position at which the item was
+ *      inserted.
+ */
+
+SHORT winhInsertMenuSeparator(HWND hMenu,       // in: menu to add separator to
+                              SHORT iPosition,  // in: index where to add separator or MIT_END
+                              SHORT sId)        // in: separator menu ID (doesn't really matter)
+{
+    MENUITEM mi;
+    mi.iPosition = iPosition;
+    mi.afStyle = MIS_SEPARATOR;             // append separator
+    mi.afAttribute = 0;
+    mi.id = sId;
+    mi.hwndSubMenu = 0;
+    mi.hItem = 0;
+
+    return (SHORT1FROMMR(WinSendMsg(hMenu,
+                                    MM_INSERTITEM,
+                                    (MPARAM)&mi,
+                                    (MPARAM)"")));
+}
+
+/*
+ *@@ winhQueryMenuItemText:
+ *      this returns a menu item text as a PSZ
+ *      to a newly allocated buffer or NULL if
+ *      not found.
+ *
+ *      If something != NULL is returned, you
+ *      should free() the buffer afterwards.
+ *
+ *      Use the WinSetMenuItemText macro to
+ *      set the menu item text.
+ */
+
+PSZ winhQueryMenuItemText(HWND hwndMenu,
+                          USHORT usItemID)  // in: menu item ID (not index)
+{
+    PSZ     prc = NULL;
+
+    SHORT sLength = SHORT1FROMMR(WinSendMsg(hwndMenu, MM_QUERYITEMTEXTLENGTH,
+                                           (MPARAM)(ULONG)usItemID,
+                                           (MPARAM)NULL));
+    if (sLength)
+    {
+        prc = (PSZ)malloc(sLength + 1);
+        WinSendMsg(hwndMenu,
+                   MM_QUERYITEMTEXT,
+                   MPFROM2SHORT(usItemID, sLength + 1),
+                   (MPARAM)prc);
+    }
+
+    return (prc);
+}
+
+/*
+ *@@ winhAppend2MenuItemText:
+ *
+ *@@added V0.9.2 (2000-03-08) [umoeller]
+ */
+
+BOOL winhAppend2MenuItemText(HWND hwndMenu,
+                             USHORT usItemID,  // in: menu item ID (not index)
+                             const char *pcszAppend, // in: text to append
+                             BOOL fTab)    // in: if TRUE, add \t before pcszAppend
+{
+    BOOL brc = FALSE;
+    CHAR szItemText[400];
+    if (WinSendMsg(hwndMenu,
+                   MM_QUERYITEMTEXT,
+                   MPFROM2SHORT(usItemID,
+                                sizeof(szItemText)),
+                   (MPARAM)szItemText))
+    {
+        // text copied:
+        if (fTab)
+        {
+            if (strchr(szItemText, '\t'))
+                // we already have a tab:
+                strcat(szItemText, " ");
+            else
+                strcat(szItemText, "\t");
+        }
+        strcat(szItemText, pcszAppend);
+
+        brc = (BOOL)WinSendMsg(hwndMenu,
+                               MM_SETITEMTEXT,
+                               MPFROMSHORT(usItemID),
+                               (MPARAM)szItemText);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ winhMenuRemoveEllipse:
+ *      removes a "..." substring from a menu item
+ *      title, if found. This is useful if confirmations
+ *      have been turned off for a certain menu item, which
+ *      should be reflected in the menu.
+ */
+
+VOID winhMenuRemoveEllipse(HWND hwndMenu,
+                           USHORT usItemId)    // in:  item to remove "..." from
+{
+    CHAR szBuf[255];
+    CHAR *p;
+    WinSendMsg(hwndMenu,
+               MM_QUERYITEMTEXT,
+               MPFROM2SHORT(usItemId, sizeof(szBuf)-1),
+               (MPARAM)&szBuf);
+    if ((p = strstr(szBuf, "...")))
+        strcpy(p, p+3);
+    WinSendMsg(hwndMenu,
+               MM_SETITEMTEXT,
+               MPFROMSHORT(usItemId),
+               (MPARAM)&szBuf);
+}
+
+/*
+ *@@ winhQueryItemUnderMouse:
+ *      this queries the menu item which corresponds
+ *      to the given mouse coordinates.
+ *      Returns the ID of the menu item and stores its
+ *      rectangle in *prtlItem; returns (-1) upon errors.
+ */
+
+SHORT winhQueryItemUnderMouse(HWND hwndMenu,      // in: menu handle
+                              POINTL *pptlMouse,  // in: mouse coordinates
+                              RECTL *prtlItem)    // out: rectangle of menu item
+{
+    SHORT   s, sItemId, sItemCount;
+    HAB     habDesktop = WinQueryAnchorBlock(HWND_DESKTOP);
+
+    sItemCount = SHORT1FROMMR(WinSendMsg(hwndMenu, MM_QUERYITEMCOUNT, MPNULL, MPNULL));
+
+    for (s = 0;
+         s <= sItemCount;
+         s++)
+    {
+        sItemId = SHORT1FROMMR(WinSendMsg(hwndMenu,
+                                          MM_ITEMIDFROMPOSITION,
+                                          (MPARAM)(ULONG)s, MPNULL));
+        WinSendMsg(hwndMenu,
+                   MM_QUERYITEMRECT,
+                   MPFROM2SHORT(sItemId, FALSE),
+                   (MPARAM)prtlItem);
+        if (WinPtInRect(habDesktop, prtlItem, pptlMouse))
+            return (sItemId);
+    }
+    /* sItemId = (SHORT)WinSendMsg(hwndMenu, MM_ITEMIDFROMPOSITION, (MPARAM)(sItemCount-1), MPNULL);
+    return (sItemId); */
+    return (-1); // error: no valid menu item
+}
+
+/*
+ *@@category: Helpers\PM helpers\Slider helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Slider helpers                                                 *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhReplaceWithLinearSlider:
+ *      this destroys the control with the ID ulID in hwndDlg
+ *      and creates a linear slider at the same position with the
+ *      same ID (effectively replacing it).
+ *
+ *      This is needed because the IBM dialog editor (DLGEDIT.EXE)
+ *      keeps crashing when creating sliders. So the way to do
+ *      this easily is to create some other control with DLGEDIT
+ *      where the slider should be later and call this function
+ *      on that control when the dialog is initialized.
+ *
+ *      You need to specify _one_ of the following with ulSliderStyle:
+ *      -- SLS_HORIZONTAL: horizontal slider (default)
+ *      -- SLS_VERTICAL: vertical slider
+ *
+ *      plus _one_ additional common slider style for positioning:
+ *      -- for horizontal sliders: SLS_BOTTOM, SLS_CENTER, or SLS_TOP
+ *      -- for vertical sliders: SLS_LEFT, SLS_CENTER, or SLS_RIGHT
+ *
+ *      Additional common slider styles are:
+ *      -- SLS_PRIMARYSCALE1: determines the location of the scale
+ *                  on the slider shaft by using increment
+ *                  and spacing specified for scale 1 as
+ *                  the incremental value for positioning
+ *                  the slider arm. Scale 1 is displayed
+ *                  above the slider shaft of a horizontal
+ *                  slider and to the right of the slider
+ *                  shaft of a vertical slider. This is
+ *                  the default for a slider.
+ *      -- SLS_PRIMARYSCALE2: not supported by this function
+ *      -- SLS_READONLY: creates a read-only slider, which
+ *                  presents information to the user but
+ *                  allows no interaction with the user.
+ *      -- SLS_RIBBONSTRIP: fills, as the slider arm moves, the
+ *                  slider shaft between the home position
+ *                  and the slider arm with a color value
+ *                  different from slider shaft color,
+ *                  similar to mercury in a thermometer.
+ *      -- SLS_OWNERDRAW: notifies the application whenever the
+ *                  slider shaft, the ribbon strip, the
+ *                  slider arm, and the slider background
+ *                  are to be drawn.
+ *      -- SLS_SNAPTOINCREMENT: causes the slider arm, when positioned
+ *                  between two values, to be positioned
+ *                  to the nearest value and redrawn at
+ *                  that position.
+ *
+ *      Additionally, for horizontal sliders:
+ *      -- SLS_BUTTONSLEFT: specifies that the optional slider
+ *                  buttons are to be used and places them
+ *                  to the left of the slider shaft. The
+ *                  buttons move the slider arm by one
+ *                  position, left or right, in the
+ *                  direction selected.
+ *      -- SLS_BUTTONSRIGHT: specifies that the optional slider
+ *                  buttons are to be used and places them
+ *                  to the right of the slider shaft. The
+ *                  buttons move the slider arm by one
+ *                  position, left or right, in the
+ *                  direction selected.
+ *      -- SLS_HOMELEFT: specifies the slider arm's home
+ *                  position. The left edge is used as the
+ *                  base value for incrementing (default).
+ *      -- SLS_HOMERIGHT: specifies the slider arm's home
+ *                  position. The right edge is used as
+ *                  the base value for incrementing.
+ *
+ *      Instead, for vertical sliders:
+ *      -- SLS_BUTTONSBOTTOM: specifies that the optional slider
+ *                  buttons are to be used and places them
+ *                  at the bottom of the slider shaft. The
+ *                  buttons move the slider arm by one
+ *                  position, up or down, in the direction
+ *                  selected.
+ *      -- SLS_BUTTONSTOP: specifies that the optional slider
+ *                  buttons are to be used and places them
+ *                  at the top of the slider shaft. The
+ *                  buttons move the slider arm by one
+ *                  position, up or down, in the direction
+ *                  selected.
+ *      -- SLS_HOMEBOTTOM: specifies the slider arm's home
+ *                  position. The bottom of the slider is
+ *                  used as the base value for
+ *                  incrementing.
+ *      -- SLS_HOMETOP: specifies the slider arm's home
+ *                  position. The top of the slider is
+ *                  used as the base value for
+ *                  incrementing.
+ *
+ *      Notes: This function automatically adds WS_PARENTCLIP,
+ *      WS_TABSTOP, and WS_SYNCPAINT to the specified styles.
+ *      For the WS_TABSTOP style, hwndInsertAfter is important.
+ *      If you specify HWND_TOP, your window will be the first
+ *      in the tab stop list.
+ *
+ *      It also shows the slider after having done all the
+ *      processing in here by calling WinShowWindow.
+ *
+ *      Also, we only provide support for scale 1 here, so
+ *      do not specify SLS_PRIMARYSCALE2 with ulSliderStyle,
+ *      and we have the slider calculate all the spacings.
+ *
+ *      This returns the HWND of the slider or NULLHANDLE upon
+ *      errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+HWND winhReplaceWithLinearSlider(HWND hwndParent,   // in: parent of old control and slider
+                                 HWND hwndOwner,          // in: owner of old control and slider
+                                 HWND hwndInsertAfter,    // in: the control after which the slider should
+                                                          // come up, or HWND_TOP, or HWND_BOTTOM
+                                 ULONG ulID,              // in: ID of old control and slider
+                                 ULONG ulSliderStyle,     // in: SLS_* styles
+                                 ULONG ulTickCount)       // in: number of ticks (scale 1)
+{
+    HWND    hwndSlider = NULLHANDLE;
+    HWND    hwndKill = WinWindowFromID(hwndParent, ulID);
+    if (hwndKill)
+    {
+        SWP swpControl;
+        if (WinQueryWindowPos(hwndKill, &swpControl))
+        {
+            SLDCDATA slcd;
+
+            // destroy the old control
+            WinDestroyWindow(hwndKill);
+
+            // initialize slider control data
+            slcd.cbSize = sizeof(SLDCDATA);
+            slcd.usScale1Increments = ulTickCount;
+            slcd.usScale1Spacing = 0;           // have slider calculate it
+            slcd.usScale2Increments = 0;
+            slcd.usScale2Spacing = 0;
+
+            // create a slider with the same ID at the same
+            // position
+            hwndSlider = WinCreateWindow(hwndParent,
+                                         WC_SLIDER,
+                                         NULL,           // no window text
+                                         ulSliderStyle
+                                            | WS_PARENTCLIP
+                                            | WS_SYNCPAINT
+                                            | WS_TABSTOP,
+                                         swpControl.x,
+                                         swpControl.y,
+                                         swpControl.cx,
+                                         swpControl.cy,
+                                         hwndOwner,
+                                         hwndInsertAfter,
+                                         ulID,           // same ID as destroyed control
+                                         &slcd,          // slider control data
+                                         NULL);          // presparams
+
+            WinSendMsg(hwndSlider,
+                       SLM_SETTICKSIZE,
+                       MPFROM2SHORT(SMA_SETALLTICKS,
+                                    6),     // 15 pixels high
+                       NULL);
+
+            WinShowWindow(hwndSlider, TRUE);
+        }
+    }
+
+    return (hwndSlider);
+}
+
+/*
+ *@@ winhSetSliderTicks:
+ *      this adds ticks to the given linear slider,
+ *      which are ulPixels pixels high. A useful
+ *      value for this is 4.
+ *
+ *      This queries the slider for the primary
+ *      scale values. Only the primary scale is
+ *      supported.
+ *
+ *      If mpEveryOther is 0, this sets all ticks
+ *      on the primary slider scale.
+ *
+ *      If mpEveryOther is != 0, SHORT1FROMMP
+ *      specifies the first tick to set, and
+ *      SHORT2FROMMP specifies every other tick
+ *      to set from there. For example:
+ +          MPFROM2SHORT(9, 10)
+ *      would set tick 9, 19, 29, and so forth.
+ *
+ *      Returns FALSE upon errors.
+ *
+ *@@added V0.9.1 (99-12-04) [umoeller]
+ */
+
+BOOL winhSetSliderTicks(HWND hwndSlider,
+                        MPARAM mpEveryOther,
+                        ULONG ulPixels)
+{
+    BOOL brc = FALSE;
+
+    if (mpEveryOther == 0)
+    {
+        // set all ticks:
+        brc = (BOOL)WinSendMsg(hwndSlider,
+                               SLM_SETTICKSIZE,
+                               MPFROM2SHORT(SMA_SETALLTICKS,
+                                            ulPixels),
+                               NULL);
+    }
+    else
+    {
+        SLDCDATA  slcd;
+        WNDPARAMS   wp;
+        memset(&wp, 0, sizeof(WNDPARAMS));
+        wp.fsStatus = WPM_CTLDATA;
+        wp.cbCtlData = sizeof(slcd);
+        wp.pCtlData = &slcd;
+        // get primary scale data from the slider
+        if (WinSendMsg(hwndSlider,
+                       WM_QUERYWINDOWPARAMS,
+                       (MPARAM)&wp,
+                       0))
+        {
+            USHORT usStart = SHORT1FROMMP(mpEveryOther),
+                   usEveryOther = SHORT2FROMMP(mpEveryOther);
+
+            USHORT usScale1Max = slcd.usScale1Increments,
+                   us;
+
+            brc = TRUE;
+
+            for (us = usStart; us < usScale1Max; us += usEveryOther)
+            {
+                if (!(BOOL)WinSendMsg(hwndSlider,
+                                      SLM_SETTICKSIZE,
+                                      MPFROM2SHORT(us,
+                                                   ulPixels),
+                                      NULL))
+                {
+                    brc = FALSE;
+                    break;
+                }
+            }
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ winhReplaceWithCircularSlider:
+ *      this destroys the control with the ID ulID in hwndDlg
+ *      and creates a linear slider at the same position with the
+ *      same ID (effectively replacing it).
+ *
+ *      This is needed because the IBM dialog editor (DLGEDIT.EXE)
+ *      cannot create circular sliders. So the way to do this
+ *      easily is to create some other control with DLGEDIT
+ *      where the slider should be later and call this function
+ *      on that control when the dialog is initialized.
+ *
+ *      You need to specify the following with ulSliderStyle:
+ *      --  CSS_CIRCULARVALUE: draws a circular thumb, rather than a line,
+ *                  for the value indicator.
+ *      --  CSS_MIDPOINT: makes the mid-point tick mark larger.
+ *      --  CSS_NOBUTTON: does not display value buttons. Per default, the
+ *                  slider displays "-" and "+" buttons to the bottom left
+ *                  and bottom right of the knob. (BTW, these bitmaps can be
+ *                  changed using CSM_SETBITMAPDATA.)
+ *      --  CSS_NONUMBER: does not display the value on the dial.
+ *      --  CSS_NOTEXT: does not display title text under the dial.
+ *                  Otherwise, the text in the pszTitle parameter
+ *                  will be used.
+ *      --  CSS_NOTICKS (only listed in pmstddlg.h, not in PMREF):
+ *                  obviously, this prevents tick marks from being drawn.
+ *      --  CSS_POINTSELECT: permits the values on the circular slider
+ *                  to change immediately when dragged.
+ *                  Direct manipulation is performed by using a mouse to
+ *                  click on and drag the circular slider. There are two
+ *                  modes of direct manipulation for the circular slider:
+ *                  <BR><B>1)</B> The default direct manipulation mode is to scroll to
+ *                  the value indicated by the position of the mouse.
+ *                  This could be important if you used a circular slider
+ *                  for a volume control, for example. Increasing the volume
+ *                  from 0% to 100% too quickly could result in damage to
+ *                  both the user's ears and the equipment.
+ *                  <BR><B>2)</B>The other mode of direct manipulation permits
+ *                  the value on the circular slider to change immediately when dragged.
+ *                  This mode is enabled using the CSS_POINTSELECT style bit. When this
+ *                  style is used, the value of the dial can be changed by tracking
+ *                  the value with the mouse, which changes values quickly.
+ *      --  CSS_PROPORTIONALTICKS: allow the length of the tick marks to be calculated
+ *                  as a percentage of the radius (for small sliders).
+ *      --  CSS_360: permits the scroll range to extend 360 degrees.
+ *                  CSS_360 forces the CSS_NONUMBER style on. This is necessary
+ *                  to keep the value indicator from corrupting the number value.
+ *
+ *      FYI: The most commonly known circular slider in OS/2, the one in the
+ *      default "Sound" object, has a style of 0x9002018a, meaning
+ *      CSS_NOTEXT | CSS_POINTSELECT | CSS_NOTICKS.
+ *
+ *      Notes: This function automatically adds WS_PARENTCLIP,
+ *      WS_TABSTOP, and WS_SYNCPAINT to the specified styles.
+ *      For the WS_TABSTOP style, hwndInsertAfter is important.
+ *      If you specify HWND_TOP, your window will be the first
+ *      in the tab stop list.
+ *
+ *      It also shows the slider after having done all the
+ *      processing in here by calling WinShowWindow.
+ *
+ *      This returns the HWND of the slider or NULLHANDLE upon
+ *      errors.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+HWND winhReplaceWithCircularSlider(HWND hwndParent,   // in: parent of old control and slider
+                                   HWND hwndOwner,          // in: owner of old control and slider
+                                   HWND hwndInsertAfter,    // in: the control after which the slider should
+                                                            // come up, or HWND_TOP, or HWND_BOTTOM
+                                   ULONG ulID,              // in: ID of old control and slider
+                                   ULONG ulSliderStyle,     // in: SLS_* styles
+                                   SHORT sMin,              // in: minimum value (e.g. 0)
+                                   SHORT sMax,              // in: maximum value (e.g. 100)
+                                   USHORT usIncrement,      // in: minimum increment (e.g. 1)
+                                   USHORT usTicksEvery)     // in: ticks ever x values (e.g. 20)
+{
+    HWND    hwndSlider = NULLHANDLE;
+    HWND    hwndKill = WinWindowFromID(hwndParent, ulID);
+    if (hwndKill)
+    {
+        SWP swpControl;
+        if (WinQueryWindowPos(hwndKill, &swpControl))
+        {
+            // destroy the old control
+            WinDestroyWindow(hwndKill);
+
+            // WinRegisterCircularSlider();
+
+            // create a slider with the same ID at the same
+            // position
+            hwndSlider = WinCreateWindow(hwndParent,
+                                         WC_CIRCULARSLIDER,
+                                         "dummy",        // no window text
+                                         ulSliderStyle
+                                            // | WS_PARENTCLIP
+                                            // | WS_SYNCPAINT
+                                            | WS_TABSTOP,
+                                         swpControl.x,
+                                         swpControl.y,
+                                         swpControl.cx,
+                                         swpControl.cy,
+                                         hwndOwner,
+                                         hwndInsertAfter,
+                                         ulID,           // same ID as destroyed control
+                                         NULL,           // control data
+                                         NULL);          // presparams
+
+            if (hwndSlider)
+            {
+                // set slider range
+                WinSendMsg(hwndSlider,
+                           CSM_SETRANGE,
+                           (MPARAM)(ULONG)sMin,
+                           (MPARAM)(ULONG)sMax);
+
+                // set slider increments
+                WinSendMsg(hwndSlider,
+                           CSM_SETINCREMENT,
+                           (MPARAM)(ULONG)usIncrement,
+                           (MPARAM)(ULONG)usTicksEvery);
+
+                // set slider value
+                WinSendMsg(hwndSlider,
+                           CSM_SETVALUE,
+                           (MPARAM)0,
+                           (MPARAM)0);
+
+                // for some reason, the slider always has
+                // WS_CLIPSIBLINGS set, even though we don't
+                // set this; we must unset this now, or
+                // the slider won't draw itself (%&$&%"$&%!!!)
+                WinSetWindowBits(hwndSlider,
+                                 QWL_STYLE,
+                                 0,         // unset bit
+                                 WS_CLIPSIBLINGS);
+
+                WinShowWindow(hwndSlider, TRUE);
+            }
+        }
+    }
+
+    return (hwndSlider);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Spin button helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Spin button helpers                                            *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhSetDlgItemSpinData:
+ *      sets a spin button's limits and data within a dialog window.
+ *      This only works for decimal spin buttons.
+ */
+
+VOID winhSetDlgItemSpinData(HWND hwndDlg,       // in: dlg window
+                            ULONG idSpinButton,  // in: item ID of spin button
+                            ULONG min,           // in: minimum allowed value
+                            ULONG max,           // in: maximum allowed value
+                            ULONG current)       // in: new current value
+{
+    HWND hwndSpinButton = WinWindowFromID(hwndDlg, idSpinButton);
+    if (hwndSpinButton)
+    {
+        WinSendMsg(hwndSpinButton,
+                   SPBM_SETLIMITS,          // Set limits message
+                   (MPARAM)max,             // Spin Button maximum setting
+                   (MPARAM)min);             // Spin Button minimum setting
+
+        WinSendMsg(hwndSpinButton,
+                   SPBM_SETCURRENTVALUE,    // Set current value message
+                   (MPARAM)current,
+                   (MPARAM)NULL);
+    }
+}
+
+/*
+ *@@ winhAdjustDlgItemSpinData:
+ *      this can be called on a spin button control to
+ *      have its current data snap to a grid. This only
+ *      works for LONG integer values.
+ *
+ *      For example, if you specify 100 for the grid and call
+ *      this func after you have received SPBN_UP/DOWNARROW,
+ *      the spin button's value will always in/decrease
+ *      in steps of 100.
+ *
+ *      This returns the "snapped" value to which the spin
+ *      button was set.
+ *
+ *      If you specify lGrid == 0, this returns the spin
+ *      button's value only (V0.9.0).
+ *
+ *@@changed V0.9.0 [umoeller]: added check for lGrid == 0 (caused division by zero previously)
+ */
+
+LONG winhAdjustDlgItemSpinData(HWND hwndDlg,     // in: dlg window
+                               USHORT usItemID,  // in: item ID of spin button
+                               LONG lGrid,       // in: grid
+                               USHORT usNotifyCode) // in: SPBN_UP* or *DOWNARROW of WM_CONTROL message
+{
+    HWND hwndSpin = WinWindowFromID(hwndDlg, usItemID);
+    LONG lBottom, lTop, lValue;
+    // get value, which has already increased /
+    // decreased by 1
+    WinSendMsg(hwndSpin,
+               SPBM_QUERYVALUE,
+               (MPARAM)&lValue,
+               MPFROM2SHORT(0, SPBQ_ALWAYSUPDATE));
+
+    if ((lGrid)
+        && (    (usNotifyCode == SPBN_UPARROW)
+             || (usNotifyCode == SPBN_DOWNARROW)
+           )
+       )
+    {
+        // only if the up/down buttons were pressed,
+        // snap to the nearest grid; if the user
+        // manually enters something (SPBN_CHANGE),
+        // we'll accept that value
+        lValue = (lValue / lGrid) * lGrid;
+        // add /subtract grid
+        if (usNotifyCode == SPBN_UPARROW)
+            lValue += lGrid;
+        // else we'll have -= lGrid already
+
+        // balance with spin button limits
+        WinSendMsg(hwndSpin,
+                   SPBM_QUERYLIMITS,
+                   (MPARAM)&lTop,
+                   (MPARAM)&lBottom);
+        if (lValue < lBottom)
+            lValue = lTop;
+        else if (lValue > lTop)
+            lValue = lBottom;
+
+        WinSendMsg(hwndSpin,
+                   SPBM_SETCURRENTVALUE,
+                   (MPARAM)(lValue),
+                   MPNULL);
+    }
+    return (lValue);
+}
+
+/*
+ *@@category: Helpers\PM helpers\List box helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   List box helpers                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhQueryLboxItemText:
+ *      returns the text of the specified
+ *      list box item in a newly allocated
+ *      buffer, which you should free() afterwards,
+ *      or NULL upon errors.
+ *
+ *@@added V0.9.1 (99-12-14) [umoeller]
+ */
+
+PSZ winhQueryLboxItemText(HWND hwndListbox,
+                          SHORT sIndex)
+{
+    PSZ   pszReturn = 0;
+    SHORT cbText = SHORT1FROMMR(WinSendMsg(hwndListbox,
+                                           LM_QUERYITEMTEXTLENGTH,
+                                           (MPARAM)(ULONG)sIndex,
+                                           0));
+    if ((cbText) && (cbText != LIT_ERROR))
+    {
+        pszReturn = (PSZ)malloc(cbText + 1);        // add zero terminator
+        WinSendMsg(hwndListbox,
+                   LM_QUERYITEMTEXT,
+                   MPFROM2SHORT(sIndex,
+                                cbText + 1),
+                   (MPARAM)pszReturn);
+    }
+
+    return (pszReturn);
+}
+
+/*
+ *@@ winhMoveLboxItem:
+ *      this moves one list box item from one
+ *      list box to another, including the
+ *      item text and the item "handle"
+ *      (see LM_QUERYITEMHANDLE).
+ *
+ *      sTargetIndex can either be a regular
+ *      item index or one of the following
+ *      (as in LM_INSERTITEM):
+ *      -- LIT_END
+ *      -- LIT_SORTASCENDING
+ *      -- LIT_SORTDESCENDING
+ *
+ *      If (fSelectTarget == TRUE), the new
+ *      item is also selected in the target
+ *      list box.
+ *
+ *      Returns FALSE if moving failed. In
+ *      that case, the list boxes are unchanged.
+ *
+ *@@added V0.9.1 (99-12-14) [umoeller]
+ */
+
+BOOL winhMoveLboxItem(HWND hwndSource,
+                      SHORT sSourceIndex,
+                      HWND hwndTarget,
+                      SHORT sTargetIndex,
+                      BOOL fSelectTarget)
+{
+    BOOL brc = FALSE;
+
+    PSZ pszItemText = winhQueryLboxItemText(hwndSource, sSourceIndex);
+    if (pszItemText)
+    {
+        ULONG   ulItemHandle = winhQueryLboxItemHandle(hwndSource,
+                                                       sSourceIndex);
+                    // probably 0, if not used
+        LONG lTargetIndex = WinInsertLboxItem(hwndTarget,
+                                              sTargetIndex,
+                                              pszItemText);
+        if (    (lTargetIndex != LIT_ERROR)
+             && (lTargetIndex != LIT_MEMERROR)
+           )
+        {
+            // successfully inserted:
+            winhSetLboxItemHandle(hwndTarget, lTargetIndex, ulItemHandle);
+            if (fSelectTarget)
+                winhSetLboxSelectedItem(hwndTarget, lTargetIndex, TRUE);
+
+            // remove source
+            WinDeleteLboxItem(hwndSource,
+                              sSourceIndex);
+
+            brc = TRUE;
+        }
+
+        free(pszItemText);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ winhLboxSelectAll:
+ *      this selects or deselects all items in the
+ *      given list box, depending on fSelect.
+ *
+ *      Returns the number of items in the list box.
+ */
+
+ULONG winhLboxSelectAll(HWND hwndListBox,   // in: list box
+                        BOOL fSelect)       // in: TRUE = select, FALSE = deselect
+{
+    LONG lItemCount = WinQueryLboxCount(hwndListBox);
+    ULONG ul;
+
+    for (ul = 0; ul < lItemCount; ul++)
+    {
+        WinSendMsg(hwndListBox,
+                   LM_SELECTITEM,
+                   (MPARAM)ul,      // index
+                   (MPARAM)fSelect);
+    }
+
+    return (lItemCount);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Scroll bar helpers
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Scroll bar helpers                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhUpdateScrollBar:
+ *      updates the given scroll bar according to the given
+ *      values. This updates the scroll bar's thumb size,
+ *      extension, and position, all in one shot.
+ *
+ *      This simplifies the typical functionality of a scroll
+ *      bar in a client window which is to be scrolled. I am
+ *      wondering why IBM never included such a function, since
+ *      it is so damn basic and still writing it cost me a whole
+ *      day.
+ *
+ *      Terminology:
+ *
+ *      -- "window": the actual window with scroll bars which displays
+ *         a subrectangle of the available data.
+ *         The width or height of this must be passed in ulWinPels.
+ *
+ *      -- "viewport": the entire data to be displayed, of which the
+ *         "window" can only display a subrectangle, if the viewport
+ *         is larger than the window. The width or height of this must be
+ *         passed in ulViewportPels. This can be smaller than ulWinPels (if the
+ *         window is larger than the data), the same or larger than ulWinPels
+ *         (if the window is too small to show all the data).
+ *
+ *      -- "window offset": the offset of the current window within
+ *         the viewport. For horizontal scroll bars, this is
+ *         the X coordinate, counting from the left of the window
+ *         (0 means leftmost).
+ *         For vertical scroll bars, this is counted from the _top_
+ *         of the viewport (0 means topmost, as opposed to OS/2
+ *         window coordinates!).
+ *         This is because for vertical scroll bars controls, higher
+ *         values move the thumb _down_.
+ *
+ *      The scroll bar is disabled if the entire viewport is visible,
+ *      that is, if ulViewportPels <= ulWinPels. In that case
+ *      FALSE is returned. If (fAutoHide == TRUE), the scroll
+ *      bar is not only disabled, but also hidden from the display.
+ *      In that case, you will need to reformat your output because
+ *      your viewport becomes larger without the scroll bar.
+ *
+ *      This function will set the range of the scroll bar to 0 up
+ *      to a value depending on the viewport size. For vertical scroll
+ *      bars, 0 means topmost (which is kinda sick with the OS/2
+ *      coordinate system), for horizontal scroll bars, 0 means leftmost.
+ *      The maximum value of the scroll bar will be:
+ +          (ulViewportPels - ulWinPels) / usScrollUnitPels
+ *
+ *      The thumb size of the scroll bar will also be adjusted
+ *      based on the viewport and window size, as it should be.
+ *
+ *@@added V0.9.1 (2000-02-14) [umoeller]
+ *@@changed V0.9.3 (2000-04-30) [umoeller]: fixed pels/unit confusion
+ *@@changed V0.9.3 (2000-05-08) [umoeller]: now handling scroll units automatically
+ */
+
+BOOL winhUpdateScrollBar(HWND hwndScrollBar,    // in: scroll bar (vertical or horizontal)
+                         ULONG ulWinPels,       // in: vertical or horizontal dimension of
+                                                // visible window part (in pixels),
+                                                // excluding the scroll bar!
+                         ULONG ulViewportPels,  // in: dimension of total data part, of
+                                                // which ulWinPels is a sub-dimension
+                                                // (in pixels);
+                                                // if <= ulWinPels, the scrollbar will be
+                                                // disabled
+                         ULONG ulCurPelsOfs,    // in: current offset of visible part
+                                                // (in pixels)
+                         BOOL fAutoHide)        // in: hide scroll bar if disabled
+{
+    BOOL brc = FALSE;
+
+    // _Pmpf(("Entering winhUpdateScrollBar"));
+
+    // for large viewports, adjust scroll bar units
+    USHORT  usScrollUnitPels = 1;
+    if (ulViewportPels > 10000)
+        usScrollUnitPels = 100;
+
+    if (ulViewportPels > ulWinPels)
+    {
+        // scrollbar needed:
+        USHORT  usThumbDivisorUnits = usScrollUnitPels;
+        USHORT  lMaxAllowedUnitOfs;
+        // _Pmpf(("winhUpdateScrollBar: ulViewportPels > ulWinPels, enabling scroller"));
+        // divisor for thumb size (below)
+        if (ulViewportPels > 10000)
+            // for very large viewports, we need to
+            // raise the divisor, because we only
+            // have a USHORT
+            usThumbDivisorUnits = usScrollUnitPels * 100;
+
+        // viewport is larger than window:
+        WinEnableWindow(hwndScrollBar, TRUE);
+        if (fAutoHide)
+            WinShowWindow(hwndScrollBar, TRUE);
+
+        // calculate limit
+        lMaxAllowedUnitOfs = ((ulViewportPels - ulWinPels + usScrollUnitPels)
+                               // scroll unit is 10
+                               / usScrollUnitPels);
+
+        // _Pmpf(("    usCurUnitOfs: %d", ulCurUnitOfs));
+        // _Pmpf(("    usMaxUnits: %d", lMaxAllowedUnitOfs));
+
+        // set thumb position and limit
+        WinSendMsg(hwndScrollBar,
+                   SBM_SETSCROLLBAR,
+                   (MPARAM)(ulCurPelsOfs), //  / usThumbDivisorUnits),   // position: 0 means top
+                   MPFROM2SHORT(0,  // minimum
+                                lMaxAllowedUnitOfs));    // maximum
+
+        // set thumb size based on ulWinPels and
+        // ulViewportPels
+        WinSendMsg(hwndScrollBar,
+                   SBM_SETTHUMBSIZE,
+                   MPFROM2SHORT(    ulWinPels / usThumbDivisorUnits,       // visible
+                                    ulViewportPels / usThumbDivisorUnits), // total
+                   0);
+        brc = TRUE;
+    }
+    else
+    {
+        // _Pmpf(("winhUpdateScrollBar: ulViewportPels <= ulWinPels"));
+        // entire viewport is visible:
+        WinEnableWindow(hwndScrollBar, FALSE);
+        if (fAutoHide)
+            WinShowWindow(hwndScrollBar, FALSE);
+    }
+
+    // _Pmpf(("End of winhUpdateScrollBar"));
+
+    return (brc);
+}
+
+/*
+ *@@ winhHandleScrollMsg:
+ *      this helper handles a WM_VSCROLL or WM_HSCROLL
+ *      message posted to a client window when the user
+ *      has worked on a client scroll bar. Calling this
+ *      function is ALL you need to do to handle those
+ *      two messages.
+ *
+ *      This is most useful in conjunction with winhUpdateScrollBar.
+ *      See that function for the terminology also.
+ *
+ *      This function calculates the new scrollbar position
+ *      (from the mp2 value, which can be line up/down,
+ *      page up/down, or slider track) and calls WinScrollWindow
+ *      accordingly. The window part which became invalid
+ *      because of the scrolling is automatically invalidated
+ *      (using WinInvalidateRect), so expect a WM_PAINT after
+ *      calling this function.
+ *
+ *      This function assumes that the scrollbar operates
+ *      on values starting from zero. The maximum value
+ *      of the scroll bar is:
+ +          ulViewportPels - (prcl2Scroll->yTop - prcl2Scroll->yBottom)
+ *
+ *      This function also automatically changes the scroll bar
+ *      units, should you have a viewport size which doesn't fit
+ *      into the SHORT's that the scroll bar uses internally. As
+ *      a result, this function handles a the complete range of
+ *      a ULONG for the viewport.
+ *
+ *      Replace "bottom" and "top" with "right" and "left" for
+ *      horizontal scrollbars in the above formula.
+ *
+ *@@added V0.9.1 (2000-02-13) [umoeller]
+ *@@changed V0.9.3 (2000-04-30) [umoeller]: changed prototype, fixed pels/unit confusion
+ *@@changed V0.9.3 (2000-05-08) [umoeller]: now handling scroll units automatically
+ */
+
+BOOL winhHandleScrollMsg(HWND hwnd2Scroll,          // in: client window to scroll
+                         HWND hwndScrollBar,        // in: vertical or horizontal scroll bar window
+                         PLONG plCurPelsOfs,        // in/out: current viewport offset;
+                                                    // this is updated with the proper scroll units
+                         PRECTL prcl2Scroll,        // in: hwnd2Scroll rectangle to scroll
+                                                    // (in window coordinates);
+                                                    // this is passed to WinScrollWindow,
+                                                    // which considers this inclusive!
+                         LONG ulViewportPels,       // in: total viewport dimension,
+                                                    // into which *plCurPelsOfs is an offset
+                         USHORT usLineStepPels,     // in: pixels to scroll line-wise
+                                                    // (scroll bar buttons pressed)
+                         ULONG msg,                 // in: either WM_VSCROLL or WM_HSCROLL
+                         MPARAM mp2)                // in: complete mp2 of WM_VSCROLL/WM_HSCROLL;
+                                                    // this has two SHORT's (usPos and usCmd),
+                                                    // see PMREF for details
+{
+    LONG    lOldPelsOfs = *plCurPelsOfs;
+    USHORT  usPosUnits = SHORT1FROMMP(mp2), // in scroll units
+            usCmd = SHORT2FROMMP(mp2);
+    LONG    lMaxAllowedUnitOfs;
+    ULONG   ulWinPels;
+
+    // for large viewports, adjust scroll bar units
+    USHORT  usScrollUnitPels = 1;
+    if (ulViewportPels > 10000)
+        usScrollUnitPels = 100;
+
+    // calculate window size (vertical or horizontal)
+    if (msg == WM_VSCROLL)
+        ulWinPels = (prcl2Scroll->yTop - prcl2Scroll->yBottom);
+    else
+        ulWinPels = (prcl2Scroll->xRight - prcl2Scroll->xLeft);
+
+    lMaxAllowedUnitOfs = ((LONG)ulViewportPels - ulWinPels) / usScrollUnitPels;
+
+    // _Pmpf(("Entering winhHandleScrollMsg"));
+
+    switch (usCmd)
+    {
+        case SB_LINEUP:
+            // _Pmpf(("SB_LINEUP"));
+            *plCurPelsOfs -= usLineStepPels;  //  * usScrollUnitPels);
+        break;
+
+        case SB_LINEDOWN:
+            // _Pmpf(("SB_LINEDOWN"));
+            *plCurPelsOfs += usLineStepPels;  //  * usScrollUnitPels);
+        break;
+
+        case SB_PAGEUP:
+            *plCurPelsOfs -= ulWinPels; // convert to units
+        break;
+
+        case SB_PAGEDOWN:
+            *plCurPelsOfs += ulWinPels; // convert to units
+        break;
+
+        case SB_SLIDERTRACK:
+            *plCurPelsOfs = (usPosUnits * usScrollUnitPels);
+            // _Pmpf(("    SB_SLIDERTRACK: usUnits = %d", usPosUnits));
+        break;
+
+        case SB_SLIDERPOSITION:
+            *plCurPelsOfs = (usPosUnits * usScrollUnitPels);
+        break;
+    }
+
+    // are we close to the lower limit?
+    /* if (*plCurUnitOfs < usLineStepUnits) // usScrollUnit)
+        *plCurUnitOfs = 0;
+    // are we close to the upper limit?
+    else if (*plCurUnitOfs + usLineStepUnits > lMaxUnitOfs)
+    {
+        _Pmpf(("        !!! limiting: %d to %d", *plCurUnitOfs, lMaxUnitOfs));
+        *plCurUnitOfs = lMaxUnitOfs;
+    } */
+
+    if (*plCurPelsOfs < 0)
+        *plCurPelsOfs = 0;
+    if (*plCurPelsOfs > (lMaxAllowedUnitOfs * usScrollUnitPels))
+    {
+        // _Pmpf(("        !!! limiting 2: %d to %d", *plCurUnitOfs, lMaxAllowedUnitOfs));
+        *plCurPelsOfs = (lMaxAllowedUnitOfs * usScrollUnitPels);
+    }
+    if (    (*plCurPelsOfs != lOldPelsOfs)
+         || (*plCurPelsOfs == 0)
+         || (*plCurPelsOfs == (lMaxAllowedUnitOfs * usScrollUnitPels))
+       )
+    {
+        RECTL   rcl2Scroll,
+                rcl2Update;
+
+        // changed:
+        WinSendMsg(hwndScrollBar,
+                   SBM_SETPOS,
+                   (MPARAM)(*plCurPelsOfs / usScrollUnitPels), //  / usScrollUnit),
+                   0);
+        // scroll window rectangle:
+        rcl2Scroll.xLeft =  prcl2Scroll->xLeft;
+        rcl2Scroll.xRight =  prcl2Scroll->xRight;
+        rcl2Scroll.yBottom =  prcl2Scroll->yBottom;
+        rcl2Scroll.yTop =  prcl2Scroll->yTop;
+
+        if (msg == WM_VSCROLL)
+            WinScrollWindow(hwnd2Scroll,
+                            0,
+                            (*plCurPelsOfs - lOldPelsOfs)  // scroll units changed
+                            ,    // * usScrollUnitPels,     // convert to pels
+                            &rcl2Scroll,  // rcl to scroll
+                            prcl2Scroll, // clipping rect
+                            NULLHANDLE, // no region
+                            &rcl2Update,
+                            0);
+        else
+            WinScrollWindow(hwnd2Scroll,
+                            -(*plCurPelsOfs - lOldPelsOfs) // scroll units changed
+                            ,    // * usScrollUnitPels,
+                            0,
+                            &rcl2Scroll,  // rcl to scroll
+                            prcl2Scroll, // clipping rect
+                            NULLHANDLE, // no region
+                            &rcl2Update,
+                            0);
+
+        // WinScrollWindow has stored the invalid window
+        // rectangle which needs to be repainted in rcl2Update:
+        WinInvalidateRect(hwnd2Scroll, &rcl2Update, FALSE);
+    }
+
+    // _Pmpf(("End of winhHandleScrollMsg"));
+
+    return (TRUE);
+}
+
+/*
+ *@@ winhProcessScrollChars:
+ *      helper for processing WM_CHAR messages for
+ *      client windows with scroll bars.
+ *
+ *      If your window has scroll bars, you normally
+ *      need to process a number of keystrokes to be
+ *      able to scroll the window contents. This is
+ *      tiresome to code, so here is a helper.
+ *
+ *      When receiving WM_CHAR, call this function.
+ *      If this returns TRUE, the keystroke has been
+ *      a scroll keystroke, and the window has been
+ *      updated (by sending WM_VSCROLL or WM_HSCROLL
+ *      to hwndClient). Otherwise, you should process
+ *      the keystroke as usual because it's not a
+ *      scroll keystroke.
+ *
+ *      The following keystrokes are processed here:
+ *
+ *      -- "cursor up, down, right, left": scroll one
+ *         line in the proper direction.
+ *      -- "page up, down": scroll one page up or down.
+ *      -- "Home": scroll leftmost.
+ *      -- "Ctrl+ Home": scroll topmost.
+ *      -- "End": scroll rightmost.
+ *      -- "Ctrl+ End": scroll bottommost.
+ *      -- "Ctrl + page up, down": scroll topmost or bottommost.
+ *
+ *      This is roughly CUA behavior.
+ *
+ *      Returns TRUE if the message has been
+ *      processed.
+ *
+ *@@added V0.9.3 (2000-04-29) [umoeller]
+ */
+
+BOOL winhProcessScrollChars(HWND hwndClient,    // in: client window
+                            HWND hwndVScroll,   // in: vertical scroll bar
+                            HWND hwndHScroll,   // in: horizontal scroll bar
+                            MPARAM mp1,         // in: WM_CHAR mp1
+                            MPARAM mp2,         // in: WM_CHAR mp2
+                            ULONG ulVertMax,    // in: maximum viewport cy
+                            ULONG ulHorzMax)    // in: maximum viewport cx
+{
+    BOOL    fProcessed = FALSE;
+    USHORT usFlags    = SHORT1FROMMP(mp1);
+    // USHORT usch       = SHORT1FROMMP(mp2);
+    USHORT usvk       = SHORT2FROMMP(mp2);
+
+    // _Pmpf(("Entering winhProcessScrollChars"));
+
+    if (usFlags & KC_VIRTUALKEY)
+    {
+        ULONG   ulMsg = 0;
+        SHORT   sPos = 0;
+        SHORT   usCmd = 0;
+        fProcessed = TRUE;
+
+        switch (usvk)
+        {
+            case VK_UP:
+                ulMsg = WM_VSCROLL;
+                usCmd = SB_LINEUP;
+            break;
+
+            case VK_DOWN:
+                ulMsg = WM_VSCROLL;
+                usCmd = SB_LINEDOWN;
+            break;
+
+            case VK_RIGHT:
+                ulMsg = WM_HSCROLL;
+                usCmd = SB_LINERIGHT;
+            break;
+
+            case VK_LEFT:
+                ulMsg = WM_HSCROLL;
+                usCmd = SB_LINELEFT;
+            break;
+
+            case VK_PAGEUP:
+                ulMsg = WM_VSCROLL;
+                if (usFlags & KC_CTRL)
+                {
+                    sPos = 0;
+                    usCmd = SB_SLIDERPOSITION;
+                }
+                else
+                    usCmd = SB_PAGEUP;
+            break;
+
+            case VK_PAGEDOWN:
+                ulMsg = WM_VSCROLL;
+                if (usFlags & KC_CTRL)
+                {
+                    sPos = ulVertMax;
+                    usCmd = SB_SLIDERPOSITION;
+                }
+                else
+                    usCmd = SB_PAGEDOWN;
+            break;
+
+            case VK_HOME:
+                if (usFlags & KC_CTRL)
+                    // vertical:
+                    ulMsg = WM_VSCROLL;
+                else
+                    ulMsg = WM_HSCROLL;
+
+                sPos = 0;
+                usCmd = SB_SLIDERPOSITION;
+            break;
+
+            case VK_END:
+                if (usFlags & KC_CTRL)
+                {
+                    // vertical:
+                    ulMsg = WM_VSCROLL;
+                    sPos = ulVertMax;
+                }
+                else
+                {
+                    ulMsg = WM_HSCROLL;
+                    sPos = ulHorzMax;
+                }
+
+                usCmd = SB_SLIDERPOSITION;
+            break;
+
+            default:
+                // other:
+                fProcessed = FALSE;
+        }
+
+        if (    ((usFlags & KC_KEYUP) == 0)
+             && (ulMsg)
+           )
+        {
+            HWND   hwndScrollBar = ((ulMsg == WM_VSCROLL)
+                                        ? hwndVScroll
+                                        : hwndHScroll);
+            if (WinIsWindowEnabled(hwndScrollBar))
+            {
+                USHORT usID = WinQueryWindowUShort(hwndScrollBar,
+                                                   QWS_ID);
+                WinSendMsg(hwndClient,
+                           ulMsg,
+                           MPFROMSHORT(usID),
+                           MPFROM2SHORT(sPos,
+                                        usCmd));
+            }
+        }
+    }
+
+    // _Pmpf(("End of  winhProcessScrollChars"));
+
+    return (fProcessed);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Window positioning
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Window positioning helpers                                     *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhSaveWindowPos:
+ *      saves the position of a certain window. As opposed
+ *      to the barely documented WinStoreWindowPos API, this
+ *      one only saves one regular SWP structure for the given
+ *      window, as returned by WinQueryWindowPos for hwnd.
+ *
+ *      If the window is currently maximized or minimized,
+ *      we won't store the current window size and position
+ *      (which wouldn't make much sense), but retrieve the
+ *      "restored" window position from the window words
+ *      instead.
+ *
+ *      The window should still be visible on the screen
+ *      when calling this function. Do not call it in WM_DESTROY,
+ *      because then the SWP data is no longer valid.
+ *
+ *      This returns TRUE if saving was successful.
+ *
+ *@@changed V0.9.1 (99-12-19) [umoeller]: added minimize/maximize support
+ */
+
+BOOL winhSaveWindowPos(HWND hwnd,   // in: window to save
+                       HINI hIni,   // in: INI file (or HINI_USER/SYSTEM)
+                       PSZ pszApp,  // in: INI application name
+                       PSZ pszKey)  // in: INI key name
+{
+    BOOL brc = FALSE;
+    SWP swp;
+    if (WinQueryWindowPos(hwnd, &swp))
+    {
+        if (swp.fl & (SWP_MAXIMIZE | SWP_MINIMIZE))
+        {
+            // window currently maximized or minimized:
+            // retrieve "restore" position from window words
+            swp.x = WinQueryWindowUShort(hwnd, QWS_XRESTORE);
+            swp.y = WinQueryWindowUShort(hwnd, QWS_YRESTORE);
+            swp.cx = WinQueryWindowUShort(hwnd, QWS_CXRESTORE);
+            swp.cy = WinQueryWindowUShort(hwnd, QWS_CYRESTORE);
+        }
+
+        brc = PrfWriteProfileData(hIni, pszApp, pszKey, &swp, sizeof(swp));
+    }
+    return (brc);
+}
+
+/*
+ *@@ winhRestoreWindowPos:
+ *      this will retrieve a window position which was
+ *      previously stored using winhSaveWindowPos.
+ *
+ *      The window should not be visible to avoid flickering.
+ *      "fl" must contain the SWP_flags as in WinSetWindowPos.
+ *
+ *      Note that only the following may be used:
+ *      --  SWP_MOVE        reposition the window
+ *      --  SWP_SIZE        also resize the window to
+ *                          the stored position; this might
+ *                          lead to problems with different
+ *                          video resolutions, so be careful.
+ *      --  SWP_SHOW        make window visible too
+ *      --  SWP_NOREDRAW    changes are not redrawn
+ *      --  SWP_NOADJUST    do not send a WM_ADJUSTWINDOWPOS message
+ *                          before moving or sizing
+ *      --  SWP_ACTIVATE    activate window (make topmost)
+ *      --  SWP_DEACTIVATE  deactivate window (make bottommost)
+ *
+ *      Do not specify any other SWP_* flags.
+ *
+ *      If SWP_SIZE is not set, the window will be moved only.
+ *
+ *      This returns TRUE if INI data was found.
+ *
+ *      This function automatically checks for whether the
+ *      window would be positioned outside the visible screen
+ *      area and will adjust coordinates accordingly. This can
+ *      happen when changing video resolutions.
+ */
+
+BOOL winhRestoreWindowPos(HWND hwnd,   // in: window to restore
+                          HINI hIni,   // in: INI file (or HINI_USER/SYSTEM)
+                          PSZ pszApp,  // in: INI application name
+                          PSZ pszKey,  // in: INI key name
+                          ULONG fl)    // in: "fl" parameter for WinSetWindowPos
+{
+    BOOL    brc = FALSE;
+    SWP     swp, swpNow;
+    ULONG   cbswp = sizeof(swp);
+    ULONG   fl2 = fl;
+
+    if (PrfQueryProfileData(hIni, pszApp, pszKey, &swp, &cbswp))
+    {
+        ULONG ulScreenCX = WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN);
+        ULONG ulScreenCY = WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN);
+
+        brc = TRUE;
+
+        if ((fl & SWP_SIZE) == 0)
+        {
+            // if no resize, we need to get the current position
+            brc = WinQueryWindowPos(hwnd, &swpNow);
+            swp.cx = swpNow.cx;
+            swp.cy = swpNow.cy;
+        }
+
+        if (brc)
+        {
+            // check for full visibility
+            if ( (swp.x + swp.cx) > ulScreenCX)
+                swp.x = ulScreenCX - swp.cx;
+            if ( (swp.y + swp.cy) > ulScreenCY)
+                swp.y = ulScreenCY - swp.cy;
+        }
+
+        brc = TRUE;
+
+    }
+    else
+        // window pos not found in INI: unset SWP_MOVE etc.
+        fl2 &= ~(SWP_MOVE | SWP_SIZE);
+
+    WinSetWindowPos(hwnd,
+                    NULLHANDLE,       // insert-behind window
+                    swp.x,
+                    swp.y,
+                    swp.cx,
+                    swp.cy,
+                    fl2);        // SWP_* flags
+
+    return (brc);
+}
+
+/*
+ *@@ winhAdjustControls:
+ *      helper function for dynamically adjusting a window's
+ *      controls when the window is resized.
+ *
+ *      This is most useful with dialogs loaded from resources
+ *      which should be sizeable. Normally, when the dialog
+ *      frame is resized, the controls stick to their positions,
+ *      and for dialogs with many controls, programming the
+ *      changes can be tiresome.
+ *
+ *      Enter this function. ;-) Basically, this takes a
+ *      static array of MPARAM's as input (plus one dynamic
+ *      storage area for the window positions).
+ *
+ *      This function must get called in three contexts:
+ *      during WM_INITDLG, during WM_WINDOWPOSCHANGED, and
+ *      during WM_DESTROY, with varying parameters.
+ *
+ *      In detail, there are four things you need to do to make
+ *      this work:
+ *
+ *      1)  Set up a static array (as a global variable) of
+ *          MPARAM's, one for each control in your array.
+ *          Each MPARAM will have the control's ID and the
+ *          XAF* flags (winh.h) how the control shall be moved.
+ *          Use MPFROM2SHORT to easily create this. Example:
+ *
+ +          MPARAM ampControlFlags[] =
+ +              {  MPFROM2SHORT(ID_CONTROL_1, XAC_MOVEX),
+ +                 MPFROM2SHORT(ID_CONTROL_2, XAC_SIZEY),
+ +                  ...
+ +              }
+ *
+ *          This can safely be declared as a global variable
+ *          because this data will only be read and never
+ *          changed by this function.
+ *
+ *      2)  In WM_INITDLG of your dialog function, set up
+ *          an XADJUSTCTRLS structure, preferrably in your
+ *          window words (QWL_USER).
+ *
+ *          ZERO THAT STRUCTURE (memset(&xac, 0, sizeof(XADJUSTCTRLS),
+ *          or this func will not work.
+ *
+ *          Call this function with pswpNew == NULL and
+ *          pxac pointing to that new structure. This will
+ *          query the positions of all the controls listed
+ *          in the MPARAMs array and store them in the
+ *          XADJUSTCTRLS area.
+ *
+ *      3)  Intercept WM_WINDOWPOSCHANGED:
+ *
+ +          case WM_WINDOWPOSCHANGED:
+ +          {
+ +              // this msg is passed two SWP structs:
+ +              // one for the old, one for the new data
+ +              // (from PM docs)
+ +              PSWP pswpNew = PVOIDFROMMP(mp1);
+ +              PSWP pswpOld = pswpNew + 1;
+ +
+ +              // resizing?
+ +              if (pswpNew->fl & SWP_SIZE)
+ +              {
+ +                  PXADJUSTCTRLS pxac = ... // get it from your window words
+ +
+ +                  winhAdjustControls(hwndDlg,             // dialog
+ +                                     ampControlFlags,     // MPARAMs array
+ +                                     sizeof(ampControlFlags) / sizeof(MPARAM),
+ +                                                          // items count
+ +                                     pswpNew,             // mp1
+ +                                     pxac);               // storage area
+ +              }
+ +              mrc = WinDefDlgProc(hwnd, msg, mp1, mp2); ...
+ *
+ *      4)  In WM_DESTROY, call this function again with pmpFlags,
+ *          pswpNew, and pswpNew set to NULL. This will clean up the
+ *          data which has been allocated internally (pointed to from
+ *          the XADJUSTCTRLS structure).
+ *          Don't forget to free your storage for XADJUSTCTLRS
+ *          _itself_, that's the job of the caller.
+ *
+ *      This might sound complicated, but it's a lot easier than
+ *      having to write dozens of WinSetWindowPos calls oneself.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL winhAdjustControls(HWND hwndDlg,           // in: dialog (req.)
+                        MPARAM *pmpFlags,       // in: init flags or NULL for cleanup
+                        ULONG ulCount,          // in: item count (req.)
+                        PSWP pswpNew,           // in: pswpNew from WM_WINDOWPOSCHANGED or NULL for cleanup
+                        PXADJUSTCTRLS pxac)     // in: adjust-controls storage area (req.)
+{
+    BOOL    brc = FALSE;
+    ULONG   ul = 0;
+
+    /* if (!WinIsWindowVisible(hwndDlg))
+        return (FALSE); */
+
+    if ((pmpFlags) && (pxac))
+    {
+        PSWP    pswpThis;
+        MPARAM  *pmpThis;
+        LONG    ldcx, ldcy;
+        ULONG   cWindows = 0;
+
+        // setup mode:
+        if (pxac->fInitialized == FALSE)
+        {
+            // first call: get all the SWP's
+            WinQueryWindowPos(hwndDlg, &pxac->swpMain);
+            // _Pmpf(("winhAdjustControls: queried main cx = %d, cy = %d",
+               //      pxac->swpMain.cx, pxac->swpMain.cy));
+
+            pxac->paswp = (PSWP)malloc(sizeof(SWP) * ulCount);
+
+            pswpThis = pxac->paswp;
+            pmpThis = pmpFlags;
+
+            for (ul = 0;
+                 ul < ulCount;
+                 ul++)
+            {
+                HWND hwndThis = WinWindowFromID(hwndDlg, SHORT1FROMMP(*pmpThis));
+                if (hwndThis)
+                {
+                    WinQueryWindowPos(hwndThis, pswpThis);
+                    cWindows++;
+                }
+
+                pswpThis++;
+                pmpThis++;
+            }
+
+            pxac->fInitialized = TRUE;
+            // _Pmpf(("winhAdjustControls: queried %d controls", cWindows));
+        }
+
+        if (pswpNew)
+        {
+            // compute width and height delta
+            ldcx = (pswpNew->cx - pxac->swpMain.cx);
+            ldcy = (pswpNew->cy - pxac->swpMain.cy);
+
+            // _Pmpf(("winhAdjustControls: new cx = %d, cy = %d",
+              //       pswpNew->cx, pswpNew->cy));
+
+            // now adjust the controls
+            cWindows = 0;
+            pswpThis = pxac->paswp;
+            pmpThis = pmpFlags;
+            for (ul = 0;
+                 ul < ulCount;
+                 ul++)
+            {
+                HWND hwndThis = WinWindowFromID(hwndDlg, SHORT1FROMMP(*pmpThis));
+                if (hwndThis)
+                {
+                    LONG    x = pswpThis->x,
+                            y = pswpThis->y,
+                            cx = pswpThis->cx,
+                            cy = pswpThis->cy;
+
+                    ULONG   ulSwpFlags = 0;
+                    // get flags for this control
+                    USHORT  usFlags = SHORT2FROMMP(*pmpThis);
+
+                    if (usFlags & XAC_MOVEX)
+                    {
+                        x += ldcx;
+                        ulSwpFlags |= SWP_MOVE;
+                    }
+                    if (usFlags & XAC_MOVEY)
+                    {
+                        y += ldcy;
+                        ulSwpFlags |= SWP_MOVE;
+                    }
+                    if (usFlags & XAC_SIZEX)
+                    {
+                        cx += ldcx;
+                        ulSwpFlags |= SWP_SIZE;
+                    }
+                    if (usFlags & XAC_SIZEY)
+                    {
+                        cy += ldcy;
+                        ulSwpFlags |= SWP_SIZE;
+                    }
+
+                    if (ulSwpFlags)
+                    {
+                        WinSetWindowPos(hwndThis,
+                                        NULLHANDLE, // hwndInsertBehind
+                                        x, y, cx, cy,
+                                        ulSwpFlags);
+                        cWindows++;
+                        brc = TRUE;
+                    }
+                }
+
+                pswpThis++;
+                pmpThis++;
+            }
+
+            // _Pmpf(("winhAdjustControls: set %d windows", cWindows));
+        }
+    }
+    else
+    {
+        // pxac == NULL:
+        // cleanup mode
+        if (pxac->paswp)
+            free(pxac->paswp);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ winhCenterWindow:
+ *      centers a window within its parent window. If that's
+ *      the PM desktop, it will be centered according to the
+ *      whole screen.
+ *      For dialog boxes, use WinCenteredDlgBox as a one-shot
+ *      function.
+ *
+ *      Note: When calling this function, the window should
+ *      not be visible to avoid flickering.
+ *      This func does not show the window either, so call
+ *      WinShowWindow afterwards.
+ */
+
+void winhCenterWindow(HWND hwnd)
+{
+   RECTL rclParent;
+   RECTL rclWindow;
+
+   WinQueryWindowRect(hwnd, &rclWindow);
+   WinQueryWindowRect(WinQueryWindow(hwnd, QW_PARENT), &rclParent);
+
+   rclWindow.xLeft   = (rclParent.xRight - rclWindow.xRight) / 2;
+   rclWindow.yBottom = (rclParent.yTop   - rclWindow.yTop  ) / 2;
+
+   WinSetWindowPos(hwnd, NULLHANDLE, rclWindow.xLeft, rclWindow.yBottom,
+                    0, 0, SWP_MOVE);
+}
+
+/*
+ *@@ winhCenteredDlgBox:
+ *      just like WinDlgBox, but the dlg box is centered on the screen;
+ *      you should mark the dlg template as not visible in the dlg
+ *      editor, or display will flicker.
+ *      As opposed to winhCenterWindow, this _does_ show the window.
+ */
+
+ULONG winhCenteredDlgBox(HWND hwndParent,
+                         HWND hwndOwner,
+                         PFNWP pfnDlgProc,
+                         HMODULE hmod,
+                         ULONG idDlg,
+                         PVOID pCreateParams)
+{
+    ULONG   ulReply;
+    HWND    hwndDlg = WinLoadDlg(hwndParent,
+                                 hwndOwner,
+                                 pfnDlgProc,
+                                 hmod,
+                                 idDlg,
+                                 pCreateParams);
+    winhCenterWindow(hwndDlg);
+    ulReply = WinProcessDlg(hwndDlg);
+    WinDestroyWindow(hwndDlg);
+    return (ulReply);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Presentation parameters
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Presparams helpers                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhQueryWindowFont:
+ *      returns the window font presentation parameter
+ *      in a newly allocated buffer, which you must
+ *      free() afterwards. Returns NULL if not found.
+ *
+ *@@added V0.9.1 (2000-02-14) [umoeller]
+ */
+
+PSZ winhQueryWindowFont(HWND hwnd)
+{
+    CHAR  szNewFont[100] = "";
+    WinQueryPresParam(hwnd,
+                      PP_FONTNAMESIZE,
+                      0,
+                      NULL,
+                      (ULONG)sizeof(szNewFont),
+                      (PVOID)&szNewFont,
+                      QPF_NOINHERIT);
+    if (szNewFont[0] != 0)
+        return (strdup(szNewFont));
+
+    return (NULL);
+}
+
+/*
+ *@@ winhSetWindowFont:
+ *      this sets a window's font by invoking
+ *      WinSetPresParam on it.
+ *
+ *      If (pszFont == NULL), a default font will be set
+ *      (on Warp 4, "9.WarpSans", on Warp 3, "8.Helv").
+ *
+ *      winh.h also defines the winhSetDlgItemFont macro.
+ *
+ *      Returns TRUE if successful or FALSE otherwise.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL winhSetWindowFont(HWND hwnd,
+                       PSZ pszFont)
+{
+    CHAR    szFont[256];
+
+    if (pszFont == NULL)
+    {
+        if (doshIsWarp4())
+            strhncpy0(szFont, "9.WarpSans", sizeof(szFont));
+        else
+            strhncpy0(szFont, "8.Helv", sizeof(szFont));
+    }
+    else
+        strhncpy0(szFont, pszFont, sizeof(szFont));
+
+    return (WinSetPresParam(hwnd,
+                            PP_FONTNAMESIZE,
+                            strlen(szFont)+1,
+                            szFont));
+}
+
+/*
+ *@@ winhSetControlsFont:
+ *      this sets the font for all the controls of hwndDlg
+ *      which have a control ID in the range of usIDMin to
+ *      usIDMax. "Unused" IDs (i.e. -1) will also be set.
+ *
+ *      If (pszFont == NULL), a default font will be set
+ *      (on Warp 4, "9.WarpSans", on Warp 3, "8.Helv").
+ *
+ *      Returns the no. of controls set.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+ULONG winhSetControlsFont(HWND hwndDlg,      // in: dlg to set
+                          SHORT usIDMin,     // in: minimum control ID to be set (inclusive)
+                          SHORT usIDMax,     // in: maximum control ID to be set (inclusive)
+                          const char *pcszFont)  // in: font to use (e.g. "9.WarpSans") or NULL
+{
+    ULONG   ulrc = 0;
+    HENUM   henum;
+    HWND    hwndItem;
+    CHAR    szFont[256];
+    ULONG   cbFont;
+
+    if (pcszFont == NULL)
+    {
+        if (doshIsWarp4())
+            strhncpy0(szFont, "9.WarpSans", sizeof(szFont));
+        else
+            strhncpy0(szFont, "8.Helv", sizeof(szFont));
+    }
+    else
+        strhncpy0(szFont, pcszFont, sizeof(szFont));
+    cbFont = strlen(szFont)+1;
+
+    // set font for all the dialog controls
+    henum = WinBeginEnumWindows(hwndDlg);
+    while ((hwndItem = WinGetNextWindow(henum)))
+    {
+        SHORT sID = WinQueryWindowUShort(hwndItem, QWS_ID);
+        if (    (sID == -1)
+             || ((sID >= usIDMin) && (sID <= usIDMax))
+           )
+            if (WinSetPresParam(hwndItem,
+                                PP_FONTNAMESIZE,
+                                cbFont,
+                                szFont))
+                // successful:
+                ulrc++;
+    }
+    WinEndEnumWindows(henum);
+    return (ulrc);
+}
+
+/*
+ *@@ winhStorePresParam:
+ *      this appends a new presentation parameter to an
+ *      array of presentation parameters which can be
+ *      passed to WinCreateWindow. This is preferred
+ *      over setting the presparams using WinSetPresParams,
+ *      because that call will cause a lot of messages.
+ *
+ *      On the first call, pppp _must_ be NULL. This
+ *      will allocate memory for storing the given
+ *      data as necessary and modify *pppp to point
+ *      to the new array.
+ *
+ *      On subsequent calls with the same pppp, memory
+ *      will be reallocated, the old data will be copied,
+ *      and the new given data will be appended.
+ *
+ *      Use free() on your PPRESPARAMS pointer (whose
+ *      address was passed) after WinCreateWindow.
+ *
+ *      Example:
+ +      PPRESPARAMS ppp = NULL;
+ +      CHAR szFont[] = "9.WarpSans";
+ +      LONG lColor = CLR_WHITE;
+ +      winhStorePresParam(&ppp, PP_FONTNAMESIZE, sizeof(szFont), szFont);
+ +      winhStorePresParam(&ppp, PP_BACKGROUNDCOLOR, sizeof(lColor), &lColor);
+ +      WinCreateWindow(...., ppp);
+ +      free(ppp);
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL winhStorePresParam(PPRESPARAMS *pppp,      // in: data pointer (modified)
+                        ULONG ulAttrType,       // in: PP_* index
+                        ULONG cbData,           // in: sizeof(*pData), e.g. sizeof(LONG)
+                        PVOID pData)            // in: presparam data (e.g. a PLONG to a color)
+{
+    BOOL        brc = FALSE;
+    if (pppp)
+    {
+        ULONG       cbOld = 0,
+                    cbNew;
+        PBYTE       pbTemp = 0;
+        PPRESPARAMS pppTemp = 0;
+        PPARAM      pppCopyTo = 0;
+
+        if (*pppp != NULL)
+            // subsequent calls:
+            cbOld = (**pppp).cb;
+
+        cbNew = sizeof(ULONG)       // PRESPARAMS.cb
+                + cbOld             // old count, which does not include PRESPARAMS.cb
+                + sizeof(ULONG)     // PRESPARAMS.aparam[0].id
+                + sizeof(ULONG)     // PRESPARAMS.aparam[0].cb
+                + cbData;           // PRESPARAMS.aparam[0].ab[]
+
+        pbTemp = (PBYTE)malloc(cbNew);
+        if (pbTemp)
+        {
+            pppTemp = (PPRESPARAMS)pbTemp;
+
+            if (*pppp != NULL)
+            {
+                // copy old data
+                memcpy(pbTemp, *pppp, cbOld + sizeof(ULONG)); // including PRESPARAMS.cb
+                pppCopyTo = (PPARAM)(pbTemp             // new buffer
+                                     + sizeof(ULONG)    // skipping PRESPARAMS.cb
+                                     + cbOld);          // old PARAM array
+            }
+            else
+                // first call:
+                pppCopyTo = pppTemp->aparam;
+
+            pppTemp->cb = cbNew - sizeof(ULONG);     // excluding PRESPARAMS.cb
+            pppCopyTo->id = ulAttrType;
+            pppCopyTo->cb = cbData;       // byte count of PARAM.ab[]
+            memcpy(pppCopyTo->ab, pData, cbData);
+
+            free(*pppp);
+            *pppp = pppTemp;
+
+            brc = TRUE;
+        }
+    }
+    return (brc);
+}
+
+/*
+ *@@ winhQueryPresColor:
+ *      returns the specified color. This is queried in the
+ *      following order:
+ *
+ *      1)  hwnd's pres params are searched for ulPP
+ *          (which should be a PP_* index);
+ *      2)  if (fInherit == TRUE), the parent windows
+ *          are searched also;
+ *      3)  if this fails or (fInherit == FALSE), WinQuerySysColor
+ *          is called to get lSysColor (which should be a SYSCLR_*
+ *          index).
+ *
+ *      The return value is always an RGB LONG, _not_ a color index.
+ *      This is even true for the returned system colors, which are
+ *      converted to RGB.
+ *
+ *      If you do any painting with this value, you should switch
+ *      the HPS you're using to RGB mode (use gpihSwitchToRGB for that).
+ *
+ *      Some useful ulPP / lSysColor pairs
+ *      (default values as in PMREF):
+ *
+ +          --  PP_FOREGROUNDCOLOR          SYSCLR_WINDOWTEXT (for most controls also)
+ +                                          SYSCLR_WINDOWSTATICTEXT (for static controls)
+ +                 Foreground color (default: black)
+ +          --  PP_BACKGROUNDCOLOR          SYSCLR_BACKGROUND
+ +                                          SYSCLR_DIALOGBACKGROUND
+ +                                          SYSCLR_FIELDBACKGROUND (for disabled scrollbars)
+ +                                          SYSCLR_WINDOW (application surface -- empty clients)
+ +                 Background color (default: light gray)
+ +          --  PP_ACTIVETEXTFGNDCOLOR
+ +          --  PP_HILITEFOREGROUNDCOLOR    SYSCLR_HILITEFOREGROUND
+ +                 Highlighted foreground color, for example for selected menu
+ +                 (def.: white)
+ +          --  PP_ACTIVETEXTBGNDCOLOR
+ +          --  PP_HILITEBACKGROUNDCOLOR    SYSCLR_HILITEBACKGROUND
+ +                 Highlighted background color (def.: dark gray)
+ +          --  PP_INACTIVETEXTFGNDCOLOR
+ +          --  PP_DISABLEDFOREGROUNDCOLOR  SYSCLR_MENUDISABLEDTEXT
+ +                 Disabled foreground color (dark gray)
+ +          --  PP_INACTIVETEXTBGNDCOLOR
+ +          --  PP_DISABLEDBACKGROUNDCOLOR
+ +                 Disabled background color
+ +          --  PP_BORDERCOLOR              SYSCLR_WINDOWFRAME
+ +                                          SYSCLR_INACTIVEBORDER
+ +                 Border color (around pushbuttons, in addition to
+ +                 the 3D colors)
+ +          --  PP_ACTIVECOLOR              SYSCLR_ACTIVETITLE
+ +                 Active color
+ +          --  PP_INACTIVECOLOR            SYSCLR_INACTIVETITLE
+ +                 Inactive color
+ *
+ *      For menus:
+ +          --  PP_MENUBACKGROUNDCOLOR      SYSCLR_MENU
+ +          --  PP_MENUFOREGROUNDCOLOR      SYSCLR_MENUTEXT
+ +          --  PP_MENUHILITEBGNDCOLOR      SYSCLR_MENUHILITEBGND
+ +          --  PP_MENUHILITEFGNDCOLOR      SYSCLR_MENUHILITE
+ +          --  ??                          SYSCLR_MENUDISABLEDTEXT
+ +
+ *      For containers (according to the API ref. at EDM/2):
+ +          --  PP_FOREGROUNDCOLOR          SYSCLR_WINDOWTEXT
+ +          --  PP_BACKGROUNDCOLOR          SYSCLR_WINDOW
+ +          --  PP_HILITEFOREGROUNDCOLOR    SYSCLR_HILITEFOREGROUND
+ +          --  PP_HILITEBACKGROUNDCOLOR    SYSCLR_HILITEBACKGROUND
+ +          --  PP_BORDERCOLOR
+ +                  (used for separator lines, eg. in Details view)
+ +          --  PP_ICONTEXTBACKGROUNDCOLOR
+ +                  (column titles in Details view?!?)
+ +
+ *      For listboxes / entryfields / MLE's:
+ +          --  PP_BACKGROUNDCOLOR          SYSCLR_ENTRYFIELD
+ *
+ *  PMREF has more of these.
+ *
+ *@@changed V0.9.0 [umoeller]: removed INI key query, using SYSCLR_* instead; function prototype changed
+ *@@changed V0.9.0 [umoeller]: added fInherit parameter
+ */
+
+LONG winhQueryPresColor(HWND    hwnd,       // in: window to query
+                        ULONG   ulPP,       // in: PP_* index
+                        BOOL    fInherit,   // in: search parent windows too?
+                        LONG    lSysColor)  // in: SYSCLR_* index
+{
+    ULONG   ul,
+            attrFound,
+            abValue[32];
+
+    if (ulPP != (ULONG)-1)
+        if ((ul = WinQueryPresParam(hwnd,
+                                    ulPP,
+                                    0,
+                                    &attrFound,
+                                    (ULONG)sizeof(abValue),
+                                    (PVOID)&abValue,
+                                    (fInherit)
+                                         ? 0
+                                         : QPF_NOINHERIT)))
+            return (abValue[0]);
+
+    // not found: get system color
+    return (WinQuerySysColor(HWND_DESKTOP, lSysColor, 0));
+}
+
+/*
+ *@@category: Helpers\PM helpers\Help (IPF)
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Help instance helpers                                          *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhCreateHelp:
+ *      creates a help instance and connects it with the
+ *      given frame window.
+ *
+ *      If (pszFileName == NULL), we'll retrieve the
+ *      executable's fully qualified file name and
+ *      replace the extension with .HLP simply. This
+ *      avoids the typical "Help not found" errors if
+ *      the program isn't started in its own directory.
+ *
+ *      If you have created a help table in memory, specify it
+ *      with pHelpTable. To load a help table from the resources,
+ *      specify hmod (or NULLHANDLE) and set pHelpTable to the
+ *      following:
+ +
+ +          (PHELPTABLE)MAKELONG(usTableID, 0xffff)
+ *
+ *      Returns the help window handle or NULLHANDLE on errors.
+ *
+ *      Based on an EDM/2 code snippet.
+ *
+ *@@added V0.9.4 (2000-07-03) [umoeller]
+ */
+
+HWND winhCreateHelp(HWND    hwndFrame,      // in: app's frame window handle; can be NULLHANDLE
+                    PSZ     pszFileName,    // in: help file name or NULL
+                    HMODULE hmod,           // in: module with help table or NULLHANDLE (current)
+                    PHELPTABLE pHelpTable,  // in: help table or resource ID
+                    PSZ     pszWindowTitle) // in: help window title or NULL
+{
+    PPIB     ppib;
+    PTIB     ptib;
+    HELPINIT hi;
+    PSZ      pszExt;
+    CHAR     szName[ CCHMAXPATH ];
+    HWND     hwndHelp;
+
+    if (     (pszFileName == NULL)
+          // || (*pszFileName)
+       )
+    {
+        DosGetInfoBlocks(&ptib, &ppib);
+
+        DosQueryModuleName(ppib->pib_hmte, sizeof(szName), szName);
+
+        pszExt = strrchr(szName, '.');
+        if (pszExt)
+            strcpy(pszExt, ".hlp");
+        else
+            strcat(szName, ".hlp");
+
+        pszFileName = szName;
+    }
+
+    hi.cb                       = sizeof(HELPINIT);
+    hi.ulReturnCode             = 0;
+    hi.pszTutorialName          = NULL;
+    hi.phtHelpTable             = pHelpTable;
+    hi.hmodHelpTableModule      = hmod;
+    hi.hmodAccelActionBarModule = NULLHANDLE;
+    hi.idAccelTable             = 0;
+    hi.idActionBar              = 0;
+    hi.pszHelpWindowTitle       = pszWindowTitle;
+    hi.fShowPanelId             = CMIC_HIDE_PANEL_ID;
+    hi.pszHelpLibraryName       = pszFileName;
+
+    hwndHelp = WinCreateHelpInstance(WinQueryAnchorBlock(hwndFrame),
+                                     &hi);
+    if ((hwndFrame) && (hwndHelp))
+    {
+        WinAssociateHelpInstance(hwndHelp, hwndFrame);
+    }
+
+    return (hwndHelp);
+}
+
+/*
+ *@@ winhDestroyHelp:
+ *      destroys the help instance created by winhCreateHelp.
+ *
+ *      Based on an EDM/2 code snippet.
+ *
+ *@@added V0.9.4 (2000-07-03) [umoeller]
+ */
+
+void winhDestroyHelp(HWND hwndHelp,
+                     HWND hwndFrame)    // can be NULLHANDLE if not used with winhCreateHelp
+{
+    if (hwndHelp)
+    {
+        if (hwndFrame)
+            WinAssociateHelpInstance(NULLHANDLE, hwndFrame);
+        WinDestroyHelpInstance(hwndHelp);
+    }
+}
+
+/*
+ *@@category: Helpers\PM helpers\Application control
+ */
+
+/* ******************************************************************
+ *
+ *   Application control
+ *
+ ********************************************************************/
+
+/*
+ *@@ CallBatchCorrectly:
+ *      fixes the specified PROGDETAILS for
+ *      command files in the executable part
+ *      by inserting /C XXX into the parameters
+ *      and setting the executable according
+ *      to an environment variable.
+ *
+ *@@added V0.9.6 (2000-10-16) [umoeller]
+ */
+
+VOID CallBatchCorrectly(PPROGDETAILS pProgDetails,
+                        PSZ *ppszParams,            // in/out: modified parameters (reallocated)
+                        const char *pcszEnvVar,     // in: env var spec'g command proc
+                                                    // (e.g. "OS2_SHELL"); can be NULL
+                        const char *pcszDefProc)    // in: def't command proc (e.g. "CMD.EXE")
+{
+    // XXX.CMD file as executable:
+    // fix args to /C XXX.CMD
+    PSZ pszNewParams = strdup("/C ");
+    xstrcat(&pszNewParams, pProgDetails->pszExecutable);
+    if (*ppszParams)
+    {
+        // append old params
+        xstrcat(&pszNewParams, " ");
+        xstrcat(&pszNewParams, *ppszParams);
+        free(*ppszParams);
+    }
+    *ppszParams = pszNewParams;
+            // freed by caller
+
+    // set executable to $(OS2_SHELL)
+    pProgDetails->pszExecutable = NULL;
+    if (pcszEnvVar)
+        pProgDetails->pszExecutable = getenv(pcszEnvVar);
+    if (!pProgDetails->pszExecutable)
+        pProgDetails->pszExecutable = (PSZ)pcszDefProc;
+                // should be on PATH
+}
+
+/*
+ *@@ winhStartApp:
+ *      wrapper around WinStartApp which fixes the
+ *      specified PROGDETAILS to (hopefully) work
+ *      work with all executable types:
+ *
+ *      1.  This fixes the executable info to support:
+ *
+ *          -- starting "*" executables (command prompts
+ *             for OS/2, DOS, Win-OS/2);
+ *
+ *          -- starting ".CMD" and ".BAT" files as
+ *             PROGDETAILS.pszExecutable.
+ *
+ *      2.  Handles and merges special and default
+ *          environments for the app to be started.
+ *          If PROGDETAILS.pszEnvironment is empty
+ *          and the application is a Win-OS/2 app,
+ *          this uses the default Win-OS/2 settings
+ *          as specified in the "Win-OS/2" WPS settings
+ *          object.
+ *
+ *      Since this calls WinStartApp in turn, this
+ *      requires a message queue on the calling thread.
+ *
+ *@@added V0.9.6 (2000-10-16) [umoeller]
+ */
+
+HAPP winhStartApp(HWND hwndNotify,                  // in: notify window (as with WinStartApp)
+                  const PROGDETAILS *pcProgDetails) // in: program data
+{
+    HAPP            happ = NULLHANDLE;
+    PSZ             pszParamsPatched = NULL;
+    BOOL            fIsWindowsApp = FALSE,
+                    fIsWindowsEnhApp = FALSE;
+    PROGDETAILS     ProgDetails;
+    PSZ             pszWinOS2Env = 0;
+
+    memcpy(&ProgDetails, pcProgDetails, sizeof(PROGDETAILS));
+            // pointers still point into old prog details buffer
+    ProgDetails.Length = sizeof(PROGDETAILS);
+    ProgDetails.progt.fbVisible = SHE_VISIBLE;
+    ProgDetails.pszEnvironment = 0; // "WORKPLACE\0\0";
+    memset(&ProgDetails.swpInitial, 0, sizeof(SWP));
+
+    // duplicate parameters...
+    // we need this for string manipulations below...
+    if (ProgDetails.pszParameters)
+        pszParamsPatched = strdup(ProgDetails.pszParameters);
+
+    // _Pmpf((__FUNCTION__ ": old progc: 0x%lX", pcProgDetails->progt.progc));
+    // _Pmpf(("  pszTitle: %s", (ProgDetails.pszTitle) ? ProgDetails.pszTitle : NULL));
+    // _Pmpf(("  pszIcon: %s", (ProgDetails.pszIcon) ? ProgDetails.pszIcon : NULL));
+
+    // program type fixups
+    switch (ProgDetails.progt.progc)
+    {
+        case ((ULONG)-1):       // we get that sometimes...
+        case PROG_DEFAULT:
+            // ###
+        break;
+    }
+
+    // now try again...
+    switch (ProgDetails.progt.progc)
+    {
+        case PROG_31_ENH:
+        case PROG_31_ENHSEAMLESSCOMMON:
+        case PROG_31_ENHSEAMLESSVDM:
+            fIsWindowsApp = TRUE;
+            fIsWindowsEnhApp = TRUE;
+        break;
+
+        case PROG_WINDOW_AUTO:
+        case PROG_30_STD:
+        case PROG_31_STD:
+        case PROG_WINDOW_REAL:
+        case PROG_30_STDSEAMLESSVDM:
+        case PROG_31_STDSEAMLESSVDM:
+        case PROG_30_STDSEAMLESSCOMMON:
+        case PROG_31_STDSEAMLESSCOMMON:
+            fIsWindowsApp = TRUE;
+        break;
+    }
+
+    /*
+     * command lines fixups:
+     *
+     */
+
+    if (strcmp(ProgDetails.pszExecutable, "*") == 0)
+    {
+        /*
+         * "*" for command sessions:
+         *
+         */
+
+        if (fIsWindowsEnhApp)
+        {
+            // enhanced Win-OS/2 session:
+            PSZ psz2 = strdup("/3 ");
+            if (pszParamsPatched)
+            {
+                // append existing params
+                xstrcat(&psz2, pszParamsPatched);
+                free(pszParamsPatched);
+            }
+
+            pszParamsPatched = psz2;
+        }
+
+        if (fIsWindowsApp)
+        {
+            // cheat: WinStartApp doesn't support NULL
+            // for Win-OS2 sessions, so manually start winos2.com
+            ProgDetails.pszExecutable = "WINOS2.COM";
+            // this is a DOS app, so fix this to DOS fullscreen
+            ProgDetails.progt.progc = PROG_VDM;
+        }
+        else
+            // for all other executable types
+            // (including OS/2 and DOS sessions),
+            // set pszExecutable to NULL; this will
+            // have WinStartApp start a cmd shell
+            ProgDetails.pszExecutable = NULL;
+
+    } // end if (strcmp(pProgDetails->pszExecutable, "*") == 0)
+    else
+        switch (ProgDetails.progt.progc)
+        {
+            /*
+             *  .CMD files fixups
+             *
+             */
+
+            case PROG_FULLSCREEN:       // OS/2 fullscreen
+            case PROG_WINDOWABLEVIO:    // OS/2 window
+            {
+                PSZ pszExtension = doshGetExtension(ProgDetails.pszExecutable);
+                if (pszExtension)
+                {
+                    if (stricmp(pszExtension, "CMD") == 0)
+                    {
+                        CallBatchCorrectly(&ProgDetails,
+                                           &pszParamsPatched,
+                                           "OS2_SHELL",
+                                           "CMD.EXE");
+                    }
+                }
+            break; }
+
+            case PROG_VDM:              // DOS fullscreen
+            case PROG_WINDOWEDVDM:      // DOS window
+            {
+                PSZ pszExtension = doshGetExtension(ProgDetails.pszExecutable);
+                if (pszExtension)
+                {
+                    if (stricmp(pszExtension, "BAT") == 0)
+                    {
+                        CallBatchCorrectly(&ProgDetails,
+                                           &pszParamsPatched,
+                                           NULL,
+                                           "COMMAND.COM");
+                    }
+                }
+            break; }
+        } // end switch (ProgDetails.progt.progc)
+
+    /*
+     *  Fix environment for Win-OS/2
+     *
+     */
+
+    if (    !(xstrIsString(ProgDetails.pszEnvironment)) // env empty
+         && (fIsWindowsApp)                             // and win-os2 app
+       )
+    {
+        ULONG ulSize = 0;
+        // get default environment (from Win-OS/2 settings object)
+        // from OS2.INI
+        PSZ pszDefEnv = prfhQueryProfileData(HINI_USER,
+                                             "WINOS2",
+                                             "PM_GlobalWindows31Settings",
+                                             &ulSize);
+        if (pszDefEnv)
+        {
+            PSZ pszDefEnv2 = (PSZ)malloc(ulSize + 2);
+            if (pszDefEnv2)
+            {
+                PSZ p = pszDefEnv2;
+                memset(pszDefEnv2, 0, ulSize + 2);
+                memcpy(pszDefEnv2, pszDefEnv, ulSize);
+
+                for (p = pszDefEnv2;
+                     p < pszDefEnv2 + ulSize;
+                     p++)
+                    if (*p == ';')
+                        *p = 0;
+
+                // okay.... now we got an OS/2-style environment
+                // with 0, 0, 00 strings
+
+                pszWinOS2Env = pszDefEnv2;  // freed below
+
+                // use this
+                ProgDetails.pszEnvironment = pszWinOS2Env;
+            }
+
+            free(pszDefEnv);
+        }
+    }
+
+    // _Pmpf((__FUNCTION__ ": calling WinStartApp"));
+    // _Pmpf(("    exec: %s",
+    //             (ProgDetails.pszExecutable)
+                    // ? ProgDetails.pszExecutable
+                // : "NULL"));
+    // _Pmpf(("    startupDir: %s",
+       //      (ProgDetails.pszStartupDir)
+          //       ? ProgDetails.pszStartupDir
+             //    : "NULL"));
+    // _Pmpf(("    params: %s",
+       //      (pszParamsPatched)
+          //       ? pszParamsPatched
+             //    : "NULL"));
+    // _Pmpf(("    new progc: 0x%lX", ProgDetails.progt.progc));
+
+    ProgDetails.pszParameters = pszParamsPatched;
+
+    happ = WinStartApp(hwndNotify,
+                                // receives WM_APPTERMINATENOTIFY
+                       &ProgDetails,
+                       pszParamsPatched,
+                       NULL,            // "reserved", PMREF says...
+                       SAF_INSTALLEDCMDLINE);
+                            // we MUST use SAF_INSTALLEDCMDLINE
+                            // or no Win-OS/2 session will start...
+                            // whatever is going on here... Warp 4 FP11
+
+                            // do not use SAF_STARTCHILDAPP, or the
+                            // app will be terminated automatically
+
+    // _Pmpf((__FUNCTION__ ": got happ 0x%lX", happ));
+
+    if (pszParamsPatched)
+        free(pszParamsPatched);
+    if (pszWinOS2Env)
+        free(pszWinOS2Env);
+
+    return (happ);
+}
+
+/*
+ *@@ winhAnotherInstance:
+ *      this tests whether another instance of the same
+ *      application is already running.
+ *
+ *      To identify instances of the same application, the
+ *      application must call this function during startup
+ *      with the unique name of an OS/2 semaphore. As with
+ *      all OS/2 semaphores, the semaphore name must begin
+ *      with "\\SEM32\\". The semaphore isn't really used
+ *      except for testing for its existence, since that
+ *      name is unique among all processes.
+ *
+ *      If another instance is found, TRUE is returned. If
+ *      (fSwitch == TRUE), that instance is switched to,
+ *      using the tasklist.
+ *
+ *      If no other instance is found, FALSE is returned only.
+ *
+ *      Based on an EDM/2 code snippet.
+ *
+ *@@added V0.9.0 (99-10-22) [umoeller]
+ */
+
+BOOL winhAnotherInstance(PSZ pszSemName,    // in: semaphore ID
+                         BOOL fSwitch)      // in: if TRUE, switch to first instance if running
+{
+    HMTX hmtx;
+
+    if (DosCreateMutexSem(pszSemName,
+                          &hmtx,
+                          DC_SEM_SHARED,
+                          TRUE)
+              == NO_ERROR)
+        // semapore created: this doesn't happen if the semaphore
+        // exists already, so no other instance is running
+        return (FALSE);
+
+    // else: instance running
+    hmtx = NULLHANDLE;
+
+    // switch to other instance?
+    if (fSwitch)
+    {
+        // yes: query mutex creator
+        if (DosOpenMutexSem(pszSemName,
+                            &hmtx)
+                    == NO_ERROR)
+        {
+            PID     pid = 0;
+            TID     tid = 0;        // unused
+            ULONG   ulCount;        // unused
+
+            if (DosQueryMutexSem(hmtx, &pid, &tid, &ulCount) == NO_ERROR)
+            {
+                HSWITCH hswitch = WinQuerySwitchHandle(NULLHANDLE, pid);
+                if (hswitch != NULLHANDLE)
+                    WinSwitchToProgram(hswitch);
+            }
+
+            DosCloseMutexSem(hmtx);
+        }
+    }
+
+    return (TRUE);      // another instance exists
+}
+
+/*
+ *@@ winhAddToTasklist:
+ *      this adds the specified window to the tasklist
+ *      with hIcon as its program icon (which is also
+ *      set for the main window). This is useful for
+ *      the old "dialog as main window" trick.
+ *
+ *      Returns the HSWITCH of the added entry.
+ */
+
+HSWITCH winhAddToTasklist(HWND hwnd,       // in: window to add
+                          HPOINTER hIcon)  // in: icon for main window
+{
+    SWCNTRL     swctl;
+    HSWITCH hswitch = 0;
+    swctl.hwnd = hwnd;                     // window handle
+    swctl.hwndIcon = hIcon;                // icon handle
+    swctl.hprog = NULLHANDLE;              // program handle (use default)
+    WinQueryWindowProcess(hwnd, &(swctl.idProcess), NULL);
+                                           // process identifier
+    swctl.idSession = 0;                   // session identifier ?
+    swctl.uchVisibility = SWL_VISIBLE;     // visibility
+    swctl.fbJump = SWL_JUMPABLE;           // jump indicator
+    // get window title from window titlebar
+    if (hwnd)
+        WinQueryWindowText(hwnd, sizeof(swctl.szSwtitle), swctl.szSwtitle);
+    swctl.bProgType = PROG_DEFAULT;        // program type
+    hswitch = WinAddSwitchEntry(&swctl);
+
+    // give the main window the icon
+    if ((hwnd) && (hIcon))
+        WinSendMsg(hwnd,
+                   WM_SETICON,
+                   (MPARAM)hIcon,
+                   NULL);
+
+    return (hswitch);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Miscellaneous
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Miscellaneous                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhSleep:
+ *      sleeps at least the specified amount of time,
+ *      without blocking the message queue.
+ *
+ *@@added V0.9.4 (2000-07-11) [umoeller]
+ */
+
+VOID winhSleep(HAB hab,
+               ULONG ulSleep)    // in: sleep time in milliseconds
+{
+    ULONG ul = 0;
+    QMSG qmsg;
+    for (ul = 0;
+         ul < (ulSleep / 50);
+         ul++)
+    {
+        DosSleep(50);
+        while (WinPeekMsg(hab,
+                          &qmsg, 0, 0, 0,
+                          PM_REMOVE))
+            WinDispatchMsg(hab, &qmsg);
+
+    }
+}
+
+/*
+ *@@ winhFileDlg:
+ *      one-short function for opening an "Open" file
+ *      dialog.
+ *
+ *      On input, pszFile specifies the directory and
+ *      file specification (e.g. "F:\*.txt").
+ *
+ *      Returns TRUE if the user pressed OK. In that
+ *      case, the fully qualified filename is written
+ *      into pszFile again.
+ *
+ *      Returns FALSE if the user pressed Cancel.
+ *
+ *      Notes about flFlags:
+ *
+ *      -- WINH_FOD_SAVEDLG: display a "Save As" dialog.
+ *         Otherwise an "Open" dialog is displayed.
+ *
+ *      -- WINH_FOD_INILOADDIR: load a directory from the
+ *         specified INI key and switch the dlg to it.
+ *         In that case, on input, pszFile must only
+ *         contain the file filter without any path
+ *         specification, because that is loaded from
+ *         the INI key. If the INI key does not exist,
+ *         the current process directory will be used.
+ *
+ *      -- WINH_FOD_INISAVEDIR: if the user presses OK,
+ *         the directory of the selected file is written
+ *         to the specified INI key so that it can be
+ *         reused later. This flag is independent of
+ *         WINH_FOD_INISAVEDIR: you can specify none,
+ *         one, or both of them.
+ *
+ *@@added V0.9.3 (2000-04-29) [umoeller]
+ */
+
+BOOL winhFileDlg(HWND hwndOwner,    // in: owner for file dlg
+                 PSZ pszFile,       // in: file mask; out: fully q'd filename
+                                    //    (should be CCHMAXPATH in size)
+                 ULONG flFlags,     // in: any combination of the following:
+                                    // -- WINH_FOD_SAVEDLG: save dlg; else open dlg
+                                    // -- WINH_FOD_INILOADDIR: load FOD path from INI
+                                    // -- WINH_FOD_INISAVEDIR: store FOD path to INI on OK
+                 HINI hini,         // in: INI file to load/store last path from (can be HINI_USER)
+                 PSZ pszApplication, // in: INI application to load/store last path from
+                 PSZ pszKey)        // in: INI key to load/store last path from
+{
+    FILEDLG fd;
+    memset(&fd, 0, sizeof(FILEDLG));
+    fd.cbSize = sizeof(FILEDLG);
+    fd.fl = FDS_CENTER;
+
+    if (flFlags & WINH_FOD_SAVEDLG)
+        fd.fl |= FDS_SAVEAS_DIALOG;
+    else
+        fd.fl |= FDS_OPEN_DIALOG;
+
+    // default: copy pszFile
+    strcpy(fd.szFullFile, pszFile);
+
+    if ( (hini) && (flFlags & WINH_FOD_INILOADDIR) )
+    {
+        // overwrite with initial directory for FOD from OS2.INI
+        if (PrfQueryProfileString(hini,
+                                  pszApplication,
+                                  pszKey,
+                                  "",      // default string
+                                  fd.szFullFile,
+                                  sizeof(fd.szFullFile)-10)
+                    >= 2)
+        {
+            // found: append "\*"
+            strcat(fd.szFullFile, "\\");
+            strcat(fd.szFullFile, pszFile);
+        }
+    }
+
+    if (    WinFileDlg(HWND_DESKTOP,    // parent
+                       hwndOwner, // owner
+                       &fd)
+        && (fd.lReturn == DID_OK)
+       )
+    {
+        // save path back?
+        if (    (hini)
+             && (flFlags & WINH_FOD_INISAVEDIR)
+           )
+        {
+            // get the directory that was used
+            PSZ p = strrchr(fd.szFullFile, '\\');
+            if (p)
+            {
+                // contains directory:
+                // copy to OS2.INI
+                PSZ pszDir = strhSubstr(fd.szFullFile, p);
+                if (pszDir)
+                {
+                    PrfWriteProfileString(hini,
+                                          pszApplication,
+                                          pszKey, // "XWPSound:LastDir"
+                                          pszDir);
+                    free(pszDir);
+                }
+            }
+        }
+
+        strcpy(pszFile, fd.szFullFile);
+
+        return (TRUE);
+    }
+
+    return (FALSE);
+}
+
+/*
+ *@@ winhSetWaitPointer:
+ *      this sets the mouse pointer to "Wait".
+ *      Returns the previous pointer (HPOINTER),
+ *      which should be stored somewhere to be
+ *      restored later. Example:
+ +          HPOINTER hptrOld = winhSetWaitPointer();
+ +          ...
+ +          WinSetPointer(HWND_DESKTOP, hptrOld);
+ */
+
+HPOINTER winhSetWaitPointer(VOID)
+{
+    HPOINTER hptr = WinQueryPointer(HWND_DESKTOP);
+    WinSetPointer(HWND_DESKTOP,
+                  WinQuerySysPointer(HWND_DESKTOP,
+                                     SPTR_WAIT,
+                                     FALSE));   // no copy
+    return (hptr);
+}
+
+/*
+ *@@ winhQueryWindowText:
+ *      this returns the window text of the specified
+ *      HWND in a newly allocated buffer, which has
+ *      the exact size of the window text.
+ *
+ *      This buffer must be free()'d later.
+ */
+
+PSZ winhQueryWindowText(HWND hwnd)
+{
+    PSZ     pszText = NULL;
+    ULONG   cbText = WinQueryWindowTextLength(hwnd);
+                                    // additional null character
+    if (cbText)
+    {
+        pszText = (PSZ)malloc(cbText + 1);
+        if (pszText)
+            WinQueryWindowText(hwnd,
+                               cbText + 1,
+                               pszText);
+    }
+    return (pszText);
+}
+
+/*
+ *@@ winhReplaceWindowText:
+ *      this is a combination of winhQueryWindowText
+ *      and xstrrpl (stringh.c) to replace substrings
+ *      in a window.
+ *
+ *      This is useful for filling in placeholders
+ *      a la "%1" in control windows, e.g. static
+ *      texts.
+ *
+ *      This replaces only the first occurence of
+ *      pszSearch.
+ *
+ *      Returns TRUE only if the window exists and
+ *      the search string was replaced.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+BOOL winhReplaceWindowText(HWND hwnd,           // in: window whose text is to be modified
+                           PSZ pszSearch,       // in: search string (e.g. "%1")
+                           PSZ pszReplaceWith)  // in: replacement string for pszSearch
+{
+    BOOL    brc = FALSE;
+    PSZ     pszText = winhQueryWindowText(hwnd);
+    if (pszText)
+    {
+        if (xstrrpl(&pszText, 0, pszSearch, pszReplaceWith, 0) > 0)
+        {
+            WinSetWindowText(hwnd, pszText);
+            brc = TRUE;
+        }
+        free(pszText);
+    }
+    return (brc);
+}
+
+/*
+ *@@ winhEnableDlgItems:
+ *      this enables/disables a whole range of controls
+ *      in a window by enumerating the child windows
+ *      until usIDFirst is found. If so, that subwindow
+ *      is enabled/disabled and all the following windows
+ *      in the enumeration also, until usIDLast is found.
+ *
+ *      Note that this affects _all_ controls following
+ *      the usIDFirst window, no matter what ID they have
+ *      (even if "-1"), until usIDLast is found.
+ *
+ *      Returns the no. of controls which were enabled/disabled
+ *      (null if none).
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.1 (99-12-20) [umoeller]: renamed from winhEnableDlgItems
+ */
+
+ULONG winhEnableControls(HWND hwndDlg,                  // in: dialog window
+                         USHORT usIDFirst,              // in: first affected control ID
+                         USHORT usIDLast,               // in: last affected  control ID (inclusive)
+                         BOOL fEnable)
+{
+    HENUM   henum1 = NULLHANDLE;
+    HWND    hwndThis = NULLHANDLE;
+    ULONG   ulCount = 0;
+
+    henum1 = WinBeginEnumWindows(hwndDlg);
+    while ((hwndThis = WinGetNextWindow(henum1)) != NULLHANDLE)
+    {
+        USHORT usIDCheckFirst = WinQueryWindowUShort(hwndThis, QWS_ID),
+               usIDCheckLast;
+        if (usIDCheckFirst == usIDFirst)
+        {
+            WinEnableWindow(hwndThis, fEnable);
+            ulCount++;
+
+            while ((hwndThis = WinGetNextWindow(henum1)) != NULLHANDLE)
+            {
+                WinEnableWindow(hwndThis, fEnable);
+                ulCount++;
+                usIDCheckLast = WinQueryWindowUShort(hwndThis, QWS_ID);
+                if (usIDCheckLast == usIDLast)
+                    break;
+            }
+
+            break;  // outer loop
+        }
+    }
+    WinEndEnumWindows(henum1);
+    return (ulCount);
+}
+
+/*
+ *@@ winhCreateStdWindow:
+ *      much like WinCreateStdWindow, but this one
+ *      allows you to have the standard window
+ *      positioned automatically, using a given
+ *      SWP structure (*pswpFrame).
+ *
+ *      Alternatively, you can set pswpFrame to NULL
+ *      and specify FCF_SHELLPOSITION with flFrameCreateFlags.
+ *      If you want the window to be shown, specify
+ *      SWP_SHOW (and maybe SWP_ACTIVATE) in *pswpFrame.
+ *
+ *@@added V0.9.0 [umoeller]
+ *@@changed V0.9.5 (2000-08-13) [umoeller]: flStyleClient never worked, fixed
+ */
+
+HWND winhCreateStdWindow(HWND hwndFrameParent,      // in: normally HWND_DESKTOP
+                         PSWP pswpFrame,            // in: frame wnd pos
+                         ULONG flFrameCreateFlags,  // in: FCF_* flags
+                         ULONG ulFrameStyle,        // in: WS_* flags (e.g. WS_VISIBLE, WS_ANIMATE)
+                         PSZ pszFrameTitle,
+                         ULONG ulResourcesID,       // in: according to FCF_* flags
+                         PSZ pszClassClient,
+                         ULONG flStyleClient,
+                         ULONG ulID,                // in: frame window ID
+                         PVOID pClientCtlData,      // in: pCtlData structure pointer for client
+                         PHWND phwndClient)         // out: created client wnd
+{
+    FRAMECDATA  fcdata;
+    HWND        hwndFrame;
+    RECTL       rclClient;
+
+    fcdata.cb            = sizeof(FRAMECDATA);
+    fcdata.flCreateFlags = flFrameCreateFlags;
+    fcdata.hmodResources = (HMODULE)NULL;
+    fcdata.idResources   = ulResourcesID;
+
+    /* Create the frame and client windows.  */
+    hwndFrame = WinCreateWindow(hwndFrameParent,
+                                WC_FRAME,
+                                pszFrameTitle,
+                                ulFrameStyle,
+                                0,0,0,0,         // size and position = 0
+                                NULLHANDLE,      // no owner
+                                HWND_TOP,        // z-order
+                                ulID,            // frame window ID
+                                &fcdata,         // frame class data
+                                NULL);           // no presparams
+
+    if (hwndFrame)
+    {
+        *phwndClient = WinCreateWindow(hwndFrame,      // parent
+                                       pszClassClient, // class
+                                       NULL,           // no title
+                                       flStyleClient,  // style
+                                       0,0,0,0,        // size and position = 0
+                                       hwndFrame,      // owner
+                                       HWND_BOTTOM,    // bottom z-order
+                                       FID_CLIENT,     // frame window ID
+                                       pClientCtlData, // class data
+                                       NULL);          // no presparams
+
+        if (*phwndClient)
+        {
+            if (pswpFrame)
+                // position frame
+                WinSetWindowPos(hwndFrame,
+                                pswpFrame->hwndInsertBehind,
+                                pswpFrame->x,
+                                pswpFrame->y,
+                                pswpFrame->cx,
+                                pswpFrame->cy,
+                                pswpFrame->fl);
+
+            // position client
+            WinQueryWindowRect(hwndFrame, &rclClient);
+            WinCalcFrameRect(hwndFrame, &rclClient,
+                             TRUE);     // calc client from frame
+            WinSetWindowPos(*phwndClient,
+                            HWND_TOP,
+                            rclClient.xLeft,
+                            rclClient.yBottom,
+                            rclClient.xRight - rclClient.xLeft,
+                            rclClient.yTop - rclClient.yBottom,
+                            SWP_MOVE | SWP_SIZE | SWP_SHOW);
+        }
+    }
+    return (hwndFrame);
+}
+
+/*
+ *@@ winhRepaintWindows:
+ *      this repaints all children of hwndParent.
+ *      If this is passed as HWND_DESKTOP, the
+ *      whole screen is repainted.
+ */
+
+VOID winhRepaintWindows(HWND hwndParent)
+{
+    HWND    hwndTop;
+    HENUM   henum = WinBeginEnumWindows(HWND_DESKTOP);
+    while ((hwndTop = WinGetNextWindow(henum)))
+        if (WinIsWindowShowing(hwndTop))
+            WinInvalidateRect(hwndTop, NULL, TRUE);
+    WinEndEnumWindows(henum);
+}
+
+/*
+ *@@ winhFindMsgQueue:
+ *      returns the message queue which matches
+ *      the given process and thread IDs. Since,
+ *      per IBM definition, every thread may only
+ *      have one MQ, this should be unique.
+ *
+ *@@added V0.9.2 (2000-03-08) [umoeller]
+ */
+
+HMQ winhFindMsgQueue(PID pid,           // in: process ID
+                     TID tid,           // in: thread ID
+                     HAB* phab)         // out: anchor block
+{
+    HWND    hwndThis = 0,
+            rc = 0;
+    HENUM   henum = WinBeginEnumWindows(HWND_OBJECT);
+    while ((hwndThis = WinGetNextWindow(henum)))
+    {
+        CHAR    szClass[200];
+        if (WinQueryClassName(hwndThis, sizeof(szClass), szClass))
+        {
+            if (strcmp(szClass, "#32767") == 0)
+            {
+                // message queue window:
+                PID pidWin = 0;
+                TID tidWin = 0;
+                WinQueryWindowProcess(hwndThis,
+                                      &pidWin,
+                                      &tidWin);
+                if (    (pidWin == pid)
+                     && (tidWin == tid)
+                   )
+                {
+                    // get HMQ from window words
+                    rc = WinQueryWindowULong(hwndThis, QWL_HMQ);
+                    if (rc)
+                        if (phab)
+                            *phab = WinQueryAnchorBlock(hwndThis);
+                    break;
+                }
+            }
+        }
+    }
+    WinEndEnumWindows(henum);
+
+    return (rc);
+}
+
+/*
+ *@@ winhFindHardErrorWindow:
+ *      this searches all children of HWND_OBJECT
+ *      for the PM hard error windows, which are
+ *      invisible most of the time. When a hard
+ *      error occurs, that window is made a child
+ *      of HWND_DESKTOP instead.
+ *
+ *      Stolen from ProgramCommander/2 (C) Roman Stangl.
+ *
+ *@@added V0.9.3 (2000-04-27) [umoeller]
+ */
+
+VOID winhFindPMErrorWindows(HWND *phwndHardError,  // out: hard error window
+                            HWND *phwndSysError)   // out: system error window
+{
+    PID     pidObject;  // HWND_OBJECT's process and thread id
+    TID     tidObject;
+    PID     pidObjectChild;     // HWND_OBJECT's child window process and thread id
+    TID     tidObjectChild;
+    HENUM   henumObject;  // HWND_OBJECT enumeration handle
+    HWND    hwndObjectChild;   // Window handle of current HWND_OBJECT child
+    UCHAR   ucClassName[32];  // Window class e.g. #1 for WC_FRAME
+    CLASSINFO classinfoWindow;  // Class info of current HWND_OBJECT child
+
+    *phwndHardError = NULLHANDLE;
+    *phwndSysError = NULLHANDLE;
+
+    // query HWND_OBJECT's window process
+    WinQueryWindowProcess(WinQueryObjectWindow(HWND_DESKTOP), &pidObject, &tidObject);
+    // enumerate all child windows of HWND_OBJECT
+    henumObject = WinBeginEnumWindows(HWND_OBJECT);
+    while ((hwndObjectChild = WinGetNextWindow(henumObject)) != NULLHANDLE)
+    {
+        // see if the current HWND_OBJECT child window runs in the
+        // process of HWND_OBJECT (PM)
+        WinQueryWindowProcess(hwndObjectChild, &pidObjectChild, &tidObjectChild);
+        if (pidObject == pidObjectChild)
+        {
+            // get the child window's data
+            WinQueryClassName(hwndObjectChild,
+                              sizeof(ucClassName),
+                              (PCH)ucClassName);
+            WinQueryClassInfo(WinQueryAnchorBlock(hwndObjectChild),
+                              (PSZ)ucClassName,
+                              &classinfoWindow);
+            if (    (!strcmp((PSZ)ucClassName, "#1")
+                 || (classinfoWindow.flClassStyle & CS_FRAME))
+               )
+            {
+                // if the child window is a frame window and running in
+                // HWND_OBJECT's (PM's) window process, it must be the
+                // PM Hard Error or System Error window
+                WinQueryClassName(WinWindowFromID(hwndObjectChild,
+                                                  FID_CLIENT),
+                                  sizeof(ucClassName),
+                                  (PSZ)ucClassName);
+                if (!strcmp((PSZ)ucClassName, "PM Hard Error"))
+                {
+                    *phwndHardError = hwndObjectChild;
+                    if (*phwndSysError)
+                        // we found the other one already:
+                        // stop searching, we got both
+                        break;
+                }
+                else
+                {
+                    printf("Utility: Found System Error %08X\n", (int)hwndObjectChild);
+                    *phwndSysError = hwndObjectChild;
+                    if (*phwndHardError)
+                        // we found the other one already:
+                        // stop searching, we got both
+                        break;
+                }
+            }
+        } // end if (pidObject == pidObjectChild)
+    } // end while ((hwndObjectChild = WinGetNextWindow(henumObject)) != NULLHANDLE)
+    WinEndEnumWindows(henumObject);
+}
+
+/*
+ *@@ winhCreateFakeDesktop:
+ *      this routine creates and displays a frameless window over
+ *      the whole screen in the color of PM's Desktop to fool the
+ *      user that all windows have been closed (which in fact might
+ *      not be the case).
+ *      This window's background color is set to the Desktop's
+ *      (PM's one, not the WPS's one).
+ *      Returns the HWND of this window.
+ */
+
+HWND winhCreateFakeDesktop(HWND hwndSibling)
+{
+    // presparam for background
+    typedef struct _BACKGROUND
+    {
+        ULONG   cb;     // length of the aparam parameter, in bytes
+        ULONG   id;     // attribute type identity
+        ULONG   cb2;    // byte count of the ab parameter
+        RGB     rgb;    // attribute value
+    } BACKGROUND;
+
+    BACKGROUND  background;
+    LONG        lDesktopColor;
+
+    // create fake desktop window = empty window with
+    // the size of full screen
+    lDesktopColor = WinQuerySysColor(HWND_DESKTOP,
+                                     SYSCLR_BACKGROUND,
+                                     0);
+    background.cb = sizeof(background.id)
+                  + sizeof(background.cb)
+                  + sizeof(background.rgb);
+    background.id = PP_BACKGROUNDCOLOR;
+    background.cb2 = sizeof(RGB);
+    background.rgb.bBlue = (CHAR1FROMMP(lDesktopColor));
+    background.rgb.bGreen= (CHAR2FROMMP(lDesktopColor));
+    background.rgb.bRed  = (CHAR3FROMMP(lDesktopColor));
+
+    return (WinCreateWindow(HWND_DESKTOP,  // parent window
+                            WC_FRAME,      // class name
+                            "",            // window text
+                            WS_VISIBLE,    // window style
+                            0, 0,          // position and size
+                            WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN),
+                            WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN),
+                            NULLHANDLE,    // owner window
+                            hwndSibling,   // sibling window
+                            1,             // window id
+                            NULL,          // control data
+                            &background)); // presentation parms
+}
+
+/*
+ *@@ winhAssertWarp4Notebook:
+ *      this takes hwndDlg as a notebook dialog page and
+ *      goes thru all its controls. If a control with an
+ *      ID <= udIdThreshold is found, this is assumed to
+ *      be a button which is to be given the BS_NOTEBOOKBUTTON
+ *      style. You should therefore give all your button
+ *      controls which should be moved such an ID.
+ *
+ *      You can also specify how many dialog units
+ *      all the other controls will be moved downward in
+ *      ulDownUnits; this is useful to fill up the space
+ *      which was used by the buttons before moving them.
+ *      Returns TRUE if anything was changed.
+ *
+ *      This function is useful if you wish to create
+ *      notebook pages using dlgedit.exe which are compatible
+ *      with both Warp 3 and Warp 4. This should be executed
+ *      in WM_INITDLG of the notebook dlg function if the app
+ *      has determined that it is running on Warp 4.
+ */
+
+BOOL winhAssertWarp4Notebook(HWND hwndDlg,
+                             USHORT usIdThreshold,    // in: ID threshold
+                             ULONG ulDownUnits)       // in: dialog units or 0
+{
+    BOOL brc = FALSE;
+
+    if (doshIsWarp4())
+    {
+        POINTL ptl;
+        HWND hwndItem;
+        HENUM henum = 0;
+
+        BOOL    fIsVisible = WinIsWindowVisible(hwndDlg);
+        if (ulDownUnits)
+        {
+            ptl.x = 0;
+            ptl.y = ulDownUnits;
+            WinMapDlgPoints(hwndDlg, &ptl, 1, TRUE);
+        }
+
+        if (fIsVisible)
+            WinEnableWindowUpdate(hwndDlg, FALSE);
+
+        henum = WinBeginEnumWindows(hwndDlg);
+        while ((hwndItem = WinGetNextWindow(henum)))
+        {
+            USHORT usId = WinQueryWindowUShort(hwndItem, QWS_ID);
+            // _Pmpf(("hwndItem: 0x%lX, ID: 0x%lX", hwndItem, usId));
+            if (usId <= usIdThreshold)
+            {
+                // pushbutton to change:
+                // _Pmpf(("  Setting bit"));
+                WinSetWindowBits(hwndItem,
+                                 QWL_STYLE,
+                                 BS_NOTEBOOKBUTTON, BS_NOTEBOOKBUTTON);
+                brc = TRUE;
+            }
+            else
+                // no pushbutton to change: move downwards
+                // if desired
+                if (ulDownUnits)
+                {
+                    SWP swp;
+                    // _Pmpf(("Moving downwards %d pixels", ptl.y));
+                    WinQueryWindowPos(hwndItem, &swp);
+                    WinSetWindowPos(hwndItem, 0,
+                                    swp.x,
+                                    swp.y - ptl.y,
+                                    0, 0,
+                                    SWP_MOVE);
+                }
+        }
+        WinEndEnumWindows(henum);
+
+        if (fIsVisible)
+            WinShowWindow(hwndDlg, TRUE);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ winhDrawFormattedText:
+ *      this func takes a rectangle and draws pszText into
+ *      it, breaking the words as neccessary. The line spacing
+ *      is determined from the font currently selected in hps.
+ *
+ *      As opposed to WinDrawText, this can draw several lines
+ *      at once, and format the _complete_ text according to the
+ *      flCmd parameter, which is like with WinDrawText.
+ *
+ *      After this function returns, *prcl is modified like this:
+ *      -- yTop and yBottom contain the upper and lower boundaries
+ *         which were needed to draw the text. This depends on
+ *         whether DT_TOP etc. were specified.
+ *         To get the height of the rectangle used, calculate the
+ *         delta between yTop and yBottom.
+ *
+ *      -- xLeft and xRight are modified to contain the outmost
+ *         left and right coordinates which were needed to draw
+ *         the text. This will be set to the longest line which
+ *         was encountered.
+ *
+ *      You can specify DT_QUERYEXTENT with flDraw to only have
+ *      these text boundaries calculated without actually drawing.
+ *
+ *      This returns the number of lines drawn.
+ *
+ *      Note that this calls WinDrawText with DT_TEXTATTRS set,
+ *      that is, the current text primitive attributes will be
+ *      used (fonts and colors).
+ *
+ *@@changed V0.9.0 [umoeller]: prcl.xLeft and xRight are now updated too upon return
+ */
+
+ULONG winhDrawFormattedText(HPS hps,     // in: presentation space; its settings
+                                         // are used, but not altered
+                            PRECTL prcl, // in/out: rectangle to use for drawing
+                                         // (modified)
+                            PSZ pszText, // in: text to draw (zero-terminated)
+                            ULONG flCmd) // in: flags like in WinDrawText; I have
+                                         // only tested DT_TOP and DT_LEFT though.
+                                         // DT_WORDBREAK | DT_TEXTATTRS are always
+                                         // set.
+                                         // You can specify DT_QUERYEXTENT to only
+                                         // have prcl calculated without drawing.
+{
+    PSZ     p = pszText;
+    LONG    lDrawn = 1,
+            lTotalDrawn = 0,
+            lLineCount = 0,
+            lOrigYTop = prcl->yTop;
+    ULONG   ulTextLen = strlen(pszText),
+            ulCharHeight,
+            flCmd2,
+            xLeftmost = prcl->xRight,
+            xRightmost = prcl->xLeft;
+    RECTL   rcl2;
+
+    flCmd2 = flCmd | DT_WORDBREAK | DT_TEXTATTRS;
+
+    ulCharHeight = gpihQueryLineSpacing(hps, pszText);
+
+    while (   (lDrawn)
+           && (lTotalDrawn < ulTextLen)
+          )
+    {
+        memcpy(&rcl2, prcl, sizeof(rcl2));
+        lDrawn = WinDrawText(hps,
+                             ulTextLen-lTotalDrawn,
+                             p,
+                             &rcl2,
+                             0, 0,                       // colors
+                             flCmd2);
+
+        // update char counters
+        p += lDrawn;
+        lTotalDrawn += lDrawn;
+
+        // update x extents
+        if (rcl2.xLeft < xLeftmost)
+            xLeftmost = rcl2.xLeft;
+        if (rcl2.xRight > xRightmost)
+            xRightmost = rcl2.xRight;
+
+        // update y for next line
+        prcl->yTop -= ulCharHeight;
+
+        // increase line count
+        lLineCount++;
+    }
+    prcl->xLeft = xLeftmost;
+    prcl->xRight = xRightmost;
+    prcl->yBottom = prcl->yTop;
+    prcl->yTop = lOrigYTop;
+
+    return (lLineCount);
+}
+
+/*
+ *@@ winhKillTasklist:
+ *      this will destroy the Tasklist (window list) window.
+ *      Note: you will only be able to get it back after a
+ *      reboot, not a WPS restart. Only for use at shutdown and such.
+ *      This trick by Uri J. Stern at
+ *      http://zebra.asta.fh-weingarten.de/os2/Snippets/Howt8881.HTML
+ */
+
+VOID winhKillTasklist(VOID)
+{
+    SWBLOCK  swblock;
+    HWND     hwndTasklist;
+    // the tasklist has entry #0 in the SWBLOCK
+    WinQuerySwitchList(NULLHANDLE, &swblock, sizeof(SWBLOCK));
+    hwndTasklist = swblock.aswentry[0].swctl.hwnd;
+    WinPostMsg(hwndTasklist,
+               0x0454,     // undocumented msg for killing tasklist
+               NULL, NULL);
+}
+
+// the following must be added for EMX (99-10-22) [umoeller]
+#ifndef NERR_BufTooSmall
+      #define NERR_BASE       2100
+      #define NERR_BufTooSmall        (NERR_BASE+23)
+            // the API return buffer is too small
+#endif
+
+/*
+ *@@ winhQueryPendingSpoolJobs:
+ *      returns the number of pending print jobs in the spooler
+ *      or 0 if none. Useful for testing before shutdown.
+ */
+
+ULONG winhQueryPendingSpoolJobs(VOID)
+{
+    // BOOL    rcPending = FALSE;
+    ULONG       ulTotalJobCount = 0;
+
+    SPLERR      splerr;
+    USHORT      jobCount;
+    ULONG       cbBuf;
+    ULONG       cTotal;
+    ULONG       cReturned;
+    ULONG       cbNeeded;
+    ULONG       ulLevel;
+    ULONG       i,j;
+    PSZ         pszComputerName;
+    PBYTE       pBuf = NULL;
+    PPRQINFO3   prq;
+    PPRJINFO2   prj2;
+
+    ulLevel = 4L;
+    pszComputerName = (PSZ)NULL;
+    splerr = SplEnumQueue(pszComputerName, ulLevel, pBuf, 0L, // cbBuf
+                          &cReturned, &cTotal,
+                          &cbNeeded, NULL);
+    if (    (splerr == ERROR_MORE_DATA)
+         || (splerr == NERR_BufTooSmall)
+       )
+    {
+        if (!DosAllocMem((PPVOID)&pBuf,
+                         cbNeeded,
+                         PAG_READ | PAG_WRITE | PAG_COMMIT))
+        {
+            cbBuf = cbNeeded;
+            splerr = SplEnumQueue(pszComputerName, ulLevel, pBuf, cbBuf,
+                                  &cReturned, &cTotal,
+                                  &cbNeeded, NULL);
+            if (splerr == NO_ERROR)
+            {
+                // set pointer to point to the beginning of the buffer
+                prq = (PPRQINFO3)pBuf;
+
+                // cReturned has the count of the number of PRQINFO3 structures
+                for (i = 0;
+                     i < cReturned;
+                     i++)
+                {
+                    // save the count of jobs; there are this many PRJINFO2
+                    // structures following the PRQINFO3 structure
+                    jobCount = prq->cJobs;
+                    // _Pmpf(( "Job count in this queue is %d",jobCount ));
+
+                    // increment the pointer past the PRQINFO3 structure
+                    prq++;
+
+                    // set a pointer to point to the first PRJINFO2 structure
+                    prj2=(PPRJINFO2)prq;
+                    for (j = 0;
+                         j < jobCount;
+                         j++)
+                    {
+                        // increment the pointer to point to the next structure
+                        prj2++;
+                        // increase the job count, which we'll return
+                        ulTotalJobCount++;
+
+                    } // endfor jobCount
+
+                    // after doing all the job structures, prj2 points to the next
+                    // queue structure; set the pointer for a PRQINFO3 structure
+                    prq = (PPRQINFO3)prj2;
+                } //endfor cReturned
+            } // endif NO_ERROR
+            DosFreeMem(pBuf);
+        }
+    } // end if Q level given
+
+    return (ulTotalJobCount);
+}
+
+/*
+ *@@ winhSetNumLock:
+ *      this sets the NumLock key on or off, depending
+ *      on fState.
+ *
+ *      Based on code from WarpEnhancer, (C) Achim Hasenmller.
+ *
+ *@@added V0.9.1 (99-12-18) [umoeller]
+ */
+
+VOID winhSetNumLock(BOOL fState)
+{
+    // BOOL  fRestoreKBD = FALSE;  //  Assume we're not going to close Kbd
+    BYTE KeyStateTable[256];
+    ULONG ulActionTaken; //  Used by DosOpen
+    HFILE hKbd;
+
+    // read keyboard state table
+    if (WinSetKeyboardStateTable(HWND_DESKTOP, &KeyStateTable[0],
+                                 FALSE))
+    {
+        // first set the PM state
+        if (fState)
+            KeyStateTable[VK_NUMLOCK] |= 0x01; //  Turn numlock on
+        else
+            KeyStateTable[VK_NUMLOCK] &= 0xFE; //  Turn numlock off
+
+        // set keyboard state table with new state values
+        WinSetKeyboardStateTable(HWND_DESKTOP, &KeyStateTable[0], TRUE);
+    }
+
+    // now set the OS/2 keyboard state
+
+    // try to open OS/2 keyboard driver
+    if (!DosOpen("KBD$",
+                 &hKbd, &ulActionTaken,
+                 0,     // cbFile
+                 FILE_NORMAL,
+                 OPEN_ACTION_FAIL_IF_NEW | OPEN_ACTION_OPEN_IF_EXISTS,
+                 OPEN_SHARE_DENYNONE | OPEN_ACCESS_READWRITE,
+                 NULL))
+    {
+        SHIFTSTATE ShiftState;
+        ULONG DataLen = sizeof(SHIFTSTATE);
+
+        memset(&ShiftState, '\0', DataLen);
+        DosDevIOCtl(hKbd, IOCTL_KEYBOARD, KBD_GETSHIFTSTATE,
+                    NULL, 0L, NULL,
+                    &ShiftState, DataLen, &DataLen);
+
+        if (fState)
+            ShiftState.fsState |= 0x0020; // turn NumLock on
+        else
+            ShiftState.fsState &= 0xFFDF; // turn NumLock off
+
+        DosDevIOCtl(hKbd, IOCTL_KEYBOARD, KBD_SETSHIFTSTATE,
+                    &ShiftState, DataLen, &DataLen,
+                    NULL, 0L, NULL);
+        // now close OS/2 keyboard driver
+        DosClose(hKbd);
+    }
+    return;
+}
+
+/*
+ *@@category: Helpers\PM helpers\Workplace Shell\WPS class list
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   WPS Class List helpers                                         *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ winhQueryWPSClassList:
+ *      this returns the WPS class list in a newly
+ *      allocated buffer. This is just a shortcut to
+ *      the usual double WinEnumObjectClasses call.
+ *
+ *      The return value is actually of the POBJCLASS type,
+ *      so you better cast this manually. We declare this
+ *      this as PBYTE though because POBJCLASS requires
+ *      INCL_WINWORKPLACE.
+ *      See WinEnumObjectClasses() for details.
+ *
+ *      The buffer is allocated using malloc(), so
+ *      you should free() it when it is no longer
+ *      needed.
+ *
+ *      This returns NULL if an error occured.
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PBYTE winhQueryWPSClassList(VOID)
+{
+    ULONG       ulSize;
+    POBJCLASS   pObjClass = 0;
+
+    // get WPS class list size
+    if (WinEnumObjectClasses(NULL, &ulSize))
+    {
+        // allocate buffer
+        pObjClass = (POBJCLASS)malloc(ulSize+1);
+        // and load the classes into it
+        WinEnumObjectClasses(pObjClass, &ulSize);
+    }
+
+    return ((PBYTE)pObjClass);
+}
+
+/*
+ *@@ winhQueryWPSClass:
+ *      this returns the POBJCLASS item if pszClass is registered
+ *      with the WPS or NULL if the class could not be found.
+ *
+ *      The return value is actually of the POBJCLASS type,
+ *      so you better cast this manually. We declare this
+ *      this as PBYTE though because POBJCLASS requires
+ *      INCL_WINWORKPLACE.
+ *
+ *      This takes as input the return value of winhQueryWPSClassList,
+ *      which you must call first.
+ *
+ *      <B>Usage:</B>
+ +          PBYTE   pClassList = winhQueryWPSClassList(),
+ +                  pWPFolder;
+ +          if (pClassList)
+ +          {
+ +              if (pWPFolder = winhQueryWPSClass(pClassList, "WPFolder"))
+ +                  ...
+ +              free(pClassList);
+ +          }
+ *
+ *@@added V0.9.0 [umoeller]
+ */
+
+PBYTE winhQueryWPSClass(PBYTE pObjClass,  // in: buffer returned by
+                                          // winhQueryWPSClassList
+                        const char *pszClass)     // in: class name to query
+{
+    PBYTE   pbReturn = 0;
+
+    POBJCLASS pocThis = (POBJCLASS)pObjClass;
+    // now go thru the WPS class list
+    while (pocThis)
+    {
+        if (strcmp(pocThis->pszClassName, pszClass) == 0)
+        {
+            pbReturn = (PBYTE)pocThis;
+            break;
+        }
+        // next class
+        pocThis = pocThis->pNext;
+    } // end while (pocThis)
+
+    return (pbReturn);
+}
+
+/*
+ *@@ winhRegisterClass:
+ *      this works just like WinRegisterObjectClass,
+ *      except that it returns a more meaningful
+ *      error code than just FALSE in case registering
+ *      fails.
+ *
+ *      This returns NO_ERROR if the class was successfully
+ *      registered (WinRegisterObjectClass returned TRUE).
+ *
+ *      Otherwise, we do a DosLoadModule if maybe the DLL
+ *      couldn't be loaded in the first place. If DosLoadModule
+ *      did not return NO_ERROR, this function returns that
+ *      return code, which can be:
+ *
+ *      --  2   ERROR_FILE_NOT_FOUND: pcszModule does not exist
+ *      --  2   ERROR_FILE_NOT_FOUND
+ *      --  3   ERROR_PATH_NOT_FOUND
+ *      --  4   ERROR_TOO_MANY_OPEN_FILES
+ *      --  5   ERROR_ACCESS_DENIED
+ *      --  8   ERROR_NOT_ENOUGH_MEMORY
+ *      --  11  ERROR_BAD_FORMAT
+ *      --  26  ERROR_NOT_DOS_DISK (unknown media type)
+ *      --  32  ERROR_SHARING_VIOLATION
+ *      --  33  ERROR_LOCK_VIOLATION
+ *      --  36  ERROR_SHARING_BUFFER_EXCEEDED
+ *      --  95  ERROR_INTERRUPT (interrupted system call)
+ *      --  108 ERROR_DRIVE_LOCKED (by another process)
+ *      --  123 ERROR_INVALID_NAME (illegal character or FS name not valid)
+ *      --  127 ERROR_PROC_NOT_FOUND (DosQueryProcAddr error)
+ *      --  180 ERROR_INVALID_SEGMENT_NUMBER
+ *      --  182 ERROR_INVALID_ORDINAL
+ *      --  190 ERROR_INVALID_MODULETYPE (probably an application)
+ *      --  191 ERROR_INVALID_EXE_SIGNATURE (probably not LX DLL)
+ *      --  192 ERROR_EXE_MARKED_INVALID (by linker)
+ *      --  194 ERROR_ITERATED_DATA_EXCEEDS_64K (in a DLL segment)
+ *      --  195 ERROR_INVALID_MINALLOCSIZE
+ *      --  196 ERROR_DYNLINK_FROM_INVALID_RING
+ *      --  198 ERROR_INVALID_SEGDPL
+ *      --  199 ERROR_AUTODATASEG_EXCEEDS_64K
+ *      --  201 ERROR_RELOCSRC_CHAIN_EXCEEDS_SEGLIMIT
+ *      --  206 ERROR_FILENAME_EXCED_RANGE (not matching 8+3 spec)
+ *      --  295 ERROR_INIT_ROUTINE_FAILED (DLL init routine failed)
+ *
+ *      In all these cases, pszBuf may contain a meaningful
+ *      error message from DosLoadModule, especially if an import
+ *      could not be resolved.
+ *
+ *      Still worse, if DosLoadModule returned NO_ERROR, we
+ *      probably have some SOM internal error. A probable
+ *      reason is that the parent class of pcszClassName
+ *      is not installed, but that's WPS/SOM internal
+ *      and cannot be queried from outside the WPS context.
+ *
+ *      In that case, ERROR_OPEN_FAILED (110) is returned.
+ *      That one sounded good to me. ;-)
+ */
+
+APIRET winhRegisterClass(const char* pcszClassName, // in: e.g. "XFolder"
+                         const char* pcszModule,    // in: e.g. "C:\XFOLDER\XFLDR.DLL"
+                         PSZ pszBuf,                // out: error message from DosLoadModule
+                         ULONG cbBuf)               // in: sizeof(*pszBuf), passed to DosLoadModule
+{
+    APIRET arc = NO_ERROR;
+
+    if (!WinRegisterObjectClass((PSZ)pcszClassName, (PSZ)pcszModule))
+    {
+        // failed: do more error checking then, try DosLoadModule
+        HMODULE hmod = NULLHANDLE;
+        arc = DosLoadModule(pszBuf, cbBuf,
+                            (PSZ)pcszModule,
+                            &hmod);
+        if (arc == NO_ERROR)
+        {
+            // DosLoadModule succeeded:
+            // some SOM error then
+            DosFreeModule(hmod);
+            arc = ERROR_OPEN_FAILED;
+        }
+    }
+    // else: ulrc still 0 (== no error)
+
+    return (arc);
+}
+
+/*
+ *@@ winhIsClassRegistered:
+ *      quick one-shot function which checks if
+ *      a class is currently registered. Calls
+ *      winhQueryWPSClassList and winhQueryWPSClass
+ *      in turn.
+ *
+ *@@added V0.9.2 (2000-02-26) [umoeller]
+ */
+
+BOOL winhIsClassRegistered(const char *pcszClass)
+{
+    BOOL    brc = FALSE;
+    PBYTE   pClassList = winhQueryWPSClassList();
+    if (pClassList)
+    {
+        if (winhQueryWPSClass(pClassList, pcszClass))
+            brc = TRUE;
+        free(pClassList);
+    }
+
+    return (brc);
+}
+
+/*
+ *@@category: Helpers\PM helpers\Workplace Shell
+ */
+
+/*
+ *@@ winhResetWPS:
+ *      restarts the WPS using PrfReset. Returns
+ *      one of the following:
+ *
+ *      -- 0: no error.
+ *      -- 1: PrfReset failed.
+ *      -- 2 or 4: PrfQueryProfile failed.
+ *      -- 3: malloc() failed.
+ *
+ *@@added V0.9.4 (2000-07-01) [umoeller]
+ */
+
+ULONG winhResetWPS(HAB hab)
+{
+    ULONG ulrc = 0;
+    // find out current profile names
+    PRFPROFILE Profiles;
+    Profiles.cchUserName = Profiles.cchSysName = 0;
+    // first query their file name lengths
+    if (PrfQueryProfile(hab, &Profiles))
+    {
+        // allocate memory for filenames
+        Profiles.pszUserName  = (PSZ)malloc(Profiles.cchUserName);
+        Profiles.pszSysName  = (PSZ)malloc(Profiles.cchSysName);
+
+        if (Profiles.pszSysName)
+        {
+            // get filenames
+            if (PrfQueryProfile(hab, &Profiles))
+            {
+
+                // "change" INIs to these filenames:
+                // THIS WILL RESET THE WPS
+                if (PrfReset(hab, &Profiles) == FALSE)
+                    ulrc = 1;
+                free(Profiles.pszSysName);
+                free(Profiles.pszUserName);
+            }
+            else
+                ulrc = 2;
+        }
+        else
+            ulrc = 3;
+    }
+    else
+        ulrc = 4;
+
+    return (ulrc);
+}
Index: /trunk/src/helpers/wphandle.c
===================================================================
--- /trunk/src/helpers/wphandle.c	(revision 8)
+++ /trunk/src/helpers/wphandle.c	(revision 8)
@@ -0,0 +1,780 @@
+
+/*
+ *@@sourcefile wphandle.c:
+ *      this file contains the logic for dealing with
+ *      those annoying WPS object handles in OS2SYS.INI.
+ *      This does not use WPS interfaces, but parses
+ *      the profiles directly.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes (new with V0.81):
+ *      --  wph*   WPS object helper functions
+ *
+ *      This code is mostly written by Henk Kelder and published
+ *      with his kind permission.
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\wphandle.h"
+ */
+
+/*
+ *      This file Copyright (C) 1997-2000 Ulrich Mller,
+ *                                        Henk Kelder.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_WINSHELLDATA
+#include <os2.h>
+
+#define OPTIONS_SIZE 32767
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <io.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\wphandle.h"
+
+/****************************************************
+ *                                                  *
+ *  helper functions                                *
+ *                                                  *
+ ****************************************************/
+
+static USHORT wphSearchBufferForHandle(PBYTE pHandlesBuffer, ULONG ulBufSize, USHORT usParent, PSZ pszFname);
+PNODE wphFindPartName(PBYTE pHandlesBuffer, ULONG ulBufSize, USHORT usID, PSZ pszFname, USHORT usMax);
+
+#define MakeDiskHandle(usObjID) (usObjID | 0x30000)
+
+#define IsObjectDisk(hObject) ((hObject & 0x30000) == 0x30000)
+
+/*
+ *@@category: Helpers\PM helpers\Workplace Shell\Handles (OS2SYS.INI)
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Helper functions                                               *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ wphQueryProfileData:
+ *      like PrfQueryProfileData, but allocates sufficient
+ *      memory and returns a pointer to that buffer.
+ *      pulSize must point to a ULONG which will then
+ *      contain the no. of copied bytes.
+ */
+
+PBYTE wphQueryProfileData(HINI hIniSystem,    // in: can be HINI_USER or HINI_SYSTEM
+                          PSZ pApp, PSZ pKey, // in: load what?
+                          PULONG pulSize)     // out: bytes loaded
+{
+    PBYTE pData = NULL;
+    if (PrfQueryProfileSize(hIniSystem, pApp, pKey, pulSize))
+    {
+        pData = (PBYTE)malloc(*pulSize);
+        PrfQueryProfileData(hIniSystem, pApp, pKey, pData, pulSize);
+    }
+    // _Pmpf(("  wphQueryProfileData(%lX, %s, %s, %d)", hIniSystem, pApp, pKey, *pulSize));
+    return (pData);
+}
+
+/*
+ *@@ wphEnumProfileKeys:
+ *      allocates memory for a buffer and copies the keys
+ *      for pApp into it.
+ *      Returns the pointer to the buffer.
+ *      pulKeysSize must point to a ULONG which will then
+ *      contain the size of the returned buffer.
+ */
+
+PBYTE wphEnumProfileKeys(HINI hIniSystem,       // in: can be HINI_USER or HINI_SYSTEM
+                         PSZ pApp,              // in: app to query
+                         PULONG pulKeysSize)    // out: sizeof(return buffer)
+{
+    PBYTE pszKeys = NULL;
+    if (PrfQueryProfileSize(hIniSystem, pApp, NULL, pulKeysSize))
+    {
+        pszKeys = (PBYTE)malloc(*pulKeysSize);
+        if (pszKeys)
+            PrfQueryProfileData(hIniSystem, pApp, NULL, pszKeys, pulKeysSize);
+    }
+    return (pszKeys);
+}
+
+/*
+ * wphResetBlockBuffer:
+ *      Reset the block buffer, make sure the buffer is re-read.
+ */
+
+/* VOID wphResetBlockBuffer(VOID)
+{
+   if (pHandlesBuffer)
+   {
+       free(pHandlesBuffer);
+       pHandlesBuffer = NULL;
+   }
+} */
+
+/*
+ *@@ wphQueryActiveHandles:
+ *      this copies the contents of PM_Workplace:ActiveHandles
+ *      in OS2SYS.INI into a given buffer. There are always two
+ *      buffers in OS2SYS.INI for object handles, called
+ *      "PM_Workplace:HandlesX" with "X" either being "0" or "1".
+ *      It seems that every time the WPS does something in the
+ *      handles section, it writes the data to the inactive
+ *      buffer first and then makes it the active buffer by
+ *      changing the "active handles" key. You can test this
+ *      by creating a shadow on your Desktop.
+ *
+ *      This function copies the key only, but not the actual
+ *      handles blocks (use wphReadAllBlocks for that).
+ *
+ *      This gets called by the one-shot function
+ *      wphQueryHandleFromPath.
+ */
+
+BOOL wphQueryActiveHandles(HINI hIniSystem,       // in: can be HINI_USER or HINI_SYSTEM
+                           PSZ pszHandlesAppName, // out: active handles buffer.
+                           USHORT usMax)          // in:  sizeof(pszHandlesAppName)
+{
+    PBYTE pszHandles;
+    ULONG ulProfileSize;
+
+    pszHandles = wphQueryProfileData(hIniSystem,
+                                     ACTIVEHANDLES, HANDLESAPP,
+                                     &ulProfileSize);
+    if (!pszHandles)
+    {
+        strncpy(pszHandlesAppName, HANDLES, usMax-1);
+        return TRUE;
+    }
+    // fNewFormat = TRUE;
+    strncpy(pszHandlesAppName, pszHandles, usMax-1);
+    free(pszHandles);
+    return TRUE;
+}
+
+/*
+ *@@ wphReadAllBlocks:
+ *      this reads all the BLOCK's in the active handles
+ *      section in OS2SYS.INI into one common buffer, for
+ *      which sufficient memory is allocated.
+ *
+ *      Each of these blocks is a maximum of 64 KB, hell
+ *      knows why, maybe the INIs can't handle more. This
+ *      func combines all the BLOCK's into one buffer anyways,
+ *      which can then be searched.
+ *
+ *      Use wphQueryActiveHandles to pass the active handles
+ *      section to this function.
+ *
+ *      This gets called by the one-shot function
+ *      wphQueryHandleFromPath.
+ */
+
+BOOL wphReadAllBlocks(HINI hiniSystem,       // in: can be HINI_USER or HINI_SYSTEM
+                      PSZ pszActiveHandles,  // in: active handles section
+                      PBYTE* ppBlock,        // in/out: pointer to buffer, which
+                                             //    will point to the allocated
+                                             //    memory afterwards
+                      PULONG pulSize)        // out: size of the allocated buffer
+{
+    PBYTE   pbAllBlocks,
+            pszBlockName,
+            p;
+    ULONG   ulBlockSize;
+    ULONG   ulTotalSize;
+    BYTE    rgfBlock[100];
+    BYTE    szAppName[10];
+    USHORT  usBlockNo;
+
+    pbAllBlocks = wphEnumProfileKeys(hiniSystem, pszActiveHandles, &ulBlockSize);
+    if (!pbAllBlocks)
+       return FALSE;
+
+    // query size of all individual blocks and calculate total
+    memset(rgfBlock, 0, sizeof rgfBlock);
+    ulTotalSize = 0L;
+    pszBlockName = pbAllBlocks;
+    while (*pszBlockName)
+    {
+        if (!memicmp(pszBlockName, "BLOCK", 5))
+        {
+           usBlockNo = atoi(pszBlockName + 5);
+           if (usBlockNo < 100)
+           {
+               rgfBlock[usBlockNo] = TRUE;
+
+               if (!PrfQueryProfileSize(hiniSystem,
+                                        pszActiveHandles,
+                                        pszBlockName,
+                                        &ulBlockSize))
+               {
+                   free(pbAllBlocks);
+                   return FALSE;
+               }
+               ulTotalSize += ulBlockSize;
+           }
+        }
+        pszBlockName += strlen(pszBlockName) + 1;
+    }
+
+    // *pulSize now contains the total size of all blocks
+    *ppBlock = (PBYTE)malloc(ulTotalSize);
+    if (!(*ppBlock))
+    {
+       /* MessageBox("wphReadAllBlocks", "Not enough memory for profile data!"); */
+         free(pbAllBlocks);
+         return FALSE;
+    }
+
+    // now get all blocks into the memory we allocated
+    p = *ppBlock;
+    (*pulSize) = 0;
+    for (usBlockNo = 1; usBlockNo < 100; usBlockNo++)
+    {
+        if (!rgfBlock[usBlockNo])
+            break;
+
+        sprintf(szAppName, "BLOCK%u", (UINT)usBlockNo);
+        ulBlockSize = ulTotalSize;
+        if (!PrfQueryProfileData(hiniSystem,
+                                 pszActiveHandles,
+                                 szAppName,
+                                 p,
+                                 &ulBlockSize))
+        {
+            free(pbAllBlocks);
+            free(*ppBlock);
+            return FALSE;
+        }
+        p += ulBlockSize;
+        (*pulSize) += ulBlockSize;
+        ulTotalSize -= ulBlockSize;
+    }
+
+    free(pbAllBlocks);
+    return TRUE;
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Get HOBJECT from filename                                      *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ wphSearchBufferForHandle:
+ *      returns the four-digit object handle which corresponds
+ *      to pszFname, searching pHandlesBuffer. Note that you
+ *      must OR the return value with 0x30000 to make this
+ *      a valid WPS file-system handle.
+ *
+ *      You must pass a handles buffer to this function which
+ *      has been filled using wphReadAllBlocks above.
+ *
+ *      This gets called by the one-shot function
+ *      wphQueryHandleFromPath.
+ */
+
+USHORT wphSearchBufferForHandle(PBYTE pHandlesBuffer, // in: handles buffer (all BLOCK's)
+                                ULONG ulBufSize,      // in: sizeof(pHandlesBuffer)
+                                USHORT usParent,      // in: parent NODE ID;
+                                                      //     must be 0 initially
+                                PSZ pszFname) // in: fully qlf'd filename to search for
+{
+    PDRIV pDriv;
+    PNODE pNode;
+    PBYTE pCur;                 // current
+    PBYTE p,
+          pEnd;                 // *end of buffer
+    USHORT usPartSize;
+
+    // _Pmpf(("Entering wphSearchBufferForHandle for %s", pszFname));
+
+    // The composed BLOCKs in the handles buffer make up a tree of
+    // DRIVE and NODE structures (see wphandle.h). Each NODE stands
+    // for either a directory or a file. (We don't care about the
+    // DRIVE structures because the root directory gets a NODE also.)
+    // Each NODE contains the non-qualified file name, an object ID,
+    // and the object ID of its parent NODE.
+    // We can thus work our way through the buffer by splitting the
+    // fully qualified filename that we're searching for into the
+    // different directory names and, each time, searching for the
+    // corresponding NODE. If we have found that, we go for the next.
+    // Example for C:\OS2\E.EXE:
+    //   1) first search for the "C:" NODE
+    //   2) then find the "OS2" node which has "C" as its parent NODE
+    //      (we do this by comparing the parent object handles)
+    //   3) then find the "E.EXE" NODE the same way
+    // The "E.EXE" NODE then has the object handle we're looking for.
+
+    // So first find the length of the first filename part (which
+    // should be 2 for the drive letter, "C:")
+    p = strchr(pszFname, '\\');
+    if (p)
+        // backslash found:
+        usPartSize = p - pszFname;      // extract first part
+    else
+        usPartSize = strlen(pszFname);
+
+    // now set the pointer for the end of the BLOCKs buffer
+    pEnd = pHandlesBuffer + ulBufSize;
+
+    // pCur is our variable pointer where we're at now; there
+    // is some offset of 4 bytes at the beginning (duh)
+    pCur = pHandlesBuffer + 4;
+
+    // _Pmpf(("  Searching for: %s, usPartSize: %d", pszFname, usPartSize));
+
+    // go!
+    while (pCur < pEnd)
+    {
+        // the first four chars tell us whether it's
+        // a DRIVE or a NODE structure
+        if (!memicmp(pCur, "DRIV", 4))
+        {
+            // pCur points to a DRIVE node:
+            // we don't care about these, because the root
+            // directory has a real NODE too, so we just
+            // skip this
+            pDriv = (PDRIV)pCur;
+            pCur += sizeof(DRIV) + strlen(pDriv->szName);
+        }
+        else if (!memicmp(pCur, "NODE", 4))
+        {
+            // pCur points to a regular NODE: offset pointer first
+            pNode = (PNODE)pCur;
+            pCur += sizeof (NODE) + pNode->usNameSize;
+
+            // does the NODE have the same parent that we
+            // are currently searching for? This is "0"
+            // for root directories (and initially for us
+            // too)
+            if (usParent == pNode->usParentHandle)
+            {
+                // yes:
+                // _Pmpf(("  found matching parents (%lX): %s", usParent, pNode->szName));
+
+                // does the NODE have the same partname length?
+                if (pNode->usNameSize == usPartSize)
+                {
+                    // yes:
+                    // _Pmpf(("    found matching partnames sizes: %d", pNode->usNameSize));
+
+                    // do the partnames match too?
+                    if (memicmp(pszFname, pNode->szName, usPartSize) == 0)
+                    {
+                        // OK!! proper NODE found!
+                        // _Pmpf(("      FOUND %s!!", pNode->szName));
+
+                        // now check if this was the last NODE
+                        // we were looking for
+                        if (strlen(pszFname) == usPartSize)
+                           // yes: return ID
+                           return (pNode->usHandle);
+
+                        // else: update our status;
+                        // get next partname
+                        pszFname += usPartSize + 1;
+                        // calc next partname length
+                        p = strchr(pszFname, '\\');
+                        if (p)
+                           usPartSize = p - pszFname;
+                        else
+                           usPartSize = strlen(pszFname);
+
+                        // get next parent to search for
+                        // (which is the current handle)
+                        usParent = pNode->usHandle;
+                    }
+                }
+            }
+        }
+        else
+            // neither DRIVE nor NODE: error
+            return (0);
+
+    } // end while
+
+    // not found: end of buffer reached
+    return (0);
+}
+
+/*
+ *@@ wphQueryHandleFromPath:
+ *      find the object handle for pszName; this
+ *      can either be an object ID ("<WP_DESKTOP>")
+ *      or a fully qualified filename.
+ *      This is a one-shot function, using wphQueryActiveHandles,
+ *      wphReadAllBlocks, and wphSearchBufferForHandle.
+ *
+ *      Returns:
+ *      --  -1:     file does not exist
+ *      --  -2:     error querying handles buffer
+ *      --  0:      object handle not found; might not exist
+ *      --  other:  found object handle
+ *
+ *      NOTE: This function uses C runtime library
+ *      string comparison functions. These only work
+ *      properly if you have set the locale for the
+ *      C runtime properly. This is, for example, a
+ *      problem with file names containing German umlauts,
+ *      which are not found properly.
+ *      You should put some
+ +          setlocale(LC_ALL, "");
+ *      statement somewhere, which reacts to the LANG
+ *      variable which OS/2 puts into CONFIG.SYS per
+ *      default.
+ */
+
+HOBJECT wphQueryHandleFromPath(HINI hIniUser,   // in: user ini file
+                               HINI hIniSystem, // in: system ini file
+                               PSZ pszName)     // in: fully qlf'd filename
+{
+    PBYTE       pHandlesBuffer = NULL;
+    ULONG       cbHandlesBuffer = 0,
+                cbLocation = 0;
+    USHORT      usObjID;
+    PVOID       pvData;
+    HOBJECT     hObject = 0L;
+    BYTE        szFullPath[300];
+    CHAR        szActiveHandles[100];
+
+    // _Pmpf(("wphQueryHandleFromPath: %s", pszName));
+
+    // try to get the objectID via PM_Workplace:Location, since
+    // pszName might also be a "<WP_DESKTOP>" like thingy
+    pvData = wphQueryProfileData(hIniUser,
+                                 LOCATION,   // "PM_Workplace:Location" (<WP_DESKTOP> etc.)
+                                 pszName,
+                                 &cbLocation);
+    if (pvData)
+    {
+        // found there:
+        if (cbLocation >= sizeof(HOBJECT))
+            hObject = *(PULONG)pvData;
+        free(pvData);
+        // _Pmpf(("  object ID found, hObject: %lX", hObject));
+    }
+    if (hObject)
+        return (hObject);
+
+    // not found there: is pszName an existing pathname?
+    if (access(pszName, 0))  // check existence
+    {
+        // == -1: file does not exist
+        // _Pmpf(("  path not found, returning -1"));
+        return (-1);
+    }
+
+    // else: make full path for pszName
+    _fullpath(szFullPath, pszName, sizeof(szFullPath));
+
+    // check if the HandlesBlock is valid
+    wphQueryActiveHandles(hIniSystem, szActiveHandles, sizeof(szActiveHandles));
+    // _Pmpf(("  szActiveHandles: %s", szActiveHandles));
+
+    // now load all the BLOCKs into a common buffer
+    if (!wphReadAllBlocks(hIniSystem,
+                           szActiveHandles,
+                           &pHandlesBuffer, &cbHandlesBuffer))
+        // error:
+        return (-2);
+
+    // and search that buffer
+    usObjID = wphSearchBufferForHandle(pHandlesBuffer,
+                                       cbHandlesBuffer,
+                                       0,                  // usParent
+                                       szFullPath);
+
+    if (usObjID)
+        // found: OR 0x30000
+        hObject = MakeDiskHandle(usObjID);
+
+    free(pHandlesBuffer);
+
+    return (hObject);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Get filename from HOBJECT                                      *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ wphFindPartName:
+ *      this searches pHandlesBuffer for usHandle and, if found,
+ *      appends the object partname to pszFname.
+ *      This function recurses, if neccessary.
+ *
+ *      This gets called by the one-shot function
+ *      wphQueryPathFromHandle.
+ */
+
+PNODE wphFindPartName(PBYTE pHandlesBuffer, // in: handles buffer
+                      ULONG ulBufSize,      // in: buffer size
+                      USHORT usHandle,      // in: handle to search for
+                      PSZ pszFname,         // out: object partname
+                      USHORT usMax)         // in: sizeof(pszFname)
+{
+    PDRIV pDriv;
+    PNODE pNode;
+    PBYTE p, pEnd;
+    USHORT usSize;
+
+    pEnd = pHandlesBuffer + ulBufSize;
+    p = pHandlesBuffer + 4;
+    while (p < pEnd)
+    {
+        if (!memicmp(p, "DRIV", 4))
+        {
+            pDriv = (PDRIV)p;
+            p += sizeof(DRIV) + strlen(pDriv->szName);
+        }
+        else if (!memicmp(p, "NODE", 4))
+        {
+            pNode = (PNODE)p;
+            p += sizeof (NODE) + pNode->usNameSize;
+            if (pNode->usHandle == usHandle)
+            {
+                usSize = usMax - strlen(pszFname);
+                if (usSize > pNode->usNameSize)
+                    usSize = pNode->usNameSize;
+                if (pNode->usParentHandle)
+                {
+                    if (!wphFindPartName(pHandlesBuffer, ulBufSize,
+                                         pNode->usParentHandle,
+                                         pszFname,
+                                         usMax))
+                       return (NULL);
+                    strcat(pszFname, "\\");
+                    strncat(pszFname, pNode->szName, usSize);
+                    return pNode;
+                }
+                else
+                {
+                    strncpy(pszFname, pNode->szName, usSize);
+                    return pNode;
+                }
+            }
+        }
+        else
+           return (NULL);
+    }
+    return (NULL);
+}
+
+/*
+ *@@ wphQueryPathFromHandle:
+ *      reverse to wphQueryHandleFromPath, this gets the
+ *      filename for hObject.
+ *      This is a one-shot function, using wphQueryActiveHandles,
+ *      wphReadAllBlocks, and wphFindPartName.
+ *
+ *@@changed V0.9.4 (2000-08-03) [umoeller]: now returning BOOL
+ */
+
+BOOL wphQueryPathFromHandle(HINI hIniSystem,    // in: HINI_SYSTEM or other INI handle
+                            HOBJECT hObject,    // in: five-digit object handle
+                            PSZ pszFname,       // out: filename, if found
+                            USHORT usMax)       // in: sizeof(*pszFname)
+{
+    BOOL        brc = FALSE;
+
+    if (IsObjectDisk(hObject))
+    {
+        // use lower byte only
+        USHORT      usObjID = LOUSHORT(hObject);
+
+        PBYTE       pHandlesBuffer = NULL;
+        ULONG       cbHandlesBuffer = 0;
+
+        CHAR        szActiveHandles[100];
+        // PNODE       pReturnNode = 0;
+
+        wphQueryActiveHandles(hIniSystem, szActiveHandles, sizeof(szActiveHandles));
+
+        if (wphReadAllBlocks(hIniSystem,
+                             szActiveHandles,
+                             &pHandlesBuffer,
+                             &cbHandlesBuffer))
+        {
+            memset(pszFname, 0, usMax);
+            if (wphFindPartName(pHandlesBuffer,
+                                cbHandlesBuffer,
+                                usObjID,
+                                pszFname,
+                                usMax))
+                brc = TRUE;
+
+            free(pHandlesBuffer);
+        }
+    }
+
+    return (brc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Manipulation functions                                         *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ WriteAllBlocks:
+ *      writes all blocks back to OS2SYS.INI.
+ *
+ *@@added V0.9.5 (2000-08-20) [umoeller]
+ */
+
+BOOL WriteAllBlocks(HINI hini, PSZ pszHandles, PBYTE pBuffer, ULONG ulSize)
+{
+    PSZ     p,
+            pEnd;
+    BYTE    szBlockName[10];
+    INT     iCurBlock;
+    ULONG   ulCurSize;
+    PBYTE   pStart;
+    PDRIV   pDriv;
+    PNODE   pNode;
+
+    pStart    = pBuffer;
+    ulCurSize = 4;
+    p    = pBuffer + 4;
+    pEnd = pBuffer + ulSize;
+    iCurBlock = 1;
+    while (p < pEnd)
+    {
+        // ULONG   cbWrite = 0;
+        while (p < pEnd)
+        {
+            ULONG   ulPartSize = 0;
+            if (!memicmp(p, "DRIV", 4))
+            {
+                pDriv = (PDRIV)p;
+                ulPartSize = sizeof(DRIV) + strlen(pDriv->szName);
+            }
+            else if (!memicmp(p, "NODE", 4))
+            {
+                pNode = (PNODE)p;
+                ulPartSize = sizeof (NODE) + pNode->usNameSize;
+            }
+
+            if (ulCurSize + ulPartSize > 0x0000FFFF)
+                break;
+
+            ulCurSize += ulPartSize;
+            p         += ulPartSize;
+        }
+        sprintf(szBlockName, "BLOCK%d", iCurBlock++);
+
+        PrfWriteProfileData(hini,
+                            pszHandles,     // app
+                            szBlockName,    // key
+                            pStart,
+                            ulCurSize);
+        pStart    = p;
+        ulCurSize = 0;
+    }
+
+    while (iCurBlock < 20)
+    {
+        ULONG ulBlockSize;
+
+        sprintf(szBlockName, "BLOCK%d", iCurBlock++);
+
+        if (PrfQueryProfileSize(hini,
+                                pszHandles,
+                                szBlockName,
+                                &ulBlockSize)
+                && ulBlockSize > 0)
+            // delete block:
+            PrfWriteProfileData(hini,
+                                pszHandles,
+                                szBlockName,
+                                NULL, 0);       // delete
+            // WriteProfileData(pszHandles, szBlockName, hini, NULL, 0);
+    }
+
+    return TRUE;
+}
+
+/*
+ *@@ DeleteNode:
+ *      deletes a NODE in the specified buffer.
+ *
+ *@@added V0.9.5 (2000-08-20) [umoeller]
+ */
+
+ULONG DeleteNode(PBYTE pBuffer, PNODE pNode, ULONG ulSize)
+{
+    ULONG ulDelSize = sizeof (NODE) + pNode->usNameSize;
+    USHORT usID = pNode->usHandle; // pNode->usID;
+    ULONG ulMoveSize;
+
+    if (memcmp(pNode->chName, "NODE", 4))
+        return ulSize;
+
+    ulMoveSize = (pBuffer + ulSize) - ((PBYTE)pNode + ulDelSize);
+    ulSize -= ulDelSize;
+
+    memmove(pNode, (PBYTE)pNode + ulDelSize, ulMoveSize);
+
+    while (     (PBYTE)pNode < pBuffer + ulSize
+             && !memcmp(pNode->chName, "NODE", 4)
+             && pNode->usParentHandle == usID)
+        ulSize = DeleteNode(pBuffer, pNode, ulSize);
+
+    return ulSize;
+ }
+
+/*
+ *@@ DeleteDrive:
+ *      delete all information about a drive.
+ *      in the specified buffer.
+ *
+ *@@added V0.9.5 (2000-08-20) [umoeller]
+ */
+
+ULONG DeleteDrive(PBYTE pBuffer, PDRIV pDriv, ULONG ulSize)
+{
+    ULONG ulDelSize;
+    ULONG ulMoveSize;
+
+    if (memcmp(pDriv->chName, "DRIV", 4))
+        return ulSize;
+
+    ulDelSize = sizeof (DRIV) + strlen(pDriv->szName);
+    ulMoveSize = (pBuffer + ulSize) - ((PBYTE)pDriv + ulDelSize);
+    ulSize -= ulDelSize;
+
+    memmove(pDriv, (PBYTE)pDriv + ulDelSize, ulMoveSize);
+    return ulSize;
+}
+
+
+
Index: /trunk/src/helpers/xprf.c
===================================================================
--- /trunk/src/helpers/xprf.c	(revision 8)
+++ /trunk/src/helpers/xprf.c	(revision 8)
@@ -0,0 +1,962 @@
+
+/*
+ *@@sourcefile xprf.c:
+ *      contains replacement profile (INI) functions.
+ *      This file is new with V0.9.5 (2000-08-10) [umoeller].
+ *
+ *      These functions are quite similar to the regular Prf*
+ *      APIs, with the following advantages:
+ *
+ *      -- They do not use shared memory, but the default
+ *         memory allocation functions (malloc() etc.).
+ *         This greatly reduces the pressure on the process's
+ *         address space which apparently the default Prf*
+ *         functions are causing with large INI files.
+ *
+ *      -- Profiles are written out to disk in one flush, not
+ *         in the 32 KB chunks that the regular Prf* functions
+ *         use.
+ *
+ *      Other differences:
+ *
+ *      -- They cannot be used interchangably. Once you open
+ *         a profile using xprfOpenProfile, you must use the
+ *         extended functions.
+ *
+ *      -- The INI file is kept open after xprfOpenProfile
+ *         until xprfCloseProfile is called. Besides, we use
+ *         DosProtectOpen so that a second open of the same INI
+ *         file will always fail until xprfCloseProfile is called
+ *         (sorta like a mutex for profiles).
+ *         This is independent of the process or thread which
+ *         is doing the second open.
+ *
+ *         This is because since we are not using shared memory,
+ *         some other sort of protection had to be introduced
+ *         to make sure no two processes operate on the same INI.
+ *
+ *         Still, these functions are NOT thread-safe for the same profile.
+ *         If you open the profile on one thread and write and read
+ *         concurrently on two threads, there's no protection, and everything
+ *         will blow up. The functions are reeantrant though, so for different
+ *         profiles there will be no problems.
+ *
+ *      -- All changes to the INI files using xprfWriteProfileData
+ *         are only made to the file in memory. Only xprfCloseProfile
+ *         will flush the changes to disk. (I am not sure how the
+ *         default PrfCloseProfile handles this on files other than
+ *         HINI_USER and HINI_SYSTEM.)
+ *
+ *      -- HINI_USER and HINI_SYSTEM don't work. To retrieve data
+ *         from them, use the standard Prf* functions. You can however
+ *         use the new functions to create a duplicate of these files.
+ *
+ *      -- One similarity: All data items are limited to 64K,
+ *         as with the standard profiles. This is not a limitation
+ *         of the code, but of the INI file format, which uses
+ *         USHORT's for all item length specifications.
+ *
+ *      Thanks go out to Carsten Arnold for sending the INI file
+ *      format information (xprf.h) to me, which enabled me to
+ *      create this in the first place.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes:
+ *      --  xprf*   replacement profile (INI) functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\xprf.h"
+ *@@added V0.9.5 (2000-08-10) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WINSHELLDATA
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\linklist.h"
+#include "helpers\prfh.h"
+#include "helpers\xprf.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Profile (INI) replacement functions
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Declarations                                                   *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ XINIAPPDATA:
+ *      application structure in XINI.
+ */
+
+typedef struct _XINIAPPDATA
+{
+    PSZ         pszAppName;
+    LINKLIST    llKeys;             // contains PXINIKEYDATA pointers
+    ULONG       cKeys;              // count of items on list
+} XINIAPPDATA, *PXINIAPPDATA;
+
+/*
+ *@@ XINIKEYDATA:
+ *      key/data structure in XINIAPPDATA.
+ */
+
+typedef struct _XINIKEYDATA
+{
+    PSZ         pszKeyName;
+    PBYTE       pbData;
+    ULONG       cbData;
+} XINIKEYDATA, *PXINIKEYDATA;
+
+/* ******************************************************************
+ *                                                                  *
+ *   Helpers                                                        *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * FindApp:
+ *      attempts to find the specified application
+ *      in the given profile data.
+ *      Returns NULL if not found.
+ *
+ *      Private helper.
+ */
+
+PXINIAPPDATA FindApp(PXINI pXIni,           // in: profile opened with xprfOpenProfile
+                     const char *pcszApp)
+{
+    PXINIAPPDATA pReturn = NULL;
+    PLISTNODE pAppNode = lstQueryFirstNode(&pXIni->llApps);
+    while (pAppNode)
+    {
+        PXINIAPPDATA pAppDataThis = (PXINIAPPDATA)pAppNode->pItemData;
+        if (strcmp(pAppDataThis->pszAppName, pcszApp) == 0)
+        {
+            pReturn = pAppDataThis;
+            break;
+        }
+
+        pAppNode = pAppNode->pNext;
+    }
+
+    return (pReturn);
+}
+
+/*
+ * CreateApp:
+ *      creates a new application in the specified
+ *      profile structure and appends it to the list.
+ *      This does NOT check for whether the app is
+ *      already in the profile.
+ *
+ *      Private helper.
+ */
+
+PXINIAPPDATA CreateApp(PXINI pXIni,         // in: profile opened with xprfOpenProfile
+                       const char *pcszApp)
+{
+    PXINIAPPDATA pAppData = (PXINIAPPDATA)malloc(sizeof(XINIAPPDATA));
+    if (pAppData)
+    {
+        pAppData->pszAppName = strdup(pcszApp);
+        lstInit(&pAppData->llKeys, FALSE);
+        pAppData->cKeys = 0;
+
+        // store in INI's apps list
+        lstAppendItem(&pXIni->llApps, pAppData);
+        pXIni->cApps++;
+    }
+
+    return (pAppData);
+}
+
+/*
+ * FindKey:
+ *      attempts to find the specified key
+ *      in the given application.
+ *      Returns NULL if not found.
+ *
+ *      Private helper.
+ */
+
+PXINIKEYDATA FindKey(PXINIAPPDATA pAppData,
+                     const char *pcszKey)
+{
+    PXINIKEYDATA pReturn = NULL;
+    PLISTNODE pKeyNode = lstQueryFirstNode(&pAppData->llKeys);
+    while (pKeyNode)
+    {
+        PXINIKEYDATA pKeyDataThis = (PXINIKEYDATA)pKeyNode->pItemData;
+        if (strcmp(pKeyDataThis->pszKeyName, pcszKey) == 0)
+        {
+            pReturn = pKeyDataThis;
+            break;
+        }
+
+        pKeyNode = pKeyNode->pNext;
+    }
+
+    return (pReturn);
+}
+
+/*
+ * CreateKey:
+ *      creates a new key in the specified application
+ *      structure and appends it to the list.
+ *      This does NOT check for whether the key is
+ *      already in the application.
+ *
+ *      Private helper.
+ */
+
+PXINIKEYDATA CreateKey(PXINIAPPDATA pAppData,
+                       const char *pcszKey,     // in: key name
+                       PBYTE pbData,            // in: data for key
+                       ULONG cbData)            // in: sizeof (*pbData)
+{
+    PXINIKEYDATA pKeyData = (PXINIKEYDATA)malloc(sizeof(XINIKEYDATA));
+    if (pKeyData)
+    {
+        pKeyData->pszKeyName = strdup(pcszKey);
+
+        pKeyData->pbData = (PBYTE)malloc(cbData);
+        if (pKeyData->pbData)
+        {
+            memcpy(pKeyData->pbData, pbData, cbData);
+            pKeyData->cbData = cbData;
+            // store in app's keys list
+            lstAppendItem(&pAppData->llKeys, pKeyData);
+            pAppData->cKeys++;
+        }
+        else
+        {
+            // malloc failed:
+            free(pKeyData);
+            pKeyData = 0;
+        }
+    }
+
+    return (pKeyData);
+}
+
+/*
+ * FreeKey:
+ *      frees the specified key. Does not remove
+ *      the key from the keys list in XINIAPPDATA
+ *      however.
+ *
+ *      Private helper.
+ */
+
+VOID FreeKey(PXINIKEYDATA pKeyDataThis)
+{
+    if (pKeyDataThis->pszKeyName)
+        free(pKeyDataThis->pszKeyName);
+    if (pKeyDataThis->pbData)
+        free(pKeyDataThis->pbData);
+    free(pKeyDataThis);
+}
+
+/*
+ * FreeApp:
+ *      frees the specified application. Does not remove
+ *      the application from the application list in XINI
+ *      however.
+ *
+ *      Private helper.
+ */
+
+VOID FreeApp(PXINIAPPDATA pAppDataThis)
+{
+    PLISTNODE pKeyNode = lstQueryFirstNode(&pAppDataThis->llKeys);
+    while (pKeyNode)
+    {
+        PXINIKEYDATA pKeyDataThis = (PXINIKEYDATA)pKeyNode->pItemData;
+
+        FreeKey(pKeyDataThis);
+
+        pKeyNode = pKeyNode->pNext;
+    }
+
+    if (pAppDataThis->pszAppName)
+        free(pAppDataThis->pszAppName);
+
+    lstClear(&pAppDataThis->llKeys);
+
+    free(pAppDataThis);
+}
+
+/*
+ * FreeINI:
+ *      cleans up the specified ini structure entirely.
+ *
+ *      Private helper.
+ */
+
+BOOL FreeINI(PXINI pXIni)       // in: profile opened with xprfOpenProfile
+{
+    BOOL brc = FALSE;
+
+    if (pXIni)
+    {
+        PLISTNODE pAppNode = lstQueryFirstNode(&pXIni->llApps);
+        while (pAppNode)
+        {
+            PXINIAPPDATA pAppDataThis = (PXINIAPPDATA)pAppNode->pItemData;
+
+            FreeApp(pAppDataThis);
+
+            pAppNode = pAppNode->pNext;
+        }
+
+        lstClear(&pXIni->llApps);
+        free(pXIni);
+    }
+
+    return (brc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   Read/write data                                                *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ * ReadINI:
+ *      reads in the entire INI file from disk and
+ *      initializes all internal data structures.
+ *
+ *      Private helper.
+ */
+
+BOOL ReadINI(PXINI pXIni)       // in: profile opened with xprfOpenProfile
+{
+    BOOL brc = FALSE;
+    FILESTATUS3 fs3;
+
+    if (DosProtectQueryFileInfo(pXIni->hFile,
+                                FIL_STANDARD,
+                                &fs3,
+                                sizeof(fs3),
+                                pXIni->hLock)
+            == NO_ERROR)
+    {
+        PBYTE   pbFileData = (PBYTE)malloc(fs3.cbFile);
+        if (pbFileData)
+        {
+            ULONG ulSet = 0;
+            APIRET arc = DosProtectSetFilePtr(pXIni->hFile,
+                                              0,
+                                              FILE_BEGIN,
+                                              &ulSet,
+                                              pXIni->hLock);
+            if (    (arc == NO_ERROR)
+                 && (ulSet == 0)
+               )
+            {
+                ULONG cbRead = 0;
+                arc = DosProtectRead(pXIni->hFile,
+                                     pbFileData,
+                                     fs3.cbFile,
+                                     &cbRead,
+                                     pXIni->hLock);
+                if (    (arc == NO_ERROR)
+                     && (cbRead == fs3.cbFile)
+                   )
+                {
+                    PINIFILE_HEADER pHeader = (PINIFILE_HEADER)pbFileData;
+                    if (pHeader->magic == 0xFFFFFFFF)
+                    {
+                        ULONG   ulAppOfs = pHeader->offFirstApp;
+
+                        // it was a valid profile, so return TRUE
+                        brc = TRUE;
+
+                        // create-applications loop
+                        while ((ulAppOfs) && (brc))
+                        {
+                            // application struct
+                            PINIFILE_APP pApp = (PINIFILE_APP)(pbFileData + ulAppOfs);
+                            ULONG   ulKeysOfs = pApp->offFirstKeyInApp;
+                            PXINIAPPDATA pIniApp
+                                = CreateApp(pXIni,
+                                            (PBYTE)(pbFileData + pApp->offAppName));
+                            if (!pIniApp)
+                            {
+                                brc = FALSE;
+                                break;
+                            }
+
+                            // create-keys loop
+                            while ((ulKeysOfs) && (brc))
+                            {
+                                PINIFILE_KEY pKey = (PINIFILE_KEY)(pbFileData + ulKeysOfs);
+
+                                PXINIKEYDATA pIniKey
+                                    = CreateKey(pIniApp,
+                                                (PSZ)(pbFileData + pKey->offKeyName),
+                                                (PBYTE)(pbFileData + pKey->offKeyData),
+                                                pKey->lenKeyData);
+
+                                if (!pKey)
+                                {
+                                    brc = FALSE;
+                                    break;
+                                }
+
+                                // next key; can be null
+                                ulKeysOfs = pKey->offNextKeyInApp;
+                            }
+
+                            // next application; can be null
+                            ulAppOfs = pApp->offNextApp;
+                        }
+                    }
+                }
+            }
+
+            free(pbFileData);
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ * WriteINI:
+ *      writes the entire data structure back to disk.
+ *      Does not close the file.
+ *
+ *      Private helper.
+ */
+
+BOOL WriteINI(PXINI pXIni)      // in: profile opened with xprfOpenProfile
+{
+    BOOL    brc = FALSE;
+    ULONG   ulTotalFileSize = sizeof(INIFILE_HEADER);
+    ULONG   ulSet = 0;
+    PBYTE   pbData2Write = NULL;
+
+    // check how much memory we'll need:
+    // one INIFILE_HEADER
+    // + for each app: one INIFILE_APP plus app name length
+    // + for each key: one INIFILE_KEY plus key name length plus data length
+
+    // go thru all apps
+    PLISTNODE pAppNode = lstQueryFirstNode(&pXIni->llApps);
+    while (pAppNode)
+    {
+        PLISTNODE pKeyNode;
+
+        PXINIAPPDATA pAppDataThis = (PXINIAPPDATA)pAppNode->pItemData;
+
+        // one INIFILE_APP plus app name length for each app
+        ulTotalFileSize +=   sizeof(INIFILE_APP)
+                           + strlen(pAppDataThis->pszAppName)
+                           + 1;         // null terminator
+
+        // for each app, go thru all keys
+        pKeyNode = lstQueryFirstNode(&pAppDataThis->llKeys);
+        while (pKeyNode)
+        {
+            PXINIKEYDATA pKeyDataThis = (PXINIKEYDATA)pKeyNode->pItemData;
+            ulTotalFileSize +=   sizeof(INIFILE_KEY)
+                               + strlen(pKeyDataThis->pszKeyName)
+                               + pKeyDataThis->cbData
+                               + 1;         // null terminator
+
+            pKeyNode = pKeyNode->pNext;
+        }
+
+        pAppNode = pAppNode->pNext;
+    }
+
+    // allocate buffer for total size
+    pbData2Write = (PBYTE)malloc(ulTotalFileSize);
+    if (pbData2Write)
+    {
+        APIRET arc = NO_ERROR;
+
+        // set header in buffer
+        PINIFILE_HEADER pHeader = (PINIFILE_HEADER)pbData2Write;
+        // pointer into buffer for current write
+        // (used below)
+        ULONG   ulCurOfs = sizeof(INIFILE_HEADER);
+
+        // 1) set up header
+
+        pHeader->magic = 0xFFFFFFFF;
+        if (pXIni->cApps)
+            // we have any applications:
+            pHeader->offFirstApp = sizeof(INIFILE_HEADER);
+                     // offset of first application in file
+        else
+            // empty INI file:
+            pHeader->offFirstApp = 0;
+        pHeader->lenFile = ulTotalFileSize;
+        pHeader->filler1 = 0;
+        pHeader->filler2 = 0;
+
+        // 2) for-all-applications loop
+
+        pAppNode = lstQueryFirstNode(&pXIni->llApps);
+        while (pAppNode)
+        {
+            PXINIAPPDATA pAppDataThis = (PXINIAPPDATA)pAppNode->pItemData;
+            ULONG       cbAppName = strlen(pAppDataThis->pszAppName) + 1;
+
+            // layout of application entry in file:
+            // -- struct PINIFILE_APP       (ulCurOfs right now)
+            // -- application name (null-terminated)
+            // --   INIFILE_KEY 1
+            // --   INIFILE_KEY 2 ...
+            // -- next struct PINIFILE_APP
+
+            // make pointer to application entry
+            PINIFILE_APP pIniAppCurrent = (PINIFILE_APP)(pbData2Write + ulCurOfs);
+
+            // write out application entry
+            // pIniAppCurrent->offNextApp = 0;
+            // pIniAppCurrent->offFirstKeyInApp = ulCurOfs + cbAppName;
+            pIniAppCurrent->filler1 = 0;
+            pIniAppCurrent->lenAppName
+                = pIniAppCurrent->_lenAppName
+                = cbAppName;
+            // set offset to application name: put this right after the application
+            ulCurOfs += sizeof(INIFILE_APP);
+            pIniAppCurrent->offAppName = ulCurOfs;
+
+            // write app name (null-terminated)
+            memcpy(pbData2Write + ulCurOfs,
+                   pAppDataThis->pszAppName,
+                   cbAppName);
+            ulCurOfs += cbAppName;
+
+            // ulCurOfs points to INIFILE_KEY entry now
+            if (pAppDataThis->cKeys)
+            {
+                // we have keys:
+                PLISTNODE pKeyNode = lstQueryFirstNode(&pAppDataThis->llKeys);
+                pIniAppCurrent->offFirstKeyInApp = ulCurOfs;
+
+                // 3) for-all-keys loop per application
+
+                while (pKeyNode)
+                {
+                    PXINIKEYDATA pKeyDataThis = (PXINIKEYDATA)pKeyNode->pItemData;
+                    ULONG       cbKeyName = strlen(pKeyDataThis->pszKeyName) + 1;
+
+                    PINIFILE_KEY pIniKeyCurrent = (PINIFILE_KEY)(pbData2Write + ulCurOfs);
+                    pIniKeyCurrent->filler1 = 0;
+                    ulCurOfs += sizeof(INIFILE_KEY);
+                            // has offset to key name now
+
+                    // a) key name
+                    pIniKeyCurrent->lenKeyName
+                        = pIniKeyCurrent->_lenKeyName
+                        = cbKeyName;
+                    pIniKeyCurrent->offKeyName = ulCurOfs;
+                    memcpy(pbData2Write + ulCurOfs,
+                           pKeyDataThis->pszKeyName,
+                           cbKeyName);
+                    ulCurOfs += cbKeyName;
+                            // has offset to data now
+
+                    // b) data
+                    pIniKeyCurrent->lenKeyData
+                        = pIniKeyCurrent->_lenKeyData
+                        = pKeyDataThis->cbData;
+                    pIniKeyCurrent->offKeyData = ulCurOfs;
+                    memcpy(pbData2Write + ulCurOfs,
+                           pKeyDataThis->pbData,
+                           pKeyDataThis->cbData);
+                    ulCurOfs += pKeyDataThis->cbData;
+                            // points to after all key data now;
+                            // this receives either the next key/data block
+                            // or the next application or nothing
+
+                    // ofs of next key:
+                    if (pKeyNode->pNext)
+                        pIniKeyCurrent->offNextKeyInApp = ulCurOfs;
+                    else
+                        // last key:
+                        pIniKeyCurrent->offNextKeyInApp = 0;
+
+                    pKeyNode = pKeyNode->pNext;
+                }
+
+                // ulCurOfs points to after the last keys entry now
+            }
+            else
+                // no keys:
+                pIniAppCurrent->offFirstKeyInApp = 0;
+
+            // done with keys (there may be none!);
+            // now set offset to next app in current application
+            if (pAppNode->pNext)
+                pIniAppCurrent->offNextApp = ulCurOfs;
+            else
+                // this was the last one:
+                pIniAppCurrent->offNextApp = 0;
+
+            // next app
+            pAppNode = pAppNode->pNext;
+        }
+
+        // write out everything
+        arc = DosProtectSetFilePtr(pXIni->hFile,
+                                   0,
+                                   FILE_BEGIN,
+                                   &ulSet,
+                                   pXIni->hLock);
+        if (arc == NO_ERROR)
+        {
+            ULONG cbWritten = 0;
+            arc = DosProtectWrite(pXIni->hFile,
+                                  pbData2Write,
+                                  ulTotalFileSize,
+                                  &cbWritten,
+                                  pXIni->hLock);
+            if (arc == NO_ERROR)
+            {
+                arc = DosProtectSetFileSize(pXIni->hFile,
+                                            ulTotalFileSize,
+                                            pXIni->hLock);
+                if (arc == NO_ERROR)
+                    brc = TRUE;
+            }
+        }
+
+        free(pbData2Write);
+    }
+
+    return (brc);
+}
+
+/* ******************************************************************
+ *                                                                  *
+ *   API Functions                                                  *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ xprfOpenProfile:
+ *      opens an extended profile (XINI). This is similar to
+ *      PrfOpenProfile, but cannot be used interchangably.
+ *      See the top of this file (xprf.c) for general remarks.
+ *
+ *      If the full path is not specified, the current
+ *      directory is used. This uses DosProtectOpen to
+ *      open the profile, so see CPREF for additional remarks.
+ *
+ *      If the specified profile does not exist yet, it is
+ *      created by this function.
+ *
+ *      Note that you must not open the system profiles
+ *      (OS2.INI and OS2SYS.INI) using this function because
+ *      this would keep OS/2 from updating them.
+ *
+ *      This returns 0 (NO_ERROR) on success. Otherwise either
+ *      an OS/2 error code (ERROR_*) or one of the profile error
+ *      codes defined in prfh.h is returned.
+ */
+
+APIRET xprfOpenProfile(const char *pcszFilename,    // in: profile name
+                       PXINI *ppxini)               // out: profile handle
+{
+    APIRET  arc = NO_ERROR;
+    PXINI   pXIni = NULL;
+
+    if (pcszFilename)
+        if (strlen(pcszFilename) < CCHMAXPATH - 1)
+        {
+            HFILE   hFile = NULLHANDLE;
+            ULONG   ulAction = 0;
+            FHLOCK  hLock = 0;
+            arc = DosProtectOpen((PSZ)pcszFilename,
+                                 &hFile,
+                                 &ulAction,
+                                 1024,          // initial size
+                                 FILE_NORMAL,
+                                 OPEN_ACTION_CREATE_IF_NEW
+                                    | OPEN_ACTION_OPEN_IF_EXISTS,
+                                 OPEN_FLAGS_FAIL_ON_ERROR
+                                    | OPEN_FLAGS_NO_CACHE
+                                    | OPEN_FLAGS_SEQUENTIAL
+                                    | OPEN_SHARE_DENYREADWRITE
+                                    | OPEN_ACCESS_READWRITE,
+                                 NULL, // no EAs
+                                 &hLock);
+            if (arc == NO_ERROR)
+            {
+                pXIni = (PXINI)malloc(sizeof(XINI));
+                if (!pXIni)
+                    arc = ERROR_NOT_ENOUGH_MEMORY;
+                else
+                {
+                    // OK: initialize XINI
+                    memset(pXIni, 0, sizeof(XINI));
+                    memcpy(pXIni->acMagic, XINI_MAGIC_BYTES, sizeof(XINI_MAGIC_BYTES));
+                    strcpy(pXIni->szFilename, pcszFilename);
+                    pXIni->hFile = hFile;
+                    pXIni->hLock = hLock;
+
+                    lstInit(&pXIni->llApps, FALSE);
+
+                    if (ulAction == FILE_CREATED)
+                        // file newly created: rewrite on close
+                        pXIni->fDirty = TRUE;
+                    else
+                        // file existed: read data
+                        if (!ReadINI(pXIni))
+                        {
+                            // error:
+                            FreeINI(pXIni);
+                            pXIni = NULL;
+                            arc = PRFERR_READ;
+                        }
+
+                    if ((pXIni) && (ppxini))
+                        *ppxini = pXIni;
+                }
+            }
+        }
+
+    return (arc);
+}
+
+/*
+ *@@ xprfQueryProfileData:
+ *      similar to PrfQueryProfileData.
+ *
+ *      @@todo: Still to be written.
+ */
+
+/* BOOL xprfQueryProfileData(PXINI hIni,            // in: profile opened with xprfOpenProfile
+                          const char *pcszApp,
+                          const char *pcszKey,
+                          PVOID pBuffer,
+                          PULONG pulBufferMax)
+{
+    BOOL brc = FALSE;
+
+    return (brc);
+} */
+
+/*
+ *@@ xprfWriteProfileData:
+ *      writes data into an extended profile (XINI).
+ *      This operates similar to PrfWriteProfileData,
+ *      that is:
+ *
+ *      -- If all of pcszApp, pcszKey, pData, and
+ *         ulDataLen are specified, the given data is
+ *         written into the specified application/key
+ *         pair.
+ *
+ *      -- If pcszApp and pcszKey are specified, but
+ *         pData and ulDataLen are null, the specified
+ *         key is deleted.
+ *
+ *      -- If pcszApp is specified, but all of pcszKey,
+ *         pData, and ulDataLen are null, the entire
+ *         specified application is deleted.
+ *
+ *      You cannot specify HINI_SYSTEM or HINI_USER for
+ *      hINi.
+ *
+ *      Note that if data has been added or removed,
+ *      the INI file on disk is not updated automatically.
+ *      Instead, our memory copy of it is only marked
+ *      as "dirty" so that the file will be rewritten
+ *      on xprfCloseProfile.
+ */
+
+BOOL xprfWriteProfileData(PXINI hIni,           // in: profile opened with xprfOpenProfile
+                          const char *pcszApp,  // in: application name
+                          const char *pcszKey,  // in: key name or NULL
+                          PVOID pData,          // in: data to write or NULL
+                          ULONG ulDataLen)      // in: sizeof(*pData) or null
+{
+    BOOL brc = FALSE;
+
+    if (hIni)
+    {
+        PXINI pXIni = (PXINI)hIni;
+        if (memcmp(pXIni->acMagic, XINI_MAGIC_BYTES, sizeof(XINI_MAGIC_BYTES))
+                        == 0)
+        {
+            // check if application exists
+            PXINIAPPDATA pAppData = FindApp(pXIni,
+                                            pcszApp);
+
+            // now check: does caller want entire application deleted?
+            if (!pcszKey)
+            {
+                // yes, delete application: did we find it?
+                if (pAppData)
+                {
+                    // yes: kill that
+                    FreeApp(pAppData);
+                    // and remove from list
+                    brc = lstRemoveItem(&pXIni->llApps, pAppData);
+                    pXIni->cApps--;
+                    // rewrite profile on close
+                    pXIni->fDirty = TRUE;
+                }
+                else
+                    // application doesn't exist:
+                    brc = TRUE;
+            }
+            else
+            {
+                // caller has specified key:
+                // does caller want a key to be deleted?
+                if (!ulDataLen)
+                {
+                    // yes: delete key:
+                    if (pAppData)
+                    {
+                        // app exists:
+                        // find key
+                        PXINIKEYDATA pKeyData = FindKey(pAppData,
+                                                        pcszKey);
+                        if (pKeyData)
+                        {
+                            // key exists: kill that
+                            FreeKey(pKeyData);
+                            // and remove from app's keys list
+                            brc = lstRemoveItem(&pAppData->llKeys, pKeyData);
+                            pAppData->cKeys--;
+                            // rewrite profile on close
+                            pXIni->fDirty = TRUE;
+                        }
+                        else
+                            // key doesn't even exist:
+                            brc = TRUE;
+                    }
+                    else
+                        // app doesn't even exist:
+                        brc = TRUE;
+                }
+                else
+                {
+                    // key and data specified: let's add something...
+
+                    if (!pAppData)
+                        // app doesn't exist yet:
+                        // create
+                        pAppData = CreateApp(pXIni,
+                                             pcszApp);
+
+                    if (pAppData)
+                    {
+                        // found or created app:
+                        // check if key exists
+                        PXINIKEYDATA pKeyData = FindKey(pAppData,
+                                                        pcszKey);
+                        if (!pKeyData)
+                            // doesn't exist yet:
+                            // create
+                            pKeyData = CreateKey(pAppData,
+                                                 pcszKey,
+                                                 (PBYTE)pData,
+                                                 ulDataLen);
+
+                        if (pKeyData)
+                        {
+                           // mark as dirty
+                           pXIni->fDirty = TRUE;
+                           brc = TRUE;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    return (brc);
+}
+
+/*
+ *@@ xprfCloseProfile:
+ *      closes a profile opened with xprfOpenProfile.
+ *      If the profile is "dirty", that is, if any data
+ *      has been changed using xprfWriteProfileData, the
+ *      file is written back to disk before closing.
+ *
+ *      You cannot specify HINI_SYSTEM or HINI_USER for
+ *      hINi.
+ */
+
+BOOL xprfCloseProfile(PXINI hIni)       // in: profile opened with xprfOpenProfile
+{
+    BOOL brc = FALSE;
+
+    if (hIni)
+    {
+        PXINI pXIni = (PXINI)hIni;
+        if (memcmp(pXIni->acMagic, XINI_MAGIC_BYTES, sizeof(XINI_MAGIC_BYTES))
+                        == 0)
+        {
+            brc = TRUE;
+
+            if (pXIni->fDirty)
+                brc = WriteINI(pXIni);
+
+            if (brc)
+            {
+                APIRET arc = DosProtectClose(pXIni->hFile,
+                                             pXIni->hLock);
+                if (arc == NO_ERROR)
+                {
+                    pXIni->hFile = 0;
+                    pXIni->hLock = 0;
+
+                    FreeINI(pXIni);
+                }
+                else
+                    brc = FALSE;
+            }
+        }
+    }
+
+    return (brc);
+}
+
+
Index: /trunk/src/helpers/xprf2.c
===================================================================
--- /trunk/src/helpers/xprf2.c	(revision 8)
+++ /trunk/src/helpers/xprf2.c	(revision 8)
@@ -0,0 +1,511 @@
+
+/*
+ *@@sourcefile xprf2.c:
+ *      copy-profile functions which use the replacement
+ *      profile functions in xprf.c.
+ *
+ *      This can be used for a bomb-proof "save system profiles"
+ *      (see xprfSaveINIs).
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes:
+ *      --  xprf*   replacement profile (INI) functions
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@header "helpers\xprf.h"
+ *@@added V0.9.5 (2000-08-10) [umoeller]
+ */
+
+/*
+ *      Copyright (C) 2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#define INCL_DOS
+#define INCL_DOSERRORS
+#define INCL_WINSHELLDATA
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\dosh.h"
+#include "helpers\prfh.h"
+#include "helpers\stringh.h"
+#include "helpers\xprf.h"
+
+#pragma hdrstop
+
+/*
+ *@@category: Helpers\Profile (INI) replacement functions
+ */
+
+/* ******************************************************************
+ *                                                                  *
+ *   Copy API Functions                                             *
+ *                                                                  *
+ ********************************************************************/
+
+/*
+ *@@ xprfCopyKey:
+ *      copies a single key from an Prf* HINI to an xprf* PXINI.
+ *      hiniTarget must therefore have been opened using
+ *      xprfOpenProfile.
+ *
+ *      This returns 0 (NO_ERROR) if copying succeeded. Otherwise either
+ *      an OS/2 error code (ERROR_*) or one of the profile error
+ *      codes defined in prfh.h is returned.
+ */
+
+APIRET xprfCopyKey(HINI hiniSource,       // in: source profile (can be HINI_USER or HINI_SYSTEM)
+                   PSZ pszSourceApp,      // in: source application
+                   PSZ pszKey,            // in: source/target key
+                   PXINI hiniTarget,      // in: target profile opened with xprfOpenProfile
+                   PSZ pszTargetApp)      // in: target app
+{
+    ULONG   ulSizeOfData = 0;
+    APIRET  arc = NO_ERROR;
+
+    if (PrfQueryProfileSize(hiniSource, pszSourceApp, pszKey, &ulSizeOfData))
+    {
+        PSZ pData = 0;
+
+        // copy data
+        if (ulSizeOfData == 0)
+        {
+            // data size == 0: this shouldn't really happen,
+            // but if it does, we'll just create a NULL string.
+            // Users have reported that some INI files seem to
+            // contain those "empty" keys. I don't see how these
+            // can exist, but they seem to...
+            pData = (PSZ)malloc(1);
+            *pData = 0;
+        }
+        else
+            pData = (PSZ)malloc(ulSizeOfData);
+
+        if (pData)
+        {
+            fflush(stdout);
+            if (PrfQueryProfileData(hiniSource,
+                                    pszSourceApp,
+                                    pszKey,
+                                    pData,
+                                    &ulSizeOfData))
+            {
+                if (!xprfWriteProfileData(hiniTarget,
+                                          pszTargetApp,
+                                          pszKey,
+                                          pData,
+                                          ulSizeOfData))
+                    arc = PRFERR_WRITE;
+            }
+            else
+                arc = PRFERR_READ;
+
+            free(pData);
+        }
+        else
+            arc = ERROR_NOT_ENOUGH_MEMORY;
+    }
+    else
+        arc = PRFERR_DATASIZE;
+
+    return (arc);
+}
+
+/*
+ *@@ xprfCopyApp:
+ *      copies a single application from an Prf* HINI to an xprf* PXINI.
+ *      hiniTarget must therefore have been opened using
+ *      xprfOpenProfile.
+ *
+ *      This calls xprfCopyKey for each key in the application.
+ *
+ *      This returns 0 (NO_ERROR) if copying succeeded. Otherwise either
+ *      an OS/2 error code (ERROR_*) or one of the profile error
+ *      codes defined in prfh.h is returned.
+ */
+
+APIRET xprfCopyApp(HINI hiniSource,   // in: source profile (can be HINI_USER or HINI_SYSTEM)
+                   PSZ pszSourceApp,  // in: source application
+                   PXINI hiniTarget,  // in: target profile opened with xprfOpenProfile
+                   PSZ pszTargetApp,  // in: name of pszSourceApp in hiniTarget
+                   PSZ pszErrorKey)   // out: failing key in case of error; ptr can be NULL
+{
+    APIRET arc = NO_ERROR;
+    PSZ pszKeysList;
+
+    if (pszErrorKey)
+        *pszErrorKey = 0;
+
+    pszKeysList = prfhQueryKeysForApp(hiniSource, pszSourceApp);
+    if (pszKeysList)
+    {
+        PSZ pKey2 = pszKeysList;
+
+        while (*pKey2 != 0)
+        {
+            // copy this key
+            arc = xprfCopyKey(hiniSource,
+                              pszSourceApp,
+                              pKey2,
+                              hiniTarget,
+                              pszTargetApp);
+            if (arc)
+            {
+                // error: copy failing key to buffer
+                if (pszErrorKey)
+                    strcpy(pszErrorKey, pKey2);
+                break;
+            }
+            pKey2 += strlen(pKey2)+1;
+        } // end while (*pKey2 != 0)
+
+        free (pszKeysList);
+    }
+    else
+        arc = PRFERR_KEYSLIST;
+
+    return (arc);
+}
+
+/*
+ *@@ xprfCopyProfile:
+ *      this copies an entire profile.
+ *
+ *      The source profile must have been opened using the
+ *      regular OS/2 PrfOpenProfile. You can also specify
+ *      HINI_USER or HINI_SYSTEM.
+ *
+ *      pszNew specifies the file name for the new profile.
+ *      This must end in *.INI. The new profile is opened
+ *      using xprfOpenProfile, so see additional remarks there.
+ *
+ *      This returns 0 (NO_ERROR) on success. Otherwise either
+ *      an OS/2 error code (ERROR_*) or one of the profile error
+ *      codes defined in prfh.h is returned.
+ */
+
+APIRET xprfCopyProfile(HINI hOld,           // in: source profile (can be HINI_USER or HINI_SYSTEM)
+                       PSZ pszNew,          // in: new filename (can be fully qualified)
+                       PFN_PRF_PROGRESS pfnProgressCallback,
+                       ULONG ulUser,        // in: passed to pfnProgressCallback
+                       ULONG ulCount,       // in: index of INI being copied (0 <= ulCount <= ulMax)
+                       ULONG ulMax)         // in: maximum index (for progress); 0 means 1 INI, 1 means 2 INIs, ...
+{
+    APIRET  arc = NO_ERROR;
+    PXINI   pxiniNew = NULL;
+    ULONG   ulSizeOfAppsList;
+
+    if (!pszNew)
+        arc = ERROR_INVALID_PARAMETER;
+
+    if (arc == NO_ERROR)
+    {
+
+        DosDelete(pszNew);
+
+        // open new profile
+
+        arc = xprfOpenProfile(pszNew,
+                              &pxiniNew);
+    }
+
+    // get size of applications list
+    if (arc == NO_ERROR)
+    {
+        if (!PrfQueryProfileSize(hOld, NULL, NULL, &ulSizeOfAppsList))
+            arc = PRFERR_APPSLIST;
+        else
+            if (ulSizeOfAppsList == 0)
+                arc = PRFERR_APPSLIST;
+    }
+
+    if (arc == NO_ERROR)
+    {
+        // get applications list
+        PSZ pApps = (PSZ)malloc(ulSizeOfAppsList);
+        PSZ pApp2 = pApps;
+        if (!PrfQueryProfileData(hOld,
+                                 NULL,
+                                 NULL,
+                                 pApps,
+                                 &ulSizeOfAppsList))
+            arc = PRFERR_APPSLIST;
+
+        // applications loop
+
+        while (   (*pApp2 != 0)
+               && (arc == NO_ERROR)
+              )
+        {
+            CHAR szErrorKey[1000];
+            // copy application (this will call prfhCopyKey in turn)
+            arc = xprfCopyApp(hOld,
+                              pApp2,
+                              pxiniNew,
+                              pApp2,
+                              szErrorKey);
+
+            if (pfnProgressCallback)
+            {
+                ULONG ulNow2, ulMax2;
+                ulNow2 = ((1000*(pApp2-pApps)) / ulSizeOfAppsList) + (ulCount*1000);
+                ulMax2 = (ulMax+1)*1000;
+                if (!pfnProgressCallback(ulUser, ulNow2, ulMax2))
+                    // aborted:
+                    arc = PRFERR_ABORTED;
+            }
+
+            // go for next app
+            pApp2 += strlen(pApp2)+1;
+
+        } // end while (*pApp2 != 0) && MBID_NOERROR
+
+        if (pApps)
+            free(pApps);
+    }
+
+    xprfCloseProfile(pxiniNew);
+
+    // progress
+    if (pfnProgressCallback)
+        pfnProgressCallback(ulUser, (ulCount+1) * 1000, (ulMax+1) * 1000);
+
+    return (arc);
+}
+
+/*
+ *@@ xprfSaveINIs:
+ *      this rewrites the OS/2 user and system profiles
+ *      (OS2.INI and OS2SYS.INI) to disk. This is done
+ *      by doing the following:
+ *
+ *      -- First, both profiles are dumped into two temporary
+ *         profiles (using xprfCopyProfile on each of them).
+ *         These are put into the same directory as the source
+ *         profiles (?:\OS2 normally) with a file name extension
+ *         of OS2*.XFL.
+ *
+ *      -- Only if that succeeded, the original profiles are
+ *         renamed to OS2*.BAK. Existing OS2*.BAK files are
+ *         deleted.
+ *
+ *      -- If that succeded, the temporary OS2*.XFL files are
+ *         renamed to OS2*.INI.
+ *
+ *      This is now used during XShutdown to dump the system
+ *      profiles before shutting down the system. As opposed to
+ *      prfhSaveINIs, which was previously used, this does not
+ *      use the Prf* functions for the target profiles so that
+ *      those ugly "Error saving INI files" errors are less likely.
+ *
+ *      This returns 0 (NO_ERROR) if copying succeeded. Otherwise either
+ *      an OS/2 error code (ERROR_*) or one of the profile error
+ *      codes defined in prfh.h is returned.
+ */
+
+APIRET xprfSaveINIs(HAB hab,               // in:  anchor block
+                    PFN_PRF_PROGRESS pfnProgressCallback,
+                    ULONG ulUser)
+{
+    PRFPROFILE Profiles;
+    APIRET  arc = NO_ERROR;
+
+    // FILESTATUS3 fs3;
+    CHAR    szSysNew[CCHMAXPATH],
+            szUserNew[CCHMAXPATH],
+            szSysBackup[CCHMAXPATH],
+            szUserBackup[CCHMAXPATH];
+
+    /*
+     * get system profiles:
+     *
+     */
+
+    Profiles.cchUserName = Profiles.cchSysName = 0;
+    if (!PrfQueryProfile(hab, &Profiles))
+        arc = PRFERR_QUERY;
+    else
+    {
+        Profiles.pszUserName  = (PSZ)malloc(Profiles.cchUserName);
+        Profiles.pszSysName  = (PSZ)malloc(Profiles.cchSysName);
+        if (    (Profiles.pszSysName == NULL)
+             || (Profiles.pszUserName == NULL)
+           )
+            arc = PRFERR_QUERY;
+        else
+            if (!PrfQueryProfile(hab, &Profiles))
+                arc = PRFERR_QUERY;
+    }
+
+    if (arc == NO_ERROR)
+    {
+        PSZ _p;
+
+        /*
+         * create new profile names:
+         *      same as old profiles, but with *.XFL ext.
+         */
+
+        // system INI
+        strcpy(szSysBackup, Profiles.pszSysName);
+        strcpy(szSysNew, Profiles.pszSysName);
+        _p = strhistr(szSysBackup, ".INI");
+        if (!_p)
+            arc = PRFERR_INVALID_FILE_NAME;
+        else
+            strcpy(_p, ".BAK");
+        _p = strhistr(szSysNew, ".INI");
+        if (!_p)
+            arc = PRFERR_INVALID_FILE_NAME;
+        else
+            strcpy(_p, ".XFL");
+
+        // user INI
+        strcpy(szUserBackup, Profiles.pszUserName);
+        strcpy(szUserNew, Profiles.pszUserName);
+        _p = strhistr(szUserBackup, ".INI");
+        if (!_p)
+            arc = PRFERR_INVALID_FILE_NAME;
+        else
+            strcpy(_p, ".BAK");
+        _p = strhistr(szUserNew, ".INI");
+        if (!_p)
+            arc = PRFERR_INVALID_FILE_NAME;
+        else
+            strcpy(_p, ".XFL");
+
+        /*
+         * create OS2SYS.XFL:
+         *
+         */
+
+        if (arc == NO_ERROR)
+            arc = xprfCopyProfile(HINI_SYSTEM,
+                                  szSysNew,              // new filename
+                                  pfnProgressCallback,
+                                  ulUser, 0, 1);
+        /*
+         * create OS2SYS.XFL:
+         *
+         */
+
+        if (arc == NO_ERROR)
+            arc = xprfCopyProfile(HINI_USER,
+                                  szUserNew,              // new filename
+                                  pfnProgressCallback,
+                                  ulUser, 1, 1);
+    }
+
+    /*
+     * renaming stuff for OS2SYS.INI
+     *
+     */
+
+    if (arc == NO_ERROR)
+    {
+        // attrib -r -s -h -a OS2SYS.BAK
+        doshSetPathAttr(szSysBackup, FILE_NORMAL);
+        // delete OS2SYS.BAK
+        DosDelete(szSysBackup);
+        // attrib -r -s -h -a OS2SYS.INI
+        doshSetPathAttr(Profiles.pszSysName, FILE_NORMAL);
+        // REN OS2SYS.INI OS2SYS.BAK
+        DosMove(Profiles.pszSysName, szSysBackup);
+    }
+
+    /*
+     * renaming stuff for OS2.INI
+     *
+     */
+
+    if (arc == NO_ERROR)
+    {
+        // attrib -r -s -h -a OS2SYS.BAK
+        doshSetPathAttr(szUserBackup, FILE_NORMAL);
+        // delete OS2SYS.BAK
+        DosDelete(szUserBackup);
+        // attrib -r -s -h -a OS2SYS.INI
+        doshSetPathAttr(Profiles.pszUserName, FILE_NORMAL);
+        // REN OS2SYS.INI OS2SYS.BAK
+        DosMove(Profiles.pszUserName, szUserBackup);
+    }
+
+    if (arc == NO_ERROR)
+    {
+        // finally, replace system profiles
+        arc = DosMove(szSysNew, Profiles.pszSysName);
+        if (arc == NO_ERROR)
+            arc = DosMove(szUserNew, Profiles.pszUserName);
+    }
+
+    if (    (Profiles.pszSysName)
+         && (Profiles.pszUserName)
+       )
+    {
+        free(Profiles.pszSysName);
+        free(Profiles.pszUserName);
+    }
+
+    return (arc);
+}
+
+// testing
+
+/* BOOL _Optlink fnCallback(ULONG ulUser, ULONG ulNow, ULONG ulMax)
+{
+    printf("\r done %03d%%", ulNow * 100 / ulMax);
+    return (TRUE);
+}
+
+int main(int argc, char* argv[])
+{
+    if (argc != 3)
+        printf("Syntax: xprf2 <source.ini> <target.ini>\n");
+    else
+    {
+        HAB hab = WinInitialize(0);
+
+        // HINI hiniSource = PrfOpenProfile(hab, argv[1]);
+        // if (hiniSource)
+        {
+            APIRET arc = xprfCopyProfile(hiniSource,
+                                         argv[2],
+                                         fnCallback,
+                                         0, 0, 0);
+            xprfSaveProfiles(hab, NULL, 0);
+//             if (arc)
+
+
+   //              printf("xprfCopyProfile returned %d.\n", arc);
+        }
+        // else
+            // printf("Cannot open %s\n", argv[1]);
+
+        WinTerminate(hab);
+    }
+
+    return (0);
+} */
+
Index: /trunk/src/helpers/xstring.c
===================================================================
--- /trunk/src/helpers/xstring.c	(revision 8)
+++ /trunk/src/helpers/xstring.c	(revision 8)
@@ -0,0 +1,434 @@
+
+/*
+ *@@sourcefile xstring.c:
+ *      string functions with memory management.
+ *
+ *      Usage: All OS/2 programs.
+ *
+ *      Function prefixes:
+ *      --  xstr*       extended string functions.
+ *
+ *      The functions in this file used to be in stringh.c
+ *      before V0.9.3 (2000-04-01).
+ *
+ *      Note: Version numbering in this file relates to XWorkplace version
+ *            numbering.
+ *
+ *@@added V0.9.3 (2000-04-01) [umoeller]
+ *@@header "helpers\xstring.h"
+ */
+
+/*
+ *      Copyright (C) 1999-2000 Ulrich Mller.
+ *      This file is part of the XWorkplace source package.
+ *      XWorkplace is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published
+ *      by the Free Software Foundation, in version 2 as it comes in the
+ *      "COPYING" file of the XWorkplace main distribution.
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ */
+
+#define OS2EMX_PLAIN_CHAR
+    // this is needed for "os2emx.h"; if this is defined,
+    // emx will define PSZ as _signed_ char, otherwise
+    // as unsigned char
+
+#include <os2.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "setup.h"                      // code generation and debugging options
+
+#include "helpers\stringh.h"
+#include "helpers\xstring.h"            // extended string helpers
+
+/*
+ *@@category: Helpers\C helpers\String management
+ */
+
+/*
+ *@@ xstrcpy:
+ *      copies pszString to *ppszBuf, for which memory is allocated
+ *      as necessary.
+ *
+ *      If *ppszBuf != NULL, the existing memory is freed.
+ *
+ *      Returns the length of the new string (including the null
+ *      terminator), or null upon errors.
+ *
+ *      Example:
+ +          PSZ psz = NULL;
+ +          xstrcpy(&psz, "blah");
+ *      would have "psz" point to newly allocated buffer containing
+ *      "blah".
+ *
+ *@@changed V0.9.2 (2000-04-01) [umoeller]: renamed from strhxcpy
+ */
+
+ULONG xstrcpy(XSTR ppszBuf,
+              const char *pszString)
+{
+    ULONG   ulrc = 0;
+    if (ppszBuf)
+    {
+        if (*ppszBuf)
+            free(*ppszBuf);
+        ulrc = strlen(pszString) + 1;
+        *ppszBuf = (PSZ)malloc(ulrc);
+        strcpy(*ppszBuf, pszString);
+    }
+    return (ulrc);
+}
+
+#ifdef __XWPMEMDEBUG__ // setup.h, helpers\memdebug.c
+
+/*
+ *@@ xstrcatDebug:
+ *      debug version of xstrcat.
+ *
+ *      stringh.h automatically maps xstrcat to this
+ *      function if __XWPMEMDEBUG__ is defined.
+ *
+ *@@addded V0.9.1 (99-12-20) [umoeller]
+ *@@changed V0.9.2 (2000-04-01) [umoeller]: renamed from strhxcatDebug
+ */
+
+ULONG xstrcatDebug(XSTR ppszBuf,
+                   const char *pszString,
+                   const char *file,
+                   unsigned long line,
+                   const char *function)
+{
+    ULONG   ulrc = 0;
+    if ((ppszBuf) && (pszString))
+    {
+        if (*ppszBuf == NULL)
+            xstrcpy(ppszBuf, pszString);
+        else
+        {
+            ULONG   cbOld = strlen(*ppszBuf),
+                    cbString = strlen(pszString);
+            PSZ     pszOldCopy = strdup(*ppszBuf);
+
+            ulrc = cbOld + cbString + 1;
+            if (*ppszBuf)
+                free(*ppszBuf);
+            *ppszBuf = (PSZ)memdMalloc(ulrc, file, line, function);
+            // copy old string
+            memcpy(*ppszBuf,
+                   pszOldCopy,
+                   cbOld);
+            // append new string
+            memcpy(*ppszBuf + cbOld,
+                   pszString,
+                   cbString + 1);       // include null terminator
+            free(pszOldCopy);       // fixed V0.9.1 (99-12-20) [umoeller]
+        }
+    }
+    return (ulrc);
+}
+
+#else // __XWPMEMDEBUG__
+
+/*
+ *@@ xstrcat:
+ *      appends pszString to *ppszBuf, which is re-allocated as
+ *      necessary.
+ *
+ *      If *ppszBuf is NULL, this behaves just as xstrcpy.
+ *
+ *      Returns the length of the new string (including the null
+ *      terminator), or null upon errors.
+ *
+ *      Example:
+ +          PSZ psz = strdup("blah");
+ +          xstrcat(&psz, "blup");
+ *      would do the following:
+ *      a)  free the old value of psz ("blah");
+ *      b)  reallocate psz;
+ *      c)  so that psz afterwards points to a new string containing
+ *          "blahblup".
+ *
+ *@@changed V0.9.1 (99-12-20) [umoeller]: fixed memory leak
+ *@@changed V0.9.1 (2000-01-03) [umoeller]: crashed if pszString was null; fixed
+ *@@changed V0.9.2 (2000-04-01) [umoeller]: renamed from strhxcat
+ *@@changed V0.9.3 (2000-05-11) [umoeller]: returned 0 if pszString was initially empty; fixed
+ */
+
+ULONG xstrcat(XSTR ppszBuf,
+              const char *pszString)
+{
+    ULONG   ulrc = 0;
+    if ((ppszBuf) && (pszString))
+    {
+        if (*ppszBuf == NULL)
+            ulrc = xstrcpy(ppszBuf, pszString);
+        else
+        {
+            ULONG   cbOld = strlen(*ppszBuf),
+                    cbString = strlen(pszString);
+            PSZ     pszOldCopy = strdup(*ppszBuf);
+
+            ulrc = cbOld + cbString + 1;
+            if (*ppszBuf)
+                free(*ppszBuf);
+            *ppszBuf = (PSZ)malloc(ulrc);
+            // copy old string
+            memcpy(*ppszBuf,
+                   pszOldCopy,
+                   cbOld);
+            // append new string
+            memcpy(*ppszBuf + cbOld,
+                   pszString,
+                   cbString + 1);       // include null terminator
+            free(pszOldCopy);       // fixed V0.9.1 (99-12-20) [umoeller]
+        }
+    }
+    return (ulrc);
+}
+
+#endif // else __XWPMEMDEBUG__
+
+#ifdef __XWPMEMDEBUG__ // setup.h, helpers\memdebug.c
+
+/*
+ *@@ xstrrplDebug:
+ *
+ *@@added V0.9.3 (2000-04-11) [umoeller]
+ */
+
+ULONG xstrrplDebug(PSZ *ppszBuf,     // in/out: text buffer
+                   ULONG ulOfs,      // in: where to begin search (can be 0)
+                   const char *pszSearch,    // in: search string
+                   const char *pszReplace,   // in: replacement string
+                   PULONG pulAfterOfs,  // out: offset where found (can be NULL)
+                   const char *file,
+                   unsigned long line,
+                   const char *function)
+{
+    ULONG    ulrc = 0;
+
+    if ((ppszBuf) && (pszSearch) && (pszReplace))
+    {
+        ULONG   cbBuf = 0,
+                cbSearch = strlen(pszSearch);
+        if (*ppszBuf)                       // fixed V0.9.0 (99-11-08) [umoeller]
+            cbBuf =  strlen(*ppszBuf);
+
+        if ((ulOfs < cbBuf) && (cbSearch))
+        {
+            PSZ     pFound = strstr((*ppszBuf) + ulOfs,
+                                    pszSearch);
+
+            if (pFound)
+            {
+                ULONG   cbReplace = strlen(pszReplace),
+                        // length of new string
+                        cbNew = cbBuf
+                                + cbReplace
+                                - cbSearch
+                                + 1,                  // null terminator
+                        // offset where pszSearch was found
+                        ulFoundOfs = pFound - *ppszBuf;
+
+                // allocate new buffer
+                PSZ     pszNew = (PSZ)memdMalloc(cbNew,
+                                                 file, line, function);
+
+                if (ulFoundOfs)
+                {
+                    // copy until offset
+                    strncpy(pszNew,
+                            *ppszBuf,
+                            ulFoundOfs);
+                }
+
+                if (cbReplace)
+                {
+                    // copy replacement
+                    strncpy(pszNew + ulFoundOfs,
+                            pszReplace,
+                            cbReplace);
+                }
+                // copy rest
+                strcpy(pszNew + ulFoundOfs + cbReplace,
+                       pFound + cbSearch);
+
+                // replace PSZ pointer
+                memdFree(*ppszBuf, file, line, function);
+                *ppszBuf = pszNew;
+
+                // return new length
+                ulrc = cbNew;
+                if (pulAfterOfs)
+                    *pulAfterOfs = ulFoundOfs + cbReplace;
+            }
+        }
+    }
+    return (ulrc);
+}
+
+#else
+
+/*
+ *@@ xstrrpl:
+ *      replaces pszSearch with pszReplace in *ppszBuf.
+ *
+ *      If pszSearch was found, *ppszBuf is
+ *      re-allocated so the buffer cannot overflow. As
+ *      a result, *ppszBuf must be free()'able.
+ *
+ *      Returns the length of the new string or 0 if
+ *      pszSearch was not found (and ppszBuf was therefore
+ *      not changed).
+ *
+ *      If the string was found and (pulAfterOfs != NULL),
+ *      *pulAfterOfs will be set to the first character
+ *      after the new replacement string. This allows you
+ *      to call this func again with the same strings to
+ *      have several occurences replaced.
+ *
+ *      Only the first occurence is replaced. To replace
+ *      all occurences in a buffer, repeat calling this
+ *      function until it returns 0.
+ *
+ *      <B>Example usage:</B>
+ +          PSZ psz = strdup("Test string");
+ +          xstrrpl(&psz, "Test", "Dummy");
+ *
+ *      would reallocate psz to point to a new string
+ *      containing "Dummy string".
+ *
+ *@@changed V0.9.0 [umoeller]: totally rewritten.
+ *@@changed V0.9.0 (99-11-08) [umoeller]: crashed if *ppszBuf was NULL. Fixed.
+ *@@changed V0.9.2 (2000-04-01) [umoeller]: renamed from strhxrpl
+ */
+
+ULONG xstrrpl(PSZ *ppszBuf,     // in/out: text buffer; cannot be NULL
+              ULONG ulOfs,      // in: where to begin search (can be 0)
+              const char *pszSearch,    // in: search string; cannot be NULL
+              const char *pszReplace,   // in: replacement string; cannot be NULL
+              PULONG pulAfterOfs)  // out: offset where found (can be NULL)
+{
+    ULONG    ulrc = 0;
+
+    if ((ppszBuf) && (pszSearch) && (pszReplace))
+    {
+        ULONG   cbBuf = 0,
+                cbSearch = strlen(pszSearch);
+        if (*ppszBuf)                       // fixed V0.9.0 (99-11-08) [umoeller]
+            cbBuf =  strlen(*ppszBuf);
+
+        if ((ulOfs < cbBuf) && (cbSearch))
+        {
+            PSZ     pFound = strstr((*ppszBuf) + ulOfs,
+                                    pszSearch);
+
+            if (pFound)
+            {
+                ULONG   cbReplace = strlen(pszReplace),
+                        // length of new string
+                        cbNew = cbBuf
+                                + cbReplace
+                                - cbSearch
+                                + 1,                  // null terminator
+                        // offset where pszSearch was found
+                        ulFoundOfs = pFound - *ppszBuf;
+
+                // allocate new buffer
+                PSZ     pszNew = (PSZ)malloc(cbNew);
+
+                if (ulFoundOfs)
+                {
+                    // copy until offset
+                    strncpy(pszNew,
+                            *ppszBuf,
+                            ulFoundOfs);
+                }
+
+                if (cbReplace)
+                {
+                    // copy replacement
+                    strncpy(pszNew + ulFoundOfs,
+                            pszReplace,
+                            cbReplace);
+                }
+                // copy rest
+                strcpy(pszNew + ulFoundOfs + cbReplace,
+                       pFound + cbSearch);
+
+                // replace PSZ pointer
+                free(*ppszBuf);
+                *ppszBuf = pszNew;
+
+                // return new length
+                ulrc = cbNew;
+                if (pulAfterOfs)
+                    *pulAfterOfs = ulFoundOfs + cbReplace;
+            }
+        }
+    }
+    return (ulrc);
+}
+
+#endif // else __XWPMEMDEBUG__
+
+/*
+ *@@ xstrins:
+ *      this inserts one string into another.
+ *
+ *      pszInsert is inserted into pszBuffer at offset
+ *      ulInsertOfs (which counts from 0).
+ *
+ *      A newly allocated string is returned. pszBuffer is
+ *      not changed. The new string should be free()'d after
+ *      use.
+ *
+ *      Upon errors, NULL is returned.
+ *
+ *@@changed V0.9.0 [umoeller]: completely rewritten.
+ *@@changed V0.9.2 (2000-04-01) [umoeller]: renamed from strhxins
+ */
+
+PSZ xstrins(PSZ pszBuffer,
+            ULONG ulInsertOfs,
+            const char *pszInsert)
+{
+    PSZ     pszNew = NULL;
+
+    if ((pszBuffer) && (pszInsert))
+    {
+        do {
+            ULONG   cbBuffer = strlen(pszBuffer);
+            ULONG   cbInsert = strlen(pszInsert);
+
+            // check string length
+            if (ulInsertOfs > cbBuffer + 1)
+                break;  // do
+
+            // OK, let's go.
+            pszNew = (PSZ)malloc(cbBuffer + cbInsert + 1);  // additional null terminator
+
+            // copy stuff before pInsertPos
+            memcpy(pszNew,
+                   pszBuffer,
+                   ulInsertOfs);
+            // copy string to be inserted
+            memcpy(pszNew + ulInsertOfs,
+                   pszInsert,
+                   cbInsert);
+            // copy stuff after pInsertPos
+            strcpy(pszNew + ulInsertOfs + cbInsert,
+                   pszBuffer + ulInsertOfs);
+        } while (FALSE);
+    }
+
+    return (pszNew);
+}
+
+
