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

   Copyright 2006 2007 2008 2009 2010 2011 2012 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.5.0

#include "zfuncs.h"


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


//  Pause and wait for user KB input

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


//  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;
}


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

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

void zappcrash(cchar *pMess, ... )
{
   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
         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()
{
   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[])
{
   int      cc1, cc2, ii, err;
   char     command[1000];
   
   if (getuid() == 0) return;                                              //  already root  v.4.9   

   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)

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)
{
   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)
{
   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;
}


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

//  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;
}


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

//  Lock or unlock a multi-process multi-thread resource.
//  Only one process/thread may posess a given lock.
//  A reboot or process exit or crash releases the lock.
//  global_lock() returns fd > 0 if success, -1 otherwise.
//  To unlock, call global_unlock(fd).

int global_lock(cchar *lockname)                                           //  v.4.7
{
   int       err, fd;
   char      lockfile[100];

   strcpy(lockfile,"/tmp/global_lock_");
   strcat(lockfile,lockname);

   fd = open(lockfile,O_RDWR|O_CREAT,0666);                                //  open or create the lock file
   if (fd < 0) {
      printf("global_lock(), %s \n",strerror(errno));
      return -1;
   }

   err = flock(fd,LOCK_EX|LOCK_NB);                                        //  request exclusive non-blocking lock
   if (err) {
      close(fd);
      return -1;
   }

   return fd + 1;                                                          //  return value is >= 1
}


int global_unlock(int fd)
{
   int err = close(fd - 1);
   if (err < 0) return -1;
   else return 1;
}


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

//  start a detached thread using a simplified protocol 

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;
}


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

//  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
{
   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

   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)
{
   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);
   *psen1 = *psen2 = 0;                                                    //  destroy sentinels
   
   zmalloc_tot -= bytes;

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

   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)
{
   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)
{
   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];
   else  --zmalloc_count[ii];

   return;
}


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

void zmalloc_report()
{
   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];
      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()
//


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
{
   FILE           *fid;
   va_list        arglist;
   char           buff[10000], *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,9999,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,9999,fid,1);                                     //  next output, less trailing \n
   if (prec) return strdupz(prec,0,"command_output");                      //  return output to caller

   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
}


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

//  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()
   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)
{
   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 
{
   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;
   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;
   
   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)
{
   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
   
   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;
   pthread_exit(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.

int strHash(cchar *string, int max) 
{
   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
   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)
{
   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);
   dest[cc] = 0;
   return cc;
}

int strTrim2(char *string)
{
   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;
   
   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);
   strcpy(pp,string);
   return pp;
}


//  copy into existing 'zmalloc' string if present and long enough
//  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);
   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)
{
   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)
{
   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)
{
   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)
{
   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");
   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"); 
   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
   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
   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)
{
   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)
{
   #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
{
   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 seekint, 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] - seekint;                                             //  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;
         }
      }
   }
}


//  perform a binary search on sorted set of records in memory
//  return matching record number (0 based) or -1 if not found

int bsearch(char *seekrec, char *allrecs, int recl, int nrecs)
{
   int      ii, jj, kk, rkk;

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

   while (true)
   {
      kk = strcmp(allrecs+ii*recl,seekrec);                                //  compare member rec to seek rec

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

      else if (kk < 0)
      {
         ii += jj;                                                         //  too low, go up in set
         if (ii > nrecs) 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()
{
   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()
{
   static int64   seed = 23459876;
   return drandz(&seed);
}


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

   spline1: define a curve using a set of data points (x and y values)
   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:  /prefix/bin/appname                       //  = PREFIX/bin/appname
     + other application directories are derived as follows:
         /prefix/share/appname/data/            desktop, parameters, etc.
         /prefix/share/doc/appname/             README, CHANGES, user guide
         /prefix/share/appname/icons/           icon files .png
         /prefix/share/appname/locales/         translations: appname-de.po  etc.
         /home/user/.appname/                   parameters etc. are copied here

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

***/

namespace zfuncs
{
   char        zappname[20];
   char        zprefix[200], zdatadir[200], zdocdir[200];                  //  app directories
   char        zicondir[200], zlocalesdir[200], zuserdir[200];
   char        zlang[8] = "en";                                            //  "lc" or "lc_RC"
   char        JPGquality[4] = "85";                                       //  JPG file save quality
   cchar       *F1_help_topic = 0;                                         //  current F1 help topic
   int         open_popup_windows = 0;                                     //  open popup window count
   pthread_t   tid_main = 0;                                               //  main thread ID               v.4.8
}

cchar * get_zprefix() { return zfuncs::zprefix; }                          //  /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            secs, err;
   time_t         Tnow;
   char           *chTnow;
   struct stat    statdat;
   va_list        arglist;
   FILE           *fid;
   
   catch_signals();                                                        //  catch segfault, do backtrace
   
   strcpy(zappname,appname);                                               //  save app name
   
   #ifndef PREFIX                                                          //  install location                v.4.7
      #define PREFIX "/usr"
   #endif
   
   strncpy0(work,PREFIX,199);                                              //  /usr or /home/<userid>          v.4.6
   strcpy(zprefix,work);                                                   //  /prefix
   strncatv(zdatadir,199,work,"/share/",zappname,"/data",null);            //  /prefix/share/appname/data
   strncatv(zicondir,199,work,"/share/",zappname,"/icons",null);           //  /prefix/share/appname/icons
   strncatv(zlocalesdir,199,work,"/share/",zappname,"/locales",null);      //  /prefix/share/appname/locales
   strncatv(zdocdir,199,work,"/share/doc/",zappname,null);                 //  /prefix/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);
   }

   tid_main = pthread_self();                                              //  v.4.8
   Tnow = time(0);
   chTnow = ctime(&Tnow);
   chTnow[19] = 0;

   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);
      err = stat(logfile,&statdat);
      if (! err) {
         secs = Tnow - statdat.st_mtime;                                   //  if log file age > 1 hour        v.5.0
         if (secs > 3600) rename(logfile,oldlog);                          //    rename to *.old
      }
      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");
   }

   printf("=========== %s %s \n",zappname,chTnow);
   
   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
//  look for user guide file in  /usr/share/doc/appname/
//                       and in  /usr/share/doc/appname/extras/            //  distros are different  v.4.7

void showz_userguide(cchar *context)
{
   using namespace zfuncs;

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

   snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,zlang);             //  look for userguide-lc_RC.html
   err = access(docfile,R_OK);
   if (! err) goto add_context;

   snprintf(docfile,199,"%s/extras/userguide-%s.html",zdocdir,zlang);
   err = access(docfile,R_OK);
   if (! err) goto add_context;

   strncpy0(lang,zlang,3);
   snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,lang);              //  look for userguide-lc.html
   err = access(docfile,R_OK);
   if (! err) goto add_context;

   snprintf(docfile,199,"%s/extras/userguide-%s.html",zdocdir,lang);
   err = access(docfile,R_OK);
   if (! err) goto add_context;

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

   snprintf(docfile,199,"%s/extras/userguide-en.html",zdocdir);
   err = access(docfile,R_OK);
   if (! err) goto add_context;

   zmessageACK(null,ZTX("help file not found: %s"),docfile);               //  give up
   return;

add_context:   

   if (context && *context)
      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("changelog");                                             //  renamed  v.4.7
   return;
}

void showz_translations()
{
   showz_doctext("translations");                                          //  renamed  v.4.7
   return;
}


//  find and show a text in a popup window
//  the text file may also be a compressed .gz file
//  look for: /usr/share/doc/appname/file.gz                               //  v.4.7
//            /usr/share/doc/appname/file                                  //  distros do it differently
//            /usr/share/doc/appname/extras/file.gz
//            /usr/share/doc/appname/extras/file

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

   struct stat statb;
   char     buff1[200], buff2[220];
   cchar    *command;
   int      err;

   command = "zcat";   
   snprintf(buff1,199,"%s/%s.gz",zdocdir,file);                            //  look for .../appname/file.gz
   err = stat(buff1,&statb);
   if (! err) goto showit;

   command = "cat";   
   snprintf(buff1,199,"%s/%s",zdocdir,file);                               //  look for .../appname/file
   err = stat(buff1,&statb);
   if (! err) goto showit;

   command = "zcat";   
   snprintf(buff1,199,"%s/extras/%s.gz",zdocdir,file);                     //  look for .../appname/extras/file.gz
   err = stat(buff1,&statb);
   if (! err) goto showit;

   command = "cat";   
   snprintf(buff1,199,"%s/extras/%s",zdocdir,file);                        //  look for .../appname/extras/file
   err = stat(buff1,&statb);
   if (! err) goto showit;
   
   command = "echo file not found:";
   snprintf(buff1,199,"%s/%s",zdocdir,file);

showit:
   snprintf(buff2,219,"%s %s",command,buff1);
   popup_command(buff2,600,400);
   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",zprefix,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;
}


//  show a local or remote html file using the user's preferred browser
//  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);
   printf(" %s \n",command);                                               //  v.4.1 
   err = system(command);
   return;
}


/**************************************************************************
   GTK utility functions
**************************************************************************/

//  Iterate main loop every "skip" calls

void zmainloop(int skip)
{
   static int  xskip = 0;
   
   if (skip) {
      if (++xskip < skip) return;
      xskip = 0;
   }

   if (! pthread_equal(pthread_self(),zfuncs::tid_main)) return;           //  thread caller, do nothing    v.5.0

   gdk_flush();                                                            //  v.5.0
   while (gtk_events_pending()) gtk_main_iteration();                      //  main() caller OK
   return;
}


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

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

void zthreadcrash()
{
   if (pthread_equal(pthread_self(),zfuncs::tid_main)) return;             //  v.4.8
   zappcrash("forbidden function called from thread");
   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
      printf("%s",message);
      return;
   }
   
   zthreadcrash();
   
   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 
      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);
   
   zmainloop();                                                            //  v.4.8
   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)
{
   GtkTextBuffer  *textbuff;
   GtkTextIter    iter;
   GtkTextMark    *mark;
   
   if (! mLog) return;
   
   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
   return;
}


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

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

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

   if (! mLog) return;

   buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   gtk_text_buffer_set_text(buff,"",-1);
   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;

   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
   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;
   
   if (ftf)
   {                                                                       //  get all window text
      ftf = 0;
      if (precs) g_free(precs);                                            //  free prior memory if there
      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
   }
   
   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;

   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; 

   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;

   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
***************************************************************************/

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
{
   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;
}


//  create a popup menu                                                    //  v.4.7

GtkWidget * create_popmenu()
{
   GtkWidget   *popmenu;
   
   popmenu = gtk_menu_new();
   return popmenu;
}


//  add a menu item to a popup menu                                        //  v.4.7

GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, mtFunc func)
{
   GtkWidget   *wmitem;

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


//  pop-up the menu at current mouse position                              //  v.4.7

void popup_menu(GtkWidget *popmenu)
{
   gtk_menu_popup(GTK_MENU(popmenu),0,0,0,0,0,gtk_get_current_event_time());
   gtk_widget_show_all(popmenu);
   return;
}


//  add submenu item to menu item, optional response function

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"))
      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);
   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

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

   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);
   return 0;
}


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


//  counter, keeps track of open or pending zdialogs

namespace zfuncs {  int   zdialog_busy;  }


//  private functions for widget events and dialog completion

int  zdialog_widget_event(GtkWidget *, zdialog *zd);
int  zdialog_response_event(GtkWidget *, int zstat, zdialog *zd);


//  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

zdialog * zdialog_new(cchar *title, GtkWidget *parent, ...)                //  parent added
{
   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);

   if (! title) title = " ";                                               //  v.5.0

   dialog = gtk_dialog_new();                                              //  attributes optional
   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");

   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
      }
   }
   
   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");
   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
   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
   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);
   
   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),
                        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 
      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

   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
   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, ...)                     //  (GTK, this does not work)
{
   va_list        arglist;
   cchar          *radio2;
   GtkWidget      *gwidget, *widget;
   GSList         *glist;

   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);

   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
   if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid");

   GtkWidget *window = zd->widget[0].widget;
   gtk_window_set_default_size(GTK_WINDOW(window),width,height);
   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
      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
      zd->widget[iiw].data = wdata;
      if (utf8_check(wdata))
         printf("zdialog: bad UTF8 encoding %s \n",wdata);
   }
   
   zd->disabled++;                                                         //  disable for widget stuffing 

   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
      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 
      pp = strField(data,'|',1); 
      if (pp) gdkcolor.red = 256 * atoi(pp);                               //  bugfix 
      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
      }
      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
   }

   zd->disabled--;                                                         //  re-enable dialog 
   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 
   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
   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_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd);

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

   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
   gtk_window_present(GTK_WINDOW(dialog));
   if (! zd->parent)                                                       //  if no parent, force to top
      gtk_window_set_keep_above(GTK_WINDOW(dialog),1);                     //  v.4.9

   G_SIGNAL(dialog,"focus-in-event",zdialog_focus_event_signal,zd);        //  connect focus event function     v.5.0
   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

   zfuncs::zdialog_busy++;                                                 //  count open zdialogs    v.4.7

   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).

int 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             *wname, *type, *wdata;
   char              sdata[20];
   double            dval;
   static int        cbadded = 0;

   if (! zd) return 1;                                                     //  detect destroyed dialog   v.5.0
   if (zd->sentinel != zdsentinel) return 1;
   
   for (ii = 0; ii < zdmaxbutts; ii++)                                     //  check completion buttons
      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 1;                                                            //  v.5.0
   }

   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 1;

found_widget:

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

   wname = 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
   {
      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 
   
   lastwidget = widget;                                                    //  remember last widget updated

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

   zd->disabled = 0;                                                       //  re-enable widgets
   return 1;                                                               //  v.5.0
}


//  zdialog response handler for "focus-in-event" signal                   //  v.5.0

int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd)
{
   if (zd->zstat) return 0;                                                //  already complete
   if (zd->help_topic) 
      zfuncs::F1_help_topic = zd->help_topic;                              //  set help topic
   zdialog_send_event(zd,"focus");                                         //  notify dialog event function
   return 0;                                                               //  must be 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;                                                               //  must be 0
}

int zdialog_KBrelease(GtkWidget *, GdkEventKey *kbevent, zdialog *zd)
{
   zdialog_event  *evfunc = 0;                                             //  dialog event callback function

   int      KBkey = kbevent->keyval;
   char     event[8];
   
   if (KBkey == GDK_F1) {
      showz_userguide(zfuncs::F1_help_topic);                              //  context help
      return 1;
   }

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                              //  do user callback function    v.4.7
      strcpy(event,"KB x");                                                //    with event = "KB x"
      event[3] = KBkey;                                                    //    (x = key now released)
      evfunc(zd,event);
      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).

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

   if (! zd) return 1;                                                     //  detect destroyed dialog   v.4.9
   if (zd->sentinel != zdsentinel) return 1;
   if (zd->zstat) return 1;                                                //  already complete 

   if (zstat > -1) zstat = -1;                                             //  insure cancel status      v.5.0
   zd->zstat = zstat;                                                      //  set zdialog status

   if (zd->eventCB) {
      evfunc = (zdialog_event *) zd->eventCB;                              //  do callback function
      evfunc(zd,"zstat");                                                  //  (should do zfree())
   }
   
   return 1;                                                               //  v.5.0
}


//  send an event to an active dialog

int zdialog_send_event(zdialog *zd, cchar *event)
{
   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)
{
   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)
{
   using namespace zfuncs;
   
   if (! zd) return 0;                                                     //  detect destroyed dialog
   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
      zdialog_busy--;                                                      //  v.4.7
   }

   if (zd->widget[0].widget) {                                             //  multiple destroys OK
      if (zd->saveposn) 
         zdialog_save_position(zd);                                        //  save position for next use  v.4.7
      gtk_widget_destroy(zd->widget[0].widget);                            //  destroy GTK dialog
      zdialog_busy--;                                                      //  v.4.7
      zd->widget[0].widget = 0;
   }
   
   if (! zd->zstat) zd->zstat = -1;                                        //  status = destroyed
   return 1;
}


//  free zdialog memory (destroy first, if not already)

int zdialog_free(zdialog *&zd)                                             //  reference
{
   if (! zd) return 0;                                                     //  detect destroyed dialog
   if (zd->sentinel != zdsentinel) return 0;
   zdialog_destroy(zd);                                                    //  destroy GTK dialog if there
   zd->sentinel = 0;                                                       //  mark invalid 
   zfree(zd->widget[0].data);                                              //  bugfix memory leak

   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
         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
   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)
{
   while (true)
   {
      zmainloop();
      if (! zd) return -1;                                                 //  dialog destroyed
      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)
{
   GtkWidget   *widget; 
   
   if (zd->sentinel != zdsentinel) return 0;
   widget = zdialog_widget(zd, name);
   if (! widget) return 0;

   gtk_editable_select_region(GTK_EDITABLE(widget),0,-1);                  //  focus on widget
   gtk_widget_grab_focus(widget);

   return 1;
}


//  set cursor for zdialog (e.g. busy cursor)

void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor)
{
   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 
}

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
{
   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 
   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) 
      gtk_combo_box_append_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);

   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
   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) 
      gtk_combo_box_prepend_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);

   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 
   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

int zdialog_cb_delete(zdialog *zd, cchar *name, cchar *data)
{
   int      ii, nn;

   if (! zd) return 0;                                                     //  detect destroyed dialog
   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;

   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

   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 
   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_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),"");

   return 0;
}


//  make a combo box drop down to show all entries

int zdialog_cb_popup(zdialog *zd, cchar *name)
{
   int      ii;

   if (! zd) return 0;                                                     //  detect destroyed dialog
   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

   gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget));

   return 0;
}


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

//  functions to save and recall zdialog window positions

namespace zdposn_names
{
   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;

   dialog = zd->widget[0].widget;
   if (! dialog) return;                                                   //  destroyed                 v.4.7
   
   parent = zd->parent;                                                    //  parent window

   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;
}


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

   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)

   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 next line"), 123, "qwerty");
      
      A German .po file (appname-de.po) would have the following:

         msgid ""
         "answer: %d %s \n"
         " next line"
         msgstr ""
         "Antwort: %d %s \n"
         " nächste Zeile"

***/

namespace ZTXnames                                                         //  remove GOFUNC usage becasue of
{                                                                          //    GCC optimization errors
   FILE        *fidr, *fidw;
   char        buff[ZTXmaxcc], *ppq1, *ppq2;
   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        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 file at application startup
//  prepare english strings and translations for quick access

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

   int         ii, err;
   char        *pp, pofile[200];
   char        lc[4], lc_RC[8];                                            //  language (de), * + region (de_AT) 
   struct stat statb;

   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(zlang,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(zlang,pp,6);                                        //  "lc_RC" language/region code
      else strcpy(zlang,"en");                                             //  use English
   }

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

   if (strnEqu(zlang,"en",2)) return;                                      //  English, do nothing    v.4.3
   
   strncpy0(lc,zlang,3);                                                   //  language code alone, e.g. "de"
   strncpy0(lc_RC,zlang,6);                                                //  + opt. region code, "de" or "de_AT"

   snprintf(pofile,199,"%s/locales/%s-%s.po",zuserdir,zappname,lc_RC);     //  look for user custom .po file
   err = stat(pofile,&statb);                                              //  (a translation project is underway)
   if (err) {
      snprintf(pofile,199,"%s/locales/%s-%s.po",zuserdir,zappname,lc);
      err = stat(pofile,&statb);
   }

   if (err) {                                                              //  look for normal installed .po file
      snprintf(pofile,199,"%s/%s-%s.po",zlocalesdir,zappname,lc_RC);
      err = stat(pofile,&statb);
      if (err) {
         snprintf(pofile,199,"%s/%s-%s.po",zlocalesdir,zappname,lc);       //  default to lc only       v.5.0
         err = stat(pofile,&statb);
      }
   }

   if (! err) printf("using translation file: %s \n",pofile);
   else {
      printf("no translation file found for %s \n",lc_RC);
      return;
   }

   fidr = fopen(pofile,"r");                                               //  open .po file
   if (! fidr) {
      printf("cannot open translation file: %s \n",pofile);
      return;
   }
   
   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");                            //  add to translation tables
         ttext[Ntext] = strdupz(Ttext,0,"ZTX");
         *Etext = *Ttext = 0;
         Ntext++;
         if (Ntext == ZTXmaxent)                                           //  cannot continue
            zappcrash("more than %d translations",ZTXmaxent);
      }
   }
   
   fclose(fidr);

   printf(".po file has %d entries \n",Ntext);
   
   for (ii = 0; ii < Ntext; ii++)
   {
      pp = ZTXmergetext(etext[ii]);                                        //  merge segmented text strings
      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 if thousands of strings must be searched.

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

   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 file 
   is copied from the install location to a user location (if not 
   already there) where it will be modified. This user .po file
   will be used for translations going forward.
   
   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.
   
   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 file in the user location.
   
***************************************************************************/


//  This function is called from the application menu "Translate".
//  Set translation mode ON or OFF (zfuncs::Ftranslate). If ON and no 
//  .po translation file is found in /home/<user>/.appname/locales/,
//  then it is copied from the installed .po file 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 for
//  input of the revised translations.

void ZTX_translation_start(GtkWidget *parent)                              //  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        *pp, command[400];
   char        pofile1[200], pofile2[200];
   struct stat statb;
   
   if (strnEqu(zlang,"en",2)) {                                            //  locale cannot be english
      zmessageACK(0,"cannot translate locale %s",zlang);
      return;
   }

   if (Ftranslate) return;                                                 //  already active      v.5.0
   Ftranslate = 1;

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

   zd = zdialog_new("Translation Mode",parent,"apply","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);

   if (zd->zstat == 1) {                                                   //  [apply]
      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
   }
   else Ftranslate = 0;                                                    //  v.5.0

   zdialog_free(zd);

   if (Ftranslate)                                                         //  start or continue translation mode
   {
      snprintf(pofile1,199,"%s/%s-%s.po",zlocalesdir,zappname,zlang);      //  installed .po file for lc_RC
      snprintf(pofile2,199,"%s/locales/%s-%s.po",zuserdir,zappname,zlang); //  user .po file for new translations
      
      err = stat(pofile2,&statb);                                          //  look for user .po file
      if (err) {
         err = stat(pofile1,&statb);                                       //  look for installed .po file
         if (err) printf("No .po file found %s \n",pofile1);
         else {
            pp = strrchr(pofile2,'/');                                     //  create /locales if needed
            *pp = 0;                                                       //  bugfix                       v.5.0
            mkdir(pofile2,0754);
            *pp = '/';
            snprintf(command,399,"cp -R %s %s",pofile1,pofile2);           //  copy installed .po file to user
            err = system(command);
            if (! err) printf(".po file copied from %s to %s \n",pofile1,pofile2);
            else printf("cannot copy .po file from %s to %s \n",pofile1,pofile2);
         }
      }
      
      ZTXinit(zlang);                                                      //  re-initialize translations

      zddump = zdialog_new("Translations",parent,"apply","cancel",null);   //  create dialog for translation edits
      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 file

   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(zlang,"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 file and clear the window

void ZTXnames::ZTX_translation_update()                                    //  modified v.5.0
{
   using namespace zfuncs;
   using namespace ZTXnames;

   int         ii, err, ftf, Nupd;
   char        pofile1[200], pofile2[200];
   char        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

   memset(Fused,0,ZTXmaxent);                                              //  clear 'english text used' flags

   snprintf(pofile1,199,"%s/locales/%s-%s.po",zuserdir,zappname,zlang);    //  user .po file for new translations
   fidr = fopen(pofile1,"r");                                              //  open .po file for read
   if (! fidr) {
      printf("cannot read translation file: %s \n",pofile1);
      return;
   }
   
   strcpy(pofile2,pofile1);                                                //  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;
   }

   Nupd = 0;                                                               //  count updates to .po file
   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])) {                                 //  log translation added or updated
               printf("changed: %s \n         %s \n",                      //  v.5.0
                              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);

   if (Nupd) {                                                             //  rename *.po.new to *.po
      err = rename(pofile2,pofile1);
      if (err) printf("cannot rename %s to *.po",pofile2);
   }
   else remove(pofile2);                                                   //  no changes, delete *.po.new
   
   ZTXinit(zlang);                                                         //  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 (blank_null(wporec)) continue;                                 //  bugfix   v.5.0
         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;
}


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

//  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;

   if (! monofont) 
      monofont = pango_font_description_from_string("monospace 9");
   
   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
         gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(parent));
      
      if (parent)
         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
      gtk_window_present(GTK_WINDOW(mWin));
      if (! parent) gtk_window_set_keep_above(GTK_WINDOW(mWin),1);

      zfuncs::open_popup_windows++;                                        //  v.4.7
   }

   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,1);

   if (strEqu(action,"close")) {                                           //  close window
      if (mWin) gtk_widget_destroy(mWin);
      mWin = 0;
   }
   
   if (text && strEqu(text,"destroypop")) {                                //  "destroy" signal from [x]  
      zfuncs::open_popup_windows--;                                        //  v.4.7
      mWin = 0;
   }

   zmainloop();                                                            //  v.4.7
   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()
{
   char        *buff;
   int         err, contx = 0;
   
   write_popup_text("open",command,ww,hh,parent);                          //  bugfix 

   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, ... )
{
   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);
   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, ...)
{
   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, ... )
{
   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

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
      g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,zd);

   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);
   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;
}


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

//  Display a dialog with a message and 1-5 choice buttons.
//  Returns choice 1-N corresponding to button selected.
//  Parent window may be NULL.
//  List of buttons must be NULL terminated.

int zdialog_choose(cchar *title, GtkWidget *parent, cchar *message, ...)   //  new v.4.7
{
   zdialog     *zd;
   va_list     arglist;
   int         ii, zstat, Nbutts;
   cchar       *butts[6];

   va_start(arglist,message);

   for (ii = 0; ii < 5; ii++)
   {
      butts[ii] = va_arg(arglist,cchar *);
      if (! butts[ii]) break;
   }

   Nbutts = ii;
   if (! Nbutts) zappcrash("zdialog_choose(), no buttons");

repeat:

   zd = zdialog_new(title,parent,butts[0],butts[1],butts[2],butts[3],butts[4],null);
   zdialog_add_widget(zd,"hbox","hbmess","dialog","space=5");
   zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5");
   zdialog_run(zd);
   zstat = zdialog_wait(zd);
   zdialog_free(zd);

   if (zstat < 1) goto repeat;
   return zstat;
}


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

//    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();


//  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

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, setfname = 0;
   int            fcstat, bcode = 0, qnum, hide = 0;
   char           *qual, *file1, *file2, **flist = 0;
   struct stat    fstat;
   
   zthreadcrash();                                                         //  thread usage not allowed

   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");
      setfname = 1;
   }

   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");
      setfname = 1;
   }
   
   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,"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"))
      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;                                                  //  set folder name
            gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir);
         }
         if (setfname)                                                     //  set new file name      v.4.7
            gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile);
         zfree(pdir);
      }
      else if (S_ISREG(fstat.st_mode))                                     //  select given file
         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile);
      else if (S_ISDIR(fstat.st_mode))                                     //  select given folder
         gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile);
   }

   gtk_widget_show_all(dialog); 

   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 fcreturn;
      NF = g_slist_length(gslist);                                         //  no. selected files
      if (! NF) goto fcreturn;

      flist = (char **) zmalloc((NF+1)*sizeof(char *),"zgetfile");         //  allocate returned list
 
      for (ii = 0; ii < NF; ii++) 
      {                                                                    //  process selected files
         file1 = (char *) g_slist_nth_data(gslist,ii);
         if (strlen(file1) >= maxfcc)
            file1 = (char *) "ridiculously long filespec";
         file2 = strdupz(file1,0,"zgetfile");                              //  re-allocate memory 
         g_free(file1);
         flist[ii] = file2;
      }
      flist[ii] = 0;                                                       //  EOL marker
   }

fcreturn:
   if (gslist) g_slist_free(gslist);
   gtk_widget_destroy(dialog);
   return flist;
}


//  zgetfile private function - get preview images for image files

void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget)
{
   using namespace zfuncs;

   GdkPixbuf   *thumbnail;
   char        *filename;

   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 = get_thumbnail(filename,192);                                //  use 192x192 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;
}


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

   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_paper_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(GtkWidget *parent)
{
   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
            (GTK_WINDOW(parent),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(GtkWidget *parent)
{
   using namespace print_image;
   
   zdialog     *zd;
   int         zstat;

   zd = zdialog_new(ZTX("margins"),parent,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;
}


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

//  connect a user callback function to a window drag-drop event

void drag_drop_connect(GtkWidget *window, drag_drop_func *ufunc)
{
   int  drag_drop_connect2(GtkWidget *, void *, int, int, void *, int, int, void *);
   char     string[] = "STRING";
   GtkTargetEntry  file_drop_target = { string, 0, 0 };

   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

   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");
      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");
   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;
}


/**************************************************************************
   Miscellaneous GDK/GTK functions
***************************************************************************/

//  Get thumbnail image for given image file.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.

GdkPixbuf * get_thumbnail(char *fpath, int size)                           //  v.5.0
{
   using namespace zfuncs;

   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   int               err;
   char              *bpath;
   struct stat       statf;

   zthreadcrash();                                                         //  thread usage not allowed

   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;
   }
   
   thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror);
   return thumbpxb;                                                        //  return pixbuf to caller
}


//  make a cursor from a graphic file in application's icon directory

GdkCursor * zmakecursor(cchar *iconfile)
{
   using namespace zfuncs;

   GError         *gerror = 0;
   GdkPixbuf      *pixbuf;
   GdkDisplay     *display;
   GdkCursor      *cursor = 0;
   char           iconpath[200];

   zthreadcrash();                                                         //  thread usage not allowed

   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;
}


//  move the mouse pointer to given position

int gdk_window_move_pointer(GdkWindow *window, int px, int py)             //  v.5.0
{
   GdkDisplay  *display;
   GdkScreen   *screen;
   int         rpx, rpy;
   
   display = gdk_drawable_get_display(GDK_DRAWABLE(window));
   if (! display) return 0;
   screen = gdk_drawable_get_screen(GDK_DRAWABLE(window));
   if (! screen) return 0;
   gdk_window_get_root_coords(window,px,py,&rpx,&rpy);
   gdk_display_warp_pointer(display,screen,rpx,rpy);
   return 1;
}


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

     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.
     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 28 million pixels/sec. on my 3.3 GHz CPU.              //  v.5.0
     (i.e. a 10 megapix image needs about 10/28 = 0.36 seconds)
     
     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
            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

   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
      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
   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
      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
         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];   //  4th color = alpha
      
      if (red == acolor && green == acolor && blue == acolor) {            //  avoid acolor in image
         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 
   }
      
   return pixbuf2;
}


/**************************************************************************
    parameter management functions
***************************************************************************/

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

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 

   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);

   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
         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 
      {
         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
      {
         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);
      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]);
}



