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

   Fotocx - edit photos and manage collections

   Copyright 2007-2026 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

   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.

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

   Fotocx image edit - Tools menu functions

   m_index                 dialog to create/update image index file
   index_rebuild           update image index file with updates search
   index_rebuild_old       use old image index file without updates search
   m_move_fotocx_home      move home folder from $HOME/.fotocx to chosen folder
   m_settings              user preferences and settings dialog
   m_KB_shortcuts          edit keyboard shortcuts
   KB_shortcuts_load       load KB shortcuts file into memory
   m_RGB_hist              show RGB brightness histogram graph
   m_magnify               magnify the image within a radius of the mouse
   m_measure_image         measure distances within an image
   m_show_RGB              show RGB values for clicked image positions
   m_grid_settings         configure grid lines
   m_toggle_grid           toggle grid lines on/off
   m_line_color            choose color for foreground lines
   m_darkbrite             highlight darkest and brightest pixels
   m_monitor_color         monitor color and contrast check
   m_duplicates            find all duplicated images
   m_resources             memory allocated, CPU time, map cache
   m_translations          language translation functions                              //  26.0

   developer menu
   m_zmalloc_report        report memory allocated by tag name
   m_zmalloc_growth        report memory increases by tag name
   m_mouse_events          show mouse events popup text
   m_zappcrash_test        zappcrash test
   m_audit_userguide       check that all F1 help topics are present


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

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

using namespace zfuncs;

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

//  Index Image Files menu function
//  Dialog to get top image folders, thumbnails folder, indexed metadata tags.
//  Update the index config file and generate new image index.

namespace index_names
{
   zdialog     *zd_indexlog = 0;                                                       //  log report window
   GtkWidget   *wlog = 0;                                                              //  log report text widget
   int8        *Xstatus = 0, *Tstatus = 0;
   int         index_thread_busy, thumb_thread_busy;
   int         FfullindexReq = 0;                                                      //  force full re-index

   int         index_updates;                                                          //  file indexing progress counters
   int         final_index_updates;
   int         atd_updates;
   int         thumb_updates;
   int         thumb_deletes;
}


//  index menu function

void m_index(GtkWidget *, ch *)
{
   using namespace index_names;

   int index_CBfunc(GtkWidget *widget, int line, int pos, ch *input);
   int index_dialog_event(zdialog *zd, ch *event);

   zdialog        *zd;
   FILE           *fid;
   ch             buff[200], sthumbfolder[200];
   ch             *pp;
   GtkWidget      *widget;
   int            line, cc, zstat;
   ch             *greet1 = TX("Folders for image files "
                               "(subfolders included automatically)");
   ch             *greet2 = TX("Select to add, click on X to delete.");
   ch             *greet3 = TX("Thumbnails folder");
   ch             *greet4 = TX("extra metadata tags to include in index");
   ch             *greet5 = TX("force a full re-index of all image files");
   ch             *termmess = TX("Index function terminated. \n"
                                 "Indexing is required for search and map functions \n"
                                 "and to make thumbnail gallery pages display fast.");

   F1_help_topic = TX("INDEX FILES");
   printf("m_index \n");

   FfullindexReq = 0;

   if (Fblock("index files")) return;

/***
          ______________________________________________________________
         |                   Index Image Files                          |
         |                                                              |
         | Folders for image files (subfolders included automatically). |
         | [Select] Select to add, click on X to delete.                |
         |  __________________________________________________________  |
         | | X  /home/<user>/Pictures                                 | |
         | | X  /home/<user>/...                                      | |
         | |                                                          | |
         | |                                                          | |
         | |__________________________________________________________| |
         |                                                              |
         | [Select] Thumbnails folder                                   |
         |  __________________________________________________________  |
         | |__________________________________________________________| |
         |                                                              |
         | [Select] extra metadata tags to include in index             |
         |                                                              |
         | [x] force a full re-index of all image files                 |
         |                                                              |
         |                                         [Help] [Proceed] [X] |
         |______________________________________________________________|

***/

   zd = zdialog_new("Index Image Files",Mwin,"Help",TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbgreet1","dialog");
   zdialog_add_widget(zd,"label","labgreet1","hbgreet1",greet1,"space=3");
   zdialog_add_widget(zd,"hbox","hbtop","dialog");
   zdialog_add_widget(zd,"button","browsetop","hbtop",TX("Select"),"space=3");         //  browse top button
   zdialog_add_widget(zd,"label","labgreet2","hbtop",greet2,"space=5");

   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,"scrwin","scrtop","vbtop2",0,"expand");
   zdialog_add_widget(zd,"text","topfolders","scrtop");                                //  topfolders text

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbthumb1","dialog");
   zdialog_add_widget(zd,"button","browsethumb","hbthumb1",TX("Select"),"space=3");    //  browse thumb button
   zdialog_add_widget(zd,"label","labgreet3","hbthumb1",greet3,"space=3");
   zdialog_add_widget(zd,"hbox","hbthumb2","dialog");
   zdialog_add_widget(zd,"text","sthumbfolder","hbthumb2",0,"expand");                 //  thumbnail folder

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbxmeta","dialog");
   zdialog_add_widget(zd,"button","browsxmeta","hbxmeta",TX("Select"),"space=3");      //  browse xmeta metadata
   zdialog_add_widget(zd,"label","labgreet4","hbxmeta",greet4,"space=5");

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");                         //  force full re-index
   zdialog_add_widget(zd,"hbox","hbforce","dialog");
   zdialog_add_widget(zd,"check","forcex","hbforce",greet5,"space=3");

   widget = zdialog_gtkwidget(zd,"topfolders");                                        //  set mouse/KB event function
   txwidget_set_eventfunc(widget,index_CBfunc);                                        //    for top folders text window

   txwidget_clear(widget);                                                             //  default top folder
   txwidget_append(widget,0," X  %s/Pictures\n",getenv("HOME"));                       //    /home/<user>/Pictures

   snprintf(sthumbfolder,200,"%s/thumbnails",get_zhomedir());                          //  default thumbnails folder
   zdialog_stuff(zd,"sthumbfolder",sthumbfolder);                                      //    /home/<user>/.fotocx/thumbnails
   
   xmetaNtags = 0;                                                                     //  default no extra indexed metadata

   fid = fopen(image_folders_file,"r");                                                //  read image folders file
   if (fid) {                                                                          //    stuff data into dialog widgets
      txwidget_clear(widget);
      while (true) {
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails:",11)) {                                       //  if "thumbnails: /..."
            if (buff[12] == '/')                                                       //    stuff thumbnails folder
               zdialog_stuff(zd,"sthumbfolder",buff+12);
         }
         else txwidget_append(widget,0," X  %s\n",buff);                               //  stuff " X  /dir1/dir2..."
      }
      fclose(fid);
   }

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

   while (zstat == 1) {                                                                //  [help]
      zd->zstat = 0;                                                                   //  keep dialog active
      m_help(0,"Help");
      zstat = zdialog_wait(zd);
   }

   if (zstat != 2)                                                                     //  canceled
   {
      zdialog_free(zd);
      zmessageACK(Mwin,termmess);                                                      //  warn: index not finished
      Xindexlev = 0;
      Fblock(0);
      return;
   }

   fid = fopen(image_folders_file,"w");                                                //  open/write image folders file
   if (! fid) {                                                                        //  fatal error
      zmessageACK(Mwin,TX("cannot write image folders file: %s"),strerror(errno));
      Xindexlev = 0;                                                                   //  26.0
      Fblock(0);
      return;
   }

   widget = zdialog_gtkwidget(zd,"topfolders");                                        //  get top folders from dialog widget

   for (line = 0; ; line++) {
      pp = txwidget_line(widget,line,1);                                               //  loop widget text lines
      if (! pp || ! *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 folder to file
   }
   
   zdialog_fetch(zd,"sthumbfolder",buff,200);                                          //  get thumbnails folder from dialog
   strTrim2(buff);                                                                     //  remove surrounding blanks
   cc = strlen(buff);
   if (cc && buff[cc-1] == '/') buff[cc-1] = 0;                                        //  remove trailing '/'
   fprintf(fid,"thumbnails: %s\n",buff);                                               //  write thumbnails folder to file
   
   fclose(fid);
   zdialog_free(zd);                                                                   //  close dialog
   
   index_rebuild(2,1);                                                                 //  build image index and thumbnails      25.1

   Fblock(0);
   return;
}


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

int index_CBfunc(GtkWidget *widget, int line, int pos, ch *input)
{
   GdkWindow   *gdkwin;
   ch          *pp;
   ch          *dirlist[maxtopfolders];
   int         ii, jj;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   gdkwin = gtk_widget_get_window(widget);                                             //  stop updates between clear and refresh
   gdk_window_freeze_updates(gdkwin);

   for (ii = jj = 0; ii < maxtopfolders; ii++)                                         //  loop text lines in widget
   {                                                                                   //    " X  /dir1/dir2/... "
      pp = txwidget_line(widget,ii,1);
      if (! pp || strlen(pp) < 4) break;
      if (ii == line && pos < 3) continue;                                             //  if "X" clicked, skip deleted line
      dirlist[jj] = zstrdup(pp,"index-dialog");
      jj++;
   }

   txwidget_clear(widget);

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

   gdk_window_thaw_updates(gdkwin);
   return 1;
}


//  index dialog event and completion function

int index_dialog_event(zdialog *zd, ch *event)
{
   using namespace index_names;

   int         ii, nn, yn;
   GtkWidget   *widget;
   ch          **flist, *pp, *sthumbfolder;
   zlist_t     *mlist;
   ch          *topmess = TX("Choose top image folders");
   ch          *thumbmess = TX("Choose thumbnail folder");
   ch          *xmetamess = TX("All image files will be re-indexed. \n"
                               "Continue?");

   if (strmatch(event,"browsetop")) {                                                  //  [browse] top folders
      flist = zgetfiles(topmess,MWIN,"folders",getenv("HOME"));                        //  get top folders from user
      if (! flist) return 1;
      widget = zdialog_gtkwidget(zd,"topfolders");                                     //  add to dialog list
      for (ii = 0; flist[ii]; ii++) {
         txwidget_append2(widget,0," X  %s\n",flist[ii]);                              //  " X  /dir1/dir2/..."
         zfree(flist[ii]);
      }
      zfree(flist);
   }

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

   if (strmatch(event,"browsxmeta")) {                                                 //  [select]
      yn = zmessageYN(Mwin,xmetamess);                                                 //  add optional indexed metadata
      if (! yn) return 1;

      mlist = zlist_from_file(meta_index_file);                                        //  index extra metadata tags
      if (! mlist) mlist = zlist_new(0);
      nn = select_meta_tags(mlist,xmetaXtags,1);                                       //  user edit
      if (nn) {
         zlist_to_file(mlist,meta_index_file);                                         //  changes made, replace file
         FfullindexReq = 1;                                                            //  full index required
      }
      zlist_free(mlist);
   }

   if (strmatch(event,"forcex")) {                                                     //  force full re-index
      zdialog_fetch(zd,"forcex",ii);
      if (ii) FfullindexReq = 1;
   }

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

   if (zd->zstat == 1) {                                                               //  [help]
      zd->zstat = 0;
      showz_docfile(Mwin,UGfile,"INDEX_FILES");
      return 1;
   }

   return 1;                                                                           //  proceed or cancel status
}


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

//  Rebuild the image index from the index config data.
//  index level = 0/1/2  =  no index / old files only / old + search new files
//  Called from main() when Fotocx is started (index level from user setting)
//  Called from menu function m_index() (index level = 2, keepopen = 1)

void index_rebuild(int indexlev, int keepopen)
{
   using namespace index_names;

   int      indexlog_dialog_event(zdialog *zd, ch *event);
   void     indexlog(int bold, ch *format, ...);
   int      index_compare(ch *rec1, ch *rec2);
   void *   thumb_thread(void *);
   void *   index_thread(void *);

   xxrec_t     *xxrec = 0, **xxrec_old = 0, **xxrec_new = 0;
   FILE        *fid;
   zlist_t     *mlist;
   ch          **topfol2, **misstop2;
   int         *topcc2, *misscc2;
   int         ii, jj, nn;
   int         Ntop, Nthumb;
   int         ftf, NF, orec, orec2, nrec, xrec, comp;
   int         cc, cc1, cc2, err;
   int         Nnew, Nold;
   ch          *pp, *pp1, *pp2;
   ch          buff[XFCC+500];
   ch          **flist, *file, *thumbfile;
   ch          *xtagname;
   STATB       statB;
   double      startime;
   int         Nraw, Nimage, Nvideo;
   double      rawgb, imagegb, videogb;

   ch   *indexmess = TX("No image file index was found.\n"
                        "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.");

   ch   *indexerr2 = TX("Thumbnails folder: %s \n"
                        "Please remove.");

   ch   *thumberr = TX("Thumbnails folder: \n %s \n"
                       "must be named .../thumbnails");

   ch   *duperr = TX("Duplicate or nested folders: \n %s \n %s \n"
                     "Please remove.");

   startime = get_seconds();                                                           //  index function start time

   xxrec_old = xxrec_new = 0;                                                          //  initz.
   Xstatus = Tstatus = 0;
   Xindexlev = 0;

   //  create log window for reporting status and statistics

   if (zd_indexlog) zdialog_free(zd_indexlog);                                         //  25.1
   zd_indexlog = 0;
   wlog = 0;

   if (indexlev > 1) {                                                                 //  popup window log report               25.1
      zd_indexlog = zdialog_new("build index",Mwin,"X",null);
      zdialog_add_widget(zd_indexlog,"scrwin","scrwin","dialog",0,"expand");
      zdialog_add_widget(zd_indexlog,"text","text","scrwin");
      wlog = zdialog_gtkwidget(zd_indexlog,"text");
      zdialog_resize(zd_indexlog,700,500);
      zdialog_run(zd_indexlog,indexlog_dialog_event,"parent");
   }

   //  get current top image folders and thumbnails folder

   for (ii = 0; ii < Ntopfolders; ii++)                                                //  free prior
      zfree(topfolders[ii]);

   Ntop = Nthumb = 0;
   thumbfolder = 0;                                                                    //  25.1

   fid = fopen(image_folders_file,"r");                                                //  read index config file
   if (fid) {
      while (true) {                                                                   //  get top image folders
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails: /",13)) {                                     //  get thumbnails folder
            zstrcopy(thumbfolder,buff+12,"thumb-folder");
            Nthumb++;
         }
         else {
            topfolders[Ntop] = zstrdup(buff,"top-folders");
            if (++Ntop == maxtopfolders) break;
         }
      }
      fclose(fid);
   }

   Ntopfolders = Ntop;

   if (indexlev == 0)                                                                  //  indexing is disabled
   {
      if (Nthumb != 1)                                                                 //  no index config file, or invalid
      {
         thumbfolder = zstrdup("","thumb-folder",200);                                 //  set default thumbnails folder
         snprintf(thumbfolder,200,"%s/thumbnails",get_zhomedir());

         fid = fopen(image_folders_file,"w");                                          //  create stump index config file
         if (! fid) {                                                                  //  (thumbnails folder only)
            zmessageACK(Mwin,TX("index config file error: %s"),strerror(errno));
            Xindexlev = 0;                                                             //  26.0
            return;
         }
         fprintf(fid,"thumbnails: %s\n",thumbfolder);
         fclose(fid);
      }

      if (! dirfile(thumbfolder)) {                                                    //  create thumbnails folder if needed
         pp = zescape_quotes(thumbfolder);
         err = zshell("log ack","mkdir -p -m 0750 \"%s\" ",pp);
         if (err) {
            Xindexlev = 0;                                                             //  26.0
            return;
         }
      }

      Nthumb = 1;
      indexlog(0,TX("thumbnails folder: %s \n"),thumbfolder);                          //  log thumbnails folder

      Ntopfolders = 0;                                                                 //  no top image folders
      indexlog(0,TX("no image index: reports disabled \n"));                           //  no image index
      Xindexlev = 0;                                                                   //  25.1
      return;
   }

   if (! Ntopfolders) {                                                                //  if nothing found, must ask user
      zmessageACK(Mwin,TX("specify at least 1 top image folder"));
      goto cleanup;
   }

   if (Nthumb != 1) {                                                                  //  0 or >1 thumbnail folders
      zmessageACK(Mwin,TX("specify a thumbnail folder"));
      goto cleanup;
   }

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

   if (! dirfile(thumbfolder)) {                                                       //  create thumbnails folder if needed
      pp = zescape_quotes(thumbfolder);
      err = zshell("log ack","mkdir -p -m 0750 \"%s\" ",pp);
      if (err) {
         Xindexlev = 0;                                                                //  26.0
         return;
      }
   }

   for (ii = 0; ii < Ntopfolders; ii++) {                                              //  disallow top dir = thumbnail dir
      if (strmatch(topfolders[ii],thumbfolder)) {
         zmessageACK(Mwin,indexerr2,topfolders[ii]);
         goto cleanup;
      }
   }

   image_file_type_init();                                                             //  initz. to detect thumbnail files      25.1

   for (ii = 0; ii < Ntopfolders; ii++)                                                //  check for duplicate folders
   for (jj = ii+1; jj < Ntopfolders; jj++)                                             //   or nested folders
   {
      cc1 = strlen(topfolders[ii]);
      cc2 = strlen(topfolders[jj]);
      if (cc1 <= cc2) {
         pp1 = zstrdup(topfolders[ii],"top-folders",2);
         pp2 = zstrdup(topfolders[jj],"top-folders",2);
      }
      else {
         pp1 = zstrdup(topfolders[jj],"top-folders",2);
         pp2 = zstrdup(topfolders[ii],"top-folders",2);
         cc = cc1;
         cc1 = cc2;
         cc2 = cc;
      }
      strcpy(pp1+cc1,"/");
      strcpy(pp2+cc2,"/");
      nn = strmatchN(pp1,pp2,cc1+1);
      zfree(pp1);
      zfree(pp2);
      if (nn) {
         zmessageACK(Mwin,duperr,topfolders[ii],topfolders[jj]);                       //  user must fix
         goto cleanup;
      }
   }

   for (ii = jj = Nmisstops = 0; ii < Ntopfolders; ii++)                               //  check top image folders
   {
      if (! dirfile(topfolders[ii]))                                                   //  move missing top image folders
         misstops[Nmisstops++] = topfolders[ii];                                       //    into a separate list
      else topfolders[jj++] = topfolders[ii];                                          //      (poss. not mounted)
   }

   Ntopfolders = jj;                                                                   //  valid top image folders
   if (! Ntopfolders) {                                                                //  none, must ask user
      zmessageACK(Mwin,indexmess);
      goto cleanup;
   }

   topfol2 = (ch **) zmalloc(Ntopfolders * sizeof(ch *),"top-folders");                //  top folders with '/' appended
   topcc2 = (int *) zmalloc(Ntopfolders * sizeof(int),"top-folders");                  //  cc of same

   misstop2 = 0;
   misscc2 = 0;
   if (Nmisstops) {
      misstop2 = (ch **) zmalloc(Nmisstops * sizeof(ch *),"top-folders");              //  missing top folders with '/'
      misscc2 = (int *) zmalloc(Nmisstops * sizeof(int),"top-folders");                //  cc of same
   }

   for (ii = 0; ii < Ntopfolders; ii++) {                                              //  save top folders with appended '/'
      topfol2[ii] = zstrdup(topfolders[ii],"top-folders",2);                           //    for use with later comparisons
      strcat(topfol2[ii],"/");
      topcc2[ii] = strlen(topfol2[ii]);
   }

   for (ii = 0; ii < Nmisstops; ii++) {
      misstop2[ii] = zstrdup(misstops[ii],"top-folders",2);
      strcat(misstop2[ii],"/");
      misscc2[ii] = strlen(misstop2[ii]);
   }

   indexlog(0,TX("top image folders: \n"));
   for (ii = 0; ii < Ntopfolders; ii++)                                                //  log top folders
      indexlog(0," %s\n",topfolders[ii]);

   nn = 0.001 * diskspace(topfolders[0]);                                              //  log free disk space in GB
   indexlog(0,TX("free disk space: %d GB \n"),nn);

   if (Nmisstops)
   for (ii = 0; ii < Nmisstops; ii++)                                                  //  log missing top folders
      indexlog(0," %s *** not mounted *** \n",misstops[ii]);

   indexlog(0,TX("thumbnails folder: %s \n"),thumbfolder);                             //  log thumbnails folder

   mlist = zlist_from_file(meta_index_file);                                           //  get extra indexed metadata tags
   xmetaNtags = 0;

   if (mlist) {
      indexlog(0,TX("extra metadata tags: \n"));
      xmetaNtags = zlist_count(mlist);
      if (xmetaNtags > xmetaXtags) {
         zmessageACK(Mwin,TX("extra indexed metadata tags > %d"),xmetaXtags);
         goto cleanup;
      }
      for (ii = 0; ii < xmetaNtags; ii++) {                                            //  log extra metadata tags
         xtagname = zlist_get(mlist,ii);
         xmeta_tags[ii] = xtagname;
         indexlog(0,"%s, ",xtagname);
      }
      indexlog(0,"\n");
   }
   
   Nblacklist = 0;

   fid = fopen(blacklist_file,"r");                                                    //  read blacklisted folders/files
   if (fid)                                                                            //  e.g. /pathname/* or */filename.tif
   {
      while (true) {
         pp = fgets_trim(buff,XFCC,fid,1);
         if (! pp) break;
         if (strlen(pp) < 2) continue;
         indexlog(0,"blacklist file: %s \n",pp);                                       //  log blacklist
         blacklist[Nblacklist] = zstrdup(pp,"blacklist");
         Nblacklist++;
         if (Nblacklist == 1000) {
            zmessageACK(Mwin,TX("blacklist exceeds 1000 files"));
            fclose(fid);
            goto cleanup;
         }
      }
      fclose(fid);
   }
   else indexlog(0,"no blacklist files \n");

   if (indexlev == 0) {
      indexlog(0,TX("no image index: reports disabled \n"));                           //  no image index, reports disabled
      Xindexlev = 0;
      goto cleanup;
   }

   if (indexlev == 1) {
      indexlog(0,TX("old image index: reports will omit new files \n"));               //  image index has old files only
      index_rebuild_old();
      indexlog(0,TX("image index records found: %d \n"),Nxxrec);                       //  note: superfluous - see above
      Xindexlev = 1;
      goto cleanup;
   }

   if (indexlev == 2) {                                                                //  update image index for all image files
      indexlog(0,TX("full image index: reports will be complete \n"));
      Xindexlev = 2;
   }

   if (FfullindexReq) {                                                                //  user: force full re-index
      Nold = 0;
      Xindexlev = 2;
      goto get_new_files;                                                              //  skip old index processing
   }

   //  read old image index file and build "old list" of xxrec_tab[] records

   indexlog(0,TX("reading image index file ...\n"));

   cc = maximages * sizeof(xxrec_t *);
   xxrec_old = (xxrec_t **) zmalloc(cc,"xxrec-old");                                   //  "old" image xxrec_tab[] recs
   Nold = 0;
   ftf = 1;

   while (true)
   {
      xxrec = read_xxrec_seq(ftf);                                                     //  read curr. xxrec_tab[] recs
      if (! xxrec) break;

      err = stat(xxrec->file,&statB);                                                  //  file status
      if (err) {                                                                       //  file is missing
         for (ii = 0; ii < Nmisstops; ii++)
            if (strmatchN(xxrec->file,misstop2[ii],misscc2[ii])) break;                //  within missing top folders?
         if (ii == Nmisstops) continue;                                                //  no, exclude file
      }
      else {                                                                           //  file is present
         if (! S_ISREG(statB.st_mode)) continue;                                       //  not a regular file, remove
         for (ii = 0; ii < Ntopfolders; ii++)
            if (strmatchN(xxrec->file,topfol2[ii],topcc2[ii])) break;                  //  within top folders?
         if (ii == Ntopfolders) continue;                                              //  no, exclude file
      }

      xxrec_old[Nold] = xxrec;                                                         //  file is included

      if (++Nold == maximages) {
         zmessageACK(Mwin,TX("exceeded max. images: %d"),maximages);
         Xindexlev = 0;                                                                //  26.0
         goto cleanup;
      }
   }
   
   indexlog(0,"prior image index records found: %d \n",Nold);

   //  sort old xxrec_tab[] recs in order of file name and file mod date

   if (Nold > 1)
      HeapSort((ch **) xxrec_old,Nold,index_compare);                                  //  smp sort

   //  replace older recs with newer recs that were appended to the file
   //    and are now sorted in file name sequence

   if (Nold > 1)
   {
      for (orec = 0, orec2 = 1; orec2 < Nold; orec2++)
      {
         if (strmatch(xxrec_old[orec]->file,xxrec_old[orec2]->file))                   //  memory for replaced recs is lost
            xxrec_old[orec] = xxrec_old[orec2];
         else {
            orec++;
            xxrec_old[orec] = xxrec_old[orec2];
         }
      }

      Nold = orec + 1;                                                                 //  new count
   }

get_new_files:

   //  find all image files and create "new list" of xxrec_tab[] recs

   indexlog(0,TX("find all image files ...\n"));

   cc = maximages * sizeof(xxrec_t *);
   xxrec_new = (xxrec_t **) zmalloc(cc,"xxrec-new");                                   //  "new" image xxrec_tab[] recs
   Nnew = 0;

   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < Ntopfolders; ii++)                                                //  loop top folders
   {
      err = find_imagefiles(topfolders[ii],1+16+32,flist,NF);                          //  find all image files in top folders
      if (err) {
         zmessageACK(Mwin,TX("find_imagefiles() failure \n"));
         Xindexlev = 0;                                                                //  26.0
         goto cleanup;
      }

      if (Nnew + NF > maximages) {
         zmessageACK(Mwin,TX("exceeded max. images: %d"),maximages);
         Xindexlev = 0;                                                                //  26.0
         goto cleanup;
      }

      for (jj = 0; jj < NF; jj++)                                                      //  loop found image files
      {
         file = flist[jj];
         nrec = Nnew++;
         xxrec_new[nrec] = (xxrec_t *) zmalloc(sizeof(xxrec_t),"xxrec-new");           //  allocate xxrec
         xxrec_new[nrec]->file = file;                                                 //  filespec
         stat(file,&statB);
         pretty_datetime(statB.st_mtime,xxrec_new[nrec]->fdate);                       //  file mod date
      }                                                                                //  remaining data left null

      if (flist) zfree(flist);
   }

   zadd_locked(NFbusy,-1);
   
   indexlog(0,TX("current image files found: %d \n"),Nnew);

   //  sort new xxrec_tab[] recs in order of file name and file mod date

   if (Nnew > 1)
      HeapSort((ch **) xxrec_new,Nnew,index_compare);                                  //  smp sort

   if (xxrec_tab)
   {
      for (ii = 0; ii < Nxxrec; ii++)                                                  //  free memory for old xxrec_tab
      {
         zfree(xxrec_tab[ii]->file);
         if (xxrec_tab[ii]->keywords) zfree(xxrec_tab[ii]->keywords);
         if (xxrec_tab[ii]->title) zfree(xxrec_tab[ii]->title);
         if (xxrec_tab[ii]->desc) zfree(xxrec_tab[ii]->desc);
         if (xxrec_tab[ii]->xmeta) zfree(xxrec_tab[ii]->xmeta);
         zfree(xxrec_tab[ii]);
      }

      zfree(xxrec_tab);
   }

   //  create image xxrec_tab[] table in memory

   cc = maximages * sizeof(xxrec_t *);                                                 //  make new table with max. capacity
   xxrec_tab = (xxrec_t **) zmalloc(cc,"xxrec-tab");

   cc = maximages * sizeof(int);
   Xstatus = (int8 *) zmalloc(cc,"xxrec-Xstat");                                       //  1/2/3/4 = missing/stale/OK/blacklist
   Tstatus = (int8 *) zmalloc(cc,"xxrec-Tstat");                                       //             file or thumbnail

   //  merge and compare old and new xxrec_tab[] recs

   nrec = orec = xrec = 0;

   while (true)                                                                        //  merge old and new xxrec_tab[] recs
   {
      if (nrec == Nnew && orec == Nold) break;                                         //  both EOF, done

      if (nrec < Nnew && orec < Nold)                                                  //  if neither EOF, compare file names
         comp = strcmp(xxrec_old[orec]->file, xxrec_new[nrec]->file);
      else comp = 0;

      if (nrec == Nnew || comp < 0) {                                                  //  old xxrec_tab[] rec has no match
         xxrec_tab[xrec] = xxrec_old[orec];                                            //  copy old xxrec_tab[] rec to output
         xxrec_old[orec] = 0;
         Xstatus[xrec] = 1;                                                            //  mark file as missing
         orec++;
      }

      else if (orec == Nold || comp > 0) {                                             //  new xxrec_tab[] rec has no match
         xxrec_tab[xrec] = xxrec_new[nrec];                                            //  copy new xxrec_tab[] rec to output
         xxrec_new[nrec] = 0;
         Xstatus[xrec] = 2;                                                            //  mark file as needing update
         nrec++;
      }

      else {                                                                           //  old and new xxrec_tab[] recs match
         xxrec_tab[xrec] = xxrec_old[orec];
         if (strmatch(xxrec_old[orec]->fdate, xxrec_new[nrec]->fdate)) {               //  compare file dates
            Xstatus[xrec] = 3;                                                         //  same, mark file as up to date
         }
         else {
            Xstatus[xrec] = 2;                                                         //  different, mark as needing update
            strcpy(xxrec_tab[xrec]->fdate, xxrec_new[nrec]->fdate);                    //  use current file date
         }
         xxrec_old[orec] = 0;
         orec++;

         zfree(xxrec_new[nrec]->file);
         zfree(xxrec_new[nrec]);
         xxrec_new[nrec] = 0;
         nrec++;
      }

      Tstatus[xrec] = 3;                                                               //  thumbnail OK

      if (Xstatus[xrec] > 1)                                                           //  if file not missing, test and
         if (! thumbfile_OK(xxrec_tab[xrec]->file))                                    //    mark thumbnail update needed
            Tstatus[xrec] = 2;

      file = xxrec_tab[xrec]->file;                                                    //  indexed file

      for (ii = 0; ii < Nblacklist; ii++)                                              //  test if blacklisted
         if (MatchWild(blacklist[ii],file) == 0) break;
      if (ii < Nblacklist) Xstatus[xrec] = Tstatus[xrec] = 4;                          //  mark blacklisted file

      xrec++;                                                                          //  count output recs
      if (xrec == maximages) {
         zmessageACK(Mwin,TX("max. image limit reached: %d"),xrec);
         goto cleanup;
      }
   }

   Nxxrec = xrec;                                                                      //  final count

   if (xxrec_new) zfree(xxrec_new);                                                    //  free memory
   if (xxrec_old) zfree(xxrec_old);
   xxrec_new = xxrec_old = 0;

   for (ii = index_updates = thumb_updates = 0; ii < Nxxrec; ii++) {                   //  count updates required
      if (Xstatus[ii] == 2) index_updates++;                                           //  index rec updates
      if (Tstatus[ii] == 2) thumb_updates++;                                           //  thumbnail file updates
   }

   indexlog(1,TX("index updates needed: %d thumbnails: %d \n"),
                              index_updates,thumb_updates);

   //  Process entries needing update in the new xxrec_tab[] table
   //  (new files or files dated later than in old xxrec_tab[] table).
   //  Get updated metadata from image file metadata.
   //  Add missing or update stale thumbnail file.

   zadd_locked(NFbusy,+1);
   Fescape = 0;

   index_updates = atd_updates = thumb_updates = 0;

   txwidget_append2(wlog,0,"  index  title/desc   thumbs \n");
   txwidget_append2(wlog,0,"---- \n");                                                 //  replaced with progress counters       25.1
   zmainsleep(0.1);

   index_thread_busy = 1;                                                              //  initially busy
   start_detached_thread(index_thread,0);                                              //  start metadata index thread

   while (index_thread_busy)                                                           //  while thread running
   {
      if (Fescape) goto bailout;
      if (wlog) {                                                                      //  update log window progress counters
         index_updates = progress_getvalue();                                          //  progress from meta_getN()             26.0
         txwidget_replace(wlog,0,-1,"%7d     %7d  %7d \n",
                           index_updates,atd_updates,thumb_updates);                   //  25.1
      }
      zmainsleep(0.001);
   }
   
   index_updates = final_index_updates;                                                //  from index thread                     26.0
   
   thumb_thread_busy = 1;                                                              //  initially busy
   start_detached_thread(thumb_thread,0);                                              //  start thumbnails thread

   while (thumb_thread_busy)                                                           //  while thread running
   {
      if (Fescape) goto bailout;
      if (wlog)                                                                        //  update log window progress counters
         txwidget_replace(wlog,0,-1,"%7d     %7d  %7d \n",
                           index_updates,atd_updates,thumb_updates);                   //  25.1
      zmainsleep(0.001);
   }

bailout:

   zadd_locked(NFbusy,-1);

   if (Fescape || Fshutdown) goto cleanup;                                             //  indexed function killed

   indexlog(1,TX("index updates: %d title/desc updates: %d \n"                         //  final statistics
                 "thumbnail updates: %d, deletes: %d \n"),
                   index_updates, atd_updates, thumb_updates, thumb_deletes);

   //  write updated xxrec_tab[] records to image index file

   indexlog(0,TX("writing updated image index file \n"));

   if (Nxxrec) {
      ftf = 1;
      for (ii = 0; ii < Nxxrec; ii++)
         write_xxrec_seq(xxrec_tab[ii],ftf);
      write_xxrec_seq(null,ftf);                                                       //  close output
   }

   indexlog(0,TX("all image files, including unmounted folders: %d \n"),Nxxrec);

   //  keep xxrec_tab[] records in memory only for files actually present

   for (ii = jj = 0; ii < Nxxrec; ii++)
   {
      if (Xstatus[ii] == 1) continue;                                                  //  missing

      if (Xstatus[ii] == 4) {                                                          //  blacklisted files
         indexlog(0,"blacklist: %s \n",xxrec_tab[ii]->file);
         continue;
      }

      xxrec_tab[jj++] = xxrec_tab[ii];
   }

   Nxxrec = jj;                                                                        //  new count

   indexlog(0,TX("after removal of missing and blacklisted: %d \n"),Nxxrec);

   //  get RAW and image file counts and megabytes

   Nraw = Nimage = Nvideo = 0;
   rawgb = imagegb = videogb = 0;

   for (ii = 0; ii < Nxxrec; ii++)
   {
      if (image_file_type(xxrec_tab[ii]->file) == IMAGE) {
         Nimage++;
         imagegb += atoi(xxrec_tab[ii]->fsize);                                        //  25.4
      }
      if (image_file_type(xxrec_tab[ii]->file) == VIDEO) {
         Nvideo++;
         videogb += atoi(xxrec_tab[ii]->fsize);
      }
      else if (image_file_type(xxrec_tab[ii]->file) == RAW) {
         Nraw++;
         rawgb += atoi(xxrec_tab[ii]->fsize);
      }
   }

   indexlog(0,"Image files: %d (%.1f GB) + RAW files: %d (%.1f GB) + Video files: %d \n",
                        Nimage, imagegb/GIGA, Nraw, rawgb/GIGA, Nvideo, videogb/GIGA);

   //  find orphan thumbnails and delete them

   err = find_imagefiles(thumbfolder,2+16,flist,NF);                                   //  thumbnails, recurse folders
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      NF = 0;
   }

   indexlog(0,"thumbnails found: %d \n",NF);

   if (NF > 1.1 * Nxxrec)
   {
      indexlog(0,TX("deleting orphan thumbnails ... \n"));
      thumb_deletes = 0;

      for (ii = 0; ii < NF; ii++)
      {
         thumbfile = flist[ii];
         file = thumb2imagefile(thumbfile);                                            //  thumbnail corresponding file
         if (file) {                                                                   //  exists, keep thumbnail
            zfree(file);
            zfree(thumbfile);
            continue;
         }

         pp = thumbfile + strlen(thumbfolder);                                         //  corresponding file within
         for (jj = 0; jj < Nmisstops; jj++)                                            //    a missing top image folder?
            if (strmatchN(misstop2[jj],pp,misscc2[jj])) break;
         if (jj < Nmisstops) continue;                                                 //  yes, keep thumbnail

         remove(thumbfile);                                                            //  remove thumbnail file
         zfree(thumbfile);
         thumb_deletes++;
      }

      if (flist) zfree(flist);

      indexlog(0,TX("thumbnails deleted: %d \n"),thumb_deletes);
   }

cleanup:                                                                               //  free allocated memory

   FfullindexReq = 0;                                                                  //  cancel user full xxrec_tab[] reqest

   if (Fescape) {
      zsleep(1);                                                                       //  user aborted
      Fescape = 0;
      Xindexlev = 0;                                                                   //  no index
   }

   if (! Nxxrec) Xindexlev = 0;                                                        //  no index

   printf("image files: %d  index level: %d \n",Nxxrec,Xindexlev);                     //  report status                         25.1
   if (Xindexlev == 2) Findexnew = 0;                                                  //  reset new/mod files count             25.1

   if (xxrec_old) zfree(xxrec_old);
   if (xxrec_new) zfree(xxrec_new);
   xxrec_old = xxrec_new = 0;

   if (Xstatus) zfree(Xstatus);
   if (Tstatus) zfree(Tstatus);
   Xstatus = Tstatus = 0;

   nn = index_updates + thumb_updates;
   if (! keepopen && (nn < 6)) {                                                       //  few updates made                      25.1
      indexlog(1,"index and thumb updates made: %d \n",nn);
      zdialog_free(zd_indexlog);                                                       //  kill index log report
      zd_indexlog = 0;
      wlog = 0;
   }

   indexlog(0,TX("index completed, %.1f seconds \n"),get_seconds() - startime);
   indexlog(0,TX("you can close this window now \n")); 
   indexlog(1,"\n");                                                                   //  25.2

   return;
}


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

//  thumbnail thread - create thumbnail files for image files needing update

namespace index_thumb_names
{
   int   threads_started;
   int   threads_completed;
}

void * thumb_thread(void *arg)
{
   using namespace index_names;
   using namespace index_thumb_names;

   void  *thumb_thread2(void *);
   int   xrec;

   thumb_thread_busy = 1;
   thumb_updates = 0;
   threads_started = threads_completed = 0;

   progress_setgoal(Nxxrec);

   for (xrec = 0; xrec < Nxxrec; xrec++)
   {
      if (Tstatus[xrec] != 2) continue;                                                //  thumbnail update not needed
      while (threads_started - threads_completed >= NSMP)
         zsleep(0.001);
      if (Fescape) break;                                                              //  user kill
      threads_started++;
      start_detached_thread(thumb_thread2,&Nval[xrec]);                                //  update thumbnail file
      thumb_updates++;
      progress_addvalue(1);
   }

   while (threads_started - threads_completed > 0)                                     //  wait for last thread
      zsleep(0.001);
   thumb_thread_busy = 0;
   progress_setgoal(0);
   return 0;
}


void * thumb_thread2(void *arg)
{
   using namespace index_names;
   using namespace index_thumb_names;

   int      xrec;
   ch       *file;

   xrec = *((int *) arg);
   file = xxrec_tab[xrec]->file;                                                       //  image file
   zfuncs::zappcrash_context1 = file;
   update_thumbfile(file);                                                             //  do thumbnail update
   zadd_locked(threads_completed,+1);                                                  //  25.1
   return 0;
}


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

//  index thread
//  get metadata for image files needing update and build image xxrec_tab[]
//  thread has no GTK calls

namespace index_thread_names
{
   xxrec_t  **xxrecs = 0;
   ch       **files = 0, *file;
   ch       *tagname[100], **tagval = 0;
   int      NF, NK, NKXX;
   ch       *keywords, *title, *desc;
   int      Nth = 8;                                                                   //  exif_put() threads
}


void * index_thread(void *arg)
{
   using namespace index_names;
   using namespace index_thread_names;

   int      cc, xcc, acc;
   int      ff, kk, fkk;
   ch       xmetarec[xmetaXcc];                                                        //  max. extra metadata cc
   xxrec_t  *xxrec;
   int      xrec;
   STATB    statB;
   ch       *fdate, *fsize;
   ch       *pdate, *psize, *bpc, *rating;
   ch       *location, *country, *gps_data;
   ch       *make, *model, *lens, *exp, *fn, *fl, *iso;

   index_thread_busy = 1;
   
   xxrecs = 0;                                                                         //  26.2
   files = 0;

   if (! Nxxrec) goto exit_thread;

   for (kk = 0; kk < NKX; kk++)                                                        //  get tagnames for data in xxrec_tab[]
      tagname[kk] = tagnamex[kk];

   NKXX = xmetaNtags;                                                                  //  extra indexed metadata count
   for (kk = 0; kk < NKXX; kk++)                                                       //  add tags for extra indexed metadata
      tagname[NKX+kk] = xmeta_tags[kk];
   NK = NKX + NKXX;                                                                    //  standard + extra metadata tags

   for (kk = 0; kk < 3; kk++)                                                          //  add alt. tags for image title         ATD
      tagname[NK+kk] = tagname_alt_title[kk];
   NK += 3;

   for (kk = 0; kk < 6; kk++)                                                          //  add alt. tags for description         ATD
      tagname[NK+kk] = tagname_alt_desc[kk];
   NK += 6;

   cc = Nxxrec * sizeof(ch *);                                                         //  get list of files needing update
   files = (ch**) zmalloc(cc,"index_thread");
   xxrecs = (xxrec_t **) zmalloc(cc,"index_thread");

   for (xrec = ff = 0; xrec < Nxxrec; xrec++)
   {
      if (Xstatus[xrec] != 2) continue;                                                //  index update not needed
      xxrecs[ff] = xxrec_tab[xrec];
      file = xxrecs[ff]->file;                                                         //  check file exists
      if (! regfile(file,&statB)) {
         printf("*** index_thread: file not found: %s \n",file);
         continue;
      }
      files[ff] = file;                                                                //  image file needing update
      ff++;
   }

   NF = ff;                                                                            //  file count
   if (! NF) goto exit_thread;

   cc = NF * NK * sizeof(ch *);                                                        //  get space for returned tag data
   tagval = (ch **) zmalloc(cc,"index_thread");
   
   meta_getN(files,NF,tagname,tagval,NK,1);                                            //  get metadata for all files
   
   final_index_updates = NF;                                                           //  final update count                    26.0

   for (ff = 0; ff < NF; ff++)                                                         //  loop image files
   {
      if (Fescape) break;

      file = files[ff];
      zfuncs::zappcrash_context1 = file;

      xxrec = xxrecs[ff];

      fkk = ff * NK;                                                                   //  tag data for file ff

      fdate = tagval[fkk+0];                                                           //  metadata returned
      fsize = tagval[fkk+1];
      pdate = tagval[fkk+2];
      psize = tagval[fkk+3];
      bpc = tagval[fkk+4];
      rating = tagval[fkk+5];
      keywords = tagval[fkk+6];
      title = tagval[fkk+7];
      desc = tagval[fkk+8];
      location = tagval[fkk+9];
      country = tagval[fkk+10];
      gps_data = tagval[fkk+11];
      make = tagval[fkk+12];
      model = tagval[fkk+13];
      lens = tagval[fkk+14];
      exp = tagval[fkk+15];
      fn = tagval[fkk+16];
      fl = tagval[fkk+17];
      iso = tagval[fkk+18];

      if (fdate) strncpy0(xxrec->fdate,fdate,20);
      if (fsize) strncpy0(xxrec->fsize,fsize,16);
      if (pdate) {
         strncpy0(xxrec->pdate,pdate,20);
         xxrec->pdate[4] = xxrec->pdate[7] = ':';                                      //  force yyyy:mm:dd (metadata mixed -/:)
      }
      if (psize) strncpy0(xxrec->psize,psize,16);
      if (bpc) strncpy0(xxrec->bpc,bpc,4);
      if (rating) strncpy0(xxrec->rating,rating,4);

      if (keywords) {
         xxrec->keywords = zstrdup(keywords,"index_thread");
         cc = strlen(xxrec->keywords);
         while (cc > 0 && xxrec->keywords[cc-1] == ' ') cc--;                          //  remove trailing ", " (prior fotocx)
         while (cc > 0 && xxrec->keywords[cc-1] == ',') cc--;
         xxrec->keywords[cc] = 0;
      }
      else xxrec->keywords = 0;

      if (title) xxrec->title = zstrdup(title,"index_thread");
      else xxrec->title = 0;

      if (desc) xxrec->desc = zstrdup(desc,"index_thread");
      else xxrec->desc = 0;

      if (location) strncpy0(xxrec->location,location,40);
      if (country) strncpy0(xxrec->country,country,40);
      if (gps_data) strncpy0(xxrec->gps_data,gps_data,24);

      if (make) strncpy0(xxrec->make,make,20);
      if (model) strncpy0(xxrec->model,model,20);
      if (lens) strncpy0(xxrec->lens,lens,20);
      if (exp) strncpy0(xxrec->exp,exp,12);
      if (fn) strncpy0(xxrec->fn,fn,12);
      if (fl) strncpy0(xxrec->fl,fl,12);
      if (iso) strncpy0(xxrec->iso,iso,12);

      xcc = 0;

      for (kk = NKX; kk < NKX+NKXX; kk++)                                              //  extra indexed metadata if any
      {
         if (! tagval[fkk+kk]) continue;
         acc = strlen(tagname[kk]) + strlen(tagval[fkk+kk]) + 3;
         if (acc + xcc >= xmetaXcc-2) {                                                //  limit tag and data < xmetaXcc
            printf("*** indexed metadata too big: %s  %s \n",tagname[kk],file);
            continue;
         }
         strcpy(xmetarec+xcc,tagname[kk]);                                             //  construct series
         xcc += strlen(tagname[kk]);                                                   //    "tagname=tagval^ "
         xmetarec[xcc++] = '=';
         strcpy(xmetarec+xcc,tagval[fkk+kk]);
         xcc += strlen(tagval[fkk+kk]);
         strcpy(xmetarec+xcc,"^ ");
         xcc += 2;
      }

      if (xcc > 0) xxrec->xmeta = zstrdup(xmetarec,"index_thread");                    //  add extra metadata to xxrec_tab[]
   }

   void * atd_thread(void *arg);                                                       //  process missing title/description     25.1
   do_wthreads(atd_thread,Nth);                                                        //  Nth threads

exit_thread:

   if (xxrecs) zfree(xxrecs);                                                          //  free memory
   if (files) zfree(files);

   if (NF && NK) {
      for (kk = 0; kk < NF * NK; kk++)
         if (tagval[kk]) zfree(tagval[kk]);
      zfree(tagval);
   }

   index_thread_busy = 0;
   return 0;
}


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

//  thread to stuff missing title/description tags from alternative tags

void * atd_thread(void *arg)                                                           //  25.1
{
   using namespace index_names;
   using namespace index_thread_names;

   int      index = *((int *) arg);
   int      ff, kk, fkk, Fatd;
   ch       *file;
   ch       *title, *desc;
   char     *tagnameatd[2], *tagvalatd[2];
   xxrec_t  *xxrec;

   for (ff = index; ff < NF; ff += Nth)                                                //  loop image files
   {
      if (Fescape) break;

      file = files[ff];
      zfuncs::zappcrash_context1 = file;

      xxrec = xxrecs[ff];

      fkk = ff * NK;                                                                   //  tag data for file ff

      title = tagval[fkk+7];
      desc = tagval[fkk+8];

      Fatd = 0;

      if (! title) {                                                                   //  look for alt. image title tag data
         for (kk = NKX+NKXX; kk < NKX+NKXX+3; kk++)
            if (tagval[fkk+kk]) break;
         if (kk < NKX+NKXX+3) {
            xxrec->title = zstrdup(tagval[fkk+kk],"index_thread");
            Fatd = 1;
         }
      }

      if (! desc) {                                                                    //  look for alt. description tag data
         for (kk = NKX+NKXX+3; kk < NKX+NKXX+9; kk++)
            if (tagval[fkk+kk]) break;
         if (kk < NKX+NKXX+9) {
            xxrec->desc = zstrdup(tagval[fkk+kk],"index_thread");
            Fatd = 1;
         }
      }

      if (Fatd) {                                                                      //  update either or both
         printf("consolidate title and description: %s \n",file);
         tagnameatd[0] = tagnamex[7];
         tagnameatd[1] = tagnamex[8];
         tagvalatd[0] = xxrec->title;
         tagvalatd[1] = xxrec->desc;
         meta_put(file,tagnameatd,tagvalatd,2);
         atd_updates++;
      }
   }

   return 0;
}


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

//  index log window dialog response function

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

   if (! zd->zstat) return 1;                                                          //  continue

   if (NFbusy) {
      Fescape = 1;                                                                     //  [kill] index process
      zmainsleep(1);
   }

   zdialog_free(zd);                                                                   //  kill index log report
   zd_indexlog = 0;
   wlog = 0;

   return 1;
}


//  log index status and statistics to report window and log file

void indexlog(int bold, ch *format, ...)
{
   using namespace index_names;

   va_list  arglist;
   ch       text[200];

   va_start(arglist,format);
   vsnprintf(text,200,format,arglist);
   va_end(arglist);

   printf("%s",text);                                                                  //  output to log file
   if (wlog) txwidget_append2(wlog,bold,text);                                         //  output to report window
   return;
}


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

//  sort compare function - compare xxrec_tab[] records and return
//    <0   0   >0   for   rec1 < rec2   rec1 == rec2   rec1 > rec2

int index_compare(ch *rec1, ch *rec2)
{
   xxrec_t *xxrec1 = (xxrec_t *) rec1;
   xxrec_t *xxrec2 = (xxrec_t *) rec2;

   int nn = strcmp(xxrec1->file,xxrec2->file);
   if (nn) return nn;
   nn = strcmp(xxrec1->fdate,xxrec2->fdate);
   return nn;
}


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

//  Rebuild image xxrec_tab[] table from existing image index file
//    without searching for new and modified files.

void index_rebuild_old()
{
   using namespace index_names;

   int      ftf, cc, rec, rec2, Ntab;
   xxrec_t  *xxrec;

   if (Xindexlev > 0) return;                                                          //  not needed

   panelmessage(1,3,TX("build index, old files only"));                                //  25.1

   //  read image index file and build table of xxrec_tab[] records

   cc = maximages * sizeof(xxrec_t *);
   xxrec_tab = (xxrec_t **) zmalloc(cc,"index-rebuild");                               //  image xxrec_tab[] recs
   Ntab = 0;
   ftf = 1;

   while (true)
   {
      zmainloop();
      xxrec = read_xxrec_seq(ftf);                                                     //  read curr. xxrec_tab[] recs
      if (! xxrec) break;
      if (! regfile(xxrec->file)) continue;
      xxrec_tab[Ntab] = xxrec;
      Ntab++;
      if (Ntab == maximages) {
         zmessageACK(Mwin,TX("exceeded max. images: %d"),maximages);
         Xindexlev = 0;                                                                //  26.0
         return;
      }
   }

   //  sort xxrec_tab[] recs in order of file name and file mod date

   if (Ntab)
      HeapSort((ch **) xxrec_tab,Ntab,index_compare);                                  //  smp sort

   //  replace older recs with newer (appended) recs now sorted together

   if (Ntab)
   {
      for (rec = 0, rec2 = 1; rec2 < Ntab; rec2++)
      {
         if (strmatch(xxrec_tab[rec]->file,xxrec_tab[rec2]->file))
            xxrec_tab[rec] = xxrec_tab[rec2];
         else {
            rec++;
            xxrec_tab[rec] = xxrec_tab[rec2];
         }
      }

      Ntab = rec + 1;                                                                  //  new count
   }

   Nxxrec = Ntab;
   if (Nxxrec) Xindexlev = 1;                                                          //  image index OK
   return;
}


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

//  move the fotocx home folder (user data and settings) 
//  get user input (folder name) and copy all files to the new location
//  default home folder: /home/<user>/.fotocx
//  if home folder has been moved, the new folder is saved in: 
//     /home/<user>/.fotocx-home  (1-line text file) 

void m_move_fotocx_home(GtkWidget *, ch *)                                             //  26.2
{
   int move_home_dialog_event(zdialog *zd, ch *event);
   
   ch       oldhomefolder[200], newhomefolder[200];                                    //  old and new fotocx home folder
   ch       defaulthome[200];                                                          //  default home folder  ~/.fotocx
   ch       movedhomelink[200];                                                        //  link to moved home folder  ~/.fotocx-home
   ch       oldthumbfolder[200], newthumbfolder[200];                                  //  old and new thumbnails folder
   ch       imagefoldersfile[200];                                                     //  image index file inside home folder
   ch       *pp;
   FILE     *fid;
   int      ii, err, zstat; 
   zdialog  *zd, *zd2;
   zlist_t  *zlist;

   F1_help_topic = TX("MOVE FOTOCX HOME");
   printf("move_fotocx_home \n");
   
   if (Fblock("move fotocx home")) return;
   
   snprintf(defaulthome,200,"%s/.fotocx",getenv("HOME"));                              //  default home folder  ~/.fotocx
   snprintf(movedhomelink,200,"%s/.fotocx-home",getenv("HOME"));                       //  link to moved home folder  ~/.fotocx-home

/***
          _____________________________________________________
         |             Move Fotocx Home Folder                 |
         |                                                     |
         | [Select] Home Folder [____________________________] |
         |                                                     |
         |                                       [Proceed] [X] |
         |_____________________________________________________|

***/

   zd = zdialog_new("Move Fotocx Home Folder",Mwin,"Proceed","X",null);

   zdialog_add_widget(zd,"hbox","hbsel","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","Select","hbsel","Select","space=3");
   zdialog_add_widget(zd,"label","labsel","hbsel","Home Folder","space=3");
   zdialog_add_widget(zd,"text","newhomefolder","hbsel",0,"expand");

   strncpy0(oldhomefolder,get_zhomedir(),200);
   strncpy0(newhomefolder,oldhomefolder,200);
   zdialog_stuff(zd,"newhomefolder",newhomefolder);

   zdialog_resize(zd,500,0);
   zdialog_run(zd,move_home_dialog_event,"parent");                                    //  run dialog

retry:
   zd->zstat = 0;
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   if (zstat != 1) goto cancel;                                                        //  [X] cancel
   
   zdialog_fetch(zd,"newhomefolder",newhomefolder,200);                                //  get new home folder
   strTrim2(newhomefolder);
   
   if (strmatch(newhomefolder,oldhomefolder)) {                                        //  no change, do nothing
      zmessageACK(Mwin,"home folder was not changed");
      goto cancel;
   }

   if (strchr(newhomefolder,' ')) {
      zmessageACK(Mwin,"new home folder contains a blank");
      goto retry;
   }

   if (! dirfile(newhomefolder)) {                                                     //  create folder if needed
      err = zshell("log ack","mkdir -p -m 0750 %s",newhomefolder);
      if (err) goto retry;
   }
   
   zd2 = zmessage_post(zd->dialog,"parent",0,"copying files to %s",newhomefolder);     //  may need time if thumbnails inside
   
   err = zshell("log ack","cp -r -p %s/* %s",oldhomefolder,newhomefolder);             //  copy files to new home folder
   zdialog_free(zd2);
   if (err) {                                                                          //  failed
      zmessageACK(Mwin,"copy files error: %s",strerror(errno));
      goto cancel;
   }
   
   if (strmatch(newhomefolder,defaulthome))                                            //  if using default home folder,
      remove(movedhomelink);                                                           //    remove link to moved home folder
   else 
   {                                                                                   //  if moved home folder, create
      fid = fopen(movedhomelink,"w");                                                  //    link to moved home folder
      if (! fid) {
         zmessageACK(Mwin,"cannot create: %s \n %s",movedhomelink,strerror(errno));
         goto cancel;
      }
      fprintf(fid,"%s",newhomefolder);                                                 //  /home/<user>/.fotocx-home
      fclose(fid);                                                                     //    contains new home folder
   }

   snprintf(imagefoldersfile,200,"%s/image_index/image_folders",newhomefolder);        //  file has list of top image folders
   zlist = zlist_from_file(imagefoldersfile);                                          //    and top thumbnails folder
   if (zlist) {
      for (ii = 0; ii < zlist_count(zlist); ii++) {                                    //  read list of folders
         strncpy0(oldthumbfolder,zlist_get(zlist,ii),200);
         if (strmatchN(oldthumbfolder,"thumbnails: ",12)) break;                       //  find thumbnails folder
      }
      if (ii < zlist_count(zlist)) {
         pp = strstr(oldthumbfolder,oldhomefolder);                                    //  thumbnails are in old home folder?
         if (pp) {                                                                     //  yes, copy thumbnails is needed
            repl_1str(oldthumbfolder,newthumbfolder,200,oldhomefolder,newhomefolder);
            zlist_put(zlist,newthumbfolder,ii);                                        //  replace old home folder with new
            zlist_to_file(zlist,imagefoldersfile);                                     //  replace image_folders file
            zstrcpy(oldthumbfolder,oldthumbfolder+12);                                 //  remove "thumbnails: " 
            zstrcpy(newthumbfolder,newthumbfolder+12);
            printf("thumbnails folder changed: \n %s --> %s \n",
                                 oldthumbfolder,newthumbfolder);
         }
      }
      zlist_free(zlist);
   }

   zmessageACK(Mwin,"files in %s can be deleted",oldhomefolder);
   zmessageACK(Mwin,"fotocx will exit");
   zdialog_free(zd);
   Fblock(0);
   quitxx();
   
cancel:
   zdialog_free(zd);
   Fblock(0);
   return;
}


//  dialog event and completion function

int move_home_dialog_event(zdialog *zd, ch *event)
{
   ch    *pp;
   ch    newhomefolder[200];
   ch    *title = "Choose new Fotocx home folder";
   
   if (strmatch(event,"Select")) {                                                     //  [Select] new folder
      zdialog_fetch(zd,"newhomefolder",newhomefolder,200);
      pp = zgetfile(title,MWIN,"folder",newhomefolder);
      if (! pp) return 1;
      strncpy0(newhomefolder,pp,200);                                                  //  get entered folder
      if (! strstr(newhomefolder,"fotocx")) {                                          //  if not containing "fotocx"
         strncatv(newhomefolder,200,"/fotocx",0);                                      //    append /fotocx
      }
      zdialog_stuff(zd,"newhomefolder",newhomefolder);                                 //  stuff new folder
      zfree(pp);
   }
   
   return 1;
}


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

//  user preferences and settings dialog

namespace settings
{
   void  get_raw_commands(zdialog *zd);
   
   ch       *startopt[6] = { "start folder", "start file", "start album",              //  startup options
                      "previous file", "recent files", "newest files" };

   ch       *tiffopt[4][2] = {                                                         //  TIFF file compression options
               { "NONE", "1" },
               { "LZW", "5" },
               { "PACKBITS", "32773" },
               { "DEFLATE", "8" } };
   int      NTO = 4;

   int      Frestart;
}


//  menu function

void m_settings(GtkWidget *, ch *)
{
   using namespace settings;

   int   settings_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      ii;
   ch       txrgb[20], pct_scale[40];
   ch       lcfile[200];
   zlist_t  *zlist;

   snprintf(pct_scale,40,TX("min. separation, %c of scale"),'%');                      //  "separation, % of scale"

   F1_help_topic = TX("SETTINGS");

   printf("m_settings \n");

   if (Fblock("settings")) return;

/***
       _____________________________________________________________________
      |                Preferences and Settings                             |
      |                                                                     |
      |   Language      |  [ en |v] 2-character code                        |                                                 // 26.0
      |  Startup View   |  [ Previous File |v] [browse]                     |
      |  Background     |  F-view [###]  G-view [###]                       |
      |  Menu Style     |  (o) Icons  (o) Text  (o) Both  Icon size [40]    |
      |  Menu Colors    |  Text [###]  Background [###]                     |
      |  Dialog Font    |  [ Ubuntu Bold 11   ] [choose]                    |
      |  Show Hidden    |  [x] show hidden files and folders                |                                                //  26.4
      |  Zoom Speed     |  [ 2 ] clicks per 2x image increase               |
      |   Pan Mode      |  (o) drag  (o) scroll  [x] fast                   |
      |  JPEG files     |  [ 90 ] quality level (70+)                       |
      |  TIFF files     |  [ LZW |v] compression method                     |
      |  Curve Node     |  [ 5 ] min. separation, % of scale                |
      |  Map Markers    |  [ 9 ] pixel size                                 |
      |  Overlay Text   |  [ 80 ] [ 100 ] text on image line wrap range     |
      | Image Position  |  (o) left  (o) center  (o) right                  |
      |  Confirm Exit   |  [x] confirm Fotocx exit                          |
      |  Index Level    |  [ 2 ] $ fotocx  [ 1 ] $ fotocx <filename>        |
      |  RAW loader     |  command [___________________________________|v]  |
      |  RAW Options    |  [_] edit commands   [x] use embedded image color |
      |   RAW files     |  [ .cr2 .dng .raf .nef .orf .rw2 .raw ]           |
      |  Video Files    |  [ .mp4 .mov .wmv .mpeg .mpg .h264 .webm  ]       |
      |   Video App     |  [ vlc -q              |v]                        |
      |                                                                [OK] |
      |_____________________________________________________________________|

***/

   zd = zdialog_new("Preferences and Settings",Mwin,"OK",null);

   //  left and right vertical boxes
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=2|homog");
   zdialog_add_widget(zd,"vsep","sep1","hb1",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=2|homog");

   //  language code
   zdialog_add_widget(zd,"label","language","vb1",TX("Language"));                                                            // 26.0
   zdialog_add_widget(zd,"hbox","hblc","vb2");
   zdialog_add_widget(zd,"combo","lc","hblc",0,"size=10");
   zdialog_add_widget(zd,"label","lablc","hblc",TX("2-character code"),"space=8");

   //  startup view
   zdialog_add_widget(zd,"label","startup view","vb1",TX("Startup View"));
   zdialog_add_widget(zd,"hbox","hbsd","vb2");
   zdialog_add_widget(zd,"combo","startopt","hbsd",0,"space=3|size=30");
   zdialog_add_widget(zd,"button","startopt-browse","hbsd",TX("Browse"),"space=5");

   //  background colors
   zdialog_add_widget(zd,"label","background colors","vb1",TX("Background"));
   zdialog_add_widget(zd,"hbox","hbbg","vb2");
   zdialog_add_widget(zd,"label","labfbg","hbbg","F-View","space=5");
   zdialog_add_widget(zd,"colorbutt","FBrgb","hbbg");
   zdialog_add_widget(zd,"label","space","hbbg",0,"space=8");
   zdialog_add_widget(zd,"label","labgbg","hbbg","G-View","space=5");
   zdialog_add_widget(zd,"colorbutt","GBrgb","hbbg");

   //  menu style
   zdialog_add_widget(zd,"label","menu style","vb1",TX("Menu Style"));
   zdialog_add_widget(zd,"hbox","hbms","vb2");
   zdialog_add_widget(zd,"radio","icons","hbms",TX("Icons"),"space=3");
   zdialog_add_widget(zd,"radio","text","hbms",TX("Text"),"space=3");
   zdialog_add_widget(zd,"radio","both","hbms",TX("Both"),"space=3");
   zdialog_add_widget(zd,"label","space","hbms",0,"space=8");
   zdialog_add_widget(zd,"label","labis","hbms",TX("Icon size"));
   zdialog_add_widget(zd,"zspin","iconsize","hbms","26|64|1|32","space=2");

   //  menu colors
   zdialog_add_widget(zd,"label","menu colors","vb1",TX("Menu Colors"));
   zdialog_add_widget(zd,"hbox","hbmc","vb2");
   zdialog_add_widget(zd,"label","labmb","hbmc",TX("Text"),"space=5");
   zdialog_add_widget(zd,"colorbutt","MFrgb","hbmc");
   zdialog_add_widget(zd,"label","space","hbmc",0,"space=5");
   zdialog_add_widget(zd,"label","labmb","hbmc",TX("Background"),"space=8");
   zdialog_add_widget(zd,"colorbutt","MBrgb","hbmc");

   //  dialog font
   zdialog_add_widget(zd,"label","dialog font","vb1",TX("Dialog Font"));
   zdialog_add_widget(zd,"hbox","hbdf","vb2");
   zdialog_add_widget(zd,"zentry","font","hbdf","Sans 10","size=20");
   zdialog_add_widget(zd,"button","choosefont","hbdf",TX("Choose"),"space=5");
   
   //  show hidden
   zdialog_add_widget(zd,"label","show hidden","vb1",TX("Show Hidden"));
   zdialog_add_widget(zd,"hbox","hbhide","vb2");
   zdialog_add_widget(zd,"check","showhidden","hbhide");
   zdialog_add_widget(zd,"label","labhide","hbhide",TX("show hidden files and folders"),"space=10");

   //  zoom count
   zdialog_add_widget(zd,"label","zoom count","vb1",TX("Zoom Speed"));
   zdialog_add_widget(zd,"hbox","hbz","vb2");
   zdialog_add_widget(zd,"zspin","zoomcount","hbz","1|40|1|2","size=3");
   zdialog_add_widget(zd,"label","labz","hbz",TX("clicks per 2x image increase"),"space=5");

   //  image pan mode
   zdialog_add_widget(zd,"label","pan mode","vb1",TX("Pan Mode"));
   zdialog_add_widget(zd,"hbox","hbpm","vb2");
   zdialog_add_widget(zd,"radio","drag","hbpm",TX("drag"));
   zdialog_add_widget(zd,"radio","scroll","hbpm",TX("scroll"),"space=8");
   zdialog_add_widget(zd,"check","fast","hbpm",TX("fast"),"space=10");

   //  JPEG save quality
   zdialog_add_widget(zd,"label","jpeg qual","vb1",TX("JPEG files"));
   zdialog_add_widget(zd,"hbox","hbjpeg","vb2");
   zdialog_add_widget(zd,"zspin","jpegqual","hbjpeg","1|100|1|90");
   zdialog_add_widget(zd,"label","labqual","hbjpeg",TX("quality level (70+)"),"space=10");

   //  TIFF compression method
   zdialog_add_widget(zd,"label","tiff comp","vb1",TX("TIFF files"));
   zdialog_add_widget(zd,"hbox","hbtiff","vb2");
   zdialog_add_widget(zd,"combo","tiffcomp","hbtiff",0,"size=10");
   zdialog_add_widget(zd,"label","labmeth","hbtiff",TX("compression method"),"space=10");

   //  curve edit node separation
   zdialog_add_widget(zd,"label","curve node","vb1",TX("Curve Node"));
   zdialog_add_widget(zd,"hbox","hbncap","vb2");
   zdialog_add_widget(zd,"zspin","nodecap","hbncap","3|20|1|5","size=2");
   zdialog_add_widget(zd,"label","labncap","hbncap",pct_scale,"space=10");

   //  map marker size
   zdialog_add_widget(zd,"label","map marker","vb1",TX("Map Markers"));
   zdialog_add_widget(zd,"hbox","hbmmk","vb2");
   zdialog_add_widget(zd,"zspin","map_dotsize","hbmmk","5|20|1|8","size=2");
   zdialog_add_widget(zd,"label","labmmk","hbmmk",TX("pixel size"),"space=10");

   //  overlay text line length range
   zdialog_add_widget(zd,"label","overlay text","vb1",TX("Overlay Text"));
   zdialog_add_widget(zd,"hbox","hbovtx","vb2");
   zdialog_add_widget(zd,"zspin","captext_cc0","hbovtx","60|200|1|80","size=3");
   zdialog_add_widget(zd,"zspin","captext_cc1","hbovtx","80|300|1|100","size=3|space=10");
   zdialog_add_widget(zd,"label","labovtx","hbovtx",TX("text on image, line wrap range"),"space=10");

   //  image position
   zdialog_add_widget(zd,"label","image posn","vb1",TX("Image Position"));
   zdialog_add_widget(zd,"hbox","hbshift","vb2");
   zdialog_add_widget(zd,"radio","ipleft","hbshift",TX("left"));
   zdialog_add_widget(zd,"radio","ipcenter","hbshift",TX("center"),"space=10");
   zdialog_add_widget(zd,"radio","ipright","hbshift",TX("right"),"space=5");

   //  confirm exit
   zdialog_add_widget(zd,"label","confirm exit","vb1",TX("Confirm Exit"));
   zdialog_add_widget(zd,"hbox","hbquit","vb2");
   zdialog_add_widget(zd,"check","askquit","hbquit");
   zdialog_add_widget(zd,"label","labquit","hbquit",TX("confirm Fotocx exit"),"space=10");

   //  index level
   zdialog_add_widget(zd,"label","index levels","vb1",TX("Index Level"));
   zdialog_add_widget(zd,"hbox","hbxlev","vb2");
   zdialog_add_widget(zd,"zspin","findexlev","hbxlev","0|2|1|2","size=3");
   zdialog_add_widget(zd,"label","labxlev2","hbxlev","$ fotocx (2)","space=5");
   zdialog_add_widget(zd,"label","space","hbxlev",0,"space=10");
   zdialog_add_widget(zd,"zspin","fmindexlev","hbxlev","0|2|1|0","size=3");
   zdialog_add_widget(zd,"label","labfmxlev2","hbxlev","$ fotocx <filename> (1)","space=5");

   //  RAW loader
   zdialog_add_widget(zd,"label","raw loader","vb1","RAW loader");
   zdialog_add_widget(zd,"hbox","hbrc","vb2");
   zdialog_add_widget(zd,"label","labrc","hbrc",TX("command:"),"space=5");
   zdialog_add_widget(zd,"combo","rawcommand","hbrc",0,"space=3");

   //  RAW conversion options
   zdialog_add_widget(zd,"label","raw options","vb1","RAW Options");
   zdialog_add_widget(zd,"hbox","hbrc","vb2");
   zdialog_add_widget(zd,"zbutton","editrawcomms","hbrc",TX("edit commands"));
   zdialog_add_widget(zd,"label","space","hbrc",0,"space=10");
   zdialog_add_widget(zd,"check","matchembed","hbrc",0,"space=3");
   zdialog_add_widget(zd,"label","labprof","hbrc",TX("match embedded image color"));

   //  RAW file types
   zdialog_add_widget(zd,"label","raw files","vb1","RAW Files");
   zdialog_add_widget(zd,"hbox","hbrft","vb2");
   zdialog_add_widget(zd,"zentry","rawtypes","hbrft",".raw .dng");

   //  video file types
   zdialog_add_widget(zd,"label","video files","vb1","Video Files");
   zdialog_add_widget(zd,"hbox","hbvft","vb2");
   zdialog_add_widget(zd,"zentry","videotypes","hbvft",".mp4 .mov");

   //  video play app
   zdialog_add_widget(zd,"label","video command","vb1","Video App");
   zdialog_add_widget(zd,"hbox","hbvc","vb2");
   zdialog_add_widget(zd,"zentry","videocomm","hbvc",video_command,"size=40");

   zdialog_add_ttip(zd,"lc",TX("enter 2-character language code: en, de, fr, etc."));                                         // 26.0
   zdialog_add_ttip(zd,"startup view",TX("start with previous image file, gallery of recent files, etc."));
   zdialog_add_ttip(zd,"background colors",TX("background colors for file and gallery view windows"));
   zdialog_add_ttip(zd,"menu style",TX("icons only, text only, or both"));
   zdialog_add_ttip(zd,"menu colors",TX("menu text and background colors"));
   zdialog_add_ttip(zd,"dialog font",TX("font name and size for menus and dilogs"));
   zdialog_add_ttip(zd,"zoom count",TX("choose 1-8 clicks or [+] keys for 2x image zoom"));
   zdialog_add_ttip(zd,"pan mode",TX("drag with mouse, scroll against mouse, 1x or 2x speed"));
   zdialog_add_ttip(zd,"jpeg qual",TX("jpeg quality level - 70+ for good image quality"));
   zdialog_add_ttip(zd,"tiff comp",TX("TIFF file compression method"));
   zdialog_add_ttip(zd,"curve node",TX("min. edit curve node separation, % of scale length"));
   zdialog_add_ttip(zd,"map marker",TX("map marker (red dot) diameter, pixels"));
   zdialog_add_ttip(zd,"overlay text",TX("title/description/... text line length range for line wrap"));
   zdialog_add_ttip(zd,"image posn",TX("if image < window, center or shift left or right"));
   zdialog_add_ttip(zd,"confirm exit",TX("use option to stop accidental quit"));
   zdialog_add_ttip(zd,"index levels",TX("0/1/2 = no image index, old index only, old + update new/modified"));
   zdialog_add_ttip(zd,"raw loader",TX("shell command used for converting RAW files to RGB"));
   zdialog_add_ttip(zd,"raw options",TX("edit custom shell commands, match colors to embedded jpeg image"));
   zdialog_add_ttip(zd,"raw files",TX("file .ext names to recognize RAW files"));
   zdialog_add_ttip(zd,"video files",TX("file .ext names to recognize video files"));
   zdialog_add_ttip(zd,"video command",TX("custom command to play video files"));

//  stuff dialog fields with current settings

   snprintf(lcfile,200,"%s/$ language codes",translations_folder);                     //  26.0
   zlist = zlist_from_file(lcfile);
   for (ii = 0; ii < zlist_count(zlist); ii++)
      zdialog_stuff(zd,"lc",zlist_get(zlist,ii));
   zdialog_stuff(zd,"lc",lc);                                                          //  set current language code
   zlist_free(zlist);

   zdialog_stuff(zd,"startopt",TX("start folder"));                                    //  make list of start display options
   zdialog_stuff(zd,"startopt",TX("start file"));
   zdialog_stuff(zd,"startopt",TX("start album"));
   zdialog_stuff(zd,"startopt",TX("previous file"));
   zdialog_stuff(zd,"startopt",TX("recent files"));
   zdialog_stuff(zd,"startopt",TX("newest files"));
   zdialog_stuff(zd,"startopt",TX(startdisplay));                                      //  restore current option

   snprintf(txrgb,20,"%d|%d|%d",FBrgb[0],FBrgb[1],FBrgb[2]);                           //  F-view background color
   zdialog_stuff(zd,"FBrgb",txrgb);
   snprintf(txrgb,20,"%d|%d|%d",GBrgb[0],GBrgb[1],GBrgb[2]);                           //  G-view background color
   zdialog_stuff(zd,"GBrgb",txrgb);

   zdialog_stuff(zd,"icons",0);                                                        //  menu style
   zdialog_stuff(zd,"text",0);
   zdialog_stuff(zd,"both",0);
   if (strmatch(menu_style,"icons"))
      zdialog_stuff(zd,"icons",1);
   else if (strmatch(menu_style,"text"))
      zdialog_stuff(zd,"text",1);
   else zdialog_stuff(zd,"both",1);

   zdialog_stuff(zd,"iconsize",iconsize);                                              //  icon size

   snprintf(txrgb,20,"%d|%d|%d",MFrgb[0],MFrgb[1],MFrgb[2]);                           //  menus font color
   zdialog_stuff(zd,"MFrgb",txrgb);
   snprintf(txrgb,20,"%d|%d|%d",MBrgb[0],MBrgb[1],MBrgb[2]);                           //  menus background color
   zdialog_stuff(zd,"MBrgb",txrgb);

   zdialog_stuff(zd,"font",application_font);                                          //  curr. menu + dialog font              26.3
   zdialog_stuff(zd,"showhidden",Fshowhidden);                                         //  show hidden files option              26.4
   zdialog_stuff(zd,"zoomcount",zoomcount);                                            //  zooms for 2x increase

   zdialog_stuff(zd,"drag",0);                                                         //  image drag/scroll options
   zdialog_stuff(zd,"scroll",0);
   zdialog_stuff(zd,"fast",0);

   if (Fdragopt == 1) zdialog_stuff(zd,"drag",1);                                      //  drag image (mouse direction)
   if (Fdragopt == 2) zdialog_stuff(zd,"scroll",1);                                    //  scroll image (opposite direction)
   if (Fdragopt == 3) zdialog_stuff(zd,"drag",1);                                      //  fast drag
   if (Fdragopt == 4) zdialog_stuff(zd,"scroll",1);                                    //  fast scroll
   if (Fdragopt >= 3) zdialog_stuff(zd,"fast",1);                                      //  fast option

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

   for (ii = 0; ii < NTO; ii++)                                                        //  TIFF file compression options
      zdialog_stuff(zd,"tiffcomp",tiffopt[ii][0]);

   for (ii = 0; ii < NTO; ii++)                                                        //  set current option in widget
      if (tiff_comp_method == atoi(tiffopt[ii][1])) break;
   if (ii < NTO) zdialog_stuff(zd,"tiffcomp",tiffopt[ii][0]);

   zdialog_stuff(zd,"nodecap",zfuncs::splcurve_minx);                                  //  edit curve min. node distance

   zdialog_stuff(zd,"map_dotsize",map_dotsize);                                        //  map dot size

   zdialog_stuff(zd,"captext_cc0",captext_cc[0]);                                      //  overlay text line cc range
   zdialog_stuff(zd,"captext_cc1",captext_cc[1]);

   zdialog_stuff(zd,"ipleft",0);                                                       //  F-view image position
   zdialog_stuff(zd,"ipcenter",0);
   zdialog_stuff(zd,"ipright",0);
   if (strmatch(ImagePosn,"left")) zdialog_stuff(zd,"ipleft",1);
   if (strmatch(ImagePosn,"center")) zdialog_stuff(zd,"ipcenter",1);
   if (strmatch(ImagePosn,"right")) zdialog_stuff(zd,"ipright",1);

   if (Faskquit) zdialog_stuff(zd,"askquit",1);                                        //  ask to quit option
   else zdialog_stuff(zd,"askquit",0);

   zdialog_stuff(zd,"findexlev",Findexlev);                                            //  index level, always
   zdialog_stuff(zd,"fmindexlev",FMindexlev);                                          //  index level, file manager call

   get_raw_commands(zd);                                                               //  get raw commands from corresp. file

   zdialog_stuff(zd,"matchembed",Fraw_match_embed);                                    //  match embedded image color

   zdialog_stuff(zd,"rawtypes",RAWfiletypes);                                          //  RAW file types
   zdialog_stuff(zd,"videotypes",VIDEOfiletypes);                                      //  VIDEO file types
   zdialog_stuff(zd,"videocomm",video_command);                                        //  video play command

   Frestart = 0;                                                                       //  some changes require restart

//  run dialog and wait for completion

   zdialog_resize(zd,500,500);
   zmainloop();                                                                        //  GTK bug - help widgets resize
   zdialog_run(zd,settings_dialog_event,"save");                                       //  run dialog and wait for completion
   zdialog_wait(zd);
   zdialog_free(zd);
   Fblock(0);

   save_params();                                                                      //  save parameter changes
   gtk_window_present(MWIN);                                                           //  refresh window

   if (Frestart) {                                                                     //  start new session if needed
      new_session("-x1");                                                              //  no re-index needed
      zsleep(1);                                                                       //  delay before SIGTERM in quitxx()
      quitxx();
   }

   return;
}


//  settings dialog event function

int settings_dialog_event(zdialog *zd, ch *event)
{
   using namespace settings;

   int            ii, jj, nn;
   ch             *pp, temp[200];
   ch             *ppc;
   ch             txrgb[20], langcode[20]; 
   GtkWidget      *font_dialog;

   if (zd->zstat) return 1;                                                            //  [OK] or [x]
   
   if (strmatch(event,"lc"))                                                           //  language code                         26.0
   {
      zdialog_fetch(zd,"lc",langcode,20);
      strncpy0(lc,langcode,3);                                                         //  use only 2-char. code
      Frestart = 1;
   }

   if (strmatch(event,"startopt"))                                                     //  set startup display
   {
      zdialog_fetch(zd,"startopt",temp,200);
      if (strmatch(temp,TX("start folder")))
         zstrcpy(startdisplay,"start folder");
      if (strmatch(temp,TX("start file")))
         zstrcpy(startdisplay,"start file");
      if (strmatch(temp,TX("start album")))
         zstrcpy(startdisplay,"start album");
      if (strmatch(temp,TX("previous file")))
         zstrcpy(startdisplay,"previous file");
      if (strmatch(temp,TX("recent files")))
         zstrcpy(startdisplay,"recent files");
      if (strmatch(temp,TX("newest files")))
         zstrcpy(startdisplay,"newest files");
   }

   if (strmatch(event,"startopt-browse"))                                              //  browse for startup folder or file
   {
      if (strmatch(startdisplay,"start folder")) {                                     //  set startup gallery
         if (! startfolder && topfolders[0])
            startfolder = zstrdup(topfolders[0],"settings");                           //  default
         pp = zgetfile(TX("Select startup folder"),MWIN,"folder",startfolder);
         if (! pp) return 1;
         if (image_file_type(pp) != FDIR) {
            zmessageACK(Mwin,TX("startup folder is invalid"));
            zfree(pp);
            return 1;
         }
         if (startfolder) zfree(startfolder);
         startfolder = pp;
      }

      if (strmatch(startdisplay,"start file")) {                                       //  set startup image file
         pp = zgetfile(TX("Select startup image file"),MWIN,"file",startfile);
         if (! pp) return 1;
         if (image_file_type(pp) != IMAGE) {
            zmessageACK(Mwin,TX("startup file is invalid"));
            zfree(pp);
            return 1;
         }
         if (startfile) zfree(startfile);
         startfile = pp;
      }

      if (strmatch(startdisplay,"start album")) {                                      //  specific album
         pp = zgetfile(TX("Select startup album"),MWIN,"file",albums_folder);
         if (! pp) return 1;
         if (! strstr(pp,albums_folder)) {
            zmessageACK(Mwin,TX("startup album is invalid"));
            zfree(pp);
            return 1;
         }
         if (startalbum) zfree(startalbum);
         startalbum = pp;
      }
   }

   if (strmatch(event,"FBrgb"))
   {
      zdialog_fetch(zd,"FBrgb",txrgb,20);                                              //  F-view background color
      ppc = substring(txrgb,"|",1);
      if (ppc) FBrgb[0] = atoi(ppc);
      ppc = substring(txrgb,"|",2);
      if (ppc) FBrgb[1] = atoi(ppc);
      ppc = substring(txrgb,"|",3);
      if (ppc) FBrgb[2] = atoi(ppc);
   }

   if (strmatch(event,"GBrgb"))
   {
      zdialog_fetch(zd,"GBrgb",txrgb,20);                                              //  G-view background color
      ppc = substring(txrgb,"|",1);
      if (ppc) GBrgb[0] = atoi(ppc);
      ppc = substring(txrgb,"|",2);
      if (ppc) GBrgb[1] = atoi(ppc);
      ppc = substring(txrgb,"|",3);
      if (ppc) GBrgb[2] = atoi(ppc);
   }

   if (strstr("icons text both iconsize",event))                                       //  menu options
   {
      zdialog_fetch(zd,"icons",nn);                                                    //  menu style = icons
      if (nn) zstrcopy(menu_style,"icons","settings");

      zdialog_fetch(zd,"text",nn);                                                     //  menu style = text
      if (nn) zstrcopy(menu_style,"text","settings");

      zdialog_fetch(zd,"both",nn);                                                     //  menu style = icons + text
      if (nn) zstrcopy(menu_style,"both","settings");

      zdialog_fetch(zd,"iconsize",nn);                                                 //  icon size
      if (nn != iconsize) {
         iconsize = nn;
      }

      Frestart = 1;
   }

   if (strstr("MFrgb MBrgb",event))
   {
      zdialog_fetch(zd,"MFrgb",txrgb,20);                                              //  menu text color
      ppc = substring(txrgb,"|",1);
      if (ppc) MFrgb[0] = atoi(ppc);
      ppc = substring(txrgb,"|",2);
      if (ppc) MFrgb[1] = atoi(ppc);
      ppc = substring(txrgb,"|",3);
      if (ppc) MFrgb[2] = atoi(ppc);

      zdialog_fetch(zd,"MBrgb",txrgb,20);                                              //  menu background color
      ppc = substring(txrgb,"|",1);
      if (ppc) MBrgb[0] = atoi(ppc);
      ppc = substring(txrgb,"|",2);
      if (ppc) MBrgb[1] = atoi(ppc);
      ppc = substring(txrgb,"|",3);
      if (ppc) MBrgb[2] = atoi(ppc);

      Frestart = 1;
   }

   if (strmatch(event,"choosefont"))                                                   //  choose menu/dialog font
   {
      zdialog_fetch(zd,"font",temp,200);
      font_dialog = gtk_font_chooser_dialog_new(TX("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);
      application_font = zstrdup(pp,"settings");                                       //  26.3
      zsetfont(pp);
      g_free(pp);
      
      Frestart = 1;                                                                    //  26.3
   }

   if (strmatch(event,"showhidden"))                                                   //  show hidden option                    26.4
      zdialog_fetch(zd,"showhidden",Fshowhidden);

   if (strmatch(event,"zoomcount"))
   {
      zdialog_fetch(zd,"zoomcount",zoomcount);                                         //  zooms for 2x image size
      zoomratio = pow( 2.0, 1.0 / zoomcount);                                          //  2.0, 1.4142, 1.2599, 1.1892 ...
   }

   if (strstr("drag scroll fast",event))                                               //  drag/scroll option
   {
      zdialog_fetch(zd,"drag",nn);
      if (nn) Fdragopt = 1;                                                            //  1/2 = drag/scroll
      else Fdragopt = 2;
      zdialog_fetch(zd,"fast",nn);                                                     //  3/4 = drag/scroll fast
      if (nn) Fdragopt += 2;
   }

   if (strmatch(event,"jpegqual"))
      zdialog_fetch(zd,"jpegqual",jpeg_def_quality);                                   //  JPEG file save quality

   if (strmatch(event,"tiffcomp"))                                                     //  TIFF file compression method
   {
      zdialog_fetch(zd,"tiffcomp",temp,20);
      for (ii = 0; ii < NTO; ii++)
         if (strmatch(temp,tiffopt[ii][0])) break;
      if (ii < NTO) tiff_comp_method = atoi(tiffopt[ii][1]);
   }

   if (strmatch(event,"nodecap"))                                                      //  edit curve min. node distance
      zdialog_fetch(zd,"nodecap",zfuncs::splcurve_minx);

   if (strmatch(event,"map_dotsize"))                                                  //  map dot size
      zdialog_fetch(zd,"map_dotsize",map_dotsize);

   if (strmatch(event,"captext_cc0"))                                                  //  overlay text line cc range
      zdialog_fetch(zd,"captext_cc0",captext_cc[0]);

   if (strmatch(event,"captext_cc1"))
      zdialog_fetch(zd,"captext_cc1",captext_cc[1]);

   if (strstr("ipleft ipcenter ipright",event))                                        //  image position in wider window
   {
      zdialog_fetch(zd,"ipleft",nn);
      if (nn) zstrcopy(ImagePosn,"left","settings");
      zdialog_fetch(zd,"ipcenter",nn);
      if (nn) zstrcopy(ImagePosn,"center","settings");
      zdialog_fetch(zd,"ipright",nn);
      if (nn) zstrcopy(ImagePosn,"right","settings");
   }

   if (strmatch(event,"askquit"))                                                      //  ask to quit option
      zdialog_fetch(zd,"askquit",Faskquit);

   if (strstr("findexlev fmindexlev",event))                                           //  index level,
   {
      zdialog_fetch(zd,"findexlev",Findexlev);                                         //  fotocx started directly
      zdialog_fetch(zd,"fmindexlev",FMindexlev);                                       //  fotocx started via file manager
      if (Findexlev < FMindexlev) Findexlev = FMindexlev;                              //  disallow F < FM
   }

   if (strmatch("rawcommand",event)) {
      zdialog_fetch(zd,"rawcommand",temp,200);                                         //  get new RAW loader command
      if (*temp > ' ') {
         if (raw_loader_command) zfree(raw_loader_command);
         raw_loader_command = zstrdup(temp,"settings");
      }
   }

   if (strmatch(event,"editrawcomms"))                                                 //  edit raw loader commands file
   {
      zdialog_edit_textfile(Mwin,raw_commands_file);                                   //  edit raw commands file
      get_raw_commands(zd);                                                            //  refresh dialog raw commands list      25.0
      return 1;
   }

   if (strmatch(event,"matchembed"))                                                   //  option, match embedded image color
      zdialog_fetch(zd,"matchembed",Fraw_match_embed);

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

      for (ii = jj = 0; temp[ii]; ii++) {                                              //  insure blanks between types
         if (temp[ii] == '.' && 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;
   }

   if (strmatch(event,"videotypes"))                                                   //  VIDEO file types, .mp4 .mov ...
   {
      zdialog_fetch(zd,"videotypes",temp,200);
      pp = zstrdup(temp,"settings",100);

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

      if (VIDEOfiletypes) zfree(VIDEOfiletypes);
      VIDEOfiletypes = pp;
   }

   if (strmatch(event,"videocomm"))                                                    //  user-selected video command
   {
      zdialog_fetch(zd,"videocomm",temp,200);
      zstrcopy(video_command,temp,"settings");
   }

   return 1;
}


//  local function to stuff settings dialog RAW loader commands
//  from the corresponding text file in Fotocx home folder

void settings::get_raw_commands(zdialog *zd)
{
   zlist_t  *zrawcomms = 0;
   int      nn, ii;
   ch       *pp, text[200];

   zdialog_combo_clear(zd,"rawcommand");                                               //  25.0

   zrawcomms = zlist_from_file(raw_commands_file);
   if (zrawcomms) {
      nn = zlist_count(zrawcomms);
      for (ii = 0; ii < nn; ii++) {
         pp = zlist_get(zrawcomms,ii);
         if (! pp) continue;
         strncpy0(text,pp,200);
         strTrim(text);
         if (strlen(text) < 8) continue;
         zdialog_stuff(zd,"rawcommand",text);
      }
   }

   zdialog_stuff(zd,"rawcommand",raw_loader_command);                                  //  reset to current option
   return;
}


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

//  keyboard shortcuts

namespace KBshortcutnames
{
   zdialog     *zd;

   int         Nreserved = 17;                                                         //  reserved shortcuts (hard coded)
   ch          *reserved[17] = {
      "+", "-", "=", "Z", "F1", "F10", "F11", "Escape", "Delete",                      //  26.0
      "Left", "Right", "Up", "Down", "Home", "End", "Page_Up", "Page_Down" };

   KBsutab_t   KBsutab2[maxkbsu];                                                      //  KB shortcuts list during editing
   int         Nkbsu2;
}


//  KB shortcuts menu function

void m_KB_shortcuts(GtkWidget *, ch *)
{
   using namespace KBshortcutnames;

   int  KBshorts_dialog_event(zdialog *zd, ch *event);
   void KB_shortcuts_edit();

   int         zstat, ii;
   GtkWidget   *widget;

   F1_help_topic = TX("KB SHORTCUTS");

   printf("m_KB_shortcuts \n");

/***
          _______________________________________________
         |             Keyboard Shortcuts                |
         |                                               |
         | Reserved Shortcuts                            |                             //  26.0
         |  + / =             Zoom-in                    |
         |  -                 Zoom-out                   |
         |  Z                 1x <--> fit to window      |
         |  F1                User Guide, Context Help   |
         |  F10/F11           Full Screen / no menus     |
         |  Escape            Quit dialog, Quit Fotocx   |
         |  Delete            Delete/Trash file          |
         |  L/R Arrow keys    Previous/Next file         |
         |  U/D Arrow keys    Gallery row up/down        |
         |  Page U/D keys     Gallery page up/down       |
         |  Home/End          Gallery start/end          |
         |                                               |
         | Custom Shortcuts                              |
         |  F                 File View                  |
         |  G                 Gallery View               |
         |  M                 Map View                   |
         |  R                 Rotate                     |
         |  T                 Retouch                    |
         | ...                ...                        |
         |                                               |
         |                                    [Edit] [X] |
         |_______________________________________________|

***/

   zd = zdialog_new("Keyboard Shortcuts",Mwin,"Edit","X",null);
   zdialog_add_widget(zd,"scrwin","scrlist","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"text","shortlist","scrlist",0,"expand");

   widget = zdialog_gtkwidget(zd,"shortlist");                                         //  list fixed shortcuts

   txwidget_append(widget,1,"Reserved Shortcuts \n");                                  //  26.0
   txwidget_append(widget,0," +           Zoom-in \n");
   txwidget_append(widget,0," =           Zoom-in \n");
   txwidget_append(widget,0," -           Zoom-out \n");
   txwidget_append(widget,0," Z           1x <--> fit to window \n");
   txwidget_append(widget,0," F1          User Guide, Context Help \n");
   txwidget_append(widget,0," F10/F11     Full Screen / no menus \n");
   txwidget_append(widget,0," Escape      Quit dialog / Quit Fotocx \n");
   txwidget_append(widget,0," Delete      Delete/Trash file \n");
   txwidget_append(widget,0," Arrow keys  Previous/Next file \n");
   txwidget_append(widget,0," Page keys   Gallery page up/down \n");
   txwidget_append(widget,0," Home/End    Gallery start/end \n");
   txwidget_append(widget,0,"\n");
   txwidget_append(widget,1,"Custom Shortcuts \n");

   for (ii = 0; ii < Nkbsu; ii++)                                                      //  list custom shortcuts, translated     26.0
      txwidget_append(widget,0," %-14s %s \n",
                     KBsutab[ii].key, TX(KBsutab[ii].menu));

   zdialog_resize(zd,400,600);
   zdialog_run(zd,KBshorts_dialog_event,"save");
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   if (zstat == 1) KB_shortcuts_edit();
   return;
}


//  dialog event and completion function

int KBshorts_dialog_event(zdialog *zd, ch *event)
{
   if (zd->zstat) zdialog_destroy(zd);
   return 1;
}


//  KB shortcuts edit function

void KB_shortcuts_edit()
{
   using namespace KBshortcutnames;

   int  KBshorts_CBfunc1(GtkWidget *widget, int line, int pos, ch *input);
   int  KBshorts_CBfunc2(GtkWidget *widget, int line, int pos, ch *input);
   int  KBshorts_keyfunc(GtkWidget *dialog, GdkEventKey *event);
   int  KBshorts_edit_dialog_event(zdialog *zd, ch *event);

   int         ii;
   GtkWidget   *widget;
   ch          *sortlist[maxkbsf];                                                     //  all eligible funcs, sorted

/***
          _________________________________________________________________
         |              Edit KB Shortcuts                                  |
         |_________________________________________________________________|
         |  F              File View               |  Blur                 |
         |  G              Gallery View            |  Bookmarks            |
         |  M              Map View                |  Captions             |
         |  R              Rotate                  |  Color Depth          |
         |  T              Retouch                 |  Color Saturation     |
         |  K              KB Shortcuts Menu       |  Copy to Cache        |
         | ...             ...                     |  ...                  |
         |_________________________________________|_______________________|
         |                                                                 |
         | shortcut key: (press key)  (select function)                    |
         |                                                                 |
         |                                         [Add] [Remove] [OK] [X] |
         |_________________________________________________________________|

***/

   zd = zdialog_new("Edit KB Shortcuts",Mwin,"Add","Delete","OK","X",null);
   zdialog_add_widget(zd,"hbox","hblists","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrlist","hblists",0,"expand");
   zdialog_add_widget(zd,"text","shortlist","scrlist",0,"expand");
   zdialog_add_widget(zd,"vsep","separator","hblists",0,"space=10");
   zdialog_add_widget(zd,"scrwin","scrmenus","hblists",0,"expand");
   zdialog_add_widget(zd,"text","menufuncs","scrmenus");
   zdialog_add_widget(zd,"hbox","hbshort","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labshort","hbshort",TX("shortcut key:"),"space=5");
   zdialog_add_widget(zd,"label","shortkey","hbshort",TX("(press key)"),"size=10");
   zdialog_add_widget(zd,"label","shortfunc","hbshort",TX("(select function)"),"space=5");

   for (ii = 0; ii < Nkbsu; ii++) {                                                    //  copy current shortcuts list
      KBsutab2[ii].key = zstrdup(KBsutab[ii].key,"KB_shortcuts");                      //    for use during editing
      KBsutab2[ii].menu = zstrdup(KBsutab[ii].menu,"KB_shortcuts");
   }
   Nkbsu2 = Nkbsu;

   widget = zdialog_gtkwidget(zd,"shortlist");                                         //  show shortcuts list in dialog
   txwidget_clear(widget);
   for (ii = 0; ii < Nkbsu2; ii++)
      txwidget_append(widget,0,"%-14s %s \n",                                          //  26.0
                        KBsutab2[ii].key, TX(KBsutab2[ii].menu));

   txwidget_set_eventfunc(widget,KBshorts_CBfunc1);                                    //  set mouse/KB event function

   for (ii = 0; ii < Nkbsf; ii++)                                                      //  copy eligible shortcut funcs
      sortlist[ii] = zstrdup(KBsftab[ii].menu,"KB_shortcuts");

   HeapSort(sortlist,Nkbsf);                                                           //  sort copied list

   widget = zdialog_gtkwidget(zd,"menufuncs");                                         //  clear dialog
   txwidget_clear(widget);

   for (ii = 0; ii < Nkbsf; ii++)                                                      //  show sorted funcs list
      txwidget_append(widget,0,"%s\n",sortlist[ii]);

   txwidget_set_eventfunc(widget,KBshorts_CBfunc2);                                    //  set mouse/KB event function

   widget = zdialog_gtkwidget(zd,"dialog");                                            //  capture KB keys pressed
   G_SIGNAL(widget,"key-press-event",KBshorts_keyfunc,0);

   zdialog_resize(zd,600,400);
   zdialog_run(zd,KBshorts_edit_dialog_event,"save");

   return;
}


//  mouse callback function to select existing shortcut from list
//    and stuff into dialog "shortfunc"

int KBshorts_CBfunc1(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace KBshortcutnames;

   ch       *txline;
   ch       shortkey[20];
   ch       shortfunc[60];

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txline = txwidget_line(widget,line,1);                                              //  get clicked line
   if (! txline || ! *txline) return 1;
   txwidget_highlight_line(widget,line);

   strncpy0(shortkey,txline,14);                                                       //  get shortcut key and menu
   strncpy0(shortfunc,txline+15,60);
   zfree(txline);

   strTrim(shortkey);
   strTrim(shortfunc);

   zdialog_stuff(zd,"shortkey",shortkey);                                              //  stuff into dialog
   zdialog_stuff(zd,"shortfunc",shortfunc);

   return 1;
}


//  mouse callback function to select new shortcut function from menu list
//    and stuff into dialog "shortfunc"

int KBshorts_CBfunc2(GtkWidget *widget, int line, int pos, ch *input)
{
   using namespace KBshortcutnames;

   ch       *txline;

   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   txline = txwidget_line(widget,line,1);                                              //  get clicked line
   if (! txline || ! *txline) return 1;
   txwidget_highlight_line(widget,line);

   zdialog_stuff(zd,"shortfunc",txline);                                               //  stuff into dialog
   zfree(txline);
   return 1;
}


//  intercept KB key events, stuff into dialog "shortkey"

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

   int      Ctrl = 0, Alt = 0, Shift = 0;
   int      key, ii, cc;
   ch       kbname[20];

   key = event->keyval;

   if (event->state & GDK_CONTROL_MASK) Ctrl = 1;
   if (event->state & GDK_SHIFT_MASK) Shift = 1;
   if (event->state & GDK_MOD1_MASK) Alt = 1;

   if (key == GDK_KEY_F1) {                                                            //  key is F1 (context help)
      KBevent(event);                                                                  //  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(kbname,"F1");
      kbname[1] += ii;
   }

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

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

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

   zdialog_stuff(zd,"shortkey",kbname);                                                //  stuff key name into dialog
   zdialog_stuff(zd,"shortfunc",TX("(no selection)"));                                 //  clear menu choice

   return 1;
}


//  dialog event and completion function

int KBshorts_edit_dialog_event(zdialog *zd, ch *event)
{
   using namespace KBshortcutnames;

   int  KBshorts_edit_menufuncs_event(zdialog *zd, ch *event);
   int  KBshorts_CBfunc2(GtkWidget *widget, int line, int pos, ch *input);

   int         ii, jj;
   GtkWidget   *widget;
   ch          shortkey[20];
   ch          shortfunc[60];
   FILE        *fid = 0;

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

   if (zd->zstat == 1)                                                                 //  add shortcut
   {
      zd->zstat = 0;                                                                   //  keep dialog active

      if (Nkbsu2 == maxkbsu) {
         zmessageACK(Mwin,TX("exceed %d shortcuts"),maxkbsu);
         return 1;
      }

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

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

      if (ii < Nkbsu2) {                                                               //  if found, remove from list
         zfree(KBsutab2[ii].key);
         zfree(KBsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            KBsutab2[jj] = KBsutab2[jj+1];
         --Nkbsu2;
      }

      for (ii = 0; ii < Nkbsu2; ii++)                                                  //  find matching shortcut func in list
         if (strmatch(shortfunc,KBsutab2[ii].menu)) break;

      if (ii < Nkbsu2) {                                                               //  if found, remove from list
         zfree(KBsutab2[ii].key);
         zfree(KBsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            KBsutab2[jj] = KBsutab2[jj+1];
         --Nkbsu2;
      }

      for (ii = 0; ii < Nkbsf; ii++)                                                   //  look up shortcut func in list
         if (strmatch(shortfunc,KBsftab[ii].menu)) break;                              //    of eligible menu funcs
      if (ii == Nkbsf) return 1;
      strncpy0(shortfunc,KBsftab[ii].menu,60);                                         //  english menu func

      ii = Nkbsu2++;                                                                   //  add new shortcut to end of list
      KBsutab2[ii].key = zstrdup(shortkey,"KB_shortcuts");
      KBsutab2[ii].menu = zstrdup(shortfunc,"KB_shortcuts");

      widget = zdialog_gtkwidget(zd,"shortlist");                                      //  clear shortcuts list in dialog
      txwidget_clear(widget);

      for (ii = 0; ii < Nkbsu2; ii++)                                                  //  show updated shortcuts in dialog
         txwidget_append2(widget,0,"%-14s %s \n",
                              KBsutab2[ii].key,TX(KBsutab2[ii].menu));                 //  26.0
      return 1;
   }

   if (zd->zstat == 2)                                                                 //  remove shortcut
   {
      zd->zstat = 0;                                                                   //  keep dialog active

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

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

      if (ii < Nkbsu2) {                                                               //  if found, remove from list
         zfree(KBsutab2[ii].key);
         zfree(KBsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            KBsutab2[jj] = KBsutab2[jj+1];
         --Nkbsu2;
      }

      widget = zdialog_gtkwidget(zd,"shortlist");                                      //  clear shortcuts list in dialog
      txwidget_clear(widget);

      for (ii = 0; ii < Nkbsu2; ii++)                                                  //  show updated shortcuts in dialog
         txwidget_append2(widget,0,"%-14s %s \n",
                            KBsutab2[ii].key,TX(KBsutab2[ii].menu));                   //  26.0

      zdialog_stuff(zd,"shortkey","");                                                 //  clear entered key and menu
      zdialog_stuff(zd,"shortfunc",TX("(no selection)"));
      return 1;
   }

   if (zd->zstat == 3)                                                                 //  done - save new shortcut list
   {
      zdialog_free(zd);                                                                //  kill menu funcs list

      fid = fopen(KB_shortcuts_file,"w");                                              //  update KB shortcuts file
      if (! fid) {
         zmessageACK(Mwin,strerror(errno));
         return 1;
      }
      for (ii = 0; ii < Nkbsu2; ii++)
         fprintf(fid,"%-14s %s \n",KBsutab2[ii].key,KBsutab2[ii].menu);
      fclose(fid);

      KB_shortcuts_load();                                                             //  reload shortcuts list from file
      return 1;
   }

   zdialog_free(zd);                                                                   //  cancel
   return 1;
}


//  Read KB_shortcuts file and load shortcuts table in memory.
//  Called at fotocx startup time.

void KB_shortcuts_load()
{
   using namespace KBshortcutnames;

   int         ii, jj;
   ch          buff[200];
   ch          *pp1, *pp2;
   ch          *key, *menu;
   FILE        *fid;

   for (ii = 0; ii < Nkbsu; ii++) {                                                    //  clear shortcuts data
      zfree(KBsutab[ii].key);
      zfree(KBsutab[ii].menu);
   }
   Nkbsu = 0;

   fid = fopen(KB_shortcuts_file,"r");                                                 //  read KB shortcuts file
   if (! fid) return;

   for (ii = 0; ii < maxkbsu; )
   {
      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;
      key = zstrdup(pp1,"KB_shortcuts");                                               //  shortcut key or combination

      for (jj = 0; jj < Nreserved; jj++)                                               //  outlaw reserved shortcut
         if (strmatchcase(key,reserved[jj])) break;
      if (jj < Nreserved) {
         zmessage_post(Mwin,"parent",3,TX("Reserved KB shortcut ignored: %s"),key);    //  26.0
         continue;
      }

      pp1 = pp2 + 1;
      while (*pp1 && *pp1 == ' ') pp1++;
      if (! *pp1) continue;
      menu = zstrdup(pp1,"KB_shortcuts");                                              //  corresp. menu

      KBsutab[ii].key = key;                                                           //  add to shortcuts table
      KBsutab[ii].menu = zstrdup(menu,"KB_shortcuts");                                 //  keep english
      ii++;
   }

   Nkbsu = ii;

   fclose(fid);
   return;
}


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

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

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


//  menu function

void m_RGB_hist(GtkWidget *, ch *menu)                                                 //  menu function
{
   using namespace RGB_hist_names;

   void RGB_hist_graph(GtkWidget *drawin, cairo_t *cr, int *rgbw);
   int  show_RGB_hist_dialog_event(zdialog *zd, ch *event);

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

   printf("m_RGB_hist \n");

   viewmode('F');                                                                      //  file view mode

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   if (menu && strmatch(menu,"kill")) {
      if (zd_RGB_hist) zdialog_free(zd_RGB_hist);
      zd_RGB_hist = 0;
      return;
   }

   if (zd_RGB_hist) {                                                                  //  dialog already present
      gtk_widget_queue_draw(drawwin_hist);                                             //  refresh drawing windows
      return;
   }

   if (menu) F1_help_topic = TX("RGB HISTOGRAM");

   zd = zdialog_new(TX("Brightness Histogram"),Mwin,null);
   zdialog_add_widget(zd,"frame","frhist","dialog",0,"expand");                        //  frames for 2 drawing areas
   zdialog_add_widget(zd,"frame","frscale","dialog");
   frhist = zdialog_gtkwidget(zd,"frhist");
   frscale = zdialog_gtkwidget(zd,"frscale");

   drawwin_hist = gtk_drawing_area_new();                                              //  histogram drawing area
   gtk_container_add(GTK_CONTAINER(frhist),drawwin_hist);
   G_SIGNAL(drawwin_hist,"draw",RGB_hist_graph,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",brightness_scale,0);

   zdialog_add_widget(zd,"hbox","hbcolors","dialog");
   zdialog_add_widget(zd,"check","all","hbcolors","All","space=5");
   zdialog_add_widget(zd,"check","red","hbcolors","Red","space=5");
   zdialog_add_widget(zd,"check","green","hbcolors","Green","space=5");
   zdialog_add_widget(zd,"check","blue","hbcolors","Blue","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_RGB_hist_dialog_event,"save");

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

   zd_RGB_hist = zd;
   return;
}


//  dialog event and completion function

int show_RGB_hist_dialog_event(zdialog *zd, ch *event)
{
   using namespace RGB_hist_names;

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

   if (zstrstr("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_hist);                                             //  refresh drawing window
   }

   return 1;
}


//  draw brightness histogram graph in drawing window

void RGB_hist_graph(GtkWidget *drawin, cairo_t *cr, int *rgbw)
{
   int         bin, Nbins = 256, histogram[256][4];                                    //  bin count, R/G/B/all
   int         px, py, dx, dy;
   int         ww, hh, winww, winhh;
   int         ii, rgb, maxhist, bright;
   uint8       *pixi;

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

   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 histogram
   for (rgb = 0; rgb < 4; rgb++)
      histogram[bin][rgb] = 0;

   ww = Fpxb->ww;                                                                      //  image area within window
   hh = Fpxb->hh;

   for (ii = 0; ii < ww * hh; ii++)
   {
      if (sa_stat == sa_stat_fini && ! 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

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

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

   maxhist = 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 (histogram[bin][rgb] > maxhist)                                               //      which can be huge
         maxhist = histogram[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 * histogram[bin][rgb] / maxhist;                             //  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 brightness_scale(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;
}


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

//  magnify image within a given radius of dragged mouse

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

   float       Xmag;                                                                   //  magnification, 1 - 5x
   int         Mx, My;                                                                 //  mouse location, image space
   int         Mrad;                                                                   //  mouse radius
}


//  menu function

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

   ch    *mess = TX("Drag mouse on image. \n"
                   "Left click to cancel.");

   F1_help_topic = TX("MAGNIFY IMAGE");

   printf("m_magnify \n");

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

***/

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');                                                                      //  file view mode

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

   else {
      zdialog *zd = zdialog_new("Magnify Image",Mwin,"X",null);
      zd_magnify = 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",TX("Radius"),"space=5");
      zdialog_add_widget(zd,"zspin","Mrad","hbr","50|500|10|200");
      zdialog_add_widget(zd,"hbox","hbx","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labx","hbx",TX("X-size"),"space=5");
      zdialog_add_widget(zd,"zspin","Xmag","hbx","2|10|1|2");

      zdialog_fetch(zd,"Mrad",Mrad);                                                   //  initial mouse radius
      zdialog_fetch(zd,"Xmag",Xmag);                                                   //  initial magnification

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

      zdialog_send_event(zd,"Mrad");                                                   //  initializations
      zdialog_send_event(zd,"Xmag");
   }

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


//  dialog event and completion callback function

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

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

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

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

   if (strmatch(event,"Mrad")) {
      zdialog_fetch(zd,"Mrad",Mrad);                                                   //  new mouse radius
      return 1;
   }

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

   return 1;
}


//  pixel paint mouse function

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

   static int     ftf = 1;

   if (! curr_file) return;
   if (FGM != 'F') return;
   if (! zd_magnify) return;

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

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

   else
   {
      if (! ftf) Fpaint2();                                                            //  refresh image
      ftf = 1;
      gdk_window_set_cursor(gdkwin,dragcursor);
   }

   return;
}


//  Get pixels from mouse circle within full size image Fpxb, magnify
//  and move into Mpxb, paint. Mpxb is scaled image for display.

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

   int         Frad;                                                                   //  mouse radius, image space
   int         Fx, Fy, Fww, Fhh;                                                       //  mouse circle, enclosing box, Fpxb
   static int  pFx, pFy, pFww, pFhh;
   PIXBUF      *pxb1, *pxb2, *pxbx;
   int         ww1, hh1, rs1, ww2, hh2, rs2;
   uint8       *pixels1, *pixels2, *pix1, *pix2;
   int         nch = Fpxb->nc;
   int         px1, py1, px2, py2;
   int         xlo, xhi, ylo, yhi;
   int         xc, yc, dx, dy;
   float       R2, R2lim;
   cairo_t     *cr;

   Frad = Mrad / Mscale;                                                               //  keep magnify circle constant

   //  get box enclosing PRIOR mouse circle, restore those pixels

   if (! ftf)                                                                          //  continuation of mouse drag
   {
      pxb1 = gdk_pixbuf_new_subpixbuf(Fpxb->pixbuf,pFx,pFy,pFww,pFhh);                 //  unmagnified pixels, Fpxb
      if (! pxb1) return;

      ww1 = pFww * Mscale;                                                             //  scale to Mpxb
      hh1 = pFhh * Mscale;
      pxbx = gdk_pixbuf_scale_simple(pxb1,ww1,hh1,BILINEAR);
      g_object_unref(pxb1);
      pxb1 = pxbx;

      px1 = pFx * Mscale;                                                              //  copy into Mpxb
      py1 = pFy * Mscale;
      gdk_pixbuf_copy_area(pxb1,0,0,ww1,hh1,Mpxb->pixbuf,px1,py1);

      g_object_unref(pxb1);
   }

   //  get box enclosing current mouse circle in Fpxb

   Fx = Mx - Frad;                                                                     //  mouse circle, enclosing box
   Fy = My - Frad;                                                                     //  (Fpxb, 1x image)
   Fww = Fhh = Frad * 2;

   //  clip current mouse box to keep within image

   if (Fx < 0) {
      Fww += Fx;
      Fx = 0;
   }

   if (Fy < 0) {
      Fhh += Fy;
      Fy = 0;
   }

   if (Fx + Fww > Fpxb->ww)
      Fww = Fpxb->ww - Fx;

   if (Fy + Fhh > Fpxb->hh)
      Fhh = Fpxb->hh - Fy;

   if (Fww <= 0 || Fhh <= 0) return;

   pFx = Fx;                                                                           //  save this box for next restore
   pFy = Fy;
   pFww = Fww;
   pFhh = Fhh;

   //  scale box for Mpxb, then magnify by Xmag

   pxb1 = gdk_pixbuf_new_subpixbuf(Fpxb->pixbuf,Fx,Fy,Fww,Fhh);                        //  Fpxb mouse area, 1x
   if (! pxb1) return;

   ww1 = Fww * Mscale;
   hh1 = Fhh * Mscale;
   pxbx = gdk_pixbuf_scale_simple(pxb1,ww1,hh1,BILINEAR);
   g_object_unref(pxb1);
   pxb1 = pxbx;                                                                        //  Mpxb mouse area, Mscale
   rs1 = gdk_pixbuf_get_rowstride(pxb1);
   pixels1 = gdk_pixbuf_get_pixels(pxb1);

   ww2 = ww1 * Xmag;
   hh2 = hh1 * Xmag;
   pxb2 = gdk_pixbuf_scale_simple(pxb1,ww2,hh2,BILINEAR);                              //  magnified mouse area
   rs2 = gdk_pixbuf_get_rowstride(pxb2);
   pixels2 = gdk_pixbuf_get_pixels(pxb2);

   //  copy magnified pixels within mouse radius only

   xlo = (ww2 - ww1) / 2;                                                              //  pxb2 overlap area with pxb1
   xhi = ww2 - xlo;
   ylo = (hh2 - hh1) / 2;
   yhi = hh2 - ylo;

   xc = (Mx - Fx) * Mscale;                                                            //  mouse center in pxb1
   yc = (My - Fy) * Mscale;
   R2lim = Frad * Mscale;                                                              //  mouse radius in pxb1

   for (py2 = ylo; py2 < yhi; py2++)                                                   //  loop pxb2 pixels
   for (px2 = xlo; px2 < xhi; px2++)
   {
      px1 = px2 - xlo;                                                                 //  corresp. pxb1 pixel
      py1 = py2 - ylo;
      if (px1 < 0 || px1 >= ww1) continue;
      if (py1 < 0 || py1 >= hh1) continue;
      dx = px1 - xc;
      dy = py1 - yc;
      R2 = sqrtf(dx * dx + dy * dy);
      if (R2 > R2lim) continue;                                                        //  outside mouse radius
      pix1 = pixels1 + py1 * rs1 + px1 * nch;
      pix2 = pixels2 + py2 * rs2 + px2 * nch;
      memcpy(pix1,pix2,nch);
   }

   px1 = Fx * Mscale;                                                                  //  copy into Mpxb
   py1 = Fy * Mscale;
   gdk_pixbuf_copy_area(pxb1,0,0,ww1,hh1,Mpxb->pixbuf,px1,py1);

   g_object_unref(pxb1);
   g_object_unref(pxb2);

   cr = draw_context_create(gdkwin,draw_context);
   if (! cr) return;

   Fpaint4(Fx,Fy,Fww,Fhh,cr);

   draw_mousecircle(Mx,My,Frad,0,cr);

   draw_context_destroy(draw_context);

   return;
}


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

//  Show the last two pixel positions clicked and the distance between.

namespace measure_image_names
{
   zdialog  *zd;
   int      p1x, p1y, p2x, p2y;
   int      dx, dy, dh;
   int      Npix;
}

void  measure_image_mousefunc();


//  menu function

void m_measure_image(GtkWidget *, ch *)
{
   using namespace measure_image_names;

   int   measure_image_dialog_event(zdialog *zd, ch *event);

   ch       *mess = TX("Click image to select pixels");

   F1_help_topic = TX("MEASURE IMAGE");

   printf("m_measure_image \n");

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');                                                                      //  file view mode

/***
          ______________________________________
         |           Measure Image              |
         |                                      |
         | Click image to select pixels         |
         |                                      |
         | Pixel A: xxxx xxxx  B: xxxx xxxx     |
         | Distance X: xxxx  Y: xxxx  H: xxxx   |
         |                                      |
         |                                  [X] |
         |______________________________________|

***/

   zd = zdialog_new("Measure Image",Mwin,"X",null);

   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","hbpix","dialog");
   zdialog_add_widget(zd,"label","labpix","hbpix","Pixel A: 0000 0000  Pixel B: 0000 0000","space=3");
   zdialog_add_widget(zd,"hbox","hbdist","dialog");
   zdialog_add_widget(zd,"label","labdist","hbdist","Distance X: 0000  Y: 0000  H: 0000","space=3");

   zdialog_run(zd,measure_image_dialog_event,"save");                                  //  run dialog
   takeMouse(measure_image_mousefunc,dotcursor);                                       //  connect mouse function

   Npix = 0;                                                                           //  no clicked pixel positions yet
   p1x = p1y = p2x = p2y = 0;
   dx = dy = dh = 0;

   return;
}


//  dialog event and completion function

int measure_image_dialog_event(zdialog *zd, ch *event)
{
   using namespace measure_image_names;

   if (zd->zstat) {                                                                    //  any status
      freeMouse();                                                                     //  disconnect mouse function
      zdialog_free(zd);                                                                //  kill dialog
      erase_toptext(102);                                                              //  clear pixel labels
      Fpaint2();
      return 1;
   }

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

   return 1;
}


//  mouse function

void measure_image_mousefunc()
{
   using namespace measure_image_names;

   ch       text[100];

   if (! LMclick) return;                                                              //  left click

   LMclick = 0;

   if (Npix == 0) {                                                                    //  first clicked pixel
      Npix = 1;
      p1x = Mxclick;
      p1y = Myclick;
      add_toptext(102,p1x,p1y,"A","Sans 8");
      Fpaint2();
   }

   else if (Npix == 1) {                                                               //  2nd clicked pixel
      Npix = 2;
      p2x = Mxclick;
      p2y = Myclick;
   }

   else if (Npix == 2) {                                                               //  next clicked pixel
      p1x = p2x;                                                                       //  pixel 2 --> pixel 1
      p1y = p2y;
      p2x = Mxclick;                                                                   //  new pixel --> pixel 2
      p2y = Myclick;
   }

   if (Npix < 2) return;

   erase_toptext(102);
   add_toptext(102,p1x,p1y,"A","Sans 8");
   add_toptext(102,p2x,p2y,"B","Sans 8");
   Fpaint2();

   dx = abs(p1x - p2x);
   dy = abs(p1y - p2y);
   dh = sqrt(dx*dx + dy*dy) + 0.5;

   snprintf(text,100,"Pixel A: %d %d  Pixel B: %d %d",p1x,p1y,p2x,p2y);
   zdialog_stuff(zd,"labpix",text);

   snprintf(text,100,"Distance X: %d  Y: %d  H: %d",dx,dy,dh);
   zdialog_stuff(zd,"labdist",text);

   return;
}


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

//  Show RGB values for 1-9 pixels selected with mouse-clicks.
//  Additional pixel position tracks active mouse position

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

zdialog     *RGBSzd;
int         RGBSpixel[10][2];                                                          //  0-9 clicked pixels + current mouse
int         RGBSnpix = 0;                                                              //  no. clicked pixels, 0-9
int         RGBSdelta = 0;                                                             //  abs/delta mode
int         RGBSlabels = 0;                                                            //  pixel labels on/off


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

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

   F1_help_topic = TX("SHOW RGB");

   printf("m_show_RGB \n");

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');                                                                      //  file view mode

   if (! E0pxm && ! E1pxm && ! E3pxm) {
      E0pxm = PXM_load(curr_file,1);                                                   //  never edited
      if (! E0pxm) return;                                                             //  get poss. 16-bit file
   }

/***
    ____________________________________________
   |                                            |
   |  Click image to select pixels              |
   |  [x] delta  [x] labels                     |
   |                                            |
   |   Pixel           Red     Green   Blue     |
   |   A NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |                                      //  pixel coordinates, RGB values 0-255.99
   |   B NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   C NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   D NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   E NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   F NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   G NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   H NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |   I NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |     NNNNN NNNNN   NNN.NN  NNN.NN  NNN.NN   |
   |                                            |
   |                                [Clear] [X] |
   |____________________________________________|

***/

   RGBSnpix = 0;                                                                       //  no clicked pixels yet
   RGBSlabels = 0;                                                                     //  no labels yet

   if (RGBSzd) zdialog_free(RGBSzd);                                                   //  delete previous if any
   zdialog *zd = zdialog_new("Show RGB",Mwin,"Clear","X",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,"vbox","vbdat","dialog",0,"space=5");                         //  vbox for 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);

   for (ii = 0; ii < 10; ii++)
   {                                                                                   //  10 hbox's with 10 labels
      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,dotcursor);                                            //  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, ch *event)
{
   if (zd->zstat) {
      if (zd->zstat == 1) {                                                            //  clear
         zd->zstat = 0;                                                                //  keep dialog active
         RGBSnpix = 0;                                                                 //  clicked pixel count = 0
         erase_toptext(102);                                                           //  erase labels on image
      }
      else {                                                                           //  done or kill
         freeMouse();                                                                  //  disconnect mouse function
         zdialog_free(RGBSzd);                                                         //  kill dialog
         RGBSzd = 0;
         erase_toptext(102);
      }
      Fpaint2();
      return 0;
   }

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(show_RGB_mousefunc,dotcursor);                                         //  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,TX("Edit function must be active"));
      }
   }

   if (strmatch(event,"labels"))                                                       //  get labels on/off
      zdialog_fetch(zd,"labels",RGBSlabels);

   return 0;
}


//  mouse function
//  fill table positions 0-8 with last clicked pixel positions and RGB data
//  next table position tracks current mouse position and RGB data

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 return;                                                                        //  must have E0/E1/E3

   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 clicked pixel
      }

      ii = RGBSnpix;                                                                   //  labeled position to fill, 0-8
      RGBSpixel[ii][0] = Mxclick;                                                      //  save newest pixel
      RGBSpixel[ii][1] = Myclick;
      RGBSnpix++;                                                                      //  count is 1-9
   }

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

   return;
}


//  timer function
//  display RGB values for last 0-9 clicked pixels and current mouse position

int show_RGB_timefunc(void *arg)
{
   ch       label[9][4] = { " A ", " B ", " C ", " D ", " E ",                         //  labels A-I for last 0-9 clicked pixels
                                  " F ", " G ", " H ", " I " };
   PXM      *pxm = 0;
   int      ii, jj, px, py;
   int      ww, hh;
   float    red3, green3, blue3;
   float    *ppixa, *ppixb;
   ch       text[100], pixx[8] = "pixx";

   static float   priorvals[10][3];                                                    //  remembers prior pixel values

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

   if (! E0pxm && ! E1pxm && ! E3pxm) {
      E0pxm = PXM_load(curr_file,1);
      if (! E0pxm) return 0;                                                           //  get poss. 16-bit file
   }

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

   if (RGBSdelta && ! E3pxm) {
      RGBSdelta = 0;                                                                   //  delta mode only if edit active
      zdialog_stuff(RGBSzd,"delta",RGBSdelta);                                         //  update dialog
   }

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

   for (ii = 0; ii < RGBSnpix; ii++)                                                   //  0-9 clicked pixels
   {
      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 < RGBSnpix + 1; jj++) {
         RGBSpixel[jj-1][0] = RGBSpixel[jj][0];                                        //  remove pixel outside limits
         RGBSpixel[jj-1][1] = RGBSpixel[jj][1];                                        //    and pack the remaining down
      }                                                                                //  include last+1 = curr. mouse position

      ii--;
      RGBSnpix--;
   }

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

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

      if (ii > RGBSnpix) {                                                             //  no pixel there yet
         zdialog_stuff(RGBSzd,pixx,"");                                                //  blank report line
         continue;
      }

      px = RGBSpixel[ii][0];                                                           //  next pixel to report
      py = RGBSpixel[ii][1];

      if (px >= ww || py >= hh) continue;                                              //  PXM may have changed

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

      if (RGBSdelta) {                                                                 //  delta RGB for edited image
         ppixb = PXMpix(E1pxm,px,py);                                                  //  "before" image E1
         red3 -= ppixb[0];
         green3 -= ppixb[1];
         blue3 -= ppixb[2];
      }

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

      snprintf(text+14,86,"   %6.2f  %6.2f  %6.2f ",red3,green3,blue3);
      zdialog_labelfont(RGBSzd,pixx,"monospace 9",text);
   }

   for (ii = 0; ii < RGBSnpix; ii++)                                                   //  paint only when pixels change
   {
      px = RGBSpixel[ii][0];                                                           //  next pixel to report
      py = RGBSpixel[ii][1];

      if (px >= ww || py >= hh) continue;                                              //  PXM may have changed

      ppixa = PXMpix(pxm,px,py);                                                       //  get pixel RGB values
      if (ppixa[0] != priorvals[ii][0]) break;
      if (ppixa[1] != priorvals[ii][1]) break;
      if (ppixa[2] != priorvals[ii][2]) break;
   }

   if (ii < RGBSnpix) Fpaint2();

   for (ii = 0; ii < RGBSnpix; ii++)                                                   //  record pixels for change detection
   {
      px = RGBSpixel[ii][0];                                                           //  next pixel to report
      py = RGBSpixel[ii][1];

      if (px >= ww || py >= hh) continue;                                              //  PXM may have changed

      ppixa = PXMpix(pxm,px,py);                                                       //  get pixel RGB values
      priorvals[ii][0] = ppixa[0];
      priorvals[ii][1] = ppixa[1];
      priorvals[ii][2] = ppixa[2];
   }

   return 1;
}


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

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

void m_grid_settings(GtkWidget *widget, ch *menu)
{
   int grid_settings_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd;

   F1_help_topic = TX("GRID SETTINGS");

   printf("m_grid_settings \n");

   viewmode('F');                                                                      //  file view mode

/***
       ____________________________________________
      |             Grid Settings                  |
      |                                            |
      |  x-count  [____]     y-count  [____]       |
      |                                            |
      | x-offset =================[]=============  |
      | y-offset ==============[]================  |
      |                                            |
      |                              [Recent] [OK] |                                   //  25.1
      |____________________________________________|

***/

   zd = zdialog_new("Grid Settings",Mwin,"Recent","OK",null);

   zdialog_add_widget(zd,"hbox","hb0","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labx","hb0",TX("x-count"),"space=7");
   zdialog_add_widget(zd,"zspin","countx","hb0","0|100|1|2","space=4");
   zdialog_add_widget(zd,"label","space","hb0",0,"space=8");
   zdialog_add_widget(zd,"label","laby","hb0",TX("y-count"));
   zdialog_add_widget(zd,"zspin","county","hb0","0|100|1|2","space=4");

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

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

   zdialog_stuff(zd,"countx",gridsettings[GXC]);
   zdialog_stuff(zd,"county",gridsettings[GYC]);
   zdialog_stuff(zd,"offsetx",gridsettings[GXF]);
   zdialog_stuff(zd,"offsety",gridsettings[GYF]);

   zdialog_set_modal(zd);
   zdialog_run(zd,grid_settings_dialog_event,"parent");
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  dialog event function

int grid_settings_dialog_event(zdialog *zd, ch *event)
{
   static zlist_t  *picklist = 0;

   ch       *gridchoice, newchoice[20];
   int      countx, county, nn, err;

   countx = gridsettings[GXC];                                                         //  current grid settings
   county = gridsettings[GYC];
   if (countx < 0 || countx > 100) countx = 0;
   if (county < 0 || county > 100) county = 0;

   if (! picklist)
      picklist = zlist_from_file(grid_picklist_file);                                  //  get grid picklist from file
   if (! picklist) {
      picklist = zlist_new(0);                                                         //  make default picklist
      zlist_put(picklist,"1 x 1",0);                                                   //    with two entries
      zlist_put(picklist,"2 x 2",1);
      err = zlist_to_file(picklist,grid_picklist_file);
      if (err) {
         zmessageACK(Mwin,TX("grid picklist file error: %s"),strerror);
         picklist = 0;
      }
      return 1;
   }

   if (zd->zstat == 1)                                                                 //  [Recent]
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      if (! picklist) return 1;
      gridchoice = popup_choose(picklist);                                             //  get choice, "NN x NN"
      if (! gridchoice) return 1;
      countx = county = -1;
      sscanf(gridchoice,"%d x %d ",&countx,&county);
      if (countx < 0 || countx > 100 || county < 0 || county > 100) {
         zmessageACK(Mwin,TX("grid counts not 0-100: %s"),gridchoice);
         return 1;
      }

      gridsettings[GXC] = countx;                                                      //  set grids
      gridsettings[GYC] = county;
      gridsettings[GON] = 1;
      zdialog_stuff(zd,"countx",countx);
      zdialog_stuff(zd,"county",county);
      Fpaint2();
      return 1;
   }

   if (zd->zstat == 2)                                                                 //  [OK]
   {
      if (countx || county) gridsettings[GON] = 1;
      else gridsettings[GON] = 0;
      Fpaint2();

      if (picklist) {
         snprintf(newchoice,20,"%d x %d",countx,county);                               //  new picklist entry N x N
         nn = zlist_find(picklist,newchoice,0);                                        //  if entry already present,
         if (nn >= 0) zlist_remove(picklist,nn);                                       //     remove it
         zlist_prepend(picklist,newchoice,0);                                          //  prepend newest entry
         zlist_to_file(picklist,grid_picklist_file);                                   //  update grid_picklist file
      }

      return 1;
   }

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

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

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

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

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

   if (gridsettings[GXC] || gridsettings[GYC])                                         //  if either grid enabled, show grid
      gridsettings[GON] = 1;

   Fpaint2();
   return 1;
}


//  toggle grid lines on and off

void m_toggle_grid(GtkWidget *, ch *menu)
{
   F1_help_topic = TX("GRID SETTINGS");

   printf("m_toggle_grid \n");

   gridsettings[GON] = 1 - gridsettings[GON];
   Fpaint2();
   return;
}


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

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

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

   zdialog  *zd;

   F1_help_topic = TX("LINE COLOR");

   printf("m_line_color \n");

   viewmode('F');                                                                      //  file view mode

   zd = zdialog_new("Line Color",Mwin,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","Black","hb1",TX("Black"),"space=3");                 //  add radio button per color
   zdialog_add_widget(zd,"radio","White","hb1",TX("White"),"space=3");
   zdialog_add_widget(zd,"radio","Red","hb1",TX("Red"),"space=3");
   zdialog_add_widget(zd,"radio","Green","hb1",TX("Green"),"space=3");

   zdialog_stuff(zd,"Black",0);                                                        //  all are initially off
   zdialog_stuff(zd,"White",0);
   zdialog_stuff(zd,"Red",0);
   zdialog_stuff(zd,"Green",0);

   if (LINE_COLOR[0] == BLACK[0] && LINE_COLOR[1] == BLACK[1] && LINE_COLOR[2] == BLACK[2])
      zdialog_stuff(zd,"Black",1);
   if (LINE_COLOR[0] == WHITE[0] && LINE_COLOR[1] == WHITE[1] && LINE_COLOR[2] == WHITE[2])
      zdialog_stuff(zd,"White",1);
   if (LINE_COLOR[0] == RED[0] && LINE_COLOR[1] == RED[1] && LINE_COLOR[2] == RED[2])
      zdialog_stuff(zd,"Red",1);
   if (LINE_COLOR[0] == GREEN[0] && LINE_COLOR[1] == GREEN[1] && LINE_COLOR[2] == GREEN[2])
      zdialog_stuff(zd,"Green",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, ch *event)
{
   if (strmatch(event,"Black")) memcpy(LINE_COLOR,BLACK,3*sizeof(int));                //  set selected color
   if (strmatch(event,"White")) memcpy(LINE_COLOR,WHITE,3*sizeof(int));
   if (strmatch(event,"Red"))   memcpy(LINE_COLOR,RED,3*sizeof(int));
   if (strmatch(event,"Green")) memcpy(LINE_COLOR,GREEN,3*sizeof(int));
   if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"line_color");
   Fpaint2();

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


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

//  dark-brite menu function
//  highlight darkest and brightest pixels by blinking them

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

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

   int    darkbrite_dialog_event(zdialog* zd, ch *event);

   F1_help_topic = TX("DARK/BRIGHT PIXELS");

   printf("m_darkbrite \n");

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');                                                                      //  file view mode

/***
          _______________________
         |  Dark/Bright Pixels   |
         |                       |
         |  Dark Limit [ 40]     |
         |  Bright Limit [160]   |
         |                       |
         |                   [X] |
         |_______________________|

***/

   zdialog *zd = zdialog_new(TX("Dark/Bright Pixels"),Mwin,"X",null);                  //  darkbrite dialog
   zdialog_add_widget(zd,"hbox","hbD","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labD","hbD",TX("Dark Limit"),"space=3");
   zdialog_add_widget(zd,"zspin","limD","hbD","0|255|1|0");
   zdialog_add_widget(zd,"hbox","hbB","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labB","hbB",TX("Bright Limit"),"space=3");
   zdialog_add_widget(zd,"zspin","limB","hbB","0|255|1|0");

   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

   zd_darkbrite = zd;                                                                  //  global pointer for Fpaint()

   while (true)
   {
      if (zd->zstat) break;                                                            //  dark/bright pixel blink
      flip = 0;
      Fpaint2();
      zmainsleep(0.5);                                                                 //  time pixels have image color
      flip = 1;
      Fpaint2();
      zmainsleep(0.1);                                                                 //  time pixels have blink color
   }

   zdialog_free(zd);
   zd_darkbrite = 0;
   Fpaint2();

   return;
}


//  darkbrite dialog event and completion function

int darkbrite_dialog_event(zdialog *zd, ch *event)                                     //  set dark and bright limits
{
   using namespace darkbrite;

   if (strmatch(event,"limD"))
      zdialog_fetch(zd,"limD",darklim);

   if (strmatch(event,"limB"))
      zdialog_fetch(zd,"limB",brightlim);

   return 0;
}


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

void darkbrite_paint()
{
   using namespace darkbrite;

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

   if (flip == 0) return;                                                              //  show true pixels

   for (py = 0; py < Mpxb->hh; py++)                                                   //  blink dark and bright pixels
   for (px = 0; px < Mpxb->ww; px++)
   {
      pix = PXBpix(Mpxb,px,py);
      P = PIXBRIGHT(pix);
      if (P < D) pix[0] = pix[1] = pix[2] = 255;                                       //  dark pixel >> white
      else if (P > B) pix[0] = pix[1] = pix[2] = 0;                                    //  bright pixel >> black
   }

   return;
}


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

//  monitor color and contrast test function

void m_monitor_color(GtkWidget *, ch *)
{
   ch          file[200];
   int         err;
   ch          *savecurrfile = 0;
   ch          *savegallery = 0;
   zdialog     *zd;
   ch          *message = TX("Brightness should show a gradual ramp \n"
                             "extending all the way to the edges.");

   F1_help_topic = TX("MONITOR COLOR");

   printf("m_monitor_color \n");

   if (curr_file)
      savecurrfile = zstrdup(curr_file,"monitor-color");                               //  save view mode
   if (navi::galleryname)
      savegallery = zstrdup(navi::galleryname,"monitor-color");

   viewmode('F');                                                                      //  set file view mode

   snprintf(file,200,"%s/moncolor.png",get_zimagedir());                               //  color chart file

   err = f_open(file);
   if (err) goto restore;

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

   zd = zdialog_new("check monitor",Mwin,"X",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_set_modal(zd);
   zdialog_run(zd,0,"0/0");
   zdialog_wait(zd);                                                                   //  wait for dialog complete
   zdialog_free(zd);

restore:

   Fzoom = 0;

   if (savecurrfile) {
      f_open(savecurrfile);
      zfree(savecurrfile);
   }

   if (savegallery) {
      gallery(savegallery,"init",0);
      gallery(0,"sort",-2);                                                            //  recall sort and position
      zfree(savegallery);
   }
   else gallery(topfolders[0],"init",0);

   return;
}


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

//  find all duplicated files and create corresponding gallery of duplicates

namespace duplicates_names
{
   int         thumbsize;
   int         Nfiles;
   ch          **files;
   int         Fallfiles, Fgallery;
}


//  menu function

void m_duplicates(GtkWidget *, ch *)
{
   using namespace duplicates_names;

   int  duplicates_dialog_event(zdialog *zd, ch *event);
   void duplicates_randomize();

   PIXBUF      *pxb;
   GError      *gerror;
   uint8       *pixels, *pix1;
   uint8       *pixelsii, *pixelsjj, *pixii, *pixjj;
   FILE        *fid = 0;
   zdialog     *zd;
   int         Ndups = 0;                                                              //  image file and duplicate counts
   int         thumbsize, pixdiff, pixcount;
   int         zstat, ii, jj, kk, cc, err, ndup;
   int         ww, hh, rs, px, py;
   int         trgb, diff, sumdiff;
   int         percent;
   ch          text[100], *pp;
   ch          tempfile[200], albumfile[200];

   typedef struct                                                                      //  thumbnail data
   {
      int      trgb;                                                                   //  mean RGB sum
      int      ww, hh, rs;                                                             //  pixel dimensions
      ch       *file;                                                                  //  file name
      uint8    *pixels;                                                                //  image pixels
   }  thumbdat_t;

   thumbdat_t     **thumbdat = 0;

   F1_help_topic = TX("FIND DUPLICATES");

   printf("m_duplicates \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   viewmode('G');                                                                      //  gallery view mode

   free_resources();

   //   duplicates_randomize();          1-shot test function


/***
       _______________________________________________
      |         Find Duplicate Images                 |
      |                                               |
      | (o) All files   (o) Current gallery           |
      | File count: nnnn                              |
      |                                               |
      | thumbnail size [ 64 ]  [calculate]            |
      | pixel difference [ 2 ]  pixel count [ 2 ]     |
      |                                               |
      | Thumbnails: nnnnnn nn%   Duplicates: nnn nn%  |
      |                                               |
      | /topfolder/subfolder1/subfolder2/...          |
      | imagefile.jpg                                 |
      |                                               |
      |                                 [Proceed] [X] |
      |_______________________________________________|

***/

   zd = zdialog_new(TX("Find Duplicate Images"),Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbwhere","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","allfiles","hbwhere",TX("All files"),"space=3");
   zdialog_add_widget(zd,"radio","gallery","hbwhere",TX("Current gallery"),"space=8");

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfiles","hbfiles",TX("File count:"),"space=3");
   zdialog_add_widget(zd,"label","filecount","hbfiles","0");

   zdialog_add_widget(zd,"hbox","hbthumb","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labthumb","hbthumb",TX("Thumbnail size"),"space=3");
   zdialog_add_widget(zd,"zspin","thumbsize","hbthumb","32|512|16|256","space=3");
   zdialog_add_widget(zd,"zbutton","calculate","hbthumb",TX("Calculate"),"space=5");

   zdialog_add_widget(zd,"hbox","hbdiff","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labdiff","hbdiff",TX("Pixel difference"),"space=3");
   zdialog_add_widget(zd,"zspin","pixdiff","hbdiff","1|20|1|1","space=3");
   zdialog_add_widget(zd,"label","space","hbdiff",0,"space=8");
   zdialog_add_widget(zd,"label","labsum","hbdiff",TX("Pixel count"),"space=3");
   zdialog_add_widget(zd,"zspin","pixcount","hbdiff","1|999|1|1","space=3");

   zdialog_add_widget(zd,"hbox","hbstats","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labthumbs1","hbstats",TX("Thumbnails:"),"space=3");
   zdialog_add_widget(zd,"label","thumbs","hbstats","0");
   zdialog_add_widget(zd,"label","Tpct","hbstats","0%","space=3");
   zdialog_add_widget(zd,"label","space","hbstats",0,"space=5");
   zdialog_add_widget(zd,"label","labdups1","hbstats",TX("Duplicates:"),"space=3");
   zdialog_add_widget(zd,"label","dups","hbstats","0");
   zdialog_add_widget(zd,"label","Dpct","hbstats","0%","space=3");

   zdialog_add_widget(zd,"hbox","hbfolder","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","currfolder","hbfolder"," ","space=8");
   zdialog_add_widget(zd,"hbox","hbfile","dialog");
   zdialog_add_widget(zd,"label","currfile","hbfile"," ","space=8");

   zdialog_stuff(zd,"allfiles",1);                                                     //  default all files
   Fallfiles = 1;

   files = (ch **) zmalloc(maximages * sizeof(ch *),"duplicates");

   for (ii = jj = 0; ii < Nxxrec; ii++)                                                //  count all files
   {
      pp = xxrec_tab[ii]->file;
      pp = image2thumbfile(pp);                                                        //  omit those without a thumbnail
      if (! pp) continue;
      files[jj++] = pp;
   }

   Nfiles = jj;

   snprintf(text,20,"%d",Nfiles);                                                      //  file count >> dialog
   zdialog_stuff(zd,"filecount",text);

   zdialog_run(zd,duplicates_dialog_event,"parent");                                   //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for user inputs
   if (zstat != 1) goto cleanup;                                                       //  canceled

   if (Nfiles < 2) {
      zmessageACK(Mwin,TX("<2 images"));
      goto cleanup;
   }

   zdialog_fetch(zd,"thumbsize",thumbsize);                                            //  thumbnail size to use
   zdialog_fetch(zd,"pixdiff",pixdiff);                                                //  pixel difference threshold
   zdialog_fetch(zd,"pixcount",pixcount);                                              //  pixel count threshold

   cc = Nfiles * sizeof(thumbdat_t);                                                   //  allocate memory for thumbnail data
   thumbdat = (thumbdat_t **) zmalloc(cc,"duplicates");

   NFbusy = 1;
   Fescape = 0;

   for (ii = 0; ii < Nfiles; ii++)                                                     //  screen out thumbnails not
   {                                                                                   //    matching an image file
      pp = thumb2imagefile(files[ii]);
      if (pp) zfree(pp);
      else {
         zfree(files[ii]);
         files[ii] = 0;
      }
   }

   for (ii = 0; ii < Nfiles; ii++)                                                     //  read thumbnails into memory
   {
      if (Fescape) goto cleanup;

      if (! files[ii]) continue;

      thumbdat[ii] = (thumbdat_t *) zmalloc(sizeof(thumbdat_t),"duplicates");
      thumbdat[ii]->file = files[ii];                                                  //  thumbnail file
      files[ii] = 0;

      kk = thumbsize;
      gerror = 0;                                                                      //  read thumbnail >> pixbuf
      pxb = gdk_pixbuf_new_from_file_at_size(thumbdat[ii]->file,kk,kk,&gerror);
      if (! pxb) {
         printf("*** file: %s \n %s",thumbdat[ii]->file,gerror->message);
         zfree(thumbdat[ii]->file);
         zfree(thumbdat[ii]);
         thumbdat[ii] = 0;
         continue;
      }

      ww = gdk_pixbuf_get_width(pxb);                                                  //  pixbuf dimensions
      hh = gdk_pixbuf_get_height(pxb);
      rs = gdk_pixbuf_get_rowstride(pxb);
      pixels = gdk_pixbuf_get_pixels(pxb);

      thumbdat[ii]->ww = ww;                                                           //  thumbnail dimensions
      thumbdat[ii]->hh = hh;
      thumbdat[ii]->rs = rs;
      cc = rs * hh;
      thumbdat[ii]->pixels = (uint8 *) zmalloc(cc,"duplicates");
      memcpy(thumbdat[ii]->pixels,pixels,cc);                                          //  thumbnail pixels

      trgb = 0;                                                                        //  compute mean RGB sum
      for (py = 0; py < hh; py++)
      for (px = 0; px < ww; px++) {
         pix1 = pixels + py * rs + px * 3;
         trgb += pix1[0] + pix1[1] + pix1[2];
      }
      thumbdat[ii]->trgb = trgb / ww / hh;                                             //  thumbnail mean RGB sum

      g_object_unref(pxb);

      snprintf(text,100,"%d",ii);                                                      //  stuff thumbs read into dialog
      zdialog_stuff(zd,"thumbs",text);

      percent = 100.0 * ii / Nfiles;                                                   //  and percent read
      snprintf(text,20,"%02d %c",percent,'%');
      zdialog_stuff(zd,"Tpct",text);

      zmainloop();                                                                     //  keep GTK alive
   }

   for (ii = jj = 0; ii < Nfiles; ii++)                                                //  remove empty members of thumbdat[]
      if (thumbdat[ii]) thumbdat[jj++] = thumbdat[ii];
   Nfiles = jj;                                                                        //  new count

   for (ii = 0; ii < Nfiles; ii++)                                                     //  replace thumbnail filespecs
   {                                                                                   //    with corresp. image filespecs
      if (! thumbdat[ii]) continue;
      pp = thumb2imagefile(thumbdat[ii]->file);
      zfree(thumbdat[ii]->file);
      thumbdat[ii]->file = pp;
   }

   snprintf(tempfile,200,"%s/duplicate_images",temp_folder);                           //  open file for gallery output
   fid = fopen(tempfile,"w");
   if (! fid) goto filerror;

   Ndups = 0;                                                                          //  total duplicates

   for (ii = 0; ii < Nfiles; ii++)                                                     //  loop all thumbnails ii
   {
      zmainloop();

      if (Fescape) goto cleanup;

      percent = 100.0 * (ii+1) / Nfiles;                                               //  show percent processed
      snprintf(text,20,"%02d %c",percent,'%');
      zdialog_stuff(zd,"Dpct",text);

      if (! thumbdat[ii]) continue;                                                    //  removed from list

      pp = strrchr(thumbdat[ii]->file,'/');
      if (! pp) continue;
      *pp = 0;
      zdialog_stuff(zd,"currfolder",thumbdat[ii]->file);                               //  update folder and file
      zdialog_stuff(zd,"currfile",pp+1);                                               //    in dialog
      *pp = '/';

      trgb = thumbdat[ii]->trgb;
      ww = thumbdat[ii]->ww;
      hh = thumbdat[ii]->hh;
      rs = thumbdat[ii]->rs;
      pixelsii = thumbdat[ii]->pixels;

      ndup = 0;                                                                        //  file duplicates

      for (jj = ii+1; jj < Nfiles; jj++)                                               //  loop all thumbnails jj
      {
         if (! thumbdat[jj]) continue;                                                 //  removed from list

         if (abs(trgb - thumbdat[jj]->trgb) > 1) continue;                             //  brightness not matching
         if (ww != thumbdat[jj]->ww) continue;                                         //  size not matching
         if (hh != thumbdat[jj]->hh) continue;

         pixelsjj = thumbdat[jj]->pixels;
         sumdiff = 0;

         for (py = 0; py < hh; py++)
         for (px = 0; px < ww; px++)
         {
            pixii = pixelsii + py * rs + px * 3;
            pixjj = pixelsjj + py * rs + px * 3;

            diff = abs(pixii[0] - pixjj[0])
                 + abs(pixii[1] - pixjj[1])
                 + abs(pixii[2] - pixjj[2]);
            if (diff < pixdiff) continue;                                              //  pixels match within threshold

            sumdiff++;                                                                 //  count unmatched pixels
            if (sumdiff >= pixcount) {                                                 //  if over threshold,
               py = hh; px = ww; }                                                     //    break out both loops
         }

         if (sumdiff >= pixcount) continue;                                            //  thumbnails not matching

         if (ndup == 0) {
            fprintf(fid,"%s\n",thumbdat[ii]->file);                                    //  first duplicate, output file name
            ndup++;
            Ndups++;
         }

         fprintf(fid,"%s\n",thumbdat[jj]->file);                                       //  output duplicate image file name
         zfree(thumbdat[jj]->file);                                                    //  remove from list
         zfree(thumbdat[jj]->pixels);
         zfree(thumbdat[jj]);
         thumbdat[jj] = 0;
         ndup++;
         Ndups++;

         snprintf(text,100,"%d",Ndups);                                                //  update total duplicates found
         zdialog_stuff(zd,"dups",text);

         zmainloop();
      }
   }

   fclose(fid);
   fid = 0;

   if (Ndups == 0) {
      zmessageACK(Mwin,TX("0 duplicates"));
      goto cleanup;
   }

   navi::gallerytype = SEARCH;                                                         //  generate gallery of duplicate images
   gallery(tempfile,"initF",0);
   gallery(0,"paint",0);                                                               //  position at top
   viewmode('G');

   snprintf(albumfile,200,"%s/duplicate_images",albums_folder);                        //  save search results in album
   err = cp_copy(tempfile,albumfile);                                                  //    "duplicate_images"
   if (err) zmessageACK(Mwin,strerror(err));

cleanup:

   zdialog_free(zd);

   if (fid) fclose(fid);
   fid = 0;

   if (files) {
      for (ii = 0; ii < Nfiles; ii++)
         if (files[ii]) zfree(files[ii]);
      zfree(files);
   }

   if (thumbdat) {
      for (ii = 0; ii < Nfiles; ii++) {
         if (! thumbdat[ii]) continue;
         zfree(thumbdat[ii]->file);
         zfree(thumbdat[ii]->pixels);
         zfree(thumbdat[ii]);
      }
      zfree(thumbdat);
   }

   NFbusy = 0;
   Fescape = 0;
   return;

filerror:
   zmessageACK(Mwin,TX("file error: %s"),strerror(errno));
   goto cleanup;
}


//  dialog event and completion function

int  duplicates_dialog_event(zdialog *zd, ch *event)
{
   using namespace duplicates_names;

   double      freemem, reqmem;
   ch          text[20], *pp;
   int         nn, ii, jj;

   if (strmatch(event,"allfiles"))
   {
      zdialog_fetch(zd,"allfiles",nn);
      Fallfiles = nn;
      if (! Fallfiles) return 1;

      for (ii = jj = 0; ii < Nxxrec; ii++)                                             //  count all files
      {
         pp = xxrec_tab[ii]->file;
         pp = image2thumbfile(pp);                                                     //  omit those without thumbnail
         if (! pp) continue;
         files[jj++] = pp;
      }

      Nfiles = jj;

      snprintf(text,20,"%d",Nfiles);                                                   //  file count >> dialog
      zdialog_stuff(zd,"filecount",text);
      return 1;
   }

   if (strmatch(event,"gallery"))
   {
      zdialog_fetch(zd,"gallery",nn);
      Fgallery = nn;
      if (! Fgallery) return 1;

      for (ii = jj = 0; ii < navi::Gfiles; ii++)                                       //  scan current gallery
      {
         if (navi::Gindex[ii].folder) continue;                                        //  skip folders
         pp = navi::Gindex[ii].file;
         pp = image2thumbfile(pp);                                                     //  get corresp. thumbnail file
         if (! pp) continue;
         files[jj++] = pp;                                                             //  save thumbnail
      }

      Nfiles = jj;

      snprintf(text,20,"%d",Nfiles);                                                   //  file count >> dialog
      zdialog_stuff(zd,"filecount",text);
      return 1;
   }

   if (strmatch(event,"calculate"))                                                    //  calculate thumbnail size
   {
      zd->zstat = 0;                                                                   //  keep dialog active

      freemem = availmemory() - 1000;                                                  //  free memory, MB

      for (thumbsize = 32; thumbsize <= 512; thumbsize += 8) {                         //  find largest thumbnail size
         reqmem = 0.8 * thumbsize * thumbsize * 3 * Nfiles;                            //    that probably works
         reqmem = reqmem / MEGA;
         if (reqmem > freemem) break;
      }

      thumbsize -= 8;                                                                  //  biggest size that fits
      if (thumbsize < 32) {
         zmessageACK(Mwin,TX("too many files, cannot continue"));
         return 1;
      }

      zdialog_stuff(zd,"thumbsize",thumbsize);                                         //  stuff into dialog
      return 1;
   }

   if (! zd->zstat) return 1;                                                          //  wait for user input
   if (zd->zstat != 1) Fescape = 1;                                                    //  cancel
   return 1;                                                                           //  proceed
}


//  Make small random changes to all images.
//  Used for testing and benchmarking Find Duplicates.

void duplicates_randomize()
{
   using namespace duplicates_names;

   ch       *file;
   int      px, py;
   int      ii, jj, kk;
   float    *pixel;

   for (ii = 0; ii < Nxxrec; ii++)                                                     //  loop all files
   {
      if (drandz() > 0.95) continue;                                                   //  leave 5% duplicates

      zmainloop();                                                                     //  keep GTK alive

      file = zstrdup(xxrec_tab[ii]->file,"duplicates");
      if (! file) continue;

      printf(" %d  %s \n",ii,file);                                                    //  log progress

      f_open(file);                                                                    //  open and read file
      E0pxm = PXM_load(file,1);
      if (! E0pxm) continue;

      jj = 2 + 49 * drandz();                                                          //  random 2-50 pixels

      for (kk = 0; kk < jj; kk++)
      {
         px = E0pxm->ww * drandz();                                                    //  random pixel
         py = E0pxm->hh * drandz();
         pixel = PXMpix(E0pxm,px,py);
         pixel[0] = pixel[1] = pixel[2] = 0;                                           //  RGB = black
      }

      f_save(file,"jpg",8,0,1);                                                        //  write file

      PXM_free(E0pxm);
      zfree(file);
   }

   return;
}


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

//  show resource consumption: CPU, memory, map tiles on disk

namespace resources_names
{
   double   time0;
}


//  menu function

void m_resources(GtkWidget *, ch *)
{
   using namespace resources_names;

   int  resources_dialog_event(zdialog *zd, ch *event);

   zdialog   *zd;

   F1_help_topic = TX("SHOW RESOURCES");

/***
          ___________________________________
         |          Resources                |
         |                                   |
         | Time: hh:mm:ss                    |
         | CPU time: nn.nnn seconds          |
         | Real memory: nnn MB               |
         | Maps cache: tiles: nnnn  MB: nnn  |
         | [x] Clear maps cache              |
         |                     [Sample] [X]  |
         |___________________________________|

***/

   zd = zdialog_new(TX("Resources"),Mwin,"Sample","X",null);

   zdialog_add_widget(zd,"hbox","hbtime","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtime","hbtime","hh:mm:ss","space=3");
   zdialog_add_widget(zd,"hbox","hbcpu","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcpu","hbcpu","CPU time: 1.234 seconds","space=3");
   zdialog_add_widget(zd,"hbox","hbmem","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmem","hbmem","Real memory: 123 MB","space=3");
   zdialog_add_widget(zd,"hbox","hbmaps","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmaps","hbmaps","Maps cache: tiles: 1234  MB: 123","space=3");
   zdialog_add_widget(zd,"hbox","hbclear","dialog",0,"space=3");
   zdialog_add_widget(zd,"zbutton","clear","hbclear","Clear maps cache","space=3");

   time0 = 0;

   zdialog_run(zd,resources_dialog_event);                                             //  run dialog

   zdialog_send_event(zd,"sample");                                                    //  take first sample now

   return;
}


//  dialog event and completion function

int resources_dialog_event(zdialog *zd, ch *event)
{
   using namespace resources_names;

   time_t          reptime1;
   ch              reptime2[40];
   double          time1;
   ch              text[100];
   FILE            *fid;
   int             MB, nn, bs, tbs = 0, nf = 0;

   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"sample")) zd->zstat = 1;                                        //  initial sample

   if (strmatch(event,"clear")) {
      zshell("log","rm -R ~/.cache/champlain/*");                                      //  clear maps cache
      snprintf(text,100,"maps cache: tiles: 0  MB: 0 ");
      zdialog_stuff(zd,"labmaps",text);
      return 1;
   }

   if (zd->zstat && zd->zstat != 1) {                                                  //  not [sample], kill dialog
      zdialog_free(zd);
      return 1;
   }

   zd->zstat = 0;                                                                      //  keep dialog active

   reptime1 = time(0);                                                                 //  report current time
   strncpy0(reptime2,ctime(&reptime1),40);                                             //  Day Mon dd hh:mm:ss yyyy
   reptime2[19] = 0;                                                                   //  hh:mm:ss
   snprintf(text,100,"time: %s ",reptime2);
   zdialog_stuff(zd,"labtime",text);

   time1 = CPUtime();                                                                  //  report CPU time
   snprintf(text,100,"CPU time: %.3f seconds",time1 - time0);
   time0 = time1;
   zdialog_stuff(zd,"labcpu",text);

   MB = memused();                                                                     //  get process memory
   snprintf(text,100,"real memory: %d MB",MB);
   zdialog_stuff(zd,"labmem",text);

   fid = popen("find -H ~/.cache/champlain/ -type f -printf '%b\n'","r");              //  count map tiles and space used
   if (fid) {
      while (true) {
         nn = fscanf(fid,"%d",&bs);
         if (nn == EOF) break;
         if (nn == 1) {
            nf += 1;
            tbs += bs;
         }
      }
      pclose(fid);
   }

   tbs = tbs * 0.0004883;                                                              //  512 byte blocks to megabytes

   snprintf(text,100,"maps cache: tiles: %d  MB: %d ",nf,tbs);
   zdialog_stuff(zd,"labmaps",text);

   return 1;
}


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

//  translation functions in zfuncs.cc

void m_translations(GtkWidget *, ch *)
{
   F1_help_topic = TX("TRANSLATIONS");
   translate(translations_folder);
   return;
}


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

//    DEVELOPER MENU

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

//  popup report of zmalloc() memory allocation per tag

void m_zmalloc_report(GtkWidget *, ch *)                                               //  25.1
{
   F1_help_topic = TX("DEVELOPER MENU");
   zmalloc_report(Mwin);
   return;
}


//  popup report of zmalloc() memory allocation per tag
//  report only tags showing increased allocation from prior max.

void m_zmalloc_growth(GtkWidget *, ch *)                                               //  25.1
{
   F1_help_topic = TX("DEVELOPER MENU");
   zmalloc_growth(Mwin);
   return;
}


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

//  toggle: show mouse events with popup text next to mouse pointer

void m_mouse_events(GtkWidget *, ch *)
{
   F1_help_topic = TX("DEVELOPER MENU");
   Fmousevents = 1 - Fmousevents;
   return;
}


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

//  zappcrash test - make a segment fault
//  test with: $ fotocx -d -m "zappcrash test"

void m_zappcrash_test(GtkWidget *, ch *)
{
   int   *nulladdr = 0;

   F1_help_topic = TX("DEVELOPER MENU");

   printf("*** zappcrash test \n");
   zfuncs::zappcrash_context1 = "zappcrash test";
   zfuncs::zappcrash_context2 = "zappcrash test";
   printf("zappcrash test, ref. null address: %d \n",*nulladdr);
   return;
}


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

//  audit that all F1 help topics are present in the user guide
//  and that all internal links are valid

void m_audit_userguide(GtkWidget *, ch *)                                              //  26.4
{
   static ch   sourcefolder[200] = "/home2/mico/programs/fotocx-26.4";                 //  kornelix developer only
   ch          UGfile2[200];
   cch         *file;
   ch          buff[200];
   ch          topicname[100];
   ch          *sourcetopics[500], *ugtopics[500];
   ch          *pp, *pp1, *pp2;
   int         Ntopics, Nugtopics, maxtopics = 500;
   int         cc, nn, ii, jj;
   FILE        *fid1, *fid2;

   F1_help_topic = TX("DEVELOPER MENU");
   
   printf("Audit User Guide \n");

   pp = zdialog_text1(Mwin,"source files folder",sourcefolder,40);                     //  get source files folder
   if (! pp) return;
   strncpy0(sourcefolder,pp,194);
   printf("source folder: %s \n",sourcefolder);

   snprintf(UGfile2,200,"%s/data/%s",sourcefolder,UGfile);                             //  userguide file in source folder
   printf("userguide file: %s \n",UGfile2);

   snprintf(command,200,"find %s/*.cc",sourcefolder);

   fid1 = popen(command,"r");
   if (! fid1) {
      printf("invalid source folder \n");
      return;
   }
   
   nn = 0;

   while (true)                                                                        //  find all source files *.cc
   {
      file = fgets_trim(buff,200,fid1);
      if (! file) break;
      printf("source file: %s \n",file);

      fid2 = fopen(file,"r");                                                          //  open source file
      if (! fid2) {
         printf("*** file error %s \n",strerror(errno));
         continue;
      }
      
      while (true)
      {
         pp = fgets(buff,200,fid2);                                                    //  read source file records
         if (! pp) break;
         pp = strstr(pp,"F1_help_topic");                                              //  search for "F1_help_topic"
         if (! pp) continue;
         pp1 = strchr(pp+15,'"');                                                      //  get following topic name
         if (! pp1) continue;
         pp2 = strchr(pp1+3,'"');
         if (! pp2) continue;
         cc = pp2 - pp1;
         if (cc < 2 || cc > 99) continue;
         strncpy0(topicname,pp1+1,cc);
         for (int ii = 0; ii < cc; ii++)                                               //  force uppercase  
            topicname[ii] = toupper(topicname[ii]);
         pp = TX(topicname);                                                           //  get translation
         if (! pp) continue;
         sourcetopics[nn] = (char *) zmalloc(strlen(pp)+1,"auditUG");                  //  save translation
         strcpy(sourcetopics[nn],pp);
//       printf("topic: %s  translation: %s \n",topicname,sourcetopics[nn]);
         nn++;
         if (nn == maxtopics) {
            printf("*** max %d source topics exceeded \n",maxtopics);
            return;
         }
      }
      
      fclose(fid2);
   }
   
   pclose(fid1);

   Ntopics = nn;
   printf("source file topics: %d \n",Ntopics);

   fid2 = fopen(UGfile2,"r");                                                          //  read userguide.xx file
   if (! fid2) {
      printf("*** file error %s \n",strerror(errno));
      return;
   }

   nn = 0;
   
   while (true)
   {
      pp = fgets(buff,200,fid2);                                                       //  read userguide file records
      if (! pp) break;
      pp = strstr(buff,"TOPIC:");                                                      //  find TOPIC: XXXXXXX record
      if (! pp) continue;
      pp += 6;
      while (*pp && *pp <= ' ') pp++;
      if (*pp <= ' ') continue;
      cc = strlen(pp);
      while (cc > 2 && *(pp+cc-1) <= ' ') cc--;                                        //  prevent trailing blanks
      if (cc < 2) continue;
      *(pp+cc) = 0;
      ugtopics[nn] = (char *) zmalloc(cc+1,"auditUG");
      strcpy(ugtopics[nn],pp);
//    printf("UG topic: %s \n",ugtopics[nn]);
      nn++;
      if (nn == maxtopics) {
         printf("*** max %d UG topics exceeded \n",maxtopics);
         return;
      }
   }      

   fclose(fid2);
      
   Nugtopics = nn;
   printf("userguide topics: %d \n",Nugtopics);
      
   for (ii = 0; ii < Ntopics; ii++)                                                    //  find source file topics 
   {                                                                                   //    in userguide topics
      for (jj = 0; jj < Nugtopics; jj++)
         if (strmatch(sourcetopics[ii],ugtopics[jj])) break;
      if (jj == Nugtopics)
         printf("*** source topic %s not found in userguide topics \n",sourcetopics[ii]);
   }
   
   showz_docfile(Mwin,UGfile,"validate");                                              //  audit userguide internal links
   return;
}



