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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

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

*********************************************************************************

   Fotoxx image edit - Tools menu functions

   m_index              dialog to create/update image index file
   index_rebuild        create/update image index file
   m_options            user options dialog
   m_KBshortcuts        edit keyboard shortcuts
   KBshorts_load        load KB shortcuts file into memory
   m_show_brdist        show brightness distribution graph
   m_gridlines          setup for grid lines
   m_line_color         choose color for foreground lines
   m_show_RGB           show RGB values for clicked image positions
   m_magnify            magnify the image within a radius of the mouse
   m_darkbright         highlight darkest and brightest pixels
   m_moncolor           monitor color and contrast check
   m_mongamma           monitor gamma check and adjust
   m_changelang         choose GUI language
   m_untranslated       report misting translations
   m_calibrate_printer  printer color calibration
   m_resources          print memory allocated and CPU time used
   m_zappcrash          zappcrash test

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

#define EX extern                                                                //  enable extern declarations
#include "fotoxx.h"                                                              //  (variables in fotoxx.h are refs)
#include <sys/wait.h>
#include <sys/mman.h>

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

//  Index Image Files menu function
//  Manual index request, or missing information, or new install.
//  Dialog to supply top image directories and thumbnails directory.
//  Update the corresponding config. files and generate new image index.

void m_index(GtkWidget *, cchar *menu)
{
   void index_clickfunc(GtkWidget *widget, int line, int pos);
   int index_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;
   FILE           *fid;
   char           filespec[200], buff[200], thumbdirk[200];
   char           *pp;
   GtkWidget      *widget;
   int            ftf, cc, zstat;
   cchar          *greet1 = ZTX("Select directories containing image files \n"
                                "(subdirectories are included automatically).");
   cchar          *greet2 = ZTX("Select to add, click on X to delete.");
   cchar          *greet3 = ZTX("Select directory for thumbnails.");
   cchar          *termmess = ZTX("Index function terminated. Fotoxx will close. \n" 
                                  "Indexing is required for search and map functions \n"
                                  "and to make the gallery pages acceptably fast.");

   F1_help_topic = "index_files";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/***
       _____________________________________________________
      |              Index Image Files                      |                    //  15.06
      |                                                     |
      |  Select directories containing image files.         |
      |  (subdirectories are included automatically).       |
      |  [Select] Select to add, click on X to delete.      |
      |  _________________________________________________  |
      | | X  /home/<user>/Pictures                        | |
      | | X  /home/<user>/...                             | |
      | |                                                 | |
      | |                                                 | |
      | |                                                 | |
      | |_________________________________________________| |
      |                                                     |
      | [Select] Select directory for thumbnails.           |
      | [_________________________________________________] |
      |                                                     |
      |                           [Help] [Proceed] [Cancel] |
      |_____________________________________________________|

***/

   zd = zdialog_new(ZTX("Index Image Files"),Mwin,Bhelp,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbgreet1","dialog");
   zdialog_add_widget(zd,"label","labgreet1","hbgreet1",greet1,"space=5");
   zdialog_add_widget(zd,"hbox","hbtop","dialog");
   zdialog_add_widget(zd,"button","browsetop","hbtop",Bselect,"space=5");        //  browsetop button
   zdialog_add_widget(zd,"label","labgreet2","hbtop",greet2);
   zdialog_add_widget(zd,"hbox","hbtop2","dialog",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtop2",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbtop2","hbtop2",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtop2",0,"space=3");
   zdialog_add_widget(zd,"frame","frtop","vbtop2",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrtop","frtop",0,"expand");
   zdialog_add_widget(zd,"text","topdirks","scrtop");                            //  topdirks text
   zdialog_add_widget(zd,"vbox","vbspace","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbthumb1","dialog");
   zdialog_add_widget(zd,"button","browsethumb","hbthumb1",Bselect,"space=3");   //  browsethumb button
   zdialog_add_widget(zd,"label","labgreet3","hbthumb1",greet3,"space=5");

   zdialog_add_widget(zd,"hbox","hbthumb2","dialog");
   zdialog_add_widget(zd,"frame","frthumb","hbthumb2",0,"space=5|expand");
   zdialog_add_widget(zd,"text","thumbdirk","frthumb");                          //  thumbdirk text

   widget = zdialog_widget(zd,"topdirks");                                       //  set click function for top directories
   textwidget_set_clickfunc(widget,index_clickfunc);                             //    text window

   snprintf(thumbdirk,200,"%s/thumbnails",get_zuserdir());                       //  stuff default thumbnails directory
   zdialog_stuff(zd,"thumbdirk",thumbdirk);                                      //    /home/<user>/.fotoxx/thumbnails

   snprintf(filespec,200,"%s/top_directories",index_dirk);                       //  read top image directories file,
   widget = zdialog_widget(zd,"topdirks");                                       //    stuff data into dialog widgets

   fid = fopen(filespec,"r");
   if (fid) {
      zdialog_stuff(zd,"topdirks","");                                           //  clear text list
      while (true) {
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails directory: /",23)) {                     //  if "thumbnails directory: /..."
            zdialog_stuff(zd,"thumbdirk",buff+22);                               //  stuff thumbnails directory
            break;                                                               //  last record
         }
         else wprintf(widget," X  %s\n",buff);                                   //  stuff " X  /dir1/dir2..."
      }
      fclose(fid);
   }
   else wprintf(widget," X  %s/Pictures\n",getenv("HOME"));                      //  default  /home/<user>/Pictures     15.07

   zdialog_resize(zd,400,350);                                                   //  run dialog
   zdialog_run(zd,index_dialog_event,"parent");
   zstat = zdialog_wait(zd);                                                     //  wait for completion

   if (zstat != 2) {                                                             //  canceled
      zdialog_free(zd);
      Fblock = 0;
      if (! FindexOK) {                                                          //  index started and not finished
         zmessageACK(Mwin,termmess);                                             //  cannot continue                    16.04
         m_quit(0,0);
      }
      return;
   }

   snprintf(filespec,200,"%s/top_directories",index_dirk);                       //  open/write top directories file
   fid = fopen(filespec,"w");
   if (! fid) {                                                                  //  fatal bug
      zmessageACK(Mwin,"top directories file: \n %s",strerror(errno));           //  16.04
      Fblock = 0;
      m_quit(0,0);
   }

   widget = zdialog_widget(zd,"topdirks");                                       //  get top directories from dialog widget

   ftf = 1;
   while (true) {
      pp = wscanf(widget,ftf);                                                   //  loop widget text lines
      if (! pp) break;
      pp += 4;                                                                   //  skip " X  "
      if (*pp != '/') continue;
      strncpy0(buff,pp,200);                                                     //  /dir1/dir2/...
      cc = strlen(buff);
      if (cc < 5) continue;                                                      //  ignore blanks or rubbish
      if (buff[cc-1] == '/') buff[cc-1] = 0;                                     //  remove trailing '/'
      fprintf(fid,"%s\n",buff);                                                  //  write top directory to output file
   }

   zdialog_fetch(zd,"thumbdirk",buff,200);                                       //  get thumbnails directory from dialog
   strTrim2(buff);                                                               //  remove surrounding blanks
   cc = strlen(buff);
   if (cc && buff[cc-1] == '/') buff[cc-1] = 0;                                  //  remove trailing '/'
   if (cc > 10)
      fprintf(fid,"thumbnails directory: %s\n",buff);                            //  write thumbnails directory to output file
   fclose(fid);

   zdialog_free(zd);                                                             //  close dialog

   index_rebuild(1);                                                             //  build image index and thumbnail files
   if (! FindexOK) m_index(0,"retry");                                           //  failed
   Fblock = 0;                                                                   //  OK
   return;
}


// ------------------------------------------------------------------------------

//  mouse click function for top directories text window
//  remove directory from list where "X" is clicked

void index_clickfunc(GtkWidget *widget, int line, int pos)                       //  15.07
{
   GtkTextBuffer  *textbuff;
   GdkWindow      *gdkwin;
   char     *pp;
   char     *dirlist[maxtopdirks];
   int      ftf = 1, ii, jj;
   
   gdkwin = gtk_widget_get_window(widget);                                       //  stop updates between clear and refresh
   gdk_window_freeze_updates(gdkwin);
   
   for (ii = jj = 0; ii < maxtopdirks; ii++)                                     //  loop text lines in widget
   {                                                                             //    " X  /dir1/dir2/... "
      pp = wscanf(widget,ftf);
      if (! pp) break;
      if (ii == line && pos < 3) continue;                                       //  if "X" clicked, skip deleted line
      dirlist[jj] = zstrdup(pp);
      jj++;
   }
   
   textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));                   //  clear widget text
   gtk_text_buffer_set_text(textbuff,"",0);

   for (ii = 0; ii < jj; ii++)                                                   //  stuff remaining lines back into widget
   {
      wprintf(widget,"%s\n",dirlist[ii]);
      zfree(dirlist[ii]);
   }

   gdk_window_thaw_updates(gdkwin);
   return;
}


// ------------------------------------------------------------------------------

//  index dialog event and completion function

int index_dialog_event(zdialog *zd, cchar *event)
{
   GtkWidget   *widget;
   char        **flist, *thumbdirk;
   int         ii;
   cchar       *topmess = ZTX("Choose top image directories");
   cchar       *thumbmess = ZTX("Choose thumbnail directory");

   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"browsetop")) {                                            //  [browse] top directories
      flist = zgetfiles(topmess,MWIN,"folders",getenv("HOME"));                  //  get top directories from user
      if (! flist) return 1;
      widget = zdialog_widget(zd,"topdirks");                                    //  add to dialog list
      for (ii = 0; flist[ii]; ii++) {
         wprintf(widget," X  %s\n",flist[ii]);                                   //  " X  /dir1/dir2/..."
         zfree(flist[ii]);
      }
      zfree(flist);
   }

   if (strmatch(event,"browsethumb")) {                                          //  [browse] thumbnail directory
      thumbdirk = zgetfile(thumbmess,MWIN,"folder",getenv("HOME"));
      if (! thumbdirk) return 1;
      thumbdirk = zstrdup(thumbdirk,12);
      if (! strstr(thumbdirk,"/thumbnails"))                                     //  if not containing /thumbnails,
         strcat(thumbdirk,"/thumbnails");                                        //    append /thumbnails
      zdialog_stuff(zd,"thumbdirk",thumbdirk);
      zfree(thumbdirk);
   }

   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1) {                                                         //  [help]
      zd->zstat = 0;
      showz_userguide("index_files");
      return 1;
   }

   return 1;                                                                     //  [proceed] or cancel status
}


// ------------------------------------------------------------------------------

//  Rebuild the image index from the top directories list. 
//  Called from main() when Fotoxx is started (menu = 0)
//  Called from menu function m_index() (menu = 1)

namespace index_names                                                            //  16.04
{
   zdialog  *zd_indexlog = 0;
   sxrec_t  *sxrec_old = 0, *sxrec_new = 0;
   char     *Fupdate = 0;
   int      Nold, Nnew;
   int      indexupdates, thumbupdates, sumupdates, thumbdeletes;
   int      Findexdone, Fuserkill;
}


void index_rebuild(int menu)
{
   using namespace index_names;

   int    indexlog_dialog_event(zdialog *zd, cchar *event);
   int    index_compare(cchar *rec1, cchar *rec2);
   void   *index_thread(void *);

   GtkWidget   *wlog;
   FILE        *fid;
   int         ii, jj, ntop, nthumb, err, Fnew;
   int         ftf, cc, NF, orec, nrec, comp, Fretry = 0;
   char        *pp, filespec[200];
   char        buff[XFCC+500];
   char        **flist, *file, *thumbfile;
   STATB       statdat;
   int         imagesfound, updatesreq;
   double      startime;

   cchar *indexmess = ZTX("No image file index was found.\n"                     //  15.06
                          "An image file index will be created.\n"
                          "Your image files will not be changed.\n" 
                          "This may need considerable time if you \n"
                          "have many thousands of image files.");

   cchar *indexerr = ZTX("Invalid directory: \n  %s \n"
                         "Please remove.");
   
   cchar *thumberr = ZTX("Thumbnails directory: \n  %s \n"
                         "must be named .../thumbnails");
   
   cchar *duperr = ZTX("Duplicate directory: \n  %s \n"
                       "Please remove.");

   char     statsmess[100] = "index updates: %d  thumbnail updates: %d, deletes: %d \n";


   startime = get_seconds();                                                     //  index function start time
   FindexOK = 0;                                                                 //  must run to completion
   Fblock = 1;

   //  get current top image directories and thumbnails directory
   //  from /home/<user>/.fotoxx/image_index/top_directories

   ntop = nthumb = 0;
   snprintf(filespec,200,"%s/top_directories",index_dirk);                       //  read top directories file

   fid = fopen(filespec,"r");
   if (fid) {
      while (true) {                                                             //  get top image directories
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails directory: /",23)) {
            if (thumbdirk) zfree(thumbdirk);
            thumbdirk = zstrdup(buff+22);
            nthumb = 1;
            break;
         }
         if (topdirks[ntop]) zfree(topdirks[ntop]);
         topdirks[ntop] = zstrdup(buff);                                         //  top directories list in global space
         if (++ntop == maxtopdirks) break;
      }
      fclose(fid);
   }

   Ntopdirks = ntop;

   if (! ntop) {                                                                 //  if nothing found, must ask user
      zmessageACK(Mwin,indexmess);
      goto retry;
   }

   for (ii = 0; ii < ntop; ii++) {                                               //  validate top directories
      err = stat(topdirks[ii],&statdat);
      if (err || ! S_ISDIR(statdat.st_mode)) {
         zmessageACK(Mwin,indexerr,topdirks[ii]);                                //  15.07
         goto retry;
      }
   }

   if (! nthumb) {                                                               //  no thumbnails directory, must ask user
      zmessageACK(Mwin,ZTX("no thumbnails directory defined"));
      goto retry;
   }

   cc = strlen(thumbdirk) - 11 ;                                                 //  check /thumbnails name             15.07
   if (! strmatch(thumbdirk+cc,"/thumbnails")) {
      zmessageACK(Mwin,thumberr,thumbdirk);
      goto retry;
   }

   err = stat(thumbdirk,&statdat);                                               //  create thumbnails directory if needed
   if (err || ! S_ISDIR(statdat.st_mode))
      err = mkdir(thumbdirk,0750);
   if (err) {
      zmessageACK(Mwin,"%s \n %s",thumbdirk,strerror(errno));
      goto retry;
   }
   
   for (ii = 0; ii < ntop; ii++) {                                               //  disallow top dir = thumbnail dir   15.06
      if (strmatch(topdirks[ii],thumbdirk)) {
         zmessageACK(Mwin,indexerr,topdirks[ii]);                                //  15.07
         goto retry;
      }
   }

   for (ii = 0; ii < ntop; ii++)                                                 //  check for duplicate directories    15.07
   for (jj = ii+1; jj < ntop; jj++) {
      if (strmatch(topdirks[ii],topdirks[jj])) {
         zmessageACK(Mwin,duperr,topdirks[jj]);
         goto retry;
      }
   }

   //  if index update is not wanted, exit now (-noindex parameter)

   if (Fnoindex && menu == 0) {                                                  //  repositioned                       15.11
      zmessage_post(Mwin,1,"metadata index update suppressed");                  //  16.04
      printz("metadata index update suppressed \n");
      Fblock = 0;
      return;
   }

   //  create log window for reporting status and statistics

   if (zd_indexlog) zdialog_free(zd_indexlog);                                   //  make dialog for output log
   zd_indexlog = zdialog_new(0,Mwin,BOK,Bcancel,null);
   zdialog_set_decorated(zd_indexlog,0);
   zdialog_add_widget(zd_indexlog,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd_indexlog,"scrwin","scrwin","frame");
   zdialog_add_widget(zd_indexlog,"text","text","scrwin");
   wlog = zdialog_widget(zd_indexlog,"text");

   zdialog_resize(zd_indexlog,500,300);
   zdialog_run(zd_indexlog,indexlog_dialog_event,"parent");

   wprintf(wlog,"top image directories:\n");                                     //  log top image directories
   for (ii = 0; ii < Ntopdirks; ii++)
      wprintf(wlog," %s\n",topdirks[ii]);

   wprintf(wlog,"thumbnails directory: \n");                                     //  and thumbnails directory
   wprintf(wlog," %s \n",thumbdirk);                                             //  and thumbnails directory

   //  read image index file and build "old list" of index recs

   cc = maximages * sizeof(sxrec_t);
   sxrec_old = (sxrec_t *) zmalloc(cc);                                          //  "old" image index recs

   Nold = 0;
   ftf = 1;

   while (true)
   {
      err = read_sxrec_seq(sxrec_old[Nold],ftf);                                 //  read curr. index recs
      if (err) break;
      if (++Nold == maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d \n",maximages);              //  16.04
         Fblock = 0;
         m_quit(0,0);
      }
   }

   //  find all image files and create "new list" of index recs

   cc = maximages * sizeof(sxrec_t);
   sxrec_new = (sxrec_t *) zmalloc(cc);                                          //  "new" image index recs

   Fupdate = (char *) zmalloc(maximages);                                        //  flags, index update needed

   Nnew = 0;

   for (ii = 0; ii < Ntopdirks; ii++)
   {
      if (ii == 0) printz("top directories:\n");                                 //  16.04
      printz(" %s\n",topdirks[ii]);

      err = find_imagefiles(topdirks[ii],flist,NF,0);
      if (err) {
         zmessageACK(Mwin,"find_imagefiles() failure \n");                       //  16.04
         Fblock = 0;
         m_quit(0,0);
      }

      if (Nnew + NF > maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d \n",maximages);              //  16.04
         Fblock = 0;
         m_quit(0,0);
      }

      for (jj = 0; jj < NF; jj++)
      {
         file = flist[jj];
         nrec = Nnew++;
         sxrec_new[nrec].file = file;                                            //  image filespec                     16.04
         stat(file,&statdat);
         compact_time(statdat.st_mtime,sxrec_new[nrec].fdate);                   //  file mod date
         sxrec_new[nrec].pdate[0] = 0;                                           //  image date = empty
         strcpy(sxrec_new[nrec].rating,"0");                                     //  stars = "0"
         strcpy(sxrec_new[nrec].size,"0x0");                                     //  size = "0x0"
         sxrec_new[nrec].tags = 0;                                               //  tags = empty
         sxrec_new[nrec].capt = 0;                                               //  caption = empty
         sxrec_new[nrec].comms = 0;                                              //  comments = empty
         sxrec_new[nrec].gtags = 0;                                              //  geotags = empty
      }

      if (NF) zfree(flist);
   }

   imagesfound = Nnew;                                                           //  total images found
   wprintf(wlog,"image files found: %d \n",imagesfound);
   printz("image files found: %d \n",imagesfound);
   
   if (imagesfound == 0) {                                                       //  no images found                    15.12
      zmessageACK(Mwin,ZTX("Top directories have no images"));
      goto retry;
   }

   //  merge and compare lists
   //  if filespecs match and have the same date, then "old" record is OK
   //  otherwise flag the file for index record update

   if (Nold)
      HeapSort((char *) sxrec_old,sizeof(sxrec_t),Nold,index_compare);
   if (Nnew)
      HeapSort((char *) sxrec_new,sizeof(sxrec_t),Nnew,index_compare);
   
   updatesreq = imagesfound;

   for (orec = nrec = 0; nrec < Nnew; nrec++)                                    //  loop all image files
   {
      Fupdate[nrec] = 1;                                                         //  assume index update is needed

      if (orec == Nold) continue;                                                //  no more old recs

      while (true)
      {
         comp = strcasecmp(sxrec_old[orec].file,sxrec_new[nrec].file);           //  compare orec file to nrec file
         if (comp == 0)                                                          //  without case, then with case if match
            comp = strcmp(sxrec_old[orec].file,sxrec_new[nrec].file);
         if (comp >= 0) break;                                                   //  orec >= nrec
         orec++;                                                                 //  orec < nrec, next orec
         if (orec == Nold) break;                                                //  16.04 moved down
      }
      
      if (comp == 0)                                                             //  orec = nrec (same image file)
      {
         if (strmatch(sxrec_new[nrec].fdate,sxrec_old[orec].fdate))              //  file dates match
         {
            Fupdate[nrec] = 0;                                                   //  index update not needed
            updatesreq--;
            strncpy0(sxrec_new[nrec].pdate,sxrec_old[orec].pdate,15);            //  copy data from old to new
            strncpy0(sxrec_new[nrec].rating,sxrec_old[orec].rating,2);
            strncpy0(sxrec_new[nrec].size,sxrec_old[orec].size,15);
            sxrec_new[nrec].tags = sxrec_old[orec].tags;
            sxrec_old[orec].tags = 0;
            sxrec_new[nrec].capt = sxrec_old[orec].capt;
            sxrec_old[orec].capt = 0;
            sxrec_new[nrec].comms = sxrec_old[orec].comms;
            sxrec_old[orec].comms = 0;
            sxrec_new[nrec].gtags = sxrec_old[orec].gtags;
            sxrec_old[orec].gtags = 0;
         }
         orec++;                                                                 //  next orec
      }
   }

   wprintf(wlog,"index updates needed: %d \n",updatesreq);

   indexupdates = thumbupdates = sumupdates = thumbdeletes = 0;
   Findexdone = Fuserkill = 0;

   wprintf(wlog,"updating image index and thumbnails ... \n");
   wprintf(wlog,-2,statsmess,indexupdates,thumbupdates,thumbdeletes);
   zmainloop();

   //  Process entries needing update in the new index list
   //  (new files or files dated later than image index date).
   //  Get updated metadata from image file EXIF/IPTC data.
   
   start_detached_thread(index_thread,0);                                        //  parallel thread does this part     16.06

   //  find missing and stale thumbnails and update them

   for (nrec = 0; nrec < Nnew; nrec++)                                           //  loop all index recs
   {
      if (Fuserkill) break;                                                      //  killed by user                     16.04
      file = sxrec_new[nrec].file;                                               //  image file
      thumbfile = image_thumbfile(file,&Fnew);                                   //  update thumbnail if missing or stale
      if (thumbfile) zfree(thumbfile);
      if (Fnew) thumbupdates++;                                                  //  count thumbnail updates
      if (indexupdates + thumbupdates + thumbdeletes > sumupdates) {             //  update progress counters
         wprintf(wlog,-2,statsmess,indexupdates,thumbupdates,thumbdeletes);
         sumupdates = indexupdates + thumbupdates + thumbdeletes;
         zmainloop();
      }
   }

   //  find orphan thumbnails and delete them
   
   if (! Fuserkill)
   {
      snprintf(command,CCC,"find %s -type f",thumbdirk);
      fid = popen(command,"r");
      if (! fid) zappcrash("find command: %s",strerror(errno));                  //  15.03

      while (true)                                                               //  16.04
      {
         thumbfile = fgets_trim(buff,XFCC,fid);
         if (! thumbfile) break;
         file = thumb2imagefile(thumbfile);
         if (file) { zfree(file); continue; }
         remove(thumbfile);
         thumbdeletes++;
         wprintf(wlog,-2,statsmess,indexupdates,thumbupdates,thumbdeletes);
         zmainloop();
         if (Fuserkill) break;                                                   //  killed by user                     16.04
      }

      pclose(fid);
   }

   //  wait for parallel thread to be completed

   while (! Findexdone)                                                          //  wait for parallel process          16.04
   {
      zmainloop();                                                               //  keep GTK alive                     16.04
      wprintf(wlog,-2,statsmess,indexupdates,thumbupdates,thumbdeletes);
      zsleep(0.01);
   }
   
   if (Fuserkill) goto retry;

   wprintf(wlog,"%s\n",Bcompleted);                                              //  index complete and OK

   printz(statsmess,indexupdates,thumbupdates,thumbdeletes);                     //  16.04
   printz("index time: %.1f seconds \n",get_seconds() - startime);               //  log elapsed time                   15.08

   FindexOK = 1;                                                                 //  completed OK
   Fnoindex = 0;                                                                 //  -noindex option moot

   Fretry = 0;                                                                   //  normal completion
   goto cleanup;

retry:                                                                           //  error or user cancel               16.04
   Fretry = 1;                                                                   //  (user can try again or quit) 

cleanup:                                                                         //  free allocated memory              16.04

   if (sxrec_old)
   {
      for (orec = 0; orec < Nold; orec++)
      {
         zfree(sxrec_old[orec].file);
         if (sxrec_old[orec].tags) zfree(sxrec_old[orec].tags);
         if (sxrec_old[orec].capt) zfree(sxrec_old[orec].capt);
         if (sxrec_old[orec].comms) zfree(sxrec_old[orec].comms);
         if (sxrec_old[orec].gtags) zfree(sxrec_old[orec].gtags);
      }

      zfree(sxrec_old);
      sxrec_old = 0;
   }
   
   if (sxrec_new)
   {
      for (nrec = 0; nrec < Nnew; nrec++)
      {
         zfree(sxrec_new[nrec].file);
         if (sxrec_new[nrec].tags) zfree(sxrec_new[nrec].tags);
         if (sxrec_new[nrec].capt) zfree(sxrec_new[nrec].capt);
         if (sxrec_new[nrec].comms) zfree(sxrec_new[nrec].comms);
         if (sxrec_new[nrec].gtags) zfree(sxrec_new[nrec].gtags);
      }

      zfree(sxrec_new);
      sxrec_new = 0;
   }

   if (Fupdate)
   {
      zfree(Fupdate);
      Fupdate = 0;
   }

   if (menu == 0 && zd_indexlog) {                                               //  kill log window
      for (ii = 0; ii < 10; ii++) {
         zmainloop();                                                            //  push out last message
         zsleep(0.1);
      }
      zdialog_send_event(zd_indexlog,"kill");
   }   

   Fblock = 0;                                                                   //  unblock
   if (Fretry) m_index(0,"retry");                                               //  retry if wanted                    16.04
   return;
}


// ------------------------------------------------------------------------------

//  thread process - get image file metadata and build index data                //  16.06
//  thread has no GTK calls

void * index_thread(void *)
{
   using namespace index_names;
   
   int      nrec, err, ftf;
   char     *file, *pp;

   cchar    *exifkeys[11] = { "FileName", exif_date_key, iptc_keywords_key,
            iptc_rating_key, exif_size_key, exif_comment_key, iptc_caption_key,
            exif_city_key, exif_country_key, exif_lati_key, exif_longi_key };

   char     *ppv[11], *filename;
   char     *exifdate, *iptctags, *iptcrating;
   char     *exifsize, *iptccapt, *exifcomms;
   char     *exifcity, *exifcountry, *exiflat, *exiflong;
   char     city2[100], country2[100], lat2[20], long2[20], gtags2[200];


   for (nrec = 0; nrec < Nnew; nrec++)                                           //  loop all index recs
   {
      file = sxrec_new[nrec].file;                                               //  image file
      if (! Fupdate[nrec]) continue;                                             //  no update needed

      err = exif_get(file,exifkeys,ppv,11);                                      //  get iptc/exif metadata
      if (err) {
         printz("exif_get() failure: %s \n",file);                               //  metadata unreadable
         continue;
      }

      filename = ppv[0];                                                         //  iptc/exif metadata
      exifdate = ppv[1];
      iptctags = ppv[2];
      iptcrating = ppv[3];
      exifsize = ppv[4];
      exifcomms = ppv[5];
      iptccapt = ppv[6];
      exifcity = ppv[7];
      exifcountry = ppv[8];
      exiflat = ppv[9];
      exiflong = ppv[10];

      pp = strrchr(file,'/');
      if (! filename || ! strmatch(filename,pp+1)) {                             //  image file has no metadata
         printz("exif_get() no data: %s \n",file);
         continue;
      }

      if (exifdate && strlen(exifdate) > 3)                                      //  exif date (photo date)
         exif_tagdate(exifdate,sxrec_new[nrec].pdate);
      else strcpy(sxrec_new[nrec].pdate,"null");                                 //  not present

      if (iptcrating && strlen(iptcrating)) {                                    //  iptc rating
         sxrec_new[nrec].rating[0] = *iptcrating;
         sxrec_new[nrec].rating[1] = 0;
      }
      else strcpy(sxrec_new[nrec].rating,"0");                                   //  not present

      if (exifsize && strlen(exifsize))                                          //  exif size
         strncpy0(sxrec_new[nrec].size,exifsize,15);

      if (iptctags && strlen(iptctags)) {                                        //  iptc tags
         sxrec_new[nrec].tags = iptctags;
         iptctags = 0;
      }

      if (iptccapt && strlen(iptccapt)) {                                        //  iptc caption
         sxrec_new[nrec].capt = iptccapt;
         iptccapt = 0;
      }

      if (exifcomms && strlen(exifcomms)) {                                      //  exif comments
         sxrec_new[nrec].comms = exifcomms;
         exifcomms = 0;
      }

      strcpy(city2,"null");                                                      //  geotags = empty
      strcpy(country2,"null");
      strcpy(lat2,"null");
      strcpy(long2,"null");

      if (exifcity) strncpy0(city2,exifcity,99);                                 //  get from exif if any
      if (exifcountry) strncpy0(country2,exifcountry,99);
      if (exiflat) strncpy0(lat2,exiflat,10);
      if (exiflong) strncpy0(long2,exiflong,10);

      *gtags2 = 0;
      strncatv(gtags2,200,city2,"^ ",country2,"^ ",lat2,"^ ",long2,null);
      sxrec_new[nrec].gtags = zstrdup(gtags2);

      zfree(filename);
      if (exifdate) zfree(exifdate);                                             //  free EXIF data
      if (iptctags) zfree(iptctags);
      if (iptcrating) zfree(iptcrating);
      if (exifsize) zfree(exifsize);
      if (exifcomms) zfree(exifcomms);
      if (iptccapt) zfree(iptccapt);
      if (exifcity) zfree(exifcity);
      if (exifcountry) zfree(exifcountry);
      if (exiflat) zfree(exiflat);
      if (exiflong) zfree(exiflong);

      indexupdates++;                                                            //  count index updates                16.04
      if (Fuserkill) break;                                                      //  killed by user                     16.04
      zsleep(0.002);                                                             //  deliberate slowdown                16.04
   }

   //  write updated index records to index file
   
   if (! Fuserkill)                                                              //  16.04
   {
      ftf = 1;
      for (nrec = 0; nrec < Nnew; nrec++) {
         err = write_sxrec_seq(&sxrec_new[nrec],ftf);
         if (err) zappcrash("update image index: %s",strerror(errno));           //  15.03
      }
      write_sxrec_seq(null,ftf);                                                 //  close output
   }

   Findexdone = 1;                                                               //  thread completed
   pthread_exit(0);
}


// ------------------------------------------------------------------------------

//  index log window dialog response function

int indexlog_dialog_event(zdialog *zd, cchar *event)
{
   using namespace index_names;

   int      nn;
   cchar    *canmess = ZTX("Indexing is required for first-time startup.");

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"kill")) {                                                 //  auto-kill from index_rebuild()
      zdialog_free(zd);
      zd_indexlog = 0;
      return 1;
   }

   if (zd->zstat == 1) {                                                         //  [OK] button
      if (FindexOK) {     
         zdialog_free(zd);                                                       //  index completed, kill dialog
         zd_indexlog = 0;
         return 1;
      }
   }
   
   if (zd->zstat == 2) {                                                         //  [Cancel] button
      if (FindexOK) {
         zdialog_free(zd);                                                       //  index completed, kill dialog
         zd_indexlog = 0;
         Fuserkill = 1;                                                          //  16.04
         return 1;
      }
      nn = zdialog_choose(Mwin,canmess,Bcontinue,Bcancel,null);                  //  ask for confirmation               15.07
      if (nn == 1) {
         zd->zstat = 0;                                                          //  continue
         return 1;
      }
      zdialog_free(zd);                                                          //  cancel
      zd_indexlog = 0;
      Fuserkill = 1;                                                             //  16.04
      return 1;
   }

   zd->zstat = 0;                                                                //  keep dialog open while index busy
   return 1;
}


// ------------------------------------------------------------------------------

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

int index_compare(cchar *rec1, cchar *rec2)
{
   char * file1 = ((sxrec_t *) rec1)->file;
   char * file2 = ((sxrec_t *) rec2)->file;
   int nn = strcasecmp(file1,file2);                                             //  compare ignoring case
   if (nn) return nn;
   nn = strcmp(file1,file2);                                                     //  if equal, use utf8 compare
   return nn;
}


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

//  user options dialog

namespace useroptions
{
   int      options_fclone;

   cchar    *startopt[7][2] = {  
               "recent", ZTX("Recent Files Gallery"),                            //  fotoxx startup display options
               "newest", ZTX("Newest Files Gallery"),
               "prevG",  ZTX("Previous Gallery"),
               "prevI",  ZTX("Previous Image"),
               "blank",  ZTX("Blank Window"),
               "dirk",   ZTX("Directory"),
               "file",   ZTX("Image File")  };
}


//  menu function

void m_options(GtkWidget *, cchar *)
{
   using namespace useroptions;
   
   int   options_dialog_event(zdialog *zd, cchar *event);

   int      ii;   
   zdialog  *zd;
   char     thumbsize[8];
   char     fbgcolor[20], gbgcolor[20];

/***
       ____________________________________________________________________
      |                          User Options                              |
      |                                                                    |
      |  Startup Display [ previous image |v]                              |
      |  [Browse] [______________________________________________________] |
      |                                                                    |
      |  Menu Style  (o) Icons  (o) Icons + Text     Icon Size [___|-+]    |
      |  Background color: File View [#####]  Gallery View [#####]         |                                            16.08
      |  Gallery Sort  (o) alphabetic  (o) mod date  (o) photo date        |
      |  Dialog font and size  [sans 10 _________] [choose]                |
      |  Image Pan/Scroll: [drag magnified |v]      Zooms for 2x [___|-+]  |
      |  JPEG save quality [____|-+]     Thumbnail size [___|v]            |
      |  Curve node capture distance [____|-+]                             |
      |  Map marker size [___|-+]                                          |     //  OSM                                16.05
      |  [x] show hidden directories in gallery view                       |
      |  [x] prev/next shows last file version only                        |
      |  [x] shift image right when editing                                |
      |  RAW file types  [_______________________________________________] |
      |                                                                    |
      |                                                             [done] |
      |____________________________________________________________________|

***/

   F1_help_topic = "user_options";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

   zd = zdialog_new(ZTX("User Options"),Mwin,Bdone,null);

   zdialog_add_widget(zd,"hbox","hbstart","dialog");
   zdialog_add_widget(zd,"label","labstart","hbstart",ZTX("Startup Display"),"space=5");
   zdialog_add_widget(zd,"combo","startopt","hbstart",0);
   
   for (ii = 0; ii < 7; ii++)
      zdialog_cb_app(zd,"startopt",startopt[ii][1]);                             //  startup display option             15.07

   zdialog_add_widget(zd,"hbox","hbbrowse","dialog","space=3");
   zdialog_add_widget(zd,"button","browse","hbbrowse",Bbrowse,"space=5");
   zdialog_add_widget(zd,"entry","browsefile","hbbrowse",0,"expand");

   zdialog_add_widget(zd,"hbox","hbmenu","dialog");                              //  menu style
   zdialog_add_widget(zd,"label","labms","hbmenu",ZTX("Menu Style"),"space=5");
   zdialog_add_widget(zd,"radio","icons","hbmenu",ZTX("Icons"),"space=3");
   zdialog_add_widget(zd,"radio","both","hbmenu",ZTX("Icons + Text"),"space=5");
   zdialog_add_widget(zd,"label","space","hbmenu",0,"space=10");
   zdialog_add_widget(zd,"label","labis","hbmenu",ZTX("Icon size"));
   zdialog_add_widget(zd,"spin","iconsize","hbmenu","32|48|1|40","space=3");
   
   zdialog_add_widget(zd,"hbox","hbcolor","dialog");                             //  window background color            16.07
   zdialog_add_widget(zd,"label","labbg","hbcolor",ZTX("Background color:"),"space=5");
   zdialog_add_widget(zd,"label","labfbg","hbcolor",ZTX("File View"),"space=5");
   zdialog_add_widget(zd,"colorbutt","Fbgcolor","hbcolor");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=8");
   zdialog_add_widget(zd,"label","labgbg","hbcolor",ZTX("Gallery View"),"space=5");
   zdialog_add_widget(zd,"colorbutt","Gbgcolor","hbcolor");
   
   zdialog_add_widget(zd,"hbox","hbsort","dialog");                              //  gallery sort                       16.01
   zdialog_add_widget(zd,"label","labsort","hbsort",ZTX("Gallery Sort"),"space=5");
   zdialog_add_widget(zd,"radio","sortalpha","hbsort",ZTX("alphabetic"),"space=3");
   zdialog_add_widget(zd,"radio","sortfile","hbsort",ZTX("file mod date"),"space=5");
   zdialog_add_widget(zd,"radio","sortphoto","hbsort",ZTX("photo date"),"space=5");
   
   zdialog_add_widget(zd,"hbox","hbfont","dialog");                              //  screen font                        15.09
   zdialog_add_widget(zd,"label","labfont","hbfont",ZTX("Dialog font and size"),"space=5");
   zdialog_add_widget(zd,"entry","font","hbfont","Sans 10","size=20");
   zdialog_add_widget(zd,"button","choosefont","hbfont",Bchoose,"space=5");
   
   zdialog_add_widget(zd,"hbox","hbips","dialog");
   zdialog_add_widget(zd,"label","labips","hbips",ZTX("Image Pan/scroll:"),"space=5");
   zdialog_add_widget(zd,"combo","panscroll","hbips",0);
   zdialog_add_widget(zd,"label","space","hbips",0,"space=10");
   zdialog_add_widget(zd,"label","labzoom","hbips",ZTX("Zooms for 2x"),"space=5");
   zdialog_add_widget(zd,"spin","zoomcount","hbips","1|8|1|2");
   
   zdialog_cb_app(zd,"panscroll","drag");                                        //  drag/scroll option                 15.07
   zdialog_cb_app(zd,"panscroll","scroll");
   zdialog_cb_app(zd,"panscroll","drag magnified");
   zdialog_cb_app(zd,"panscroll","scroll magnified");

   zdialog_add_widget(zd,"hbox","hbjpeg","dialog");
   zdialog_add_widget(zd,"label","labqual","hbjpeg",ZTX("JPEG save quality"),"space=5");
   zdialog_add_widget(zd,"spin","quality","hbjpeg","1|100|1|90");
   zdialog_add_widget(zd,"label","space","hbjpeg",0,"space=10");
   zdialog_add_widget(zd,"label","labthumb","hbjpeg",ZTX("Thumbnail size"),"space=5");
   zdialog_add_widget(zd,"combo","thumbsize","hbjpeg",0,"space=5");
   
   zdialog_add_widget(zd,"hbox","hbcap","dialog");                               //                                     15.10
   zdialog_add_widget(zd,"label","labcap","hbcap",ZTX("Curve node capture distance"),"space=5");
   zdialog_add_widget(zd,"spin","nodecap","hbcap","3|20|1|5");

   zdialog_add_widget(zd,"hbox","hbosm","dialog");                               //  OSM                                16.05
   zdialog_add_widget(zd,"label","labosm","hbosm",ZTX("Map marker size"),"space=5");
   zdialog_add_widget(zd,"spin","map_dotsize","hbosm","5|20|1|8");

   zdialog_add_widget(zd,"hbox","hbhidden","dialog");
   zdialog_add_widget(zd,"check","showhidden","hbhidden",ZTX("show hidden directories in gallery view"),"space=5");

   zdialog_add_widget(zd,"hbox","hblastver","dialog");
   zdialog_add_widget(zd,"check","lastver","hblastver",ZTX("prev/next shows last file version only"),"space=5");

   zdialog_add_widget(zd,"hbox","hbshiftright","dialog");
   zdialog_add_widget(zd,"check","shiftright","hbshiftright",ZTX("shift image to right margin"),"space=5");

   zdialog_add_widget(zd,"hbox","hbrawfile","dialog");
   zdialog_add_widget(zd,"label","rawlab2","hbrawfile",ZTX("RAW file types"),"space=5");
   zdialog_add_widget(zd,"entry","rawtypes","hbrawfile",".raw .rw2","expand");


   for (ii = 0; ii < 7; ii++) {
      if (strmatch(startdisplay,startopt[ii][0]))                                //  set startup display option         15.07
         zdialog_stuff(zd,"startopt",startopt[ii][1]);
   }

   if (strmatch(startdisplay,"dirk"))                                            //  if directory or file option,       15.07
      zdialog_stuff(zd,"browsefile",startdirk);                                  //    set current directory or file
   if (strmatch(startdisplay,"file")) 
      zdialog_stuff(zd,"browsefile",startfile);
   
   zdialog_stuff(zd,"icons",0);                                                  //  menu style
   zdialog_stuff(zd,"both",0);
   if (strmatch(menu_style,"icons"))
      zdialog_stuff(zd,"icons",1);
   else zdialog_stuff(zd,"both",1);
   
   snprintf(fbgcolor,20,"%d|%d|%d",Fbgcolor[0],Fbgcolor[1],Fbgcolor[2]);         //  F-view background color            16.07
   zdialog_stuff(zd,"Fbgcolor",fbgcolor);
   snprintf(gbgcolor,20,"%d|%d|%d",Gbgcolor[0],Gbgcolor[1],Gbgcolor[2]);         //  G-view background color            16.08
   zdialog_stuff(zd,"Gbgcolor",gbgcolor);
   
   zdialog_stuff(zd,"sortalpha",0);                                              //  gallery sort option                16.01
   zdialog_stuff(zd,"sortfile",0);
   zdialog_stuff(zd,"sortphoto",0);
   
   if (gallerysort == 1) zdialog_stuff(zd,"sortalpha",1);
   if (gallerysort == 2) zdialog_stuff(zd,"sortfile",1);
   if (gallerysort == 3) zdialog_stuff(zd,"sortphoto",1);

   zdialog_stuff(zd,"iconsize",iconsize);                                        //  icon size
   
   zdialog_stuff(zd,"font",dialog_font);                                         //  curr. dialog font                  16.05
   
   if (Fdragopt == 1) zdialog_stuff(zd,"panscroll","drag");                      //  set pan/scroll option              15.07
   if (Fdragopt == 2) zdialog_stuff(zd,"panscroll","scroll");
   if (Fdragopt == 3) zdialog_stuff(zd,"panscroll","drag magnified");
   if (Fdragopt == 4) zdialog_stuff(zd,"panscroll","scroll magnified");

   if (zoomcount >= 1 && zoomcount <= 8)                                         //  zooms for 2x increase
      zdialog_stuff(zd,"zoomcount",zoomcount);

   zdialog_stuff(zd,"quality",jpeg_def_quality);                                 //  default jpeg file save quality

   zdialog_cb_app(zd,"thumbsize","128");                                         //  thumbnail file pixel size          15.02
   zdialog_cb_app(zd,"thumbsize","256");
   zdialog_cb_app(zd,"thumbsize","512");
   snprintf(thumbsize,8,"%d",thumbfilesize);                                     //  preload with current value
   zdialog_stuff(zd,"thumbsize",thumbsize);
   
   zdialog_stuff(zd,"nodecap",splcurve_minx);                                    //  edit curve min. node distance      15.10
   zdialog_stuff(zd,"map_dotsize",map_dotsize);                                  //  map dot size                       16.05

   zdialog_stuff(zd,"showhidden",Fshowhidden);                                   //  show hidden dirs in gallery view   15.04
   zdialog_stuff(zd,"lastver",Flastversion);                                     //  prev/next shows last version only
   zdialog_stuff(zd,"shiftright",Fshiftright);                                   //  shift image to right margin        16.01

   zdialog_stuff(zd,"rawtypes",RAWfiletypes);                                    //  RAW file types

   options_fclone = 0;

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

   Fblock = 0;

   if (options_fclone) {                                                         //  start new and exit current fotoxx
      m_clone(0,0);
      m_quit(0,0);
   }

   return;
}


//  options dialog event function

int options_dialog_event(zdialog *zd, cchar *event)
{
   using namespace useroptions;

   int            ii, jj, nn;
   char           *pp, temp[200];
   cchar          *ppc;
   static char    browsefile[500] = "";
   char           fbgcolor[20], gbgcolor[20];
   GtkWidget      *font_dialog;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"thumbsize")) {                                            //  warn if thumbfilesize is changed   15.02
      zdialog_fetch(zd,"thumbsize",nn);
      if (nn != thumbfilesize)
         zmessageACK(Mwin,ZTX("Delete present thumbnails to make effective"));
   }

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]

   if (zd->zstat && zd->zstat != 1) return 1;                                    //  cancel

   if (strmatch(event,"browse"))                                                 //  browse for startup directory or file
   {
      zdialog_fetch(zd,"startopt",temp,200);                                     //  get startup option
      for (ii = 0; ii < 7; ii++) {
         if (strmatch(temp,startopt[ii][1])) {
            zfree(startdisplay);
            startdisplay = zstrdup(startopt[ii][0]);
         }
      }
      
      zdialog_fetch(zd,"browsefile",browsefile,500);                             //  initial browse location            15.08
      if (! *browsefile && topdirks[0]) 
         strncpy0(browsefile,topdirks[0],500);

      if (strmatch(startdisplay,"dirk")) {
         pp = zgetfile(ZTX("Select startup directory"),MWIN,"folder",browsefile);
         if (! pp) return 1;
         zdialog_stuff(zd,"browsefile",pp);
         zfree(pp);
      }

      if (strmatch(startdisplay,"file")) {
         pp = zgetfile(ZTX("Select startup image file"),MWIN,"file",browsefile);
         if (! pp) return 1;
         zdialog_stuff(zd,"browsefile",pp);
         zfree(pp);
      }
   }
   
   if (strmatch(event,"choosefont"))                                             //  choose menu/dialog font
   {
      zdialog_fetch(zd,"font",temp,200);
      font_dialog = gtk_font_chooser_dialog_new(ZTX("select font"),MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),temp);
      gtk_dialog_run(GTK_DIALOG(font_dialog));
      pp = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(font_dialog));
      gtk_widget_destroy(font_dialog);
      if (! pp) return 1;
      zdialog_stuff(zd,"font",pp);
      zsetfont(pp);                                                              //  16.05
      dialog_font = zstrdup(pp);
      g_free(pp);
   }
   
   if (zd->zstat != 1) return 1;                                                 //  wait for dialog completion

   zdialog_fetch(zd,"startopt",temp,200);                                        //  get startup option
   for (ii = 0; ii < 7; ii++) {
      if (strmatch(temp,startopt[ii][1])) {
         zfree(startdisplay);
         startdisplay = zstrdup(startopt[ii][0]);
      }
   }

   if (strmatch(startdisplay,"dirk")) {                                          //  startup option = directory 
      zdialog_fetch(zd,"browsefile",browsefile,500);                             //  startup directory
      if (startdirk) zfree(startdirk);
      startdirk = zstrdup(browsefile);
      if (image_file_type(startdirk) != FDIR) {
         zmessageACK(Mwin,ZTX("startup directory is invalid"));
         zd->zstat = 0;
         return 1;
      }
   }

   if (strmatch(startdisplay,"file")) {                                          //  startup option = image file
      zdialog_fetch(zd,"browsefile",browsefile,500);                             //  startup file
      if (startfile) zfree(startfile);
      startfile = zstrdup(browsefile);
      if (image_file_type(startfile) != IMAGE) {
         zmessageACK(Mwin,ZTX("startup file is invalid"));
         zd->zstat = 0;
         return 1;
      }
   }

   zdialog_fetch(zd,"icons",nn);                                                 //  menu type = icons
   if (nn) {
      if (! strmatch(menu_style,"icons")) {
         zfree(menu_style);
         menu_style = zstrdup("icons");
         options_fclone++;
      }
   }

   zdialog_fetch(zd,"both",nn);                                                  //  menu type = icons + text
   if (nn) {
      if (! strmatch(menu_style,"both")) {
         zfree(menu_style);
         menu_style = zstrdup("both");
         options_fclone++;
      }
   }

   zdialog_fetch(zd,"iconsize",nn);                                              //  icon size
   if (nn != iconsize) {
      iconsize = nn;
      options_fclone++;
   }
   
   zdialog_fetch(zd,"Fbgcolor",fbgcolor,20);                                     //  F-view background color            16.07
   ppc = strField(fbgcolor,"|",1);
   if (ppc) Fbgcolor[0] = atoi(ppc);
   ppc = strField(fbgcolor,"|",2);
   if (ppc) Fbgcolor[1] = atoi(ppc);
   ppc = strField(fbgcolor,"|",3);
   if (ppc) Fbgcolor[2] = atoi(ppc);

   zdialog_fetch(zd,"Gbgcolor",gbgcolor,20);                                     //  G-view background color            16.08
   ppc = strField(gbgcolor,"|",1);
   if (ppc) Gbgcolor[0] = atoi(ppc);
   ppc = strField(gbgcolor,"|",2);
   if (ppc) Gbgcolor[1] = atoi(ppc);
   ppc = strField(gbgcolor,"|",3);
   if (ppc) Gbgcolor[2] = atoi(ppc);

   gallerysort = 1;                                                              //  gallery sort option
   zdialog_fetch(zd,"sortalpha",nn);
   if (nn) gallerysort = 1;
   zdialog_fetch(zd,"sortfile",nn);
   if (nn) gallerysort = 2;
   zdialog_fetch(zd,"sortphoto",nn);
   if (nn) gallerysort = 3;
   
   zdialog_fetch(zd,"panscroll",temp,200);                                       //  get drag/scroll option             15.07
   if strmatch(temp,"drag") Fdragopt = 1;
   if strmatch(temp,"scroll") Fdragopt = 2;
   if strmatch(temp,"drag magnified") Fdragopt = 3;
   if strmatch(temp,"scroll magnified") Fdragopt = 4;

   zdialog_fetch(zd,"zoomcount",zoomcount);                                      //  zooms for 2x image, 1-4
   zoomratio = pow( 2.0, 1.0 / zoomcount);                                       //  2.0, 1.4142, 1.2599, 1.1892

   zdialog_fetch(zd,"quality",jpeg_def_quality);                                 //  default jpeg file save quality
   zdialog_fetch(zd,"thumbsize",thumbfilesize);                                  //  thumbnail file pixel size          15.02
   zdialog_fetch(zd,"nodecap",splcurve_minx);                                    //  edit curve min. node distance      15.10
   zdialog_fetch(zd,"map_dotsize",map_dotsize);                                  //  map dot size                       16.05
   zdialog_fetch(zd,"showhidden",Fshowhidden);                                   //  show hidden dirs in gallery view   15.04
   zdialog_fetch(zd,"lastver",Flastversion);                                     //  prev/next shows last version only
   zdialog_fetch(zd,"shiftright",Fshiftright);                                   //  shift image to right margin        16.01

   zdialog_fetch(zd,"rawtypes",temp,200);                                        //  RAW file types, .raw .rw2 ...
   pp = zstrdup(temp,100);

   for (ii = jj = 0; temp[ii]; ii++) {                                           //  insure blanks between .raw types
      if (temp[ii] == '.' && temp[ii-1] != ' ') pp[jj++] = ' ';
      pp[jj++] = temp[ii];
   }
   if (pp[jj-1] != ' ') pp[jj++] = ' ';                                          //  insure 1 final blank
   pp[jj] = 0;

   if (RAWfiletypes) zfree(RAWfiletypes);
   RAWfiletypes = pp;

   save_params();                                                                //  done, save modified parameters

   return 1;
}


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

//  edit keyboard shortcuts

namespace KBshortcutnames
{
   zdialog     *zd;

   char        *shortcutkey2[maxshortcuts];                                      //  KB shortcut keys (or combinations)
   char        *shortcutmenu2[maxshortcuts];                                     //  corresponding menu names
   int         Nshortcuts2;                                                      //  count of entries

   int         Nreserved = 11;                                                   //  reserved shortcuts (hard coded)
   cchar       *reserved[11] = { "F", "G", "W", "M", "F1", "+", "=", "-",        //  use upper case for this table
                                 "Z", "S", "X"  };                               //  (upper case X is Shift+X) 
   int         Nallowed = 26;
   cchar       *allowed[26];                                                     //  allowed shortcuts
   cchar       *allowed_en[26] = {
                  "Open Image File",            "Open Previous File", 
                  "Rename Image File",          "Save File Version", 
                  "Delete/Trash Image File",    "Quit Fotoxx",
                  "Show RGB",                   "Grid Lines", 
                  "Magnify Image",              "Edit Metadata", 
                  "View Metadata (short)",      "Select", 
                  "Trim/Rotate",                "Resize", 
                  "Add Text",                   "Voodoo 1",
                  "Voodoo 2",                   "Retouch Combo", 
                  "Tone Mapping",               "Zonal Flatten", 
                  "Edit Brightness",            "Sharpen", 
                  "Red Eyes",                   "Undo", 
                  "Redo",                       "Keyboard Shortcuts"  };
}


//  KB shortcuts menu function - list current shortcuts  

void m_KBshortcuts(GtkWidget *, cchar *)                                         //  15.12
{
   using namespace KBshortcutnames;
   
   void KBshortcuts_edit();

   int         zstat;
   GtkWidget   *widget;
   char        textline[100];

   F1_help_topic = "KB_shortcuts";

   zd = zdialog_new(ZTX("Keyboard Shortcuts"),Mwin,Bedit,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hblist","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frlist","hblist",0,"space=5|expand");
   zdialog_add_widget(zd,"text","shortlist","frlist",0,"expand");

   widget = zdialog_widget(zd,"shortlist");

   wprintx(widget,0,"Reserved Shortcuts \n",1);
   wprintx(widget,0," F   image File view mode \n");
   wprintx(widget,0," G   Gallery view mode \n");
   wprintx(widget,0," W   World maps view mode \n");
   wprintx(widget,0," M   OSM maps view mode \n");
   wprintx(widget,0," F1  User Guide topic for current function \n");
   wprintx(widget,0," +   zoom-in \n");
   wprintx(widget,0," =   zoom-in \n");
   wprintx(widget,0," -   zoom-out \n");
   wprintx(widget,0," Z   toggle image full size / fit window \n");
   wprintx(widget,0," S   sync gallery to current image file \n");
   wprintx(widget,0," X   magnify image at mouse drag \n");
   wprintx(widget,0,"\n");
   wprintx(widget,0,"The above may be used for custom shortcuts \n");
   wprintx(widget,0,"only in combination with Ctrl/Alt/Shift. \n");
   wprintx(widget,0,"\n");

   wprintx(widget,0,"Custom Shortcuts \n",1);

   for (int ii = 0; ii < Nshortcuts; ii++) {
      snprintf(textline,100," %-20s %s \n",shortcutkey[ii],shortcutmenu[ii]);
      wprintx(widget,0,textline);
   }

   zdialog_run(zd,0);
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   if (zstat != 1) return;
   else KBshortcuts_edit();
   return;
}


//  KB shortcuts edit dialog

void KBshortcuts_edit()
{
   using namespace KBshortcutnames;

   void KBshorts_clickfunc(GtkWidget *widget, int line, int pos);
   int  KBshorts_keyfunc(GtkWidget *dialog, GdkEventKey *event);
   int  KBshorts_dialog_event(zdialog *zd, cchar *event);

   int         ii, cc;
   GtkWidget   *widget;
   char        textline[100];

/***
          _______________________________________________
         |              Edit KB Shortcuts                |
         |_______________________________________________|
         | Alt+G                  Grid Lines             |
         | Alt+U                  Undo                   |
         | Alt+R                  Redo                   |
         | T                      Trim/Rotate            |
         | ...                    ...                    |
         |_______________________________________________|
         |                                               |
         | shortcut key: [ CTRL+G ] [ (dropdown list)  ] |
         |                                               |
         |                      [delete] [done] [cancel] |
         |_______________________________________________|

***/

   zd = zdialog_new(ZTX("Edit KB Shortcuts"),Mwin,Bdelete,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hblist","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frlist","hblist",0,"space=5|expand");
   zdialog_add_widget(zd,"text","shortlist","frlist",0,"expand");
   zdialog_add_widget(zd,"hbox","hbshort","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labshort","hbshort",ZTX("shortcut key:"),"space=5");
   zdialog_add_widget(zd,"label","shortkey","hbshort",ZTX("(enter key)"));
   zdialog_add_widget(zd,"label","space","hbshort",0,"expand");
   zdialog_add_widget(zd,"comboE","shortmenu","hbshort",0,"space=5");

   widget = zdialog_widget(zd,"shortlist");
   textwidget_set_clickfunc(widget,KBshorts_clickfunc);

   cc = maxshortcuts * sizeof(char *);
   memset(shortcutkey2,0,cc);
   memset(shortcutmenu2,0,cc);

   for (ii = 0; ii < Nallowed; ii++)                                             //  use translated menu names
      allowed[ii] = ZTX(allowed_en[ii]);

   for (ii = 0; ii < Nshortcuts; ii++) {                                         //  copy global shortcuts list
      shortcutkey2[ii] = zstrdup(shortcutkey[ii]);                               //    for subsequent editing
      shortcutmenu2[ii] = zstrdup(shortcutmenu[ii]);
   }
   Nshortcuts2 = Nshortcuts;

   wclear(widget);

   for (int ii = 0; ii < Nshortcuts2; ii++) {                                    //  show shortcuts list in dialog
      snprintf(textline,100,"%-20s %s \n",shortcutkey2[ii],shortcutmenu2[ii]);
      wprintx(widget,0,textline);
   }

   for (int ii = 0; ii < Nallowed; ii++)                                         //  add allowed menu names to
      zdialog_cb_app(zd,"shortmenu",allowed[ii]);                                //    combobox dropdown list

   widget = zdialog_widget(zd,"dialog");
   G_SIGNAL(widget,"key-press-event",KBshorts_keyfunc,0);
   G_SIGNAL(widget,"key-release-event",KBshorts_keyfunc,0);

   zdialog_resize(zd,420,0);
   zdialog_run(zd,KBshorts_dialog_event,"save");
   return;
}


//  mouse click functions to select existing shortcut from list

void KBshorts_clickfunc(GtkWidget *widget, int line, int pos)
{
   using namespace KBshortcutnames;

   char     *txline;
   char     shortkey[20];
   char     shortmenu[60];

   txline = textwidget_get_line(widget,line,1);
   if (! txline) return;

   strncpy0(shortkey,txline,20);
   strncpy0(shortmenu,txline+21,60);
   zfree(txline);

   strTrim(shortkey);
   strTrim(shortmenu);

   zdialog_stuff(zd,"shortkey",shortkey);
   zdialog_stuff(zd,"shortmenu",shortmenu);

   return;
}


//  intercept KB key events

int KBshorts_keyfunc(GtkWidget *dialog, GdkEventKey *event)
{
   using namespace KBshortcutnames;

   static int  Ctrl = 0, Alt = 0, Shift = 0;
   int         key, type, ii, cc;
   char        keyname[20];

   key = event->keyval;
   type = event->type;

   if (type == GDK_KEY_PRESS) type = 1;
   else type = 0;

   if (key == GDK_KEY_Control_L || key == GDK_KEY_Control_R) {                   //  Ctrl key
       Ctrl = type;
       return 1;
   }

   if (key == GDK_KEY_Alt_L || key == GDK_KEY_Alt_R) {                           //  Alt key
       Alt = type;
       return 1;
   }

   if (key == GDK_KEY_Shift_L || key == GDK_KEY_Shift_R) {                       //  Shift key
       Shift = type;
       return 1;
   }

   if (type) return 1;                                                           //  wait for key release

   if (key == GDK_KEY_F1) {                                                      //  key is F1 (context help)
      KBstate(event,0);                                                          //  send to main app
      return 1;
   }

   if (key >= GDK_KEY_F2 && key <= GDK_KEY_F9) {                                 //  key is F2 to F9
      ii = key - GDK_KEY_F1;
      strcpy(keyname,"F1");
      keyname[1] += ii;
   }

   else if (key > 255) return 1;                                                 //  not a simple Ascii key

   else {
      *keyname = 0;                                                              //  build input key combination
      if (Ctrl) strcat(keyname,"Ctrl+");                                         //  [Ctrl+] [Alt+] [Shift+] key
      if (Alt) strcat(keyname,"Alt+");
      if (Shift) strcat(keyname,"Shift+");
      cc = strlen(keyname);
      keyname[cc] = toupper(key);                                                //  x --> X, Ctrl+x --> Ctrl+X
      keyname[cc+1] = 0;
   }

   for (ii = 0; ii < Nreserved; ii++)
      if (strmatch(keyname,reserved[ii])) break;
   if (ii < Nreserved) {
      zmessageACK(Mwin,ZTX("\"%s\"  Reserved, cannot be used"),keyname);
      Ctrl = Alt = 0;
      return 1;
   }

   zdialog_stuff(zd,"shortkey",keyname);                                         //  stuff key name into dialog
   zdialog_stuff(zd,"shortmenu","");                                             //  clear menu choice

   return 1;
}


//  dialog event and completion function

int KBshorts_dialog_event(zdialog *zd, cchar *event)
{
   using namespace KBshortcutnames;

   int         ii, jj, err;
   GtkWidget   *widget;
   char        textline[100];
   char        shortkey[20];
   char        shortmenu[60];
   char        kbfile[200];
   FILE        *fid = 0;

   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

   if (strmatch(event,"shortmenu"))                                              //  menu choice made
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"shortkey",shortkey,20);                                  //  get shortcut key and menu
      zdialog_fetch(zd,"shortmenu",shortmenu,60);                                //    from dialog widgets
      if (*shortkey <= ' ' || *shortmenu <= ' ') return 0;

      for (ii = 0; ii < Nshortcuts2; ii++)                                       //  find matching shortcut key in list
         if (strmatch(shortcutkey2[ii],shortkey)) break;

      if (ii < Nshortcuts2) {                                                    //  if found, remove from list
         zfree(shortcutkey2[ii]);
         zfree(shortcutmenu2[ii]);
         for (jj = ii; jj < Nshortcuts2; jj++) {
            shortcutkey2[jj] = shortcutkey2[jj+1];
            shortcutmenu2[jj] = shortcutmenu2[jj+1];
         }
         --Nshortcuts2;
      }

      for (ii = 0; ii < Nshortcuts2; ii++)                                       //  find matching shortcut menu in list
         if (strmatch(shortcutmenu2[ii],shortmenu)) break;

      if (ii < Nshortcuts2) {                                                    //  if found, remove from list
         zfree(shortcutkey2[ii]);
         zfree(shortcutmenu2[ii]);
         for (jj = ii; jj < Nshortcuts2; jj++) {
            shortcutkey2[jj] = shortcutkey2[jj+1];
            shortcutmenu2[jj] = shortcutmenu2[jj+1];
         }
         --Nshortcuts2;
      }

      if (Nshortcuts2 == maxshortcuts) {
         zmessageACK(Mwin,"exceed %d shortcuts",maxshortcuts);
         return 1;
      }

      ii = Nshortcuts2++;                                                        //  add new shortcut to end of list
      shortcutkey2[ii] = zstrdup(shortkey);
      shortcutmenu2[ii] = zstrdup(shortmenu);

      widget = zdialog_widget(zd,"shortlist");
      wclear(widget);

      for (ii = 0; ii < Nshortcuts2; ii++) {                                     //  show existing shortcuts in dialog
         snprintf(textline,100,"%-20s %s \n",shortcutkey2[ii],shortcutmenu2[ii]);
         wprintx(widget,0,textline);
      }

      return 1;
   }

   if (! zd->zstat) return 1;                                                    //  wait for completion
   
   if (zd->zstat == 1)                                                           //  delete shortcut
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"shortkey",shortkey,20);                                  //  get shortcut key
      if (*shortkey <= ' ') return 0;

      for (ii = 0; ii < Nshortcuts2; ii++)                                       //  find matching shortcut key in list
         if (strmatch(shortcutkey2[ii],shortkey)) break;

      if (ii < Nshortcuts2) {                                                    //  if found, remove from list
         zfree(shortcutkey2[ii]);
         zfree(shortcutmenu2[ii]);
         for (jj = ii; jj < Nshortcuts2; jj++) {
            shortcutkey2[jj] = shortcutkey2[jj+1];
            shortcutmenu2[jj] = shortcutmenu2[jj+1];
         }
         --Nshortcuts2;
      }

      widget = zdialog_widget(zd,"shortlist");
      wclear(widget);

      for (ii = 0; ii < Nshortcuts2; ii++) {                                     //  show existing shortcuts in dialog
         snprintf(textline,100,"%-20s %s \n",shortcutkey2[ii],shortcutmenu2[ii]);
         wprintx(widget,0,textline);
      }

      zdialog_stuff(zd,"shortkey","");
      zdialog_stuff(zd,"shortmenu","");
      return 1;
   }

   if (zd->zstat == 2)                                                           //  done - save the shortcuts
   {
      zdialog_free(zd);

      for (ii = 0; ii < Nshortcuts2; ii++) {                                     //  convert menunames back to English
         for (jj = 0; jj < Nallowed; jj++)
            if (strmatch(allowed[jj],shortcutmenu2[ii])) break;
         if (jj < Nallowed) {
            zfree(shortcutmenu2[ii]);
            shortcutmenu2[ii] = zstrdup(allowed_en[jj]);
         }
      }

      err = locale_filespec("user","KB-shortcuts",kbfile);                       //  write shortcuts file
      if (! err) fid = fopen(kbfile,"w");
      if (err || ! fid) {
         zmessageACK(Mwin,ZTX("unable to save KB-shortcuts file"));
         return 1;
      }
      for (ii = 0; ii < Nshortcuts2; ii++)
         fprintf(fid,"%-20s %s \n",shortcutkey2[ii],shortcutmenu2[ii]);
      fclose(fid);

      for (ii = 0; ii < Nshortcuts; ii++) {                                      //  clear global shortcuts data
         if (shortcutkey[ii]) zfree(shortcutkey[ii]);
         if (shortcutmenu[ii]) zfree(shortcutmenu[ii]);
         shortcutkey[ii] = shortcutmenu[ii] = 0;
      }

      KBshortcuts_load();                                                        //  reload and activate
      return 1;
   }

   zdialog_free(zd);                                                             //  cancel or [x]
   return 1;
}


//  read KB-shortcuts file and load shortcuts table in memory
//  also called from initzfunc() at fotoxx startup

int KBshortcuts_load()
{
   int         ii, err;
   char        kbfile[200], buff[200];
   char        *pp1, *pp2;
   FILE        *fid;

   Nshortcuts = 0;
   err = locale_filespec("user","KB-shortcuts",kbfile);
   if (err) return 0;
   ii = 0;
   fid = fopen(kbfile,"r");
   if (! fid) return 0;
   while (true) {
      pp1 = fgets_trim(buff,200,fid,1);                                          //  next record
      if (! pp1) break;
      if (*pp1 == '#') continue;                                                 //  comment
      if (*pp1 <= ' ') continue;                                                 //  blank
      pp2 = strchr(pp1,' ');
      if (! pp2) continue;
      if (pp2 - pp1 > 20) continue;
      *pp2 = 0;
      shortcutkey[ii] = zstrdup(pp1);                                            //  shortcut key
      pp1 = pp2 + 1;
      while (*pp1 && *pp1 == ' ') pp1++;
      if (! *pp1) continue;
      shortcutmenu[ii] = zstrdup(ZTX(pp1));                                      //  menu can be English or translation
      if (++ii == maxshortcuts) break;
   }

   fclose(fid);
   Nshortcuts = ii;
   return 0;
}


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

//  show a brightness distribution graph - live update as image is edited

namespace brdist_names
{
   GtkWidget   *drawwin_dist, *drawwin_scale;                                    //  brightness distribution graph widgets
   int         RGBW[4] = { 0, 0, 0, 1 };                                         //     "  colors: red/green/blue/white (all)
}


//  menu function

void m_show_brdist(GtkWidget *, cchar *menu)                                     //  menu function
{
   using namespace brdist_names;

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

   GtkWidget   *frdist, *frscale, *widget;
   zdialog     *zd;

   if (! Fpxb) return;

   if (menu && strmatch(menu,"kill")) {
      if (zdbrdist) zdialog_destroy(zdbrdist);
      zdbrdist = 0;
      return;
   }

   if (zdbrdist) {                                                               //  dialog already present
      gtk_widget_queue_draw(drawwin_dist);                                       //  refresh drawing windows
      return;
   }

   if (menu) F1_help_topic = "brightness_graph";

   zd = zdialog_new(ZTX("Brightness Distribution"),Mwin,null);
   zdialog_add_widget(zd,"frame","frdist","dialog",0,"expand");                  //  frames for 2 drawing areas
   zdialog_add_widget(zd,"frame","frscale","dialog");
   frdist = zdialog_widget(zd,"frdist");
   frscale = zdialog_widget(zd,"frscale");

   drawwin_dist = gtk_drawing_area_new();                                        //  histogram drawing area
   gtk_container_add(GTK_CONTAINER(frdist),drawwin_dist);
   G_SIGNAL(drawwin_dist,"draw",brdist_drawgraph,RGBW);

   drawwin_scale = gtk_drawing_area_new();                                       //  brightness scale under histogram
   gtk_container_add(GTK_CONTAINER(frscale),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,300,12);
   G_SIGNAL(drawwin_scale,"draw",brdist_drawscale,0);

   zdialog_add_widget(zd,"hbox","hbcolors","dialog");
   zdialog_add_widget(zd,"check","all","hbcolors",Ball,"space=5");
   zdialog_add_widget(zd,"check","red","hbcolors",Bred,"space=5");
   zdialog_add_widget(zd,"check","green","hbcolors",Bgreen,"space=5");
   zdialog_add_widget(zd,"check","blue","hbcolors",Bblue,"space=5");

   zdialog_stuff(zd,"red",RGBW[0]);
   zdialog_stuff(zd,"green",RGBW[1]);
   zdialog_stuff(zd,"blue",RGBW[2]);
   zdialog_stuff(zd,"all",RGBW[3]);

   zdialog_resize(zd,300,250);
   zdialog_run(zd,show_brdist_dialog_event,"save");

   widget = zdialog_widget(zd,"dialog");                                         //  stop focus on this window          16.06
   gtk_window_set_accept_focus(GTK_WINDOW(widget),0);

   zdbrdist = zd;
   return;
}


//  dialog event and completion function

int show_brdist_dialog_event(zdialog *zd, cchar *event)
{
   using namespace brdist_names;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat) {
      zdialog_free(zd);
      zdbrdist = 0;
      return 0;
   }

   if (strstr("all red green blue",event)) {                                     //  update chosen colors
      zdialog_fetch(zd,"red",RGBW[0]);
      zdialog_fetch(zd,"green",RGBW[1]);
      zdialog_fetch(zd,"blue",RGBW[2]);
      zdialog_fetch(zd,"all",RGBW[3]);
      gtk_widget_queue_draw(drawwin_dist);                                       //  refresh drawing window
   }

   return 0;
}


//  draw brightness distribution graph in drawing window 

void brdist_drawgraph(GtkWidget *drawin, cairo_t *cr, int *rgbw)
{
   int         bin, Nbins = 85, brdist[85][4];                                   //  bin count, R/G/B/all
   int         px, py, dx, dy;
   int         ww, hh, winww, winhh;
   int         ii, rgb, maxdist, bright;
   float       *pixf, pixg[3];
   uint8       *pixi;

   if (! Fpxb) return;
   if (rgbw[0]+rgbw[1]+rgbw[2]+rgbw[3] == 0) return;

   paintlock(1);                                                                 //  16.03

   winww = gtk_widget_get_allocated_width(drawin);                               //  drawing window size
   winhh = gtk_widget_get_allocated_height(drawin);

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

   ww = Fpxb->ww;
   hh = Fpxb->hh;

   for (ii = 0; ii < ww * hh; ii++)                                              //  overhaul
   {
      if (sa_stat == 3 && ! sa_pixmap[ii]) continue;                             //  stay within active select area

      py = ii / ww;                                                              //  image pixel
      px = ii - ww * py;

      dx = Mscale * px - Morgx + Dorgx;                                          //  stay within visible window
      if (dx < 0 || dx > Dww-1) continue;                                        //    for zoomed image
      dy = Mscale * py - Morgy + Dorgy;
      if (dy < 0 || dy > Dhh-1) continue;

      pixi = PXBpix(Fpxb,px,py);                                                 //  use displayed image
      pixg[0] = pixi[0];                                                         //  convert uint8 RGB to float
      pixg[1] = pixi[1];
      pixg[2] = pixi[2];
      pixf = pixg;

      for (rgb = 0; rgb < 3; rgb++) {                                            //  get R/G/B brightness levels
         bright = pixf[rgb] * Nbins / 256.0;                                     //  scale 0 to Nbins-1
         if (bright < 0 || bright > 255) {
            printz("pixel %d/%d: %.2f %.2f %.2f \n",px,py,pixf[0],pixf[1],pixf[2]);
            paintlock(0);                                                        //  16.03
            return;
         }
         ++brdist[bright][rgb];
      }

      bright = (pixf[0] + pixf[1] + pixf[2]) * 0.333 * Nbins / 256.0;            //  R+G+B, 0 to Nbins-1
      ++brdist[bright][3];
   }

   paintlock(0);                                                                 //  16.03

   maxdist = 0;

   for (bin = 1; bin < Nbins-1; bin++)                                           //  find max. bin over all RGB
   for (rgb = 0; rgb < 3; rgb++)                                                 //    omit bins 0 and last
      if (brdist[bin][rgb] > maxdist)                                            //      which can be huge
         maxdist = brdist[bin][rgb];

   for (rgb = 0; rgb < 4; rgb++)                                                 //  R/G/B/white (all)
   {
      if (! rgbw[rgb]) continue;                                                 //  color not selected for graph
      if (rgb == 0) cairo_set_source_rgb(cr,1,0,0);
      if (rgb == 1) cairo_set_source_rgb(cr,0,1,0);
      if (rgb == 2) cairo_set_source_rgb(cr,0,0,1);
      if (rgb == 3) cairo_set_source_rgb(cr,0,0,0);                              //  color "white" = R+G+B uses black line

      cairo_move_to(cr,0,winhh-1);                                               //  start at (0,0)

      for (px = 0; px < winww; px++)                                             //  x from 0 to window width
      {
         bin = Nbins * px / winww;                                               //  bin = 0-Nbins for x = 0-width
         py = 0.9 * winhh * brdist[bin][rgb] / maxdist;                          //  height of bin in window
         py = winhh * sqrt(1.0 * py / winhh);
         py = winhh - py - 1;
         cairo_line_to(cr,px,py);                                                //  draw line from bin to bin
      }

      cairo_stroke(cr);
   }

   return;
}


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

//  Paint a horizontal stripe drawing area with a color progressing from
//  black to white. This represents a brightness scale from 0 to 255.

void brdist_drawscale(GtkWidget *drawarea, cairo_t *cr, int *)
{
   int      px, ww, hh;
   float    fbright;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   for (px = 0; px < ww; px++)                                                   //  draw brightness scale
   {
      fbright = 1.0 * px / ww;
      cairo_set_source_rgb(cr,fbright,fbright,fbright);
      cairo_move_to(cr,px,0);
      cairo_line_to(cr,px,hh-1);
      cairo_stroke(cr);
   }

   return;
}


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

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

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

   zdialog     *zd;
   int         G;

   if (menu && strmatchN(menu,"grid ",5))                                        //  grid N from some edit functions
      currgrid = menu[5] - '0';

   else if (! widget) {                                                          //  from KB shortcut
      toggle_grid(2);                                                            //  toggle grid lines on/off
      return;
   }

   else currgrid = 0;                                                            //  must be menu call

   if (currgrid == 0)
      F1_help_topic = "grid_lines";

   zd = zdialog_new(ZTX("Grid Lines"),Mwin,Bdone,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");
   zdialog_add_widget(zd,"vbox","vbspace","hb0",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb3","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb0",0,"homog");

   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","space=2");
   zdialog_add_widget(zd,"spin","countx","vb2","0|100|1|2","space=2");
   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|2");
   zdialog_add_widget(zd,"check","enabley","vb4",0);

   zdialog_add_widget(zd,"hbox","hboffx","dialog");
   zdialog_add_widget(zd,"label","lab3x","hboffx",Bxoffset,"space=7");
   zdialog_add_widget(zd,"hscale","offsetx","hboffx","0|100|1|0","expand");
   zdialog_add_widget(zd,"label","space","hboffx",0,"space=20");

   zdialog_add_widget(zd,"hbox","hboffy","dialog");
   zdialog_add_widget(zd,"label","lab3y","hboffy",Byoffset,"space=7");
   zdialog_add_widget(zd,"hscale","offsety","hboffy","0|100|1|0","expand");
   zdialog_add_widget(zd,"label","space","hboffy",0,"space=20");

   G = currgrid;                                                                 //  grid logic overhaul

   if (! gridsettings[G][GXS])                                                   //  if [G] never set, get defaults
      for (int ii = 0; ii < 9; ii++)
         gridsettings[G][ii] = gridsettings[0][ii];

   zdialog_stuff(zd,"enablex",gridsettings[G][GX]);                              //  current settings >> dialog widgets
   zdialog_stuff(zd,"enabley",gridsettings[G][GY]);
   zdialog_stuff(zd,"spacex",gridsettings[G][GXS]);
   zdialog_stuff(zd,"spacey",gridsettings[G][GYS]);
   zdialog_stuff(zd,"countx",gridsettings[G][GXC]);
   zdialog_stuff(zd,"county",gridsettings[G][GYC]);
   zdialog_stuff(zd,"offsetx",gridsettings[G][GXF]);
   zdialog_stuff(zd,"offsety",gridsettings[G][GYF]);

   zdialog_run(zd,gridlines_dialog_event);
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  dialog event function

int gridlines_dialog_event(zdialog *zd, cchar *event)
{
   int      G = currgrid;

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  done or cancel
   {
      if (zd->zstat != 1) return 1;
      if (gridsettings[G][GX] || gridsettings[G][GY])
         gridsettings[G][GON] = 1;
      else gridsettings[G][GON] = 0;
      Fpaint2();
      return 1;
   }

   if (strmatch(event,"enablex"))                                                //  x/y grid enable or disable
      zdialog_fetch(zd,"enablex",gridsettings[G][GX]);

   if (strmatch(event,"enabley"))
      zdialog_fetch(zd,"enabley",gridsettings[G][GY]);

   if (strmatch(event,"spacex"))                                                 //  x/y grid spacing (if counts == 0)
      zdialog_fetch(zd,"spacex",gridsettings[G][GXS]);

   if (strmatch(event,"spacey"))
      zdialog_fetch(zd,"spacey",gridsettings[G][GYS]);

   if (strmatch(event,"countx"))                                                 //  x/y grid line counts
      zdialog_fetch(zd,"countx",gridsettings[G][GXC]);

   if (strmatch(event,"county"))
      zdialog_fetch(zd,"county",gridsettings[G][GYC]);

   if (strmatch(event,"offsetx"))                                                //  x/y grid starting offsets
      zdialog_fetch(zd,"offsetx",gridsettings[G][GXF]);

   if (strmatch(event,"offsety"))
      zdialog_fetch(zd,"offsety",gridsettings[G][GYF]);

   if (gridsettings[G][GX] || gridsettings[G][GY])                               //  if either grid enabled, show grid
      gridsettings[G][GON] = 1;

   Fpaint2();
   return 1;
}


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

void toggle_grid(int action)
{
   int   G = currgrid;

   if (action == 0) gridsettings[G][GON] = 0;                                    //  grid off
   if (action == 1) gridsettings[G][GON] = 1;                                    //  grid on
   if (action == 2) gridsettings[G][GON] = 1 - gridsettings[G][GON];             //  toggle grid

   if (gridsettings[G][GON])
      if (! gridsettings[G][GX] && ! gridsettings[G][GY])                        //  if grid on and x/y both off,
         gridsettings[G][GX] = gridsettings[G][GY] = 1;                          //    set both grids on

   Fpaint2();
   return;
}


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

//  choose color for foreground lines
//  (area outline, mouse circle, trim rectangle ...)

void  m_line_color(GtkWidget *, cchar *menu)
{
   int line_color_dialog_event(zdialog *zd, cchar *event);

   zdialog  *zd;

   if (strmatch(menu,ZTX("Line Color")))
      F1_help_topic = "line_color";
   if (strmatch(menu,ZTX("Area Color")))
      F1_help_topic = "area_color";

   zd = zdialog_new(ZTX("Line Color"),Mwin,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio",Bblack,"hb1",Bblack,"space=3");                 //  add radio button per color
   zdialog_add_widget(zd,"radio",Bwhite,"hb1",Bwhite,"space=3");
   zdialog_add_widget(zd,"radio",Bred,"hb1",Bred,"space=3");
   zdialog_add_widget(zd,"radio",Bgreen,"hb1",Bgreen,"space=3");

   zdialog_stuff(zd,Bblack,0);                                                   //  all are initially off
   zdialog_stuff(zd,Bwhite,0);
   zdialog_stuff(zd,Bred,0);
   zdialog_stuff(zd,Bgreen,0);

   if (LINE_COLOR == BLACK)                                                      //  set current color on
      zdialog_stuff(zd,Bblack,1);
   if (LINE_COLOR == WHITE)
      zdialog_stuff(zd,Bwhite,1);
   if (LINE_COLOR == RED)
      zdialog_stuff(zd,Bred,1);
   if (LINE_COLOR == GREEN)
      zdialog_stuff(zd,Bgreen,1);

   zdialog_run(zd,line_color_dialog_event,"save");                               //  run dialog, parallel
   return;
}


//  dialog event and completion function

int line_color_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strstr("Black White Red Green",event))
   {
      if (strmatch(event,"Black")) LINE_COLOR = BLACK;                           //  set selected color
      if (strmatch(event,"White")) LINE_COLOR = WHITE;
      if (strmatch(event,"Red"))   LINE_COLOR = RED;
      if (strmatch(event,"Green")) LINE_COLOR = GREEN;
      if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"line_color");
      Fpaint2();
   }

   if (zd->zstat) zdialog_free(zd);                                              // [x] button
   return 1;
}


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

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

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

zdialog     *RGBSzd;
int         RGBSpixel[10][2];                                                    //  last 10 pixels clicked
int         RGBSnpix;                                                            //  count of pixels
int         RGBSmetric = 1;                                                      //  1/2/3 = /RGB/EV/OD
int         RGBSdelta = 0;                                                       //  abs/delta mode
int         RGBSlabels = 0;                                                      //  pixel labels on/off
int         RGBSupdate = 0;                                                      //  window update needed


void m_show_RGB(GtkWidget *, cchar *menu)
{
   int   show_RGB_event(zdialog *zd, cchar *event);

   cchar       *mess = ZTX("Click image to select pixels.");
   cchar       *header = " Pixel            Red     Green   Blue";
   char        hbx[8] = "hbx", pixx[8] = "pixx";                                 //  last char. is '0' to '9'
   int         ii;

   F1_help_topic = "show_RGB";

   if (! curr_file) return;                                                      //  no image file
   RGBSnpix = 0;                                                                 //  no pixels yet

/***
    _________________________________________
   |                                         |
   |  Click image to select pixels.          |
   |  [x] delta  [x] labels                  |
   |   Metric: (o) RGB  (o) EV               |
   |   Pixel           Red    Green  Blue    |
   |   A xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   B xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   C xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   D xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   E xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   F xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   G xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   H xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   I xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   J xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |     xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |                                         |
   |                        [clear] [done]   |
   |_________________________________________|

***/

   if (RGBSzd) zdialog_free(RGBSzd);                                             //  delete previous if any
   zdialog *zd = zdialog_new(ZTX("Show RGB"),Mwin,Bclear,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 && E3pxm) zdialog_stuff(zd,"delta",1);

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

   if (RGBSmetric == 1) zdialog_stuff(zd,"radRGB",1);                            //  get which metric to use
   if (RGBSmetric == 2) zdialog_stuff(zd,"radEV",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_labelfont(zd,"labheader","monospace 9",header);                       //  15.09

   for (ii = 0; ii < 10; ii++)
   {                                                                             //  10 labels for pixel data output
      hbx[2] = '0' + ii;
      pixx[3] = '0' + ii;
      zdialog_add_widget(zd,"hbox",hbx,"vbdat");
      zdialog_add_widget(zd,"label",pixx,hbx);
   }

   zdialog_run(zd,show_RGB_event,"save");                                        //  run dialog
   takeMouse(show_RGB_mousefunc,dragcursor);                                     //  connect mouse function
   g_timeout_add(200,show_RGB_timefunc,0);                                       //  start timer function, 200 ms

   return;
}


//  dialog event function

int show_RGB_event(zdialog *zd, cchar *event)
{
   int      button;

   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

   if (zd->zstat) {
      if (zd->zstat == 1) {                                                      //  clear
         zd->zstat = 0;                                                          //  keep dialog active
         RGBSnpix = 0;                                                           //  point count = 0
         erase_toptext(102);                                                     //  erase labels on image
         RGBSupdate++;                                                           //  update display window
      }
      else {                                                                     //  done or kill
         freeMouse();                                                            //  disconnect mouse function
         zdialog_free(RGBSzd);                                                   //  kill dialog
         RGBSzd = 0;
         erase_toptext(102);
      }
      Fpaint2();
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(show_RGB_mousefunc,dragcursor);                                  //  connect mouse function

   if (strmatch(event,"delta")) {                                                //  set absolute/delta mode
      zdialog_fetch(zd,"delta",RGBSdelta);
      if (RGBSdelta && ! E3pxm) {
         RGBSdelta = 0;                                                          //  block delta mode if no edit underway
         zdialog_stuff(zd,"delta",0);
         zmessageACK(Mwin,ZTX("Edit function must be active"));                  //  13.02.1
      }
   }

   if (strmatch(event,"labels")) {                                               //  get labels on/off
      zdialog_fetch(zd,"labels",RGBSlabels);
      RGBSupdate++;                                                              //  update window
   }

   if (strmatch(event,"radRGB")) {                                               //  metric = RGB
      zdialog_fetch(zd,event,button);
      if (button) RGBSmetric = 1;
   }

   if (strmatch(event,"radEV")) {                                                //  metric = EV
      zdialog_fetch(zd,event,button);
      if (button) RGBSmetric = 2;
   }

   return 0;
}


//  mouse function
//  table positions [0] to [RGBSnpix-1] for labeled pixel positions, fixed
//  table position [RGBSnpix] = values from current mouse position

void show_RGB_mousefunc()                                                        //  mouse function
{
   int      ii;
   PXM      *pxm;

   if (E3pxm) pxm = E3pxm;                                                       //  report image being edited
   else if (E1pxm) pxm = E1pxm;
   else if (E0pxm) pxm = E0pxm;
   else {
      E0pxm = PXM_load(curr_file,1);                                             //  never edited,
      if (! E0pxm) return;                                                       //  create E0 image for my use
      pxm = E0pxm;
      curr_file_bpc = f_load_bpc;                                                //  16.07
   }

   if (Mxposn < 0 || Mxposn > pxm->ww-1) return;                                 //  mouse outside image, ignore
   if (Myposn < 0 || Myposn > pxm->hh-1) return;

   if (LMclick)                                                                  //  left click, add labeled position
   {
      LMclick = 0;

      if (RGBSnpix == 9) {                                                       //  if all 9 labeled positions filled,
         for (ii = 1; ii < 9; ii++) {                                            //    remove first (oldest) and
            RGBSpixel[ii-1][0] = RGBSpixel[ii][0];                               //      push the rest back
            RGBSpixel[ii-1][1] = RGBSpixel[ii][1];
         }
         RGBSnpix = 8;                                                           //  position for newest
      }

      ii = RGBSnpix;                                                             //  next position to fill
      RGBSpixel[ii][0] = Mxclick;                                                //  save newest pixel
      RGBSpixel[ii][1] = Myclick;
      RGBSnpix++;
   }

   ii = RGBSnpix;                                                                //  fill next position from active mouse
   RGBSpixel[ii][0] = Mxposn;
   RGBSpixel[ii][1] = Myposn;

/******   
   if (sa_stat == 3) {                                                           //  output area edge distance          16.08
      ii = Myposn * pxm->ww + Mxposn;
      printz("pixel: %d %d  edge dist: %d \n",Mxposn,Myposn,sa_pixmap[ii]);
   }
******/

   RGBSupdate++;                                                                 //  update window
   return;
}


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

int show_RGB_timefunc(void *arg)                                                 //  up to 10 pixels, live update
{
   static char    label[10][4] = { " A ", " B ", " C ", " D ", " E ",
                                   " F ", " G ", " H ", " I ", " J " };
   PXM         *pxm = 0;
   int         ii, jj, px, py, delta;
   int         ww, hh;
   float       red1, green1, blue1;
   float       red3, green3, blue3;
   float       *ppixa, *ppixb;
   char        text[100], pixx[8] = "pixx";

   #define EVfunc(rgb) (log2(rgb) - 7)                                           //  RGB units to EV (128 == 0 EV)

   if (! RGBSzd) return 0;                                                       //  user quit, cancel timer

   if (RGBSdelta && E3pxm) delta = 1;                                            //  delta mode only if edit underway
   else delta = 0;

   if (E3pxm) pxm = E3pxm;                                                       //  report image being edited
   else if (E1pxm) pxm = E1pxm;
   else if (E0pxm) pxm = E0pxm;
   else return 1;

   ww = pxm->ww;
   hh = pxm->hh;

   if (RGBSnpix)                                                                 //  some labeled positions to report
   {
      for (ii = 0; ii < 10; ii++)
      {
         px = RGBSpixel[ii][0];                                                  //  next pixel to report
         py = RGBSpixel[ii][1];
         if (px >= 0 && px < ww && py >= 0 && py < hh) continue;                 //  within image limits
         for (jj = ii+1; jj < 10; jj++) {
            RGBSpixel[jj-1][0] = RGBSpixel[jj][0];                               //  remove pixel outside limits
            RGBSpixel[jj-1][1] = RGBSpixel[jj][1];                               //    and pack the remaining down
         }
         RGBSpixel[9][0] = RGBSpixel[9][1] = 0;                                  //  last position is empty
         RGBSnpix--;
         ii--;
         RGBSupdate++;                                                           //  update window
      }
   }

   if (RGBSupdate)                                                               //  refresh window labels
   {
      RGBSupdate = 0;
      erase_toptext(102);

      if (RGBSlabels) {
         for (ii = 0; ii < RGBSnpix; ii++) {                                     //  show pixel labels on image
            px = RGBSpixel[ii][0];
            py = RGBSpixel[ii][1];
            add_toptext(102,px,py,label[ii],"Sans 8");
         }
      }

      Fpaint2();
   }

   red1 = green1 = blue1 = 0;

   for (ii = 0; ii < 10; ii++)                                                   //  loop positons 0 to 9
   {
      pixx[3] = '0' + ii;                                                        //  widget names "pix0" ... "pix9"

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

      ppixa = PXMpix(pxm,px,py);                                                 //  get pixel RGB values
      red3 = ppixa[0];
      green3 = ppixa[1];
      blue3 = ppixa[2];

      if (delta && E1pxm && px < E1pxm->ww && py < E1pxm->hh) {                  //  delta RGB for ongoing edited image
         ppixb = PXMpix(E1pxm,px,py);                                            //  "before" image E1
         red1 = ppixb[0];
         green1 = ppixb[1];
         blue1 = ppixb[2];
      }

      if (ii == RGBSnpix)
         snprintf(text,100,"   %5d %5d  ",px,py);                                //  mouse pixel, format "   xxxx yyyy"
      else snprintf(text,100," %c %5d %5d  ",'A'+ii,px,py);                      //  fixed pixel, format " A xxxx yyyy"

      if (RGBSmetric == 1) {                                                     //  output RGB values
         if (delta) {
            red3 -= red1;                                                        //  delta RGB
            green3 -= green1;
            blue3 -= blue1;
         }
         snprintf(text+14,86,"   %6.2f  %6.2f  %6.2f ",red3,green3,blue3);
      }

      if (RGBSmetric == 2) {                                                     //  output EV values
         red3 = EVfunc(red3);
         green3 = EVfunc(green3);
         blue3 = EVfunc(blue3);
         if (delta) {
            red3 -= EVfunc(red1);                                                //  delta EV
            green3 -= EVfunc(green1);
            blue3 -= EVfunc(blue1);
         }
         snprintf(text+14,86,"   %6.3f  %6.3f  %6.3f ",red3,green3,blue3);
      }

      zdialog_labelfont(RGBSzd,pixx,"monospace 9",text);                         //  15.09
   }

   return 1;
}


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

//  magnify image within a given radius of dragged mouse

namespace magnify_names
{
   int   magnify_dialog_event(zdialog* zd, cchar *event);
   void  magnify_mousefunc();
   void  magnify_dopixels(int ftf);

   float       Xsize;                                                            //  magnification, 1 - 10x
   int         Mxpos, Mypos;                                                     //  mouse location, image space
   int         Drad;                                                             //  mouse radius, window space
   uint8       kernel[601][601];                                                 //  up to radius 300
}


//  menu function

void m_magnify(GtkWidget *, cchar *)
{
   using namespace magnify_names;

   F1_help_topic = "magnify";

   cchar *   mess = ZTX("Drag mouse on image. \n"
                        "Left click to cancel. \n"
                        "Key X to toggle dialog.");

   /***
             __________________________
            |    Magnify Image         |
            |                          |
            |  Drag mouse on image.    |
            |  Left click to cancel.   |
            |  Key X to toggle dialog. |
            |                          |
            |  radius  [_____|-+]      |
            |  X-size  [_____|-+]      |
            |                          |
            |                [cancel]  |
            |__________________________|

   ***/

   if (zdmagnify) {                                                              //  toggle magnify mode                15.01
      zdialog_send_event(zdmagnify,"kill");
      return;
   }

   else {
      zdialog *zd = zdialog_new(ZTX("Magnify Image"),Mwin,Bcancel,null);
      zdmagnify = zd;

      zdialog_add_widget(zd,"label","labdrag","dialog",mess,"space=5");

      zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labr","hbr",Bradius,"space=5");
      zdialog_add_widget(zd,"spin","Drad","hbr","100|300|10|100");
      zdialog_add_widget(zd,"hbox","hbx","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labx","hbx",ZTX("X-size"),"space=5");
      zdialog_add_widget(zd,"spin","Xsize","hbx","1.5|5|0.1|2");

      zdialog_fetch(zd,"Drad",Drad);                                             //  initial mouse radius
      zdialog_fetch(zd,"Xsize",Xsize);                                           //  initial magnification

      zdialog_resize(zd,200,0);
      zdialog_restore_inputs(zd);                                                //  preload prior user inputs
      zdialog_run(zd,magnify_dialog_event,"save");                               //  run dialog, parallel

      zdialog_send_event(zd,"Drad");                                             //  initializations
      zdialog_send_event(zd,"Xsize");
   }

   takeMouse(magnify_mousefunc,dragcursor);                                      //  connect mouse function

   gtk_window_present(MWIN);                                                     //  keep focus on main window          15.01
   return;
}


//  dialog event and completion callback function

int magnify_names::magnify_dialog_event(zdialog *zd, cchar *event)
{
   using namespace magnify_names;

   int      dx, dy, rad, kern;

   if (strmatch(event,"escape")) event = "kill";                                 //  escape = cancel                    15.07

   if (strmatch(event,"kill")) zd->zstat = 1;                                    //  from slide show 

   if (zd->zstat) {                                                              //  terminate
      zdmagnify = 0;
      zdialog_free(zd);
      freeMouse();
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(magnify_mousefunc,dragcursor);

   if (strmatch(event,"Drad"))
   {
      zdialog_fetch(zd,"Drad",Drad);                                             //  new mouse radius

      for (dy = -Drad; dy <= Drad; dy++)                                         //  build kernel
      for (dx = -Drad; dx <= Drad; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         if (rad < Drad-3) kern = 1;                                             //  margin for circle                  15.01
         else kern = 0;
         kernel[dx+Drad][dy+Drad] = kern;
      }

      return 1;
   }

   if (strmatch(event,"Xsize"))
   {
      zdialog_fetch(zd,"Xsize",Xsize);                                           //  new magnification
      return 1;
   }

   return 1;
}


//  pixel paint mouse function

void magnify_names::magnify_mousefunc()
{
   using namespace magnify_names;

   static int     ftf;
   
   if (! curr_file) return;
   if (FGWM != 'F') return;

   if (Mxdown) Fpaint2();                                                        //  drag start, erase prior if any
   Mxdown = 0;

   if (Mxdrag || Mydrag)                                                         //  drag in progress
   {
      Mxpos = Mxdrag;                                                            //  save mouse position
      Mypos = Mydrag;
      Mxdrag = Mydrag = 0;
      magnify_dopixels(ftf);                                                     //  magnify pixels inside mouse
      gdk_window_set_cursor(gdkwin,blankcursor);
      ftf = 0;
   }

   else
   {
      ftf = 1;
      gdk_window_set_cursor(gdkwin,dragcursor);
   }

   return;
}


//  get pixels from mouse circle within full size image
//  scale and move into pixbuf and write to window

void magnify_names::magnify_dopixels(int ftf)
{
   using namespace magnify_names;

   static PIXBUF   *pxb1 = 0, *pxb2 = 0;

   int         Srad, drad, Dxpos, Dypos, rx, ry;
   static int  org1x, org1y, org2x, org2y;
   static int  fww, fhh, ww1, hh1, ww2, hh2;
   uint8       *pixels1, *pixels2, *pix1, *pix2;
   int         rs1, rs2, nch1, nch2, px1, py1, px2, py2;
   float       scale;
   cairo_t     *cr;

   if (! ftf && pxb2)                                                            //  continuation of mouse drag
   {
      org1x = org2x - Dorgx + Morgx;                                             //  replace prior display area
      if (org1x < 0) org1x = 0;                                                  //    with original image pixels
      if (org1x + ww2 > Mpxb->ww) ww2 = Mpxb->ww - org1x;

      org1y = org2y - Dorgy + Morgy;
      if (org1y < 0) org1y = 0;
      if (org1y + hh2 > Mpxb->hh) hh2 = Mpxb->hh - org1y;

      g_object_unref(pxb2);
      pxb2 = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,org1x,org1y,ww2,hh2);
      if (! pxb2) return;
      cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(cr,pxb2,org2x,org2y);
      cairo_paint(cr);
      cairo_destroy(cr);
   }

   fww = Fpxb->ww;                                                               //  curr. image size
   fhh = Fpxb->hh;

   scale = Mscale * Xsize;                                                       //  image to window pixels scale
   Srad = Drad / scale;                                                          //  mouse radius scaled to 1x image

   if (Srad > fww/3) Srad = fww/3;                                               //  restrict to 1/3 image size
   if (Srad > fhh/3) Srad = fhh/3;

   drad = Srad * scale;                                                          //  restricted mouse radius
   drad = drad / 2 * 2 + 1;                                                      //  make odd

   if (Mxpos < Srad) Mxpos = Srad;                                               //  restrict within image limits
   if (Mxpos > fww-1-Srad) Mxpos = fww-1-Srad;

   if (Mypos < Srad) Mypos = Srad;
   if (Mypos > fhh-1-Srad) Mypos = fhh-1-Srad;

   Dxpos = Mxpos * Mscale - Morgx + Dorgx;                                       //  corresp. mouse position in window
   Dypos = Mypos * Mscale - Morgy + Dorgy;

   org1x = Mxpos - Srad;                                                         //  source pixels in 1x image
   org1y = Mypos - Srad;

   ww1 = hh1 = Srad * 2;                                                         //  enclosing square

   pxb1 = gdk_pixbuf_new_subpixbuf(Fpxb->pixbuf,org1x,org1y,ww1,hh1);            //  pixbuf with source pixels
   if (! pxb1) return;

   ww2 = hh2 = drad * 2 + 1;                                                     //  enclosing square for scaled pixels

   if (pxb2) g_object_unref(pxb2);
   pxb2 = gdk_pixbuf_scale_simple(pxb1,ww2,hh2,BILINEAR);                        //  create pixbuf with scaled pixels

   g_object_unref(pxb1);
   if (! pxb2) return;

   org2x = Dxpos - (Mxpos - org1x) * scale;                                      //  pixbuf position in window
   org2y = Dypos - (Mypos - org1y) * scale;

   if (org2x < Dorgx) org2x = Dorgx;                                             //  keep within window
   if (org2x + ww2 > Dww - Dorgx)
      org2x = Dww - Dorgx - ww2;

   if (org2y < Dorgy) org2y = Dorgy;
   if (org2y + hh2 > Dhh - Dorgy)
      org2y = Dhh - Dorgy - hh2;

   pixels1 = gdk_pixbuf_get_pixels(Mpxb->pixbuf);                                //  image pixels at window scale
   rs1 = gdk_pixbuf_get_rowstride(Mpxb->pixbuf);                                 //    corresponding to square area
   nch1 = gdk_pixbuf_get_n_channels(Mpxb->pixbuf);

   pixels2 = gdk_pixbuf_get_pixels(pxb2);                                        //  magnified pixels to display
   rs2 = gdk_pixbuf_get_rowstride(pxb2);
   nch2 = gdk_pixbuf_get_n_channels(Mpxb->pixbuf);

   for (ry = -drad; ry <= drad; ry++)                                            //  loop all pixels
   for (rx = -drad; rx <= drad; rx++)
   {
      if (kernel[rx+drad][ry+drad]) continue;                                    //  within mouse circle, no changes

      px1 = rx + drad + org2x - Dorgx + Morgx;                                   //  outside mouse circle
      py1 = ry + drad + org2y - Dorgy + Morgy;
      pix1 = pixels1 + py1 * rs1 + px1 * nch1;                                   //  normal image pixel                 15.08

      px2 = rx + drad;
      py2 = ry + drad;
      pix2 = pixels2 + py2 * rs2 + px2 * nch2;                                   //  magnified pixel                    15.08

      memcpy(pix2,pix1,3);                                                       //  normal pixel replaces magnified pixel
   }

   paintlock(1);                                                                 //  16.03
   cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(cr,pxb2,org2x,org2y);
   cairo_paint(cr);

   Dxpos = org2x + drad + 2;                                                     //  draw circle around magnified area  15.01
   Dypos = org2y + drad + 2;
   cairo_set_source_rgb(cr,0,0,0);
   cairo_set_line_width(cr,2);
   cairo_arc(cr,Dxpos,Dypos,drad-2,0,2*PI);
   cairo_stroke(cr);
   cairo_destroy(cr);
   paintlock(0);                                                                 //  16.03

   return;
}


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

//  dark_brite menu function
//  highlight darkest and brightest pixels

namespace darkbrite {
   float    darklim = 0;
   float    brightlim = 255;
}

void m_darkbrite(GtkWidget *, const char *)
{
   using namespace darkbrite;

   int    darkbrite_dialog_event(zdialog* zd, const char *event);

   cchar    *title = ZTX("Darkest and Brightest Pixels");

   F1_help_topic = "darkbrite_pixels";

   if (! curr_file) return;                                                      //  no image file

/**
       ______________________________________
      |    Darkest and Brightest Pixels      |
      |                                      |
      |  Dark Limit   ===[]============ NNN  |
      |  Bright Limit ============[]=== NNN  |
      |                                      |
      |                              [Done]  |
      |______________________________________|

**/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,null);                             //  darkbrite dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=3|expand");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=3");
   zdialog_add_widget(zd,"label","labD","vb1",ZTX("Dark Limit"));
   zdialog_add_widget(zd,"label","labB","vb1",ZTX("Bright Limit"));
   zdialog_add_widget(zd,"hscale","limD","vb2","0|255|1|0","expand");
   zdialog_add_widget(zd,"hscale","limB","vb2","0|255|1|255","expand");
   zdialog_add_widget(zd,"label","valD","vb3");
   zdialog_add_widget(zd,"label","valB","vb3");

   zdialog_rescale(zd,"limD",0,0,255);                                           //  15.01
   zdialog_rescale(zd,"limB",0,255,255);

   zdialog_stuff(zd,"limD",darklim);                                             //  start with prior values
   zdialog_stuff(zd,"limB",brightlim);

   zdialog_resize(zd,300,0);
   zdialog_run(zd,darkbrite_dialog_event,"save");                                //  run dialog - parallel
   zddarkbrite = zd;                                                             //  global pointer for Fpaint*()

   zdialog_send_event(zd,"limD");                                                //  initz. NNN labels
   zdialog_send_event(zd,"limB");

   return;
}


//  darkbrite dialog event and completion function

int darkbrite_dialog_event(zdialog *zd, const char *event)                       //  darkbrite dialog event function
{
   using namespace darkbrite;

   char     text[8];

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)
   {
      zdialog_free(zd);
      zddarkbrite = 0;
      Fpaint2();
      return 0;
   }

   if (strmatch(event,"limD")) {
      zdialog_fetch(zd,"limD",darklim);
      snprintf(text,8,"%.0f",darklim);
      zdialog_stuff(zd,"valD",text);
   }

   if (strmatch(event,"limB")) {
      zdialog_fetch(zd,"limB",brightlim);
      snprintf(text,8,"%.0f",brightlim);
      zdialog_stuff(zd,"valB",text);
   }

   Fpaint2();
   return 0;
}


//  this function called by Fpaint() if zddarkbrite dialog active

void darkbrite_paint()
{
   using namespace darkbrite;

   int         px, py;
   uint8       *pix;
   float       P, D = darklim, B = brightlim;

   for (py = 0; py < Mpxb->hh; py++)                                             //  loop all image pixels
   for (px = 0; px < Mpxb->ww; px++)
   {
      pix = PXBpix(Mpxb,px,py);
      P = pixbright(pix);
      if (P < D) pix[0] = 100;                                                   //  dark pixel = mostly red
      else if (P > B) pix[0] = 0;                                                //  bright pixel = mostly green/blue
   }

   return;                                                                       //  not executed, avoid gcc warning
}


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

//  monitor color and contrast test function

void m_moncolor(GtkWidget *, cchar *)
{
   int  moncolor_dialog_event(zdialog *zd, cchar *event);

   char        file[200];
   int         err;
   char        *savecurrfile = 0;
   char        *savecurrdirk = 0;
   zdialog     *zd;
   cchar       *message = ZTX("Brightness should show a gradual ramp \n"
                              "extending all the way to the edges.");

   F1_help_topic = "monitor_color";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

   if (curr_file) savecurrfile = zstrdup(curr_file);
   if (curr_dirk) savecurrdirk = zstrdup(curr_dirk);

   snprintf(file,200,"%s/images/colorchart.png",get_zdatadir());                 //  color chart .png file

   err = f_open(file,0,0,1);
   if (err) goto restore;

   Fzoom = 1;
   gtk_window_set_title(MWIN,"check monitor");

   zd = zdialog_new("check monitor",Mwin,Bdone,null);                            //  start user dialog
   if (message) {
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
      zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   }

   zdialog_resize(zd,300,0);
   zdialog_run(zd,moncolor_dialog_event,"0/0");
   zdialog_wait(zd);                                                             //  wait for dialog complete
   zdialog_free(zd);

restore:

   if (savecurrdirk) {
      curr_dirk = zstrdup(savecurrdirk);
      zfree(savecurrdirk);
   }

   if (savecurrfile) {
      f_open(savecurrfile);
      zfree(savecurrfile);
   }
   else {
      curr_file = 0;
      if (curr_dirk) gallery(curr_dirk,"init",-1);
   }

   Fblock = 0;
   return;
}


//  dialog event and completion function

int moncolor_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   return 1;
}


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

//  check and adjust monitor gamma

void m_mongamma(GtkWidget *, cchar *)
{
   int   mongamma_dialog_event(zdialog *zd, cchar *event);

   int         err;
   char        gammachart[200];
   zdialog     *zd;
   char        *savecurrent = 0;

   cchar       *permit = "Chart courtesy of Norman Koren";
   cchar       *website = "http://www.normankoren.com/makingfineprints1A.html#gammachart";

   F1_help_topic = "monitor_gamma";

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

   err = shell_quiet("which xgamma");                                            //  check for xgamma
   if (err) {
      zmessageACK(Mwin,"xgamma program is not installed");
      return;
   }

   if (curr_file) savecurrent = zstrdup(curr_file);

   snprintf(gammachart,200,"%s/images/gammachart2.png",get_zdatadir());          //  gamma chart .png file
   err = f_open(gammachart);
   if (err) goto restore;

   Fzoom = 1;                                                                    //  scale 100% (required)
   gtk_window_set_title(MWIN,"adjust gamma chart");

   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,"link",permit,"hb2",website);

   zdialog_resize(zd,200,0);
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_run(zd,mongamma_dialog_event);
   zdialog_wait(zd);                                                             //  wait for dialog complete
   zdialog_free(zd);

restore:

   if (savecurrent) {
      f_open(savecurrent);                                                       //  back to current file
      zfree(savecurrent);
   }

   Fblock = 0;
   return;
}


//  dialog event function

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

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"gamma")) {
      zdialog_fetch(zd,"gamma",gamma);
      shell_ack("xgamma -quiet -gamma %.2f",gamma);
   }

   return 0;
}


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

//  set GUI language

void  m_changelang(GtkWidget *, cchar *)
{
   #define NL 8

   int  changelang_dialog_event(zdialog *zd, cchar *event);
   zdialog     *zd;
   int         ii, cc, val, zstat;
   char        progexe[300], lang1[NL], *pp;

   cchar  *langs[NL] = { "en English", "de German",                              //  english first
                        "ca Catalan", "es Spanish",
                        "fr French", "it Italian", 
                        "pt Portuguese", null  };

   cchar  *title = ZTX("Available Translations");

   F1_help_topic = "change_language";

   zd = zdialog_new(ZTX("Set Language"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"label","title","dialog",title,"space=5");
   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 (strmatchN(zfuncs::zlang,langs[ii],cc)) break;
   if (! langs[ii])
      for (ii = 0; langs[ii]; ii++)                                              //  failed, match on lc alone
         if (strmatchN(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_run(zd,changelang_dialog_event);                                      //  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],NL);
   pp = strchr(lang1,' ');                                                       //  isolate lc_RC part
   *pp = 0;

   cc = readlink("/proc/self/exe",progexe,300);                                  //  get own program path
   if (cc <= 0) {
      zmessageACK(Mwin,"cannot get /proc/self/exe");
      return;
   }
   progexe[cc] = 0;

   shell_ack("%s -l %s -p &",progexe,lang1);                                     //  start new fotoxx with language

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


//  dialog event and completion function

int changelang_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   return 1;
}


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

//  report missing translations in a popup window

void  m_untranslated(GtkWidget *, cchar *)
{
   int      ftf = 1, Nmiss = 0;
   cchar    *missing;
   char     text[40];

   F1_help_topic = "missing_translations";

   if (strmatchN(zfuncs::zlang,"en",2)) {
      zmessageACK(Mwin,"use a non-English locale");                              //  15.04
      return;
   }

   write_popup_text("open","missing translations",400,200,Mwin);

   while (true)
   {
      missing = ZTX_missing(ftf);
      if (! missing) break;
      write_popup_text("write",missing);
      Nmiss++;
   }

   snprintf(text,40,"%d missing translations",Nmiss);
   write_popup_text("write",text);
   write_popup_text("top",0);

   return;
}


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

//  printer color calibration tool                                         15.09

namespace calibprint
{
   int   dialog_event(zdialog *zd, cchar *event);
   void  printchart();
   void  scanchart();
   void  fixchart();
   void  processchart();

//  parameters for RGB step size of 23: 0 23 46 69 ... 253 (253 --> 255)
//  NC    colors per RGB dimension (12) (counting both 0 and 255)                //  step size from 16 to 23            16.03
//  CS    color step size (23) 
//  TS    tile size in pixels (70)
//  ROWS  chart rows (50)               ROWS x COLS must be >= NC*NC*NC
//  COLS  chart columns (35)

   #define NC 12
   #define CS 23
   #define TS 70
   #define ROWS 50
   #define COLS 35

   #define NC2 (NC*NC)
   #define NC3 (NC*NC*NC)

   int   RGBvals[NC];
   int   Ntiles = NC3;
   int   chartww = COLS * TS;             //  chart image size
   int   charthh = ROWS * TS;
   int   margin = 80;                     //  chart margins
   char  printchartfile[200];
   char  scanchartfile[200];
}


//  menu function

void m_calibrate_printer(GtkWidget *, cchar *menu)
{
   using namespace calibprint;
   
   zdialog     *zd;
   cchar       *title = ZTX("Calibrate Printer");

   F1_help_topic = "calibrate_printer";

   for (int ii = 0; ii < NC; ii++)                                               //  construct RGBvals table
      RGBvals[ii] = CS * ii;
   RGBvals[NC-1] = 255;                                                          //  set last value = 255

/***
       ______________________________________
      |    Calibrate Printer Colors          |
      |                                      |
      |  (o) print color chart               |
      |  (o) scan and save color chart       |
      |  (o) align and trim color chart      |
      |  (o) open and process color chart    |
      |  (o) print image with revised colors |
      |                                      |
      |                  [Proceed] [Cancel]  |
      |______________________________________|

***/

   zd = zdialog_new(title,Mwin,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"radio","printchart","dialog",ZTX("print color chart"));
   zdialog_add_widget(zd,"radio","scanchart","dialog",ZTX("scan and save color chart"));
   zdialog_add_widget(zd,"radio","fixchart","dialog",ZTX("align and trim color chart"));
   zdialog_add_widget(zd,"radio","processchart","dialog",ZTX("open and process color chart"));
   zdialog_add_widget(zd,"radio","printimage","dialog",ZTX("print image with revised colors"));

   zdialog_stuff(zd,"printchart",1);
   zdialog_stuff(zd,"scanchart",0);
   zdialog_stuff(zd,"fixchart",0);
   zdialog_stuff(zd,"processchart",0);
   zdialog_stuff(zd,"printimage",0);
   
   zdialog_resize(zd,250,0);
   zdialog_run(zd,dialog_event);
   return;
}


//  dialog event and completion function

int calibprint::dialog_event(zdialog *zd, cchar *event)
{
   using namespace calibprint;

   int      nn;
   
   F1_help_topic = "calibrate_printer";

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (! zd->zstat) return 1;                                                    //  wait for [proceed] or [cancel]

   if (zd->zstat != 1) {                                                         //  cancel
      zdialog_free(zd);
      return 1;
   }
   
   zdialog_fetch(zd,"printchart",nn);
   if (nn) {
      printchart();
      return 1;
   }
   
   zdialog_fetch(zd,"scanchart",nn);
   if (nn) {
      scanchart();
      return 1;
   }
   
   zdialog_fetch(zd,"fixchart",nn);
   if (nn) {
      fixchart();
      return 1;
   }
   
   zdialog_fetch(zd,"processchart",nn);
   if (nn) {
      processchart();
      return 1;
   }

   zdialog_fetch(zd,"printimage",nn);
   if (nn) {
      print_calibrated();
      return 1;
   }
   
   zd->zstat = 0;                                                                //  keep dialog active
   return 1;
}


//  generate and print the color chart

void calibprint::printchart()
{
   using namespace calibprint;

   int      ii, cc, fww, fhh, rx, gx, bx;
   int      row, col, px, py;
   int      chartrs;
   uint8    *chartpixels, *pix1;
   PIXBUF   *chartpxb;
   GError   *gerror = 0;
   
   fww = chartww + 2 * margin;
   fhh = charthh + 2 * margin;

   chartpxb = gdk_pixbuf_new(GDKRGB,0,8,fww,fhh);                                //  make chart image
   if (! chartpxb) {
      zmessageACK(Mwin,"cannot create pixbuf");
      return;
   }

   chartpixels = gdk_pixbuf_get_pixels(chartpxb);                                //  clear to white
   chartrs = gdk_pixbuf_get_rowstride(chartpxb);
   cc = fhh * chartrs;
   memset(chartpixels,255,cc);
   
   for (py = 0; py < charthh; py++)                                              //  fill chart tiles with colors
   for (px = 0; px < chartww; px++)
   {
      row = py / TS;
      col = px / TS;
      ii = row * COLS + col;
      if (ii >= Ntiles) break;                                                   //  last chart positions may be unused
      rx = ii / NC2;
      gx = (ii - NC2 * rx) / NC;                                                 //  RGB index values for tile ii
      bx = ii - NC2 * rx - NC * gx;
      pix1 = chartpixels + (py + margin) * chartrs + (px + margin) * 3;
      pix1[0] = RGBvals[rx];
      pix1[1] = RGBvals[gx];
      pix1[2] = RGBvals[bx];
   }
   
   for (py = margin-10; py < fhh-margin+10; py++)                                //  add green margin around tiles      15.10
   for (px = margin-10; px < fww-margin+10; px++)                                //    for easier de-skew and trim
   {
      if (py > margin-1 && py < fhh-margin &&
          px > margin-1 && px < fww-margin) continue;
      pix1 = chartpixels + py * chartrs + px * 3;
      pix1[0] = pix1[2] = 0;
      pix1[1] = 255;
   }
   
   snprintf(printchartfile,200,"%s/printchart.png",printer_color_dirk);
   gdk_pixbuf_save(chartpxb,printchartfile,"png",&gerror,null);
   if (gerror) {
      zmessageACK(Mwin,gerror->message);
      return;
   }
   
   g_object_unref(chartpxb);

   zmessageACK(Mwin,"Print chart in vertical orientation without margins.");
   print_image_file(Mwin,printchartfile);                                        //  print the chart

   return;
}


//  scan the color chart

void calibprint::scanchart()
{
   using namespace calibprint;

   zmessageACK(Mwin,ZTX("Scan the printed color chart. \n"
                        "The darkest row is at the top. \n"
                        "Save in %s/"),printer_color_dirk);
   return;
}


//  edit and fix the color chart

void calibprint::fixchart()
{
   using namespace calibprint;
   
   char     *pp;

   zmessageACK(Mwin,ZTX("Open and edit the scanned color chart file. \n"
                        "Remove any skew or rotation from scanning. \n"
                        "(Use the Fix Perspective function for this). \n"
                        "Cut off the thin green margin ACCURATELY."));
   
   pp = zgetfile("scanned color chart file",MWIN,"file",printer_color_dirk,1);
   if (! pp) return;
   strncpy0(scanchartfile,pp,200);
   f_open(scanchartfile,0,0,1,0);
   return;
}


//  process the scanned and fixed color chart

void calibprint::processchart()
{
   using namespace calibprint;

   PIXBUF   *chartpxb;
   GError   *gerror = 0;
   uint8    *chartpixels, *pix1;
   FILE     *fid;
   char     mapfile[200], *pp, *pp2;
   int      chartrs, chartnc, px, py;
   int      ii, nn, row, col, rx, gx, bx;
   int      xlo, xhi, ylo, yhi;
   float    fww, fhh;
   int      Rsum, Gsum, Bsum, Rout, Gout, Bout;
   int      r1, r2, ry, g1, g2, gy, b1, b2, by;
   int      ERR1[NC][NC][NC][3], ERR2[NC][NC][NC][3];

   zmessageACK(Mwin,ZTX("Open the trimmed color chart file"));

   pp = zgetfile("trimmed color chart file",MWIN,"file",printer_color_dirk,1);
   if (! pp) return;
   strncpy0(scanchartfile,pp,200);
   
   chartpxb = gdk_pixbuf_new_from_file(scanchartfile,&gerror);                   //  scanned chart without margins
   if (! chartpxb) {
      if (gerror) zmessageACK(Mwin,gerror->message);
      return;
   }
   
   chartww = gdk_pixbuf_get_width(chartpxb);
   charthh = gdk_pixbuf_get_height(chartpxb);
   chartpixels = gdk_pixbuf_get_pixels(chartpxb);
   chartrs = gdk_pixbuf_get_rowstride(chartpxb);
   chartnc = gdk_pixbuf_get_n_channels(chartpxb);
   fww = 1.0 * chartww / COLS;
   fhh = 1.0 * charthh / ROWS;

   for (row = 0; row < ROWS; row++)                                              //  loop each tile
   for (col = 0; col < COLS; col++)
   {
      ii = row * COLS + col;
      if (ii >= Ntiles) break;

      ylo = row * fhh;                                                           //  tile position within chart image
      yhi = ylo + fhh;
      xlo = col * fww;
      xhi = xlo + fww;

      Rsum = Gsum = Bsum = nn = 0;

      for (py = ylo+fhh/5; py < yhi-fhh/5; py++)                                 //  get tile pixels less 20% margins
      for (px = xlo+fww/5; px < xhi-fww/5; px++)
      {
         pix1 = chartpixels + py * chartrs + px * chartnc;
         Rsum += pix1[0];
         Gsum += pix1[1];
         Bsum += pix1[2];
         nn++;
      }

      Rout = Rsum / nn;                                                          //  average tile RGB values
      Gout = Gsum / nn;
      Bout = Bsum / nn;

      rx = ii / NC2;
      gx = (ii - NC2 * rx) / NC;
      bx = ii - NC2 * rx - NC * gx;
      
      ERR1[rx][gx][bx][0] = Rout - RGBvals[rx];                                  //  error = (scammed RGB) - (printed RGB)
      ERR1[rx][gx][bx][1] = Gout - RGBvals[gx];
      ERR1[rx][gx][bx][2] = Bout - RGBvals[bx];
   }

   g_object_unref(chartpxb);

   //  anneal the error values to reduce randomness
   
   for (int pass = 1; pass <= 4; pass++)                                         //  4 passes                           15.10
   {
      for (rx = 0; rx < NC; rx++)                                                //  use neighbors in 3 channels
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         r1 = rx-1;
         r2 = rx+1;
         g1 = gx-1;
         g2 = gx+1;
         b1 = bx-1;
         b2 = bx+1;

         if (r1 < 0) r1 = 0;
         if (r2 > NC-1) r2 = NC-1;
         if (g1 < 0) g1 = 0;
         if (g2 > NC-1) g2 = NC-1;
         if (b1 < 0) b1 = 0;
         if (b2 > NC-1) b2 = NC-1;
         
         Rsum = Gsum = Bsum = nn = 0;

         for (ry = r1; ry <= r2; ry++)
         for (gy = g1; gy <= g2; gy++)
         for (by = b1; by <= b2; by++)
         {
            Rsum += ERR1[ry][gy][by][0];
            Gsum += ERR1[ry][gy][by][1];
            Bsum += ERR1[ry][gy][by][2];
            nn++;
         }

         ERR2[rx][gx][bx][0] = Rsum / nn;
         ERR2[rx][gx][bx][1] = Gsum / nn;
         ERR2[rx][gx][bx][2] = Bsum / nn;
      }

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         ERR1[rx][gx][bx][0] = ERR2[rx][gx][bx][0];
         ERR1[rx][gx][bx][1] = ERR2[rx][gx][bx][1];
         ERR1[rx][gx][bx][2] = ERR2[rx][gx][bx][2];
      }

      for (rx = 1; rx < NC-1; rx++)                                              //  use neighbors in same channel      15.10
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
         ERR2[rx][gx][bx][0] = 0.5 * (ERR1[rx-1][gx][bx][0] + ERR1[rx+1][gx][bx][0]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 1; gx < NC-1; gx++)
      for (bx = 0; bx < NC; bx++)
         ERR2[rx][gx][bx][1] = 0.5 * (ERR1[rx][gx-1][bx][1] + ERR1[rx][gx+1][bx][1]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 1; bx < NC-1; bx++)
         ERR2[rx][gx][bx][2] = 0.5 * (ERR1[rx][gx][bx-1][2] + ERR1[rx][gx][bx+1][2]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         ERR1[rx][gx][bx][0] = ERR2[rx][gx][bx][0];
         ERR1[rx][gx][bx][1] = ERR2[rx][gx][bx][1];
         ERR1[rx][gx][bx][2] = ERR2[rx][gx][bx][2];
      }
   }                                                                             //  pass loop

   //  save finished color map to user-selected file

   zmessageACK(Mwin,ZTX("Set the name for the output calibration file \n"
                        "[your calibration name].dat"));

   snprintf(mapfile,200,"%s/%s",printer_color_dirk,colormapfile);
   pp = zgetfile("Color Map File",MWIN,"save",mapfile,1);
   if (! pp) return;
   pp2 = strrchr(pp,'/');
   zfree(colormapfile);
   colormapfile = zstrdup(pp2+1);
   save_params();
   zfree(pp);
   
   snprintf(mapfile,200,"%s/%s",printer_color_dirk,colormapfile);
   fid = fopen(mapfile,"w");
   if (! fid) return;

   for (rx = 0; rx < NC; rx++)
   for (gx = 0; gx < NC; gx++)
   for (bx = 0; bx < NC; bx++)
   {
      fprintf(fid,"RGB: %3d %3d %3d   ERR: %4d %4d %4d \n",
          RGBvals[rx], RGBvals[gx], RGBvals[bx],
          ERR2[rx][gx][bx][0], ERR2[rx][gx][bx][1], ERR2[rx][gx][bx][2]);
   }

   fclose(fid);

   return;
}


//  Print the current image file with adjusted colors
//  Also called from the file menu function m_print_calibrated()

void print_calibrated()
{
   using namespace calibprint;

   zdialog  *zd;
   int      zstat;
   cchar    *title = ZTX("Color map file to use");
   char     mapfile[200];
   FILE     *fid;
   PIXBUF   *pixbuf;
   GError   *gerror = 0;
   uint8    *pixels, *pix1;
   char     *pp, *pp2, printfile[100];
   int      ww, hh, rs, nc, px, py, nn, err;
   int      R1, G1, B1, R2, G2, B2;
   int      RGB[NC][NC][NC][3], ERR[NC][NC][NC][3];
   int      rr1, rr2, gg1, gg2, bb1, bb2;
   int      rx, gx, bx;
   int      ii, Dr, Dg, Db;
   float    W[8], w, Wsum, D, Dmax, F;
   float    Er, Eg, Eb;
   
   if (! curr_file) {
      zmessageACK(Mwin,ZTX("Select the image file to print."));
      return;
   }
   
   for (int ii = 0; ii < NC; ii++)                                               //  construct RGBvals table
      RGBvals[ii] = CS * ii;
   RGBvals[NC-1] = 255;                                                          //  set last value = 255

   zd = zdialog_new(title,Mwin,Bbrowse,Bproceed,Bcancel,null);                   //  show current color map file 
   zdialog_add_widget(zd,"hbox","hbmap","dialog");                               //    and allow user to choose another
   zdialog_add_widget(zd,"label","labmap","hbmap",0,"space=3");
   zdialog_stuff(zd,"labmap",colormapfile);
   zdialog_resize(zd,250,0);                                                     //  15.11
   zdialog_run(zd);
   zstat = zdialog_wait(zd);
   zdialog_free(zd);

   if (zstat == 1) {                                                             //  [browse]
      snprintf(mapfile,200,"%s/%s",printer_color_dirk,colormapfile);
      pp = zgetfile("Color Map File",MWIN,"file",mapfile,1);
      if (! pp) return;
      pp2 = strrchr(pp,'/');
      if (colormapfile) zfree(colormapfile);
      colormapfile = zstrdup(pp2+1);
      zfree(pp);
   }
   
   else if (zstat != 2) return;                                                  //  not proceed: cancel
   
   snprintf(mapfile,200,"%s/%s",printer_color_dirk,colormapfile);
   fid = fopen(mapfile,"r");                                                     //  read color map file
   if (! fid) return;
   
   for (R1 = 0; R1 < NC; R1++)
   for (G1 = 0; G1 < NC; G1++)
   for (B1 = 0; B1 < NC; B1++)
   {
      nn = fscanf(fid,"RGB: %d %d %d   ERR: %d %d %d ",
           &RGB[R1][G1][B1][0], &RGB[R1][G1][B1][1], &RGB[R1][G1][B1][2],
           &ERR[R1][G1][B1][0], &ERR[R1][G1][B1][1], &ERR[R1][G1][B1][2]);
      if (nn != 6) {
         zmessageACK(Mwin,ZTX("file format error"));                             //  16.03
         fclose(fid);
         return;
      }
   }
   
   fclose(fid);
   
   pixbuf = gdk_pixbuf_copy(Fpxb->pixbuf);                                       //  get image pixbuf to convert
   if (! pixbuf) {
      if (gerror) zmessageACK(Mwin,gerror->message);
      return;
   }
   
   if (checkpend("all")) return;
   Fblock = 1;                                                                   //  15.11
   Ffuncbusy = 1;

   ww = gdk_pixbuf_get_width(pixbuf);
   hh = gdk_pixbuf_get_height(pixbuf);
   pixels = gdk_pixbuf_get_pixels(pixbuf);
   rs = gdk_pixbuf_get_rowstride(pixbuf);
   nc = gdk_pixbuf_get_n_channels(pixbuf);
   
   poptext_window(ZTX("converting colors..."),MWIN,300,200,0,-1);

   for (py = 0; py < hh; py++)
   for (px = 0; px < ww; px++)
   {
      pix1 = pixels + py * rs + px * nc;
      R1 = pix1[0];                                                              //  image RGB values
      G1 = pix1[1];
      B1 = pix1[2];
      
      rr1 = R1/CS;                                                               //  get color map values surrounding RGB
      rr2 = rr1 + 1;
      if (rr2 > NC-1) {                                                          //  if > last entry, use last entry
         rr1--; 
         rr2--; 
      }

      gg1 = G1/CS;
      gg2 = gg1 + 1;
      if (gg2 > NC-1) { 
         gg1--; 
         gg2--; 
      }

      bb1 = B1/CS;
      bb2 = bb1 + 1;
      if (bb2 > NC-1) { 
         bb1--; 
         bb2--; 
      }

      ii = 0;
      Wsum = 0;
      Dmax = CS;
      
      for (rx = rr1; rx <= rr2; rx++)                                            //  loop 8 enclosing error map nodes
      for (gx = gg1; gx <= gg2; gx++)
      for (bx = bb1; bx <= bb2; bx++)
      {
         Dr = R1 - RGBvals[rx];                                                  //  RGB distance from enclosing node
         Dg = G1 - RGBvals[gx];
         Db = B1 - RGBvals[bx];
         D = sqrtf(Dr*Dr + Dg*Dg + Db*Db);
         if (D > Dmax) W[ii] = 0;
         else W[ii] = (Dmax - D) / Dmax;                                         //  weight of node
         Wsum += W[ii];                                                          //  sum of weights
         ii++;
      }
      
      ii = 0;
      Er = Eg = Eb = 0;

      for (rx = rr1; rx <= rr2; rx++)                                            //  loop 8 enclosing error map nodes
      for (gx = gg1; gx <= gg2; gx++)
      for (bx = bb1; bx <= bb2; bx++)
      {
         w = W[ii] / Wsum;
         Er += w * ERR[rx][gx][bx][0];                                           //  weighted sum of map node errors 
         Eg += w * ERR[rx][gx][bx][1];
         Eb += w * ERR[rx][gx][bx][2];
         ii++;
      }
      
      F = 1.0;                                                                   //  use 100% of calculated error
      R2 = R1 - F * Er;                                                          //  adjusted RGB = image RGB - error
      G2 = G1 - F * Eg;
      B2 = B1 - F * Eb;

      if (R2 < 0) R2 = 0;
      if (G2 < 0) G2 = 0;
      if (B2 < 0) B2 = 0;
      
      if (R2 > 255) R2 = 255;                                                    //  preserving RGB ratio does not help
      if (G2 > 255) G2 = 255;
      if (B2 > 255) B2 = 255;

      pix1[0] = R2;
      pix1[1] = G2;
      pix1[2] = B2;
      
      zmainloop(100);                                                            //  keep GTK alive
   }
   
   poptext_killnow();

   Ffuncbusy = 0;
   Fblock = 0;                                                                   //  15.11

   snprintf(printfile,100,"%s/printfile.png",tempdir);                           //  save revised pixbuf to print file
   gdk_pixbuf_save(pixbuf,printfile,"png",&gerror,"compression","1",null);
   if (gerror) {
      zmessageACK(Mwin,gerror->message);
      return;
   }
   
   g_object_unref(pixbuf);
   
   err = f_open(printfile,0,0,1,0);                                              //  open print file
   if (err) return;

   zmessageACK(Mwin,ZTX("Image colors are converted for printing."));
   print_image_file(Mwin,printfile);

   return;
}


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

//  Dump CPU usage, zdialog statistics, memory statistics
//  counters are reset

void m_resources(GtkWidget *, cchar *)
{
   F1_help_topic = "resources";                                                  //  15.07
   zmalloc_report();
   printz(" thumbnail cache: %.0f MB \n",navi::thumbcache_MB);
   return;
}


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

//  zappcrash test - make a segment fault

char     *zapptext = null;

void m_zappcrash(GtkWidget *, cchar *)
{
   int      ii, jj;

   for (ii = 0; ii < 100; ii++) {                                                //  fake-out compiler diagnostics
      jj = 1000.0 * drandz();
      zapptext[jj] = 'a' + ii / 10;                                              //  write to address 0 + jj
   }

   printz("zapptext: %s \n",zapptext);
   return;
}



