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

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   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/.

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

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


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

   Fotoxx image edit - Tools menu functions

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


//  Manage Collections

void m_manage_collections(GtkWidget *, cchar *)                            //  v.11.11
{
   int   manage_coll_dialog_event(zdialog *zd, cchar *event);              //  manage collectiond dialog event func

   zdialog     *zd;
   cchar       *helptext = ZTX("When editing a collection, right-click \n"
                               "an image or thumbnail to add or remove.");
   
   zfuncs::F1_help_topic = "manage_collections";

   if (mod_keep()) return;                                                 //  unsaved edits

/***
    __________________________________________
   |                                          |
   |        Manage Collections                |
   |                                          |
   |  When editing a collection, right-click  |
   |  an image or thumbnail to add or remove. |
   |                                          |
   |  [New]     Start a new collection        |
   |  [Edit]    Edit a collection             |
   |  [View]    View a collection             |
   |  [Delete]  Delete a collection           |
   |                                          |
   |  Editing: <collection-name>              |
   |  Action: xxxxxxxxxxxxxx                  |
   |                                          |
   |                                 [Done]   |
   |__________________________________________|

***/

   zd = zdialog_new(ZTX("Manage Collections"),mWin,Bdone,null);
   zd_edit_coll = zd;

   zdialog_add_widget(zd,"label","labhelp","dialog",helptext,"space=5");

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");

   zdialog_add_widget(zd,"button","new","vb1",Bnew);
   zdialog_add_widget(zd,"button","edit","vb1",Bedit);
   zdialog_add_widget(zd,"button","view","vb1",Bview);
   zdialog_add_widget(zd,"button","delete","vb1",Bdelete);

   zdialog_add_widget(zd,"hbox","hbnew","vb2");
   zdialog_add_widget(zd,"label","labnew","hbnew",ZTX("Start new collection"));
   zdialog_add_widget(zd,"hbox","hbedit","vb2");
   zdialog_add_widget(zd,"label","labedit","hbedit",ZTX("Edit a collection"));
   zdialog_add_widget(zd,"hbox","hbview","vb2");
   zdialog_add_widget(zd,"label","labview","hbview",ZTX("View a collection"));
   zdialog_add_widget(zd,"hbox","hbdel","vb2");
   zdialog_add_widget(zd,"label","labdel","hbdel",ZTX("Delete a collection"));

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbedit","dialog");
   zdialog_add_widget(zd,"label","labedit","hbedit",ZTX("Editing:"),"space=3");
   zdialog_add_widget(zd,"label","editcoll","hbedit",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbact","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labact","hbact",ZTX("Action:"),"space=3");
   zdialog_add_widget(zd,"label","lastact","hbact",0,"space=3");

   zdialog_help(zd,"manage_collections");
   zdialog_resize(zd,300,0);
   zdialog_run(zd,manage_coll_dialog_event);
   zdialog_wait(zd);
   zdialog_free(zd);
   zd_edit_coll = 0;
   
   if (edit_coll_name) zfree(edit_coll_name);                              //  no collection for editing
   edit_coll_name = 0;

   return;
}


//  manage collections dialog event and completion function

int manage_coll_dialog_event(zdialog *zd, cchar *event)                    //  v.11.11
{
   int            err;
   FILE           *fid;
   char           *pp;
   struct stat    statb;

   if (strEqu(event,"new"))                                                //  start a new collection for editing
   {
      if (edit_coll_name) {                                                //  no current collection
         zfree(edit_coll_name);
         edit_coll_name = 0;
      }

      if (edit_coll_file) zfree(edit_coll_file);
      edit_coll_file = zgetfile1(ZTX("New Collection"),"save",collections_dirk);
      if (! edit_coll_file) return 0;

      fid = fopen(edit_coll_file,"w");                                     //  open collection file
      if (! fid) return 0;
      fprintf(fid,"Fotoxx Collection \n");                                 //  write one record
      fclose(fid);
      
      pp = strrchr(edit_coll_file,'/');                                    //  file name = collection name
      edit_coll_name = strdupz(pp+1);                                      //  set current edit collection

      zdialog_stuff(zd,"editcoll",edit_coll_name);
      return 0;
   }

   if (strEqu(event,"edit"))                                               //  choose a collection for editing
   {
      if (edit_coll_name) {                                                //  no current collection
         zfree(edit_coll_name);
         edit_coll_name = 0;
      }

      if (edit_coll_file) zfree(edit_coll_file);
      edit_coll_file = zgetfile1(ZTX("Edit Collection"),"open",collections_dirk);
      if (! edit_coll_file) return 0;

      err = stat(edit_coll_file,&statb);
      if (err) return 0;

      pp = strrchr(edit_coll_file,'/');                                    //  file name = collection name
      edit_coll_name = strdupz(pp+1);                                      //  set current edit collection

      zdialog_stuff(zd,"editcoll",edit_coll_name);
      return 0;
   }
   
   if (strEqu(event,"view"))                                               //  choose a collection for viewing
   {
      if (edit_coll_file) zfree(edit_coll_file);
      edit_coll_file = zgetfile1(ZTX("View Collection"),"open",collections_dirk);
      if (! edit_coll_file) return 0;

      err = stat(edit_coll_file,&statb);
      if (err) return 0;

      image_gallery(edit_coll_file,"initF",0,m_gallery2,mWin);             //  generate gallery of files in coll.

      if (edit_coll_memberfile) zfree(edit_coll_memberfile);
      edit_coll_memberfile = image_gallery(0,"find",0);                    //  top of list
      if (! edit_coll_memberfile) return 0;
      f_open(edit_coll_memberfile,1);                                      //  open first file
      zmainloop();

      image_gallery(0,"paint1");                                           //  show new image gallery window
      return 0;
   }

   if (strEqu(event,"delete"))                                             //  choose collection to delete
   {
      if (edit_coll_file) zfree(edit_coll_file);
      edit_coll_file = zgetfile1(ZTX("Delete Collection"),"open",collections_dirk);
      if (! edit_coll_file) return 0;
      
      if (! zmessageYN(mWin,ZTX("delete %s ?"),edit_coll_file)) return 0;
      
      remove(edit_coll_file);                                              //  delete it
      return 0;
   }
   
   return 0;
}


//  Popup menu for editing a collection. 
//  This function is called when a thumbnail is right-clicked.

void edit_coll_popmenu(GtkWidget *, char *file)
{
   void edit_coll_popfunc(GtkWidget *, cchar *menu);

   GtkWidget    *popmenu;
   static char  addmenuitem[200];
   
   if (! edit_coll_name) return;                                           //  no edit in progress, do nothing
   
   if (edit_coll_memberfile) zfree(edit_coll_memberfile);                  //  save clicked thumbnail file
   edit_coll_memberfile = strdupz(file,0,"edit_coll");

   popmenu = create_popmenu();                                             //  create popup menu
   
   snprintf(addmenuitem,200,ZTX("add image to collection: %s"),edit_coll_name);
   add_popmenu_item(popmenu,addmenuitem,edit_coll_popfunc);
   add_popmenu_item(popmenu,ZTX("remove image from collection"),edit_coll_popfunc);
   add_popmenu_item(popmenu,ZTX("remove and save image"),edit_coll_popfunc);
   add_popmenu_item(popmenu,ZTX("insert saved images here"),edit_coll_popfunc);

   popup_menu(popmenu);
   return;
}


//  Response function for the edit collections popup menu

void edit_coll_popfunc(GtkWidget *, cchar *menu)
{
   int      ii, found = 0;
   FILE     *fidr, *fidw;
   char     collfile[maxfcc], buff[maxfcc];
   char     *filename, tempfile[200];

   static char *saved_files[100];
   static int  Nsaved = 0;
   
   if (! edit_coll_name) return;                                           //  no edit in progress, do nothing
   if (! edit_coll_memberfile) return;                                     //  no member file for action
   if (! zd_edit_coll) return;
   
   strcpy(collfile,collections_dirk);                                      //  edit collection full path
   strcat(collfile,edit_coll_name);
   
   strcpy(tempfile,collections_dirk);                                      //  temp file for copying
   strcat(tempfile,"tempfile");

   if (strnEqu(menu,ZTX("add image to collection"),23))
   {
      fidw = fopen(collfile,"a");                                          //  append new file at end of list
      if (! fidw) return;
      fprintf(fidw,"%s\n",edit_coll_memberfile);
      fclose(fidw);
      
      filename = strrchr(edit_coll_memberfile,'/') + 1;                    //  notify in dialog window
      sprintf(buff,"Add: %s",filename);
      zdialog_stuff(zd_edit_coll,"lastact",buff);

      return;
   }
   
   if (strEqu(menu,ZTX("remove image from collection")) ||
       strEqu(menu,ZTX("remove and save image")))
   {
      fidr = fopen(collfile,"r");                                          //  copy collection file and omit
      if (! fidr) return;                                                  //    the image file being removed
      fidw =fopen(tempfile,"w");
      if (! fidw) return;
      
      while (true)
      {
         filename = fgets_trim(buff,maxfcc,fidr);
         if (! filename) break;
         if (strEqu(filename,edit_coll_memberfile)) {
            found = 1;
            continue;
         }
         fprintf(fidw,"%s\n",filename);
      }
      
      fclose(fidr);
      fclose(fidw);
      rename(tempfile,collfile);
      
      filename = strrchr(edit_coll_memberfile,'/') + 1;                    //  notify in dialog window
      if (found) sprintf(buff,"Removed: %s",filename);
      else sprintf(buff,"Not found: %s",filename);
      zdialog_stuff(zd_edit_coll,"lastact",buff);

      if (found && strEqu(menu,ZTX("remove and save image")))              //  add file to list of saved files
      {
         if (Nsaved > 99) {
            zmessageACK(mWin,ZTX("too many saved files"));
            return;
         }
         saved_files[Nsaved] = strdupz(edit_coll_memberfile,0,"edit_coll");
         Nsaved++;
      }

      if (strEqu(collfile,image_navi::galleryname)) {      
         image_gallery(collfile,"initF",0,m_gallery2,mWin);                //  update gallery window
         image_gallery(0,"paint2",-1);
      }
      
      return;
   }
   
   if (strEqu(menu,ZTX("insert saved images here")))
   {
      if (! Nsaved) return;
      if (strNeq(collfile,image_navi::galleryname)) return;                //  gallery not the edit collection
      
      fidr = fopen(collfile,"r");                                          //  copy collection file and insert
      if (! fidr) return;                                                  //    the saved image files
      fidw =fopen(tempfile,"w");
      if (! fidw) return;
      
      while (true)
      {
         filename = fgets_trim(buff,maxfcc,fidr);
         if (! filename) break;
         fprintf(fidw,"%s\n",filename);
         if (strNeq(filename,edit_coll_memberfile)) continue;
         for (ii = 0; ii < Nsaved; ii++) {
            fprintf(fidw,"%s\n",saved_files[ii]);
            zfree(saved_files[ii]);
         }
      }
      
      fclose(fidr);
      fclose(fidw);
      rename(tempfile,collfile);
      
      sprintf(buff,"inserted %d files",Nsaved);
      zdialog_stuff(zd_edit_coll,"lastact",buff);
      Nsaved = 0;

      image_gallery(collfile,"initF",0,m_gallery2,mWin);                   //  update gallery window
      image_gallery(0,"paint2",-1);

      return;
   }
}
  

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

//  Change the top directory for all image collections.
//  This facilitates changing the Fotoxx top image directory 
//  without having to re-create all collections.

void m_move_collections(GtkWidget *, cchar *)
{
   zdialog     *zd;
   char        *pp, oldtop[200], newtop[200];
   char        command[200], buffr[maxfcc], buffw[maxfcc];
   char        collfile[200], tempfile[200];
   int         zstat, contx = 0, err;
   FILE        *fidr, *fidw;

   zfuncs::F1_help_topic = "move_collections";

   if (mod_keep()) return;                                                 //  unsaved edits

   zd = zdialog_new(ZTX("Move Collections"),mWin,Bapply,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbold","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labold","hbold",ZTX("old top directory"),"space=5");
   zdialog_add_widget(zd,"entry","oldtop","hbold",0,"scc=30");
   zdialog_add_widget(zd,"hbox","hbnew","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labnew","hbnew",ZTX("new top directory"),"space=5");
   zdialog_add_widget(zd,"entry","newtop","hbnew",0,"scc=30");
   
   zdialog_stuff(zd,"oldtop",topdirk);
   zdialog_stuff(zd,"newtop",topdirk);
   
   zdialog_help(zd,"move_collections");
   zdialog_run(zd);

   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }
   
   zdialog_fetch(zd,"oldtop",oldtop,200);                                  //  old and new top directory paths
   zdialog_fetch(zd,"newtop",newtop,200);
   zdialog_free(zd);
   
   snprintf(tempfile,200,"%s/move-collection",get_zuserdir());
   
   snprintf(command,200,"find -L %s -type f",collections_dirk);            //  find all collections
   printf("%s \n",command);

   while ((pp = command_output(contx,command,null)))                       //  loop all collections
   {
      printf("%s \n",pp);
      strncpy0(collfile,pp,200);
      zfree(pp);

      fidr = fopen(collfile,"r");                                          //  open collection and temp files
      if (! fidr) break;
      
      fidw = fopen(tempfile,"w");
      if (! fidw) break;
      
      while ((pp = fgets(buffr,maxfcc,fidr)))                              //  loop collection recs
      {
         repl_1str(buffr,buffw,oldtop,newtop);                             //  replace top directory path
         err = fputs(buffw,fidw);
         if (err < 0) break;
      }
      
      err = fclose(fidr);
      err = fclose(fidw);
      if (err) break;
      
      err = rename(tempfile,collfile);                                     //  replace collection with temp file
      if (err) break;
   }
   
   if (err) {
      err = errno;
      printf("%s \n %s \n",collfile,strerror(err));
      zmessageACK(mWin,"%s \n %s",collfile,strerror(err));
   }
   else zmessageACK(mWin,ZTX("completed"));
   
   return;
}


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

//  Batch file convert - change format (file type), resize, export 

void m_batchconvert(GtkWidget *, cchar *)                                  //  new v.10.8
{
   int   batchconvert_dialog_event(zdialog *zd, cchar *event);

   zfuncs::F1_help_topic = "batch_convert";                                //  v.10.8

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  lock menus

/***
               Batch Convert/Resize/Export

        [select files]  N files selected
        new max. width [____]  height [____]
        new file type  (o) same  (o) JPG  (o) PNG  (o) TIF
        (o) replace originals   [x] remove EXIF
        (o) export to location 
        [browse] [_________________________________]
      
                                 [proceed]  [cancel]
***/

   zdialog *zd = zdialog_new(ZTX("Batch Convert/Resize/Export"),mWin,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf","0 files selected","space=10");

   zdialog_add_widget(zd,"hbox","hbwh","dialog");
   zdialog_add_widget(zd,"label","labw","hbwh",ZTX("new max. width"),"space=5");
   zdialog_add_widget(zd,"entry","maxw","hbwh","1000","scc=5");
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=5");
   zdialog_add_widget(zd,"label","labh","hbwh",ZTX("height"),"space=5");
   zdialog_add_widget(zd,"entry","maxh","hbwh","700","scc=5");
   
   zdialog_add_widget(zd,"hbox","hbtyp","dialog",0,"space=4");
   zdialog_add_widget(zd,"label","labtyp","hbtyp",ZTX("new file type"),"space=5");
   zdialog_add_widget(zd,"radio","same","hbtyp",ZTX("same"),"space=6");
   zdialog_add_widget(zd,"radio","jpg","hbtyp","JPG","space=6");
   zdialog_add_widget(zd,"radio","png","hbtyp","PNG","space=6");
   zdialog_add_widget(zd,"radio","tif","hbtyp","TIF","space=6");

   zdialog_add_widget(zd,"hbox","hbrep","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbrep1","hbrep",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbrep2","hbrep",0,"homog|space=5");
   zdialog_add_widget(zd,"radio","replace","vbrep1",ZTX("replace originals"));
   zdialog_add_widget(zd,"radio","export","vbrep1",ZTX("export to location"));
   zdialog_add_widget(zd,"check","noexif","vbrep2",ZTX("remove EXIF"));

   zdialog_add_widget(zd,"hbox","hbloc","dialog",0,"space=3|expand");
   zdialog_add_widget(zd,"button","browse","hbloc",Bbrowse,"space=5");
   zdialog_add_widget(zd,"entry","location","hbloc",0,"scc=30|expand");

   zdialog_stuff(zd,"same",1);                                             //  defaults: 
   zdialog_stuff(zd,"replace",0);                                          //  same file type, export, no EXIF
   zdialog_stuff(zd,"export",1);
   zdialog_stuff(zd,"noexif",1);
   zdialog_stuff(zd,"maxw",batchresize[0]);                                //  v.10.9
   zdialog_stuff(zd,"maxh",batchresize[1]);

   zdialog_help(zd,"batch_convert");                                       //  zdialog help topic        v.11.08
   zdialog_run(zd,batchconvert_dialog_event,"parent");                     //  run dialog                v.11.07
   zdialog_wait(zd);                                                       //  wait for completion

   menulock(0);
   return;
}


//  dialog event and completion callback function

int batchconvert_dialog_event(zdialog *zd, cchar *event)
{
   static char    **flist = 0;
   char           countmess[50];
   int            Freplace, maxw, maxh, Fnoexif, yn;
   int            ii, err, lcc, fcc, ww, hh, fposn;
   char           location[maxfcc], *pp, *ploc, *pfile;
   cchar          *pext, *ptype;
   char           *oldfile, *newfile, *tempfile;
   double         scale, wscale, hscale;
   PXM            *pxmout;
   struct stat    statb;

   if (strEqu(event,"files"))                                              //  select images to convert
   {
      if (flist) {                                                         //  free prior list
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
      }

      flist = image_gallery_getfiles(0,mWin);                              //  get list of files to convert   v.10.9

      if (flist)                                                           //  count files selected
         for (ii = 0; flist[ii]; ii++);
      else ii = 0;

      snprintf(countmess,50,ZTX("%d files selected"),ii);                  //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }
   
   if (strEqu(event,"browse"))
   {
      ploc = zgetfile1(ZTX("Select directory"),"folder",curr_dirk);        //  get directory          v.10.9
      if (! ploc) return 0;
      zdialog_stuff(zd,"location",ploc);
      zfree(ploc);
      zdialog_stuff(zd,"export",1);
   }

   if (! zd->zstat) return 0;                                              //  dialog still busy

   if (zd->zstat != 1) goto cleanup;                                       //  dialog canceled
   
   if (! flist) {                                                          //  no files selected
      zd->zstat = 0;                                                       //  keep dialog active
      return 0;
   }

   zdialog_fetch(zd,"maxw",maxw);                                          //  new max width
   zdialog_fetch(zd,"maxh",maxh);                                          //  new max height
   zdialog_fetch(zd,"replace",Freplace);                                   //  replace originals y/n
   zdialog_fetch(zd,"noexif",Fnoexif);                                     //  copy EXIF/IPTC y/n

   zdialog_fetch(zd,"same",ii);                                            //  get new file type      v.11.12
   if (ii) ptype = 0;
   zdialog_fetch(zd,"jpg",ii);
   if (ii) ptype = ".jpg";
   zdialog_fetch(zd,"png",ii);
   if (ii) ptype = ".png";
   zdialog_fetch(zd,"tif",ii);
   if (ii) ptype = ".tif";

   zdialog_fetch(zd,"location",location,maxfcc);                           //  output location (Freplace = 0)
   strTrim2(location,location);                                            //  trim leading and trailing blanks

   if (Freplace) {
      yn = zmessageYN(mWin,ZTX("replace original files? (max. %d x %d)"),maxw,maxh);
      if (! yn) {
         zd->zstat = 0;
         return 0;
      }
   }
   else {
      yn = zmessageYN(mWin,ZTX("copy files? (max. %d x %d) \n"
                               " to location %s"),maxw,maxh,location);
      if (! yn) {
         zd->zstat = 0;
         return 0;
      }
   }

   if (! Freplace) {
      err = stat(location,&statb);
      if (err || ! S_ISDIR(statb.st_mode)) {
         zmessageACK(mWin,ZTX("location is not a valid directory"));
         zd->zstat = 0;
         return 0;
      }
   }

   if (maxw < 20 || maxh < 20) {
      zmessageACK(mWin,ZTX("max. size %d x %d is not reasonable"),maxw,maxh);
      zd->zstat = 0;
      return 0;
   }
   
   write_popup_text("open","Processing files",500,200,mWin);               //  status monitor popup window

   lcc = strlen(location);
   if (! Freplace && location[lcc-1] == '/') lcc--;                        //  remove trailing '/'

   for (ii = 0; flist[ii]; ii++)                                           //  loop selected files
   {
      oldfile = flist[ii];

      if (Freplace)
         newfile = strdupz(oldfile,8,"batchconvert");                      //  new file = old file
      else {
         pfile = strrchr(oldfile,'/');
         if (! pfile) continue;
         fcc = strlen(pfile);
         newfile = strdupz(location,fcc+9,"batchconvert");
         strcpy(newfile+lcc,pfile);                                        //  new file at location
      }
      
      pp = strrchr(newfile,'/');                                           //  find existing file .ext
      pp = strrchr(pp,'.');
      if (! pp) pp = pp + strlen(pp);
      if (strlen(pp) > 5) pp = pp + strlen(pp);

      pext = 0;
      if (ptype) pext = ptype;                                             //  new .ext from user
      else {
         if (strstr(".JPG .jpg .JPEG .jpeg",pp)) pext = ".jpg";            //  new .ext from existing .ext
         if (strstr(".PNG .png",pp)) pext = ".png";
         if (strstr(".TIF .TIFF .tif .tiff",pp)) pext = ".tif";
      }
      if (! pext) pext = ".jpg";
      
      strcpy(pp,pext);                                                     //  set new file .ext

      write_popup_text("write",newfile);                                   //  report progress
      zmainloop();

      if (! Freplace) {
         err = stat(newfile,&statb);                                       //  if export, check if file exists
         if (! err) {
            write_popup_text("write",ZTX("*** file already exists"));
            zfree(newfile);
            continue;
         }
      }

      err = f_open(oldfile,0);                                             //  open old file
      if (err) {
         write_popup_text("write",ZTX("*** file type not supported"));
         zfree(newfile);
         continue;
      }

      wscale = hscale = 1.0;
      if (Fww > maxw) wscale = 1.0 * maxw / Fww;                           //  compute new size
      if (Fhh > maxh) hscale = 1.0 * maxh / Fhh;
      if (wscale < hscale) scale = wscale;
      else scale = hscale;
      ww = Fww * scale;
      hh = Fhh * scale;
      pxmout = PXM_rescale(Fpxm8,ww,hh);                                   //  rescale file

      tempfile = strdupz(newfile,12,"batchconvert");                       //  temp file needed for EXIF/IPTC copy
      pp = strrchr(tempfile,'.');
      strcpy(pp,"-temp");
      strcpy(pp+5,pext);
      
      if (strstr(".jpg .png",pext))                                        //  write rescaled file to temp file
         PXBwrite(pxmout,tempfile);
      else 
         TIFFwrite(pxmout,tempfile);

      if (! Fnoexif)                                                       //  copy EXIF/IPTC if requested  v.11.12
         info_copy(oldfile,tempfile,0,0,0);
      
      snprintf(command,ccc,"cp -p \"%s\" \"%s\" ",tempfile,newfile);       //  copy tempfile to newfile
      err = system(command);
      if (err) write_popup_text("write",wstrerror(err));

      remove(tempfile);                                                    //  remove tempfile

      if (Freplace && ! err && strNeq(oldfile,newfile))                    //  remove oldfile if not
         remove(oldfile);                                                  //    already replaced

      zfree(newfile);
      zfree(tempfile);
      PXM_free(pxmout);
   }

   write_popup_text("write","COMPLETED");
   
   batchresize[0] = maxw;                                                  //  save preferred size    v.10.9
   batchresize[1] = maxh;

cleanup:

   if (flist) {                                                            //  free memory
      for (ii = 0; flist[ii]; ii++) 
         zfree(flist[ii]);
      zfree(flist);
      flist = 0;
   }

   zdialog_free(zd);                                                       //  kill dialog

   image_gallery(curr_file,"init");                                        //  generate new gallery list       v.11.12
   fposn = image_gallery_position(curr_file,0);
   image_gallery(0,"paint2",fposn);                                        //  refresh gallery window if active
   
   return 0;
}


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

//  open a single RAW file or convert multiple RAW files to tiff

void  m_conv_raw(GtkWidget *, cchar *menu)                                 //  simplified    v.11.08
{
   char     **raw_files;
   char     *rawfile, *outfile, *pp;
   int      ii, bpc, err, filecount;
   cchar    *ftype;

   zfuncs::F1_help_topic = "convert_RAW";

   if (! Fufraw) {
      zmessageACK(mWin,ZTX("Program ufraw-batch is required"));
      return;
   }

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;
   
   if (strEqu(menu,"Open RAW File"))                                       //  convert one file    v.11.08
   {
      rawfile = zgetfile1(ZTX("Open RAW File"),"open",curr_dirk);
      if (! rawfile) {
         menulock(0);
         return;
      }
      raw_files = (char **) zmalloc(2 * sizeof(char *),"conv-RAW");
      raw_files[0] = rawfile;
      raw_files[1] = 0;
      ftype = "tiff";                                                      //  default tiff/16 format    v.11.12
      bpc = 16;
   }
   else                                                                    //  convert multiple files
   {
      raw_files = zgetfileN(ZTX("Select RAW files to convert"),"openN",curr_dirk);
      if (! raw_files) {
         menulock(0);
         return;
      }
      ii = zdialog_choose(0,mWin,ZTX("Choose file type"),                  //  set file type    v.11.12
                          "JPG", "PNG", "TIFF-8", "TIFF-16", null);
      if (ii == 1) ftype = "jpg";
      if (ii == 2) ftype = "png";
      if (ii == 3) ftype = "tif";
      if (ii == 4) ftype = "tiff";
      bpc = 8;
      if (ii == 4) bpc = 16;
      
   }
   
   for (ii = 0; raw_files[ii]; ii++);                                      //  count selected files
   filecount = ii;

   write_popup_text("open","Converting RAW files",500,200,mWin);           //  status monitor popup window
   write_popup_text("write","converting ...");
   
   for (ii = 0; ii < filecount; ii++)
   {
      rawfile = raw_files[ii];
      outfile = strdupz(rawfile,5,"raw_out");
      pp = strrchr(rawfile,'.');
      pp = outfile + (pp - rawfile);
      pp[0] = '.';
      strcpy(pp+1,ftype);                                                  //  v.11.12

      write_popup_text("write",rawfile);                                   //  write output to log window
      
      snprintf(command,ccc,"ufraw-batch --wb=camera --out-type=%s --out-depth=%d"
                  " --overwrite --output=\"%s\" \"%s\" ", ftype, bpc, outfile, rawfile);
      err = system(command);

      if (err) {
         write_popup_text("write",wstrerror(err));
         zfree(outfile);
         continue;
      }

      f_open(outfile,0);                                                   //  open converted file in main window
      zfree(outfile);
      zmainloop();
   }

   write_popup_text("write","COMPLETED");
   write_popup_text("close",0);

   image_gallery(curr_file,"init");                                        //  update gallery file list
   image_gallery(0,"paint2",curr_file_posn);                               //  refresh gallery window if active

   for (ii = 0; raw_files[ii]; ii++)                                       //  free memory
      zfree(raw_files[ii]);
   zfree(raw_files);

   menulock(0);
   return;
}


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

//  enter or leave slideshow mode

int         ss_interval = 3;                                               //  slide show interval
int         ss_timer = 0;                                                  //  slide show timer
int         ss_latest = 0;                                                 //  show only latest revision files
char        *ss_oldfile, *ss_newfile;                                      //  image files for transition
PXM         *ss_pxmold, *ss_pxmnew, *ss_pxmmix;                            //  pixmap images: old, new, mixed
int         ss_ww, ss_hh;                                                  //  full screen window size
int         ss_busy = 0;                                                   //  transition underway
int         ss_escape = 0;                                                 //  user pressed Escape key
int         ss_paused = 0;                                                 //  user pressed P key (pause/resume)

void  ss_instant();                                                        //  transition functions
void  ss_fadein();
void  ss_rollright();
void  ss_rolldown();
void  ss_shiftleft();
void  ss_venetian();
void  ss_grate();
void  ss_rectangle();
void  ss_ellipse();
void  ss_radar();
void  ss_jaws();
char * ss_prev(int posn);
char * ss_next(int posn);
char * ss_prev_latest(int posn);
char * ss_next_latest(int posn);
char * ss_basename(char *file);


struct ss_table_t {
   cchar    *name;
   cchar    *vbox;
   void     (*func)();
};

ss_table_t  ss_table[SSNF] =                                               //  image transition types
{
   {  "arrow keys",  "vb41",   null           },                           //  name, zdialog parent, function
   {  "instant",     "vb41",   ss_instant     },
   {  "fade-in",     "vb41",   ss_fadein      },
   {  "roll-right",  "vb41",   ss_rollright   },
   {  "roll-down",   "vb42",   ss_rolldown    },
   {  "shift-left",  "vb42",   ss_shiftleft   },
   {  "venetian",    "vb42",   ss_venetian    },
   {  "grate",       "vb42",   ss_grate       },
   {  "rectangle",   "vb43",   ss_rectangle   },
   {  "ellipse",     "vb43",   ss_ellipse     },
   {  "radar",       "vb43",   ss_radar       },
   {  "jaws",        "vb43",   ss_jaws        }
};


void m_slideshow(GtkWidget *, cchar *)
{
   int   slideshow_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;
   int            zstat, secs, err, ii;
   cchar          *esc_message = ZTX("Press ESC to exit slide show");
   cchar          *latest_message = ZTX("show only latest file versions");
   
   zfuncs::F1_help_topic = "slide_show";                                   //  v.10.8
   
   ss_table[0].name = ZTX("arrow keys");                                   //  use translated names
   ss_table[1].name = ZTX("instant");
   ss_table[2].name = ZTX("fade-in");
   ss_table[3].name = ZTX("roll-right");
   ss_table[4].name = ZTX("roll-down");
   ss_table[5].name = ZTX("shift-left");
   ss_table[6].name = ZTX("venetian");
   ss_table[7].name = ZTX("grate");
   ss_table[8].name = ZTX("rectangle");
   ss_table[9].name = ZTX("ellipse");
   ss_table[10].name = ZTX("radar");
   ss_table[11].name = ZTX("jaws");

   if (! Fslideshow)                                                       //  start slide show
   {
      if (! curr_file) return;
      if (! menulock(1)) return;

      zd = zdialog_new(ZTX("Slide Show"),mWin,Bproceed,Bcancel,null);      //  user dialog
      zdialog_add_widget(zd,"hbox","hbesc","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labesc","hbesc",esc_message,"space=3");
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labsecs","hb1",ZTX("seconds"),"space=3");
      zdialog_add_widget(zd,"entry","secs","hb1","3","scc=5");
      zdialog_add_widget(zd,"check","latest","hb1",latest_message,"space=8");
      zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","lab21","hb2",ZTX("music file"),"space=3");
      zdialog_add_widget(zd,"entry","musicfile","hb2",0,"scc=30|space=5");
      zdialog_add_widget(zd,"button","browse","hb2",Bbrowse,"space=5");
      zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","lab3","hb3",ZTX("transitions"),"space=3");
      zdialog_add_widget(zd,"hbox","hb4","dialog");
      zdialog_add_widget(zd,"vbox","vb41","hb4",0,"space=8");
      zdialog_add_widget(zd,"vbox","vb42","hb4",0,"space=8");
      zdialog_add_widget(zd,"vbox","vb43","hb4",0,"space=8");
      
      for (ii = 0; ii < SSNF; ii++) {                                      //  add transition check boxes
         zdialog_add_widget(zd,"check",ss_table[ii].name,
                                 ss_table[ii].vbox,ss_table[ii].name);
         if (ss_funcs[ii]) zdialog_stuff(zd,ss_table[ii].name,1);          //  restore user preferences
      }
      
      if (ss_interval < 9999)
         zdialog_stuff(zd,"secs",ss_interval);                             //  stuff last time interval
      zdialog_stuff(zd,"musicfile",ss_musicfile);                          //  stuff last music file
      
      zdialog_help(zd,"slide_show");                                       //  zdialog help topic     v.11.08
      zdialog_run(zd,slideshow_dialog_event);                              //  run dialog
      zstat = zdialog_wait(zd);                                            //  wait for completion

      if (zstat != 1) {                                                    //  cancel
         zdialog_free(zd);
         menulock(0);
         return;
      }

      zdialog_fetch(zd,"secs",secs);                                       //  get interval, seconds
      zdialog_fetch(zd,"latest",ss_latest);                                //  get "latest files only" option

      for (ii = 0; ii < SSNF; ii++)                                        //  get selected transition types
         zdialog_fetch(zd,ss_table[ii].name,ss_funcs[ii]);                 //  v.11.08
      
      for (ii = 1; ii < SSNF; ii++)                                        //  check selections
         if (ss_funcs[ii]) break;
      if (ii < SSNF) ss_funcs[0] = 0;                                      //  if some, remove arrow keys
      else ss_funcs[0] = 1;                                                //  if none, default arrow keys
      
      char *pp = zmalloc(500,"musicfile");
      zdialog_fetch(zd,"musicfile",pp,499);                                //  get music file
      ss_musicfile = pp;

      zdialog_free(zd);

      ss_interval = secs;                                                  //  interval between slides
      if (ss_funcs[0]) ss_interval = 9999;                                 //  if manual transition, huge interval
      ss_paused = 0;                                                       //  not paused          v.11.10

      if (ss_musicfile && *ss_musicfile && strNeq(ss_musicfile,"undefined")) 
      {                                                                    //  if music file present, start it up
         sprintf(command,"xdg-open \"%s\" ",ss_musicfile);                 //  v.11.04
         printf("command: %s \n",command);
         err = system(command);
         if (err) printf("error: %s \n",wstrerror(err));
      }

      gtk_widget_hide_all(GTK_WIDGET(mMbar));                              //  enter slide show mode
      gtk_widget_hide_all(GTK_WIDGET(mTbar));                              //  (full screen, no extras)
      gtk_widget_hide_all(GTK_WIDGET(STbar));

      gdk_window_set_background(drWin->window,&black);                     //  black background             v.11.10

      gtk_window_maximize(MWIN);                                           //  avoid gtk_window_fullscreen()
      zmainloop();                                                         //  must wait for update      v.11.10
      gtk_window_get_size(MWIN,&ss_ww,&ss_hh);

      ss_pxmold = PXM_make(ss_ww,ss_hh,8);                                 //  make 3 screen-size pixmaps
      ss_pxmnew = PXM_make(ss_ww,ss_hh,8);
      ss_pxmmix = PXM_make(ss_ww,ss_hh,8);
      
      ss_newfile = strdupz(curr_file);                                     //  start with current image
      ss_oldfile = 0;

      ss_timer = get_seconds() + ss_interval;                              //  set timer for first call to
      Fslideshow = 1;                                                      //    ss_slideshow_next()
   }

   else                                                                    //  leave slide show mode
   {
      if (ss_busy) {
         ss_escape = 1;                                                    //  wait for transition done
         return;
      }

      ss_escape = 0;
      Fslideshow = 0;

      gdk_window_set_background(drWin->window,&lgray);                     //  restore background        v.11.10

      gtk_window_unmaximize(MWIN);                                         //  restore old window size   v.11.10
      gtk_widget_show_all(GTK_WIDGET(mMbar));
      gtk_widget_show_all(GTK_WIDGET(mTbar));
      gtk_widget_show_all(GTK_WIDGET(STbar));
      
      if (ss_newfile) zfree(ss_newfile);                                   //  free memory
      if (ss_oldfile) zfree(ss_oldfile);
      ss_newfile = ss_oldfile = 0;
      PXM_free(ss_pxmold);
      PXM_free(ss_pxmnew);
      PXM_free(ss_pxmmix);

      menulock(0);
   }

   Fblowup = Fslideshow;
   Fzoom = 0;                                                              //  fit image to window
   mwpaint2(); 
   return;
}


//  dialog event function - file chooser for music file

int slideshow_dialog_event(zdialog *zd, cchar *event)                      //  v.11.04
{
   char     *pp;
   
   if (! strEqu(event,"browse")) return 0;
   pp = zgetfile1(ZTX("Select music file or playlist"),"open",ss_musicfile);
   if (! pp) return 0;
   zdialog_stuff(zd,"musicfile",pp);
   zfree(pp);
   return 0;
}


//  Show next slide if time is up or user navigates with arrow keys.
//  Called by timer function and by keyboard function if Fslideshow is set.

void slideshow_next(cchar *mode)                                           //  new  v.11.01
{
   void  ss_loadimage(char *file, PXM *pxmout);

   double         secs;
   char           *pp = 0;
   int            Fkey = 0, cp, ii;
   static int     last_transition = 0;
   
   if (strEqu(mode,"pause"))                                               //  toggle slide show paused status
      ss_paused = 1 - ss_paused;                                           //  (spacebar)    v.11.10

   if (ss_busy) return;                                                    //  come back later

   if (ss_escape) {                                                        //  user pressed escape key
      m_slideshow(0,0);                                                    //  exit slide show
      return;
   }
   
   if (strEqu(mode,"timer")) {                                             //  timer trigger
      if (ss_paused) return;
      secs = get_seconds();
      if (secs < ss_timer) return;
      mode = "next";                                                       //  time for next image
      Fkey = 0;
   }
   else Fkey = 1;                                                          //  keyboard trigger
   
   cp = curr_file_posn;
   
   if (ss_latest) {                                                        //  get prev/next image file,
      if (strEqu(mode,"prev"))                                             //    latest version          v.11.10
         pp = ss_prev_latest(cp);
      if (strEqu(mode,"next"))
         pp = ss_next_latest(cp);
   }      
   else {                                                                  //  get prev or next image file
      if (strEqu(mode,"prev"))
         pp = ss_prev(cp);
      if (strEqu(mode,"next"))
         pp = ss_next(cp);
   }
   
   if (! pp) return;                                                       //  there is none

   ss_busy++;

   if (ss_oldfile) zfree(ss_oldfile);
   ss_oldfile = ss_newfile;
   ss_newfile = pp;
   
   ss_loadimage(ss_oldfile,ss_pxmold);   
   ss_loadimage(ss_newfile,ss_pxmnew);

   mutex_lock(&Fpixmap_lock);                                              //  block other window updates

   if (Fkey) ss_instant();                                                 //  KB input, do instant transition
   
   else                                                                    //  use next selected transition type
   {                                                                       //  v.11.08
      ii = last_transition;                                                //  start with last transition type + 1
      if (++ii > SSNF-1) ii = 1;                                           //  1st is omitted (arrow keys)

      while (! ss_funcs[ii])                                               //  infinite loop if none selected
         if (++ii > SSNF-1) ii = 1;                                        //  bugfix  v.11.09.1

      last_transition = ii;                                                //  next transition
      if (ii > 0) ss_table[ii].func();                                     //  do image transition
   }

   mutex_unlock(&Fpixmap_lock);

   zmainloop();                                                            //  catch-up anynch window updates?

   f_open(ss_newfile,0);                                                   //  sync all image data

   if (zdeditcctext) m_edit_cctext(0,0);                                   //  edit caption and comments dialog

   secs = get_seconds();                                                   //  set time for next image
   ss_timer = secs + ss_interval + 0.5;
   ss_busy = 0;
   return;
}


//  get previous file before current file

char * ss_prev(int cp)
{
   char     *pp;
   int      ii;
   
   for (ii = cp-1; ii >= 0; ii--)
   {
      pp = image_gallery(0,"find",ii);
      if (pp) return pp;
   }
   
   return 0;
}


//  get next file after current file

char * ss_next(int cp)
{
   char     *pp;
   int      ii;
   int      nfiles = image_navi::nfiles;

   for (ii = cp+1; ii < nfiles; ii++)
   {
      pp = image_gallery(0,"find",ii);
      if (pp) return pp;
   }
   
   return 0;
}


//  get previous file, latest version, before current file

char * ss_prev_latest(int cp)                                              //  new v.11.10
{
   char     *pp, *name1, *name2;
   int      ii;
   
   pp = image_gallery(0,"find",cp);
   if (! pp) return 0;

   name1 = ss_basename(pp);
   zfree(pp);

   for (ii = cp-1; ii >= 0; ii--)
   {
      pp = image_gallery(0,"find",ii);
      if (! pp) {
         zfree(name1);
         return 0;
      }
      name2 = ss_basename(pp);
      if (strEqu(name1,name2)) {
         zfree(name2);
         zfree(pp);
      }
      else {
         zfree(name1);
         zfree(name2);
         return pp;
      }
   }
   return 0;
}


//  get next file, latest version, after current file

char * ss_next_latest(int cp)                                              //  new v.11.10
{
   char     *pp1, *pp2, *name1, *name2;
   int      ii;
   int      nfiles = image_navi::nfiles;

   pp1 = image_gallery(0,"find",cp+1);
   if (! pp1) return 0;
   
   name1 = ss_basename(pp1);
   
   for (ii = cp+2; ii < nfiles; ii++)
   {
      pp2 = image_gallery(0,"find",ii);
      if (! pp2) {
         zfree(name1);
         return pp1;
      }
      name2 = ss_basename(pp2);
      if (strEqu(name1,name2)) {
         zfree(pp1);
         zfree(name2);
         pp1 = pp2;
      }
      else {
         zfree(pp2);
         zfree(name1);
         zfree(name2);
         return pp1;
      }
   }
   return 0;
}


//  extract base name from image file name
//  base name is file name without directory, version, extension

char * ss_basename(char *file)                                             //  v.11.10
{
   char     *pp1, *pp2, *pp3;

   pp1 = strrchr(file,'/');                                                //  skip over directory if present
   if (pp1) file = pp1 + 1;
   pp1 = strdupz(file,0,"ss.basename");
   pp2 = strrchr(pp1,'.');                                                 //  look for .ext
   if (! pp2) return pp1;
   *pp2 = 0;                                                               //  strip it off
   pp3 = pp2 - 4;
   if (! strnEqu(pp3,".v",2)) return pp1;                                  //  look for version .v00 to .v99
   if (pp3[2] < '0' || pp3[2] > '9') return pp1;
   if (pp3[3] < '0' || pp3[3] > '9') return pp1;
   *pp3 = 0;                                                               //  strip it off
   return pp1;
}


//  load image and rescale to fit in given pixmap = window size

void ss_loadimage(char *file, PXM *pxmout)                                 //  new v.11.01
{
   int         cc, fww, fhh, px, py, orgx, orgy;
   PXM         *pxmtemp1, *pxmtemp2;
   double      wscale, hscale, scale;
   uint8       *pix1, *pix2;
   
   cc = ss_ww * ss_hh * 3;                                                 //  clear output pixmap black  v.11.10
   memset(pxmout->bmp,0,cc);

   pxmtemp1 = f_load(file,8);                                              //  load image file into 1x pixmap
   if (! pxmtemp1) return;

   fww = pxmtemp1->ww;                                                     //  image size
   fhh = pxmtemp1->hh;
   
   wscale = 1.0 * ss_ww / fww;                                             //  find scale to fit in window
   hscale = 1.0 * ss_hh / fhh;
   if (wscale < hscale) scale = wscale;                                    //  use greatest ww/hh ratio
   else  scale = hscale;
   fww = fww * scale;
   fhh = fhh * scale;
   
   pxmtemp2 = PXM_rescale(pxmtemp1,fww,fhh);                               //  rescale image to fit window
   
   orgx = 0.5 * (ss_ww - fww);                                             //  origin of image in window
   orgy = 0.5 * (ss_hh - fhh);

   for (py = 0; py < fhh; py++)
   for (px = 0; px < fww; px++)
   {
      pix1 = PXMpix8(pxmtemp2,px,py);
      pix2 = PXMpix8(pxmout,px+orgx,py+orgy);
      pix2[0] = pix1[0];
      pix2[1] = pix1[1];
      pix2[2] = pix1[2];
   }

   PXM_free(pxmtemp1);
   PXM_free(pxmtemp2);
   
   return;
}


//  instant transition (for use with keyboard arrow keys)

void ss_instant()                                                          //  new v.11.01
{
   uint8    *pix3;

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww*3);
   return;
}


//  fade-out / fade-in transition

void ss_fadein()                                                           //  new v.11.01
{
   int         ii, jj, kk, px, py;
   double      newpart, oldpart;
   uint8       *pix1, *pix2, *pix3;
   
   PXM_free(ss_pxmmix);
   ss_pxmmix = PXM_copy(ss_pxmold);
   
   for (ii = 0; ii <= 100; ii += 10)
   {
      newpart = 0.01 * ii;
      oldpart = 1.0 - newpart;

      for (jj = 0; jj < 2; jj++)                                           //  four passes, each modifies 25%
      for (kk = 0; kk < 2; kk++)                                           //    of the pixels (visually smoother)
      {
         for (py = jj; py < ss_hh; py += 2)
         for (px = kk; px < ss_ww; px += 2)
         {
            pix1 = PXMpix8(ss_pxmold,px,py);
            pix2 = PXMpix8(ss_pxmnew,px,py);
            pix3 = PXMpix8(ss_pxmmix,px,py);
            pix3[0] = newpart * pix2[0] + oldpart * pix1[0];
            pix3[1] = newpart * pix2[1] + oldpart * pix1[1];
            pix3[2] = newpart * pix2[2] + oldpart * pix1[2];
         }
         
         pix3 = PXMpix8(ss_pxmmix,0,0);
         gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww*3);
      }
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww*3);
   return;
}


//  new image rolls over prior image from left to right

void ss_rollright()                                                        //  new v.11.01
{
   int         px, py;
   uint8       *pix1, *pix3;
   double      delay = 0.5 / ss_ww;                                        //  v.11.04

   PXM_free(ss_pxmmix);
   ss_pxmmix = PXM_copy(ss_pxmold);
   
   for (px = 0; px < ss_ww; px++)
   {
      pix1 = PXMpix8(ss_pxmnew,px,0);
      pix3 = PXMpix8(ss_pxmmix,px,0);
      
      for (py = 0; py < ss_hh; py++)
      {
         memmove(pix3,pix1,3);
         pix1 += ss_ww * 3;
         pix3 += ss_ww * 3;
      }
      
      pix3 = PXMpix8(ss_pxmmix,px,0);
      gdk_draw_rgb_image(drWin->window, gdkgc, px, 0, 1, ss_hh, NODITHER, pix3, ss_ww*3);
      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww*3);
   return;
}


//  new image rolls over prior image from top down

void ss_rolldown()                                                         //  new v.11.01
{
   int         py;
   uint8       *pix3;
   double      delay = 0.5 / ss_hh;
   
   for (py = 0; py < ss_hh; py++)
   {
      pix3 = PXMpix8(ss_pxmnew,0,py);
      gdk_draw_rgb_image(drWin->window, gdkgc, 0, py, ss_ww, 1, NODITHER, pix3, ss_ww*3);
      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww*3);
   return;
}


//  shift prior image to the left as next image shifts-in from the right
//  (this one flickers annoingly and cannot be fixed)

void ss_shiftleft()                                                        //  new v.11.01
{
   int            px, Nsteps = 50;
   uint8          *pix1, *pix3;

   for (px = 0; px < ss_ww; px += ss_ww / Nsteps)
   {
      pix1 = PXMpix8(ss_pxmold,px,0);
      pix3 = PXMpix8(ss_pxmnew,0,0);
      gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww - px, ss_hh, NODITHER, pix1, ss_ww * 3);
      gdk_draw_rgb_image(drWin->window, gdkgc, ss_ww - px, 0, px, ss_hh, NODITHER, pix3, ss_ww * 3);
      zsleep(0.01);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  new image opens up in horizontal rows like venetian blinds

void ss_venetian()                                                         //  new v.11.01
{
   int         py1, py2;
   uint8       *pix3;
   int         louver, Nlouvers = 20;
   int         louversize = ss_hh / Nlouvers;
   double      delay = 1.0 / louversize;
   
   for (py1 = 0; py1 < louversize; py1++)                                  //  y-row within each louver
   {
      for (louver = 0; louver < Nlouvers; louver++)                        //  louver, first to last
      {
         py2 = py1 + louver * louversize;
         if (py2 >= ss_hh) break;
         pix3 = PXMpix8(ss_pxmnew,0,py2);
         gdk_draw_rgb_image(drWin->window, gdkgc, 0, py2, ss_ww, 1, NODITHER, pix3, ss_ww*3);
      }

      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  a grate opens up to show new image

void ss_grate()                                                            //  new v.11.01
{
   int         px1, px2, py1, py2;
   uint8       *pix3;
   int         row, col, Nrow, Ncol;                                       //  rows and columns
   int         boxww, boxhh;
   double      delay;
   
   Ncol = 20;                                                              //  20 columns
   boxww = boxhh = ss_ww / Ncol;                                           //  square boxes
   Nrow = ss_hh / boxhh;                                                   //  corresp. rows
   Ncol++;                                                                 //  round up
   Nrow++;
   delay = 1.0 / boxhh;

   for (py1 = 0; py1 < boxhh; py1++)
   {
      for (row = 0; row < Nrow; row++)
      {
         py2 = py1 + row * boxhh;
         if (py2 >= ss_hh) break;
         pix3 = PXMpix8(ss_pxmnew,0,py2);
         gdk_draw_rgb_image(drWin->window, gdkgc, 0, py2, ss_ww, 1, NODITHER, pix3, ss_ww*3);
      }
      
      px1 = py1;

      for (col = 0; col < Ncol; col++)
      {
         px2 = px1 + col * boxww;
         if (px2 >= ss_ww) break;
         pix3 = PXMpix8(ss_pxmnew,px2,0);
         gdk_draw_rgb_image(drWin->window, gdkgc, px2, 0, 1, ss_hh, NODITHER, pix3, ss_ww*3);
      }
      
      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  A hole opens up from the center and expands outward

void ss_rectangle()                                                        //  new v.11.01
{
   int         px1, py1, px2, py2, px3, py3;
   int         ww1, hh1, ww2, hh2;
   uint8       *pix3;
   int         step, Nsteps = 100;
   double      delay = 1.0 / Nsteps;

   for (step = 1; step < Nsteps; step++)
   {
      ww1 = ss_ww * step / Nsteps;
      hh1 = ww1 * ss_hh / ss_ww;
      ww2 = ss_ww / Nsteps / 2;
      hh2 = ss_hh / Nsteps / 2;

      px1 = (ss_ww - ww1) / 2;
      py1 = (ss_hh - hh1) / 2;
      px2 = px1 + ww1 - ww2;
      py2 = py1;
      px3 = px1;
      py3 = py1 + hh1 - hh2;

      pix3 = PXMpix8(ss_pxmnew,px1,py1);
      gdk_draw_rgb_image(drWin->window, gdkgc, px1, py1, ww1+1, hh2+1, NODITHER, pix3, ss_ww*3);

      pix3 = PXMpix8(ss_pxmnew,px2,py2);
      gdk_draw_rgb_image(drWin->window, gdkgc, px2, py2, ww2+1, hh1+1, NODITHER, pix3, ss_ww*3);

      pix3 = PXMpix8(ss_pxmnew,px3,py3);
      gdk_draw_rgb_image(drWin->window, gdkgc, px3, py3, ww1+1, hh2+1, NODITHER, pix3, ss_ww*3);

      pix3 = PXMpix8(ss_pxmnew,px1,py1);
      gdk_draw_rgb_image(drWin->window, gdkgc, px1, py1, ww2+1, hh1+1, NODITHER, pix3, ss_ww*3);
      
      zmainloop();
      zsleep(delay);
   }
   
   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  An ellipse opens up from the center and expands outward

void ss_ellipse()                                                          //  new v.11.03
{
   uint8       *pix3;
   int         step, Nsteps = 100;
   int         px1, py1, ww;
   double      delay = 1.0 / Nsteps;
   double      a, b, a2, b2, px, py, px2, py2;
   double      ww2 = ss_ww / 2, hh2 = ss_hh / 2;
   
   for (step = 1; step < 1.3 * Nsteps; step++)
   {
      a = ww2 * step / Nsteps;                                             //  ellipse a and b constants
      b = a * ss_hh / ss_ww;                                               //    from tiny to >> image size
      a2 = a * a;
      b2 = b * b;
      
      for (py = -b; py <= +b; py += 3)                                     //  py from top of ellipse to bottom
      {
         while (py < -(hh2-2)) py += 3;
         if (py > hh2-2) break;
         py2 = py * py;
         px2 = a2 * (1.0 - py2 / b2);                                      //  corresponding px value,
         px = sqrt(px2);                                                   //  (+/- from center of ellipse)
         if (px > ww2) px = ww2;
         ww = 2 * px;                                                      //  length of line thru ellipse
         px1 = ww2 - px;                                                   //  relocate origin
         py1 = py + hh2;
         pix3 = PXMpix8(ss_pxmnew,px1,py1);
         gdk_draw_rgb_image(drWin->window, gdkgc, px1, py1, ww, 3, NODITHER, pix3, ss_ww*3);
      }

      zmainloop();
      zsleep(delay);
   }
   
   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  New image sweeps into view like a circular radar image

void ss_radar()                                                            //  new v.11.08
{
   int         px = 0, py = 0, Npx, Npy, Np = 12;                          //  smaller values make compiz jerky ***
   double      R, Rmax, T, Tmax, dT, delay;
   double      cosT, sinT;
   double      ww2 = ss_ww / 2, hh2 = ss_hh / 2;
   uint8       *pix3;
   
   Rmax = sqrt(ww2 * ww2 + hh2 * hh2);
   Tmax = pi;
   dT = 0.6 * asin(Np / Rmax);
   delay = dT / Tmax;                                                      //  1 sec. + CPU time
   if (delay < 0.001) delay = 0.001;
   
   for (T = 0; T < Tmax; T += dT)
   {
      cosT = cos(T);
      sinT = sin(T);

      for (R = -Rmax; R < Rmax; R += Np)                                   //  v.11.09
      {
         px = ww2 + R * cosT;
         py = hh2 - R * sinT;
         if (px < -Np) continue;
         if (py < -Np) continue;
         if (px > ss_ww-1) continue;
         if (py > ss_hh-1) continue;
         Npx = Npy = Np;                                                   //  do the last piece too    v.11.10
         if (px < 0) px = 0;
         if (py < 0) py = 0;
         if (px + Npx > ss_ww-1) Npx = ss_ww-1 - px;
         if (py + Npy > ss_hh-1) Npy = ss_hh-1 - py;
         pix3 = PXMpix8(ss_pxmnew,px,py);
         gdk_draw_rgb_image(drWin->window, gdkgc, px, py, Npx, Npy, NODITHER, pix3, ss_ww*3);
      }
      
      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


//  New image closes in from top and bottom with jagged teeth

void ss_jaws()                                                             //  new v.11.08
{
   int         nteeth = 20, Np = 6;
   int         tbase1, tbase2, twidth, tlength, tooth, tpos;
   int         ii, px, py, ww, ww2;
   double      delay = 2.0 * Np / ss_hh;                                   //  1 sec. + CPU time
   uint8       *pix3;
   
   twidth = ss_ww / nteeth;
   tlength = twidth;

   for (ii = 0; ii < ss_hh/2 - tlength/2; ii += Np)
   {
      tbase1 = ii;                                                         //  tooth base from top to middle
      tbase2 = ss_hh - tbase1 - 1;                                         //  tooth base from bottom to middle

      for (tooth = 0; tooth <= nteeth; tooth++)                            //  tooth first to last + 1
      {
         for (tpos = 0; tpos < tlength; tpos += Np)                        //  tooth position from base to point
         {
            ww = twidth * (tlength - tpos) / tlength;                      //  tooth width at scan line
            if (ww < 2) break;

            py = tbase1 + tpos;                                            //  top teeth scan line y
            px = twidth / 2 + tooth * twidth - ww / 2;                     //  scan line x to x + ww
            if (px < ss_ww) {
               pix3 = PXMpix8(ss_pxmnew,px,py);
               ww2 = ww;
               if (px + ww2 > ss_ww) ww2 = ss_ww - px;
               gdk_draw_rgb_image(drWin->window, gdkgc, px, py, ww2, Np, NODITHER, pix3, ss_ww*3);
            }

            py = tbase2 - tpos;                                            //  bottom teeth scan line y
            px = tooth * twidth - ww / 2;                                  //  scan line x to x + ww
            if (tooth == 0) {
               px = 0;                                                     //  leftmost tooth is half
               ww = ww / 2;
            }
            if (px < ss_ww) {
               pix3 = PXMpix8(ss_pxmnew,px,py);
               ww2 = ww;
               if (px + ww2 > ss_ww) ww2 = ss_ww - px;
               gdk_draw_rgb_image(drWin->window, gdkgc, px, py, ww2, Np, NODITHER, pix3, ss_ww*3);
            }
         }
      }
   
      zmainloop();
      zsleep(delay);
   }

   pix3 = PXMpix8(ss_pxmnew,0,0);
   gdk_draw_rgb_image(drWin->window, gdkgc, 0, 0, ss_ww, ss_hh, NODITHER, pix3, ss_ww * 3);
   return;
}


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

//  menu function, start synchronize files process manually

void m_syncfiles(GtkWidget *, cchar *)                                     //  v.11.11
{
   int      err, syncfd;

   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  edit function busy

   syncfd = global_lock(fsync_lock);                                       //  check if sync files running
   if (syncfd >= 0) global_unlock(syncfd);
   else {   
      zmessageACK(mWin,ZTX("Sync Files is already running"));              //  yes, do nothing
      menulock(0);
      return;
   }

   printf("spawn sync files subprocess \n");
   err = system("fotoxx -syncfiles manual &");                             //  spawn file sync process
   if (err) zmessageACK(mWin,"error: %s \n",wstrerror(err));

   menulock(0);
   return;
}


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

//  Synchronize files when fotoxx is installed for the first time or
//  when new image files have been created or moved outside fotoxx.
//  This is a spawned process running parallel to the user GUI process.
//  The main window with menus and toolbar is not available.

struct tag_index_rec {
   char        *file;                                                      //  image filespec
   char        *tags;                                                      //  image tags
   char        *comms;                                                     //  image comments
   char        *capt;                                                      //  image caption
   char        imagedate[12], filedate[16];                                //  image date, file date
   char        stars;                                                      //  image rating, '0' to '5'
   char        update;                                                     //  flag, update is needed
};
tag_index_rec   *xrec_old, *xrec_new;


int syncfiles_func(void *)                                                 //  v.11.11
{
   int    syncfiles_dialog_event(zdialog *zd, cchar *event);
   char * syncfiles_find(char *imagedirk, int &Finit);
   int    syncfiles_index_compare(cchar *rec1, cchar *rec2);
   int    filesync_thumbfile(char *imagefile);

   int            contx, syncfd, dosync, yn, err;
   cchar          *Fsyncreason, *ppc;
   char           *pp, text[200];
   char           *filespec, *thumbfile, *subdirk;
   char           imagefile[maxfcc], logrec[200];
   zdialog        *zd;
   int            zstat, fcount, flag;
   int            cc, fcc, dcc, Finit;
   FILE           *fid;
   struct stat    statb;

   cchar    *sync_message = ZTX("Run Tools > Synchronize Files so that gallery windows \n"
                                "will be fast and Search Images will work correctly. \n"
                                "You can view (not edit) images while synchronize runs.");

   cchar    *exifkeys[5] = { exif_date_key, iptc_tags_key, iptc_rating_key,
                             exif_comment_key, iptc_caption_key };

   zfuncs::F1_help_topic = "sync_files";
   
   printf("enter sync files process %d %d \n",Fautosync,Fmansync);

   syncfd = global_lock(fsync_lock);                                       //  obtain sync files lock
   if (syncfd < 0) {
      zmessage_post(0,5,"Sync Files is already running");
      gtk_main_quit();
      return 0;
   }

   err = system("which exiftool > /dev/null");
   if (err) {                                                              //  exiftool is required
      zmessageACK(0,Bexiftoolmissing);
      gtk_main_quit();
      return 0;
   }
   
   dosync = 0;
   Fsyncreason = "none";

   if (topdirk) {                                                          //  v.11.12.1
      err = stat(topdirk,&statb);
      if (err || ! S_ISDIR(statb.st_mode)) topdirk = 0;
   }

   if (! topdirk) {                                                        //  reasons for sync files
      Fsyncreason = ZTX("no top image directory is defined");
      dosync = 2;
   }
   else {
      printf("top image directory: %s \n",topdirk);
      err = stat(topdirk,&statb);
      if (err) Fsyncreason = ZTX("top image directory is invalid");
      if (err) dosync = 2;
   }

   err = stat(search_index_file,&statb);
   if (err) Fsyncreason = ZTX("no search index file is present");
   if (err) dosync = 2;

   if (! dosync)                                                           //  check for new image files    v.11.11
   {                                                                       //    since last session or sync
      sprintf(command,"find -L \"%s\" -type d -newermt \"%s\" ",
                                          topdirk,last_session);           //  use directory mod time       v.11.11.1
      printf("%s \n",command);
      contx = 0;
      while ((pp = command_output(contx,command,null))) {
         newfiles++;                                                       //  add to possible prior session count
         zfree(pp);
      }
      if (newfiles) {
         strncpy0(text,ZTX("new/modified files are present"),199);         //  v.11.11.1
         Fsyncreason = text;
         dosync = 1;
      }
   }
   
   if (dosync) printf("%s \n",Fsyncreason);
   else printf("%s \n",ZTX("no new files found"));

   if (! dosync && Fautosync) {                                            //  no need to sync and not manual req.
      gtk_main_quit();
      return 0;
   }

   if (dosync < 2 && ! Fmansync)                                           //  dialog not required, not manual req.
      goto start_sync;                                                     //  do auto sync


//  user dialog to request sync files and supply top image directory

sync_dialog:

   zd = zdialog_new(ZTX("Synchronize Files"),mWin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("Top Image Directory:"));
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"entry","topdirk","hb2","??","scc=40|space=5");
   zdialog_add_widget(zd,"button","browse","hb2",Bbrowse,"space=5");
   
   if (strNeq(Fsyncreason,"none")) {                                       //  show reason for file sync
      zdialog_add_widget(zd,"label","labwhy1","dialog",Fsyncreason,"space=3");
      zdialog_add_widget(zd,"label","labwhy2","dialog",sync_message,"space=3");
   }

   if (topdirk) zdialog_stuff(zd,"topdirk",topdirk);                       //  v.11.12.1
   zdialog_help(zd,"sync_files");

   zdialog_run(zd,syncfiles_dialog_event);
   zstat = zdialog_wait(zd);

   if (zstat != 1) {                                                       //  dialog canceled
      zdialog_free(zd);
      if (dosync < 2) {                                                    //  file sync is optional
         gtk_main_quit();
         return 0;
      }
      yn = zmessageYN(0,ZTX("file sync is necessary.\n"
                            "cancel anyway?"));                            //  v.12.01
      if (! yn) goto sync_dialog;
      gtk_main_quit();
      return 0;
   }

   pp = zmalloc(200);   
   zdialog_fetch(zd,"topdirk",pp,200);                                     //  get top directory
   zdialog_free(zd);
   
   err = stat(pp,&statb);                                                  //  check for validity
   if (err || ! S_ISDIR(statb.st_mode)) {
      zmessageACK(mWin,ZTX("top directory is invalid"));
      zfree(pp);
      goto sync_dialog;
   }

   if (topdirk) zfree(topdirk);
   topdirk = strdupz(pp,0,"topdirk");
   zfree(pp);
   
   cc = strlen(topdirk);                                                   //  remove trailing '/' if present
   if (topdirk[cc-1] == '/') topdirk[cc-1] = 0;                            //  v.11.12

   fid = fopen(topdirk_file,"w");                                          //  write top directory file
   if (fid) {
      fprintf(fid,"%s\n",topdirk);
      fclose(fid);
   }


start_sync:

//  look for orphan thumbnail files and delete them

   write_popup_text("open","Synchronize Files",500,300);                   //  popup window for log data
   write_popup_text("write","deleting orphan thumbnails");

   snprintf(command,ccc,"%s*.thumbnails/*.png",topdirk);                   //  find all /topdirk/.thumbnails/*.png
   fcount = 0;
   flag = 1;

   while ((thumbfile = (char *) SearchWild(command,flag)))                 //  next thumbnail file
   {
      pp = strrchr(thumbfile,'/');
      if (strnNeq(pp-12,"/.thumbnails/",13)) continue;
      fcc = strlen(pp) - 4;
      if (strNeq(pp+fcc,".png")) continue;

      strncpy0(imagefile,thumbfile,maxfcc);                                //  construct matching image file
      dcc = pp - 12 - thumbfile;
      if (dcc + fcc >= maxfcc) continue;
      memmove(imagefile+dcc,pp,fcc);
      imagefile[dcc+fcc] = 0;

      err = stat(imagefile,&statb);                                        //  look for image file
      if (! err) continue;                                                 //  found, keep thumbnail
      
      *pp = '/';
      remove(thumbfile);                                                   //  not found, delete thumbnail
      fcount++;                                                            //  count thumbnails deleted

      snprintf(logrec,199,"delete thumbnail: %s",thumbfile);
      write_popup_text("write",logrec);
   }

   snprintf(logrec,199,"deleted %d orphan thumbnails",fcount);
   write_popup_text("write",logrec);

//  look for image files without thumbnails and create the thumbnails

   snprintf(logrec,199,"creating missing and update stale thumbnails");
   write_popup_text("write",logrec);

   snprintf(command,ccc,"find \"%s\" -type d",topdirk);                    //  find directories under top directory
   contx = 0;
   fcount = 0;

   while ((subdirk = command_output(contx,command)))
   {
      pp = strrchr(subdirk,'/');
      if (pp && strEqu(pp,"/.thumbnails")) {                               //  /.../.thumbnails/ directory
         zfree(subdirk);
         continue;                                                         //  skip
      }

      Finit = 1;
      filespec = syncfiles_find(subdirk,Finit);                            //  get first image file

      while (filespec)                                                     //  loop all image files
      {
         if (image_file_type(filespec) == 2) 
         {
            if (filesync_thumbfile(filespec)) {
               snprintf(logrec,199,"create thumbnail: %s",filespec);
               write_popup_text("write",logrec);
               fcount++;                                                   //  count thumbnails created     v.11.11
            }
         }
         
         zfree(filespec);
         filespec = syncfiles_find(subdirk,Finit);                         //  next image file
      }

      zfree(subdirk);
   }

   snprintf(logrec,199,"created %d thumbnails",fcount);
   write_popup_text("write",logrec);

   //  Rebuild search index file.
   //  Process all image files within given top-level directory.
   //  Works incrementally and is very fast after the first run.

   snprintf(logrec,199,"rebuild search index");
   write_popup_text("write",logrec);

   char           **ppv, tagsbuff[tagrecl];
   char           *imagedate, *imagetags, *imagestars, *imagecomms, *imagecapt;
   int            Nold, Nnew, orec, nrec, comp;
   int            pcache, Ncache, nrec2;
   char           **ppcache;
   cchar          *Fcache[50];

   struct tm      bdt;

   fcount = MAXIMAGES;                                                     //  max. image files supported   v.12.01

   xrec_old = (tag_index_rec *) zmalloc(fcount * sizeof(tag_index_rec),"sync");
   xrec_new = (tag_index_rec *) zmalloc(fcount * sizeof(tag_index_rec),"sync");
   
   //  read current search index file and build "old list" of tags

   orec = Nold = 0;
   fid = 0;

   fid = fopen(search_index_file,"r");

   if (fid) 
   {
      while (true)                                                         //  read current search index records
      {
         pp = fgets_trim(tagsbuff,tagrecl,fid);                            //  next record
         if (! pp) break;

         if (strnEqu(pp,"file: ",6))                                       //  start new file entry
         {
            if (++Nold == fcount) zappcrash("too many image files");
            orec = Nold - 1;
            xrec_old[orec].file = strdupz(pp+6,0,"xrec_old.file");
            xrec_old[orec].imagedate[0] = 0;
            xrec_old[orec].filedate[0] = 0;
            xrec_old[orec].tags = 0;
            xrec_old[orec].stars = '0';
            xrec_old[orec].comms = 0;
            xrec_old[orec].capt = 0;
            xrec_old[orec].update = '0';
         }
         
         if (strnEqu(pp,"date: ",6))
         {
            ppc = strField(pp,' ',2);
            if (ppc) strncpy0(xrec_old[orec].imagedate,ppc,12);
            ppc = strField(pp,' ',3);
            if (ppc) strncpy0(xrec_old[orec].filedate,ppc,16);
         }
         
         if (strnEqu(pp,"tags: ",6))
            xrec_old[orec].tags = strdupz(pp+6,0,"sync");
         
         if (strnEqu(pp,"stars: ",7))
            xrec_old[orec].stars = *(pp+7);

         if (strnEqu(pp,"comms: ",7))
            xrec_old[orec].comms = strdupz(pp+7,0,"sync");

         if (strnEqu(pp,"capt: ",6))
            xrec_old[orec].capt = strdupz(pp+6,0,"sync");                  //  v.10.12
      }
      
      fclose(fid);
   }

   snprintf(logrec,199,"%d current index records found",Nold);
   write_popup_text("write",logrec);

   //  find all image files and create "new list" with no tags

   snprintf(logrec,199,"find all image files and build index records");
   write_popup_text("write",logrec);

   snprintf(command,ccc,"find \"%s\" -type d",topdirk);                    //  find directories under top directory
   contx = 0;
   Nnew = nrec = 0;

   while ((subdirk = command_output(contx,command)))
   {
      pp = strrchr(subdirk,'/');
      if (pp && strEqu(pp,"/.thumbnails"))                                 //  skip ./thumbnails directories
      {
         zfree(subdirk);
         continue;
      }

      Finit = 1;
      filespec = syncfiles_find(subdirk,Finit);                            //  get first image file

      while (filespec)                                                     //  loop all image files
      {
         if (image_file_type(filespec) == 2)                               //  construct new tag record
         {
            err = stat(filespec,&statb);
            if (err) continue;
            if (++Nnew == fcount) zappcrash("too many image files");
            nrec = Nnew - 1;
            xrec_new[nrec].file = strdupz(filespec,0,"sync");              //  image filespec
            xrec_new[nrec].imagedate[0] = 0;                               //  image date = empty
            gmtime_r(&statb.st_mtime,&bdt);                                //  file date = yyyymmddhhmmss
            snprintf(xrec_new[nrec].filedate,16,"%04d%02d%02d%02d%02d%02d",
                     bdt.tm_year + 1900, bdt.tm_mon + 1, bdt.tm_mday,
                     bdt.tm_hour, bdt.tm_min, bdt.tm_sec);
            xrec_new[nrec].tags = 0;                                       //  tags = empty
            xrec_new[nrec].stars = '0';                                    //  stars = '0'
            xrec_new[nrec].comms = 0;                                      //  comments = empty
            xrec_new[nrec].capt = 0;                                       //  caption = empty        v.10.12
         }
      
         zfree(filespec);
         filespec = syncfiles_find(subdirk,Finit);                         //  next image file
      }

      zfree(subdirk);
   }

   snprintf(logrec,199,"found %d image files",Nnew);
   write_popup_text("write",logrec);

   //  merge and compare lists
   //  if filespecs match and have the same date, then old tags are OK

   snprintf(logrec,199,"merging old and new index records");
   write_popup_text("write",logrec);

   HeapSort((char *) xrec_old,sizeof(tag_index_rec),Nold,syncfiles_index_compare);
   HeapSort((char *) xrec_new,sizeof(tag_index_rec),Nnew,syncfiles_index_compare);

   for (orec = nrec = 0; nrec < Nnew; )
   {
      xrec_new[nrec].update = '1';                                         //  assume update is needed

      if (orec == Nold) comp = +1;
      else comp = strcmp(xrec_old[orec].file, xrec_new[nrec].file);        //  compare filespecs
      
      if (comp > 0) nrec++;
      else if (comp < 0) orec++;
      else                                                                 //  matching filespecs
      {
         if (strEqu(xrec_new[nrec].filedate, xrec_old[orec].filedate))     //  and matching file dates
         {                                                                 //  copy data from old to new
            strncpy0(xrec_new[nrec].imagedate,xrec_old[orec].imagedate,12);
            xrec_new[nrec].tags = xrec_old[orec].tags;
            xrec_old[orec].tags = 0;
            xrec_new[nrec].stars = xrec_old[orec].stars;
            xrec_new[nrec].comms = xrec_old[orec].comms;
            xrec_old[orec].comms = 0;
            xrec_new[nrec].capt = xrec_old[orec].capt;
            xrec_old[orec].capt = 0;
            xrec_new[nrec].update = '0';                                   //  update is not needed
         }
         nrec++;
         orec++;
      }
   }

   for (orec = 0; orec < Nold; orec++)                                     //  release old list memory
   {
      zfree(xrec_old[orec].file);
      if (xrec_old[orec].tags) zfree(xrec_old[orec].tags);
      if (xrec_old[orec].capt) zfree(xrec_old[orec].capt);                 //  memory leak      v.11.09
      if (xrec_old[orec].comms) zfree(xrec_old[orec].comms);
   }

   zfree(xrec_old);
   xrec_old = 0;

   //  process entries needing update in new index list
   //  get updated metadata from image file EXIF/IPTC data
   
   snprintf(logrec,199,"updating index records");
   write_popup_text("write",logrec);

   Ncache = 0;                                                             //  cache empty
   pcache = 0;
   ppcache = 0;

   for (fcount = nrec = 0; nrec < Nnew; nrec++)
   {
      if (xrec_new[nrec].update == '0') continue;                          //  no update needed
      fcount++;                                                            //  running count of updates
      
      if (! Ncache)                                                        //  cache empty            v.12.01
      {
         for (nrec2 = nrec; Ncache < 50 && nrec2 < Nnew; nrec2++)
         {
            if (xrec_new[nrec2].update != '0') {                           //  add next 50 files to cache
               Fcache[Ncache] = xrec_new[nrec2].file;
               Ncache++;
            }
         }
         
         ppcache = info_getN(Fcache,Ncache,exifkeys,5);                    //  get metadata for cached files
         pcache = 0;                                                       //  1st cache position
      }

      ppv = ppcache + 5 * pcache;                                          //  get key values for next file

      Ncache--;                                                            //  index to next cache data
      pcache++;

      imagedate = ppv[0];
      imagetags = ppv[1];
      imagestars = ppv[2];
      imagecomms = ppv[3];
      imagecapt = ppv[4];
      
      if (imagedate && strlen(imagedate) > 3) {                            //  image date             v.11.10
         if (strlen(imagedate) > 9) imagedate[10] = 0;                     //  truncate to yyyy:mm:dd
         strcpy(xrec_new[nrec].imagedate,imagedate);
      }
      else strcpy(xrec_new[nrec].imagedate,"null");
      
      if (imagetags && strlen(imagetags))                                  //  image tags
         xrec_new[nrec].tags = strdupz(imagetags,0,"sync");
      else xrec_new[nrec].tags = strdupz("null"tagdelim2,0,"sync");        //  v.11.02

      if (imagestars && strlen(imagestars))                                //  image rating
         xrec_new[nrec].stars = *imagestars;

      if (imagecomms && strlen(imagecomms))                                //  image comments
         xrec_new[nrec].comms = strdupz(imagecomms,0,"sync");
      else xrec_new[nrec].comms = strdupz("null",0,"sync");

      if (imagecapt && strlen(imagecapt))                                  //  image caption          v.10.12
         xrec_new[nrec].capt = strdupz(imagecapt,0,"sync");
      else xrec_new[nrec].capt = strdupz("null",0,"sync");

      if (imagedate) zfree(imagedate);
      if (imagetags) zfree(imagetags);
      if (imagestars) zfree(imagestars);
      if (imagecomms) zfree(imagecomms);
      if (imagecapt) zfree(imagecapt);

      snprintf(logrec,199,"update index: %s",xrec_new[nrec].file);
      write_popup_text("write",logrec);
   }
   
   fid = fopen(search_index_file,"w");                                     //  write new search index file
   if (! fid) zappcrash("cannot write tags file");                         //    with merged data

   for (nrec = 0; nrec < Nnew; nrec++)
   {
      fprintf(fid,"file: %s""\n",xrec_new[nrec].file);
      fprintf(fid,"date: %s  %s""\n",xrec_new[nrec].imagedate, xrec_new[nrec].filedate);
      fprintf(fid,"tags: %s""\n",xrec_new[nrec].tags);
      fprintf(fid,"stars: %c""\n",xrec_new[nrec].stars);
      fprintf(fid,"comms: %s""\n",xrec_new[nrec].comms);
      fprintf(fid,"capt: %s""\n",xrec_new[nrec].capt);
      fprintf(fid,"\n");
   }
   
   fclose(fid);
   
   for (nrec = 0; nrec < Nnew; nrec++)                                     //  release new list memory
   {
      zfree(xrec_new[nrec].file);
      if (xrec_new[nrec].tags) zfree(xrec_new[nrec].tags);
      if (xrec_new[nrec].capt) zfree(xrec_new[nrec].capt);                 //  memory leak      v.11.09
      if (xrec_new[nrec].comms) zfree(xrec_new[nrec].comms);
   }

   zfree(xrec_new);
   xrec_new = 0;

   snprintf(logrec,199,"%d image files updated",fcount);
   write_popup_text("write",logrec);

   write_popup_text("write","COMPLETED");
   printf("sync files subprocess completed \n");

   global_unlock(syncfd);                                                  //  release file sync lock
   
   while (zfuncs::open_popup_windows) {                                    //  wait for popup window to be closed
      zmainloop();
      zsleep(0.1);
   }

   gtk_main_quit();                                                        //  gone forever
   return 0;
}


//  dialog event function - file chooser for top image directory

int syncfiles_dialog_event(zdialog *zd, cchar *event)
{
   char     *pp;
   
   if (! strEqu(event,"browse")) return 0;
   pp = zgetfile1(ZTX("Select top image directory"),"folder",topdirk);     //  get topmost directory
   if (! pp) return 0;
   zdialog_stuff(zd,"topdirk",pp);
   zfree(pp);
   return 0;
}


//  find image files in a directory, return one per call                   //  v.11.11

char * syncfiles_find(char *imagedirk, int &Finit)
{
   char           *file;
   cchar          *findcommand = "find \"%s\" -maxdepth 1 -type f";
   static int     contx, ftyp;

   if (Finit) {
      Finit = 0;
      contx = 0;
   }
   
   while ((file = command_output(contx,findcommand,imagedirk)))            //  find next file in directory
   {
      if (! file) return 0;                                                //  EOL
      ftyp = image_file_type(file);
      if (ftyp == 2) return file;                                          //  supported image file type
      zfree(file);
      continue;
   }
   
   return 0;
}


//  Find the thumbnail file for the given image file.
//  If missing or stale, add or update in /.thumbnails directory.
//  Returns 0 if thumbnail file was OK, 1 if it was created.

int filesync_thumbfile(char *imagefile)                                    //  v.11.11
{
   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   char              *pfile, *bfile, *thumbfile;
   int               err, sizew, sizeh;
   struct stat       statf, statb;
   
   err = stat(imagefile,&statf);                                           //  stat the image file
   if (err) return 0;
   pfile = strrchr(imagefile,'/');                                         //  get .../filename.xxx
   if (! pfile) return 0;
   thumbfile = strdupz(imagefile,20,"image_thumbfile");                    //  construct thumbnail file
   bfile = thumbfile + (pfile - imagefile);                                //    .../.thumbnails/filename.xxx.png
   strcpy(bfile,"/.thumbnails");
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   err = stat(thumbfile,&statb);                                           //  stat the thumbnail file
   if (! err && statb.st_mtime >= statf.st_mtime) return 0;                //  found and up to date

   *bfile = 0;
   err = stat(thumbfile,&statb);
   if (err) err = mkdir(thumbfile,0751);                                   //  create .thumbnails directory
   if (err) return 0;

   *bfile = *pfile;
   sizew = sizeh = image_navi::thumbfilesize;                              //  create thumbnail pixbuf
   thumbpxb = gdk_pixbuf_new_from_file_at_size(imagefile,sizew,sizeh,&gerror);
   if (! thumbpxb) {
      printf("gdk_pixbuf_new error: %s \n",gerror->message);               //  diagnose error    v.3.3
      return 0;
   }
   gdk_pixbuf_save(thumbpxb,thumbfile,"png",&gerror,null);                 //  save in /.thumbnails/ directory
   g_object_unref(thumbpxb);

   return 1;
}


//  sort compare function - compare tag record filespecs and return
//   <0 | 0 | >0   for   file1 < | == | > file2

int syncfiles_index_compare(cchar *rec1, cchar *rec2)
{
   char * file1 = ((tag_index_rec *) rec1)->file;
   char * file2 = ((tag_index_rec *) rec2)->file;
   return strcmp(file1,file2);
}


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

//  Show RGB values for 1-5 pixels selected with mouse-clicks.

void  show_RGB_mousefunc();
int   show_RGB_timefunc(void *);

zdialog     *RGBSzd;
int         RGBSpixel[5][2];                                               //  last 0-5 pixels clicked
int         RGBSnpix;                                                      //  count of pixels
double      RGBStime;                                                      //  last click time
int         RGBSmetric = 1;                                                //  1/2/3 = /RGB/EV/OD
int         RGBSdelta = 0;                                                 //  abs/delta mode
int         RGBSlabels = 0;                                                //  pixel labels on/off


void m_show_RGB(GtkWidget *, cchar *menu)                                  //  rewritten     v.11.07
{
   int   show_RGB_event(zdialog *zd, cchar *event);

   PangoFontDescription    *pfontdesc;
   GtkWidget   *widget;
   cchar       *mess = ZTX("Click image to select pixels.");
   cchar       *header = " Pixel          Red     Green   Blue";

   zfuncs::F1_help_topic = "show_RGB";

   if (! Fpxm8) return;                                                    //  no image file
   RGBSnpix = 0;                                                           //  no pixels yet
   pfontdesc = pango_font_description_from_string("Monospace 9");          //  monospace font

/***
   
   Click image to select pixels.
   [x] delta  [x] labels
   Metric: (o) RGB  (o) EV  (o) OD
   Pixel         Red    Green  Blue
   A xxxx xxxx   xxxxx  xxxxx  xxxxx
   B xxxx xxxx   xxxxx  xxxxx  xxxxx
   C xxxx xxxx   xxxxx  xxxxx  xxxxx
   D xxxx xxxx   xxxxx  xxxxx  xxxxx
   E xxxx xxxx   xxxxx  xxxxx  xxxxx

                            [done]
***/

   if (RGBSzd) zdialog_free(RGBSzd);                                       //  delete previous if any
   zdialog *zd = zdialog_new(ZTX("Show RGB"),mWin,Bdone,null);
   RGBSzd = zd;

   zdialog_add_widget(zd,"hbox","hbmess","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmess","hbmess",mess,"space=5");

   zdialog_add_widget(zd,"hbox","hbmym","dialog");
   zdialog_add_widget(zd,"check","delta","hbmym","delta","space=8");
   zdialog_add_widget(zd,"check","labels","hbmym","labels","space=8");

   if (RGBSdelta) zdialog_stuff(zd,"delta",1);                             //  abs/delta mode       v.11.08

   zdialog_add_widget(zd,"hbox","hbmetr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmetr","hbmetr","Metric:","space=5");
   zdialog_add_widget(zd,"radio","radRGB","hbmetr","RGB","space=3");
   zdialog_add_widget(zd,"radio","radEV","hbmetr","EV","space=3");
   zdialog_add_widget(zd,"radio","radOD","hbmetr","OD","space=3");
   
   if (RGBSmetric == 1) zdialog_stuff(zd,"radRGB",1);                      //  get which metric to use
   if (RGBSmetric == 2) zdialog_stuff(zd,"radEV",1);
   if (RGBSmetric == 3) zdialog_stuff(zd,"radOD",1);

   zdialog_add_widget(zd,"vbox","vbdat","dialog");                         //  vbox for current pixel values
   zdialog_add_widget(zd,"hbox","hbpix","vbdat");
   zdialog_add_widget(zd,"label","labheader","hbpix",header);              //  Pixel        Red    Green  Blue

   zdialog_add_widget(zd,"hbox","hb1","vbdat");
   zdialog_add_widget(zd,"label","pix1","hb1");                            //  A xxxx yyyy  rrr.r  ggg.g  bbb.b
   zdialog_add_widget(zd,"hbox","hb2","vbdat");
   zdialog_add_widget(zd,"label","pix2","hb2");
   zdialog_add_widget(zd,"hbox","hb3","vbdat");
   zdialog_add_widget(zd,"label","pix3","hb3");
   zdialog_add_widget(zd,"hbox","hb4","vbdat");
   zdialog_add_widget(zd,"label","pix4","hb4");
   zdialog_add_widget(zd,"hbox","hb5","vbdat");
   zdialog_add_widget(zd,"label","pix5","hb5");

   widget = zdialog_widget(zd,"labheader"); 
   gtk_widget_modify_font(widget,pfontdesc);                               //  use monospace font
   widget = zdialog_widget(zd,"pix1");
   gtk_widget_modify_font(widget,pfontdesc);
   widget = zdialog_widget(zd,"pix2");
   gtk_widget_modify_font(widget,pfontdesc);
   widget = zdialog_widget(zd,"pix3");
   gtk_widget_modify_font(widget,pfontdesc);
   widget = zdialog_widget(zd,"pix4");
   gtk_widget_modify_font(widget,pfontdesc);
   widget = zdialog_widget(zd,"pix5");
   gtk_widget_modify_font(widget,pfontdesc);

   zdialog_help(zd,"show_RGB");                                            //  zdialog help topic     v.11.08
   zdialog_run(zd,show_RGB_event,"save");                                  //  run dialog
   takeMouse(zd,show_RGB_mousefunc,dragcursor);                            //  connect mouse function
   g_timeout_add(300,show_RGB_timefunc,0);                                 //  start timer function, 300 ms

   return;
}


//  dialog event function

int show_RGB_event(zdialog *zd, cchar *event)
{
   int            button, ii, px, py;
   static char    label[5][4] = { " A ", " B ", " C ", " D ", " E " };
   
   if (zd->zstat) {
      freeMouse();                                                         //  disconnect mouse function
      zdialog_free(RGBSzd);                                                //  kill dialog
      RGBSzd = 0;
      erase_toptext(102);
      mwpaint2();
   }

   if (strEqu(event,"focus"))                                              //  toggle mouse capture
      takeMouse(zd,show_RGB_mousefunc,dragcursor);                         //  connect mouse function
   
   if (strEqu(event,"delta")) {                                            //  set absolute/delta mode
      zdialog_fetch(zd,"delta",RGBSdelta);
      if (RGBSdelta && ! E3pxm16) {
         RGBSdelta = 0;                                                    //  block delta mode if no edit underway
         zdialog_stuff(zd,"delta",0);                                      //  v.11.09
      }
   }
   
   if (strEqu(event,"labels")) {                                           //  get labels on/off      v.11.09
      zdialog_fetch(zd,"labels",RGBSlabels);

      erase_toptext(102);
      
      if (RGBSlabels) {                                                    //  labels off to labels on
         for (ii = 0; ii < RGBSnpix; ii++)                                 //  show pixel labels on image
         {                                                                 //  v.11.08
            px = RGBSpixel[ii][0];
            py = RGBSpixel[ii][1];
            add_toptext(102,px,py,label[ii],"Sans 8");
         }
      }
      
      mwpaint2();
   }
   
   if (strEqu(event,"radRGB")) {                                           //  metric = RGB
      zdialog_fetch(zd,event,button);
      if (button) RGBSmetric = 1;
   }
      
   if (strEqu(event,"radEV")) {                                            //  metric = EV
      zdialog_fetch(zd,event,button);
      if (button) RGBSmetric = 2;
   }
      
   if (strEqu(event,"radOD")) {                                            //  metric = OD
      zdialog_fetch(zd,event,button);
      if (button) RGBSmetric = 3;
   }
      
   return 0;
}


//  mouse function

void show_RGB_mousefunc()                                                  //  mouse function
{
   int            ii, px, py;
   static char    label[5][4] = { " A ", " B ", " C ", " D ", " E " };

   if (! LMclick) return;

   LMclick = 0;
   RGBStime = get_seconds();                                               //  mark time of pixel click

   if (RGBSnpix == 5) {                                                    //  if all 5 positions filled,
      for (ii = 1; ii < 5; ii++) {                                         //    remove first (oldest) and
         RGBSpixel[ii-1][0] = RGBSpixel[ii][0];                            //      push the rest back
         RGBSpixel[ii-1][1] = RGBSpixel[ii][1];
      }
      RGBSnpix = 4;                                                        //  position for newest
   }
   
   ii = RGBSnpix;                                                          //  next position to fill
   RGBSpixel[ii][0] = Mxclick;                                             //  save newest pixel
   RGBSpixel[ii][1] = Myclick;
   RGBSnpix++;
   
   erase_toptext(102);
   
   if (RGBSlabels) {                                                       //  v.11.09
      for (ii = 0; ii < RGBSnpix; ii++)                                    //  show pixel labels on image
      {                                                                    //  v.11.08
         px = RGBSpixel[ii][0];
         py = RGBSpixel[ii][1];
         add_toptext(102,px,py,label[ii],"Sans 8");
      }
   }
   
   mwpaint2();
   return;
}


//  timer function - continuously display RGB values for selected pixels

int show_RGB_timefunc(void *arg)                                           //  up to 5 pixels, live update
{
   int         ii, px, py, delta;
   double      red1, green1, blue1;
   double      red3, green3, blue3;
   char        text[100], pixx[8] = "pixx";
   uint8       *ppix8;
   uint16      *ppix16a, *ppix16b;
   double      c1 = 100.0 / 256.0;
   
   #define ODfunc(rgb) (2.0 - log10(c1 * rgb))                             //  RGB units (1-256) to OD units
   #define EVfunc(rgb) (log2(rgb) - 7)                                     //  RGB units to EV (128 == 0 EV)

   if (! RGBSzd) return 1;                                                 //  user quit, stop timer
   if (! RGBSnpix) return 1;                                               //  no pixels clicked yet
   
   if (RGBSdelta && E3pxm16) delta = 1;                                    //  delta mode only if edit underway
   else delta = 0;                                                         //  v.11.08
   
   red1 = green1 = blue1 = 0;
   
   if (curr_image_time > RGBStime) {                                       //  image time later than click time
      RGBSnpix = 0;                                                        //  data is no longer valid
      erase_toptext(102);
      mwpaint2();
   }

   for (ii = 0; ii < 5; ii++)                                              //  loop positons 0 to 4
   {
      pixx[3] = '1' + ii;                                                  //  widget names "pix1" ... "pix5"

      px = RGBSpixel[ii][0];                                               //  next pixel to report
      py = RGBSpixel[ii][1];
      if (ii >= RGBSnpix) {                                                //  no pixel there yet
         zdialog_stuff(RGBSzd,pixx,"");                                    //  blank report line
         continue;
      }
      
      if (E3pxm16) {                                                       //  use current image being edited
         if (px < 0 || px > E3ww-1 ||                                      //  outside image area
             py < 0 || py > E3hh-1) return 1;
         ppix16a = PXMpix(E3pxm16,px,py);
         red3 = ppix16a[0] / 256.0;                                        //  "after" image E3
         green3 = ppix16a[1] / 256.0;
         blue3 = ppix16a[2] / 256.0;
         
         if (delta) {                                                      //  delta RGB for ongoing edited image
            ppix16b = PXMpix(E1pxm16,px,py);                               //  "before" image E1
            red1 = ppix16b[0] / 256.0;                                     //  v.11.08
            green1 = ppix16b[1] / 256.0;
            blue1 = ppix16b[2] / 256.0;
         }
      }

      else if (Fpxm16) {                                                   //  use finished edited image
         if (px < 0 || px > Fww-1 || 
             py < 0 || py > Fhh-1) return 1;
         ppix16a = PXMpix(Fpxm16,px,py);
         red3 = ppix16a[0] / 256.0;
         green3 = ppix16a[1] / 256.0;
         blue3 = ppix16a[2] / 256.0;
      }

      else  {                                                              //  use 8 bpc image
         if (px < 0 || px > Fww-1 || 
             py < 0 || py > Fhh-1) return 1;
         ppix8 = (uint8 *) Fpxm8->bmp + (py * Fww + px) * 3;
         red3 = ppix8[0];
         green3 = ppix8[1];
         blue3 = ppix8[2];
      }
      
      sprintf(text," %c %4d %4d  ",'A'+ii,px,py);                          //  format pixel " A xxxx yyyy"
      
      if (RGBSmetric == 1) {                                               //  output RGB values
         if (delta) {
            red3 -= red1;                                                  //  delta RGB
            green3 -= green1;
            blue3 -= blue1;
         }
         sprintf(text+12,"   %6.2f  %6.2f  %6.2f ",red3,green3,blue3);     //  xxx.xx
      }

      if (RGBSmetric == 2) {                                               //  output EV values       v.11.08
         red3 = EVfunc(red3);
         green3 = EVfunc(green3);
         blue3 = EVfunc(blue3);
         if (delta) {
            red3 -= EVfunc(red1);                                          //  delta EV
            green3 -= EVfunc(green1);
            blue3 -= EVfunc(blue1);
         }
         sprintf(text+12,"   %6.3f  %6.3f  %6.3f ",red3,green3,blue3);     //  x.xxx
      }

      if (RGBSmetric == 3) {                                               //  output OD values       v.11.08
         red3 = ODfunc(red3); 
         green3 = ODfunc(green3);
         blue3 = ODfunc(blue3);
         if (delta) {
            red3 -= ODfunc(red1);                                          //  delta OD
            green3 -= ODfunc(green1);
            blue3 -= ODfunc(blue1);
         }
         sprintf(text+12,"   %6.3f  %6.3f  %6.3f ",red3,green3,blue3);     //  x.xxx
      }

      zdialog_stuff(RGBSzd,pixx,text);                                     //  pixel and RGB values >> label
   }

   return 1;
}


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

//  setup x and y grid lines - count/spacing, enable/disable, offsets

void m_gridlines(GtkWidget *, cchar *)
{
   int gridlines_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   
   zfuncs::F1_help_topic = "grid_lines";                                   //  v.10.8

   zd = zdialog_new(ZTX("Grid Lines"),mWin,Bdone,Bcancel,null);
   
   zdialog_add_widget(zd,"hbox","hb0","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbspace","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb3","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb0",0,"homog|space=5");

   zdialog_add_widget(zd,"label","lab1x","vb1",ZTX("x-spacing"));
   zdialog_add_widget(zd,"label","lab2x","vb1",ZTX("x-count"));
   zdialog_add_widget(zd,"label","lab4x","vb1",ZTX("x-enable"));

   zdialog_add_widget(zd,"spin","spacex","vb2","10|200|1|50");
   zdialog_add_widget(zd,"spin","countx","vb2","0|100|1|0");
   zdialog_add_widget(zd,"check","enablex","vb2",0);

   zdialog_add_widget(zd,"label","lab1y","vb3",ZTX("y-spacing"));
   zdialog_add_widget(zd,"label","lab2y","vb3",ZTX("y-count"));
   zdialog_add_widget(zd,"label","lab4y","vb3",ZTX("y-enable"));

   zdialog_add_widget(zd,"spin","spacey","vb4","10|200|1|50");
   zdialog_add_widget(zd,"spin","county","vb4","0|100|1|0");
   zdialog_add_widget(zd,"check","enabley","vb4",0);

   zdialog_add_widget(zd,"hbox","hboffx","dialog");
   zdialog_add_widget(zd,"label","lab3x","hboffx",ZTX("x-offset"),"space=7");
   zdialog_add_widget(zd,"hscale","offsetx","hboffx","-100|100|1|0","expand");

   zdialog_add_widget(zd,"hbox","hboffy","dialog");
   zdialog_add_widget(zd,"label","lab3y","hboffy",ZTX("y-offset"),"space=7");
   zdialog_add_widget(zd,"hscale","offsety","hboffy","-100|100|1|0","expand");

   zdialog_stuff(zd,"spacex",gridspace[0]);
   zdialog_stuff(zd,"spacey",gridspace[1]);
   zdialog_stuff(zd,"countx",gridcount[0]);
   zdialog_stuff(zd,"county",gridcount[1]);
   zdialog_stuff(zd,"offsetx",gridoffset[0]);
   zdialog_stuff(zd,"offsety",gridoffset[1]);
   zdialog_stuff(zd,"enablex",gridon[0]);
   zdialog_stuff(zd,"enabley",gridon[1]);

   if (gridon[0] || gridon[1]) Fgrid = 1;
   mwpaint2();

   zdialog_help(zd,"grid_lines");                                          //  zdialog help topic     v.11.08
   zdialog_run(zd,gridlines_dialog_event);
   zdialog_wait(zd);
   return;
}


//  dialog event function

int gridlines_dialog_event(zdialog *zd, cchar *event)
{
   int      zstat;

   if (zd->zstat) {
      zstat = zd->zstat;
      if (zstat != 1) Fgrid = gridon[0] = gridon[1] = 0;                   //  cancel, grid lines off    v.11.11
      zdialog_free(zd);
      mwpaint2();
      return 0;
   }

   if (strEqu(event,"enablex"))                                            //  x/y grid enable or disable
      zdialog_fetch(zd,"enablex",gridon[0]);
   
   if (strEqu(event,"enabley")) 
      zdialog_fetch(zd,"enabley",gridon[1]);
   
   if (strEqu(event,"spacex"))                                             //  x/y grid spacing (if counts == 0)
      zdialog_fetch(zd,"spacex",gridspace[0]);

   if (strEqu(event,"spacey")) 
      zdialog_fetch(zd,"spacey",gridspace[1]);

   if (strEqu(event,"countx"))                                             //  x/y grid line counts
      zdialog_fetch(zd,"countx",gridcount[0]);

   if (strEqu(event,"county")) 
      zdialog_fetch(zd,"county",gridcount[1]);

   if (strEqu(event,"offsetx"))                                            //  x/y grid starting offsets
      zdialog_fetch(zd,"offsetx",gridoffset[0]);

   if (strEqu(event,"offsety")) 
      zdialog_fetch(zd,"offsety",gridoffset[1]);
   
   if (gridon[0] || gridon[1]) Fgrid = 1;                                  //  if either grid enabled, show grid
   else Fgrid = 0;

   mwpaint2();
   return 0;
}


//  load or save grid settings from or to external int array

void load_grid(int *griddata)                                              //  v.11.11
{
   Fgrid = griddata[0];
   gridon[0] = griddata[1];
   gridon[1] = griddata[2];
   gridspace[0] = griddata[3];
   gridspace[1] = griddata[4];
   gridcount[0] = griddata[5];
   gridcount[1] = griddata[6];
   if (! gridcount[0] && ! gridspace[0]) gridcount[0] = 5;                 //  if never set, use 5 lines
   if (! gridcount[1] && ! gridspace[1]) gridcount[1] = 5;
   mwpaint2();
   return;
}

void save_grid(int *griddata)
{
   griddata[0] = Fgrid;
   griddata[1] = gridon[0];
   griddata[2] = gridon[1];
   griddata[3] = gridspace[0];
   griddata[4] = gridspace[1];
   griddata[5] = gridcount[0];
   griddata[6] = gridcount[1];
   return;
}


//  toggle grid lines on or off
//  action: 0 = off, 1 = on, 2 = toggle: on > off, off > on

void toggle_grid(int action)                                               //  v.11.11
{
   if (action == 0) Fgrid = 0;                                             //  grid off
   if (action == 1) Fgrid = 1;                                             //  grid on
   if (action == 2) Fgrid = 1 - Fgrid;                                     //  toggle grid
   
   if (Fgrid && ! gridon[0] && ! gridon[1])                                //  if grid on and x/y both off,
      gridon[0] = gridon[1] = 1;                                           //    set both grids on

   mwpaint2();
   return;
}


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

//  burn images to CD/DVD  

void  m_burn(GtkWidget *, cchar *)
{
   int      ii, cc, err;
   char     **filelist, *imagefile, *bcommand;

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  lock menus
   
   zfuncs::F1_help_topic = "burn";                                         //  v.10.8

   filelist = image_gallery_getfiles(0,mWin);                              //  get list of files to burn   v.10.9
   if (! filelist) {
      menulock(0);
      return;
   }
   
   cc = 0;
   for (ii = 0; filelist[ii]; ii++)                                        //  get memory for brasero command line
      cc += strlen(filelist[ii]) + 4;

   bcommand = zmalloc(cc+20,"brasero");
   strcpy(bcommand,"brasero");
   cc = strlen(bcommand);

   for (ii = 0; filelist[ii]; ii++)                                        //  copy files to command line
   {
      imagefile = filelist[ii];
      strcpy(bcommand+cc," \"");
      cc += 2;
      strcpy(bcommand+cc,imagefile);
      cc += strlen(imagefile);
      strcpy(bcommand+cc,"\"");
      cc += 1;
      zfree(imagefile);
   }
   
   zfree(filelist);

   strcat(bcommand," &");                                                  //  brasero command in background
   err = system(bcommand);
   if (err) zmessageACK(mWin,"error: %s",wstrerror(err));
   zfree(bcommand);

   menulock(0);
   return;
}


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

//  menu function
//  resize selected images and send to preferred e-mail program

void m_email(GtkWidget *, cchar *)                                         //  new v.10.10
{
   int   email_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;                                                        //  email dialog

   zfuncs::F1_help_topic = "e-mail";

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  lock menus

   //  [select files]  N files selected
   //  max. width   [____]
   //  max. height  [____]
   //
   //                   [proceed]  [cancel]

   zd = zdialog_new(ZTX("E-mail Images"),mWin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",ZTX("0 files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hbwh","dialog","space=10");
   zdialog_add_widget(zd,"vbox","vbwh1","hbwh",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbwh2","hbwh",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labw","vbwh1",ZTX("max. width"));
   zdialog_add_widget(zd,"label","labh","vbwh1",ZTX("max. height"));
   zdialog_add_widget(zd,"entry","maxw","vbwh2","1000","scc=5");
   zdialog_add_widget(zd,"entry","maxh","vbwh2","700","scc=5");

   zdialog_stuff(zd,"maxw",emailsize[0]);
   zdialog_stuff(zd,"maxh",emailsize[1]);
   
   zdialog_help(zd,"e-mail");                                              //  zdialog help topic     v.11.08
   zdialog_run(zd,email_dialog_event);                                     //  run dialog
   zdialog_wait(zd);                                                       //  wait for completion

   menulock(0);
   return;
}


//  dialog event and completion callback function

int email_dialog_event(zdialog *zd, cchar *event)
{
   static char    **flist = 0;
   char           countmess[50], tempdir[100], sequence[8];
   int            maxw, maxh, err, fcc, ww, hh;
   int            ii, jj, kk;
   char           *pfile, *pext, *oldfile, *newfile;
   double         scale, wscale, hscale;
   PXM            *pxmin, *pxmout;

   if (strEqu(event,"files"))                                              //  select images to resize
   {
      if (flist) {                                                         //  free prior list
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
      }
      
      flist = image_gallery_getfiles(0,mWin);                              //  get list of files to resize   v.10.9

      if (flist)                                                           //  count files selected
         for (ii = 0; flist[ii]; ii++);
      else ii = 0;

      snprintf(countmess,50,ZTX("%d files selected"),ii);                  //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }
   
   if (! zd->zstat) return 0;                                              //  dialog still busy

   if (zd->zstat != 1) goto cleanup;                                       //  dialog canceled
   
   if (! flist) {                                                          //  no files selected
      zd->zstat = 0;                                                       //  keep dialog active
      return 0;
   }

   zdialog_fetch(zd,"maxw",maxw);                                          //  new max width
   zdialog_fetch(zd,"maxh",maxh);                                          //  new max height

   if (maxw < 20 || maxh < 20) {
      zmessageACK(mWin,ZTX("max. size %d x %d is not reasonable"),maxw,maxh);
      zd->zstat = 0;
      return 0;
   }

   *tempdir = 0;                                                           //  temp dirk: /tmp/<user>/fotoxx/email
   strncatv(tempdir,99,"/tmp/",getenv("USER"),"/fotoxx/email",null);       //  v.11.09

   sprintf(command,"mkdir -p %s",tempdir);                                 //  (re)create directory
   err = system(command);
   if (err) {
      zmessageACK(mWin,"%s",strerror(err));
      goto cleanup;
   }

   sprintf(command,"rm -f %s/*.jpg",tempdir);                              //  purge prior file list
   err = system(command);
   
   write_popup_text("open","preparing files",500,200,mWin);                //  status monitor popup window

   strcpy(command,"xdg-email");                                            //  e-mail command: xdg-email

   for (ii = 0; flist[ii]; ii++)                                           //  loop selected files
   {
      oldfile = flist[ii];
      pfile = strrchr(oldfile,'/');                                        //  /filename.ext
      if (! pfile) continue;

      fcc = strlen(pfile);
      newfile = strdupz(tempdir,fcc+10,"email");                           //  add extra space for .nn and .ext
      strcat(newfile,pfile);                                               //  /tmp/<user>/fotoxx/filename.ext
      pfile = strrchr(newfile,'/');

      for (jj = kk = 1; pfile[jj]; jj++, kk++)                             //  remove special characters
      {                                                                    //   (xdg-email substitutes %nn
         pfile[kk] = pfile[jj];                                            //     and thunderbird falls over)
         if (pfile[jj] < 0) continue;                                      //  UTF-8 multibyte characters OK
         if (pfile[jj] == '.') continue;                                   //  periods OK
         if (pfile[jj] >= 'a' && pfile[jj] <= 'z') continue;               //  keep a-z, A-Z, 0-9
         if (pfile[jj] >= 'A' && pfile[jj] <= 'Z') continue;
         if (pfile[jj] >= '0' && pfile[jj] <= '9') continue;
         kk--;                                                             //  omit others
      }
      
      pfile[kk] = 0;

      pext = strrchr(pfile,'.');                                           //  find .ext if there is one
      if (! pext) pext = pfile + strlen(pfile);
      if (strlen(pext) > 5) pext = pext + strlen(pext);
      
      sprintf(sequence,"-%d",ii+1);                                        //  replace with sequence number
      strcpy(pext,sequence);                                               //  filename-nn

      pext = pext + strlen(pext);                                          //  add .jpg extension
      strcpy(pext,".jpg");                                                 //  filename-nn.jpg
      
      write_popup_text("write",newfile);                                   //  log progress
      zmainloop();

      pxmin = f_load(oldfile,8);                                           //  load image as PXM-8 pixmap
      if (! pxmin) {
         printf("f_load error: %s \n",oldfile);
         continue;
      }
      
      ww = pxmin->ww;
      hh = pxmin->hh;

      wscale = hscale = 1.0;
      if (ww > maxw) wscale = 1.0 * maxw / ww;                             //  compute new size
      if (hh > maxh) hscale = 1.0 * maxh / hh;
      if (wscale < hscale) scale = wscale;
      else scale = hscale;
      ww = ww * scale;
      hh = hh * scale;
      pxmout = PXM_rescale(pxmin,ww,hh);                                   //  rescale file

      PXBwrite(pxmout,newfile);                                            //  write to new file
      
      PXM_free(pxmin);
      PXM_free(pxmout);
      
      err = strncatv(command,1990," --attach ","\"",newfile,"\"",null);    //  --attach /tmp/<user>/fotoxx/file.jpg
      zfree(newfile);
      
      if (err) {
         zmessageACK(mWin,ZTX("too many files"));
         goto cleanup;
      }
   }

   write_popup_text("write","COMPLETED");
   write_popup_text("close",0);

   emailsize[0] = maxw;                                                    //  save preferred size
   emailsize[1] = maxh;
   
   strcat(command," &");                                                   //  wait for email completion
   printf("system: %s \n",command);
   err = system(command);

cleanup:

   if (flist) {                                                            //  free memory
      for (ii = 0; flist[ii]; ii++) 
         zfree(flist[ii]);
      zfree(flist);
      flist = 0;
   }

   zdialog_free(zd);                                                       //  kill dialog
   return 0;
}


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

//  monitor test function

void m_moncheck(GtkWidget *, cchar *)
{
   uint8       *pixel;
   int         red, green, blue;
   int         row, col, row1, row2;
   int         ww = 800, hh = 500;
   zdialog     *zd;
   
   cchar       *message = ZTX("Brightness should show a gradual ramp \n"
                              "extending all the way to the edges.");
   
   zfuncs::F1_help_topic = "check_monitor";

   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;
   mutex_lock(&Fpixmap_lock);

   PXM_free(Fpxm8);
   Fpxm8 = PXM_make(ww,hh,8);
   Fww = ww;
   Fhh = hh;
   curr_file_bpc = 8;
   curr_file_size = 0;

   for (red = 0; red <= 1; red++)                                          //  8 RGB combinations
   for (green = 0; green <= 1; green++)
   for (blue = 0; blue <= 1; blue++)
   {
      row1 = 4 * red + 2 * green + blue;                                   //  row 0 to 7
      row1 = row1 * hh / 8;                                                //  stripe, 1/8 of image
      row2 = row1 + hh / 8;
      
      for (row = row1; row < row2; row++)
      for (col = 0; col < ww; col++)
      {
         pixel = PXMpix8(Fpxm8,col,row);
         pixel[0] = red * 256 * col / ww;
         pixel[1] = green * 256 * col / ww;
         pixel[2] = blue * 256 * col / ww;
      }
   }

   Fzoom = 0;                                                              //  scale to window
   gtk_window_set_title(MWIN,"monitor check");
   mutex_unlock(&Fpixmap_lock);
   mwpaint2();                                                             //  repaint window
   curr_image_time = get_seconds();                                        //  mark time of image change   v.11.07

   zd = zdialog_new(ZTX("Monitor Check"),mWin,Bdone,null);                 //  start user dialog      v.11.04
   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_help(zd,"check_monitor");                                       //  zdialog help topic     v.11.08
   zdialog_run(zd);
   zdialog_wait(zd);                                                       //  wait for dialog complete
   zdialog_free(zd);

   f_open(curr_file,0);                                                    //  back to current file   v.11.04
   menulock(0);
   return;
}


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

//  adjust monitor gamma

void m_mongamma(GtkWidget *, cchar *)                                      //  revised v.11.04
{
   int   mongamma_event(zdialog *zd, cchar *event);
   char  gammachart[200];
   
   zdialog     *zd;
   cchar       *permit = "Chart image courtesy of Norman Koren";
   cchar       *website1 = "http://www.normankoren.com";
   cchar       *website2 = "http://www.imatest.org";
   PXM         *pxmtemp;

   zfuncs::F1_help_topic = "monitor_gamma";

   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;

   snprintf(gammachart,200,"%s/images/gammachart.png",get_zdocdir());      //  gamma chart .png file
   pxmtemp = PXBread(gammachart);
   if (! pxmtemp) {
      zmessageACK(mWin,"gamma chart is missing");
      menulock(0);
      return;
   }

   mutex_lock(&Fpixmap_lock);                                              //  curr. image = gamma chart
   PXM_free(Fpxm8);
   Fpxm8 = pxmtemp;
   Fww = Fpxm8->ww;
   Fhh = Fpxm8->hh;
   curr_file_bpc = 8;
   curr_file_size = 0;

   Fzoom = 1;                                                              //  scale 100% (required)
   gtk_window_set_title(MWIN,"adjust gamma chart");
   mutex_unlock(&Fpixmap_lock);
   mwpaint2();                                                             //  repaint window
   curr_image_time = get_seconds();                                        //  mark time of image change    v.11.07

   zd = zdialog_new(ZTX("Monitor Gamma"),mWin,Bdone,null);                 //  start user dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labgamma","hb1","gamma","space=5");
   zdialog_add_widget(zd,"hscale","gamma","hb1","0.6|1.4|0.02|1.0","expand");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","permit","hb2",permit,"space=5");
   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"link","web1","hb3",website1);
   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"link","web2","hb4",website2);
   
   zdialog_resize(zd,200,0);
   zdialog_help(zd,"monitor_gamma");                                       //  zdialog help topic     v.11.08
   zdialog_run(zd,mongamma_event);
   zdialog_wait(zd);                                                       //  wait for dialog complete
   zdialog_free(zd);

   f_open(curr_file,0);                                                    //  back to current file   v.11.04
   menulock(0);
   return;
}


//  dialog event function

int mongamma_event(zdialog *zd, cchar *event)
{
   int      err;
   double   gamma;

   if (strEqu(event,"gamma")) {
      zdialog_fetch(zd,"gamma",gamma);
      sprintf(command,"xgamma -quiet -gamma %.2f",gamma);
      err = system(command);
      if (err) zmessageACK(mWin,"error: %s",wstrerror(err));
   }
   
   return 0;
}


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

//  create or update brightness histogram graph

GtkWidget      *winhisto = 0;                                              //  brightness histogram window
GtkWidget      *winhistoH = 0;                                             //  histogram drawing area
GtkWidget      *winhistoB = 0;                                             //  brightness band underneath


void m_histogram(GtkWidget *, cchar *menu)                                 //  menu function
{
   if (menu) zfuncs::F1_help_topic = "brightness_dist";

   if (! Dpxm8) return;

   if (winhisto) {                                                         //  window already present
      histogram_paint();
      return;
   }

   winhisto = gtk_window_new(GTK_WINDOW_TOPLEVEL);                         //  create histogram window
   gtk_window_set_title(GTK_WINDOW(winhisto),ZTX("Brightness Distribution"));
   gtk_window_set_transient_for(GTK_WINDOW(winhisto),MWIN);
   gtk_window_set_default_size(GTK_WINDOW(winhisto),300,200);
   GtkWidget * vbox = gtk_vbox_new(0,3);
   gtk_container_add(GTK_CONTAINER(winhisto),vbox);

   winhistoH = gtk_drawing_area_new();                                     //  histogram drawing area
   gtk_box_pack_start(GTK_BOX(vbox),winhistoH,1,1,3);

   winhistoB = gtk_drawing_area_new();                                     //  brightness scale under histogram
   gtk_box_pack_start(GTK_BOX(vbox),winhistoB,0,0,0);                      //  (progresses from black to white)
   gtk_widget_set_size_request(winhistoB,300,12);                          //                   v.9.3

   G_SIGNAL(winhisto,"destroy",histogram_destroy,0);
   G_SIGNAL(winhistoH,"expose-event",histogram_paint,0);
   G_SIGNAL(winhistoB,"expose-event",histogram_paint,0);

   gtk_widget_show_all(winhisto);
   
   return;
}


void histogram_paint()                                                     //  paint graph window
{
   GdkGC          *gdkgc = 0;                                              //  GDK graphics context
   GdkColor       color;
   GdkColormap    *colormap = 0;

   int         brdist[40], bin, nbins = 40;                                //  increased                 v.9.3
   int         dist_maxbin = 0;
   int         winww, winhh;
   int         px, py, ww, hh, orgx, orgy;
   uint8       *pix8;
   uint16      *pix16;
   double      bright;

   if (! winhisto) return;
   if (! Dpxm8) return;

   gdkgc = gdk_gc_new(winhistoH->window);                                  //  use separate graphics context
   colormap = gtk_widget_get_colormap(winhistoH);

   for (bin = 0; bin < nbins; bin++)                                       //  clear brightness distribution
      brdist[bin] = 0;

   mutex_lock(&Fpixmap_lock);

   if (Factivearea && E3pxm16)                                             //  compute brightness distribution   
   {                                                                       //    for selected area being edited
      for (int ii = 0; ii < Fww * Fhh; ii++)
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / Fww;
         px = ii - Fww * py;
         pix16 = PXMpix(E3pxm16,px,py);
         bright = 0.003906 * pixbright(pix16);                             //  0 to 255
         brdist[int(bright / 256 * nbins)]++;                              //  0 to nbins
      }
   }

   else 
   {
      for (py = 0; py < dhh; py++)                                         //  compute brightness distribution
      for (px = 0; px < dww; px++)                                         //    for image in visible window
      {
         pix8 = (uint8 *) Dpxm8->bmp + (py * dww + px) * 3;
         bright = pixbright(pix8);                                         //  0 to 255
         brdist[int(bright / 256 * nbins)]++;                              //  0 to nbins
      }
   }

   for (bin = 0; bin < nbins; bin++)                                       //  find max. bin
      if (brdist[bin] > dist_maxbin) dist_maxbin = brdist[bin];

   mutex_unlock(&Fpixmap_lock);

   gdk_window_clear(winhistoH->window);

   winww = winhistoH->allocation.width;                                    //  drawing window size
   winhh = winhistoH->allocation.height;
   ww = winww / nbins;                                                     //  bin width
   bin = -1;

   color.red = 10000;                                                      //  a pleasing color
   color.green = 25000;
   color.blue = 40000;
   gdk_rgb_find_color(colormap,&color);
   gdk_gc_set_foreground(gdkgc,&color);

   for (px = 0; px < winww; px++)                                          //  draw each bin
   {
      if (px * nbins / winww > bin) {
         bin++;
         hh = 0.9 * winhh * brdist[bin] / dist_maxbin;
         orgx = px;
         orgy = winhh - hh;
         gdk_draw_rectangle(winhistoH->window,gdkgc,1,orgx,orgy,ww+1,hh);
      }
   }

   hh = winhistoB->allocation.height;

   for (px = 0; px < winww; px++)                                          //  draw brightness scale underneath
   {                                                                       //                      v.9.3
      color.red = color.green = color.blue = 65536 * px / winww;
      gdk_rgb_find_color(colormap,&color);
      gdk_gc_set_foreground(gdkgc,&color);
      gdk_draw_line(winhistoB->window,gdkgc,px,0,px,hh-1);
   }

   return;
}


void histogram_destroy()                                                   //  delete window
{
   if (winhisto) gtk_widget_destroy(winhisto);
   winhisto = 0;
   return;
}


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

//  set GUI language

void  m_lang(GtkWidget *, cchar *)                                         //  overhauled   v.10.1
{
   zdialog     *zd;
   int         ii, cc, err, val, zstat;
   char        lang1[8], *pp;

   cchar  *langs[12] = { "en English", "de German", "es Spanish",          //  english first
                         "fr French", "gl Galacian", "it Italian", 
                         "nl Dutch", "pt Portuguese", "ru_RU Russian", 
                         "sv Swedish", "zh_CN Chinese", null  };
   
   cchar  *title = ZTX("Available Translations");

   zfuncs::F1_help_topic = "language";                                     //  v.10.8

   zd = zdialog_new(ZTX("Set Language"),mWin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"label","title","dialog",title,"space=10");
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1");

   for (ii = 0; langs[ii]; ii++)                                           //  make radio button per language
      zdialog_add_widget(zd,"radio",langs[ii],"vb1",langs[ii]);

   cc = strlen(zfuncs::zlang);                                             //  current language
   for (ii = 0; langs[ii]; ii++)                                           //  match on lc_RC
      if (strnEqu(zfuncs::zlang,langs[ii],cc)) break;
   if (! langs[ii]) 
      for (ii = 0; langs[ii]; ii++)                                        //  failed, match on lc alone
         if (strnEqu(zfuncs::zlang,langs[ii],2)) break;
   if (! langs[ii]) ii = 0;                                                //  failed, default english
   zdialog_stuff(zd,langs[ii],1);

   zdialog_resize(zd,200,0);
   zdialog_help(zd,"language");                                            //  zdialog help topic     v.11.08
   zdialog_run(zd);                                                        //  run dialog
   zstat = zdialog_wait(zd);

   for (ii = 0; langs[ii]; ii++) {                                         //  get active radio button
      zdialog_fetch(zd,langs[ii],val);
      if (val) break;
   }

   zdialog_free(zd);                                                       //  kill dialog

   if (zstat != 1) return;                                                 //  user cancel
   if (! val) return;                                                      //  no selection
   
   strncpy0(lang1,langs[ii],8);
   pp = strchr(lang1,' ');                                                 //  isolate lc_RC part
   *pp = 0;

   sprintf(command,"fotoxx -l %s -p &",lang1);                             //  start new fotoxx with language
   err = system(command);
   if (err) printf("error: %s \n",wstrerror(err));

   m_quit(0,0);                                                            //  exit
   return;
}


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

//  edit translations while running the application interactively

void  m_translate(GtkWidget *, cchar *)                                    //  new v.11.06
{
   zfuncs::F1_help_topic = "translate";
   ZTX_translation_start(mWin);
   return;
}


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

//  create desktop icon / launcher

void  m_menu_launcher(GtkWidget *, cchar *)                                //  v.11.08
{
   char  *command;
   
   zfuncs::F1_help_topic = "menu_launcher";
   command = zdialog_text(mWin,ZTX("Make Launcher"),"fotoxx -recent");
   if (! command || ! *command) return;                                    //  bugfix        v.11.09
   zmake_menu_launcher(command,"Graphics","Image Editor");
   return;
}


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


//  user settings dialog

void m_settings(GtkWidget *, cchar *)                                      //  v.12.01
{
   int   settings_dialog_event(zdialog *zd, cchar *event);

   zdialog  *zd;
   cchar    *zooms[4] = { "1.1892071", "1.2599211", "1.4142136", "2.0" };
   double   fzoom;

/***
         Startup Display      (o) Recent Files Gallery
                              (o) Previous Image Viewed
                              (o) Blank Window
                              (o) Directory Gallery
                              [__________________________] [browse]
                              (o) Image File
                              [__________________________] [browse]

         Toolbar Style        (o) Text  (o) Icons  (o) Both
         Warn Overwrite       [x] 
         Panorama Params      Lens mm [__|v]  lens bow [__|v]
         Zoom Ratio           [___]
                                                            [done]
***/

   zfuncs::F1_help_topic = "user_settings";
   
   zd = zdialog_new(ZTX("Settings"),mWin,Bdone,null);

   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbdisp","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbdisp1","hbdisp",0,"space=5");
   zdialog_add_widget(zd,"label","labdisp","vbdisp1",ZTX("Startup Display"));
   zdialog_add_widget(zd,"vbox","vbdisp2","hbdisp",0,"expand");
   zdialog_add_widget(zd,"radio","recent","vbdisp2",ZTX("Recent Files Gallery"));
   zdialog_add_widget(zd,"radio","prev","vbdisp2",ZTX("Previous Image Viewed"));
   zdialog_add_widget(zd,"radio","blank","vbdisp2",ZTX("Blank Window"));

   zdialog_add_widget(zd,"radio","dirk","vbdisp2",ZTX("Directory Gallery"));
   zdialog_add_widget(zd,"hbox","hbstdirk","vbdisp2");
   zdialog_add_widget(zd,"entry","stdirk","hbstdirk",0,"expand|space=5");
   zdialog_add_widget(zd,"button","stdbrowse","hbstdirk",Bbrowse);

   zdialog_add_widget(zd,"radio","file","vbdisp2",ZTX("Image File"));
   zdialog_add_widget(zd,"hbox","hbstfile","vbdisp2");
   zdialog_add_widget(zd,"entry","stfile","hbstfile",0,"expand|space=5");
   zdialog_add_widget(zd,"button","stfbrowse","hbstfile",Bbrowse);

   zdialog_add_widget(zd,"hbox","hbtbs","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labtbs","hbtbs",ZTX("Toolbar Style"),"space=5");
   zdialog_add_widget(zd,"radio","text","hbtbs",ZTX("Text"),"space=5");
   zdialog_add_widget(zd,"radio","icons","hbtbs",ZTX("Icons"),"space=5");
   zdialog_add_widget(zd,"radio","both","hbtbs",ZTX("Both"),"space=5");

   zdialog_add_widget(zd,"hbox","hbwarn","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labwarn","hbwarn",ZTX("Warn Overwrite"),"space=5");
   zdialog_add_widget(zd,"check","warn","hbwarn",0);
   
   zdialog_add_widget(zd,"hbox","hbpano","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpano","hbpano","Panorama","space=5");
   zdialog_add_widget(zd,"label","space","hbpano",0,"space=5");
   zdialog_add_widget(zd,"label","labmm","hbpano","lens mm","space=3");
   zdialog_add_widget(zd,"spin","lensmm","hbpano","20|999|0.1|30");
   zdialog_add_widget(zd,"label","space","hbpano",0,"space=5");
   zdialog_add_widget(zd,"label","labow","hbpano","lens bow","space=3");
   zdialog_add_widget(zd,"spin","lensbow","hbpano","-9.99|9.99|0.01|0");
   
   zdialog_add_widget(zd,"hbox","hbzoom","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labzoom","hbzoom","Zoom Ratio","space=5");
   zdialog_add_widget(zd,"combo","zoom","hbzoom",0,"scc=8|space=5");
   
   zdialog_stuff(zd,"stdirk",startdirk);                                   //  stuff present settings into dialog
   zdialog_stuff(zd,"stfile",startfile);

   zdialog_stuff(zd,"recent",0);
   zdialog_stuff(zd,"prev",0);
   zdialog_stuff(zd,"blank",0);
   zdialog_stuff(zd,"dirk",0);
   zdialog_stuff(zd,"file",0);
   zdialog_stuff(zd,startdisplay,1);
   
   zdialog_stuff(zd,"icons",0);
   zdialog_stuff(zd,"text",0);
   zdialog_stuff(zd,"both",0);
   zdialog_stuff(zd,tbar_style,1);
   
   zdialog_stuff(zd,"warn",Fwarnoverwrite);
   
   zdialog_stuff(zd,"lensmm",lens_settings[0]);
   zdialog_stuff(zd,"lensbow",lens_settings[1]);
   
   zdialog_cb_app(zd,"zoom",zooms[0]);                                     //  4 zooms = 2x
   zdialog_cb_app(zd,"zoom",zooms[1]);                                     //  3 zooms = 2x
   zdialog_cb_app(zd,"zoom",zooms[2]);                                     //  2 zooms = 2x
   zdialog_cb_app(zd,"zoom",zooms[3]);                                     //  1 zoom = 2x
   
   convSD(zoomratio,fzoom);
   if (fzoom < 1.2) zdialog_stuff(zd,"zoom",zooms[0]);
   else if (fzoom < 1.3) zdialog_stuff(zd,"zoom",zooms[1]);
   else if (fzoom < 1.5) zdialog_stuff(zd,"zoom",zooms[2]);
   else zdialog_stuff(zd,"zoom",zooms[3]);

   zdialog_help(zd,"user_settings");
   zdialog_resize(zd,450,0);

   zdialog_run(zd,settings_dialog_event);                                  //  run dialog and wait for completion
   zdialog_wait(zd);
   zdialog_free(zd);

   return;
}


//  settings dialog event function

int settings_dialog_event(zdialog *zd, cchar *event)
{
   int         nn;
   char        *pp, temp[200];

   if (zd->zstat && zd->zstat != 1) return 0;                              //  cancel
   
   if (zd->zstat == 1)                                                     //  done, check inputs are valid
   {
      zdialog_fetch(zd,"stdirk",temp,200);                                 //  startup directory
      if (*temp > ' ' && image_file_type(temp) != 1) {
         zmessageACK(mWin,ZTX("startup directory is invalid"));
         zd->zstat = 0;
         return 0;
      }
      else {
         if (startdirk) zfree(startdirk);
         startdirk = strdupz(temp,0,"settings");
      }

      zdialog_fetch(zd,"stfile",temp,200);                                 //  startup file
      if (*temp > ' ' && image_file_type(temp) != 2) {
         zmessageACK(mWin,ZTX("startup file is invalid"));
         zd->zstat = 0;
         return 0;
      }
      else {
         if (startfile) zfree(startfile);
         startfile = strdupz(temp,0,"settings");
      }
      
      zdialog_fetch(zd,"recent",nn);                                       //  startup display
      if (nn) startdisplay = "recent";
      
      zdialog_fetch(zd,"prev",nn);
      if (nn) startdisplay = "prev";
      
      zdialog_fetch(zd,"blank",nn);
      if (nn) startdisplay = "blank";
      
      zdialog_fetch(zd,"dirk",nn);
      if (nn) startdisplay = "dirk";
      
      zdialog_fetch(zd,"file",nn);
      if (nn) startdisplay = "file";
      
      zdialog_fetch(zd,"text",nn);                                         //  toolbar style
      if (nn) {
         tbar_style = "text";
         gtk_toolbar_set_style(GTK_TOOLBAR(mTbar),GTK_TOOLBAR_TEXT);
      }

      zdialog_fetch(zd,"icons",nn);
      if (nn) {
         tbar_style = "icons";
         gtk_toolbar_set_style(GTK_TOOLBAR(mTbar),GTK_TOOLBAR_ICONS);
      }

      zdialog_fetch(zd,"both",nn);
      if (nn) {
         tbar_style = "both";
         gtk_toolbar_set_style(GTK_TOOLBAR(mTbar),GTK_TOOLBAR_BOTH);
      }

      zdialog_fetch(zd,"warn",Fwarnoverwrite);                             //  warn overwrite option
      
      zdialog_fetch(zd,"lensmm",lens_settings[0]);                         //  pano lens parameters
      zdialog_fetch(zd,"lensbow",lens_settings[1]);
      
      zdialog_fetch(zd,"zoom",temp,20);                                    //  zoom ratio
      zfree((char *) zoomratio);
      zoomratio = strdupz(temp);
      
      save_params();
      return 0;
   }

   if (strEqu(event,"stdbrowse")) {                                        //  startup directory browse
      zdialog_fetch(zd,"stdirk",temp,200);
      pp = zgetfile1(ZTX("Select startup directory"),"folder",temp);  
      if (! pp) return 0;
      zdialog_stuff(zd,"stdirk",pp);
      zfree(pp);
   }
   
   if (strEqu(event,"stfbrowse")) {                                        //  startup file browse
      zdialog_fetch(zd,"stfile",temp,200);
      pp = zgetfile1(ZTX("Select startup image file"),"open",temp);  
      if (! pp) return 0;
      zdialog_stuff(zd,"stfile",pp);
      zfree(pp);
   }

   return 0;
}


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

//  dump memory usage by category to STDOUT

void m_memory_usage(GtkWidget *, cchar *)
{
   zfuncs::F1_help_topic = "memory_usage";
   zmalloc_report();
   return;
}





