Logo Search packages:      
Sourcecode: ukopp version File versions  Download package

zfuncs.cc

/**************************************************************************
   zfuncs.cpp   collection of Linux and GDK/GTK utility functions

   Copyright 2006 2007 2008 2009 2010 2011  Michael Cornelison
   source URL:  kornelix.squarespace.com
   contact: kornelix2@googlemail.com
   
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see <http://www.gnu.org/licenses/>.

***************************************************************************/

//     zfuncs.cpp   version  v.4.7

#include "zfuncs.h"


/**************************************************************************
   system-level utility functions
***************************************************************************/

//  Output a message to stdout and wait for user ACK.
//  Works like printf.

void apppause(cchar *pMess, ... )
{
   va_list  arglist;
   char     message[200];

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);

   printf("pause: %s \n",message);
   printf("*** press return to continue: ");
   getchar();
   return;
}

void apppause()
{
   printf("*** pause, press return to continue: ");
   getchar();
   return;
}


/**************************************************************************/

//  crash with error message and traceback dump in popup window
//  works like printf

void zappcrash(cchar *pMess, ... )                                         //  v.3.8
{
   va_list     arglist;
   FILE        *fid1, *fid2;
   int         ii, cc, err, nstack = 50;
   char        message[200];
   void        *stacklist[50];
   char        **stackents;
   char        progexe[300], command[300], buff[300], hexaddr[20];
   char        *pp1, *pp2, *plim, *pfunc;

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);

   printf("zappcrash: \n %s \n",message);                                  //  output message to stdout

   nstack = backtrace(stacklist,nstack);                                   //  get traceback data
   stackents = backtrace_symbols(stacklist,nstack);

   for (ii = 0; ii < nstack; ii++)                                         //  output backtrace to stdout
      printf(" %s \n",stackents[ii]);

   fid1 = fopen("zappcrash","w");                                          //  text file for backtrace

   fprintf(fid1,"zappcrash: \n %s \n",message);                            //  output message to text file
   
   cc = readlink("/proc/self/exe",progexe,300);                            //  get own program path
   if (cc <= 0) {
      printf("cannot get /proc/self/exe \n");                              //  v.4.6
      abort();
   }
   progexe[cc] = 0;

   for (ii = 0; ii < nstack; ii++)                                         //  output backtrace to text file
   {
      pfunc = 0;
      plim = stackents[ii] + strlen(stackents[ii]);
      pp1 = strstr(stackents[ii],"[0x");                                   //  look for hex address [0x.....]
      if (pp1) pp2 = strchr(pp1,']');
      if (pp1 && pp2 && pp2 < plim) {
         *pp2 = 0;
         strncpy0(hexaddr,pp1+1,20);
         snprintf(command,300,"addr2line -e %s %s",progexe,hexaddr);       //  convert to source program    v.3.8
         fid2 = popen(command,"r");                                        //    and line number
         pfunc = fgets(buff,300,fid2);
         pclose(fid2);
         if (pfunc) pfunc = strrchr(pfunc,'/');                            //  remove directories 
         if (pfunc) pfunc++;
      }      

      fprintf(fid1," %s %s \n",stackents[ii],pfunc);                       //  write to text file
   }

   fclose(fid1);
   
   err = system("xdg-open zappcrash");                                     //  display in editor
   if (err) printf("xdg-open failure \n");
   abort();
}


/**************************************************************************/

//  application initialization function to catch segfaults

void catch_signals()                                                       //  v.2.22
{
   void sighandler(int signal);
   struct sigaction  sigact;

   sigact.sa_handler = sighandler;
   sigemptyset(&sigact.sa_mask);
   sigact.sa_flags = 0;
   sigaction(SIGSEGV,&sigact,0);
   return;
}


//  catch segfaults and produce backtrace dumps on-screen

void sighandler(int signal)
{
   zappcrash("segment fault");
   return;
}


/**************************************************************************/

//  This function will restart the current program with root privileges,
//  if the correct (sudo) password is given. It does not return.

void beroot(int argc, char *argv[])                                        //  v.3.8
{
   int      cc1, cc2, ii, err;
   char     command[1000];

   strcpy(command,"which gksu > /dev/null 2>&1");                          //  Debian
   err = system(command);
   strcpy(command,"gksu ");
   if (err) {
      strcpy(command,"which beesu > /dev/null 2>&1");                      //  Fedora, just to be different
      err = system(command);                                               //  v.4.7
      strcpy(command,"beesu ");
   }
   if (err) {
      printf("cannot find gksu or beesu \n");
      abort();
   }

   cc1 = strlen(command);                                                  //  gksu  (or)  beesu

   cc2 = readlink("/proc/self/exe",command+cc1,990);
   if (cc2 <= 0) {
      printf("cannot get /proc/self/exe \n");                              //  v.4.6
      abort();
   }
   command[cc1+cc2] = 0;                                                   //  gksu or beesu <my-program>

   for (ii = 0; ii < argc; ii++)                                           //  append command line parameters
      strncatv(command,990," ",argv[ii],null);
   
   strcat(command," &");                                                   //  return immediately

   printf("%s \n",command);
   err = system(command);
   exit(0);
}


/**************************************************************************/

//  get time in real seconds (since 2000.01.01 00:00:00)                   //  v.2.12

double get_seconds()
{
   timeval  time1;

   gettimeofday(&time1,0);
   return  time1.tv_sec + 0.000001 * time1.tv_usec - 946684800.0;
}   


/**************************************************************************/

//  start a timer or get elapsed time with millisecond resolution.

void start_timer(double &time0)                                            //  double  v.2.20
{
   timeval  timev;

   gettimeofday(&timev,0);
   time0 = timev.tv_sec + 0.000001 * timev.tv_usec;
   return;
}

double get_timer(double &time0)
{
   timeval  timev;
   double   time;

   gettimeofday(&timev,0);
   time = timev.tv_sec + 0.000001 * timev.tv_usec;
   return time - time0;
}


/**************************************************************************/

//  start a process CPU timer or get elapsed process CPU time
//  returns seconds with millisecond resolution

void start_CPUtimer(double &time0)                                         //  double  v.2.20
{
   time0 = CPUtime();
   return;
}

double get_CPUtimer(double &time0)
{
   return CPUtime() - time0;
}


/**************************************************************************/

//  get elapsed CPU time used by current process
//  returns seconds with millisecond resolution

double CPUtime()
{
   clock_t ctime = clock();
   double dtime = ctime / 1000000.0;
   return dtime;
}


/**************************************************************************/

//  Get elapsed CPU time used by current process, including all threads.
//  Returns seconds with millisecond resolution.

double CPUtime2()                                                          //  new v.4.7
{
   struct rusage  usage;
   double         utime, stime;
   int            err;
   
   err = getrusage(RUSAGE_SELF,&usage);
   if (err) return 0.0;
   utime = usage.ru_utime.tv_sec + 0.000001 * usage.ru_utime.tv_usec;
   stime = usage.ru_stime.tv_sec + 0.000001 * usage.ru_stime.tv_usec;
   return utime + stime;
}


/**************************************************************************/

//  get elapsed process time for my process, 
//  including threads and child processes.

double jobtime()                                                           //  new v.4.7
{
   double   jiffy = 1.0 / sysconf(_SC_CLK_TCK);                            //  "jiffy" time slice = 1.0 / HZ
   char     buff[200];
   double   cpu1, cpu2, cpu3, cpu4;
   FILE     *fid;
   char     *pp;

   fid = fopen("/proc/self/stat","r");
   if (! fid) return 0;
   pp = fgets(buff,200,fid);
   fclose(fid);
   if (! pp) return 0;
   
   parseprocrec(pp,14,&cpu1,15,&cpu2,16,&cpu3,17,&cpu4,null);
   return (cpu1 + cpu2 + cpu3 + cpu4) * jiffy;
}


/**************************************************************************/

//  Read and parse /proc file with records formatted "parmname xxxxxxx"
//  Find all requested parameters and return their numeric values

int parseprocfile(cchar *pfile, cchar *pname, double *value, ...)          //  EOL = 0
{
   FILE        *fid;
   va_list     arglist;
   char        buff[1000];
   const char  *pnames[20];
   double      *values[20];
   int         ii, fcc, wanted, found;
   
   pnames[0] = pname;                                                      //  1st parameter
   values[0] = value;
   *value = 0;
   
   va_start(arglist,value);

   for (ii = 1; ii < 20; ii++)                                             //  get all parameters
   {
      pnames[ii] = va_arg(arglist,char *);
      if (! pnames[ii]) break;
      values[ii] = va_arg(arglist,double *);
      *values[ii] = 0;                                                     //  initialize to zero
   }
      
   va_end(arglist);

   if (ii == 20) zappcrash("parseProcFile, too many fields");
   wanted = ii;
   found = 0;

   fid = fopen(pfile,"r");                                                 //  open /proc/xxx file
   if (! fid) return 0;

   while ((fgets(buff,999,fid)))                                           //  read record, "parmname nnnnn"
   {
      for (ii = 0; ii < wanted; ii++)
      {                                                                    //  look for my fields
         fcc = strlen(pnames[ii]);
         if (strnEqu(buff,pnames[ii],fcc)) {
            *values[ii] = atof(buff+fcc);                                  //  return value
            found++;
            break;
         }
      }

      if (found == wanted) break;                                          //  stop when all found
   }

   fclose(fid);
   return found;
}


//  Parse /proc record of the type  "xxx xxxxx xxxxx xxxxxxxx xxx"
//  Return numeric values for requested fields (starting with 1)

int parseprocrec(char *prec, int field, double *value, ...)                //  EOL = 0
{
   va_list     arglist;
   int         xfield = 1, found = 0;
   
   va_start(arglist,value);

   while (*prec == ' ') prec++;                                            //  skip leading blanks

   while (field > 0)
   {
      while (xfield < field)                                               //  skip to next wanted field
      {
         prec = strchr(prec,' ');                                          //  find next blank
         if (! prec) break;
         while (*prec == ' ') prec++;                                      //  skip multiple blanks
         xfield++;
      }
      
      if (! prec) break;
      *value = atof(prec);                                                 //  convert, return double 
      found++;

      field = va_arg(arglist,int);                                         //  next field number
      value = va_arg(arglist,double *);                                    //  next output double *
   }
   
   while (field > 0)
   {
      *value = 0;                                                          //  zero values not found
      field = va_arg(arglist,int);
      value = va_arg(arglist,double *);
   }
   
   va_end(arglist);
   return found;
}


/**************************************************************************/

//  start a detached thread using a simplified protocol                    //  v.2.11

void start_detached_thread(void * threadfunc(void *), void * arg)
{
   pthread_t         ptid;
   pthread_attr_t    ptattr;
   int               pterr;

   pthread_attr_init(&ptattr);
   pthread_attr_setdetachstate(&ptattr,PTHREAD_CREATE_DETACHED);
   pterr = pthread_create(&ptid,&ptattr,threadfunc,arg);
   if (pterr) zappcrash("start_detached_thread() failure");
   return;
}


/**************************************************************************/

//  Synchronize execution of multiple threads.
//  Simultaneously resume NT calling threads.
//  from main():        synch_threads(NT)    /* setup to synch NT threads */
//  from each thread:   synch_threads()      /* suspend, resume simultaneously */

void synch_threads(int NT)
{
   static pthread_barrier_t   barrier;
   static int                 bflag = 0;

   if (NT) {                                                               //  main(), initialize
      if (bflag) pthread_barrier_destroy(&barrier);
      pthread_barrier_init(&barrier,null,NT);
      bflag = 1;
      return;
   }

   pthread_barrier_wait(&barrier);                                         //  thread(), wait for NT threads
   return;                                                                 //  unblock
}


/**************************************************************************/

//  safely access parameters from multiple threads
//  limitation: one lock for any number of parameters

mutex    zget_lock = PTHREAD_MUTEX_INITIALIZER;

int zget_locked(int &param)                                                //  lock and return parameter
{
   mutex_lock(&zget_lock);
   return param;
}

void zput_locked(int &param, int value)                                    //  set and unlock parameter
{
   param = value;
   mutex_unlock(&zget_lock);
   return;
}

int zadd_locked(int &param, int incr)                                      //  lock, increment, unlock, return
{
   int      retval;

   mutex_lock(&zget_lock);
   retval = param + incr;
   param = retval;
   mutex_unlock(&zget_lock);
   return retval;
}


/**************************************************************************/

//  Allow only one thread at a time through a function that is otherwise 
//  not thread-safe. Function must call these functions at entry and exit.

mutex   gate_threads_mutex = PTHREAD_MUTEX_INITIALIZER;

void gate_threads_enter()
{
   mutex_lock(&gate_threads_mutex);
   return;
}

void gate_threads_leave()
{
   mutex_unlock(&gate_threads_mutex);
   return;
}


/**************************************************************************/

//  sleep for specified time in seconds (double)
//  signals can cause early return

void zsleep(double dsecs)
{
   unsigned    isecs, nsecs;
   timespec    tsecs;

   if (dsecs == 0.0) return;   
   isecs = unsigned(dsecs);
   nsecs = unsigned(1000000000.0 * (dsecs - isecs));
   tsecs.tv_sec = isecs;
   tsecs.tv_nsec = nsecs;
   nanosleep(&tsecs,null);
   return;
}


/**************************************************************************/

//  malloc() and free() wrappers with auto crash on failure and log option.
//  overflow sentinel is placed after end of allocated memory
//  zmalloc() and zfree() calls are logged if log count > 0
//  benchmark zmalloc() + zfree(): average 0.22 microseconds on 2.67 GHz CPU

#define  zmalloc_add 32
uint     zmalloc_tot = 0;
int      zmalloc_logcount = 0;                                             //  set to +N to log next N calls

void  zmalloc_tabulate(cchar *tag, int bytes);                             //  private function

char * zmalloc(size_t bytes, cchar *tag)                                   //  optional tag, v.3.5
{
   void        *maddr = malloc(bytes + zmalloc_add);
   if (! maddr) zappcrash("Memory request for %zu bytes failed. \n"
                          "Application will now terminate.",bytes);

   unsigned    *pbytes = (unsigned *) maddr;                //  0..3          caller bytes
   char        *psen1 = (char *) maddr + 4;                 //  4..7          sentinel "sen1"
   char        *ptag = (char *) maddr + 8;                  //  8..27         tag, < 20 chars.
   char        *puser =  (char *) maddr + 28;               //  28..B+27      user data, B chars.
   char        *psen2 =  (char *) puser + bytes;            //  B+28..B+31    sentinel "sen2"

   if (bytes <= 0) zappcrash("zmalloc bytes %d",bytes);
   *pbytes = bytes;
   strncpy(psen1,"sen1",4);
   if (! tag) tag = "notag";
   strncpy0(ptag,tag,20);
   memset(puser,0,bytes);                                                  //  clear user space to zeros
   strncpy(psen2,"sen2",4);
   zmalloc_tot += bytes;
   
   zmalloc_tabulate(ptag,bytes);                                           //  track usage by tag   v.3.7

   if (zmalloc_logcount) {
      printf("zmalloc %-20s loc: %p  bytes: %zu  total: %u \n",ptag,puser,bytes,zmalloc_tot);
      zmalloc_logcount--;
   }

   return puser;
}


//  free memory allocated by zmalloc(). checks for overflow.

void zfree(void *puser)                                                    //  v.3.5
{
   if (! puser) zappcrash("zfree: null address");

   void        *maddr = (char *) puser - 28;
   unsigned    *pbytes = (unsigned *) maddr;
   char        *psen1 = (char *) maddr + 4;
   char        *ptag = (char *) maddr + 8;
   size_t      bytes = *pbytes;
   char        *psen2 = (char *) puser + bytes;

   if (strncmp("sen1",psen1,4))                                            //  check sentinels
      zappcrash("zfree: invalid address %p",puser);
   if (strncmp("sen2",psen2,4)) 
      zappcrash("zfree: buffer overflow |%s|%s|",ptag,puser);              //  v.3.9
   *psen1 = *psen2 = 0;                                                    //  destroy sentinels      v.3.8
   
   zmalloc_tot -= bytes;

   zmalloc_tabulate(ptag,-bytes);                                          //  track usage by tag     v.3.7

   if (zmalloc_logcount) {
      printf("zfree   %-20s loc: %p  bytes: %zu  total: %u \n",ptag,puser,bytes,zmalloc_tot);
      zmalloc_logcount--;
   }

   free(maddr);
   return;
}


//  turn logging flag on for specified log count, or off if count = 0;

void zmalloc_log(int count)                                                //  v.3.5
{
   zmalloc_logcount = count;
   return;
}


//  private function. track how much memory is in use, per tag.
//  real tag capacity is about 80% of nominal 1000

HashTab     *zmalloc_hashtab = 0;
uint        zmalloc_count[1000], zmalloc_bytes[1000];

void zmalloc_tabulate(cchar *ptag, int bytes)                              //  v.3.7
{
   int      ii;

   if (! zmalloc_hashtab) {
      zmalloc_hashtab = new HashTab(20,1000);
      memset(zmalloc_count, 0, 1000 * sizeof(uint));
      memset(zmalloc_bytes, 0, 1000 * sizeof(uint));
   }
   
   ii = zmalloc_hashtab->Find(ptag);
   if (ii < 0) ii = zmalloc_hashtab->Add(ptag);
   if (ii < 0) zappcrash("zmalloc hash table full");

   zmalloc_bytes[ii] += bytes;
   if (bytes > 0) ++zmalloc_count[ii];                                     //  v.3.8
   else  --zmalloc_count[ii];

   return;
}


//  report total memory allocated per tag - leak detection utility

void zmalloc_report()                                                      //  v.3.5
{
   int      count, ii, first = 1;
   uint     bytes;
   char     tag[20];

   printf("\n zmalloc total memory: %u \n",zmalloc_tot);
   
   while (true)
   {
      ii = zmalloc_hashtab->GetNext(first,tag);
      if (ii < 0) break;
      ii = zmalloc_hashtab->Find(tag);
      if (ii < 0) zappcrash("zmalloc hash table bug: %s",tag);
      bytes = zmalloc_bytes[ii];
      count = zmalloc_count[ii];                                           //  added, v.3.8
      if (bytes) printf("  %-20s  %8d  %d \n",tag,count,bytes);
   }
   
   return;
}


/**************************************************************************/

//  Run a shell command and get its outputs one record at a time.
//  Start a new command with contx = 0. Do not change contx.
//  NULL return means no more output.
//  Get command exit status: err = command_status(contx)
//  Caller owns returned strings which are candidates for zfree()
//
//  strcat(buff," 2>&1");   //  combine stdout and stderr  v.2.14 removed


FILE *   CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 };
int      CO_status[10];

char * command_output(int &contx, cchar *command, ...)                     //  simplify, allow parallel usage
{                                                                          //       v.2.3
   FILE           *fid;
   va_list        arglist;
   char           buff[1000], *prec;
   
   if (contx == 0)                                                         //  start new command
   {
      for (contx = 1; contx < 10; contx++) 
         if (CO_contx[contx] == 0) break;
      if (contx == 10) zappcrash("command_output(), parallel usage > 9");
      
      va_start(arglist,command);                                           //  format command
      vsnprintf(buff,999,command,arglist);
      va_end(arglist);
      
      fid = popen(buff,"r");                                               //  execute command, output to FID
      if (fid == 0) {
         CO_status[contx] = errno;                                         //  failed to start
         return 0;
      }
      CO_contx[contx] = fid + 1000;
      CO_status[contx] = -1;                                               //  mark context busy
   }

   fid = CO_contx[contx] - 1000;
   prec = fgets_trim(buff,999,fid,1);                                      //  next output, less trailing \n
   if (prec) return strdupz(prec,0,"command_output");                      //  return output to caller      v.3.5

   CO_status[contx] = pclose(fid);                                         //  EOF, set status
   CO_contx[contx] = 0;                                                    //  mark context free
   return 0;
}

int command_status(int contx)                                              //  get command exit status
{
   int err = CO_status[contx];
   return WEXITSTATUS(err);                                                //  special BS for subprocess   v.2.3
}


/**************************************************************************/

//  signal a subprocess to pause, resume, or terminate
//  return:  0: OK  +-N: error

int signalProc(cchar *pname, cchar *signal)
{
   pid_t       pid;
   FILE        *fid;
   char        buff[100], *pp;
   int         err, nsignal = 0;

   sprintf(buff,"ps -C %s h o pid",pname);
   fid = popen(buff,"r");                                                  //  popen() instead of system()    v.3.0
   if (! fid) return 2;
   pp = fgets(buff,100,fid);
   pclose(fid);
   if (! pp) return 4;

   pid = atoi(buff);
   if (! pid) return 5;

   if (strEqu(signal,"pause")) nsignal = SIGSTOP; 
   if (strEqu(signal,"resume")) nsignal = SIGCONT; 
   if (strEqu(signal,"kill")) nsignal = SIGKILL; 

   err = kill(pid,nsignal);   
   return err;
}


/**************************************************************************/

//  run a command or program as root user
//  sucomm:  root user access command, "su" or "sudo"
//  command:  shell command or filespec of the program to start
//  returns 0 if successfully started, else returns an error code

int runroot(cchar *sucomm, cchar *command)                                 //  v.2.10
{
   char     xtcommand[500];
   int      err;
   
   if (strcmp(sucomm,"sudo") == 0)
   {
      snprintf(xtcommand,499,"xterm -geometry 40x3 -e sudo -S %s",command);
      err = system(xtcommand);
      return err;
   }
   
   if (strcmp(sucomm,"su") == 0)
   {
      snprintf(xtcommand,499,"xterm -geometry 40x3 -e su -c %s",command);
      err = system(xtcommand);
      return err;
   }

   return -1;
}


/**************************************************************************/

//  Check if a list of programs are all installed
//  If any are missing, pop-up a window of missing programs
//  Returns the number of missing programs (zero if none).

int checkinstall(cchar *prog1, ...)  //  null terminated list              //  v.2.10
{
   va_list        arglist;
   char           *buff, errmessage[200] = "missing programs:\n";
   cchar          *prog, *pp, *missprogs[20];
   int            contx, found, Nmiss = 0;

   va_start(arglist,prog1);
   prog = prog1;

   while (prog)
   {
      contx = 0;
      found = 0;
      
      while (true)
      {
         buff = command_output(contx,"whereis %s",prog);
         if (! buff) break;
         pp = strchr(buff,':');
         if (pp) pp = strchr(pp,'/');
         if (pp) found = 1;
         zfree(buff);
         continue;
      }

      if (! found) {
         if (Nmiss == 20) break;
         missprogs[Nmiss] = prog;
         Nmiss++;
      }

      prog = va_arg(arglist,cchar *);
   }
   
   va_end(arglist);
   
   if (Nmiss) {
      for (int ii = 0; ii < Nmiss; ii++)
         strncatv(errmessage,199,missprogs[ii]," ",null);
      zmessageACK(null,errmessage);
   }

   return Nmiss;
}


/**************************************************************************/

//  fgets() with additional feature: trailing \n \r are removed.
//  optional bf flag: true if trailing blanks are to be removed.
//  trailing null character is assured.

char * fgets_trim(char *buff, int maxcc, FILE *fid, int bf)
{
   int      cc;
   char     *pp;
   
   pp = fgets(buff,maxcc,fid);
   if (! pp) return pp;
   cc = strlen(buff);
   if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc;         //  utf8  v.2.4
   else    while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc;
   buff[cc] = 0;
   return pp;
}


/**************************************************************************/

//  return true if both files are in the same directory
//  both files may be files or directories

int samedirk(cchar *file1, cchar *file2)
{
   int         cc1, cc2;
   cchar       *pp1, *pp2;                                                 //  v.2.15
   
   if (! file1 || ! file2) return 0;
   pp1 = strrchr(file1,'/');
   pp2 = strrchr(file2,'/');
   if (! pp1 && ! pp2) return 1;
   if (pp1 && ! pp2) return 0;
   if (! pp1 && pp2) return 0;
   cc1 = pp1 - file1;
   cc2 = pp2 - file2;
   if (cc1 != cc2) return 0;
   if (strncmp(file1,file2,cc1) == 0) return 1;
   return 0;
}


/**************************************************************************

   Parse a pathname (filespec) and return its components.
   Returned strings are allocated in static memory (no zfree needed).
   Missing components are returned as null pointers.
   
   input ppath         outputs

   /name1/name2/       directory /name1/name2/ with no file
   /name1/name2        directory /name1/name2/ if name2 a directory,
                       otherwise directory /name1/ and file name2
   /name1/name2.xxx    if .xxx < 8 chars, returns file name2 and ext .xxx,
                       otherwise returns file name2.xxx and no ext

   returns 0 if no error, else 1
   
***************************************************************************/

int parsefile(cchar *ppath, char **pdirk, char **pfile, char **pext)       //  v.2.15
{
   struct stat    statb;
   static char    dirk[1000], file[200], ext[8];
   char           *pp;
   int            err, cc1, cc2;

   *pdirk = *pfile = *pext = null;
   
   cc1 = strlen(ppath);
   if (cc1 > 999) return 1;                                                //  ppath too long

   strcpy(dirk,ppath);
   *pdirk = dirk;
   
   err = stat(dirk,&statb);                                                //  have directory only
   if (! err && S_ISDIR(statb.st_mode)) return 0;
   
   pp = (char *) strrchr(dirk,'/');
   if (! pp) return 1;                                                     //  illegal

   pp++;
   cc2 = pp - dirk;
   if (cc2 < 2 || cc2 == cc1) return 0;                                    //  have /xxxx  or  /xxxx/
   
   if (strlen(pp) > 199) return 1;                                         //  filename too long

   strcpy(file,pp);                                                        //  file part
   *pfile = file;
   *pp = 0;                                                                //  remove from dirk part

   pp = (char *) strrchr(file,'.');
   if (! pp || strlen(pp) > 7) return 0;                                   //  file part, no .ext
   
   strcpy(ext,pp);                                                         //  .ext part
   *pext = ext;
   *pp = 0;                                                                //  remove from file part
   return 0;
}


/**************************************************************************

   utility to measure CPU time spent in various functions or code blocks        v.2.7
   
   cpu_profile_init()            initialize at start of test
   cpu_profile_report()          report CPU time per function
   cpu_profile_enter(fnum)       at entry to a function         //  inline, defined in zfuncs.h
   cpu_profile_exit(fnum)        at exit from a function        //  inline, defined in zfuncs.h

***************************************************************************/

volatile double   cpu_profile_table[100];
volatile double   cpu_profile_timer;
volatile double   cpu_profile_elapsed;
volatile int      cpu_profile_kill = 0;

void cpu_profile_init()
{
   void *   cpu_profile_timekeeper(void *);

   for (int ii = 0; ii < 99; ii++) 
      cpu_profile_table[ii] = 0;
   cpu_profile_elapsed = 0;
   start_detached_thread(cpu_profile_timekeeper,null);
}

void cpu_profile_report()
{
   cpu_profile_kill++;

   printf("elapsed: %.2f \n",cpu_profile_elapsed);

   for (int ii = 0; ii < 100; ii++)
   {
      double dtime = cpu_profile_table[ii];
      if (dtime) printf("cpu profile func: %d  time: %.2f \n",ii,dtime);
   }
}

void * cpu_profile_timekeeper(void *)
{
   timeval  time0, time1;

   gettimeofday(&time0,0);

   while (true)
   {
      gettimeofday(&time1,0);
      cpu_profile_elapsed = time1.tv_sec - time0.tv_sec
              + 0.000001 * (time1.tv_usec - time0.tv_usec);
      zsleep(0.001);
      if (cpu_profile_kill) break;
   }
   
   cpu_profile_kill = 0;
   return 0;
}


/**************************************************************************

    strField()

    cchar * strField(cchar *string, cchar *delim, int Nth)

    Get the Nth field in input string, which contains at least N fields 
    delimited by the character(s) in delim (e.g. blank, comma).
    
    Returns a pointer to the found field (actually a pointer to a
    copy of the found field, with a null terminator appended).
    
    If a delimiter is immediately followed by another delimiter, it is 
    considered a field with zero length, and the string "" is returned.

    Leading blanks in a field are omitted from the returned field.
    A field with only blanks is returned as a single blank.

    The last field may be terminated by null or a delimiter.
    
    Characters within quotes (") are treated as data within a field, 
    i.e. blanks and delimiters are not processed as such.
    The quotes are removed from the returned field.

    If there are less than N fields, a null pointer is returned.
    
    The last 100 fields are saved and recycled in a circular manner.
    The caller does not have to free memory. If more memory depth is 
    needed, caller must copy the returned data elsewhere.
    
    The input string must be < 1000 characters.
    The output string may be modified if the length is not increased.
    
    Example: input string: ,a,bb,  cc,   ,dd"ee,ff"ggg,
             (first and last characters are comma)
             delimiter: comma
             Nth   returned string
              1:   (null string)
              2:   a
              3:   bb
              4:   cc
              5:   (one blank)
              6:   ddee,ffggg
              7:   (null pointer >> no more fields)

***************************************************************************/

cchar * strField(cchar *string, cchar *delim, int Nth)
{
   static int     ftf = 1, nret = 0;
   static char    *retf[100]; 
   char           *pf1, pf2[1000];
   cchar          quote = '"';
   int            ii, nf, fcc = 0;
   static char    blankstring[2], nullstring[1];
   
   gate_threads_enter();                                                   //  allow one thread at a time   v.4.7
   
   if (ftf)                                                                //  overall first call
   {
      ftf = 0;
      for (ii = 0; ii < 100; ii++) retf[ii] = 0;
      strcpy(blankstring," ");
      *nullstring = 0;
   }

   if (strlen(string) > 999) { gate_threads_leave(); return 0; }
   if (Nth < 1) { gate_threads_leave(); return 0; }

   pf1 = (char *) string - 1;                                              //  start parse
   nf = 0;
   
   while (nf < Nth)
   {
      pf1++;                                                               //  start field
      nf++;
      fcc = 0;

      while (*pf1 == ' ') pf1++;                                           //  skip leading blanks

      while (true)
      {
         if (*pf1 == quote) {                                              //  pass chars between single quotes
            pf1++;                                                         //  (but without the quotes)
            while (*pf1 && *pf1 != quote) pf2[fcc++] = *pf1++;
            if (*pf1 == quote) pf1++;
         }

         else if (strchr(delim,*pf1) || *pf1 == 0) break;                  //  found delimiter or null
         
         else pf2[fcc++] = *pf1++;                                         //  pass normal character
      }

      if (*pf1 == 0) break;
   }
      
   if (nf < Nth) { gate_threads_leave(); return 0; }                       //  no Nth field
   
   if (fcc == 0) {                                                         //  empty field
      if (*string && pf1[-1] == ' ' && !strchr(delim,' '))                 //  all blanks and blank not delim.
         { gate_threads_leave(); return blankstring; }                     //     return one blank
      if (*pf1 == 0) { gate_threads_leave(); return 0; }                   //  no field
      gate_threads_leave(); return nullstring;                             //  return null string
   }

   if (++nret == 100) nret = 0;                                            //  use next return slot
   if (retf[nret]) zfree(retf[nret]);
   retf[nret] = zmalloc(fcc+2,"strField");
   strncpy0(retf[nret],pf2,fcc+1);
   gate_threads_leave(); return retf[nret];
}

cchar * strField(cchar *string, cchar delim, int Nth)                      //  alternative with one delimiter
{
   char     delims[2] = "x";
   
   *delims = delim;
   return strField(string,delims,Nth);
}


/**************************************************************************

   stat = strParms(begin, input, pname, maxcc, pval)

   Parse an input string with parameter names and values:
     "pname1=pval1 | pname2 | pname3=pval3 | pname4 ..."
   
   begin    int &          must be 1 to start new string, is modified
   input    cchar *   input string
   pname    char *         output parameter name
   maxcc    int            max. length for pname, including null
   pval     double &       output parameter value
   stat     int            status: 0=OK, -1=EOL, 1=parse error
   
   Each call returns the next pname and pval.
   A pname with no pval is assigned a value of 1 (present).
   Input format:  pname1 | pname2=pval2 | pname3 ... null
   Leading blanks are ignored, and pnames may have imbedded blanks.
   pvals must convert to double using convSD (accepts decimal point or comma)

***/

int strParms(int &begin, cchar *input, char *pname, int maxcc, double &pval)
{
   static int     ii, beginx = 3579246;
   cchar          *pnamex, *delim;
   int            cc, err;

   if (begin == 1) {                                                       //  start new string
      begin = ++beginx;
      ii = 0;
   }

   if (begin != beginx) zappcrash("strParms call error");                  //  thread safe, not reentrant
   
   *pname = 0;                                                             //  initz. outputs to nothing
   pval = 0;
   
   while (input[ii] == ' ') ii++;                                          //  skip leading blanks
   if (input[ii] == 0) return -1;                                          //  no more data

   pnamex = input + ii;                                                    //  next pname
   
   for (cc = 0; ; cc++)
   {                                                                       //  look for delimiter
      if (pnamex[cc] == '=') break;
      if (pnamex[cc] == '|') break;
      if (pnamex[cc] == 0) break;
   }
   
   if (cc == 0) return 1;                                                  //  err: 2 delimiters
   if (cc >= maxcc) return 1;                                              //  err: pname too big

   strncpy0(pname,pnamex,cc+1);                                            //  pname >> caller
   strTrim(pname);                                                         //  remove trailing blanks

   if (pnamex[cc] == 0) {                                                  //  pname + null
      ii += cc;                                                            //  position for next call
      pval = 1.0;                                                          //  pval = 1 >> caller
      return 0;
   }

   if (pnamex[cc] == '|') {                                                //  pname + |
      ii += cc + 1;                                                        //  position for next call
      pval = 1.0;                                                          //  pval = 1 >> caller
      return 0;
   }

   ii += cc + 1;                                                           //  pname = pval
   err = convSD(input + ii, pval, &delim);                                 //  parse pval   (was strtod()
   if (err > 1) return 1;
   while (*delim == ' ') delim++;                                          //  skip poss. trailing blanks
   if (*delim && *delim != '|') return 1;                                  //  err: delimiter not | or null
   ii = delim - input;
   if (*delim) ii++;                                                       //  position for next call
   return 0;
}


/**************************************************************************/

//  Produce random value from hashed input string.
//  Output range is 0 to max-1.
//  rewritten v.3.7: better for "nearly equal" strings

int strHash(cchar *string, int max)                                        //  v.3.7
{
   uint     hash = 1;
   uchar    byte;

   while ((byte = *string++))
   {
      hash *= byte;
      hash = hash ^ (hash >> 7);
      hash = hash & 0x00FFFFFF;
   }

   hash = hash % max;
   return hash;
}


/**************************************************************************/

//  Hash an input string into a random printable (a-z) output string.
//  Returns outcc character random printable string in static memory.
//  Every output character is randomized from the entire input string.

cchar * strHash2(cchar *instring, int outcc)
{
   int            incc, ii, jj, rani = 0;
   int64          seed = 13579;
   static char    outstring[40];

   incc = strlen(instring);
   if (outcc > 39) zappcrash("strHash2() outcc > 39");
   
   for (ii = 0; ii < outcc; ii++)
   {
      for (jj = 0; jj < incc; jj++)
      {
         seed = seed + instring[jj];
         rani = lrandz(&seed);
      }
      outstring[ii] = 'a' + rani % 26;
   }

   outstring[ii] = 0;
   return outstring;
}


/**************************************************************************/

//  Copy string with specified max. length (including null terminator).
//  truncate if needed. null terminator is always supplied.

int strncpy0(char *dest, cchar *source, uint cc)
{
   strncpy(dest,source,cc);
   dest[cc-1] = 0;
   if (strlen(source) >= cc) return 1;                                     //  truncated  v.2.4
   else return 0;
}


/**************************************************************************/

//  Copy string with blank pad to specified length.  No null.

void strnPad(char *dest, cchar *source, int cc)
{
   strncpy(dest,source,cc);
   int ii = strlen(source);
   for (int jj = ii; jj < cc; jj++) dest[jj] = ' ';
}


/**************************************************************************/

//  Remove trailing blanks from a string. Returns remaining length.

int strTrim(char *dest, cchar *source)
{
   if (dest != source) strcpy(dest,source);
   return strTrim(dest);
}

int strTrim(char *dest)
{
   int  ii = strlen(dest);
   while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0;
   return ii;
}


/**************************************************************************/

//  Remove leading and trailing blanks from a string. 
//  Returns remaining length, possibly zero.

int strTrim2(char *dest, cchar *source)                                    //  v.2.4
{
   cchar       *pp1, *pp2;
   int         cc;

   pp1 = source;
   pp2 = source + strlen(source) - 1;
   while (*pp1 == ' ') pp1++;
   while (*pp2 == ' ' && pp2 > pp1) pp2--;
   cc = pp2 - pp1 + 1;
   memmove(dest,pp1,cc);                                                   //  allow overlap   v.3.7
   dest[cc] = 0;
   return cc;
}

int strTrim2(char *string)                                                 //  v.3.7
{
   return strTrim2(string,(cchar *) string);
}


/**************************************************************************/

//  Remove all blanks from a string. Returns remaining length.

int strCompress(char *dest, cchar *source)
{
   if (dest != source) strcpy(dest,source);
   return strCompress(dest);
}

int strCompress(char *string)
{
   int   ii, jj;

   for (ii = jj = 0; string[ii]; ii++)
   {
      if (string[ii] != ' ')
      {
         string[jj] = string[ii];
         jj++;
      }
   }
   string[jj] = 0;
   return jj;
}


/**************************************************************************/

//  Concatenate multiple strings, staying within a specified overall length.
//  The destination string is also the first source string. 
//  Null marks the end of the source strings (omission --> crash).
//  Output is truncated to fit within the specified length.
//  A final null is assured and is included in the length.
//  Returns 0 if OK, 1 if truncation was needed.

int strncatv(char *dest, int maxcc, cchar *source, ...)
{
   cchar       *ps;
   va_list     arglist;

   maxcc = maxcc - strlen(dest) - 1;
   if (maxcc < 0) return 1;
   va_start(arglist,source);
   ps = source;

   while (ps)
   {
      strncat(dest,ps,maxcc);
      maxcc = maxcc - strlen(ps);
      if (maxcc < 0) break;
      ps = va_arg(arglist,cchar *);
   }
   
   va_end(arglist);
   if (maxcc < 0) return 1;
   return 0;
}


/**************************************************************************/

//  Match 1st string to N additional strings.
//  Return matching string number 1 to N or 0 if no match.
//  Supply a null argument for end of list.

int strcmpv(cchar *string, ...)
{
   int         match = 0;
   char        *stringN;
   va_list     arglist;

   va_start(arglist,string);

   while (1)
   {
      stringN = va_arg(arglist, char *);
      if (stringN == null)
      {
         va_end(arglist);
         return 0;
      }

      match++;
      if (strcmp(string,stringN) == 0)
      {
         va_end(arglist);
         return match;
      }
   }
}


/**************************************************************************/

//  convert string to upper case

void strToUpper(char *string)
{
   int         ii;
   char        jj;
   const int   delta = 'A' - 'a';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta;
}

void strToUpper(char *dest, cchar *source)
{
   strcpy(dest,source);
   strToUpper(dest);
}


/**************************************************************************/

//  convert string to lower case

void strToLower(char *string)
{
   int         ii;
   char        jj;
   const int   delta = 'a' - 'A';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta;
}

void strToLower(char *dest, cchar *source)
{
   strcpy(dest,source);
   strToLower(dest);
}


/**************************************************************************/

//  copy string strin to strout, replacing every occurrence
//    of the substring ssin with the substring ssout

int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout)
{
   int         ccc, cc1, cc2, nfound;
   cchar       *ppp;                                                       //  v.2.15
   
   cc1 = strlen(ssin);
   cc2 = strlen(ssout);
   nfound = 0;
   
   while ((ppp = strstr(strin,ssin)))
   {
      nfound++;
      ccc = ppp - strin;
      strncpy(strout,strin,ccc);
      strout += ccc;
      strin += ccc;
      strncpy(strout,ssout,cc2);
      strin += cc1;
      strout += cc2;
   }

   strcpy(strout,strin);
   return nfound;
}


/**************************************************************************/

//  like repl_1str, but multiple pairs of substrings are processed
//   (... ssin1, ssout1, ssin2, ssout2, ... null) 

int repl_Nstrs(cchar *strin, char *strout, ...)
{
   va_list     arglist;
   cchar       *ssin, *ssout;
   char        ftemp[maxfcc];
   int         ftf, nfound;
   
   ftf = 1;
   nfound = 0;
   va_start(arglist,strout);
   
   while (true)
   {
      ssin = va_arg(arglist, char *);
      if (! ssin) break;
      ssout = va_arg(arglist, char *);

      if (ftf) {
         ftf = 0;
         nfound += repl_1str(strin,strout,ssin,ssout);
      }

      else {
         strcpy(ftemp,strout);
         nfound += repl_1str(ftemp,strout,ssin,ssout);
      }
   }

   va_end(arglist);
   return nfound;
}


/**************************************************************************/

//  Copy and convert string to hex string.
//  Each input character 'A' >> 3 output characters "41 "

void strncpyx(char *out, cchar *in, int ccin)
{
   int      ii, jj, c1, c2;
   char     cx[] = "0123456789ABCDEF";

   if (! ccin) ccin = strlen(in);

   for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3)
   {
      c1 = (uchar) in[ii] >> 4;
      c2 = in[ii] & 15;
      out[jj] = cx[c1];
      out[jj+1] = cx[c2];
      out[jj+2] = ' ';
   }
   out[jj] = 0;
   return;
}


/**************************************************************************/

//  Strip trailing zeros from ascii floating numbers
//    (e.g. 1.230000e+02  -->  1.23e+02)

void StripZeros(char *pNum)
{
   int     ii, ll;
   int     pp, k1, k2;
   char    work[20];

   ll = strlen(pNum);
   if (ll >= 20) return;

   for (ii = 0; ii < ll; ii++)
   {
      if (pNum[ii] == '.')
      {
         pp = ii;
         k1 = k2 = 0;
         for (++ii; ii < ll; ii++)
         {
            if (pNum[ii] == '0')
            {
               if (! k1) k1 = k2 = ii;
               else k2 = ii;
               continue;
            }

            if ((pNum[ii] >= '1') && (pNum[ii] <= '9'))
            {
               k1 = 0;
               continue;
            }

            break;
         }

         if (! k1) return;

         if (k1 == pp + 1) k1++;
         if (k2 < k1) return;
         strcpy(work,pNum);
         strcpy(work+k1,pNum+k2+1);
         strcpy(pNum,work);
         return;
      }
   }
}


/**************************************************************************/

//  test for blank/null string

int blank_null(cchar *string)
{
   if (! string) return 1;                                                 //  null string
   if (! *string) return 2;                                                //  zero length string
   int cc = strlen(string);
   for (int ii = 0; ii < cc; ii++)
      if (string[ii] != ' ') return 0;                                     //  non-blank string
   return 3;                                                               //  blank string
}


/**************************************************************************/

//  make a copy of a string in heap memory and allocate more space
//  returned string is subject for zfree();

char * strdupz(cchar *string, int more, cchar *tag)
{
   char  *pp = zmalloc(strlen(string)+1+more,tag);                         //  v.3.5
   strcpy(pp,string);
   return pp;
}


//  copy into existing 'zmalloc' string if present and long enough         //  v.2.8
//  else free memory and allocate a longer one
//  destination string is subject for zfree()

int strdupz(cchar *source, char *&zdest, int more, cchar *tag)
{
   int      ccs, ccd;

   ccs = strlen(source);
   if (! zdest) zdest = zmalloc(ccs+1+more,tag);                           //  v.3.5
   ccd = (int) *(zdest-8);
   if (ccd < ccs+1) {
      zfree(zdest);
      zdest = zmalloc(ccs+1+more,tag);
   }
   strcpy(zdest,source);
   return ccs;
} 


/**************************************************************************/

//  clean \x escape sequences and replace them with the escaped character
//    \n >> newline  \" >> doublequote  \\ >> backslash   etc.
//  see  $ man ascii  for the complete list

int clean_escapes(char *string)
{
   char     *pp1 = string, *pp2 = string, *pp;
   char     char1;
   char     escapes[] = "abtnvfr";
   int      count = 0;
   
   while (true)
   {
      char1 = *pp1++;

      if (char1 == 0) {
         *pp2 = 0;
         return count;
      }

      else if (char1 == '\\')  {
         char1 = *pp1++;
         pp = strchr(escapes,char1);
         if (pp) char1 = pp - escapes + 7;
         count++;
      }

      *pp2++ = char1;
   }
}


/**************************************************************************/

//  Compute the graphic character count for a UTF8 character string.
//  Depends on UTF8 rules:
//    - ascii characters are positive (actually 0x00 to 0x7F)
//    - 1st byte of multibyte sequence is negative (actually 0xC0 to 0xFD)
//    - subsequent bytes are negative and < 0xC0 (actually 0x80 to 0xBF)

int utf8len(cchar *utf8string)                                             //  v.2.3
{
   int      ii, cc;
   char     xlimit = 0xC0;

   for (ii = cc = 0; utf8string[ii]; ii++)
   {
      if (utf8string[ii] < 0)                                              //  multibyte character
         while (utf8string[ii+1] < xlimit) ii++;                           //  skip extra bytes
      cc++;
   }

   return cc;
}


/**************************************************************************/

//  Extract a UTF8 substring with a specified count of graphic characters.
//    utf8in     input UTF8 string
//    utf8out    output UTF8 string, which must be long enough 
//    pos        initial graphic character position to get (0 = first)
//    cc         max. count of graphic characters to get
//    returns    number of graphic characters extracted, <= cc
//  Output string is null terminated after last extracted character.

int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc)           //  v.2.3
{
   int      ii, jj, kk, posx, ccx;
   char     xlimit = 0xC0;

   for (ii = posx = 0; posx < pos && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)
         while (utf8in[ii+1] < xlimit) ii++;
      posx++;
   }

   jj = ii;   

   for (ccx = 0; ccx < cc && utf8in[jj]; jj++)
   {
      if (utf8in[jj] < 0)
         while (utf8in[jj+1] < xlimit) jj++;
      ccx++;
   }
   
   kk = jj - ii;

   strncpy(utf8out,utf8in+ii,kk);
   utf8out[kk] = 0;
   
   return   ccx;
}


/**************************************************************************/

//  check a string for valid utf8 encoding
//  returns:  0 = OK,  1 = bad string

int utf8_check(cchar *string)                                              //  v.2.4
{
   cchar             *pp;
   unsigned char     ch1, ch2, nch;
   
   for (pp = string; *pp; pp++)
   {
      ch1 = *pp;
      if (ch1 < 0x7F) continue;
      if (ch1 > 0xBF && ch1 < 0xE0) nch = 1;
      else if (ch1 < 0xF0) nch = 2;
      else if (ch1 < 0xF8) nch = 3;
      else if (ch1 < 0xFC) nch = 4;
      else if (ch1 < 0xFE) nch = 5;
      else return 1;
      while (nch) {
         pp++;
         ch2 = *pp;
         if (ch2 < 0x80 || ch2 > 0xBF) return 1;
         nch--;
      }
   }

   return 0;
}


/**************************************************************************/

//  Find the Nth graphic character position within a UTF8 string
//    utf8in      input UTF8 string
//    Nth         graphic character position, zero based
//  returns starting character (byte) position of Nth graphic character

int utf8_position(cchar *utf8in, int Nth)                                  //  v.2.4
{
   int      ii, posx;
   char     xlimit = 0xC0;

   for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)
         while (utf8in[ii+1] < xlimit) ii++;
      posx++;
   }

   return  ii;
}


/**************************************************************************
   bitmap functions
***************************************************************************/

//  create a new bitmap with specified bit length. 
//  initially all bits are false.

bitmap * bitmap_new(int nbits)
{
   int      cc, ii;
   bitmap   *bm;
   
   bm = (bitmap *) zmalloc(sizeof(bitmap),"bitmap");                       //  v.3.5
   bm->nbits = nbits;
   cc = (nbits + 7) / 8;
   bm->bits = (uchar *) zmalloc(cc,"bitmap");
   for (ii = 0; ii < cc; ii++) bm->bits[ii] = 0;
   return bm;
}


//  set bit in bitmap to true or false

void bitmap_set(bitmap *bm, int bit, bool value)
{
   int      ii, jj;
   uchar    bit1;

   if (bit >= bm->nbits) zappcrash("bitmap, bit %d too big",bit);
   ii = bit / 8;
   jj = bit % 8;
   bit1 = 0x80 >> jj;

   if (value) bm->bits[ii] = bm->bits[ii] | bit1;
   else {
      bit1 = bit1 ^ 0xff;
      bm->bits[ii] = bm->bits[ii] & bit1;
   }

   return;
}


//  fetch bitmap bit, return true or false

bool bitmap_get(bitmap *bm, int bit)
{
   int      ii, jj;
   uchar    bit1;

   ii = bit / 8;
   jj = bit % 8;
   bit1 = bm->bits[ii] << jj;
   if (bit1 < 127) return false;
   else return true;
}


//  delete bitmap

void bitmap_delete(bitmap *bm)
{
   zfree(bm->bits);
   zfree(bm);
   return;
}


/**************************************************************************
   variable string list functions - array / list of strings
***************************************************************************/

//  create new variable list with specified capacity

pvlist * pvlist_create(int max)
{
   pvlist      *pv;

   pv = (pvlist *) zmalloc(sizeof(pvlist),"pvlist");                       //  v.3.5
   pv->max = max;
   pv->act = 0;
   pv->list = (char **) zmalloc(max * sizeof(char *),"pvlist");
   return pv;
}

//  free memory for variable list
   
void pvlist_free(pvlist *pv)
{
   int      ii;
   
   for (ii = 0; ii < pv->act; ii++)
      zfree(pv->list[ii]);
   zfree(pv->list);
   zfree(pv);
}

//  append new entry to end of list (optional if unique)
//  if list if full, first entry is removed and rest are packed down
//  return: N >= 0: new entry added at position N
//          N = -1: not unique, not added
   
int pvlist_append(pvlist *pv, cchar *entry, int unique)
{
   int      ii;

   if (unique && pvlist_find(pv,entry) >= 0) return -1;                    //  not unique

   if (pv->act == pv->max) pvlist_remove(pv,0);                            //  if list full, remove 1st entry

   ii = pv->act;
   pv->list[ii] = strdupz(entry,0,"pvlist");                               //  add to end of list     v.3.5
   pv->act++;
   return ii;
}

//  prepend new entry to list (optional if unique)
//  prior list entries are pushed down to make room
//  if list is full, last entry is removed first
//  return: N = 0: new entry added at position 0
//          N = -1: not unique, not added
   
int pvlist_prepend(pvlist *pv, cchar *entry, int unique)
{
   int      ii;
   
   if (unique && pvlist_find(pv,entry) >= 0) return -1;                    //  not unique

   if (pv->act == pv->max) pvlist_remove(pv,pv->act-1);                    //  if list full, remove last entry

   for (ii = pv->act; ii > 0; ii--)                                        //  push all list entries down
      pv->list[ii] = pv->list[ii-1];
   pv->list[0] = strdupz(entry,0,"pvlist");                                //  add to start of list      v.3.5
   pv->act++;
   return 0;
}

//  find list entry by name, return -1 if not found
   
int pvlist_find(pvlist *pv, cchar *entry)
{
   int      ii;

   for (ii = 0; ii < pv->act; ii++)
      if (strEqu(entry,pv->list[ii])) break;
   if (ii < pv->act) return ii;
   return -1;
}

//  remove an entry by name and repack list
   
int pvlist_remove(pvlist *pv, cchar *entry)
{
   int      ii;
   
   ii = pvlist_find(pv,entry);
   if (ii < 0) return -1;
   pvlist_remove(pv,ii);
   return ii;
}

//  remove an entry by number and repack list
   
int pvlist_remove(pvlist *pv, int ii)
{
   if (ii < 0 || ii >= pv->act) return -1;
   zfree(pv->list[ii]);
   for (ii++; ii < pv->act; ii++) {
      if (! pv->act) printf("meaningless reference %d",ii);                //  stop g++ optimization bug  ////
      pv->list[ii-1] = pv->list[ii];
   }
   pv->act--;
   return 0;
}


//  return entry count

int pvlist_count(pvlist *pv)
{
   return pv->act;
}


//  replace Nth entry with new one

int pvlist_replace(pvlist * pv, int ii, cchar *entry)                      //  v.3.5
{
   if (ii < 0 || ii >= pv->act) return -1;
   zfree(pv->list[ii]);
   pv->list[ii] = strdupz(entry,0,"pvlist");
   return 0;
}


//  return Nth entry or null

char * pvlist_get(pvlist *pv, int Nth)
{
   if (Nth >= pv->act) return 0;
   return pv->list[Nth];
}


//  sort list in ascending order

int pvlist_sort(pvlist *pv)
{
   HeapSort(pv->list,pv->act);
   return 0;
}


/**************************************************************************

   Conversion Utilities

   convSI(string, inum, delim)                     string to int
   convSI(string, inum, low, high, delim)          string to int with range check

   convSD(string, dnum, delim)                     string to double
   convSD(string, dnum, low, high, delim)          string to double with range check

   convIS(inum, string, cc)                        int to string with returned cc

   convDS(fnum, digits, string, cc)                double to string with specified 
                                                     digits of precision and returned cc
   
   string      input (cchar *) or output (char *)
   inum        input (int) or output (int &)
   dnum        input (double) or output (double &)
   delim       optional returned delimiter (null or cchar **)
   low, high   input range check (int or double)
   cc          output string length (int &)
   digits      input digits of precision (int) to be used for output string
   
   NOTE: decimal point may be comma or period. 
         1000's separators must NOT be present.

   function status returned:
       0    normal conversion, no invalid digits, blank/null termination
       1    successful converstion, but trailing non-numeric found
       2    conversion OK, but outside specified limits
       3    null or blank string, converted to zero
       4    conversion error, invalid data in string
   overlapping statuses have following precedence: 4 3 2 1 0

***************************************************************************/

#define  max10  (0x7fffffff / 10)


//  Convert string to integer

int convSI(cchar *string, int &inum, cchar **delim)
{
   char        ch;
   int         sign = 0, digits = 0, tnb = 0;
   cchar       *pch = string;

   inum = 0;

   while ((ch = *pch) == ' ') pch++;                                       //  skip leading blanks

   if (ch == '-') sign = -1;                                               //  process leading +/- sign
   if (ch == '+') sign = 1;                                                //  (at most one sign character)
   if (sign) pch++;

   while ((*pch >= '0') && (*pch <= '9'))                                  //  process digits 0 - 9
   {
      if (inum > max10) goto conv_err;                                     //  value too big
      inum = 10 * inum + *pch - '0';
      digits++;
      pch++;
   }

   if (delim) *delim = pch;                                                //  terminating delimiter
   if (*pch && (*pch != ' ')) tnb++;                                       //  not null or blank

   if (! digits)                                                           //  no digits found
   {
      if (tnb) return 4;                                                   //  non-numeric (invalid) string
      else return 3;                                                       //  null or blank string
   }

   if (sign == -1) inum = -inum;                                           //  negate if - sign

   if (! tnb) return 0;                                                    //  no trailing non-numerics
   else return 1;                                                          //  trailing non-numerics

conv_err:
   inum = 0;
   return 4;
}


int convSI(cchar *string, int & inum, int lolim, int hilim, cchar **delim)
{
   int   stat = convSI(string,inum,delim);

   if (stat > 2) return stat;                                              //  invalid or null/blank
   if (inum < lolim) return 2;                                             //  return 2 if out of limits
   if (inum > hilim) return 2;                                             //  (has precedence over status 1)
   return stat;                                                            //  limits OK, return 0 or 1
}


//  Convert string to double.

int convSD(cchar *string, double &dnum, cchar **delim)
{
   char        ch;
   int         ii, sign = 0, digits = 0, ndec = 0;
   int         exp = 0, esign = 0, edigits = 0, tnb = 0;
   cchar       *pch = string;

   static int  first = 1;
   static double  decimals[21], exponents[74];

   if (first)                                                              //  first-time called
   {
      first = 0;                                                           //  pre-calculate constants
      for (ii = 1; ii <= 20; ii++) decimals[ii] = pow(10.0,-ii);
      for (ii = -36; ii <= 36; ii++) exponents[ii+37] = pow(10.0,ii);
   }

   dnum = 0.0;

   while ((ch = *pch) == ' ') pch++;                                       //  skip leading blanks

   if (ch == '-') sign = -1;                                               //  process leading +/- sign
   if (ch == '+') sign = 1;                                                //  (at most one sign character)
   if (sign) pch++;

get_digits:

   while ((*pch >= '0') && (*pch <= '9'))                                  //  process digits 0 - 9
   {
      dnum = 10.0 * dnum + (*pch - '0');
      pch++;
      digits++;
      if (ndec) ndec++;
   }

   if ((*pch == '.') || (*pch == ','))                                     //  process decimal point
   {                                                                       //  (allow comma or period)
      if (ndec) goto conv_err;
      ndec++;
      pch++;
      goto get_digits;
   }

   if ((*pch == 'e') || (*pch == 'E'))                                     //  process optional exponent
   {
      pch++;
      if (*pch == '+') esign = 1;                                          //  optional +/- sign
      if (*pch == '-') esign = -1;
      if (esign) pch++;

      if ((*pch < '0') || (*pch > '9')) goto conv_err;                     //  1st digit
      exp = *pch - '0';
      edigits++;
      pch++;

      if ((*pch >= '0') && (*pch <= '9'))                                  //  optional 2nd digit
      {
         exp = 10 * exp + (*pch - '0');
         edigits++;
         pch++;
      }

      if ((exp < -36) || (exp > 36)) goto conv_err;                        //  exponent too big 
   }

   if (delim) *delim = pch;                                                //  terminating delimiter 
   if (*pch && (*pch != ' ')) tnb++;                                       //  not null or blank

   if (!(digits + edigits))                                                //  no digits found
   {
      if (tnb) return 4;                                                   //  non-numeric (invalid) string
      else return 3;                                                       //  null or blank string
   }

   if (ndec > 1) dnum = dnum * decimals[ndec-1];                           //  compensate for decimal places

   if (sign == -1) dnum = - dnum;                                          //  negate if negative

   if (exp)                                                
   {
      if (esign == -1) exp = -exp;                                         //  process exponent
      dnum = dnum * exponents[exp+37];
   }

   if (! tnb) return 0;                                                    //  no trailing non-numerics
   else return 1;                                                          //  trailing non-numerics

conv_err:
   dnum = 0.0;
   return 4;
}


int convSD(cchar *string, double &dnum, double lolim, double hilim, cchar **delim)
{
   int stat = convSD(string,dnum,delim);

   if (stat > 2) return stat;                                              //  invalid or null/blank
   if (dnum < lolim) return 2;                                             //  return 2 if out of limits
   if (dnum > hilim) return 2;                                             //  (has precedence over status 1)
   return stat;                                                            //  limits OK, return 0 or 1
}


//  Convert int to string with returned length.

int convIS(int inum, char *string, int *cc)
{
   int   ccc;

   ccc = sprintf(string,"%d",inum);
   if (cc) *cc = ccc;
   return 0;
}


//  Convert double to string with specified digits of precision.
//  Shortest length format (f/e) will be used.  
//  Output length is returned in optional argument cc.

int convDS(double dnum, int digits, char *string, int *cc)
{
   char     *pstr;
   
   sprintf(string,"%.*g",digits,dnum);

   pstr = strstr(string,"e+");                                             //  1.23e+12  >  1.23e12
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e0");                                             //  1.23e02  >  1.23e2
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e0");            
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e-0");                                            //  1.23e-02  >  1.23e-2
   if (pstr) strcpy(pstr+2,pstr+3);

   pstr = strstr(string,"e-0");
   if (pstr) strcpy(pstr+2,pstr+3);

   if (cc) *cc = strlen(string);

   return 0;
}


//  format a number as "123 B" or "12.3 KB" or "1.23 MB" etc.
//  prec is the desired digits of precision to output.
//  WARNING: only the last 100 conversions remain available in memory.

char * formatKBMB(double fnum, int prec)                                   //  v.2.25
{
   #define kilo 1000
   #define mega (kilo*kilo)
   #define giga (kilo*kilo*kilo)

   cchar          *units;
   static char    *output[100];
   static int     ftf = 1, ii;
   double         gnum;
   
   if (ftf) {                                                              //  keep last 100 conversions
      ftf = 0;                                                             //  v.4.7
      for (ii = 0; ii < 100; ii++)
         output[ii] = (char *) zmalloc(20);
   }
   
   gnum = fabs(fnum);
   
   if (gnum > giga) {
      fnum = fnum / giga;
      units = "GB";
   }
   else if (gnum > mega) {
      fnum = fnum / mega;
      units = "MB";
   }
   else if (gnum > kilo) {
      fnum = fnum / kilo;
      units = "KB";
   }
   else units = "B ";

   gnum = fabs(fnum);   
   if (prec == 2 && gnum >= 99.5) prec++;                                  //  avoid e+nn formats
   if (prec == 3 && gnum >= 999.5) prec++;
   if (prec == 4 && gnum >= 9999.5) prec++;
   if (prec == 5 && gnum >= 99999.5) prec++;
   if (prec == 6 && gnum >= 999999.5) prec++;

   if (++ii > 99) ii = 0;   
   snprintf(output[ii],20,"%.*g %s",prec,fnum,units);
   return output[ii];
}


/**************************************************************************

    Wildcard string match

    Match candidate string to wildcard string containing any number of 
    '*' or '?' wildcard characters. '*' matches any number of characters, 
    including zero characters. '?' matches any one character.

    Returns 0 if match, 1 if no match.

***/

int MatchWild(cchar *pWild, cchar *pString)
{
   int   ii, star;

new_segment:

   star = 0;
   while (pWild[0] == '*')
   {
      star = 1;
      pWild++;
   }

test_match:

   for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++)
   {
      if (pWild[ii] != pString[ii])
      {
         if (! pString[ii]) return 1;
         if (pWild[ii] == '?') continue;
         if (! star) return 1;
         pString++;
         goto test_match;
      }
   }

   if (pWild[ii] == '*')
   {
      pString += ii;
      pWild += ii;
      goto new_segment;
   }

   if (! pString[ii]) return 0;
   if (ii && pWild[ii-1] == '*') return 0;
   if (! star) return 1;
   pString++;
   goto test_match;
}


/**************************************************************************

   SearchWild  - wildcard file search

   Find all files with total /pathname/filename matching a pattern,
   which may have any number of the wildcard characters '*' and '?'
   in either or both the pathname and filename.

   cchar * SearchWild(cchar *wfilespec, int &flag)
   
   inputs:  flag = 1 to start a new search
            flag = 2 abort a running search
            *** do not modify flag within a search ***

            wfilespec = filespec to search with optional wildcards
               e.g. "/name1/na*me2/nam??e3/name4*.ext?"
               
   return:  a pointer to one matching file is returned per call,
            or null when there are no more matching files.
             
   The search may be aborted before completion, but make a final 
   call with flag = 2 to clean up temp file. A new search with 
   flag = 1 will also finish the cleanup.
   
   NOT THREAD SAFE - do not use in parallel threads
   
   shell find command is used for the initial search because this
   is much faster than recursive use of readdir() (why?). 

   (#) is used in place of (*) in comments below to prevent 
   compiler from interpreting (#/) as end of comments

   GNU find peculiarities: 
     find /path/#      omits "." files
     find /path/       includes "." files
     find /path/#      recurses directories under /path/
     find /path/#.txt  does not recurse directories
     find /path/#/     finds all files under /path/
     find /path/#/#    finds files >= 1 directory level under /path/
     find /path/xxx#   never finds anything

   SearchWild uses simpler and more intuitive matching: 
     '/' and '.' are matched by '#'
     /path/#.txt finds all .txt files under /path/ at any directory level
   
***/

cchar * SearchWild(cchar *wpath, int &uflag)                               //  use popen() instead of scratch file
{                                                                          //     v.3.0
   static FILE    *fid = 0;
   static char    matchfile[maxfcc];
   char           searchpath[maxfcc];
   char           command[maxfcc];
   int            cc, err;
   char           *pp;
   
   if ((uflag == 1) || (uflag == 2)) {                                     //  first call or stop flag
      if (fid) {
         pclose(fid);                                                      //  if file open, close it
         fid = 0;
      }
   }
   
   if (uflag == 2) return 0;                                               //  kill flag, done
      
   if (uflag == 1)                                                         //  first call flag
   {
      cc = strlen(wpath);
      if (cc == 0) return 0;
      if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc");
      
      pp = (char *) wpath;
      repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null);                //  init. search path, escape $ and "

      pp = strchr(searchpath,'*');
      if (pp) {                                                            //  not efficient but foolproof 
         while ((*pp != '/') && (pp > searchpath)) pp--;                   //  /aaa/bbb/cc*cc... >>> /aaa/bbb/
         if (pp > searchpath) *(pp+1) = 0;
      }

      sprintf(command,"find \"%s\" -type f -or -type l",searchpath);       //  find files (ordinary, symlink)
      fid = popen(command,"r");
      if (! fid) zappcrash(strerror(errno));
      uflag = 763568954;                                                   //  begin search
   }

   if (uflag != 763568954) zappcrash("SearchWild, uflag invalid");
   
   while (true)
   {
      pp = fgets(matchfile,maxfcc-2,fid);                                  //  next matching file
      if (! pp) {
         pclose(fid);                                                      //  no more
         fid = 0;
         return 0;
      }

      cc = strlen(matchfile);                                              //  get rid of trailing \n
      matchfile[cc-1] = 0;

      err = MatchWild(wpath,matchfile);                                    //  wildcard match?
      if (err) continue;                                                   //  no

      return matchfile;                                                    //  return file
   }
}


/**************************************************************************/

//  perform a binary search on sorted list of integers
//  return matching element or -1 if not found

int bsearch(int element, int nn, int list[])
{
   int      ii, jj, kk, rkk;

   ii = nn / 2;                                                            //  next element to search
   jj = (ii + 1) / 2;                                                      //  next increment
   nn--;                                                                   //  last element
   rkk = 0;

   while (true)
   {
      kk = list[ii] - element;                                             //  check element

      if (kk > 0)
      {
         ii -= jj;                                                         //  too high, go down
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                         //  too low, go up
         if (ii > nn) return -1;
      }

      else if (kk == 0) return ii;                                         //  matched

      jj = jj / 2;                                                         //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                           //  step by 1 element
         if (! rkk) rkk = kk;                                              //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                        //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


/**************************************************************************
   heap sort functions
***************************************************************************/

#define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp)


//  heapsort for array of integers 

static void adjust(int vv[], int n1, int n2)
{
   int   *bb, jj, kk, temp;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(int vv[], int nn)
{
   int   *bb, jj, temp;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort for array of floats 

static void adjust(float vv[], int n1, int n2)
{
   float    *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(float vv[], int nn)
{
   float    *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort for array of doubles 

static void adjust(double vv[], int n1, int n2)
{
   double   *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(double vv[], int nn)
{
   double   *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort array of pointers to strings in ascending order of strings
//  pointers are sorted, strings are not changed.

static void adjust(char *vv[], int n1, int n2)
{
   char     **bb, *temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && strcmp(bb[kk],bb[kk+1]) < 0) kk++;
      if (strcmp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *vv[], int nn)
{
   char     **bb, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[0], bb[jj]);
      adjust(vv,1,jj);
   }
}


//  heapsort array of pointers to strings in user-defined order.
//  pointers are sorted, strings are not changed.

static void adjust(char *vv[], int n1, int n2, HeapSortUcomp fcomp)
{
   char     **bb, *temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++;
      if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *vv[], int nn, HeapSortUcomp fcomp)
{
   char     **bb, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp);

   bb = vv;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[0], bb[jj]);
      adjust(vv,1,jj,fcomp);
   }
}


//  heapsort for array of strings or records, 
//  using caller-supplied record compare function.
//  HeapSortUcomp returns [ -1 0 +1 ]  for  rec1 [ < = > ] rec2
//  method: build array of pointers and sort these, then
//  use this sorted array to re-order the records at the end.

static int     *vv1, *vv2;

static void adjust(char *recs, int RL, int n1, int n2, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp;
   char     *rec1, *rec2;

   bb = vv1 - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      rec1 = recs + RL * bb[kk];
      rec2 = recs + RL * bb[kk+1];
      if (kk < n2 && fcomp(rec1,rec2) < 0) kk++;
      rec1 = recs + RL * bb[jj];
      rec2 = recs + RL * bb[kk];
      if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *recs, int RL, int NR, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp, flag;
   char     *vvrec;

   vv1 = new int[NR];
   for (jj = 0; jj < NR; jj++) vv1[jj] = jj;

   for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,fcomp);

   bb = vv1 - 1;

   for (jj = NR-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(recs,RL,1,jj,fcomp);
   }

   vv2 = new int[NR];
   for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj;

   vvrec = new char[RL];
   flag = 1;
   while (flag)
   {
      flag = 0;
      for (jj = 0; jj < NR; jj++)
      {
         kk = vv2[jj];
         if (kk == jj) continue;
         memmove(vvrec,recs+jj*RL,RL);
         memmove(recs+jj*RL,recs+kk*RL,RL);
         memmove(recs+kk*RL,vvrec,RL);
         SWAP(vv2[jj],vv2[kk]);
         flag = 1;
      }
   }

   delete vv1;
   delete vv2;
   delete vvrec;
}


/**************************************************************************

         int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK)

         RECS is an array of records, to be sorted in-place.
         (record length = RL, record count = NR)

         KEYS[NK,3]  is an integer array defined as follows:
              [N,0]    starting position of Nth key field in RECS
              [N,1]    length of Nth key field in RECS
              [N,2]    type of sort for Nth key:
                        1 = char ascending
                        2 = char descending
                        3 = int*4 ascending (int, long)
                        4 = int*4 descending
                        5 = float*4 ascending (float)
                        6 = float*4 descending
                        7 = float*8 ascending (double)
                        8 = float*8 descending

***/

int MemSortComp(cchar *rec1, cchar *rec2);
int MemSortKeys[10][3], MemSortNK;

int MemSort(char *RECS, int RL, int NR, int KEYS[][3], int NK)
{
   int   ii;

   if (NR < 2) return 1;

   if (NK > 10) zappcrash("MemSort, bad NK");
   if (NK < 1) zappcrash("MemSort, bad NK");

   MemSortNK = NK;

   for (ii = 0; ii < NK; ii++)
   {
      MemSortKeys[ii][0] = KEYS[ii][0];
      MemSortKeys[ii][1] = KEYS[ii][1];
      MemSortKeys[ii][2] = KEYS[ii][2];
   }

   HeapSort(RECS,RL,NR,MemSortComp);
   return 1;
}

int MemSortComp(cchar *rec1, cchar *rec2)
{
   int            ii, stat, kpos, ktype, kleng;
   int            inum1, inum2;
   float          rnum1, rnum2;
   double         dnum1, dnum2;
   cchar          *p1, *p2;

   for (ii = 0; ii < MemSortNK; ii++)                                      //  loop each key
   {
      kpos = MemSortKeys[ii][0];                                           //  relative position
      kleng = MemSortKeys[ii][1];                                          //  length
      ktype = MemSortKeys[ii][2];                                          //  type

      p1 = rec1 + kpos;                                                    //  absolute position
      p2 = rec2 + kpos;

      switch (ktype)
      {
         case 1:                                                           //  char ascending
            stat = strncmp(p1,p2,kleng);                                   //  compare 2 key values
            if (stat) return stat;                                         //  + if rec1 > rec2, - if <
            break;                                                         //  2 keys are equal, check next key

         case 2:                                                           //  char descending
            stat = strncmp(p1,p2,kleng);
            if (stat) return -stat;
            break;

         case 3:                                                           //  int ascending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return 1;
            if (inum1 < inum2) return -1;
            break;

         case 4:                                                           //  int descending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return -1;
            if (inum1 < inum2) return 1;
            break;

         case 5:                                                           //  float ascending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return 1;
            if (rnum1 < rnum2) return -1;
            break;

         case 6:                                                           //  float descending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return -1;
            if (rnum1 < rnum2) return 1;
            break;

         case 7:                                                           //  double ascending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return 1;
            if (dnum1 < dnum2) return -1;
            break;

         case 8:                                                           //  double descending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return -1;
            if (dnum1 < dnum2) return 1;
            break;

         default:                                                          //  key type not 1-8
            zappcrash("MemSort, bad KEYS sort type");
      }
   }

   return 0;                                                               //  records match on all keys
}


/**************************************************************************/

//  random number generators with explicit context
//  and improved randomness over a small series

int lrandz(int64 *seed)                                                    //  returns 0 to 0x7fffffff
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return nrand48((unsigned int16 *) seed);
}

int lrandz()                                                               //  built-in seed    v.3.3
{
   static int64   seed = 12345678;
   return lrandz(&seed);
}

double drandz(int64 *seed)                                                 //  returns 0.0 to 0.99999...
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return erand48((unsigned int16 *) seed);
}

double drandz()                                                            //  built-in seed    v.3.3
{
   static int64   seed = 23459876;
   return drandz(&seed);
}


/**************************************************************************

   spline1: define a curve using a set of data points (x and y values)        v.2.11
   spline2: for a given x-value, return a y-value fitting the curve

   For spline1, the no. of curve-defining points must be < 100.
   For spline2, the given x-value must be within the range defined in spline1.

***/

namespace splinedata 
{
   int      nn;
   double   px1[100], py1[100], py2[100];
}


void spline1(int dnn, double *dx1, double *dy1)
{
   using namespace splinedata;

   double   sig, p, u[100];
   int      ii;
   
   nn = dnn;
   if (nn > 100) zappcrash("spline1(), > 100 data points");

   for (ii = 0; ii < nn; ii++)
   {
      px1[ii] = dx1[ii];
      py1[ii] = dy1[ii];
      if (ii && px1[ii] <= px1[ii-1]) 
         zappcrash("spline1(), x-value not increasing");
   }

   py2[0] = u[0] = 0;
   
   for (ii = 1; ii < nn-1; ii++)
   {
      sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]);
      p = sig * py2[ii-1] + 2;
      py2[ii] = (sig - 1) / p;
      u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1])
            / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p;
   }
   
   py2[nn-1] = 0;
   
   for (ii = nn-2; ii >= 0; ii--)
      py2[ii] = py2[ii] * py2[ii+1] + u[ii];

   return;
}


double spline2(double x)
{
   using namespace splinedata;

   int      kk, klo = 0, khi = nn-1;
   double   h, a, b, y;
   
   while (khi - klo > 1)
   {
      kk = (khi + klo) / 2;
      if (px1[kk] > x) khi = kk;
      else klo = kk;
   }
   
   h = px1[khi] - px1[klo];
   a = (px1[khi] - x) / h;
   b = (x - px1[klo]) / h;
   y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo] 
                                   + (b*b*b - b) * py2[khi]) * (h*h) / 6;
   
   return y;
}


/**************************************************************************

   Initialize application files according to following conventions:        //  new version   v.4.1
     + binary executable is at:  /installoc/bin/appname                    //  = PREFIX/bin/appname
     + other application directories are derived as follows:
         /installoc/share/appname/data/         desktop, parameters, etc.
         /installoc/share/doc/appname/          README, CHANGES, user guide
         /installoc/share/appname/icons/        icon files .png
         /installoc/share/appname/locales/      translations: /de/appname.po  etc.
         /home/user/.appname/                   parameters etc. are copied here

   zinstalloc     install location           has /bin and /share subtrees
   zdatadir       installed data files       .desktop, parameters, etc.
   zdocdir        user documentation         README, CHANGES, user guide
   zicondir       icons                      icon files .png
   zlocalesdir    translation files          /de/appname.po  etc.
   zuserdir       /home/user/.appname        log file, user parameters

***/

namespace zfuncs
{
   char     zappname[20];
   char     zinstalloc[200], zdatadir[200], zdocdir[200];
   char     zicondir[200], zlocalesdir[200], zuserdir[200];
   char     zlanguage[8] = "en";                                           //  "lc" or "lc_RC"
   char     JPGquality[4] = "85";                                          //  JPG file save quality
   cchar    *F1_help_topic = 0;                                            //  current F1 help topic
}

cchar * get_zinstalloc() { return zfuncs::zinstalloc; }                    //  /usr or /home/<userid>
cchar * get_zuserdir() { return zfuncs::zuserdir; }                        //  /home/user/.appname
cchar * get_zdatadir() { return zfuncs::zdatadir; }                        //  parameters, icons
cchar * get_zdocdir()  { return zfuncs::zdocdir;  }                        //  documentation files
cchar * get_zicondir()  { return zfuncs::zicondir;  }                      //  icon files
cchar * get_zlocalesdir()  { return zfuncs::zlocalesdir;  }                //  translation files


int zinitapp(cchar *appname, ...)
{
   using namespace zfuncs;

   char           work[200];
   char           logfile[200], oldlog[200];
   cchar          *appfile;
   int            err;
   struct stat    statdat;
   va_list        arglist;
   FILE           *fid;
   
   catch_signals();                                                        //  catch segfault, do backtrace
   
   strcpy(zappname,appname);                                               //  save app name
   
   #ifndef PREFIX                                                          //  v.4.7
      #define PREFIX "/usr"
   #endif
   
   strncpy0(work,PREFIX,199);                                              //  /usr or /home/<userid>          v.4.6
   strcpy(zinstalloc,work);                                                //  /installoc
   strncatv(zdatadir,199,work,"/share/",zappname,"/data",null);            //  /installoc/share/appname/data
   strncatv(zicondir,199,work,"/share/",zappname,"/icons",null);           //  /installoc/share/appname/icons
   strncatv(zlocalesdir,199,work,"/share/",zappname,"/locales",null);      //  /installoc/share/appname/locales
   strncatv(zdocdir,199,work,"/share/doc/",zappname,null);                 //  /installoc/share/doc/appname
   
   snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname);                //  /home/user/.appname/            v.4.3
   err = stat(zuserdir,&statdat);                                          //  does it exist already?
   if (err) {
      err = mkdir(zuserdir,0750);                                          //  no, create and initialize
      if (err) zappcrash("cannot create %s",zuserdir);
      va_start(arglist,appname);                                           //  copy req. application files
      while (true) {                                                       //   from install directory to 
         appfile = va_arg(arglist, cchar *);                               //    to /home/user/.appname/
         if (! appfile) break;
         snprintf(work,199,"cp %s/%s %s",zdatadir,appfile,zuserdir);
         err = system(work);
         if (err) printf("%s %s \n",work,wstrerror(err));                  //  v.4.0   do not crash
      }
      va_end(arglist);
   }

   if (! isatty(1)) {                                                      //  not attached to a terminal      v.4.1
      snprintf(logfile,199,"%s/%s.log",zuserdir,zappname);
      snprintf(oldlog,199,"%s/%s.log.old",zuserdir,zappname);              //  prior log file >> *.old.log     v.4.3
      rename(logfile,oldlog);
      fid = freopen(logfile,"a",stdout);                                   //  redirect output to log file
      fid = freopen(logfile,"a",stderr);                                   //    /home/user/.appname/appname.log
      if (! fid) printf("cannot redirect stdout and stderr \n");
   }
   
   zlockInit();                                                            //  initz. for GTK thread lock

   return 1;
}


/**************************************************************************/

//  Display help file in a separate process so application is not blocked.
//  help file: /zdocdir/userguide-lc_RC.html (or) *-lc.html (or) *-en.html
//  context: optional arg. show file starting at internal link = context

void showz_userguide(cchar *context)                                       //  added context  v.2.29
{
   using namespace zfuncs;

   int      err;
   char     docfile[200], url[200], lang[4];

   snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,zlanguage);         //  look for userguide-lc_RC.html
   err = access(docfile,R_OK);

   if (err) {
      strncpy0(lang,zlanguage,3);
      snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,lang);           //  look for userguide-lc.html
      err = access(docfile,R_OK);
   }

   if (err) {
      snprintf(docfile,199,"%s/userguide-en.html",zdocdir);                //  look for userguide-en.html
      err = access(docfile,R_OK);
   }

   if (err) {
      zmessageACK(null,ZTX("help file not found: %s"),docfile);            //  give up
      return;
   }
   
   if (context && *context)                                                //  v.3.7
      strncatv(docfile,199,"#",context,null);                              //  file://.../userguide-xx.html#context
   snprintf(url,199,"file://%s",docfile);
   showz_html(url);

   return;
}


/**************************************************************************/

//  display various admin text files in a popup window                     //  v.4.2

void showz_readme()
{
   showz_doctext("README");
   return;
}

void showz_changelog()
{
   showz_doctext("CHANGES");
   return;
}

void showz_translations()
{
   showz_doctext("TRANSLATIONS");
   return;
}


//  find and show a text in a popup window
//  the text file may also be a compressed .gz file

void showz_doctext(const char *file)                                       //  v.4.2
{
   using namespace zfuncs;

   struct stat statb;
   char     buff[200];
   int      err;
   
   snprintf(buff,199,"%s/%s",zdocdir,file);                                //  look for /usr/.../file
   err = stat(buff,&statb);
   if (! err) {
      snprintf(buff,199,"cat %s/%s",zdocdir,file);
      popup_command(buff,500,300);
      return;
   }

   snprintf(buff,199,"%s/%s.gz",zdocdir,file);                             //  look for /usr/.../file.gz
   err = stat(buff,&statb);
   if (! err) {
      snprintf(buff,199,"zcat %s/%s.gz",zdocdir,file);
      popup_command(buff,500,300);
      return;
   }
   
   snprintf(buff,199,"echo not found: %s/%s",zdocdir,file);                //  display "not found" message
   popup_command(buff,500,100);
   return;
}


/**************************************************************************/

//  create a desktop icon / launcher with icon                             //  v.4.5
//  target system needs to be LSB compliant

void zmake_menu_launcher(cchar *command, cchar *categories, cchar *genericname)
{
   using namespace zfuncs;

   char     appname[20], dtfile[200], work[200];
   cchar    *pp;
   FILE     *fid;
   int      err;
   
   pp = strField(command,' ',1);
   if (! pp) pp = "?";
   strncpy0(appname,pp,20);
  
   snprintf(dtfile,199,"%s/Desktop/kornelix-%s.desktop",getenv("HOME"),appname);
   fid = fopen(dtfile,"w");
   if (! fid) {
      zmessageACK(null,ZTX("error: %s"),strerror(errno));
      return;
   }
   
   fputs("[Desktop Entry]\n",fid);                                         //  [Desktop Entry]
   snprintf(work,199,"Name=%s\n",appname);                                 //  Name=appname
   fputs(work,fid);
   snprintf(work,199,"Categories=%s\n",categories);                        //  Categories=Cat1;Cat2; ...
   fputs(work,fid);
   snprintf(work,199,"GenericName=%s\n",genericname);                      //  GenericName=generic app name
   fputs(work,fid);
   fputs("Type=Application\n",fid);                                        //  Type=Application
   fputs("Terminal=false\n",fid);                                          //  Terminal=false
   snprintf(work,199,"Exec=%s/bin/%s\n",zinstalloc,command);               //  Exec=/usr/bin/appname -options
   fputs(work,fid);
   snprintf(work,199,"Icon=%s/%s.png\n",zicondir,appname);                 //  Icon=/usr/share/appname/icons/appname.png
   fputs(work,fid);
   fclose(fid);
   
   snprintf(work,199,"chmod 0750 %s",dtfile);                              //  make executable
   err = system(work);
   if (err) zmessLogACK(null,"error: %s",wstrerror(err));

   snprintf(work,199,"xdg-desktop-menu install %s",dtfile);                //  add menu entry
   err = system(work);
   if (err) zmessLogACK(null,"error: %s",wstrerror(err));

   return;
}


/**************************************************************************

   Translation Functions
   
   Translation files are standard .po files as used in the Gnu gettext
   system. However the .po files are used directly, and there is no need
   to merge and compile them into a binary format (.mo files).

   Initialize: 
      int ZTXinit(cchar *lang)
      lang is "lc" or "lc_RC" or null (current locale will be used)

   If files are found in both  .../lc/xxx.po  and  .../lc_RC/xxx.po,
   both sets of files are processed. If a translation is present in
   both sets, the regional dialect (lc_RC) will be used.

   Translate a text string: 
      cchar *translation = ZTX(cchar *english)
      english: text string which may have printf formats (%d %s ...)
      translation: the returned equivalent translation

   If the user language is English or if no translation is found, 
   the input string is returned, else the translated string.
   
   A text string may have a context part "context::string", where
   "context" is any string < 30 characters and "string" is the 
   English text or the translation text. The context part "context::" 
   is removed in the returned string. This is to handle the case where 
   a single English string may need multiple translations, depending 
   on context. The English string may be present multiple times in a 
   .po file, each one marked with a different context and having a 
   different translation. Context is optional in translation strings.
   
   example: 
      
      program code: 

         printf(ZTX("answer: %d %s \n more on next line"), 123, "qwerty");
      
      A German .po file would have the following entry:

         msgid ""
         "answer: %d %s \n"
         " more on next line"
         msgstr ""
         "Antwort: %d %s \n"
         " mehr in der nächsten Zeile"

***/

namespace ZTXnames                                                         //  remove GOFUNC usage becasue of
{                                                                          //    GCC optimization errors  v.2.18
   FILE        *fidr, *fidw;
   char        buff[ZTXmaxcc], *ppq1, *ppq2, lc_RC[8];
   char        *porec, *wporec;
   char        Etext[ZTXmaxcc], Ttext[ZTXmaxcc];                           //  .po text: "line 1 %s \n" "line 2"
   char        **etext, **ttext;                                           //  arrays, english and translations
   char        **estring, **tstring;                                       //  merged, un-quoted, un-escaped
   int         Ntext = 0;                                                  //  array counts
   int         Ftranslate = 0;                                             //  0/1/2 = translate none/all/missing
   zdialog     *zddump;                                                    //  dialog for editing translations
   GtkWidget   *trwin;                                                     //  text window within dialog
   void        ZTXread_pofiles();                                          //  private functions
   void        ZTXgettext(char *text);
   char        *ZTXmergetext(cchar *text);
   void        ZTX_translation_dump(int index);
   void        ZTX_translation_update();
   void        ZTXgettext2(char *text);
   void        ZTXwritetext(cchar *header, char *text);
}


//  read and process .po files at application startup
//  prepare english strings and translations for quick access

void ZTXinit(cchar *lang)                                                  //  initialize translations
{
   using namespace zfuncs;
   using namespace ZTXnames;

   int      ii;
   char     *pp;

   if (Ntext) {                                                            //  free prior translation
      for (ii = 0; ii < Ntext; ii++) {
         zfree(etext[ii]);
         zfree(ttext[ii]);
         zfree(estring[ii]);
         zfree(tstring[ii]);
      }
      zfree(etext);
      zfree(ttext);
      zfree(estring);
      zfree(tstring);
      Ntext = 0;
   }

   etext = (char **) zmalloc(ZTXmaxent * sizeof(char *),"ZTX");            //  english text and translations
   ttext = (char **) zmalloc(ZTXmaxent * sizeof(char *),"ZTX");            //  (segmented, quoted, escaped)
   estring = (char **) zmalloc(ZTXmaxent * sizeof(char *),"ZTX");          //  english strings and translations
   tstring = (char **) zmalloc(ZTXmaxent * sizeof(char *),"ZTX");          //  (merged, un-quoted, un-escaped)

   if (lang && *lang) strncpy0(zlanguage,lang,6);                          //  use language from caller
   else {                                                                  //  help Linux chaos
      pp = getenv("LANG");                                                 //  use $LANG if defined
      if (! pp) pp = getenv("LANGUAGE");                                   //  use $LANGUAGE if defined 
      if (! pp) pp = setlocale(LC_MESSAGES,"");                            //  use locale if defined
      if (pp) strncpy0(zlanguage,pp,6);                                    //  "lc_RC" language/region code
      else strcpy(zlanguage,"en");                                         //  use English
   }

   if (*zlanguage < 'a') strcpy(zlanguage,"en");                           //  use English if garbage
   printf("language: %s \n",zlanguage);

   if (strnEqu(zlanguage,"en",2)) return;                                  //  English, do nothing    v.4.3
   
   strncpy0(lc_RC,zlanguage,6);                                            //  process .../locales/lc_RC/*.po  FIRST
   if (strlen(lc_RC) > 3) ZTXread_pofiles();

   strncpy0(lc_RC,zlanguage,3);                                            //  process .../locales/lc/*.po
   ZTXread_pofiles();

   return;
}


//  private function
//  read and process .po files

void ZTXnames::ZTXread_pofiles()                                           //  revised v.4.3
{
   using namespace zfuncs;
   using namespace ZTXnames;

   int      ii, err, contx = 0;
   char     *pp, *pofile, podir[200], command[200];

   snprintf(podir,199,"%s/locales/%s",zuserdir,lc_RC);                     //  look for user .po files
   snprintf(command,199,"find %s/*.po 2>/dev/null",podir);                 //  (translation project is underway)
   err = system(command);
   if (! err) printf("using user .po files for %s \n",lc_RC);
   else {   
      snprintf(podir,199,"%s/%s",zlocalesdir,lc_RC);                       //  look for installed .po files
      snprintf(command,199,"find %s/*.po",podir);
      err = system(command);
      if (! err) printf("using installed .po files for %s \n",lc_RC);
      else printf("no translation files found for %s \n",lc_RC);
   }
   
   strcat(command," 2>/dev/null");                                         //  suppress error messages

   while ((pofile = command_output(contx,command)))                        //  get user or installed .po files
   {
      fidr = fopen(pofile,"r");                                            //  open .po file
      if (! fidr) {
         printf("cannot open translation file: %s \n",pofile);             //  error, ignore file
         zfree(pofile);
         continue;
      }
      
      porec = 0;                                                           //  no .po record yet
      *Etext = *Ttext = 0;                                                 //  no text yet

      while (true)
      {
         if (! porec) porec = fgets_trim(buff,ZTXmaxcc,fidr);              //  get next .po record
         if (! porec) break;                                               //  EOF
         
         if (blank_null(porec)) {                                          //  blank record
            porec = 0;
            continue;
         }
         if (*porec == '#') {                                              //  comment
            porec = 0;
            continue;
         }

         if (strnEqu(porec,"msgid",5))                                     //  start new english string
         {
            if (*Etext) {                                                  //  two in a row
               printf("no translation: %s \n",Etext);
               *Etext = 0;
            }

            if (*Ttext) {
               printf("orphan translation: %s \n",Ttext);
               *Ttext = 0;
            }

            porec += 5;                                                    //  get segmented text string
            ZTXgettext(Etext);                                             //  "segment1 %s \n" "segment2" ...
         }

         else if (strnEqu(porec,"msgstr",6))                               //  start new translation
         {
            porec += 6;                                                    //  get segmented string
            ZTXgettext(Ttext);
            
            if (! *Etext) {
               printf("orphan translation: %s \n",Ttext);
               *Ttext = 0;
               continue;
            }

            if (strlen(Ttext) < 3) {                                       //  translation is ""
               printf("no translation: %s \n",Etext);
               strcpy(Ttext,Etext);                                        //  substitute english text
            }
         }
         
         else
         {
            printf("unrecognized .po record: %s \n",porec);
            porec = 0;
            continue;
         }
         
         if (*Etext && *Ttext)                                             //  have an english/translation pair
         {
            etext[Ntext] = strdupz(Etext,0,"ZTX");
            ttext[Ntext] = strdupz(Ttext,0,"ZTX");
            *Etext = *Ttext = 0;
            Ntext++;
            if (Ntext == ZTXmaxent)                                        //  cannot continue
               zappcrash("more than %d translations",ZTXmaxent);
         }
      }
      
      fclose(fidr);
      zfree(pofile);
   }

   printf(".po files have %d entries \n",Ntext);
   
   for (ii = 0; ii < Ntext; ii++)
   {
      pp = ZTXmergetext(etext[ii]);
      estring[ii] = strdupz(pp,0,"ZTX");
      pp = ZTXmergetext(ttext[ii]);
      tstring[ii] = strdupz(pp,0,"ZTX");
   }
   
   return;
}


//  private function
//  read and combine multiple 'msgid' or 'msgstr' quoted strings
//  output is one string with one or more quoted segments:
//    "text line 1 %s \n" "text line 2" ...
//  each segment comes from a different line in the input .po file

void ZTXnames::ZTXgettext(char *pstring)                                   //  v.4.3
{
   using namespace ZTXnames;

   int      cc, scc = 0;

   while (true)                                                            //  join multiple quoted strings
   {
      while (*porec && *porec != '"') porec++;                             //  find opening string quote
      if (! *porec) {
         porec = fgets_trim(buff,ZTXmaxcc,fidr);                           //  get next .po record
         if (! porec) return;
         if (strnEqu(porec,"msgid",5)) return;                             //  end of this string
         if (strnEqu(porec,"msgstr",6)) return;
      }
      ppq1 = porec;                                                        //  opening quote
      ppq2 = ppq1 + 1;
      while ((*ppq2 && *ppq2 != '"') ||                                    //  find closing (non-escaped) quote
             (*ppq2 == '"' && *(ppq2-1) == '\\')) ppq2++;
      if (! *ppq2) return;
      cc = ppq2 - ppq1 + 1;                                                //  min. case is ""
      if (cc + 1 + scc >= ZTXmaxcc)
         printf("string is too long %s \n",pstring);
      else {
         strncpy0(pstring+scc,ppq1,cc+1);                                  //  accum. substrings, minus quotes
         scc += cc;
      }
      porec = ppq2 + 1;
   }
   
   return;
}


//  private function
//  convert quoted string segments into binary form that 
//  matches the compiled string in the source program
//  (remove quotes, merge strings, un-escape \n \" etc.)

char * ZTXnames::ZTXmergetext(cchar *dirtystring)                          //  v.4.3
{
   static char    cleanstring[ZTXmaxcc];
   int            ii, jj;
   
   strncpy0(cleanstring,dirtystring,ZTXmaxcc);
   clean_escapes(cleanstring);
   
   for (ii = jj = 0; cleanstring[ii]; ii++)
      if (cleanstring[ii] != '"') 
         cleanstring[jj++] = cleanstring[ii];
   cleanstring[jj] = 0;
   return cleanstring;
}


//  Translate the input english string or return the input string.
//  Look for "context::string" and return "string" only if context found.
//  This function may need a few microseconds. This can be improved 
//  if needed by sorting the english strings and using a binary search.

cchar * ZTX(cchar *english)
{
   using namespace ZTXnames;

   cchar    *pp, *pp2;
   int      ii;
   
   if (! english) return "null";                                           //  v.4.3
   
   for (ii = 0; ii < Ntext; ii++)                                          //  find translation          v.4.3
      if (strEqu(english,estring[ii])) break;
   if (ii < Ntext) pp = tstring[ii];
   else pp = english;
   
   if (Ftranslate) {
      if (ii < Ntext) ZTX_translation_dump(ii);                            //  output translation
      else printf("message not in .po: %s \n",english);
   }
  
   for (pp2 = pp; *pp2 && pp2 < pp+30; pp2++)                              //  remove context if present
      if (*pp2 == ':' && *(pp2+1) == ':') return pp2+2;

   return pp;
}


/**************************************************************************

   Online Translation Utility

   Run the application to be translated in the usual manner. Select each
   menu or toolbar function needing translation. The related english text 
   strings and current translations are dumped into a text edit window. 
   The user can edit the translations while using the application and 
   therefore more easily understand the complete context.
   
   If the application menu "Translate" is selected, the function
   ZTX_translation_start() is called and the user dialog is used to
   set translation mode ON or OFF. If ON, the .po translation files 
   are copied from the install location to a user location (if not 
   already there) where they will be modified. These user .po files
   will be used for translations going forward.
   
   Whenever the application calls ZTX() to get a translation, the 
   function ZTX_translation_dump() is called to add the english text 
   and the current translation to the text edit window.
    ______________________________________________________
   |                                                      |
   |  msgid "English text with formats %d %d ... "        |
   |  msgstr "Deutscher Text mit Formats %d %s ... "      |
   |  ...                                                 |
   |                                [apply] [cancel]      |
   |______________________________________________________|
   
   As the application is exercised (menus and dialogs), all english text 
   and current translations are added to the window. Where no translation 
   is available, the english text is repeated as the translation. The user 
   may edit the window to update the translations. When done, using the 
   [apply] button updates the translation .po files in the user location.
   
   Limitation: 
   ZTXinit() merges .po files from locales lc_RC and lc alone if both are 
   present. Where both locales translate the same english string, then
   the one in lc_RC is used. ZTX_translation() also uses both locales but 
   does not know if a modified translation belongs to the locale lc_RC or 
   lc. If an english string is translated in both locales, any modified 
   translation is written back to the .po files in lc_RC only.

***************************************************************************/


//  This function is called from the application menu "Translate".
//  Set translation mode ON or OFF (zfuncs::Ftranslate). If ON and no 
//  user .po translation files (/home/<user>/.appname/locales/lc/*.po)
//  are found, then they are copied from the installed .po files to 
//  provide a starting point for changes. A text edit window is opened 
//  to display the english strings and their current translations, 
//  if any, and collect the edited updates.

void ZTX_translation_start()                                               //  new v.4.3
{
   using namespace zfuncs;
   using namespace ZTXnames;

   int ZTX_translation_event(zdialog *zd, cchar *event);

   zdialog  *zd;
   int      err, tron, trmissing;
   char     command[200];
   char     podir1[200], podir2[200];
   
   if (strnEqu(zlanguage,"en",2)) {                                        //  locale cannot be english
      zmessageACK(0,"cannot translate locale %s",zlanguage);
      return;
   }

/*    ___________________________________________
     |                                           |
     |  Translation mode: (o) ON  (o) OFF        |
     |  [x] Show missing translations only       |
     |                                           |
     |                     [done] [cancel]       |
     |___________________________________________|
*/

   zd = zdialog_new("Translation Mode",0,"done","cancel",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labm","hb1","Translation mode:","space=5");
   zdialog_add_widget(zd,"radio","tron","hb1","ON","space=5");
   zdialog_add_widget(zd,"radio","troff","hb1","OFF","space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","trmissing","hb2","Show missing translations only");
   
   zdialog_stuff(zd,"tron",1);
   zdialog_stuff(zd,"troff",0);
   zdialog_stuff(zd,"trmissing",1);
   
   zdialog_run(zd);                                                        //  run dialog
   zdialog_wait(zd);

   Ftranslate = 0;

   if (zd->zstat == 1) {                                                   //  [done]
      zdialog_fetch(zd,"tron",tron);
      zdialog_fetch(zd,"trmissing",trmissing);
      if (tron) Ftranslate = 1;                                            //  dump all translations
      if (tron && trmissing) Ftranslate = 2;                               //  dump only missing translations
   }
   zdialog_free(zd);

   if (Ftranslate)                                                         //  start or continue translation mode
   {
      strncpy0(lc_RC,zlanguage,6);                                         //  current locale, lc or lc_RC
      
      for (int pass = 1; pass <= 2; pass++)
      {
         if (pass == 1 && strlen(lc_RC) < 3) continue;                     //  read .po files for locale lc_RC
         if (pass == 2) lc_RC[2] = 0;                                      //  read .po files for locale lc

         snprintf(podir1,199,"%s/%s",zlocalesdir,lc_RC);                   //  installed .po files for lc_RC
         snprintf(podir2,199,"%s/locales/%s",zuserdir,lc_RC);              //  user .po files for new translations
         
         snprintf(command,199,"find %s/*.po",podir2);                      //  look for user .po files
         err = system(command);                                            //  (resume translation project)
         if (err) {
            snprintf(command,199,"find %s/*.po",podir1);                   //  look for installed .po files
            err = system(command);
            if (err) printf("No .po files found in %s \n",podir1);
            else {
               snprintf(command,199,"mkdir -p -m 0750 %s",podir2);         //  create directory if not already
               err = system(command);
               snprintf(command,199,"cp %s/*.po %s",podir1,podir2);        //  copy installed .po files to user
               err = system(command);
               if (! err) printf(".po files copied from %s to %s \n",podir1,podir2);
               else printf("cannot copy .po files from %s to %s \n",podir1,podir2);
            }
         }
      }
      
      ZTXinit(zlanguage);                                                  //  re-initialize translations

      zddump = zdialog_new("Translations",0,"apply","cancel",null);        //  create dialog for translations
      zdialog_add_widget(zddump,"scrwin","scrwin","dialog",0,"expand");
      zdialog_add_widget(zddump,"edit","edit","scrwin");
      zdialog_resize(zddump,600,400);
      zdialog_run(zddump,ZTX_translation_event);
      trwin = zdialog_widget(zddump,"edit");                               //  text edit window in dialog
   }

   return;
}


//  dialog event and completion function

int ZTX_translation_event(zdialog *zd, cchar *event)                      //  v.4.3
{
   using namespace ZTXnames;
   
   if (! zd->zstat) return 1;                                              //  wait for [apply] or [cancel]

   if (zd->zstat == 1)                                                     //  [apply]
      ZTX_translation_update();                                            //  update user .po files 

   Ftranslate = 0;
   zdialog_free(zd);                                                       //  kill edit window
   zddump = 0;
   trwin = 0;
   return 1;
}


//  This function is called from ZTX() for every translation.
//  Add the english text and current translation to the text edit window.

void ZTXnames::ZTX_translation_dump(int index)                             //  new v.4.3
{
   using namespace zfuncs;
   using namespace ZTXnames;
   
   char     outstring[ZTXmaxcc];
   char     *pp1, *pp2;
   int      cc;
   
   if (strEqu(zlanguage,"en")) return;
   if (! zddump) return;                                                   //  no translation edit window

   if (! Ftranslate) return;                                               //  translate mode OFF
   if (Ftranslate == 2 &&                                                  //  only missing translations wanted
        strNeq(etext[index],ttext[index])) return;                         //    and this translation not missing

   strcpy(outstring,"msgid ");                                             //  msgid "english line 1 %s \n"
   pp1 = etext[index];                                                     //        "english line 2"
                                                                           //         ...
   while (*pp1) {
      while (*pp1 && *pp1 != '"') pp1++;
      pp2 = pp1 + 1;
      while (*pp1 && *pp2 && *pp2 != '"') pp2++;
      if (*pp1 && *pp2) {
         cc = strlen(outstring);
         strncpy0(outstring+cc,pp1,pp2-pp1+2);
         wprintf(trwin,"%s\n",outstring);
         *outstring = 0;
         pp1 = pp2 + 1;
      }
   }
   
   strcpy(outstring,"msgstr ");                                            //  msgstr "translation line 1 %s \n"
   pp1 = ttext[index];                                                     //         "translation line 2"
                                                                           //          ...
   while (*pp1) {
      while (*pp1 && *pp1 != '"') pp1++;
      pp2 = pp1 + 1;
      while (*pp1 && *pp2 && *pp2 != '"') pp2++;
      if (*pp1 && *pp2) {
         cc = strlen(outstring);
         strncpy0(outstring+cc,pp1,pp2-pp1+2);
         wprintf(trwin,"%s\n",outstring);
         *outstring = 0;
         pp1 = pp2 + 1;
      }
   }
   
   wprintf(trwin,"\n");                                                    //  blank line between pairs

   return;
}


//  merge translation edit window with user .po files and clear the window

void ZTXnames::ZTX_translation_update()                                    //  new v.4.3
{
   using namespace zfuncs;
   using namespace ZTXnames;

   int         contx, ii, err, ftf, Nupd, Npos;
   char        *pofile1, pofile2[200], *pofiles[100], podir[200];
   char        command[200], command2[400];
   char        *pp, Fused[ZTXmaxent];

   ftf = 1;
   wporec = 0;
   *Etext = *Ttext = 0;
   Nupd = 0;

   while (true)                                                            //  scan translation edit window
   {
      if (! wporec) wporec = wscanf(trwin,ftf);
      if (! wporec) break;                                                 //  EOF

      if (blank_null(wporec)) {                                            //  blank record
         wporec = 0;
         continue;
      }

      if (strnEqu(wporec,"msgid",5))                                       //  start new english string
      {
         if (*Etext) {                                                     //  two in a row
            printf("no translation: %s \n",Etext);
            *Etext = 0;
         }

         if (*Ttext) {                                                     //  should not happen
            printf("orphan translation: %s \n",Ttext);
            *Ttext = 0;
         }

         wporec += 5;                                                      //  get segmented text string
         ZTXgettext2(Etext);                                               //  "segment1 %s \n" "segment2" ...
      }

      else if (strnEqu(wporec,"msgstr",6))                                 //  start new translation
      {
         wporec += 6;                                                      //  get segmented string
         ZTXgettext2(Ttext);
         
         if (! *Ttext) {
            if (*Etext) printf("no translation: %s \n",Etext);
            *Etext = 0;
         }
         else if (! *Etext) {                                              //  orphan or redundant translation
            printf("orphan translation: %s \n",Ttext);
            *Ttext = 0;
         }
      }
      
      else
      {
         printf("unrecognized record: %s \n",wporec);
         wporec = 0;
         continue;
      }
      
      if (*Etext && *Ttext)                                                //  have an english/translation pair
      {
         for (ii = 0; ii < Ntext; ii++)                                    //  find existing (prior) entry
            if (strEqu(Etext,etext[ii])) break;                            //    in translation tables
         if (ii == Ntext) 
            printf("English changed, translation ignored: %s \n",Etext);
         else if (strNeq(Ttext,ttext[ii])) {                               //  translation was updated
            zfree(ttext[ii]);
            ttext[ii] = strdupz(Ttext,0,"ZTX");
            Nupd++;
         }
         *Etext = *Ttext = 0;
      }
   }

   printf("%d new translations found \n",Nupd);
   if (! Nupd) return;                                                     //  nothing new

   strncpy0(lc_RC,zlanguage,6);                                            //  current locale, lc_RC or lc only
   Npos = 0;
   
   for (int pass = 1; pass <= 2; pass++)                                   //  loop two locale codes
   {
      if (pass == 1 && strlen(lc_RC) < 3) continue;                        //  locale = lc_RC (if RC present)
      if (pass == 2) lc_RC[2] = 0;                                         //  then locale = lc

      snprintf(podir,199,"%s/locales/%s",zuserdir,lc_RC);                  //  user .po files for new translations
      snprintf(command,199,"find %s/*.po",podir);                          //  look for user .po files
      strcat(command," 2>/dev/null");                                      //  suppress error messages

      contx = 0;
      while ((pofile1 = command_output(contx,command)))                    //  find each .po file
      {
         if (Npos == 100) zappcrash("exceed 100 .po files");
         pofiles[Npos++] = pofile1;                                        //  add to ordered list of .po files
      }
   }

   memset(Fused,0,ZTXmaxent);
   Nupd = 0;

   for (int kpo = 0; kpo < Npos; kpo++)                                    //  loop each .po file
   {
      fidr = fopen(pofiles[kpo],"r");                                      //  open .po file for read
      if (! fidr) {
         printf("cannot read translation file: %s \n",pofiles[kpo]);
         return;
      }
      
      strcpy(pofile2,pofiles[kpo]);                                       //  open .po.new file for write
      strcat(pofile2,".new");
      fidw = fopen(pofile2,"w");
      if (! fidw) {
         printf("cannot write updated translation file: %s \n",pofile2);
         fclose(fidr);
         return;
      }

      porec = 0;
      *Etext = *Ttext = 0;

      while (true)                                                         //  loop each .po record
      {
         if (! porec) porec = fgets_trim(buff,ZTXmaxcc,fidr);              //  next record
         if (! porec) break;                                               //  EOF

         if (strnEqu(porec,"msgid",5)) {                                   //  msgid: "english 1 %s \n" "english 2" ...
            porec += 5;
            ZTXgettext(Etext);                                             //  aggregate strings
         }

         else if (strnEqu(porec,"msgstr",6)) {                             //  msgstr: "translation %s \n" ...
            porec += 6;
            ZTXgettext(Ttext);                                             //  aggregate strings
         }

         else {
            fprintf(fidw,"%s\n",porec);                                    //  other record, copy to output
            porec = 0;
            continue;
         }
         
         if (*Etext && *Ttext)                                             //  have an english/translation pair
         {
            for (ii = 0; ii < Ntext; ii++)                                 //  find matching english in
               if (strEqu(Etext,etext[ii])) break;                         //    existing translations
            if (ii < Ntext && ! Fused[ii]) {
               if (strNeq(etext[ii],ttext[ii]) && 
                   strNeq(Ttext,ttext[ii])) {                              //  translation added or updated
                  printf("changed: %s %s \n",etext[ii],ttext[ii]);
                  strcpy(Ttext,ttext[ii]);                                 //  substitute new translation
                  Nupd++;                                                  //  actual .po updates made
               }
               Fused[ii] = 1;                                              //  mark english string as used
            }
            
            ZTXwritetext("msgid",Etext);                                   //  output msgid: strings
            ZTXwritetext("msgstr",Ttext);                                  //  and msgstr: strings
            *Etext = *Ttext = 0;
         }
      }

      fclose(fidr);
      fclose(fidw);
   }

   for (ii = 0; ii < Npos; ii++)                                           //  free memory
      zfree(pofiles[ii]);
   
   printf("new .po files have %d updated translations \n",Nupd);
   if (! Nupd) return;

   strncpy0(lc_RC,zlanguage,6);                                            //  current locale, lc_RC or lc only

   for (int pass = 1; pass <= 2; pass++)                                   //  loop two locale codes
   {
      if (pass == 1 && strlen(lc_RC) < 3) continue;                        //  locale = lc_RC (if RC present)
      if (pass == 2) lc_RC[2] = 0;                                         //  then locale = lc

      snprintf(podir,199,"%s/locales/%s",zuserdir,lc_RC);                  //  user .po files for new translations
      snprintf(command,199,"find %s/*.po.new",podir);                      //  look for .po.new files
      strcat(command," 2>/dev/null");                                      //  suppress error messages

      contx = 0;
      while ((pofile1 = command_output(contx,command)))                    //  loop each .po.new file
      {
         strcpy(pofile2,pofile1);
         pp = strstr(pofile2,".new");
         if (pp) *pp = 0;
         snprintf(command2,399,"mv -f %s %s",pofile1,pofile2);             //  move *.po.new to *.po (replace)
         err = system(command2);
         if (err) printf("cannot copy %s to *.po",pofile1);
         zfree(pofile1);
      }
   }
   
   ZTXinit(zlanguage);                                                     //  put translations in use        

   return;
}


//  private function
//  read and combine multiple 'msgid' or 'msgstr' quoted strings
//  output is one string with one or more quoted segments:
//    "text line 1 %s \n" "text line 2" ...
//  each segment comes from a different line in the translation edit window

void ZTXnames::ZTXgettext2(char *pstring)                                  //  v.4.3
{
   using namespace ZTXnames;

   int      cc, scc = 0, ftf = 0;

   while (true)                                                            //  join multiple quoted strings
   {
      while (*wporec && *wporec != '"') wporec++;                          //  find opening string quote
      if (! *wporec) {
         wporec = wscanf(trwin,ftf);
         if (! wporec) return;
         if (strnEqu(wporec,"msgid",5)) return;                            //  end of this string
         if (strnEqu(wporec,"msgstr",6)) return;
      }
      ppq1 = wporec;                                                       //  opening quote
      ppq2 = ppq1 + 1;
      while ((*ppq2 && *ppq2 != '"') ||                                    //  find closing (non-escaped) quote
             (*ppq2 == '"' && *(ppq2-1) == '\\')) ppq2++;
      if (! *ppq2) {
         wporec = 0;
         return;
      }
      cc = ppq2 - ppq1 + 1;                                                //  min. case is ""
      if (cc + 1 + scc >= ZTXmaxcc)
         printf("string is too long %s \n",pstring);
      else {
         strncpy0(pstring+scc,ppq1,cc+1);                                  //  accum. substrings, minus quotes
         scc += cc;
      }
      wporec = ppq2 + 1;
   }
   
   return;
}


//  private function
//  write quoted seqmented string to output .po file, 1 record per segment
//       msgid "english string segment 1 %s \n" 
//             "english string segment 2 \n"
//              ...

void ZTXnames::ZTXwritetext(cchar *header, char *text)
{
   char     *pp1, *pp2;
   int      cc, ftf;
   char     segment[ZTXmaxcc];

   ftf = 1;
   pp1 = text;

   while (true)
   {
      while (*pp1 && *pp1 != '"') pp1++;                                   //  opening quote
      if (! *pp1) break;
      pp2 = pp1 + 1;
      while ((*pp2 && *pp2 != '"') ||                                      //  find closing (non-escaped) quote
             (*pp2 == '"' && *(pp2-1) == '\\')) pp2++;
      if (! *pp2) break;
      cc = pp2 - pp1 + 1;
      if (cc >= ZTXmaxcc) cc = ZTXmaxcc - 1;
      strncpy0(segment,pp1,cc+1);
      if (ftf) fprintf(fidw,"%s %s\n",header,segment);
      else fprintf(fidw,"%s\n",segment);
      ftf = 0;
      pp1 = pp2 + 1;
   }

   return;
}


/**************************************************************************/

//  functions to lock GDK/GTK calls from threads only
//    - do nothing if called from main thread (avoid fatal bug)
//    - detect nested calls and make inoccuous (avoid fatal bug)

#define     tmax 20                                                        //  max. simultaneous GTK threads
pthread_t   tid_main = 0;
pthread_t   tids[tmax];
int         tlocks[tmax];
int         zinit = 0;
mutex       zmutex;

void zlockInit()                                                           //  initz. call from main()
{
   if (zinit) zappcrash("zlockInit() again");                              //  redundant call

   tid_main = pthread_self();
   mutex_init(&zmutex,null);
   zinit++;

   for (int ii = 0; ii < tmax; ii++) {
      tids[ii] = 0; 
      tlocks[ii] = 0;
   }
   return;
}

void zlock()                                                               //  lock GTK if in a thread
{
   int         ii;
   pthread_t   tid_me;

   if (! zinit) zappcrash("zlock(): zlockInit() not done");

   tid_me = pthread_self();
   if (pthread_equal(tid_main,tid_me)) return;                             //  main() thread, do nothing

   mutex_lock(&zmutex);

   for (ii = 0; ii < tmax; ii++)                                           //  find my thread slot
         if (pthread_equal(tids[ii],tid_me)) goto zret;
   for (ii = 0; ii < tmax; ii++)                                           //  find a free slot
         if (tids[ii] == 0) goto zret;
   zappcrash("zlock(): too many threads");

zret:
   tids[ii] = tid_me;
   ++tlocks[ii];
   mutex_unlock(&zmutex);

   if (tlocks[ii] == 1) gdk_threads_enter();                               //  1st lock this thread, lock GTK
   return;                                                                 //  else nested lock, ignore
}

void zunlock()                                                             //  unlock GTK if in a thread
{
   int         ii;
   pthread_t   tid_me;

   tid_me = pthread_self();
   if (pthread_equal(tid_main,tid_me)) return;                             //  main() thread, do nothing

   for (ii = 0; ii < tmax; ii++)                                           //  find this thread
      if (pthread_equal(tids[ii],tid_me)) break;
   if (ii == tmax) zappcrash("zunlock(): not locked");

   --tlocks[ii];                                                           //  decrement locks
   if (tlocks[ii] == 0) {
      tids[ii] = 0;                                                        //  last lock removed, free slot
      gdk_flush();
      gdk_threads_leave();                                                 //  unlock GTK
   }
   return;
}


/**************************************************************************/

//  crash if current execution is not the main() thread

inline void zthreadcrash()
{
   if (pthread_equal(pthread_self(),tid_main)) return;
   zappcrash("forbidden function called from thread");
   return;
}


/**************************************************************************/

//  Iterate main loop every "skip" calls and only if in main() thread

void zmainloop(int skip)                                                   //  v.2.8
{
   static int  xskip = 0;
   pthread_t   tid_me;
   
   if (! zinit) zappcrash("zmainloop(): zinit() not done");

   if (skip) {
      if (++xskip < skip) return;
      xskip = 0;
   }

   tid_me = pthread_self();
   if (! pthread_equal(tid_main,tid_me)) return;                           //  not main()

   while (gtk_events_pending()) gtk_main_iteration(); 
   return;
}


/**************************************************************************/

//  write message to text view window
//  line:   +N    existing lines from top (replace)
//          -N    existing lines from bottom (replace)
//           0    next line (add new line at bottom)
//  scroll logic assumes only one \n per message

void wprintx(GtkWidget *mLog, int line, cchar *message, cchar *font)
{
   GtkTextMark          *endMark;
   GtkTextBuffer        *textBuff;
   GtkTextIter          iter1, iter2;
   GtkTextTag           *fontag = 0;
   int                  nlines, scroll = 0;
   
   if (! mLog) {                                                           //  if no GUI use STDOUT   v.3.0
      printf("%s",message);
      return;
   }
   
   zlock();
   
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));

   endMark = gtk_text_buffer_get_mark(textBuff,"wpxend");                  //  get my end mark

   if (! endMark) {
      gtk_text_buffer_get_end_iter(textBuff,&iter1);                       //  new buffer, set my end mark
      endMark = gtk_text_buffer_create_mark(textBuff,"wpxend",&iter1,0);
   }

   nlines = gtk_text_buffer_get_line_count(textBuff);                      //  lines now in buffer

   if (line == 0) scroll++;                                                //  auto scroll is on

   if (line < 0) {
      line = nlines + line + 1;                                            //  last lines: -1, -2 ...
      if (line < 1) line = 1;                                              //  above top, use line 1
   }
   
   if (line > nlines) line = 0;                                            //  below bottom, treat as append

   if (line == 0) gtk_text_buffer_get_end_iter(textBuff,&iter1);           //  append new line
   
   if (line > 0) {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1);            //  old line start
      if (line < nlines)
          gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line);          //  old line end
      if (line == nlines)                                                  //    or buffer end
          gtk_text_buffer_get_end_iter(textBuff,&iter2);                   
      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                      //  delete old line
   }

   if (font) {                                                             //  insert new line with caller font
      fontag = gtk_text_buffer_create_tag(textBuff,0,"font",font,0);       //  fontag is textBuff specific        v.3.9
      gtk_text_buffer_insert_with_tags(textBuff,&iter1,message,-1,fontag,null);
   }
   else                                                                    //  insert new line with default font
      gtk_text_buffer_insert(textBuff,&iter1,message,-1);

   if (scroll)                                                             //  scroll line into view
      gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),endMark,0,0,1,1);
   
   zunlock();
   return;
}

void wprintf(GtkWidget *mLog, int line, cchar *mess, ... )                 //  "printf" version
{
   va_list  arglist;
   char     message[1000];

   va_start(arglist,mess);
   vsnprintf(message,999,mess,arglist);
   va_end(arglist);

   wprintx(mLog,line,message);
   return;
}

void wprintf(GtkWidget *mLog, cchar *mess, ... )                           //  "printf", scrolling output
{
   va_list  arglist;
   char     message[1000];

   va_start(arglist,mess);
   vsnprintf(message,999,mess,arglist);                                    //  stop overflow, remove warning
   va_end(arglist);

   wprintx(mLog,0,message);
   return;
}


/**************************************************************************/

//  scroll a text view window to put a given line on screen
//  1st line = 1.  for last line use line = 0.

void wscroll(GtkWidget *mLog, int line)                                    //  v.2.3
{
   GtkTextBuffer  *textbuff;
   GtkTextIter    iter;
   GtkTextMark    *mark;
   
   if (! mLog) return;                                                     //  v.3.0
   
   zlock();
   textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   if (line <= 0) line = gtk_text_buffer_get_line_count(textbuff);
   line = line - 1;
   gtk_text_buffer_get_iter_at_line(textbuff,&iter,line);
   mark = gtk_text_buffer_create_mark(textbuff,0,&iter,0);                 //  bugfix, gtk_text_view_scroll_to_iter()
   gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),mark,0,0,1,1);         //    fails with no error     v.4.0
   zunlock();
   return;
}


/**************************************************************************/

//  clear a text view window and get a new buffer (a kind of defrag)

void wclear(GtkWidget *mLog)
{
   GtkTextBuffer  *buff;

   if (! mLog) return;                                                     //  v.3.0

   zlock();
   buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   gtk_text_buffer_set_text(buff,"",-1);
   zunlock();
   return;
}


//  clear a text view window from designated line to end of buffer

void wclear(GtkWidget *mLog, int line)
{
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   if (! mLog) return;                                                     //  v.3.0

   zlock();
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1);               //  iter at line start
   gtk_text_buffer_get_end_iter(textBuff,&iter2);
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                         //  delete existing line
   zunlock();
   return;
}


/**************************************************************************/

//  get text records from a text view window, one per call
//  removes trailing new line characters ( \n )

char * wscanf(GtkWidget *mLog, int & ftf)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   static char    *precs = 0, *prec1, *pret;
   static int     cc;
   
   if (! mLog) return 0;                                                   //  v.3.0
   
   if (ftf)
   {                                                                       //  get all window text
      ftf = 0;
      if (precs) g_free(precs);                                            //  free prior memory if there
      zlock();
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));            //  get all text
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      precs = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
      prec1 = precs;                                                       //  1st record
      zunlock();
   }
   
   if (! precs || (*prec1 == 0))                                           //  no more records
   {
      if (precs) g_free(precs);
      precs = 0;
      return 0;
   }

   cc = 0;
   while ((prec1[cc] != 0) && (prec1[cc] != '\n')) cc++;                   //  scan for terminator
   pret = prec1;
   prec1 = prec1 + cc;                                                     //  next record
   if (*prec1 == '\n') prec1++;
   pret[cc] = 0;                                                           //  replace \n with 0
   return pret;
}


/**************************************************************************/

//  dump text window into file
//  return:  0: OK  +N: error

int   wfiledump_maxcc = 0;

int wfiledump(GtkWidget *mLog, char *filespec)
{
   FILE        *fid;
   char        *prec;
   int         ftf, err, cc;

   if (! mLog) return 0;                                                   //  v.3.0

   fid = fopen(filespec,"w");                                              //  open file
   if (! fid) { 
      zmessageACK(null,ZTX("cannot open file %s"),filespec);
      return 1; 
   }
   
   wfiledump_maxcc = 0;
   
   ftf = 1;
   while (true)
   {
      prec = wscanf(mLog,ftf);                                             //  get text line
      if (! prec) break;
      fprintf(fid,"%s\n",prec);                                            //  output with \n
      cc = strlen(prec);
      if (cc > wfiledump_maxcc) wfiledump_maxcc = cc;
   }
   
   err = fclose(fid);                                                      //  close file
   if (err) { zmessageACK(null,"file close error"); return 2; }
   else return 0;
}


/**************************************************************************/

//  save text window to file, via file chooser dialog

void wfilesave(GtkWidget *mLog)
{
   int      err;
   char     *file;

   if (! mLog) return;                                                     //  v.3.0

   file = zgetfile1(ZTX("save screen to file"),"save","screen-save.txt");
   if (! file) return;
   err = wfiledump(mLog,file);
   if (err) zmessageACK(null,"save screen failed (%d)",err);
   zfree(file);
   return;
}


/**************************************************************************/

//  print text window to default printer
//  use landscape mode if max. print line > A4 width

void wprintp(GtkWidget *mLog)
{
   int      pid, err;
   char     tempfile[50], command[200];

   if (! mLog) return;                                                     //  v.3.0

   pid = getpid();
   snprintf(tempfile,49,"/tmp/wprintp-%d",pid);
   err = wfiledump(mLog,tempfile);
   if (err) return;

   if (wfiledump_maxcc < 97)
      snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s %s",
                     "cpi=14","lpi=8","page-left=50","page-top=50",
                     "page-right=40","page-bottom=40",tempfile);

   else
      snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s",
                     "landscape","cpi=14","lpi=8","page-left=50","page-top=50",
                     "page-right=40","page-bottom=40",tempfile);

   err = system(command);
   if (err) zmessLogACK(null,"print error %s",wstrerror(err));
   return;
}


/**************************************************************************
   simplified GTK menu bar, tool bar, status bar functions
   no locking, use in main() thread only
***************************************************************************/

int            tbIconSize = 24;                                            //  valid during toolbar construction
GtkTooltips    *tbTooltips = 0;                                            //  one instance for all toolbars


//  create menu bar and add to vertical packing box

GtkWidget * create_menubar(GtkWidget *vbox)                                //  icon size removed    v.2.29
{
   GtkWidget   *wmbar;

   wmbar = gtk_menu_bar_new(); 
   gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0);
   return wmbar;
}


//  add menu item to menu bar

GtkWidget * add_menubar_item(GtkWidget *wmbar, cchar *mname, mtFunc func)
{
   GtkWidget   *wmitem;

   wmitem = gtk_menu_item_new_with_label(mname);
   gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem);
   if (func) G_SIGNAL(wmitem,"activate",func,mname);
   return  wmitem;
}


//  add submenu item to menu item, optional response function
//  icon code removed                                                      //  v.2.29

GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, mtFunc func)
{
   GtkWidget      *wmsub, *wmsubitem;
   GtkWidget      *wicon = 0;

   wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem));
   if (wmsub == null) {
      wmsub = gtk_menu_new();
      gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub);
   }

   if (strEqu(mlab,"separator"))                                           //  v.2.16
      wmsubitem = gtk_separator_menu_item_new();
   else {
      if (wicon) {
         wmsubitem = gtk_image_menu_item_new_with_label(mlab);
         gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(wmsubitem),wicon);
      }
      else  wmsubitem = gtk_menu_item_new_with_label(mlab);
   }

   gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem);
   if (func) G_SIGNAL(wmsubitem,"activate",func,mlab);
   return  wmsubitem;
}


//  create toolbar and add to vertical packing box

GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize, int vert)
{
   using namespace zfuncs;

   GtkWidget   *wtbar;

   wtbar = gtk_toolbar_new();
   if (vert) gtk_toolbar_set_orientation(GTK_TOOLBAR(wtbar),GTK_ORIENTATION_VERTICAL);
   gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0);
   tbIconSize = iconsize;
   
   if (! tbTooltips) tbTooltips = gtk_tooltips_new();
   return  wtbar;
}


//  add toolbar button with stock icon ("gtk-quit") or custom icon ("iconfile.png")

GtkWidget * add_toolbar_button(GtkWidget *wtbar, cchar *blab, cchar *btip, cchar *icon, mtFunc func)
{
   using namespace zfuncs;

   GtkToolItem    *tbutton;
   GError         *gerror = 0;
   GdkPixbuf      *pixbuf;
   GtkWidget      *wicon = 0;
   char           iconpath[200];
   
   if (blab && strEqu(blab,"separator")) {                                 //  v.4.3
      tbutton = gtk_separator_tool_item_new();
      gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1);
      return  (GtkWidget *) tbutton;
   }

   if (icon == null || *icon == 0) 
      tbutton = gtk_tool_button_new(0,0);

   else if (strnEqu(icon,"gtk-",4)) 
      tbutton = gtk_tool_button_new_from_stock(icon);

   else {
      *iconpath = 0;
      strncatv(iconpath,199,zicondir,"/",icon,null);
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,&gerror);
      if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf);
      if (wicon) tbutton = gtk_tool_button_new(wicon,0);
      else  tbutton = gtk_tool_button_new_from_stock("gtk-missing-image");
   }

   if (blab) gtk_tool_button_set_label(GTK_TOOL_BUTTON(tbutton),blab);
   if (btip) gtk_tool_item_set_tooltip(tbutton,tbTooltips,btip,"");
   gtk_tool_item_set_homogeneous(tbutton,0);                               //  v.3.5
   gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1);
   if (func) G_SIGNAL(tbutton,"clicked",func,blab);
   return  (GtkWidget *) tbutton;
}


//  create a status bar and add to the start of a packing box

GtkWidget * create_stbar(GtkWidget *pbox)
{
   GtkWidget      *stbar;
   static PangoFontDescription    *fontdesc;

   stbar = gtk_statusbar_new(); 
   fontdesc = pango_font_description_from_string("Monospace 9");
   gtk_widget_modify_font(stbar,fontdesc);                                 //  *** GTK does not work ***
   gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0);
   gtk_widget_show(stbar);
   return  stbar;
}


//  display message in status bar - callable from threads

int stbar_message(GtkWidget *wstbar, cchar *message)
{
   static int     ctx = -1;

   zlock();
   if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all");
   gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx);
   gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message);
   zunlock();
   return 0;
}


/**************************************************************************
   simplified GTK dialog functions
***************************************************************************/

//  create a new zdialog dialog
//  optional arguments: up to zdmaxbutts button labels followed by null
//  returned dialog status: +N = button N (1 to zdmaxbutts)
//                          <0 = [x] button or other GTK destroy action

//  v.3.5 overhaul:
//  build own completion buttons, replacing the fat GTK standard buttons
//  completion buttons are also events like other widgets
//  get rid of separate dialog completion function
//  all dialogs run parallel, use zdialog_wait() if needed

//  private functions for widget events and dialog completion

void  zdialog_widget_event(GtkWidget *, zdialog *zd);
void  zdialog_response_event(GtkWidget *, int stat, zdialog *zd);


zdialog * zdialog_new(cchar *title, GtkWidget *parent, ...)                //  parent added     v.2.5
{
   zdialog        *zd;
   GtkWidget      *dialog, *hbox, *butt, *hsep;
   cchar          *bulab[zdmaxbutts];
   int            cc, ii, nbu;
   va_list        arglist;

   va_start(arglist,parent);
   for (nbu = 0; nbu < zdmaxbutts; nbu++)                                  //  get completion buttons
   {
      bulab[nbu] = va_arg(arglist, cchar *);
      if (! bulab[nbu]) break;
   }
   va_end(arglist);
   
   zlock();
   
   dialog = gtk_dialog_new();                                              //  attributes optional    v.3.3
   if (title) gtk_window_set_title(GTK_WINDOW(dialog),title);
   gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog)->vbox),2);
   gtk_dialog_set_has_separator(GTK_DIALOG(dialog),0);

   cc = sizeof(zdialog);                                                   //  allocate zdialog
   zd = (zdialog *) zmalloc(cc,"zdialog");                                 //  v.3.5

   if (parent) {                                                           //  v.4.4
      zd->parent = parent;
      gtk_window_set_transient_for(GTK_WINDOW(dialog),GTK_WINDOW(parent));
   }

   if (nbu) {                                                              //  there are some completion buttons
      hbox = gtk_hbox_new(0,0);
      gtk_box_pack_end(GTK_BOX(GTK_DIALOG(dialog)->vbox),hbox,0,0,0);      //  add hbox for buttons at dialog bottom
      hsep = gtk_hseparator_new();                                         //  add separator line
      gtk_box_pack_end(GTK_BOX(GTK_DIALOG(dialog)->vbox),hsep,0,0,5);

      for (ii = nbu-1; ii >= 0; ii--) {                                    //  add buttons to hbox
         butt = gtk_button_new_with_label(bulab[ii]);                      //  reverse order nbu-1...0
         gtk_box_pack_end(GTK_BOX(hbox),butt,0,0,5);
         G_SIGNAL(butt,"clicked",zdialog_widget_event,zd)                  //  connect to event function
         zd->compbutt[ii] = butt;                                          //  save button widgets
      }
   }
   
   zunlock();

   zd->sentinel = zdsentinel;                                              //  validity sentinel
   zd->eventCB = 0;                                                        //  no user event callback function
   zd->zstat = 0;                                                          //  no zdialog status
   zd->disabled = 1;                                                       //  widget signals disabled
   zd->saveposn = 0;                                                       //  position not saved     v.4.4
   zd->help_topic = 0;                                                     //  no help topic          v.4.5

   zd->widget[0].type = "dialog";                                          //  set up 1st widget = dialog
   zd->widget[0].name = "dialog";
   zd->widget[0].pname = 0;
   zd->widget[0].data = strdupz(title,0,"zdialog");                        //  v.3.5
   zd->widget[0].cblist = 0;
   zd->widget[0].widget = dialog;

   zd->widget[1].type = 0;                                                 //  eof - no contained widgets yet
   return zd;
}


//  add widget to existing zdialog

int zdialog_add_widget (
     zdialog *zd, cchar *type, cchar *name, cchar *pname,                  //  mandatory args
     cchar *data, int scc, int homog, int expand, int space, int wrap)     //  optional args (default = 0)
{
   GtkWidget      *widget = 0, *pwidget = 0;
   GtkTextBuffer  *editBuff = 0;
   GdkColor       gdkcolor;
   cchar          *pp, *ptype = 0;
   char           vdata[30];
   double         min, max, step, val;
   int            iiw, iip, kk, err;

   static PangoFontDescription    *monofont = 0;
   
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   for (iiw = 1; zd->widget[iiw].type; iiw++);                             //  find next avail. slot
   if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw);

   zd->widget[iiw].type = strdupz(type,0,"zdialog");                       //  initz. widget struct       v.3.5
   zd->widget[iiw].name = strdupz(name,0,"zdialog");                       //  all strings in nonvolatile mem
   zd->widget[iiw].pname = strdupz(pname,0,"zdialog");
   zd->widget[iiw].data = 0;
   zd->widget[iiw].cblist = 0;
   zd->widget[iiw].scc = scc;
   zd->widget[iiw].homog = homog;
   zd->widget[iiw].expand = expand;
   zd->widget[iiw].space = space;
   zd->widget[iiw].wrap = wrap;
   zd->widget[iiw].widget = 0;

   zd->widget[iiw+1].type = 0;                                             //  new EOF marker

   if (strcmpv(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin",
                    "label","link","entry","edit","button","togbutt","check","combo",
                    "comboE","radio","spin","hscale","vscale","colorbutt", null) == 0)
      zappcrash("zdialog, bad widget type: %s",type);

   for (iip = iiw-1; iip >= 0; iip--)                                      //  find parent (container) widget
      if (strEqu(pname,zd->widget[iip].name)) break;
   if (iip < 0) zappcrash("zdialog, no parent for widget: %s",name);

   pwidget = zd->widget[iip].widget;                                       //  parent widget, type
   ptype = zd->widget[iip].type;
   
   if (strcmpv(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0)
      zappcrash("zdialog, bad widget parent type: %s",ptype);
   
   zlock();

   if (! monofont) monofont = pango_font_description_from_string("Monospace");

   if (strEqu(type,"hbox")) widget = gtk_hbox_new(homog,space);            //  expandable container boxes
   if (strEqu(type,"vbox")) widget = gtk_vbox_new(homog,space);

   if (strEqu(type,"hsep")) widget = gtk_hseparator_new();                 //  horiz. & vert. separators
   if (strEqu(type,"vsep")) widget = gtk_vseparator_new();
         
   if (strEqu(type,"frame")) {                                             //  frame around contained widgets
      widget = gtk_frame_new(data);
      gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN);
   }

   if (strEqu(type,"scrwin")) {                                            //  scrolled window container
      widget = gtk_scrolled_window_new(0,0);
      gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),          //  v.2.10
                        GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
   }

   if (strEqu(type,"label")) widget = gtk_label_new(data);                 //  label (static text)

   if (strEqu(type,"link")) {                                              //  label is clickable    v.4.0
      widget = gtk_link_button_new(data);
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd);
   }

   if (strEqu(type,"entry")) {                                             //  1-line text entry
      widget = gtk_entry_new();
      if (data) gtk_entry_set_text(GTK_ENTRY(widget),data);
      if (scc) gtk_entry_set_width_chars(GTK_ENTRY(widget),scc);
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"edit")) {                                              //  multiline edit box
      widget = gtk_text_view_new();
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      if (data) gtk_text_buffer_set_text(editBuff,data,-1);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1);
      if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD);      //  v.4.3
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd)                 //  buffer signals, not widget
   }
      
   if (strEqu(type,"button")) {                                            //  button
      widget = gtk_button_new_with_label(data);
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd)
   }

   if (strEqu(type,"togbutt")) {                                           //  toggle button
      widget = gtk_toggle_button_new_with_label(data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }

   if (strEqu(type,"check")) {                                             //  checkbox
      if (data) widget = gtk_check_button_new_with_label(data);
      else  widget = gtk_check_button_new();
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"combo")) {                                             //  combo box
      widget = gtk_combo_box_new_text();
      zd->widget[iiw].cblist = pvlist_create(zdcbmax);                     //  for drop-down list
      if (! blank_null(data)) {
         pvlist_append(zd->widget[iiw].cblist,data);                       //  add data to drop-down list
         gtk_combo_box_append_text(GTK_COMBO_BOX(widget),data);
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),0);
      }
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }

   if (strEqu(type,"comboE")) {                                            //  combo box with entry box
      widget = gtk_combo_box_entry_new_text();
      zd->widget[iiw].cblist = pvlist_create(zdcbmax);                     //  for drop-down list
      if (! blank_null(data)) {
         gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),data);       //  entry = initial data
         pvlist_append(zd->widget[iiw].cblist,data);                       //  add data to drop-down list
         gtk_combo_box_append_text(GTK_COMBO_BOX(widget),data);
      }
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"radio")) {                                             //  radio button
      for (kk = iip+1; kk <= iiw; kk++) 
         if (strEqu(zd->widget[kk].pname,pname) &&                         //  find first radio button
             strEqu(zd->widget[kk].type,"radio")) break;                   //    with same container
      if (kk == iiw) 
         widget = gtk_radio_button_new_with_label(null,data);              //  this one is first
      else 
         widget = gtk_radio_button_new_with_label_from_widget              //  not first, add to group
              (GTK_RADIO_BUTTON(zd->widget[kk].widget),data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }

   if (strcmpv(type,"spin","hscale","vscale",null)) {                      //  spin button or sliding scale
      pp = strField(data,'|',1); err = convSD(pp,min);                     //  locale fix
      pp = strField(data,'|',2); err += convSD(pp,max);
      pp = strField(data,'|',3); err += convSD(pp,step);
      pp = strField(data,'|',4); err += convSD(pp,val);
      if (err) { min = 0; max = 100; step = 1; val = 50; }

      if (*type == 's') {
         widget = gtk_spin_button_new_with_range(min,max,step);
         gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val);
      }
      if (*type == 'h') {
         widget = gtk_hscale_new_with_range(min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
      }
      if (*type == 'v') {
         widget = gtk_vscale_new_with_range(min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
      }
      G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd)
      sprintf(vdata,"%g",val);
      data = vdata;
   }
   
   if (strEqu(type,"colorbutt")) {                                         //  color edit button        v.2.17
      if (! data) data = "0|0|0";                                          //  data format: "nnn|nnn|nnn" = RGB
      pp = strField(data,'|',1); gdkcolor.red = 256 * atoi(pp);
      pp = strField(data,'|',2); gdkcolor.green = 256 * atoi(pp);
      pp = strField(data,'|',3); gdkcolor.blue = 256 * atoi(pp);
      widget = gtk_color_button_new_with_color(&gdkcolor);
      G_SIGNAL(widget,"color-set",zdialog_widget_event,zd)
   }
      
   //  all widget types come here

   zd->widget[iiw].widget = widget;                                        //  set widget in zdialog

   if (strEqu(ptype,"hbox") || strEqu(ptype,"vbox"))                       //  add to hbox/vbox
      gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space);
   if (strEqu(ptype,"frame"))                                              //  add to frame
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strEqu(ptype,"scrwin"))                                             //  add to scroll window
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strEqu(ptype,"dialog"))                                             //  add to dialog box
      gtk_box_pack_start(GTK_BOX(GTK_DIALOG(pwidget)->vbox),
                                   widget,expand,expand,space);
   if (data) 
      zd->widget[iiw].data = strdupz(data,0,"zdialog");                    //  use heap memory   v.3.5

   zunlock();
   return 0;
}


//  add widget to existing zdialog - alternative form (clearer and easier code)
//  options: "scc=nn | homog | expand | space=nn | wrap"  (all optional, any order)

int zdialog_add_widget(zdialog *zd, cchar *type, cchar *name, 
                       cchar *parent, cchar *data, cchar *options)                                
{
   int      stat, scc = 0, homog = 0, expand = 0, space = 0, wrap = 0, begin = 1;
   char     pname[8];
   double   pval;
   
   while (true)
   {
      stat = strParms(begin,options,pname,8,pval);
      if (stat == -1) break;
      if (stat == 1) zappcrash("bad zdialog options: %s",options);
      if (strEqu(pname,"scc")) scc = (int(pval));
      else if (strEqu(pname,"homog")) homog = 1;
      else if (strEqu(pname,"expand")) expand = 1;
      else if (strEqu(pname,"space")) space = (int(pval));
      else if (strEqu(pname,"wrap")) wrap = 1;
      else zappcrash("bad zdialog options: %s",options);
   }
   
   stat = zdialog_add_widget(zd,type,name,parent,data,scc,homog,expand,space,wrap);
   return stat;
}


//  get GTK widget from zdialog and widget name

GtkWidget * zdialog_widget(zdialog *zd, cchar *name)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (int ii = 0; zd->widget[ii].type; ii++)                             //  v.4.4
   if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].widget;
   return 0;
}


//  set a common group for a set of radio buttons

int zdialog_set_group(zdialog *zd, cchar *radio1, ...)                     //  new v.3.1  (GTK, this does not work)
{
   va_list        arglist;
   cchar          *radio2;
   GtkWidget      *gwidget, *widget;
   GSList         *glist;

   zlock();

   gwidget = zdialog_widget(zd,radio1);
   glist = gtk_radio_button_get_group(GTK_RADIO_BUTTON(gwidget));
   if (! glist) zappcrash("no radio button group");

   va_start(arglist,radio1);
   
   while (true)
   {
      radio2 = va_arg(arglist,cchar *);
      if (! radio2) break;
      widget = zdialog_widget(zd,radio2);
      gtk_radio_button_set_group(GTK_RADIO_BUTTON(widget),glist);
   }
   
   va_end(arglist);

   zunlock();
   return 0;
}


//  resize dialog to a size greater than initial size
//  (as determined by the included widgets)

int zdialog_resize(zdialog *zd, int width, int height)
{
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   zlock();
   GtkWidget *window = zd->widget[0].widget;
   gtk_window_set_default_size(GTK_WINDOW(window),width,height);
   zunlock();
   return 1;
}


//  put data into a zdialog widget

int zdialog_put_data(zdialog *zd, cchar *name, cchar *data)
{
   GtkWidget      *widget;
   GtkTextBuffer  *textBuff;
   GdkColor       gdkcolor;
   int            iiw, nn, kk;
   cchar          *type, *pp;
   char           *wdata;
   double         val;
   
   if (! zd || zd->sentinel != zdsentinel) {                               //  detect destroyed dialog  v.2.2
      printf("zdialog_put_data(%s,%s), zdialog invalid \n",name,data);
      return 0;
   }
   
   for (iiw = 1; zd->widget[iiw].type; iiw++)                              //  find widget
      if (strEqu(zd->widget[iiw].name,name)) break;
   if (! zd->widget[iiw].type) {
      printf("zdialog_put_data(%s), widget invalid \n",name);              //  v.4.4
      return 0;
   }
   
   type = zd->widget[iiw].type;
   widget = zd->widget[iiw].widget;

   wdata = zd->widget[iiw].data;
   if (wdata) zfree(wdata);                                                //  free prior data memory
   zd->widget[iiw].data = 0;

   if (data) {
      wdata = strdupz(data,0,"zdialog");                                   //  set new data for widget      v.3.5
      zd->widget[iiw].data = wdata;
      if (utf8_check(wdata))
         printf("zdialog: bad UTF8 encoding %s \n",wdata);                 //  v.2.4
   }
   
   zd->disabled++;                                                         //  disable for widget stuffing  v.2.9

   zlock();

   if (strEqu(type,"label")) 
      gtk_label_set_text(GTK_LABEL(widget),data);

   if (strEqu(type,"link")) 
      gtk_label_set_text(GTK_LABEL(widget),data);

   if (strEqu(type,"entry")) 
      gtk_entry_set_text(GTK_ENTRY(widget),data);

   if (strEqu(type,"button"))                                              //  change button label  v.2.21
      gtk_button_set_label(GTK_BUTTON(widget),data);

   if (strEqu(type,"edit")) {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,data,-1);
   }
   
   if (strcmpv(type,"togbutt","check","radio",null)) 
   {
      if (! data) kk = nn = 0;
      else kk = convSI(data,nn);
      if (kk != 0) nn = 0;                                                 //  data not integer, force zero
      if (nn <= 0) nn = 0; else nn = 1;
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn);          //  set gtk widget value
   }

   if (strEqu(type,"spin")) {
      kk = convSD(data,val);
      if (kk != 0) val = 0.0;
      gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val);
   }
   
   if (strEqu(type,"colorbutt")) {                                         //  color button        v.2.17
      pp = strField(data,'|',1); 
      if (pp) gdkcolor.red = 256 * atoi(pp);                               //  bugfix   v.3.8
      pp = strField(data,'|',2); 
      if (pp) gdkcolor.green = 256 * atoi(pp);
      pp = strField(data,'|',3); 
      if (pp) gdkcolor.blue = 256 * atoi(pp);
      gtk_color_button_set_color(GTK_COLOR_BUTTON(widget),&gdkcolor);
   }
   
   if (strcmpv(type,"hscale","vscale",null)) {
      kk = convSD(data,val);
      if (kk != 0) val = 0.0;
      gtk_range_set_value(GTK_RANGE(widget),val);
   }
   
   if (strEqu(type,"combo")) {
      if (! blank_null(data)) {
         kk = pvlist_prepend(zd->widget[iiw].cblist,data,1);               //  add to drop-down list
         if (kk == 0)                                                      //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),data);
         kk = pvlist_find(zd->widget[iiw].cblist,data);
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),kk);               //  make the active entry   v.2.7
      }
      else gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1);             //  make no active entry
   }

   if (strEqu(type,"comboE")) {
      if (! blank_null(data)) {
         kk = pvlist_prepend(zd->widget[iiw].cblist,data,1);               //  add to drop-down list
         if (kk == 0)                                                      //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),data);
         gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),data);       //  stuff entry box with new data
      }
      else gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),"");       //  stuff entry box with nothing
   }

   zunlock();
   zd->disabled--;                                                         //  re-enable dialog   v.2.9
   return iiw;
}


//  get data from a dialog widget based on its name

cchar * zdialog_get_data(zdialog *zd, cchar *name)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (int ii = 1; zd->widget[ii].type; ii++)
      if (strEqu(zd->widget[ii].name,name)) 
            return zd->widget[ii].data;
   return 0;
}


//  set new limits for a numeric data entry widget (spin, hscale, vscale)

int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max)   //  new        v.4.4
{
   GtkWidget   *widget;
   cchar       *type;
   int         iiw;
   
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (iiw = 1; zd->widget[iiw].type; iiw++)
      if (strEqu(name,zd->widget[iiw].name)) break;
   if (! zd->widget[iiw].type) {
      printf("zdialog_stuff_limits, %s not found \n",name);
      return 0;
   }

   widget = zd->widget[iiw].widget;
   type = zd->widget[iiw].type;

   if (*type == 's') 
      gtk_spin_button_set_range(GTK_SPIN_BUTTON(widget),min,max);

   if (*type == 'h' || *type == 'v') 
      gtk_range_set_range(GTK_RANGE(widget),min,max);

   return 1;
}


//  put help topic into dialog

void zdialog_help(zdialog *zd, cchar *help_topic)
{
   zd->help_topic = help_topic;
   return;
}


//  run the dialog and send events to the event function
//  evfunc: int func(zdialog *zd, cchar *event)
//  posn: optional dialog box position (see zdialog_set_position)

int zdialog_run(zdialog *zd, zdialog_event evfunc, cchar *posn)            //  v.4.4
{
   int   zdialog_KBpress(GtkWidget *, GdkEventKey *event, zdialog *zd);
   int   zdialog_KBrelease(GtkWidget *, GdkEventKey *event, zdialog *zd);
   int   zdialog_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd);

   int         ii;
   GtkWidget   *widget, *dialog;
   
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog   v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   zlock();
   
   dialog = zd->widget[0].widget;

   if (posn) zdialog_set_position(zd,posn);                                //  v.4.4

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  *** stop auto-selection
   {                                                                       //  (GTK "feature")
      if (strEqu(zd->widget[ii].type,"entry")) {
         widget = zd->widget[ii].widget;
         gtk_editable_set_position(GTK_EDITABLE(widget),-1);
         break;
      }

      if (strEqu(zd->widget[ii].type,"comboE")) {                          //  also combo edit box
         widget = zd->widget[ii].widget;
         gtk_editable_set_position(GTK_EDITABLE(GTK_BIN(widget)->child),-1);
         break;
      }
   }

   if (evfunc) zd->eventCB = (void *) evfunc;                              //  link to dialog event callback

   dialog = zd->widget[0].widget;
   gtk_widget_show_all(dialog);                                            //  activate dialog

   G_SIGNAL(dialog,"event",zdialog_event_signal,zd)                        //  connect general event function     v.4.5
   G_SIGNAL(dialog,"key-press-event",zdialog_KBpress,zd)                   //  connect key press event function
   G_SIGNAL(dialog,"key-release-event",zdialog_KBrelease,zd)               //  connect key release event function
   G_SIGNAL(dialog,"response",zdialog_response_event,zd);                  //  connect dialog response function
   
   zd->disabled = 0;                                                       //  enable widget events   v.4.7
   zunlock();
   return 0;                                                               //  return now, dialog is non-modal
}


//  zdialog event handler - private function called for dialog events.
//  Updates data in zdialog, calls user callback function (if present).
//  This function always runs in main() thread, so zlock() unnecessary.

void zdialog_widget_event(GtkWidget *widget, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                             //  dialog event callback function

   GtkTextView       *textView = 0;
   GtkTextBuffer     *textBuff = 0;
   GtkTextIter       iter1, iter2;
   GdkColor          gdkcolor;
   static GtkWidget  *lastwidget = 0;
   int               ii, nn;
   cchar             *name, *type, *wdata;
   char              sdata[20];
   double            dval;
   static int        cbadded = 0;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");
   
   for (ii = 0; ii < zdmaxbutts; ii++)                                     //  check completion buttons  v.3.5
      if (zd->compbutt[ii] == widget) break;
   if (ii < zdmaxbutts) {
      zd->zstat = ii+1;                                                    //  zdialog status = button no. 
      if (zd->eventCB) {
         evfunc = (zdialog_event *) zd->eventCB;                           //  do callback function
         evfunc(zd,"zstat");
      }
      return;
   }

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget in zdialog
      if (zd->widget[ii].widget == widget) goto found_widget;

   for (ii = 1; zd->widget[ii].type; ii++) {                               //  failed, test if buffer
      if (strEqu(zd->widget[ii].type,"edit")) {                            //    of text view widget
         textView = GTK_TEXT_VIEW(zd->widget[ii].widget);
         textBuff = gtk_text_view_get_buffer(textView);
         if (widget == (GtkWidget *) textBuff) goto found_widget;
      }
   }

   printf("zdialog event %s, ignored \n",zd->widget[ii].name);             //  not found, ignore event    v.4.7
   return;

found_widget:

   if (zd->disabled) return;                                               //  stop re-entrance from own updates
   zd->disabled = 1;                                                       //    (zdialog_put_data())

   name = zd->widget[ii].name;
   type = zd->widget[ii].type;
   wdata = 0;

   if (strEqu(type,"button")) wdata = "clicked";
   
   if (strEqu(type,"entry"))
         wdata = gtk_entry_get_text(GTK_ENTRY(widget));
         
   if (strEqu(type,"edit")) {
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
   }

   if (strcmpv(type,"radio","check","togbutt",null)) 
   {
      nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
      if (nn == 0) wdata = "0";
      else wdata = "1";
   }

   if (strEqu(type,"combo"))
         wdata = gtk_combo_box_get_active_text(GTK_COMBO_BOX(widget));

   if (strEqu(type,"comboE"))
   {
      if (widget == lastwidget && cbadded) {
         pvlist_remove(zd->widget[ii].cblist,0);                           //  detect multiple edits (keystrokes)
         gtk_combo_box_remove_text(GTK_COMBO_BOX(widget),0);               //    and replace prior entry with new
      }
      wdata = gtk_entry_get_text(GTK_ENTRY(GTK_BIN(widget)->child));
      cbadded = 0;
      if (! blank_null(wdata)) {
         nn = pvlist_prepend(zd->widget[ii].cblist,wdata,1);               //  add entry to drop-down list
         if (nn == 0) {                                                    //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),wdata);
            cbadded = 1;
         }
      }
   }
   
   if (strEqu(type,"spin"))
   {
      dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
      sprintf(sdata,"%g",dval);
      wdata = sdata;
   }
   
   if (strEqu(type,"colorbutt"))                                           //  color button        v.2.17
   {
      gtk_color_button_get_color(GTK_COLOR_BUTTON(widget),&gdkcolor);
      sprintf(sdata,"%d|%d|%d",gdkcolor.red/256,gdkcolor.green/256,gdkcolor.blue/256);
      wdata = sdata;
   }
   
   if (strcmpv(type,"hscale","vscale",null))
   {
      dval = gtk_range_get_value(GTK_RANGE(widget));
      sprintf(sdata,"%g",dval);
      wdata = sdata;
   }
   
   //  all widgets come here

   if (zd->widget[ii].data) zfree(zd->widget[ii].data);                    //  clear prior data
   zd->widget[ii].data = 0;

   if (wdata) zd->widget[ii].data = strdupz(wdata,0,"zdialog");            //  set new data           v.3.5
   
   lastwidget = widget;                                                    //  remember last widget updated

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                              //  do user callback function
      evfunc(zd,name);
   }

   zd->disabled = 0;                                                       //  re-enable widgets
   return;
}


//  zdialog response handler for "event" signal (all events)

int zdialog_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd)
{
   if (zd->help_topic) 
      zfuncs::F1_help_topic = zd->help_topic;                              //  set help topic     v.4.5
   return 0;
}

//  zdialog response handler for keyboard events

int zdialog_KBpress(GtkWidget *, GdkEventKey *event, zdialog *zd)          //  prevent KB key press from being
{                                                                          //    sent to toolbar buttons
   return 0;
}

int zdialog_KBrelease(GtkWidget *, GdkEventKey *event, zdialog *zd)
{
   int   KBkey = event->keyval;
   
   if (KBkey == GDK_F1) {
      showz_userguide(zfuncs::F1_help_topic);                              //  context help   v.3.7
      return 1;
   }

   return 0;
}


//  zdialog response handler - private function called when dialog is completed.
//  called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0).

void zdialog_response_event(GtkWidget *, int zstat, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                             //  dialog event callback function

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");
   if (zd->zstat) return;                                                  //  already complete       v.3.5

   zd->zstat = zstat;                                                      //  set zdialog status

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                              //  do callback function
      evfunc(zd,"zstat");
   }

   return;
}


//  send an event to an active dialog

int zdialog_send_event(zdialog *zd, cchar *event)                          //  new v.2.17
{
   zdialog_event * evfunc = 0;                                             //  dialog event callback function

   if (zd && zd->sentinel == zdsentinel) {                                 //  check dialog is active
      evfunc = (zdialog_event *) zd->eventCB;
      if (evfunc) evfunc(zd,event);                                        //  call dialog event function
   }

   return 1;
}


//  Complete a dialog and give it a status, without user action.
//  Dialog event function will be called, zdialog_wait() will return.
//  returns:  0 = no active dialog, 1 = OK

int zdialog_send_response(zdialog *zd, int zstat)                          //  new v.2.23
{
   zdialog_event  *evfunc = 0;                                             //  dialog event callback function

   if (! zd) return 0;                                                     //  detect destroyed dialog
   if (zd->sentinel != zdsentinel) return 0;

   zd->zstat = zstat;                                                      //  set status

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                              //  do callback function
      evfunc(zd,"zstat");
   }
   return 1;
}


//  Destroy the zdialog - must be done by zdialog_run() caller
//  (else dialog continues active even after completion button).
//  Data in widgets remains valid until zdialog_free() is called.

int zdialog_destroy(zdialog *zd)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;
   
   if (zd->zstat < 0)                                                      //  destroyed by [x] button or GTK
      zd->widget[0].widget = 0;                                            //  assume GTK dialog is gone

   zlock();                                                                //  bugfix  v.2.13
   if (zd->widget[0].widget)                                               //  multiple destroys OK      v.2.7.1
      gtk_widget_destroy(zd->widget[0].widget);                            //  destroy GTK dialog
   zd->widget[0].widget = 0;
   zunlock();
   
   if (! zd->zstat) zd->zstat = -1;                                        //  status = destroyed        v.3.5
   return 1;
}


//  free zdialog memory (destroy first, if not already)

int zdialog_free(zdialog *&zd)                                             //  reference     v.3.9
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;
   
   if (zd->saveposn) zdialog_save_position(zd);                            //  save window position for next use  v.4.4
   
   zdialog_destroy(zd);                                                    //  destroy GTK dialog if there
   zd->sentinel = 0;                                                       //  mark invalid           v.2.2
   zfree(zd->widget[0].data);                                              //  bugfix memory leak     v.3.5

   for (int ii = 1; zd->widget[ii].type; ii++)                             //  loop through widgets
   {
      if (strcmpv(zd->widget[ii].type,"combo","comboE",null))              //  bugfix, free combo list   v.3.5
         pvlist_free(zd->widget[ii].cblist);
      zfree((char *) zd->widget[ii].type);                                 //  free strings
      zfree((char *) zd->widget[ii].name);
      zfree((char *) zd->widget[ii].pname);
      if (zd->widget[ii].data) zfree(zd->widget[ii].data);                 //  free data 
   }
   
   zfree(zd);                                                              //  free zdialog memory
   zd = 0;                                                                 //  clear pointer             v.3.9
   return 1;
}


//  wait for a zdialog to have a completion status or be destroyed
//  returns completion status or -1 if destroyed

int zdialog_wait(zdialog *zd)                                              //  v.3.3
{
   while (true)
   {
      zmainloop();
      if (! zd) return -1;                                                 //  dialog destroyed    v.3.5
      if (zd->sentinel != zdsentinel) return -1;
      if (zd->zstat) return zd->zstat;
      zsleep(0.01);
   }
}


//  put cursor at named widget

int zdialog_goto(zdialog *zd, cchar *name)                                 //  v.2.23
{
   GtkWidget   *widget; 
   
   if (zd->sentinel != zdsentinel) return 0;
   widget = zdialog_widget(zd, name);
   if (! widget) return 0;

   zlock();
   gtk_editable_select_region(GTK_EDITABLE(widget),0,-1);                  //  focus on widget
   gtk_widget_grab_focus(widget);
   zunlock();

   return 1;
}


//  set cursor for zdialog (e.g. busy cursor)

void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor)                    //  v.3.8
{
   GtkWidget *dialog;
   
   if (zd->sentinel != zdsentinel) return;
   dialog = zd->widget[0].widget;
   if (! dialog) return;
   gdk_window_set_cursor(dialog->window,cursor);
   return;
}


//  convenience functions for stuffing and retrieving widget data

int zdialog_stuff(zdialog *zd, cchar *name, cchar *data)                   //  stuff a string
{
   zdialog_put_data(zd, name, data);
   return 1;
}

int zdialog_stuff(zdialog *zd, cchar *name, int idata)                     //  stuff an integer
{
   char  string[16];

   sprintf(string,"%d",idata);
   zdialog_put_data(zd,name,string);
   return 1;
}

int zdialog_stuff(zdialog *zd, cchar *name, double ddata)                  //  stuff a double
{
   char  string[32];
   
   snprintf(string,31,"%g",ddata);                                         //  outputs decimal point or comma
   zdialog_put_data(zd,name,string);                                       //  (per locale)
   return 1;
}

int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc)         //  fetch string data
{
   cchar  *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      *data = 0;
      return 0;
   }
   
   return strncpy0(data,zdata,maxcc);                                      //  0 = OK, 1 = truncation  v.2.4
}

int zdialog_fetch(zdialog *zd, cchar *name, int &idata)                    //  fetch an integer
{
   cchar  *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      idata = 0;
      return 0;
   }
   
   idata = atoi(zdata);
   return 1;
}

int zdialog_fetch(zdialog *zd, cchar *name, double &ddata)                 //  fetch a double
{
   int         stat;
   cchar       *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      ddata = 0;
      return 0;
   }
   
   stat = convSD(zdata,ddata);                                             //  period or comma decimal point OK
   if (stat < 4) return 1;
   return 0;
}

int zdialog_fetch(zdialog *zd, cchar *name, float &fdata)                  //  fetch a float       v.3.4
{
   int         stat;
   cchar       *zdata;
   double      ddata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      fdata = 0;
      return 0;
   }
   
   stat = convSD(zdata,ddata);                                             //  period or comma decimal point OK
   fdata = ddata;
   if (stat < 4) return 1;
   return 0;
}


//  append new item to combo box list without changing entry box

int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data)
{
   int         ii, nn;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   if (blank_null(data)) return 0;                                         //  find widget
   for (ii = 1; zd->widget[ii].type; ii++) 
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box

   nn = pvlist_append(zd->widget[ii].cblist,data,1);                       //  append unique
   if (nn >= 0) {
      zlock();
      gtk_combo_box_append_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);
      zunlock();
   }

   return 1;
}


//  prepend new item to combo box list without changing entry box

int zdialog_cb_prep(zdialog *zd, cchar *name, cchar *data)
{
   int         ii, nn;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   if (blank_null(data)) return 0;                                         //  find widget
   for (ii = 1; zd->widget[ii].type; ii++) 
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box

   nn = pvlist_prepend(zd->widget[ii].cblist,data,1);                      //  append unique
   if (nn == 0) {
      zlock();
      gtk_combo_box_prepend_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);
      zunlock();
   }

   return 1;
}


//  get combo box drop-down list entry

char * zdialog_cb_get(zdialog *zd, cchar *name, int Nth)
{
   int      ii;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box
   return pvlist_get(zd->widget[ii].cblist,Nth);
}


//  delete entry by name from combo drop down list                         //  v.2.4

int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data)
{
   int      ii, nn;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box

   nn = pvlist_find(zd->widget[ii].cblist,data);                           //  find entry by name
   if (nn < 0) return -1;

   zlock();
   pvlist_remove(zd->widget[ii].cblist,nn);                                //  remove from memory list
   gtk_combo_box_remove_text(GTK_COMBO_BOX(zd->widget[ii].widget),nn);     //  and from widget
   gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1);      //  set no active entry
   zunlock();

   return 0;
}


//  delete all entries from combo drop down list

int zdialog_cb_clear(zdialog *zd, cchar *name)
{
   int      ii, jj, nn;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box

   zlock();

   nn = pvlist_count(zd->widget[ii].cblist);                               //  entry count
   for (jj = nn-1; jj >= 0; jj--) {
      pvlist_remove(zd->widget[ii].cblist,jj);                             //  remove from memory list
      gtk_combo_box_remove_text(GTK_COMBO_BOX(zd->widget[ii].widget),jj);  //  remove from widget
   }

   gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1);      //  set no active entry
   if (strEqu(zd->widget[ii].type,"comboE"))                               //  stuff entry box with nothing
      gtk_entry_set_text(GTK_ENTRY(GTK_BIN(zd->widget[ii].widget)->child),"");

   zunlock();   
   return 0;
}


//  make a combo box drop down to show all entries

int zdialog_cb_popup(zdialog *zd, cchar *name)                             //  v.3.8
{
   int      ii;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",null)) return 0;     //  not combo box

   zlock();
   gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget));
   zunlock();

   return 0;
}


/**************************************************************************/

//  functions to save and recall zdialog window positions

namespace zdposn_names
{
05834    struct zdposn_t {
      float    xpos, ypos;                                                 //  window position WRT parent or desktop
      char     wintitle[64];                                               //  window title (ID)
   }  zdposn[200];                                                         //  space for 200 windows

   int      Nzdposn;                                                       //  no. in use
   int      Nzdpmax = 200;                                                 //  table size
}


//  Load zdialog positions table from its file (application startup)
//  or save zdialog positions table to its file (application exit).
//  Action is "load" or "save". Number of table entries is returned.

int zdialog_positions(cchar *action)                                       //  new v.4.4
{
   using namespace zdposn_names;
   
   char     posfile[200], buff[100], wintitle[64], *pp;
   float    xpos, ypos;
   int      nn, ii;
   FILE     *fid;

   snprintf(posfile,199,"%s/zdialog_positions",get_zuserdir());            //  /home/<user>/.appname/zdialog_positions

   if (strEqu(action,"load"))                                              //  load dialog positions table from file
   {
      fid = fopen(posfile,"r");
      if (! fid) {
         Nzdposn = 0;
         return 0;
      }

      for (nn = 0; nn < Nzdpmax; nn++)
      {
         pp = fgets(buff,100,fid);
         if (! pp) break;
         if (strlen(pp) < 64) continue;
         strncpy0(wintitle,buff,64);
         strTrim(wintitle);
         if (strlen(wintitle) < 3) continue;
         ii = sscanf(buff + 64," %f %f ",&xpos,&ypos);
         if (ii != 2) continue;
         strcpy(zdposn[nn].wintitle,wintitle);
         zdposn[nn].xpos = xpos;
         zdposn[nn].ypos = ypos;
      }
      
      fclose(fid);
      Nzdposn = nn;
      return Nzdposn;
   }
   
   if (strEqu(action,"save"))                                              //  save dialog positions table to file
   {
      fid = fopen(posfile,"w");
      if (! fid) {
         printf("cannot write zdialog_positions file \n");
         return 0;
      }

      for (nn = 0; nn < Nzdposn; nn++)
         fprintf(fid,"%-64s %0.1f %0.1f \n",zdposn[nn].wintitle, 
                                 zdposn[nn].xpos, zdposn[nn].ypos);
      fclose(fid);
      return Nzdposn;
   }
   
   printf("zdialog_positions bad action: %s \n",action);
   return 0;
}


//  Set the initial or new zdialog window position from "posn"
//     null:      window manager decides
//    "mouse"     put dialog at mouse position
//    "desktop"   center dialog in desktop window
//    "parent"    center dialog in parent window
//    "save"      use the same position last set by the user
//    "nn/nn"     put NW corner of dialog in parent window at % size
//                (e.g. "50/50" puts NW corner at center of parent)

void zdialog_set_position(zdialog *zd, cchar *posn)                        //  new v.4.4
{
   using namespace zdposn_names;

   int         ii, ppx, ppy, zdpx, zdpy, pww, phh;
   float       xpos, ypos;
   char        wintitle[64], *pp;
   GtkWidget   *parent, *dialog;

   parent = zd->parent;
   dialog = zd->widget[0].widget;
   
   if (strEqu(posn,"mouse")) {
      gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);
      return;
   }

   if (strEqu(posn,"desktop")) {
      gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER);
      return;
   }

   if (strEqu(posn,"parent")) {
      gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT);
      return;
   }

   if (! parent) {                                                         //  no parent window
      ppx = ppy = 0;                                                       //  use desktop
      pww = gdk_screen_width();
      phh = gdk_screen_height();
   }
   else {
      gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy);               //  parent window NW corner
      gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh);                   //  parent window size
   }
   
   if (strEqu(posn,"save"))                                                //  use last saved window position
   {
      zd->saveposn = 1;                                                    //  set flag for zdialog_free()

      pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog));              //  get window title, used as ID
      if (! pp) return;
      if (strlen(pp) < 2) return;
      strncpy0(wintitle,pp,64);                                            //  window title, < 64 chars.
      for (ii = 0; ii < Nzdposn; ii++)                                     //  search table for title
         if (strEqu(wintitle,zdposn[ii].wintitle)) break;
      if (ii == Nzdposn) return;                                           //  not found - zdialog_free() will add

      zdpx = ppx + 0.01 * zdposn[ii].xpos * pww;                           //  position for dialog window
      zdpy = ppy + 0.01 * zdposn[ii].ypos * phh;
      gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy);
      return;
   }

   else     //  "nn/nn"                                                    //  position from caller
   {
      ii = sscanf(posn,"%f/%f",&xpos,&ypos);                               //  parse "nn/nn"
      if (ii != 2) return;
      zdpx = ppx + 0.01 * xpos * pww;                                      //  position for dialog window
      zdpy = ppy + 0.01 * ypos * phh;
      gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy);
      return;
   }
}


//  If the dialog window position is "save" then save
//  its position WRT parent or desktop for next use.

void zdialog_save_position(zdialog *zd)                                    //  new v.4.4
{
   using namespace zdposn_names;

   int         ii, ppx, ppy, pww, phh, zdpx, zdpy;
   float       xpos, ypos;
   char        wintitle[64], *pp;
   GtkWidget   *parent, *dialog;

   parent = zd->parent;
   dialog = zd->widget[0].widget;
   
   if (! parent) {                                                         //  no parent window
      ppx = ppy = 0;                                                       //  use desktop
      pww = gdk_screen_width();
      phh = gdk_screen_height();
   }
   else {
      gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy);               //  parent window NW corner
      gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh);                   //  parent window size
   }

   gtk_window_get_position(GTK_WINDOW(dialog),&zdpx,&zdpy);                //  dialog window NW corner

   xpos = 100.0 * (zdpx - ppx) / pww;                                      //  dialog window relative position
   ypos = 100.0 * (zdpy - ppy) / phh;                                      //  (as percent of parent size)

   pp = (char *) gtk_window_get_title(GTK_WINDOW(dialog));
   if (! pp) return;
   if (strlen(pp) < 2) return;
   strncpy0(wintitle,pp,64);                                               //  window title, < 64 chars.

   for (ii = 0; ii < Nzdposn; ii++)                                        //  search table for window
      if (strEqu(wintitle,zdposn[ii].wintitle)) break;
   if (ii == Nzdposn) {                                                    //  not found
      if (ii == Nzdpmax) return;                                           //  table full
      strcpy(zdposn[ii].wintitle,wintitle);                                //  add window to table
      Nzdposn++;
   }

   zdposn[ii].xpos = xpos;                                                 //  save window position
   zdposn[ii].ypos = ypos;
   return;
}


/**************************************************************************/

//  Output text to a popup window.  action: open, write, top, close
//  Window is left on screen until user destroys it with [x] button
//  or caller closes it with "close" action.

int write_popup_text(cchar *action, cchar *text, int ww, int hh, GtkWidget *parent)
{
   static GtkWidget  *mWin = 0, *mVbox, *mScroll;
   static GtkWidget  *mLog = 0;
   static PangoFontDescription   *monofont = 0;

   zlock();

   if (! monofont) 
      monofont = pango_font_description_from_string("monospace 10");
   
   if (strEqu(action,"open"))
   {
      if (mWin) gtk_widget_destroy(mWin);                                  //  only one at a time

      if (! ww) ww = 400;
      if (! hh) hh = 300;
      
      mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL);                          //  create main window
      gtk_window_set_title(GTK_WINDOW(mWin),text);
      gtk_window_set_default_size(GTK_WINDOW(mWin),ww,hh);

      if (parent)                                                          //  parent added          v.3.3
         gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(parent));
      
      if (parent)                                                          //  v.3.7
         gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER_ON_PARENT);
      else 
         gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_MOUSE);

      mVbox = gtk_vbox_new(0,0);                                           //  vertical packing box
      gtk_container_add(GTK_CONTAINER(mWin),mVbox);                        //  add to main window
      mScroll = gtk_scrolled_window_new(0,0);                              //  scrolled window
      gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0);                      //  add to main window mVbox
      gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mScroll),
                           GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
      mLog = gtk_text_view_new();                                          //  text edit window
      gtk_container_add(GTK_CONTAINER(mScroll),mLog);                      //  add to scrolled window
      gtk_widget_modify_font(mLog,monofont);                               //  set small monospaced font
      G_SIGNAL(mWin,"destroy",write_popup_text,"destroypop");              //  connect window destroy event
      gtk_widget_show_all(mWin);                                           //  show window
   }

   if (strEqu(action,"write"))                                             //  add text to window
      if (mWin) wprintf(mLog," %s\n",text);
   
   if (strEqu(action,"top"))                                               //  scroll to top line     v.4.0
      if (mWin) wscroll(mLog,2);

   if (strEqu(action,"close")) {                                           //  close window
      if (mWin) gtk_widget_destroy(mWin);
      mWin = 0;
   }
   
   if (text && strEqu(text,"destroypop"))                                  //  "destroy" signal from [x]  
      mWin = 0;

   zunlock();
   return 0;
}


/**************************************************************************/

//  execute a command and show the output in a scrolling popup window

int popup_command(cchar *command, int ww, int hh, GtkWidget *parent)       //  use write_popup_text()  v.3.1
{
   char        *buff;
   int         err, contx = 0;
   
   write_popup_text("open",command,ww,hh,parent);                          //  bugfix     v.3.8

   while ((buff = command_output(contx,command)))
   {
      write_popup_text("write",buff);
      zfree(buff);
   }
   
   write_popup_text("top",0);                                              //  back to top of window     v.4.0
   
   err = command_status(contx);
   return err;
}


/**************************************************************************/

//  display message box and wait for user acknowledgement

void zmessageACK(GtkWidget *parent, cchar *pMess, ... )                    //  v.3.7
{
   va_list        arglist;
   char           message[400];
   zdialog        *zd;
   GtkWidget      *widget;

   va_start(arglist,pMess);
   vsnprintf(message,400,pMess,arglist);
   va_end(arglist);
   
   zd = zdialog_new("",parent," X ",null);                                 //  v.3.9
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   zdialog_resize(zd,200,0);
   widget = zdialog_widget(zd,"dialog");                                   //  make modal    v.4.4
   gtk_window_set_modal(GTK_WINDOW(widget),1);
   zdialog_run(zd);
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


/**************************************************************************/

//  log error message to STDOUT as well as message box and user OK

void zmessLogACK(GtkWidget *parent, cchar *pMess, ...)                     //  v.3.5
{
   va_list        arglist;
   char           message[200];

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);
   
   printf("%s \n",message);
   zmessageACK(parent,message);
   return;
} 


/**************************************************************************/

//  display message box and wait for user Yes or No response

int zmessageYN(GtkWidget *parent, cchar *pMess, ... )                      //  v.3.7
{
   va_list        arglist;
   char           message[400];
   zdialog        *zd;
   int            zstat;
   GtkWidget      *widget;

   va_start(arglist,pMess);
   vsnprintf(message,400,pMess,arglist);
   va_end(arglist);
   
   zd = zdialog_new("message",parent,ZTX("Yes"),ZTX("No"),null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   zdialog_resize(zd,200,0);
   widget = zdialog_widget(zd,"dialog");                                   //  make modal    v.4.4
   gtk_window_set_modal(GTK_WINDOW(widget),1);
   zdialog_run(zd);
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   if (zstat == 1) return 1;
   return 0;
}


/**************************************************************************/

//  Display message box with message and button to display help topic      //  v.3.9

char  *zmessage_help_topic = 0;

void zmessage_help(GtkWidget *parent, cchar *topic, cchar *pmess, ... )
{
   int  zmessage_help_event(zdialog *zd, cchar *event);

   va_list        arglist;
   char           message[400];
   zdialog        *zd;

   va_start(arglist,pmess);
   vsnprintf(message,400,pmess,arglist);
   va_end(arglist);

   if (zmessage_help_topic) zfree(zmessage_help_topic);   
   zmessage_help_topic = strdupz(topic,0,"zmessage_help");
   
   zd = zdialog_new("context help",parent,"Help"," X ",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   zdialog_resize(zd,200,0);
   zdialog_run(zd,zmessage_help_event);
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}

int zmessage_help_event(zdialog *zd, cchar *event)
{
   if (zd->zstat == 1) {
      showz_userguide(zmessage_help_topic);
      zd->zstat = 0;
   }

   return 0;
}


/**************************************************************************/

//  display message indefinitely until timeout or user cancel
//  or caller kills it with zdialog_free()

zdialog * zmessage_post(GtkWidget *parent, int seconds, cchar *pMess, ... )
{
   int  zmessage_post_timeout(zdialog *zd);
   int  zmessage_post_event(zdialog *zd, cchar *event);

   va_list           arglist;
   char              message[400];
   static zdialog    *zd;
   
   va_start(arglist,pMess);
   vsnprintf(message,400,pMess,arglist);
   va_end(arglist);
   
   zd = zdialog_new("message",parent,ZTX("cancel"),null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   zdialog_run(zd,zmessage_post_event);
   
   if (seconds) {                                                          //  seconds added    v.4.3
      zlock();
      g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,zd);
      zunlock();
   }

   return zd;
}

int zmessage_post_timeout(zdialog *zd)
{
   if (zd && zd->zstat == 0) zdialog_free(zd);
   return 0;
}

int zmessage_post_event(zdialog *zd, cchar *event)
{
   if (zd && zd->zstat) zdialog_free(zd);
   return 0;
}


/**************************************************************************/

//  get text input from a popup dialog
//  returned text is subject for zfree()

char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext)
{
   zdialog     *zd;
   int         zstat;
   char        *text;
   GtkWidget   *widget;
   
   zd = zdialog_new(title,parent,"OK",ZTX("cancel"),null);
   zdialog_add_widget(zd,"frame","fred","dialog");
   zdialog_add_widget(zd,"edit","edit","fred");
   if (inittext) zdialog_stuff(zd,"edit",inittext);

   zdialog_resize(zd,200,0);                                               //  v.3.7
   widget = zdialog_widget(zd,"dialog");                                   //  make modal    v.4.4
   gtk_window_set_modal(GTK_WINDOW(widget),1);
   zdialog_run(zd);
   zstat = zdialog_wait(zd);
   if (zstat == 1) 
      text = (char *) zdialog_get_data(zd,"edit");
   else text = 0;
   if (text) text = strdupz(text,0,"zdialog_text");
   zdialog_free(zd);
   return text;
}


/**************************************************************************/

//    File chooser dialog for one or more files
//
//    Action:  "open"            select an existing file
//             "openN"           select multiple existing files
//             "save"            select an existing or new file
//             "folder"          select existing folder
//             "create folder"   select existing or new folder
//
//    buttx:   "hidden"          add button to toggle display of hidden files
//             "quality"         add button to set JPG file save quality
//                               optional, default = null
//
//    Returns a list of filespecs terminated with null.
//    Memory for returned list and returned files are subjects for zfree();
//    No GTK locking, use in main() thread only.


//  version to select only 1 file

char * zgetfile1(cchar *title, cchar *action, cchar *initfile, cchar *buttx)
{
   if (strEqu(action,"openN")) zappcrash("zgetfile1 called with openN");
   char **flist = zgetfileN(title,action,initfile,buttx);
   if (! flist) return 0;
   char *file = *flist;
   zfree(flist);
   return file;
}


//  select one or multiple files                                           //  overhauled   v.3.8

int   zgetfile_Fnewfolder;
int   zgetfile_Finitfile;


char ** zgetfileN(cchar *title, cchar *action, cchar *initfile, cchar *buttx)
{
   using namespace zfuncs;

   void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget);          //  private functions
   void zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event);
   void zgetfile_newfolder(GtkFileChooser *dialog, void *);

   GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN;

   GtkWidget      *dialog;
   GtkWidget      *pvwidget = gtk_image_new();
   GSList         *gslist = 0;
   cchar          *button1 = 0, *buttxx = 0;
   char           *pdir, *pfile;
   int            ii, err, NF;
   int            fcstat, bcode = 0, qnum, hide = 0;
   char           *qual, *file1, *file2, **flist = 0;
   struct stat    fstat;
   
   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   if (strEqu(action,"open")) {
      fcact = GTK_FILE_CHOOSER_ACTION_OPEN;
      button1 = ZTX("open");
   }

   if (strEqu(action,"openN")) {
      fcact = GTK_FILE_CHOOSER_ACTION_OPEN;
      button1 = ZTX("choose");
   }

   if (strEqu(action,"save")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SAVE;
      button1 = ZTX("save");
   }

   if (strEqu(action,"folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
      button1 = ZTX("open folder");
   }

   if (strEqu(action,"create folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER;
      button1 = ZTX("create folder");
   }
   
   if (buttx) {
      if (strnEqu(buttx,"hidden",6)) {                                     //  generate text for translation
         buttxx = ZTX("hidden");                                           //  bugfix v.4.4
         bcode = 103;
      }
      if (strEqu(buttx,"quality")) {
         buttxx = ZTX("quality");
         bcode = 104;
      }
   }
   
   dialog = gtk_file_chooser_dialog_new(title, null, fcact,                //  create file selection dialog
                              button1, GTK_RESPONSE_ACCEPT, 
                              ZTX("cancel"), GTK_RESPONSE_CANCEL, 
                              buttxx, bcode, null);

   gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget); 

   G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget);            //  create preview for selected file
   G_SIGNAL(dialog,"current-folder-changed",zgetfile_newfolder,0);         //  detect folder change
   G_SIGNAL(dialog,"key-release-event",zgetfile_KBkey,0);                  //  respond to F1 help key
   
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);          //  put dialog at mouse position
   gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0);           //  default: no show hidden

   if (strEqu(action,"save"))                                              //  overwrite confirmation
      gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1);

   if (strEqu(action,"openN"))                                             //  bugfix   v.3.8
      gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),1);    //  select multiple files

   if (initfile) {                                                         //  pre-select filespec
      err = stat(initfile,&fstat);
      if (err) {
         pdir = strdupz(initfile);                                         //  non-existent file
         pfile = strrchr(pdir,'/');
         if (pfile && pfile > pdir) {
            *pfile++ = 0;
            gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir);
            if (strEqu(action,"save"))                                     //  new file only if save  v.4.7
               gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile);
         }
         else 
            gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pdir);
         zfree(pdir);
      }
      else if (S_ISREG(fstat.st_mode))                                     //  select given file   v.3.8
         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile);
      else if (S_ISDIR(fstat.st_mode))
         gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile);

      zgetfile_Finitfile = 1;
   }

   gtk_widget_show_all(dialog);                                            //  v.3.9   

   while (true)
   {
      fcstat = gtk_dialog_run(GTK_DIALOG(dialog));                         //  run dialog, get status button

      if (fcstat == 103) {                                                 //  show/hide hidden files
         hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(dialog));
         hide = 1 - hide;
         gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide);
      }

      else if (fcstat == 104) {                                            //  get JPG quality parameter 
         while (true) {
            qual = zdialog_text(null,ZTX("JPG quality 0-100"),JPGquality);
            if (! qual) break;                                             //  cancel = no change
            err = convSI(qual,qnum,0,100);
            zfree(qual);
            if (err) continue;                                             //  enforce 0-100
            snprintf(JPGquality,4,"%d",qnum);
            break;
         }
      }

      else break;                                                          //  some other button 
   }

   if (fcstat == GTK_RESPONSE_ACCEPT) 
   {
      gslist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
      if (! gslist) goto byebye;
      NF = g_slist_length(gslist);                                         //  no. selected files
      if (! NF) goto byebye;

      flist = (char **) zmalloc((NF+1)*sizeof(char *),"zgetfile");         //  allocate returned list    v.3.5
 
      for (ii = 0; ii < NF; ii++) 
      {                                                                    //  process selected files
         file1 = (char *) g_slist_nth_data(gslist,ii);
         if (strlen(file1) >= maxfcc)                                      //  v.3.5
            file1 = (char *) "ridiculously long filespec";
         file2 = strdupz(file1,0,"zgetfile");                              //  re-allocate memory        v.3.5
         g_free(file1);
         flist[ii] = file2;
      }
      flist[ii] = 0;                                                       //  EOL marker
   }

byebye:
   if (gslist) g_slist_free(gslist);
   gtk_widget_destroy(dialog);
   return flist;
}


//  zgetfile private function - called when user changes folders

void zgetfile_newfolder(GtkFileChooser *dialog, void *)
{
   if (zgetfile_Finitfile) {
      zgetfile_Finitfile = 0;
      return;
   }
   zgetfile_Fnewfolder++;
   return;
}


//  zgetfile private function - get preview images for image files         //  v.2.0

void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget)
{
   GdkPixbuf   *thumbnail;
   char        *filename;

   if (zgetfile_Fnewfolder) {                                              //  unselect auto-selected irrelevant file
      zgetfile_Fnewfolder = 0;                                             //  (does not work in newfolder() above,
      gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(dialog));             //   but it does work here - why?)
   }
   
   filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog));

   if (! filename) {
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);
      return;
   }

   thumbnail = 0;
   if (image_file_type(filename) == 2)                                     //  supported image file type
      thumbnail = image_thumbnail(filename,128);                           //  use 128x128 pixels
   g_free(filename);

   if (thumbnail) {
      gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail);
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1);
      g_object_unref(thumbnail);
   }
   else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);

   return;
}


//  zgetfile private function - respond to F1 key
//  zfuncs::F1_help_topic must be pre-loaded by caller if needed.

void zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event)
{
   int KBkey = event->keyval;
   if (KBkey == GDK_F1)
      showz_userguide(zfuncs::F1_help_topic);
   return;
}


/**************************************************************************/

//  show a local or remote html file using the user's preferred browser    //  v.2.18
//  to show a local file starting at an internal live link location:
//    url = "file://directory/.../filename#livelink

void showz_html(cchar *url)
{
   char           command[500];
   static char    prog[20];
   static int     ftf = 1, err;

   if (ftf) {
      ftf = 0;
      strcpy(prog,"xdg-open");
      err = system("xdg-open --version");
      if (err) {
         strcpy(prog,"firefox");
         err = system("firefox -v");
         if (err) *prog = 0;
      }
   }
   
   if (! *prog) {
      zmessLogACK(null,"no xdg-open or firefox, cannot show document");
      return;
   }
   
   snprintf(command,499,"%s %s &",prog,url);                               //  add '&'   v.2.18
   printf(" %s \n",command);                                               //  v.4.1 
   err = system(command);
   return;
}


/**************************************************************************/

//  connect a user callback function to a window drag-drop event

void drag_drop_connect(GtkWidget *window, drag_drop_func *ufunc)           //  v.2.19
{
   int  drag_drop_connect2(GtkWidget *, void *, int, int, void *, int, int, void *);
   char     string[] = "STRING";
   GtkTargetEntry  file_drop_target = { string, 0, 0 };

   zlock();
   gtk_drag_dest_set(window, GTK_DEST_DEFAULT_ALL, &file_drop_target, 1, GDK_ACTION_COPY);
   G_SIGNAL(window, "drag-data-received", drag_drop_connect2, ufunc)
   gtk_drag_dest_add_uri_targets(window);                                  //  accept URI (file) drop     v.2.27
   zunlock();

   return;
}


//  private function 
//  get dropped file, clean escapes, pass to user function
//  passed filespec is subject for zfree()

int drag_drop_connect2(GtkWidget *, void *, int mpx, int mpy, void *sdata, int, int, void *ufunc)
{
   char  * drag_drop_unescape(cchar *escaped_string);
   drag_drop_func  *ufunc2;

   char     *text, *text2, *file, *file2;
   int      cc;
   
   text = (char *) ((GtkSelectionData *) sdata)->data;
   ufunc2 = (drag_drop_func *) ufunc;

   if (strstr(text,"file://"))                                             //  text is a filespec
   {
      file = strdupz(text+7);                                              //  get rid of junk added by GTK
      cc = strlen(file);
      while (file[cc-1] < ' ') cc--;
      file[cc] = 0;
      file2 = drag_drop_unescape(file);                                    //  clean %xx escapes from Nautilus
      zfree(file);
      ufunc2(mpx,mpy,file2);                                               //  pass file to user function
   }
   
   else 
   {
      text2 = strdupz(text,0,"drag_drop");                                 //  v.3.5
      ufunc2(mpx,mpy,text2);
   }
   
   return 1;
}


//  private function
//  Clean %xx escapes from strange Nautilus drag-drop file names

char * drag_drop_unescape(cchar *inp)
{
   int  drag_drop_convhex(char ch);

   char     inch, *out, *outp;
   int      nib1, nib2;
   
   out = zmalloc(strlen(inp)+1,"drag_drop");                               //  v.3.5
   outp = out;
   
   while ((inch = *inp++))
   {
      if (inch == '%')
      {
         nib1 = drag_drop_convhex(*inp++);
         nib2 = drag_drop_convhex(*inp++);
         *outp++ = nib1 << 4 | nib2;
      }
      else *outp++ = inch;
   }
   
   *outp = 0;
   return out;
}


//  private function - convert character 0-F to number 0-15

int drag_drop_convhex(char ch)
{
   if (ch >= '0' && ch <= '9') return  ch - '0';
   if (ch >= 'A' && ch <= 'F') return  ch - 'A' + 10;
   if (ch >= 'a' && ch <= 'f') return  ch - 'a' + 10;
   return ch;
}


//  make a cursor from a graphic file in application's icon directory

GdkCursor * zmakecursor(cchar *iconfile)                                   //  v.3.7
{
   using namespace zfuncs;

   GError         *gerror = 0;
   GdkPixbuf      *pixbuf;
   GdkDisplay     *display;
   GdkCursor      *cursor = 0;
   char           iconpath[200];

   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   display = gdk_display_get_default();
   *iconpath = 0;
   strncatv(iconpath,199,zicondir,"/",iconfile,null);
   pixbuf = gdk_pixbuf_new_from_file(iconpath,&gerror);
   if (pixbuf && display)
      cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0);
   else printf("%s \n",gerror->message);
   return cursor;
}


/**************************************************************************/
//   GDK/GTK image file utility functions
//   for functions returning char *, caller responsible for zfree()
/**************************************************************************/


/**************************************************************************

   Programs for printing an image file                      new v.4.7

   HPLIP problem: Setting paper size was made less flexible. 
   GtkPrintSettings paper size must agree with the one in the current 
   printer setup. This can only be set in the printer setup dialog, not 
   in the application. Also the print size (width, height) comes from 
   the chosen paper size and cannot be changed in the application. 
   Print margins can be changed to effect printing a smaller or shifted 
   image on a larger paper size.


   print_image_paper_setup()
   
   Do a print paper format selection, after which the page width, height
   and orientation are available to the caller. Units are CM.
   (paper width and height are reversed for landscape orientation)
   
   
   print_image_margins_setup()
   
   Optionally set the print margins. If not done they are zero
   (or printer-dependent minimum). Afterwards the margins are 
   available to the caller. Units are CM.
   

   print_image_file(cchar *imagefile)
   
   Print the image file on the printer and paper size determined by
   a prior call to print_image_page_setup.


***************************************************************************/


namespace print_image 
{
   #define MM GTK_UNIT_MM
   #define PRINTOP   GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG
   #define PORTRAIT  GTK_PAGE_ORIENTATION_PORTRAIT
   #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE
   #define QUALITY   GTK_PRINT_QUALITY_HIGH

   GtkPageSetup               *priorpagesetup = 0;
   GtkPageSetup               *pagesetup;
   GtkPrintSettings           *printsettings = 0;
   GtkPrintOperation          *printop;
   GtkPageOrientation         orientation = PORTRAIT;
   GdkPixbuf                  *pixbuf;
   cchar                      *printer = 0;
   double                     width = 21.0, height = 29.7;                 //  paper size, CM (default A4 portrait)
   double                     margins[4] = { 0, 0, 0, 0 };                 //  print margins, CM (default none)
   int                        landscape = 0;                               //  true if landscape
}


void print_image_paper_setup()
{
   using namespace print_image;
   
   char     printsettingsfile[200], pagesetupfile[200];
   
   snprintf(printsettingsfile,200,"%s/printsettings",get_zuserdir());
   snprintf(pagesetupfile,200,"%s/pagesetup",get_zuserdir());

   if (! printsettings) {                                                  //  start with prior print settings
      printsettings = gtk_print_settings_new_from_file(printsettingsfile,0);
      if (! printsettings)
         printsettings = gtk_print_settings_new();
   }
   
   if (! priorpagesetup) {                                                 //  start with prior page setup
      priorpagesetup = gtk_page_setup_new_from_file(pagesetupfile,0);
      if (! priorpagesetup)
         priorpagesetup = gtk_page_setup_new();
   }

   pagesetup = gtk_print_run_page_setup_dialog                             //  select printer, paper size
                    (0,priorpagesetup,printsettings);                      //     and orientation
   if (! pagesetup) return;

   g_object_unref(priorpagesetup);                                         //  save for next call
   priorpagesetup = pagesetup;
   
   width = gtk_page_setup_get_paper_width(pagesetup,MM);                   //  save paper width, height
   height = gtk_page_setup_get_paper_height(pagesetup,MM);
   width = width / 10;                                                     //  use cm units
   height = height / 10;
   
   orientation = gtk_print_settings_get_orientation(printsettings);        //  save orientation
   if (orientation == LANDSCAPE) landscape = 1;
   else landscape = 0;

   gtk_print_settings_set_quality(printsettings,QUALITY);                  //  set high quality 300 dpi
   gtk_print_settings_set_resolution(printsettings,300);
   
   gtk_print_settings_to_file(printsettings,printsettingsfile,0);          //  save print settings to file
   gtk_page_setup_to_file(pagesetup,pagesetupfile,0);                      //  save print settings to file

   return;
}


void print_image_margins_setup()
{
   using namespace print_image;
   
   zdialog     *zd;
   int         zstat;

   zd = zdialog_new(ZTX("margins"),0,ZTX("done"),ZTX("cancel"),0);         //  build dialog

   zdialog_add_widget(zd,"hbox","hbmlab","dialog",0,"homog");              //            top  bottom  left   right
   zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"space=3");            //  margins  [___]  [___]  [___]  [___]
   zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"space=3");
   zdialog_add_widget(zd,"label","space","vbmarg"," ");
   zdialog_add_widget(zd,"label","labtop","vbtop",ZTX("top"));
   zdialog_add_widget(zd,"label","labbot","vbbottom",ZTX("bottom"));
   zdialog_add_widget(zd,"label","lableft","vbleft",ZTX("left"));
   zdialog_add_widget(zd,"label","labright","vbright",ZTX("right"));
   zdialog_add_widget(zd,"label","labmarg","vbmarg",ZTX("margins"),"space=3");
   zdialog_add_widget(zd,"spin","mtop","vbtop","0|3|0.1|0");
   zdialog_add_widget(zd,"spin","mbottom","vbbottom","0|3|0.1|0");
   zdialog_add_widget(zd,"spin","mleft","vbleft","0|3|0.1|0");
   zdialog_add_widget(zd,"spin","mright","vbright","0|3|0.1|0");
   
   zdialog_stuff(zd,"mtop",margins[0]);                                    //  stuff prior print margins
   zdialog_stuff(zd,"mbottom",margins[1]);
   zdialog_stuff(zd,"mleft",margins[2]);
   zdialog_stuff(zd,"mright",margins[3]);

   zdialog_run(zd);                                                        //  run dialog
   zstat = zdialog_wait(zd);                                               //  wait for completion

   if (zstat != 1) {                                                       //  user canceled
      zdialog_free(zd);
      return;
   }
   
   zdialog_fetch(zd,"mtop",margins[0]);                                    //  set print margins
   zdialog_fetch(zd,"mbottom",margins[1]);
   zdialog_fetch(zd,"mleft",margins[2]);
   zdialog_fetch(zd,"mright",margins[3]);

   zdialog_free(zd);                                                       //  kill dialog

   gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM);              //  set page margins, mm units
   gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM);
   gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM);
   gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM);
   
   return;
}


void print_image_file(cchar *imagefile)
{
   using namespace print_image;

   void  print_image_page(GtkPrintOperation *, GtkPrintContext *, int page);

   GtkPrintOperationResult  printstat;
   GError   *gerror = 0;

   pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror);                   //  read image file
   if (! pixbuf) {
      zmessageACK(null,gerror->message);
      return;
   }
   
   printop = gtk_print_operation_new();                                    //  print operation
   gtk_print_operation_set_default_page_setup(printop,pagesetup);
   gtk_print_operation_set_print_settings(printop,printsettings);
   gtk_print_operation_set_n_pages(printop,1); 
   
   g_signal_connect(printop,"draw-page",G_CALLBACK(print_image_page),0);   //  start print
   printstat = gtk_print_operation_run(printop,PRINTOP,0,0); 

   if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) {
      gtk_print_operation_get_error(printop,&gerror);
      zmessageACK(null,gerror->message);
   }

   g_object_unref(printop);
   return;
}


//  draw the graphics for the print page
//  rescale with cairo: print resolution of 300 dpi is no longer ignored

void print_image_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) 
{ 
   using namespace print_image;

   cairo_t           *cairocontext;
   double            iww, ihh, pww, phh, scale;

   pww = gtk_print_context_get_width(printcontext);                        //  print context size
   phh = gtk_print_context_get_height(printcontext);

   iww = gdk_pixbuf_get_width(pixbuf);                                     //  image size
   ihh = gdk_pixbuf_get_height(pixbuf);
   
   scale = pww / iww;                                                      //  rescale to fit page
   if (phh / ihh < scale) scale = phh / ihh;

   cairocontext = gtk_print_context_get_cairo_context(printcontext);       //  use cairo to rescale
   cairo_translate(cairocontext,0,0);
   cairo_scale(cairocontext,scale,scale);
   gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0);
   cairo_paint(cairocontext);

   return;
}


/**************************************************************************  

     GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf, double angle, int acolor)

     Rotate a pixbuf through an arbitrary angle (degrees).

     The returned image has the same size as the original, but the
     pixbuf envelope is increased to accomodate the rotated original
     (e.g. a 100x100 pixbuf rotated 45 deg. needs a 142x142 pixbuf).

     Pixels added around the rotated image have all RGB values = acolor.   //  v.2.17
     Angle is in degrees. Positive direction is clockwise.
     Pixbuf must have 8 bits per channel and 3 or 4 channels.
     Loss of resolution is about 1/2 pixel.
     Speed is about 18 million pixels/sec. on my 2.67 GHz CPU.             //  v.3.2
     
     NULL is returned if the function fails for one of the following:
         - pixbuf not 8 bits/channel or < 3 channels
         - unable to create output pixbuf (lack of memory?)
    
     Algorithm:
         create output pixbuf big enough for rotated input pixbuf
         compute coefficients for affine transform
         loop all output pixels
            get next output pixel (px2,py2)
            convert to input pixel (px1,py1) using affine transform        //  v.3.2
            if outside of pixmap
               output pixel = black
               continue
            for 4 input pixels based at (px0,py0) = (int(px1),int(py1))
               compute overlap (0 to 1) with (px1,py1)
               sum RGB values * overlap
            output aggregate RGB to pixel (px2,py2)

***/

GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf1, double angle, int acolor)
{
   typedef unsigned char  *pixel;                                          //  3 RGB values, 0-255 each

   GdkPixbuf      *pixbuf2;
   GdkColorspace  color;

   int      nch, nbits, alpha;
   int      ww1, hh1, rs1, ww2, hh2, rs2;
   int      px2, py2, px0, py0;
   pixel    ppix1, ppix2, pix0, pix1, pix2, pix3;
   double   px1, py1;
   double   f0, f1, f2, f3, red, green, blue, tran = 0;
   double   a, b, d, e, ww15, hh15, ww25, hh25;
   double   pi = 3.141593;

   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   nch = gdk_pixbuf_get_n_channels(pixbuf1);
   nbits = gdk_pixbuf_get_bits_per_sample(pixbuf1);
   if (nch < 3) return 0;                                                  //  must have 3+ channels (colors)
   if (nbits != 8) return 0;                                               //  must be 8 bits per channel

   color = gdk_pixbuf_get_colorspace(pixbuf1);                             //  get input pixbuf1 attributes
   alpha = gdk_pixbuf_get_has_alpha(pixbuf1);
   ww1 = gdk_pixbuf_get_width(pixbuf1);
   hh1 = gdk_pixbuf_get_height(pixbuf1);
   rs1 = gdk_pixbuf_get_rowstride(pixbuf1);

   while (angle < -180) angle += 360;                                      //  normalize, -180 to +180
   while (angle > 180) angle -= 360;
   angle = angle * pi / 180;                                               //  radians, -pi to +pi
   
   if (fabs(angle) < 0.001) {                                              //  bugfix 0.01 >> 0.001   v.2.1
      pixbuf2 = gdk_pixbuf_copy(pixbuf1);                                  //  angle is zero within my precision
      return pixbuf2;
   }

   ww2 = int(ww1*fabs(cos(angle)) + hh1*fabs(sin(angle)));                 //  rectangle containing rotated image
   hh2 = int(ww1*fabs(sin(angle)) + hh1*fabs(cos(angle)));

   pixbuf2 = gdk_pixbuf_new(color,alpha,nbits,ww2,hh2);                    //  create output pixbuf2
   if (! pixbuf2) return 0;
   rs2 = gdk_pixbuf_get_rowstride(pixbuf2);
   
   ppix1 = gdk_pixbuf_get_pixels(pixbuf1);                                 //  input pixel array
   ppix2 = gdk_pixbuf_get_pixels(pixbuf2);                                 //  output pixel array
   
   ww15 = 0.5 * ww1;
   hh15 = 0.5 * hh1;
   ww25 = 0.5 * ww2;
   hh25 = 0.5 * hh2;

   a = cos(angle);                                                         //  affine transform coefficients   v.3.2
   b = sin(angle);
   d = - sin(angle);
   e = cos(angle);
   
   for (py2 = 0; py2 < hh2; py2++)                                         //  loop through output pixels
   for (px2 = 0; px2 < ww2; px2++)
   {

      px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15;                    //  (px1,py1) = corresponding       v.3.2
      py1 = d * (px2 - ww25) + e * (py2 - hh25) + hh15;                    //    point within input pixels

      px0 = int(px1);                                                      //  pixel containing (px1,py1)
      py0 = int(py1);
      
      if (px1 < 0 || px0 >= ww1-1 || py1 < 0 || py0 >= hh1-1) {            //  if outside input pixel array
         pix2 = ppix2 + py2 * rs2 + px2 * nch;                             //    output is acolor    v.2.17
         pix2[0] = pix2[1] = pix2[2] = acolor;
         continue;
      }

      pix0 = ppix1 + py0 * rs1 + px0 * nch;                                //  4 input pixels based at (px0,py0)
      pix1 = pix0 + rs1;
      pix2 = pix0 + nch;
      pix3 = pix0 + rs1 + nch;

      f0 = (px0+1 - px1) * (py0+1 - py1);                                  //  overlap of (px1,py1)
      f1 = (px0+1 - px1) * (py1 - py0);                                    //    in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);
   
      red =   f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];   //  sum the weighted inputs
      green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
      blue =  f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
      if (alpha) 
       tran = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3];   //  bugfix  v.3.3
      
      if (red == acolor && green == acolor && blue == acolor) {            //  avoid acolor in image     v.2.17
         if (blue == 0) blue = 1;
         else blue--;
      }
      
      pix2 = ppix2 + py2 * rs2 + px2 * nch;                                //  output pixel
      pix2[0] = int(red);
      pix2[1] = int(green);
      pix2[2] = int(blue);
      if (alpha) pix2[3] = int(tran);                                      //  bugfix  v.3.3
   }
      
   return pixbuf2;
}


/**************************************************************************

   functions for navigation of image files in a directory                  //  overhauled  v.2.3
      - get first or last image, previous or next image
      - create an image gallery window (thumbnail images)
      - use gallery window to navigate and select images
   no locking, use in main() thread only

**************************************************************************/

namespace image_navi 
{
   #define maxFiles 100000                                                 //  max image files in one directory v.4.1
   #define thumbnail_cachesize 10000                                       //  max thumbnails cached in memory  v.4.1
   #define imagetypes ".jpeg .jpg .png .tif .tiff .bmp .gif .svg .xpm"     //  supported image file types

   #define TEXTWIN GTK_TEXT_WINDOW_TEXT                                    //  GDK window of GTK text view
   #define NODITHER GDK_RGB_DITHER_NONE
   #define SCROLLWIN GTK_SCROLLED_WINDOW
   #define NEVER GTK_POLICY_NEVER
   #define ALWAYS GTK_POLICY_ALWAYS
   #define interp GDK_INTERP_BILINEAR
   #define colorspace GDK_COLORSPACE_RGB

   #define     thumbfilesize 256                                           //  default thumbnail image size
   #define     thumbxx 6                                                   //  thumbx array size
   int         thumbx[6] = { 512, 360, 256, 180, 128, 90 };                //  thumbnail image step sizes   v.4.7

   char        *galleryname = 0;                                           //  image directory or file list name
   int         gallerytype = 0;                                            //  1/2 = directory / file list
   int         nfiles = 0;                                                 //  total file count (incl. subdirks)
   int         nimages = 0;                                                //  image file count
   char        **flist = 0;                                                //  image file list

   typedef void ufunc(int Nth);                                            //  callback function for clicked image
   ufunc       *userfunc;

   GtkWidget      *wing = 0, *vboxx, *scrwing, *layout;                    //  image gallery and drawing windows
   GtkWidget      *pwing = 0;                                              //  parent window
   GtkAdjustment  *scrollbar;
   GdkGC          *gdkgc = 0;                                              //  graphics context

   int         xwinW = 1000, xwinH = 700;                                  //  gallery window initial size
   int         xwinX, xwinY;                                               //  gallery window initial position
   int         thumbsize = thumbfilesize;                                  //  thumbnail image <= thumbnail file
   int         thumbW, thumbH;                                             //  gallery window thumbnail cell size
   int         xrows, xcols;                                               //  gallery window thumbnail rows, cols
   int         xmargW, xmargH;                                             //  cell margin from left and top edge
   int         genthumbs = 0;                                              //  count newly generated thumbnails
   int         scrollposn;                                                 //  gallery window scroll position
   int         maxscroll;                                                  //  max. scroll position
   int         fpresent = 0;                                               //  force gallery window to z-top
   int         targposn = 0;                                               //  scroll-to file position (Nth)
   cchar       *toomanyfiles = "exceed %d files in one directory";
   
   //  private functions
   int    gallery_paint(GtkWidget *, GdkEventExpose *);                    //  gallery window paint function
   void   draw_text(GtkWidget *win, char *text, int x, int y, int ww);     //  draw text in gallery window
   void   gallery_destroy();                                               //  gallery window destroy event function
   void   menufuncx(GtkWidget *win, cchar *menu);                          //  function for gallery window buttons
   void   mouse_xevent(GtkWidget *, GdkEventButton *, void *);             //  gallery window mouse event function
      int    KBxpress(GtkWidget *, GdkEventKey *, void *);                    //  gallery window key press event func.
      int    KBxrelease(GtkWidget *, GdkEventKey *, void *);                  //  gallery window key release event
   char * image_navigate(cchar *filez, cchar *action, int Nth = 0);        //  image file list setup and navigate
   int    image_fcomp(cchar *file1, cchar *file2);                         //  file name compare (special sort)
}


/**************************************************************************

   public function to create/update image gallery (thumbnail window)       //  overhauled   v.4.1

   Make window scrolling window of thumbnails for a list of files
   Handle window buttons (up row, down page, open file, etc.)
   Call ufunc() when thumbnail image is clicked

   char * image_gallery(cchar *filez, cchar *action, int Nth, void ufunc(int Nth), GtkWidget *parent)

   filez: image file or directory of image files or file with list of image files

   action:  init:    filez = initial file or directory 
            initF:   filez = file with list of image files to use
            sort:    sort the file list, directories first, ignore case
            insert:  insert filez into file list at position Nth (0 to last+1)
            delete:  delete Nth file in list
            find:    return Nth file (0 base) or null if Nth > last
            paint1:  create or refresh gallery window, anchor = Nth file
            paint2:  refresh gallery window if present, anchor = Nth file
            close    close gallery window
   
   Nth: file to return (action = find) or file to scroll-to (action = paint)

   void ufunc(int Nth):
      - returns Nth of clicked thumbnail (0 to last)
      - returns -1 if gallery window is closed
      - returns -2 if key F1 is pressed (for context help)
      
   parent: optional parent window
           if present, gallery window will overlay the parent window
           (window placement can be undone by the user)

   Returned values:
      Nth: filespec,    others: null
      The returned file belongs to caller and is subject for zfree().

***************************************************************************/

char * image_gallery(cchar *filez, cchar *action, int Nth, void ufunc(int Nth), GtkWidget *parent)
{
   using namespace image_navi;

   GtkWidget      *tbarx;

   zthreadcrash();                                                         //  thread usage not allowed

   if (ufunc) userfunc = ufunc;                                            //  save callback function
   if (parent) pwing = parent;                                             //  save parent window

   if (strstr("init initF sort insert delete find",action))
      return image_navigate(filez,action,Nth);                             //  create or navigate image file list
      
   if (strEqu(action,"close")) {
      if (wing) gtk_widget_destroy(wing);
      return 0;
   }
   
   if (! strnEqu(action,"paint",5))                                        //  must be paint1 or paint2
      zappcrash("image_gallery action: %s",action);

   if (strEqu(action,"paint2") && ! wing) return 0;                        //  refresh but window not active
   if (strEqu(action,"paint1")) fpresent++;                                //  bring window to z-top

   if (Nth >= 0) targposn = Nth;                                           //  scroll-to file position      v.4.1
   else if (filez) targposn = image_gallery_position(filez,0);             //  or use filez if present      v.4.4
   if (targposn > nfiles-1) targposn = nfiles-1;                           //  (-1 for no change)           v.4.1

   if (wing) {                                                             //  repaint existing gallery window
      gallery_paint(0,0);
      return 0;
   }

   wing = gtk_window_new(GTK_WINDOW_TOPLEVEL);                             //  create new gallery window
   
   if (pwing) {
      gtk_window_get_size(GTK_WINDOW(pwing),&xwinW,&xwinH);                //  overlay parent window        v.3.8
      gtk_window_get_position(GTK_WINDOW(pwing),&xwinX,&xwinY);
      gtk_window_set_default_size(GTK_WINDOW(wing),xwinW,xwinH);
      gtk_window_move(GTK_WINDOW(wing),xwinX,xwinY);
   }
   else {
      gtk_window_set_default_size(GTK_WINDOW(wing),xwinW,xwinH+56);        //  + toolbar size to stop shrinkage
      gtk_window_set_position(GTK_WINDOW(wing),GTK_WIN_POS_MOUSE);         //  v.3.7
   }

   vboxx = gtk_vbox_new(0,0);                                              //  vertical packing box
   gtk_container_add(GTK_CONTAINER(wing),vboxx);                           //  add to main window

   tbarx = create_toolbar(vboxx,24);                                       //  add toolbar and buttons 
   gtk_toolbar_set_style(GTK_TOOLBAR(tbarx),GTK_TOOLBAR_BOTH);             //  v.4.7 

   add_toolbar_button(tbarx, ZTX("bigger"), ZTX("increase thumbnail size"), "gtk-zoom-in", menufuncx);
   add_toolbar_button(tbarx, ZTX("smaller"), ZTX("reduce thumbnail size"), "gtk-zoom-out", menufuncx);
   add_toolbar_button(tbarx, ZTX("parent"), ZTX("parent directory"), "folder.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("open"), ZTX("open a file"), "gtk-open", menufuncx);
   add_toolbar_button(tbarx, ZTX("first page"), ZTX("jump to first file"), "first-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev page"), ZTX("previous page"), "prev-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev row"), ZTX("previous row"), "prev-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next row"), ZTX("next row"), "next-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next page"), ZTX("next page"), "next-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("last page"), ZTX("jump to last file"), "last-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("close"), ZTX("close image gallery"), "gtk-close", menufuncx);
   
   scrwing = gtk_scrolled_window_new(0,0);                                 //  create scrolled window
   gtk_container_add(GTK_CONTAINER(vboxx),scrwing);                        //  add to main window
   layout = gtk_layout_new(0,0);                                           //  create drawing window
   gtk_container_add(GTK_CONTAINER(scrwing),layout);                       //  add to scrolled window
   gtk_scrolled_window_set_policy(SCROLLWIN(scrwing),NEVER,ALWAYS);        //  vertical scroll bar
   scrollbar = gtk_layout_get_vadjustment(GTK_LAYOUT(layout));

   G_SIGNAL(wing,"destroy",gallery_destroy,0)                              //  connect window events
   G_SIGNAL(layout,"expose-event",gallery_paint,0)
   gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK);                    //  connect mouse events
   G_SIGNAL(layout,"button-press-event",mouse_xevent,0)
   G_SIGNAL(wing,"key-press-event",KBxpress,0)                                //  connect KB events
   G_SIGNAL(wing,"key-release-event",KBxrelease,0)

   gtk_widget_show_all(wing);                                              //  show all widgets
   gdkgc = gdk_gc_new(layout->window);                                     //  initz. graphics context
   gallery_paint(0,0);                                                     //  repaint
   gtk_window_present(GTK_WINDOW(wing));                                   //  bring gallery window to top

   return 0;
}


//  expose event private function
//  paint gallery window - draw all thumbnail images that can fit
//  v.3.6 mostly rewritten for higher speed and scroll bar support

int image_navi::gallery_paint(GtkWidget *, GdkEventExpose *expose)
{
   using namespace image_navi;

   GdkPixbuf         *pxbT;
   GdkRectangle      rect;
   int               ii, nrows, row1, row2;
   int               currscrollposn;
   int               layoutW, layoutH;
   int               expy1, expy2, row, col;
   int               thumx, thumy, orgx, orgy, ww, hh;
   int               popup = 0;
   char              *fileC, *pp, *fname;
   char              wintitle[200];
   
   xwinW = layout->allocation.width;                                       //  curr. gallery window size
   xwinH = layout->allocation.height;
   
   thumbW = thumbsize + 10;                                                //  thumbnail cell size
   thumbH = thumbsize + 30;
   xmargW = xmargH = 5;                                                    //  edge margins

   if (! thumbsize) {
      thumbW = 400;                                                        //  zero, list view        v.4.7
      thumbH = 20;
   }
   
   xrows = int(0.2 + 1.0 * xwinH / thumbH);                                //  get thumbnail rows and cols that   v.4.1
   xcols = int(0.3 + 1.0 * xwinW / thumbW);                                //    (almost) fit in window 
   if (xrows < 1) xrows = 1;
   if (xcols < 1) xcols = 1;
   nrows = (nfiles+xcols-1) / xcols;                                       //  thumbnail rows, 1 or more          v.4.1

   layoutW = xcols * thumbW + xmargW + 10;                                 //  layout size for entire file list
   layoutH = (nrows+1) * thumbH + xmargH;                                  //  last row + 1
   if (layoutH < xwinH) layoutH = xwinH;                                   //  bugfix
   gtk_layout_set_size(GTK_LAYOUT(layout),layoutW,layoutH);

   maxscroll = layoutH - xwinH;                                            //  scroll to end of layout
   if (maxscroll < 0) maxscroll = 0;

   gtk_adjustment_set_step_increment(scrollbar,thumbH);                    //  scrollbar works in row steps
   gtk_adjustment_set_page_increment(scrollbar,thumbH * xrows);            //  and in page steps

   currscrollposn = gtk_adjustment_get_value(scrollbar);                   //  current scroll position
   scrollposn = currscrollposn;

   if (targposn >= 0) {
      scrollposn = targposn / xcols * thumbH;                              //  set initial scroll position from 
      if (scrollposn > maxscroll) scrollposn = maxscroll;                  //   target file position if defined,
      scrollposn = scrollposn / thumbH * thumbH;                           //    then disable it for local control 
   }                                                                       //     of scrolling from gallery window
   targposn = -1;                                                          

   if (scrollposn > maxscroll) scrollposn = maxscroll;                     //  bugfix              v.3.7

   if (scrollposn != currscrollposn)
      gtk_adjustment_set_value(scrollbar,scrollposn);                      //  will cause re-entrance

   if (! expose)                                                           //  initial window or navigation button
   {
      snprintf(wintitle,199,"%s  %d files",galleryname,nfiles);            //  window title: gallery name and file count
      gtk_window_set_title(GTK_WINDOW(wing),wintitle);
      gdk_window_invalidate_rect(wing->window,0,1);                        //  will cause re-entrance             v.4.1
      if (fpresent) gtk_window_present(GTK_WINDOW(wing));                  //  bring window to top
      fpresent = 0;
      return 0;
   }
   
   rect = expose->area;                                                    //  exposed area to refresh
   expy1 = rect.y;
   expy2 = expy1 + rect.height;
   row1 = expy1 / thumbH;
   row2 = expy2 / thumbH;

   ii = row1 * xcols;                                                      //  1st image file in top row
   if (ii >= nfiles) return 1;
   fileC = strdupz(flist[ii],0,"gallery_paint");                           //  next file to write to window
   *fileC = '/';                                                           //  directories have ! instead of /

   for (row = row1; row <= row2; row++)                                    //  draw file thumbnails
   for (col = 0; col < xcols; col++)
   {
      thumx = col * thumbW + xmargW;                                       //  upper left corner in window space
      thumy = row * thumbH + xmargH;
      
      if (thumy < expy2 && thumy+14 > expy1) {                             //  if in exposed area,
         pp = (char *) strrchr(fileC,'/');                                 //    draw file name 
         if (pp) fname = pp + 1;
         else fname = fileC;
         draw_text(layout,fname,thumx,thumy,thumbW);
      }
      
      if (thumbsize)                                                       //  zero >> list view         v.3.7
         pxbT = image_thumbnail(fileC,thumbsize);                          //  get thumbnail
      else pxbT = 0;

      if (genthumbs == 1 && ! popup) {                                     //  inform user of delay      v.4.0
         write_popup_text("open","please wait",200,10,wing);
         write_popup_text("write","\n generating thumbnails");             //  1st pass gtk bug, no text ////
         genthumbs = 2;
         popup = 1;
      }

      if (genthumbs < 5) zmainloop();                                      //  mysterious, necessary     ////
      
      if (pxbT) {
         thumy = thumy + 16;                                               //  thumbnail 16 pixels down from text
         orgx = 0;
         orgy = 0;
         ww = gdk_pixbuf_get_width(pxbT);
         hh = gdk_pixbuf_get_height(pxbT);

         if (thumy < expy1) {
            orgy = orgy + (expy1 - thumy);
            hh = hh - (expy1 - thumy);
         }
         if (thumy + orgy + hh > expy2)
            hh = hh - (thumy + orgy + hh - expy2);
         
         if (orgy >= 0 && hh > 0)
            gdk_draw_pixbuf(GTK_LAYOUT(layout)->bin_window,0,pxbT,
                             orgx,orgy,thumx+orgx,thumy+orgy,ww,hh,NODITHER,0,0); 
         g_object_unref(pxbT);
      }

      zfree(fileC);
      fileC = 0;
      if (++ii == nfiles) goto thumbsdone;
      fileC = strdupz(flist[ii],0,"gallery_paint");
      *fileC = '/';
   }

thumbsdone:

   if (fileC) zfree(fileC);                                                //  leak             v.4.1
   if (popup) {
      write_popup_text("close");                                           //  v.4.0
      genthumbs = 0;
   }

   return 1;
}


//  private function
//  write text for thumbnail limited by width of thumbnail

void image_navi::draw_text(GtkWidget *win, char *text, int x, int y, int ww)
{
   using namespace image_navi;

   static PangoFontDescription   *pfont = 0;
   static PangoLayout            *playout = 0;
   
   int            nn, ww2, hh2, cc;
   char           text2[100];
   static int     thumbsize2 = 0;
   static char    thumbfont[12] = "";
   
   if (thumbsize != thumbsize2) {                                          //  scale file name font to thumbnail size
      thumbsize2 = thumbsize;                                              //  v.4.7
      nn = 7 + thumbsize / 128;                                            //  font size from 7 to 10
      if (thumbsize == 0) nn = 9;                                          //  list view, use 9
      sprintf(thumbfont,"sans %d",nn);
      pfont = pango_font_description_from_string(thumbfont);
      playout = gtk_widget_create_pango_layout(win,0);
      pango_layout_set_font_description(playout,pfont);
   }

   for (cc = 50; cc > 7; cc--)                                             //  allow up to 50 graphic chars.
   {                                                                       //    and reduce until fits      v.2.3
      cc = utf8substring(text2,text,0,cc);                                 //  get substring up to cc chars.
      pango_layout_set_text(playout,text2,-1);                             //  compute layout
      pango_layout_get_pixel_size(playout,&ww2,&hh2);                      //  pixel width of layout
      if (ww2 < ww) break;                                                 //  stop when it fits
   }

   gdk_draw_layout(GTK_LAYOUT(win)->bin_window,gdkgc,x,y,playout);
   return;
}


//  private function
//  gallery window destroy event - track if window is active or not

void image_navi::gallery_destroy()
{
   using namespace image_navi;
   
   wing = 0;                                                               //  no window
   if (userfunc) userfunc(-1);                                             //  tell caller
   return;
}


//  private function - menu function for gallery window
//    - scroll window as requested
//    - jump to new file or folder as requested

void image_navi::menufuncx(GtkWidget *win, cchar *menu)
{
   using namespace image_navi;

   int         ii, scrollp;
   char        *filex, *filez, *pp;
   char        buff[maxfcc];
   
   if (strEqu(menu,ZTX("close"))) {                                        //  close image gallery window
      gtk_widget_destroy(wing);                                            //  destroy event function calls userfunc(-1)
      return;
   }

   if (strEqu(menu,ZTX("bigger")))  {                                      //  next bigger thumbnail size
      for (ii = 0; ii < thumbxx; ii++) 
         if (thumbsize == thumbx[ii]) break;
      if (ii == 0) return;
      thumbsize = thumbx[ii-1];
      targposn = scrollposn / thumbH * xcols;                              //  keep top row position        v.4.1
      gallery_paint(0,0);
      return;
   }

   if (strEqu(menu,ZTX("smaller")))  {                                     //  next smaller
      for (ii = 0; ii < thumbxx; ii++) 
         if (thumbsize == thumbx[ii]) break;
      if (ii >= thumbxx-1) thumbsize = 0;                                  //  no thumbs, list view   v.3.7
      else  thumbsize = thumbx[ii+1];
      targposn = scrollposn / thumbH * xcols;                              //  keep top row position        v.4.1
      gallery_paint(0,0);
      return;
   }

   if (strEqu(menu,ZTX("open"))) {                                         //  go to a specific file
      if (nfiles) {
         filez = strdupz(flist[0],0,"image_navi");
         *filez = '/';
      }
      else filez = 0;
      filex = zgetfile1(ZTX("select new file"),"open",filez);              //  file chooser dialog
      if (filez) zfree(filez);
      if (filex) {
         image_navigate(filex,"init");                                     //  get new file list
         targposn = image_gallery_position(filex,0);                       //  initial window scroll target
         gallery_paint(0,0);
         zfree(filex);
      }
      return;
   }

   scrollp = scrollposn;                                                   //  v.3.7

   if (strEqu(menu,ZTX("parent"))) {
      if (nfiles) filez = strdupz(flist[0],0,"image_navi");                //  get parent directory
      else filez = strdupz(getcwd(buff,maxfcc-1),0,"image_navi");
      if (! filez) return;
      *filez = '/';
      pp = strrchr(filez,'/');
      if (pp) *pp = 0;
      pp = strrchr(filez,'/');
      if (pp) *pp = 0;
      image_navigate(filez,"init");                                        //  get new file list
      gallery_paint(0,0);
      zfree(filez);
      scrollp = 0;                                                         //  v.3.7
   }
   
   if (strEqu(menu,ZTX("prev row"))) scrollp -= thumbH;
   if (strEqu(menu,ZTX("next row"))) scrollp += thumbH;
   if (strEqu(menu,ZTX("prev page"))) scrollp -= thumbH * xrows;
   if (strEqu(menu,ZTX("next page"))) scrollp += thumbH * xrows;
   if (strEqu(menu,ZTX("first page"))) scrollp = 0;
   if (strEqu(menu,ZTX("last page"))) scrollp = maxscroll;
   
   if (scrollp < 0) scrollp = 0;                                           //  enforce limits
   if (scrollp > maxscroll) scrollp = maxscroll;
   scrollp = scrollp / thumbH * thumbH;                                    //  align top row

   if (scrollp != scrollposn)
      gtk_adjustment_set_value(scrollbar,scrollp);

   return;
}


//  private function
//  mouse event function for gallery window - get selected thumbnail and file
//  user function receives clicked file, which is subject for zfree()

void image_navi::mouse_xevent(GtkWidget *, GdkEventButton *event, void *)
{
   using namespace image_navi;

   int            mousex, mousey;
   int            row, col, nrows, ii, err;
   char           *filez;
   struct stat    statb;
   
   if (! nfiles) return;                                                   //  empty window
   
   mousex = int(event->x);
   mousey = int(event->y);

   row = (mousey - xmargH) / thumbH;                                       //  find selected row, col
   col = (mousex - xmargW) / thumbW;

   nrows = 1 + (nfiles-1) / xcols;                                         //  total thumbnail rows, 1 or more
   if (col < 0 || col >= xcols) return;
   if (row < 0 || row >= nrows) return;

   ii = xcols * row + col;
   if (ii >= nfiles) return;

   filez = strdupz(flist[ii],0,"image_navi");                              //  selected file
   *filez = '/';
   
   err = stat(filez,&statb);
   if (err) {                                                              //  file is gone?
      zfree(filez);
      return;
   }

   if (S_ISDIR(statb.st_mode)) {                                           //  if directory, go there
      image_navigate(filez,"init");
      gallery_paint(0,0);
      zfree(filez);
      return;
   }
   
   if (userfunc) userfunc(ii);                                             //  clicked file position to user
   return;
}


//  private function
//  KB event function - respond to keyboard navigation keys
//  key definitions: /usr/include/gtk-2.0/gdk/gdkkeysyms.h

int image_navi::KBxpress(GtkWidget *win, GdkEventKey *event, void *)       //  prevent propagation of key-press
{                                                                          //    events to toolbar buttons  v.2.9
   return 1;
}

int image_navi::KBxrelease(GtkWidget *win, GdkEventKey *event, void *)
{
   using namespace image_navi;

   int      KBkey;
   
   KBkey = event->keyval;

   if (KBkey == GDK_plus) menufuncx(win,ZTX("bigger"));                    //  +/- = bigger/smaller thumbnails
   if (KBkey == GDK_equal) menufuncx(win,ZTX("bigger"));
   if (KBkey == GDK_minus) menufuncx(win,ZTX("smaller"));
   if (KBkey == GDK_KP_Add) menufuncx(win,ZTX("bigger"));                  //  keypad +/- also        v.2.24
   if (KBkey == GDK_KP_Subtract) menufuncx(win,ZTX("smaller"));

   if (KBkey == GDK_Left) menufuncx(win,ZTX("prev page"));                 //  left arrow = previous page
   if (KBkey == GDK_Right) menufuncx(win,ZTX("next page"));                //  right arrow = next page
   if (KBkey == GDK_Up) menufuncx(win,ZTX("prev row"));                    //  up arrow = previous row
   if (KBkey == GDK_Down) menufuncx(win,ZTX("next row"));                  //  down arrow = next row
   
   if (KBkey == GDK_Home) menufuncx(win,ZTX("first page"));                //  keys added       v.2.26
   if (KBkey == GDK_End) menufuncx(win,ZTX("last page"));
   if (KBkey == GDK_Page_Up) menufuncx(win,ZTX("prev page"));
   if (KBkey == GDK_Page_Down) menufuncx(win,ZTX("next page"));

   if (KBkey == GDK_Escape) gtk_widget_destroy(win);                       //  Escape = cancel gallery window
   
   if (KBkey == GDK_F1)
      showz_userguide(zfuncs::F1_help_topic);                              //  F1 help request     v.4.1
   
   return 1;
}


//  Public function - get file position in file list.
//  If Nth position matches file, this is returned.
//  Otherwise the file is searched from position 0.
//  Position 0-last is returned if found, or -1 if not.

int image_gallery_position(cchar *file, int Nth)                           //  new     v.4.1
{
   using namespace image_navi;
   
   int      ii;
   
   if (! nfiles) return -1;

   if (Nth >= 0 && Nth < nfiles) ii = Nth;
   else ii = 0;
   
   if (strEqu(file+1,flist[ii]+1)) return ii;                              //  1st flist[ii] char. may be !

   for (ii = 0; ii < nfiles; ii++)
      if (strEqu(file+1,flist[ii]+1)) break;

   if (ii < nfiles) return ii;
   return -1;
}


//  public function
//  determine if a file is a directory or a supported image file type
//  return: 0 = error, 1 = directory, 2 = image file, 3 = other

int image_file_type(cchar *file)
{
   using namespace image_navi;

   int            err, cc;
   cchar          *pp;
   struct stat    statbuf;

   if (! file) return 0;
   err = stat(file,&statbuf);
   if (err) return 0;

   if (S_ISDIR(statbuf.st_mode)) {                                         //  directory
      cc = strlen(file);
      if (cc > 12) {
         pp = file + cc - 12;
         if (strEqu(pp,"/.thumbnails")) return 3;                          //  .thumbnails
      }
      return 1;
   }

   if (S_ISREG(statbuf.st_mode)) {                                         //  reg. file
      pp = strrchr(file,'.');
      if (! pp) return 3;
      pp = strcasestr(imagetypes,pp);                                      //  supported image type
      if (pp) return 2;
   }
   
   return 3;
}


//  Public function
//  Get thumbnail filespec for the given image file.
//  If missing or stale, add or update in /.thumbnails directory.
//  Returned filespec is subject for zfree().
//  Use 4 threads to build missing thumbnails asynchrounously              //  v.4.4
//  (and hopefully ahead of need).

namespace image_thumbs {
   char * image_thumbfile_1x(char *imagefile);
   void * image_thumbfile_thread(void *arg);
   int    image_thumbfile_lock(char *imagefile, int lock);
   cchar    *toomanyfiles = "exceed %d files in one directory";
   char     *imagefilex;
   char     *thumbfilex;
   char     *directoryx;
   char     **filelistx;
   int      Nfilesx;
   int      indexx;
   int      busythreads;
   int      stopthreads;
   char     *lockfiles[5];
   mutex    filelock = PTHREAD_MUTEX_INITIALIZER;
   mutex    thumblock = PTHREAD_MUTEX_INITIALIZER;
}


char * image_thumbfile(char *imagefile)
{
   using namespace image_thumbs;

   char           *thumbfile, *directory;
   char           *pfile, *bfile, *buff;
   cchar          *findcommand = "find \"%s\" -maxdepth 1";
   struct stat    statf, statb;
   int            err, ftyp, contx = 0;

   zthreadcrash();                                                         //  thread usage not allowed

   err = stat(imagefile,&statf);
   if (err) return 0;
   if (! S_ISREG(statf.st_mode)) return 0;                                 //  not a regular file
   if (image_file_type(imagefile) != 2) return 0;                          //  unsupported image type
   
   pfile = strrchr(imagefile,'/');                                         //  get .../filename.xxx
   if (! pfile) return 0;
   if (pfile > imagefile+12 && strnEqu(pfile-12,"/.thumbnails/",13)) {     //  .../.thumbnails/filename.xxx ??
      thumbfile = strdupz(imagefile,0,"image_thumbfile");                  //  yes, a thumbnail file
      return thumbfile;                                                    //  return same file
   }
   
   thumbfile = strdupz(imagefile,20,"image_thumbfile");                    //  construct thumbnail file
   bfile = thumbfile + (pfile - imagefile);                                //    .../.thumbnails/filename.xxx.png
   strcpy(bfile,"/.thumbnails");
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   err = stat(thumbfile,&statb);                                           //  thumbnail file exists ??
   if (! err) {                                                            //  yes
      if (statb.st_mtime >= statf.st_mtime) return thumbfile;              //  up to date, return it
      zfree(thumbfile);
      thumbfile = image_thumbfile_1x(imagefile);                           //  refresh stale thumbnail
      return thumbfile;                                                    //  return it
   }

   zfree(thumbfile);

   directory = strdupz(imagefile,0,"image_thumbfile");                     //  thumbnail does not exist
   pfile = strrchr(directory,'/');
   pfile[1] = 0;                                                           //  image directory/
   
   if (! directoryx) directoryx = directory;                               //  first use
   else if (strNeq(directoryx,directory)) {                                //  new directory to search
      stopthreads = 1;
      while (busythreads) zsleep(0.001);                                   //  stop prior search if any
      zfree(directoryx);
      directoryx = directory;                                              //  directory for new search
   }
   else zfree(directory);                                                  //  no change

   imagefilex = imagefile;                                                 //  target thumbnail to get

   while (busythreads) {                                                   //  if ongoing search, request thumbnail
      if (! imagefilex) return thumbfilex;                                 //      from existing search threads
      zsleep(0.001);                                                       //  (this can fail rarely)
   }

   if (filelistx)                                                          //  set up empty file list
      for (int ii = 0; ii < Nfilesx; ii++) zfree(filelistx[ii]);
   else 
      filelistx = (char **) zmalloc(maxFiles * sizeof(char *),"image_thumbfile");
   Nfilesx = 0;

   while ((buff = command_output(contx,findcommand,directoryx)))           //  search image file directory
   {
      if (Nfilesx == maxFiles) {                                           //  no room
         zmessageACK(0,toomanyfiles,maxFiles);
         break;
      }

      ftyp = image_file_type(buff);
      if (ftyp == 2) {                                                     //  supported image file type
         filelistx[Nfilesx] = buff;                                        //  add to file list
         Nfilesx++;
      }
      else zfree(buff);
   }
   
   busythreads = 4;                                                        //  start search threads for this directory
   stopthreads = 0;
   indexx = -1;

   for (int ii = 0; ii < 4; ii++)
      start_detached_thread(image_thumbfile_thread,0);
   
   while (busythreads) {                                                   //  return as soon as target thumbnail
      if (! imagefilex) return thumbfilex;                                 //    has been generated
      zsleep(0.001);
   }

   if (! imagefilex) return thumbfilex;
   printf("image_thumbfile failed %s \n",imagefile);                       //  failure
   return 0;
}


//  private function
//  four threads each processing 1/4 of the entries in filelistx

void * image_thumbs::image_thumbfile_thread(void *arg)
{
   using namespace image_thumbs;

   int      ii;
   char     *thumbfile;

   while (indexx < Nfilesx)
   {
      if (imagefilex) {                                                    //  if thumbnail request is waiting
         mutex_lock(&thumblock);                                           //    take care of it first
         if (imagefilex) 
            thumbfilex = image_thumbfile_1x(imagefilex);
         imagefilex = 0;
         mutex_unlock(&thumblock);
      }
      
      if (stopthreads) break;                                              //  stop before done
     
      ii = zadd_locked(indexx,+1);                                         //  get next file from list
      if (ii >= Nfilesx) break;
      
      thumbfile = image_thumbfile_1x(filelistx[ii]);                       //  make thumbnail file if needed
      if (thumbfile) zfree(thumbfile);
   }

   zadd_locked(busythreads,-1);                                            //  decrement busy count
   pthread_exit(0);                                                        //  "return" cannot be used here
}


//  Private function to create a single thumbnail file.
//  Get thumbnail file for the given image file.
//  If missing or stale, add or update in /.thumbnails directory.
//  Returned filespec is subject for zfree().

char * image_thumbs::image_thumbfile_1x(char *imagefile)
{
   using namespace image_thumbs;

   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   char              *pfile, *bfile, *thumbfile;
   int               err, sizew, sizeh;
   struct stat       statf, statb;
   
   err = stat(imagefile,&statf);
   if (err) return 0;
   if (! S_ISREG(statf.st_mode)) return 0;                                 //  not a regular file
   if (image_file_type(imagefile) != 2) return 0;                          //  unsupported image type
   
   pfile = strrchr(imagefile,'/');                                         //  get .../filename.xxx
   if (! pfile) return 0;
   if (pfile > imagefile+12 && strnEqu(pfile-12,"/.thumbnails/",13)) {     //  .../.thumbnails/filename.xxx ??
      thumbfile = strdupz(imagefile,0,"image_thumbfile");                  //  yes, a thumbnail file
      return thumbfile;                                                    //  return same file
   }
   
   thumbfile = strdupz(imagefile,20,"image_thumbfile");                    //  construct thumbnail file
   bfile = thumbfile + (pfile - imagefile);                                //    .../.thumbnails/filename.xxx.png
   strcpy(bfile,"/.thumbnails");
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   while (! image_thumbfile_lock(imagefile,1)) zsleep(0.001);              //  lock file for me

   err = stat(thumbfile,&statb);                                           //  thumbnail file exists ??
   if (err || (statb.st_mtime < statf.st_mtime)) 
   {                                                                       //  does not exist or stale
      *bfile = 0;
      err = stat(thumbfile,&statb);
      if (err) err = mkdir(thumbfile,0751);                                //  create .thumbnails directory
      if (err) {
         image_thumbfile_lock(imagefile,0);
         return 0;
      }
      *bfile = *pfile;
      sizew = sizeh = thumbfilesize;                                       //  create thumbnail pixbuf
      thumbpxb = gdk_pixbuf_new_from_file_at_size(imagefile,sizew,sizeh,&gerror);
      if (! thumbpxb) {
         printf("gdk_pixbuf_new error: %s \n",gerror->message);            //  diagnose error    v.3.3
         image_thumbfile_lock(imagefile,0);
         return 0;
      }
      gdk_pixbuf_save(thumbpxb,thumbfile,"png",&gerror,null);              //  save in /.thumbnails/ directory
      g_object_unref(thumbpxb);
      image_navi::genthumbs++;                                             //  count generated thumbnails
   }
   
   image_thumbfile_lock(imagefile,0);                                      //  return thumbfile
   return thumbfile;
}


//  prevent interference from multiple threads working on the same image file

int image_thumbs::image_thumbfile_lock(char *imagefile, int lock)
{
   using namespace image_thumbs;

   int         ii, jj = -1;
   
   if (lock)                                                               //  lock file if possible
   {
      mutex_lock(&filelock);

      for (ii = 0; ii < 5; ii++) {
         if (! lockfiles[ii]) jj = ii;                                     //  remember 1st empty slot
         else if (strEqu(lockfiles[ii],imagefile)) {
            mutex_unlock(&filelock);                                       //  file is already locked
            return 0;
         }
      }

      if (jj < 0) zappcrash("image_thumbfile_lock > 5");
      lockfiles[jj] = imagefile;                                           //  lock the file
      mutex_unlock(&filelock);
      return 1;
   }

   else                                                                    //  unlock file
   {
      for (ii = 0; ii < 5; ii++) {
         if (! lockfiles[ii]) continue;
         if (strEqu(lockfiles[ii],imagefile)) {
            lockfiles[ii] = 0;
            return 1;
         }
      }
      zappcrash("image_thumbfile_lock gone");
      return 0;
   }
}


//  Public function
//  Get thumbnail image for given image file, from .thumbnails directory.
//  Add thumbnail file if missing, or update it if older than image file.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.

GdkPixbuf * image_thumbnail(char *fpath, int size)
{
   using namespace zfuncs;
   using namespace image_navi;

   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   int               ii, err;
   char              *bpath;
   time_t            mtime;
   struct stat       statf;
   const int         cachesize = thumbnail_cachesize;                      //  shorthand   v.3.7

   static int           nextcache, ftf = 1;
   static int           sizecache[cachesize];
   static time_t        mtimecache[cachesize];
   static char          *fpathcache[cachesize];
   static GdkPixbuf     *pixbufcache[cachesize];
   
   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   if (ftf) {                                                              //  first call   v.3.7
      for (ii = 0; ii < cachesize; ii++) 
         fpathcache[ii] = 0;                                               //  clear cache
      ftf = 0;
   }
   
   err = stat(fpath,&statf);                                               //  fpath status info
   if (err) return 0;

   if (S_ISDIR(statf.st_mode)) {                                           //  if directory, return folder image
      bpath = zmalloc(500);
      strncatv(bpath,499,zicondir,"/folder256.png",null);
      thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);
      zfree(bpath);
      return thumbpxb;
   }
   
   mtime = statf.st_mtime;                                                 //  last modification time
   
   if (! size) size = thumbfilesize;                                       //  default thumb size
   
   for (ii = nextcache; ii >= 0; ii--)                                     //  get cached pixbuf if found   v.3.7
      if (fpathcache[ii] && strEqu(fpath,fpathcache[ii]) &&
          sizecache[ii] == size && mtime == mtimecache[ii]) break;         //  check mtime (bugfix)    v.3.9
   if (ii >= 0) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);
      return thumbpxb;
   }
   for (ii = cachesize-1; ii > nextcache; ii--)                            //  continue search
      if (fpathcache[ii] && strEqu(fpath,fpathcache[ii]) &&
          sizecache[ii] == size && mtime == mtimecache[ii]) break;
   if (ii > nextcache) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);
      return thumbpxb;
   }
   
   if (size > thumbfilesize) {                                             //  support huge thumbnails    v.3.6
      thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror);
      goto addcache;
   }
   
   bpath = image_thumbfile(fpath);                                         //  get thumbnail file
   if (! bpath) return 0;
   thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);   //  get thumbnail
   zfree(bpath);
   goto addcache;

addcache:
   nextcache++;                                                            //  next cache slot (oldest)  v.3.7
   if (nextcache == cachesize) nextcache = 0;
   ii = nextcache;
   if (fpathcache[ii]) {                                                   //  free prior occupant
      zfree(fpathcache[ii]);
      g_object_unref(pixbufcache[ii]);
   }
   fpathcache[ii] = strdupz(fpath,0,"thumbnail_cache");                    //  add new occupant
   pixbufcache[ii] = gdk_pixbuf_copy(thumbpxb);                            //  this memory is not tracked
   sizecache[ii] = size;
   mtimecache[ii] = mtime;                                                 //  v.3.9

   return thumbpxb;                                                        //  return pixbuf to caller
}


/**************************************************************************

   private function - manage list of image files within a directory        //  overhauled    v.4.1

   get an image file in the same directory as given file or directory

   char * image_navi::image_navigate(cchar *filez, cchar *action, int Nth)

   action:  init:    file list = directories and image files in directory filez
            initF:   file list = filez = list of image files to use (cchar **)
            sort:    sort the file list, directories first, ignore case
            insert:  insert filez into file list at position Nth (0 to last+1)
            delete:  delete Nth file in list
            find:    returns Nth file (0 base) or null if Nth > last

   Nth: file to return for action = find
   
   Returned values:
      find: filespec, else null
      The returned file belongs to caller and is subject for zfree().

***************************************************************************/

char * image_navi::image_navigate(cchar *filez, cchar *action, int Nth)
{
   using namespace image_navi;

   char           *buff;
   cchar          *findcommand = "find \"%s\" -maxdepth 1";
   char           *pp, *file2;
   int            err, ii, ftyp, contx = 0, fposn;
   FILE           *fid;
   struct stat    statbuf;
   
   if (! strstr("init initF sort insert delete find",action))
         zappcrash("image_navigate %s",action);

   if (strnEqu(action,"init",4)) {                                         //  init or initF
      if (flist)
         for (int ii = 0; ii < nfiles; ii++) zfree(flist[ii]);             //  free prior list memory
      if (flist) zfree(flist);
      flist = (char **) zmalloc(maxFiles * sizeof(char *),"image_navi");   //  list of file pointers
      nfiles = nimages = 0;                                                //  no files
      fposn = 0;
   }

   if (strEqu(action,"init"))                                              //  initialize from given directory
   {
      if (! filez) return 0;                                               //  should not happen

      galleryname = strdupz(filez,0,"image_navi");
      gallerytype = 1;                                                     //  gallery type = directory

      err = stat(galleryname,&statbuf);
      if (err) {
         pp = (char *) strrchr(galleryname,'/');                           //  bad file, check directory part  
         if (! pp) return 0;
         pp[1] = 0;
         err = stat(galleryname,&statbuf);
         if (err) return 0;                                                //  give up, empty file list
      }

      if (S_ISREG(statbuf.st_mode)) {                                      //  if a file, get directory part
         pp = (char *) strrchr(galleryname,'/');
         if (! pp) return 0;
         pp[1] = 0;
      }

      while ((buff = command_output(contx,findcommand,galleryname)))       //  find all files
      {
         if (strEqu(buff,galleryname)) {                                   //  skip self directory
            zfree(buff);
            continue;
         }
         
         if (nfiles == maxFiles) {                                         //  message, not crash     v.4.4
            zmessageACK(0,toomanyfiles,maxFiles);
            break;
         }

         ftyp = image_file_type(buff);

         if (ftyp == 1) {                                                  //  subdirectory
            flist[nfiles] = buff;                                          //  add to file list
            flist[nfiles][0] = '!';                                        //  if directory, make it sort first
            nfiles++;
         }

         else if (ftyp == 2) {                                             //  supported image file type
            flist[nfiles] = buff;                                          //  add to file list
            nfiles++;
            nimages++;                                                     //  v.4.1
         }

         else {
            zfree(buff);                                                   //  (.thumbnails not ftyp 1)
            continue;
         }
      }

      if (nfiles > 1) 
         HeapSort(flist,nfiles,image_fcomp);                               //  Heap Sort - pointers to strings

      return 0;
   }

   if (strEqu(action,"initF"))                                             //  initialize from given list   v.2.4
   {
      galleryname = strdupz(filez,0,"image_navi");
      gallerytype = 2;                                                     //  gallery type = file list     v.4.1
      
      fid = fopen(galleryname,"r");                                        //  open file
      if (! fid) return 0;

      buff = zmalloc(maxfcc);
      
      while (true)                                                         //  read list of files
      {
         if (nfiles == maxFiles) {
            zmessageACK(0,toomanyfiles,maxFiles);
            break;
         }
         pp = fgets_trim(buff,maxfcc-1,fid,1);
         if (! pp) break;
         flist[nfiles] = strdupz(buff,0,"image_navi");                     //  add files to memory list     v.3.5
         nfiles++;
         nimages++;
      }

      fclose(fid);
      zfree(buff);                                                         //  sort removed                 v.3.9
      return 0;
   }

   if (strEqu(action,"sort"))                                              //  sort the list from initF     v.3.9
   {
      if (nfiles == 0) return 0;
      HeapSort(flist,nfiles,image_fcomp);                                  //  Heap Sort - pointers to strings
      return 0;
   }

   if (strEqu(action,"insert"))                                            //  insert new file into list    v.3.8
   {
      fposn = Nth;                                                         //  file position from caller
      if (fposn < 0) fposn = 0;                                            //  limit to allowed range
      if (fposn > nfiles) fposn = nfiles;

      if (nfiles == maxFiles-1) {                                          //  no room
         zmessageACK(0,toomanyfiles,maxFiles);
         return 0;
      }

      for (ii = nfiles; ii > fposn; ii--)                                  //  create hole is list
         flist[ii] = flist[ii-1];

      flist[fposn] = strdupz(filez,0,"image_navi");                        //  put new file in hole
      nfiles++;
   }

   if (strEqu(action,"delete"))                                            //  delete file from list     v.3.8
   {
      fposn = Nth;                                                         //  file position from caller must be OK
      if (fposn < 0 || fposn > nfiles-1) return 0;
      zfree(flist[fposn]);                                                 //  remove file from list
      nfiles--;
      for (ii = fposn; ii < nfiles; ii++) {                                //  close the hole
         if (nfiles < 0) printf("meaningless reference %d",ii);            //  stop g++ optimization bug  ////
         flist[ii] = flist[ii+1];
      }
   }
   
   if (strEqu(action,"find"))
   {
      fposn = Nth;                                                         //  file position from caller must be OK
      if (fposn < 0 || fposn > nfiles-1) return 0;
      file2 = strdupz(flist[fposn],0,"image_navi");                        //  get Nth file
      file2[0] = '/';                                                      //  restore initial '/'
      err = stat(file2,&statbuf);
      if (! err) return file2;
      zfree(file2);
   }
   
   return 0;
}


//  private function for special file name compare
//  directories sort first and upper/lower case is ignored

int image_navi::image_fcomp(cchar *file1, cchar *file2)
{
   int      nn;
   nn = strcasecmp(file1,file2);                                           //  compare ignoring case
   if (nn != 0) return nn;
   nn = strcmp(file1,file2);                                               //  if equal, do normal compare
   return nn;
}


/**************************************************************************/

//  Select files from the image gallery window, return list of files selected.
//  The dialog shows the list of files selected and can be edited.
//  The returned file list belongs to caller and is subject for zfree().
//  The file list EOL is marked with null.
//  The image_gallery() callback function is changed and must be restored by caller.

int  imgl_showthumb();
void imgl_insert_file(cchar *imagefile);
void imgl_gallery_file(int Nth);

GtkWidget      *imgl_drawarea = 0;
GtkWidget      *imgl_files = 0;
cchar          *imgl_font = "Monospace 8";
zdialog        *imgl_zd = 0;
int            imgl_fontheight = 14;
int            imgl_cursorpos = 0;


char ** image_gallery_getfiles(char *startdir, GtkWidget *parent)          //  new v.3.7
{
   int  imgl_dialog_event(zdialog *zd, cchar *event);
   int  imgl_mouseclick(GtkWidget *, GdkEventButton *event, void *);

   PangoLanguage           *plang;
   PangoFontDescription    *pfontdesc;
   PangoContext            *pcontext;
   PangoFont               *pfont;
   PangoFontMetrics        *pmetrics;
   GdkCursor               *cursor;
   GdkWindow               *gdkwin;
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   int               fontascent, fontdescent;
   int               line, nlines, ii;
   char              *imagefile, **filelist;
   
   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   image_gallery(startdir,"paint1",0,imgl_gallery_file,parent);            //  activate image gallery window

   imgl_zd = zdialog_new(ZTX("Select Files"),0,ZTX("done"),ZTX("cancel"),null);
   zdialog_add_widget(imgl_zd,"hbox","hb1","dialog",0,"expand|space=5");
   zdialog_add_widget(imgl_zd,"frame","fr11","hb1",0,"expand");
   zdialog_add_widget(imgl_zd,"scrwin","scrwin","fr11",0,"expand");
   zdialog_add_widget(imgl_zd,"edit","files","scrwin");
   zdialog_add_widget(imgl_zd,"vbox","vb12","hb1");
   zdialog_add_widget(imgl_zd,"frame","fr12","vb12");
   zdialog_add_widget(imgl_zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(imgl_zd,"button","delete","hb2",ZTX("delete"),"space=8");
   zdialog_add_widget(imgl_zd,"button","insert","hb2",ZTX("insert"),"space=8");
   zdialog_add_widget(imgl_zd,"button","addall","hb2",ZTX("add all"),"space=30");

   GtkWidget *textbox = zdialog_widget(imgl_zd,"files");                   //  disable text wrap      v.3.8
   gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textbox),GTK_WRAP_NONE);

   GtkWidget *frame = zdialog_widget(imgl_zd,"fr12");                      //  drawing area for thumbnail image
   imgl_drawarea = gtk_drawing_area_new();
   gtk_widget_set_size_request(imgl_drawarea,256,258);                     //  increased v.3.9
   gtk_container_add(GTK_CONTAINER(frame),imgl_drawarea);
   
   imgl_files = zdialog_widget(imgl_zd,"files");                           //  activate mouse-clicks for
   gtk_widget_add_events(imgl_files,GDK_BUTTON_PRESS_MASK);                //    file list widget
   G_SIGNAL(imgl_files,"button-press-event",imgl_mouseclick,0)

   pfontdesc = pango_font_description_from_string(imgl_font);              //  set default font for files window
   gtk_widget_modify_font(imgl_files,pfontdesc);

   plang = pango_language_get_default();                                   //  get font metrics (what a mess)
   pcontext = gtk_widget_get_pango_context(imgl_files);
   pfont = pango_context_load_font(pcontext,pfontdesc);
   pmetrics = pango_font_get_metrics(pfont,plang);
   fontascent = pango_font_metrics_get_ascent(pmetrics) / PANGO_SCALE;
   fontdescent = pango_font_metrics_get_descent(pmetrics) / PANGO_SCALE;
   imgl_fontheight = fontascent + fontdescent;                             //  effective line height

   zdialog_resize(imgl_zd,600,0);                                          //  start dialog
   zdialog_run(imgl_zd,imgl_dialog_event);

   cursor = gdk_cursor_new(GDK_TOP_LEFT_ARROW);                            //  arrow cursor for file list widget
   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(imgl_files),TEXTWIN);   //  (must be done after window realized)
   gdk_window_set_cursor(gdkwin,cursor);
   imgl_cursorpos = 0;

   zdialog_wait(imgl_zd);                                                  //  wait for completion

   if (imgl_zd->zstat != 1) {                                              //  cancelled
      zdialog_free(imgl_zd);                                               //  kill dialog
      image_gallery(0,"close");                                            //  close image gallery window
      return 0;
   }

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   nlines = gtk_text_buffer_get_line_count(textBuff);
   
   filelist = (char **) zmalloc((nlines+1) * sizeof(char *),"imgl.getfiles");

   for (ii = line = 0; line < nlines; line++)                              //  get list of files from dialog
   {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);              //  iter at line start
      iter2 = iter1;
      gtk_text_iter_forward_to_line_end(&iter2);
      imagefile = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);      //  get line of text
      if (imagefile && *imagefile == '/') {
         filelist[ii] = strdupz(imagefile,0,"imgl.getfiles");              //  >> next file in list
         free(imagefile);
         ii++;
      }
   }
   filelist[ii] = 0;                                                       //  mark EOL
   
   zdialog_free(imgl_zd);                                                  //  kill dialog
   image_gallery(0,"close");                                               //  close image gallery window

   if (! ii) {
      zfree(filelist);                                                     //  file list is empty
      return 0;
   }

   return filelist;                                                        //  return file list
}


//  imgl dialog event function

int imgl_dialog_event(zdialog *zd, cchar *event)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   char           *ftemp;
   static char    *imagefile = 0;
   int            line, posn;
   
   if (strEqu(event,"delete"))                                             //  delete file at cursor position
   {
      if (imagefile) zfree(imagefile);
      imagefile = 0;

      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
      line = imgl_cursorpos;
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);              //  iter at line start
      iter2 = iter1;
      gtk_text_iter_forward_to_line_end(&iter2);                           //  iter at line end

      ftemp = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);          //  get selected file
      if (! ftemp || *ftemp != '/') {
         if (ftemp) free(ftemp);
         return 0;
      }

      imagefile = strdupz(ftemp,0,"imgl.getfiles");                        //  save deleted file for poss. insert
      free(ftemp);
      
      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                      //  delete file text
      gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line+1);
      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                      //  delete empty line (\n)

      imgl_showthumb();                                                    //  thumbnail = next file
   }

   if (strEqu(event,"insert"))                                             //  insert last deleted file
   {                                                                       //    at current cursor position
      if (! imagefile) return 0;
      imgl_insert_file(imagefile);
      zfree(imagefile);
      imagefile = 0;
   }

   if (strEqu(event,"addall"))                                             //  insert all files in image gallery
   {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
      posn = 0;

      while (true)
      {
         imagefile = image_gallery(0,"find",posn);                         //  get first or next file
         if (! imagefile) break;
         posn++;
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,imgl_cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,"\n",1);                   //  insert new blank line
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,imgl_cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,imagefile,-1);             //  insert image file
         zfree(imagefile);
         imgl_cursorpos++;                                                 //  advance cursor position
      }
   }

   return 0;
}


//  add image file to list at current cursor position, set thumbnail = file

void imgl_insert_file(cchar *imagefile)
{
   GtkTextIter    iter;
   GtkTextBuffer  *textBuff;
   
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,imgl_cursorpos);
   gtk_text_buffer_insert(textBuff,&iter,"\n",1);                          //  insert new blank line
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,imgl_cursorpos);
   gtk_text_buffer_insert(textBuff,&iter,imagefile,-1);                    //  insert image file

   imgl_showthumb();                                                       //  update thumbnail
   imgl_cursorpos++;                                                       //  advance cursor position

   return;
}


//  called from image gallery window when a thumbnail is clicked
//  add image file to list at current cursor position, set thumbnail = file

void imgl_gallery_file(int Nth)                                            //  bugfix  v.4.4
{
   int            ftyp;
   char           *imagefile;
   
   if (Nth == -2) {
      showz_userguide(zfuncs::F1_help_topic);                              //  F1 context help
      return;
   }

   imagefile = image_gallery(0,"find",Nth);                                //  get file at clicked position
   if (! imagefile) return;

   ftyp = image_file_type(imagefile);                                      //  ignore directories     v.3.9
   if (ftyp == 2) imgl_insert_file(imagefile);                             //  insert file at current position
   zfree(imagefile);

   return;
}


//  process mouse click in files window: 
//  set new cursor position and set thumbnail = clicked file

int imgl_mouseclick(GtkWidget *, GdkEventButton *event, void *)
{
   int            mpy;
   GtkWidget      *scrollwin;
   GtkAdjustment  *scrolladj;
   double         scrollpos;

   if (event->type != GDK_BUTTON_PRESS) return 0;
   mpy = int(event->y);
   scrollwin = zdialog_widget(imgl_zd,"scrwin");                           //  window scroll position
   scrolladj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scrollwin));
   scrollpos = gtk_adjustment_get_value(scrolladj);
   imgl_cursorpos = (mpy + scrollpos) / imgl_fontheight;                   //  line selected
   imgl_showthumb();                                                       //  show thumbnail image
   return 0;
}


//  show thumbnail for file at current cursor position

int imgl_showthumb()
{
   int            line;
   char           *imagefile;
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   GdkPixbuf      *thumbnail = 0;

   gdk_window_clear(imgl_drawarea->window);

   line = imgl_cursorpos;
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                 //  iter at line start
   iter2 = iter1;
   gtk_text_iter_forward_to_line_end(&iter2);                              //  iter at line end

   imagefile = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);         //  get selected file
   if (*imagefile != '/') {
      free(imagefile);
      return 0;
   }

   thumbnail = image_thumbnail(imagefile,256);                             //  get thumbnail    increased v.3.9
   free(imagefile);

   if (thumbnail) {
      gdk_draw_pixbuf(imgl_drawarea->window,0,thumbnail,0,0,0,0,-1,-1,NODITHER,0,0);
      g_object_unref(thumbnail);
   }
   return 0;
}


/**************************************************************************
    parameter management functions
***************************************************************************/

08586 struct t_parmlist {                                                        //  parameter list in memory
   int      max;                                                           //  max parameter count
   int      count;                                                         //  actual parameter count
   char     **name;                                                        //  pointer to names (list of char *)
   double   *value;                                                        //  pointer to values (list of double)
} parmlist;

int      parmlistvalid = 0;                                                //  flag
char     zparmfile[maxfcc];                                                //  last used parm file


//  initialize parameter list - must be called first

int initParmlist(int max)
{
   if (! parmlistvalid) {                                                  //  start with default parms file
      strcpy(zparmfile,get_zuserdir());
      strcat(zparmfile,"/parameters");                                     //  /home/user/.appname/parameters
   }
   
   if (parmlistvalid) {                                                    //  delete old parms
      delete [] parmlist.name;
      delete [] parmlist.value;
   }

   parmlist.max = max;
   parmlist.count = 0;
   char **names = new char*[max];                                          //  allocate max pointers for names
   double *values = new double[max];                                       //  allocate max doubles for values
   parmlist.name = names;
   parmlist.value = values;
   parmlistvalid = 1;
   return 0;
}


//  Load user parameters if the file exists, else initialize the
//  user parameters file from default application parameters.

int initz_userParms()
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   int np = loadParms("parameters");
   if (! np) {
      saveParms("parameters");
      zmessageACK(null,ZTX("Initial parameters file created. \n"
                           "Inspect and revise if necessary."));
   }
   return np;
}


//  load parameters from a file, with file selection dialog

int loadParms()
{
   char     *pfile;
   int      np;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   pfile = zgetfile1(ZTX("load parameters from a file"),"open",zparmfile,"hidden");
   if (! pfile) return 0;
   
   np = loadParms(pfile);
   zfree(pfile);

   return np;
}
   

//  load parameters from a file
//  returns no. parameters loaded

int loadParms(cchar *pfile)
{
   FILE        *fid;
   int         Nth, np1, np2 = 0, err;
   char        buff[100], *fgs, *pp;
   cchar       *pname, *pvalue;
   double      dvalue;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   if (! pfile) pfile = zparmfile;
   
   if (*pfile != '/') {                                                    //  if parm file name only,
      pp = (char *) strrchr(zparmfile,'/');                                //    make complete absolute path
      if (pp) strcpy(pp+1,pfile);                                          //      in same directory as prior
      pfile = zparmfile;
   }

   fid = fopen(pfile,"r");
   if (! fid) return 0;                                                    //  bad file
   strncpy0(zparmfile,pfile,maxfcc-1);                                     //  set current parm file

   while (true)                                                            //  read file
   {
      fgs = fgets_trim(buff,99,fid,1);
      if (! fgs) break;                                                    //  EOF

      pp = strchr(buff,'#');                                               //  eliminate comments
      if (pp) *pp = 0;

      Nth = 1;                                                             //  parse parm name, value
      pname = strField(buff,' ',Nth++);
      if (! pname) continue;
      pvalue = strField(buff,' ',Nth);
      if (! pvalue) continue;
      err = convSD(pvalue,dvalue);
      if (err) continue;
      np1 = setParm(pname,dvalue);                                         //  set the parameter
      if (! np1) continue;
      np2++;
   }
   
   fclose(fid);                                                            //  close file
   return np2;                                                             //  return parameter count
}


//  save parameters to a file, with file selection dialog

int saveParms()
{
   char     *pfile;
   int      np;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   pfile = zgetfile1(ZTX("save parameters to a file"),"save",zparmfile,"hidden");
   if (! pfile) return 0;

   np = saveParms(pfile);
   zfree(pfile);

   return np;
}


//  save parameters to a file

int saveParms(cchar *pfile)
{
   FILE     *fid;
   int      np;
   char     *pp;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   if (*pfile != '/') {                                                    //  if parm file name only,
      pp = (char *) strrchr(zparmfile,'/');                                //    make complete absolute path
      if (pp) strcpy(pp+1,pfile);                                          //      in same directory as prior
      pfile = zparmfile;
   }

   fid = fopen(pfile,"w");
   if (! fid) { 
      zmessageACK(null,ZTX("cannot open file %s"),pfile);
      return 0;
   }

   strncpy0(zparmfile,pfile,999);

   for (np = 0; np < parmlist.count; np++)
      fprintf(fid," \"%s\"  %.12g \n",parmlist.name[np],parmlist.value[np]);
   
   fclose(fid);
   return np;
}


//  create a new paramater or change value of existing parameter

int setParm(cchar *parmname, double parmval)
{
   int      ii, cc;
   char     *ppname;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   for (ii = 0; ii < parmlist.count; ii++)
      if (strEqu(parmlist.name[ii],parmname)) break;

   if (ii == parmlist.max) return 0;

   if (ii == parmlist.count) {
      parmlist.count++;
      cc = strlen(parmname);
      ppname = new char[cc+1];
      strTrim(ppname,parmname);
      parmlist.name[ii] = ppname;
   }

   parmlist.value[ii] = parmval;
   return parmlist.count;
}


//  get parameter value from parameter name

double getParm(cchar *parmname)
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   for (int ii = 0; ii < parmlist.count; ii++)
   {
      if (strNeq(parmlist.name[ii],parmname)) continue;
      return parmlist.value[ii];
   }

   return NAN;
}


//  get Nth parameter name (zero-based)

char * getParm(int Nth)
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");
   if (Nth >= parmlist.count) return null;
   return parmlist.name[Nth];
}


//  list parameters in supplied text entry window

int listParms(GtkWidget *textWin)
{
   int            ii;
   cchar          *pname;
   double         pvalue;

   for (ii = 0; ii < parmlist.count; ii++)
   {
      pname = getParm(ii);
      pvalue = getParm(pname);
      wprintf(textWin," %s  %.12g \n",pname,pvalue);
   }
   
   return parmlist.count;
}


//  edit parameters with a GUI
//  textWin != null enables button to list parameters in window
//  addp != 0 enables button to add new parameters
//  return: 0 if cancel, else parameter count                              //  v.2.7
//  no locking, use in main() thread only

int editParms(GtkWidget *textWin, int addp)
{
   GtkWidget      *peDialog, *peLabel[100], *peEdit[100], *peHbox[100];
   char           ptemp[20], *pname;
   cchar          *pchval;
   double         pvalue;
   int            ii, err, iie = -1, zstat, floaded = 0;
   int            bcancel=1, bapply=2, bload=3, bsave=4, blist=5, baddp=6;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");
   
   zthreadcrash();                                                         //  thread usage not allowed     v.3.9

   build_dialog:                                                           //  build parameter edit dialog

   if (parmlist.count > 100) zappcrash("more than 100 parameters");
   
   if (textWin && addp) 
       peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, (GtkDialogFlags) 0,                //  non-modal     bugfix v.4.3 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, 
          ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else if (textWin) 
      peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, (GtkDialogFlags) 0, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, 
          ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else if (addp) 
      peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, (GtkDialogFlags) 0, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave,  
          ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else peDialog = 
      gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, (GtkDialogFlags) 0, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave,  
          ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   gtk_window_set_position(GTK_WINDOW(peDialog),GTK_WIN_POS_MOUSE);        //  v.2.7.1

   for (ii = 0; ii < parmlist.count; ii++)                                 //  labels and edit boxes side by side
   {                                                                       //  (parm names and parm values)
      peLabel[ii] = gtk_label_new(parmlist.name[ii]);
      gtk_misc_set_alignment(GTK_MISC(peLabel[ii]),1,0.5);
      gtk_label_set_width_chars(GTK_LABEL(peLabel[ii]),30);
      peEdit[ii] = gtk_entry_new();
      gtk_entry_set_width_chars(GTK_ENTRY(peEdit[ii]),12);
      sprintf(ptemp,"%.12g",parmlist.value[ii]);
      gtk_entry_set_text(GTK_ENTRY(peEdit[ii]),ptemp);
      peHbox[ii] = gtk_hbox_new(0,0);
      gtk_box_pack_start(GTK_BOX(peHbox[ii]),peLabel[ii],0,0,5);
      gtk_box_pack_start(GTK_BOX(peHbox[ii]),peEdit[ii],0,0,5);
      gtk_box_pack_start(GTK_BOX(GTK_DIALOG(peDialog)->vbox),peHbox[ii],1,1,2);
   }

   run_dialog:                                                             //  display dialog and get inputs
   
   if (iie > -1)
   {
      gtk_editable_select_region(GTK_EDITABLE(peEdit[iie]),0,-1);          //  focus on new or bad parameter
      gtk_widget_grab_focus(peEdit[iie]);
      iie = -1;
   }

   gtk_widget_show_all(peDialog);
   zstat = gtk_dialog_run(GTK_DIALOG(peDialog));

   if (zstat <= bcancel)                                                   //  kill, cancel
   {
      if (floaded) {
         zstat = zmessageYN(null,ZTX("apply?"));                           //  if file loaded, clarify  v.2.9
         if (! zstat) { 
            gtk_widget_destroy(peDialog);
            return 0;
         }
         zstat = bapply;
      }
   }
   
   if (zstat == bload)                                                     //  load from file
   {
      loadParms();
      gtk_widget_destroy(peDialog);
      floaded = 1;
      goto build_dialog;
   }
   
   for (ii = 0; ii < parmlist.count; ii++)                                 //  capture inputs and check if OK
   {
      pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
      err = convSD(pchval,pvalue);
      if (err && iie < 0) iie = ii;                                        //  remember 1st error
   }

   if (iie >= 0) goto run_dialog;                                          //  re-get bad input

   if (zstat == bapply)                                                    //  apply new values
   {
      for (ii = 0; ii < parmlist.count; ii++)                              //  capture inputs and save them  v.2.7
      {
         pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
         err = convSD(pchval,parmlist.value[ii]);
      }
      gtk_widget_destroy(peDialog);                                        //  done
      return parmlist.count;
   }

   if (zstat == bsave)                                                     //  save to file
   {
      for (ii = 0; ii < parmlist.count; ii++)                              //  apply new values  v.2.9
      {
         pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
         err = convSD(pchval,parmlist.value[ii]);
      }
      saveParms();
      floaded = 0;
      goto run_dialog;
   }
   
   if (zstat == blist)                                                     //  list parameters
   {
      listParms(textWin);
      goto run_dialog;
   }
   
   if (zstat == baddp)                                                     //  add parameter          bugfix v.4.3
   {                                                                       //  main dialog must be non-modal
      pname = zdialog_text(null,ZTX("add parameter"),ZTX("(new parm name)"));
      if (! pname) goto run_dialog;
      setParm(pname,0.0);
      zfree(pname);                                                        //  v.3.7
      floaded = 1;
      iie = parmlist.count - 1;                                            //  focus on new parm
      gtk_widget_destroy(peDialog);
      goto build_dialog;
   }
   
   gtk_widget_destroy(peDialog);                                           //  unknown status
   return 0;
}


/**************************************************************************
      xstring class (dynamic length string)
***************************************************************************/

#define  wmiv  1648734981

int   xstring::tcount = 0;                                                 //  initz. static members
int   xstring::tmem = 0;


xstring::xstring(int cc)                                                   //  new xstring(cc)
{
   wmi = wmiv;
   xmem = (cc & 0x7ffffff8) + 8;                                           //  mod 8 length
   xpp = new char[xmem];                                                   //  allocate
   if (! xpp) zappcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   xcc = 0;                                                                //  string cc = 0
   *xpp = 0;                                                               //  string = null
}


xstring::xstring(cchar *string)                                            //  new xstring("initial string")
{
   wmi = wmiv;
   xcc = 0;
   if (string) xcc = strlen(string);                                       //  string length
   xmem = (xcc & 0x7ffffff8) + 8;                                          //  mod 8 length
   xpp = new char[xmem];                                                   //  allocate
   if (! xpp) zappcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   *xpp = 0;
   if (xcc) strcpy(xpp,string);                                            //  copy string
}


xstring::xstring(const xstring & xstr)                                     //  new xstring2(xstring1)
{
   wmi = wmiv;
   xmem = xstr.xmem;                                                       //  allocate same length
   xcc = xstr.xcc;
   xpp = new char[xmem];
   if (! xpp) zappcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   strcpy(xpp,xstr.xpp);                                                   //  copy string
}


xstring::~xstring()                                                        //  delete xstring
{  
   validate();
   delete[] xpp;                                                           //  release allocated memory
   xpp = 0;
   tcount--;                                                               //  decr. object count
   tmem -= xmem;                                                           //  decr. allocated memory
   if (tcount < 0) zappcrash("xstring count < 0",null);
   if (tmem < 0) zappcrash("xstring memory < 0",null);
   if (tcount == 0 && tmem > 0) zappcrash("xstring memory leak",null);
}


xstring xstring::operator= (const xstring & xstr)                          //  xstring2 = xstring1
{
   validate();
   xstr.validate();
   if (this == &xstr) return *this;
   xcc = xstr.xcc;
   if (xmem < xcc+1)
   {
      delete[] xpp;                                                        //  expand memory if needed
      tmem -= xmem;
      xmem = (xcc & 0x7ffffff8) + 8;                                       //  mod 8 length
      xpp = new char[xmem];
      if (! xpp) zappcrash("xstring NEW failure",null);
      tmem += xmem;
   }
   strcpy(xpp,xstr.xpp);                                                   //  copy string
   return *this;
}


xstring xstring::operator= (cchar *str)                                    //  xstring = "some string"
{
   validate();
   xcc = 0;
   *xpp = 0;
   if (str) xcc = strlen(str);
   if (xmem < xcc+1)
   {
      delete[] xpp;                                                        //  expand memory if needed
      tmem -= xmem;
      xmem = (xcc & 0x7ffffff8) + 8;                                       //  mod 8 length
      xpp = new char[xmem];
      if (! xpp) zappcrash("xstring NEW failure",null);
      tmem += xmem;
   }
   if (xcc) strcpy(xpp,str);                                               //  copy string
   return *this;
}


xstring operator+ (const xstring & x1, const xstring & x2)                 //  xstring1 + xstring2
{
   x1.validate();
   x2.validate();
   xstring temp(x1.xcc + x2.xcc);                                          //  build temp xstring
   strcpy(temp.xpp,x1.xpp);                                                //    with both input strings
   strcpy(temp.xpp + x1.xcc, x2.xpp);
   temp.xcc = x1.xcc + x2.xcc;
   temp.validate();
   return temp;
}


xstring operator+ (const xstring & x1, cchar *s2)                          //  xstring + "some string"
{
   x1.validate();
   int cc2 = 0;
   if (s2) cc2 = strlen(s2);
   xstring temp(x1.xcc + cc2);                                             //  build temp xstring
   strcpy(temp.xpp,x1.xpp);                                                //    with both input strings
   if (s2) strcpy(temp.xpp + x1.xcc, s2);
   temp.xcc = x1.xcc + cc2;
   temp.validate();
   return temp;
}


xstring operator+ (cchar *s1, const xstring & x2)                          //  "some string" + xstring
{
   x2.validate();
   int cc1 = 0;
   if (s1) cc1 = strlen(s1);
   xstring temp(cc1 + x2.xcc);                                             //  build temp xstring
   if (s1) strcpy(temp.xpp,s1);                                            //    with both input strings
   strcpy(temp.xpp + cc1, x2.xpp);
   temp.xcc = cc1 + x2.xcc;
   temp.validate();
   return temp;
}


void xstring::insert(int pos, cchar *string, int cc)                       //  insert cc chars from string at pos
{                                                                          //  pad if pos > xcc or cc > string
   validate();

   int scc = strlen(string);
   if (! cc) cc = scc;

   int pad = pos - xcc;
   if (pad < 0) pad = 0;                                

   if (xmem < xcc + cc + pad + 1)                                          //  allocate more memory if needed
   {
      int newmem = xcc + cc + pad;
      newmem = (newmem & 0x7ffffff8) + 8;                                  //  mod 8 length
      char * xpp2 = new char[newmem];
      if (! xpp2) zappcrash("xstring NEW failure",null);
      strcpy(xpp2,xpp);                                                    //  copy to new space
      delete[] xpp;
      xpp = xpp2;
      tmem += newmem - xmem;
      xmem = newmem;
   }

   if (pad) memset(xpp+xcc,' ',pad);                                       //  add blanks up to pos

   for (int ii = xcc + pad; ii >= pos; ii--)                               //  make hole for inserted string
           *(xpp+ii+cc) = *(xpp+ii);

   if (cc > scc) memset(xpp+pos+scc,' ',cc-scc);                           //  blank pad if cc > string
   if (cc < scc) scc = cc;
   strncpy(xpp+pos,string,scc);                                            //  insert string, without null

   xcc += cc + pad;                                                        //  set new length
   xpp[xcc] = 0;
   validate();
}


void xstring::overlay(int pos, cchar *string, int cc)                      //  overlay substring
{
   validate();

   int scc = strlen(string);
   if (! cc) cc = scc;

   if (xmem < pos + cc + 1)                                                //  allocate more memory if needed
   {
      int newmem = pos + cc;
      newmem = (newmem & 0x7ffffff8) + 8;                                  //  mod 8 length
      char * xpp2 = new char[newmem];
      if (! xpp2) zappcrash("xstring NEW failure",null);
      strcpy(xpp2,xpp);                                                    //  copy to new space
      delete[] xpp;
      xpp = xpp2;
      tmem += newmem - xmem;
      xmem = newmem;
   }

   if (pos > xcc) memset(xpp+xcc,' ',pos-xcc);                             //  add blanks up to pos
   
   if (cc > scc) memset(xpp+pos+scc,' ',cc-scc);                           //  blank pad if cc > string
   if (cc < scc) scc = cc;
   strncpy(xpp+pos,string,scc);                                            //  insert string, without null

   if (pos + cc > xcc) xcc = pos + cc;                                     //  set new length 
   xpp[xcc] = 0;
   validate();
}


void xstring::getStats(int & tcount2, int & tmem2)                         //  get statistics
{
   tcount2 = tcount;
   tmem2 = tmem;
}


void xstring::validate() const                                             //  validate integrity
{
   if (wmi != wmiv) zappcrash("xstring bad wmi",null);
   if (xmem < xcc+1) zappcrash("xstring xmem < xcc+1",null);
   if (xcc != (int) strlen(xpp)) zappcrash("xstring xcc != strlen(xpp)",null);
}


/**************************************************************************
      Vxstring class (array or vector of xstring)
***************************************************************************/

Vxstring::Vxstring(int ii)                                                 //  constructor
{  
   pdata = 0;
   nd = ii;
   if (nd) pdata = new xstring[nd];
   if (nd && !pdata) zappcrash("Vxstring NEW fail",null);
}


Vxstring::~Vxstring()                                                      //  destructor
{
   if (nd) delete[] pdata;
   pdata = 0;
   nd = 0;
}


Vxstring::Vxstring(const Vxstring & pold)                                  //  copy constructor
{
   pdata = 0;
   nd = pold.nd;                                                           //  set size
   if (nd) pdata = new xstring[nd];                                        //  allocate memory
   if (nd && !pdata) zappcrash("Vxstring NEW fail");
   for (int ii = 0; ii < nd; ii++) pdata[ii] = pold[ii];                   //  copy defined elements
}


Vxstring Vxstring::operator= (const Vxstring & vdstr)                      //  operator =
{
   if (nd) delete[] pdata;                                                 //  delete old memory
   pdata = 0;
   nd = vdstr.nd;
   if (nd) pdata = new xstring[nd];                                        //  allocate new memory
   if (nd && !pdata) zappcrash("Vxstring NEW fail",null);
   for (int ii = 0; ii < nd; ii++) pdata[ii] = vdstr.pdata[ii];            //  copy elements
   return *this;
}


xstring & Vxstring::operator[] (int ii)                                    //  operator []
{
   static xstring xnull(0);
   if (ii < nd) return pdata[ii];                                          //  return reference
   zappcrash("Vxstring index invalid %d %d",nd,ii,null);
   return xnull;
}


const xstring & Vxstring::operator[] (int ii) const                        //  operator []
{
   static xstring xnull(0);
   if (ii < nd) return pdata[ii];                                          //  return reference
   zappcrash("Vxstring index invalid %d %d",nd,ii,null);
   return xnull;
}


int Vxstring::search(cchar *string)                                        //  find element in unsorted Vxstring
{
   for (int ii = 0; ii < nd; ii++)
        if (strEqu(pdata[ii],string)) return ii;
   return -1;
}


int Vxstring::bsearch(cchar *string)                                       //  find element in sorted Vxstring
{                                                                          //   (binary search)
   int   nn, ii, jj, kk, rkk;

   nn = nd;
   if (! nn) return 0;                                                     //  empty list

   ii = nn / 2;                                                            //  next element to search
   jj = (ii + 1) / 2;                                                      //  next increment
   nn--;                                                                   //  last element
   rkk = 0;

   while (1)
   {
      kk = strcmp(pdata[ii],string);                                       //  check element

      if (kk > 0) 
      {
         ii -= jj;                                                         //  too high, go down
         if (ii < 0) return -1;
      }

      else if (kk < 0) 
      {
         ii += jj;                                                         //  too low, go up
         if (ii > nn) return -1;
      }

      else if (kk == 0) return ii;                                         //  matched

      jj = jj / 2;                                                         //  reduce increment

      if (jj == 0) 
      {
         jj = 1;                                                           //  step by 1 element
         if (! rkk) rkk = kk;                                              //  save direction
         else 
         {
            if (rkk > 0) { if (kk < 0) return -1; }                        //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


static int  VDsortKeys[10][3], VDsortNK;

int Vxstring::sort(int NK, int keys[][3])                                  //  sort elements by subfields
{                                                                          //  key[ii][0] = position
   int     NR, RL, ii;                                                     //         [1] = length
   HeapSortUcomp  VDsortComp;                                              //         [2] = 1/2 = ascending/desc.
                                                                           //             = 3/4 =  + ignore case
   NR = nd;
   if (NR < 2) return 1;

   RL = sizeof(xstring);

   if (NK < 1) zappcrash("Vxstring::sort, bad NK",null);
   if (NK > 10) zappcrash("Vxstring::sort, bad NK",null);
   VDsortNK = NK;

   for (ii = 0; ii < NK; ii++)
   {
      VDsortKeys[ii][0] = keys[ii][0];
      VDsortKeys[ii][1] = keys[ii][1];
      VDsortKeys[ii][2] = keys[ii][2];
   }

   HeapSort((char *) pdata,RL,NR,VDsortComp);

   return 1;
}


int VDsortComp(cchar *r1, cchar *r2)
{
   xstring     *d1, *d2;
   cchar       *p1, *p2;
   int         ii, stat, kpos, ktype, kleng;

   d1 = (xstring *) r1;
   d2 = (xstring *) r2;
   p1 = *d1;
   p2 = *d2;

   for (ii = 0; ii < VDsortNK; ii++)                                       //  compare each key
   {
      kpos = VDsortKeys[ii][0];
      kleng = VDsortKeys[ii][1];
      ktype = VDsortKeys[ii][2];

      if (ktype == 1)
      {
         stat = strncmp(p1+kpos,p2+kpos,kleng);
         if (stat) return stat;
         continue;
      }

      else if (ktype == 2)
      {
         stat = strncmp(p1+kpos,p2+kpos,kleng);
         if (stat) return -stat;
         continue;
      }

      else if (ktype == 3)
      {
         stat = strncasecmp(p1+kpos,p2+kpos,kleng);
         if (stat) return stat;
         continue;
      }

      else if (ktype == 4)
      {
         stat = strncasecmp(p1+kpos,p2+kpos,kleng);
         if (stat) return -stat;
         continue;
      }

      zappcrash("Vxstring::sort, bad KEYS sort type",null);
   }

   return 0;
}


int Vxstring::sort(int pos, int cc)                                        //  sort elements ascending
{
   int   key[3];

   if (! cc) cc = 999999;
   key[0] = pos;
   key[1] = cc;
   key[2] = 1;

   sort(1,&key);

   return 1;
}


/**************************************************************************
     Hash Table class
***************************************************************************/

//  static members (robust for tables up to 60% full)

int HashTab::trys1 = 100;                                                  //  Add() tries
int HashTab::trys2 = 200;                                                  //  Find() tries


HashTab::HashTab(int _cc, int _cap)                                        //  constructor
{
   cc = 4 * (_cc + 4) / 4;                                                 //  + 1 + mod 4 length
   cap = _cap;
   int len = cc * cap;
   table = new char [len];
   if (! table) zappcrash("HashTab() new %d fail",len,null);
   memset(table,0,len);
}


HashTab::~HashTab()                                                        //  destructor
{
   delete [] table;
   table = 0;
}


//  Add a new string to table

int HashTab::Add(cchar *string)
{
   int   pos, fpos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0, fpos = -1; trys < trys1; trys++, pos += cc)              //  find next free slot at/after position
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st

      if (! table[pos])                                                    //  empty slot: string not found
      {
         if (fpos != -1) pos = fpos;                                       //  use prior deleted slot if there
         strncpy(table+pos,string,cc);                                     //  insert new string
         table[pos+cc-1] = 0;                                              //  insure null terminator
         return (pos/cc);                                                  //  return rel. table entry
      }

      if (table[pos] == -1)                                                //  deleted slot
      {
         if (fpos == -1) fpos = pos;                                       //  remember 1st one found
         continue;
      }

      if (strEqu(string,table+pos)) return -2;                             //  string already present
   }   

   return -3;                                                              //  table full (trys1 exceeded)
}


//  Delete a string from table

int HashTab::Del(cchar *string)
{
   int   pos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0; trys < trys2; trys++, pos += cc)                         //  search for string at/after position
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st

      if (! table[pos]) return -1;                                         //  empty slot, string not found

      if (strEqu(string,table+pos))                                        //  string found
      {
         table[pos] = -1;                                                  //  delete table entry
         return (pos/cc);                                                  //  return rel. table entry
      }
   }   

   zappcrash("HashTab::Del() bug",null);                                   //  exceed trys2, must not happen
   return 0;                                                               //  (table too full to function)
}


//  Find a table entry.

int HashTab::Find(cchar *string)
{
   int   pos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0; trys < trys2; trys++, pos += cc)                         //  search for string at/after position
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st
      if (! table[pos]) return -1;                                         //  empty slot, string not found
      if (strEqu(string,table+pos)) return (pos/cc);                       //  string found, return rel. entry
   }   

   zappcrash("HashTab::Find() bug",null);                                  //  cannot happen
   return 0;
}


//  return first or next table entry

int HashTab::GetNext(int & ftf, char *string)
{
   static int    pos;

   if (ftf)                                                                //  initial call
   {
      pos = 0;
      ftf = 0;
   }

   while (pos < (cap * cc))
   {
      if ((table[pos] == 0) || (table[pos] == -1))                         //  empty or deleted slot
      {
         pos += cc;
         continue;
      }

      strcpy(string,table+pos);                                            //  return string
      pos += cc;
      return 1;
   }

   return -4;                                                              //  EOF
}


int HashTab::Dump()
{
   int   ii, pos;

   for (ii = 0; ii < cap; ii++)
   {
      pos = ii * cc;

      if (table[pos] && table[pos] != -1) 
         printf("%d, %s \n", ii, table + pos);

      if (table[pos] == -1) 
         printf("%d, deleted \n", pos);
   }        

   return 1;
}


/**************************************************************************
     class for queue of dynamic strings
***************************************************************************/

Queue::Queue(int cap)                                                      //  constructor
{
   int   err;
   
   err = mutex_init(&qmutex, 0);                                           //  create mutex = queue lock
   if (err) zappcrash("Queue(), mutex init fail",null);

   qcap = cap;                                                             //  queue capacity
   ent1 = entN = qcount = 0;                                               //  state = empty
   vd = new Vxstring(qcap);                                                //  create vector of xstring's
   if (! vd) zappcrash("Queue(), NEW fail %d",cap,null);
   strcpy(wmi,"queue");
   return;
}


Queue::~Queue()                                                            //  destructor
{
   if (strNeq(wmi,"queue")) zappcrash("~Queue wmi fail",null);
   wmi[0] = 0;
   mutex_destroy(&qmutex);                                                 //  destroy mutex
   qcount = qcap = ent1 = entN = -1;
   delete vd;
   vd = 0;
   return;
}


void Queue::lock()                                                         //  lock queue (private)
{
   int   err;
   err = mutex_lock(&qmutex);                                              //  reserve mutex or suspend
   if (err) zappcrash("Queue mutex lock fail",null);
   return;
}


void Queue::unlock()                                                       //  unlock queue (private)
{
   int   err;
   err = mutex_unlock(&qmutex);                                            //  release mutex
   if (err) zappcrash("Queue mutex unlock fail",null);
   return;
}


int Queue::getCount()                                                      //  get current entry count
{
   if (strNeq(wmi,"queue")) zappcrash("Queue getCount wmi fail",null);
   return qcount;
}


int Queue::push(const xstring *newEnt, double wait)                        //  add entry to queue, with max. wait
{
   double  elaps = 0.0;
   int     count;
   
   if (strNeq(wmi,"queue")) zappcrash("Queue::push wmi fail",null);

   lock();                                                                 //  lock queue
   while (qcount == qcap) {                                                //  queue full
      unlock();                                                            //  unlock queue
      if (elaps >= wait) return -1;                                        //  too long, return -1 status
      usleep(1000);                                                        //  sleep in 1 millisec. steps
      elaps += 0.001;                                                      //  until queue not full
      lock();                                                              //  lock queue
   }

   (* vd)[entN] = *newEnt;                                                 //  copy new entry into queue
   entN++;                                                                 //  incr. end pointer
   if (entN == qcap) entN = 0;
   qcount++;                                                               //  incr. queue count
   count = qcount;
   unlock();                                                               //  unlock queue
   return count;                                                           //  return curr. queue count
}


xstring *Queue::pop1()                                                     //  get 1st (oldest) entry and remove
{
   xstring    *entry;
   
   if (strNeq(wmi,"queue")) zappcrash("Queue::pop1 wmi fail",null);

   lock();                                                                 //  lock queue

   if (qcount == 0) entry = 0;                                             //  queue empty
   else {
      entry = &(* vd)[ent1];                                               //  get first entry
      ent1++;                                                              //  index pointer to next
      if (ent1 == qcap) ent1 = 0;
      qcount--;                                                            //  decr. queue count
   }

   unlock();                                                               //  unlock queue
   return entry;
}


xstring *Queue::popN()                                                     //  get last (newest) entry and remove
{
   xstring   *entry;
   
   if (strNeq(wmi,"queue")) zappcrash("Queue::popN wmi fail",null);

   lock();                                                                 //  lock queue

   if (qcount == 0) entry = 0;                                             //  queue empty
   else {
      if (entN == 0) entN = qcap;                                          //  index pointer to prior
      entN--;
      qcount--;                                                            //  decr. queue count
      entry = &(* vd)[entN];                                               //  get last entry
   }

   unlock();                                                               //  unlock queue
   return entry;
}


/**************************************************************************

   Tree class, tree-structured data storage without limits

   Store any amount of data at any depth within a tree-structure with named nodes.
   Data can be found using an ordered list of node names or node numbers.

   Node numbers are in the sequence added using put() with names,
   or the same as those numbers used in put() with numbers.

   Internal code conventions: 
      - caller level is node 0, next level is node 1, etc.
      - node names and numbers in calls to get() and put() refer to next levels
      - number of levels = 1+nn, where nn is max. in calls to put(...nodes[], nn)

***************************************************************************/

#define wmid 1374602859                                                    //  integrity check key


//  constructor

Tree::Tree(cchar *name)
{
   wmi = wmid;
   tname = 0;
   tmem = 0;
   tdata = 0;
   nsub = 0;
   psub = 0;

   if (name) 
   {
      int cc = strlen(name);
      tname = new char[cc+1];
      if (! tname) zappcrash("Tree, no memory",null);
      strcpy(tname,name);
   }
}


//  destructor

Tree::~Tree()
{
   if (wmi != wmid) zappcrash("not a Tree",null);
   if (tname) delete [] tname;
   tname = 0;
   if (tmem) free(tdata);
   tmem = 0;
   tdata = 0;
   for (int ii = 0; ii < nsub; ii++) delete psub[ii];
   if (psub) free(psub);
   nsub = 0;
   psub = 0;
}


//  put data by node names[]

int Tree::put(void *data, int dd, char *nodes[], int nn)
{
   Tree    *tnode;
   
   if (wmi != wmid) zappcrash("not a Tree",null);
   tnode = make(nodes,nn);
   if (tnode->tdata) free(tnode->tdata);
   tnode->tdata = new char[dd];
   if (! tnode->tdata) zappcrash("Tree, no memory",null);
   tnode->tmem = dd;
   memmove(tnode->tdata,data,dd);
   return 1;
}


//  put data by node numbers[]

int Tree::put(void *data, int dd, int nodes[], int nn)
{
   Tree    *tnode;
   
   if (wmi != wmid) zappcrash("not a Tree",null);
   tnode = make(nodes,nn);
   if (tnode->tdata) free(tnode->tdata);
   tnode->tdata = new char[dd];
   if (! tnode->tdata) zappcrash("Tree, no memory",null);
   tnode->tmem = dd;
   memmove(tnode->tdata,data,dd);
   return 1;
}


//  get data by node names[]

int Tree::get(void *data, int dd, char *nodes[], int nn)
{
   Tree *tnode = find(nodes,nn);
   if (! tnode) return 0;
   if (! tnode->tmem) return 0;
   if (dd > tnode->tmem) dd = tnode->tmem;
   memmove(data,tnode->tdata,dd);
   return dd;
}


//  get data by node numbers[]

int Tree::get(void *data, int dd, int nodes[], int nn)
{
   Tree *tnode = find(nodes,nn);
   if (! tnode) return 0;
   if (! tnode->tmem) return 0;
   if (dd > tnode->tmem) dd = tnode->tmem;
   memmove(data,tnode->tdata,dd);
   return dd;
}


//  find a given node by names[]

Tree * Tree::find(char *nodes[], int nn)
{
   int      ii;
   
   for (ii = 0; ii < nsub; ii++)
      if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break;
   if (ii == nsub) return 0;
   if (nn == 1) return psub[ii];
   return psub[ii]->find(&nodes[1],nn-1);
}


//  find a given node by numbers[]

Tree * Tree::find(int nodes[], int nn)
{
   int ii = nodes[0];
   if (ii >= nsub) return 0;
   if (! psub[ii]) return 0;
   if (nn == 1) return psub[ii];
   return psub[ii]->find(&nodes[1],nn-1);
}


//  find or create a given node by names[]

Tree * Tree::make(char *nodes[], int nn)
{
   int      ii;
   Tree   **psub2;
   
   for (ii = 0; ii < nsub; ii++)
      if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break;

   if (ii == nsub)
   {
      psub2 = new Tree * [nsub+1];
      if (! psub2) zappcrash("Tree, no memory",null);
      for (ii = 0; ii < nsub; ii++) psub2[ii] = psub[ii];
      delete [] psub;
      psub = psub2;
      nsub++;
      psub[ii] = new Tree(nodes[0]);
      if (! psub[ii]) zappcrash("Tree, no memory",null);
   }

   if (nn == 1) return psub[ii];
   return psub[ii]->make(&nodes[1],nn-1);
}


//  find or create a given node by numbers[]

Tree * Tree::make(int nodes[], int nn)
{
   Tree   **psub2;
   int      ii, jj;

   ii = nodes[0];
   if ((ii < nsub) && psub[ii])
   {
      if (nn == 1) return psub[ii];
      return psub[ii]->make(&nodes[1],nn-1);
   }

   if (ii >= nsub)
   {
      psub2 = new Tree * [ii+1];
      if (psub2 == null) zappcrash("Tree, no memory",null);
      for (jj = 0; jj < nsub; jj++) psub2[jj] = psub[jj];
      for (jj = nsub; jj < ii; jj++) psub2[jj] = 0;
      delete [] psub;
      psub = psub2;
      nsub = ii + 1;
   }

   psub[ii] = new Tree("noname");
   if (! psub[ii]) zappcrash("Tree, no memory",null);

   if (nn == 1) return psub[ii];
   return psub[ii]->make(&nodes[1],nn-1);
}


//  dump tree data to stdout (call with level 0)

void Tree::dump(int level)
{
   cchar    *name;
   
   if (! tname) name = "noname";
   else name = tname;
   printf("%*s level: %d  name: %s  subs: %d  mem: %d \n",
                        level*2,"",level,name,nsub,tmem);
   for (int ii = 0; ii < nsub; ii++) 
         if (psub[ii]) psub[ii]->dump(level+1);
}


//  get node counts and total data per level
//  level 0 + nn more levels, as given in calls to put(...nodes[],nn)
//  caller must initialize counters to zero

void Tree::stats(int nn[], int nd[])
{
   nn[0] += 1;
   nd[0] += tmem;
   for (int ii = 0; ii < nsub; ii++) 
      if (psub[ii]) psub[ii]->stats(&nn[1],&nd[1]);
}




Generated by  Doxygen 1.6.0   Back to index