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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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           create/update image index file
   m_quick_index           quick incremental index rebuild, no UI
   index_rebuild_old       use old image index file without updates
   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
                           
                           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_audit_userguide       check that all F1 help topics are present
   m_zappcrash_test        zappcrash test


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

#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 items.
//  Update the index config file and generate new image index.

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


//  index menu function

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

   void index_callbackfunc(GtkWidget *widget, int line, int pos, int kbkey);
   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 = "Folders for image files "
                            "(subfolders included automatically).";
   ch             *greet2 = "Select to add, click on X to delete.";
   ch             *greet3 = "folder for thumbnails";
   ch             *greet4 = "extra metadata items to include in index";
   ch             *greet5 = "force a full re-index of all image files";
   ch             *termmess = "Index function terminated. \n"
                              "Indexing is required for search and map functions \n"
                              "and to make thumbnail gallery pages display fast.";

   F1_help_topic = "index files";

   Plog(1,"m_index \n");

   viewmode('F');

   FfullindexReq = 0;
   
   if (Fblock("index files")) return;

/***
       ______________________________________________________________
      |                      Index Image Files                       |
      |                                                              |
      | Folders for image files (subdirs included automatically).    |
      | [Select] Select to add, click on X to delete.                |
      |  __________________________________________________________  |
      | | X  /home/<user>/Pictures                                 | |
      | | X  /home/<user>/...                                      | |
      | |                                                          | |
      | |                                                          | |
      | |                                                          | |
      | |__________________________________________________________| |
      |                                                              |
      | [Select] folder for thumbnails ____________________________  |
      | |__________________________________________________________| |
      |                                                              |
      | [Select] extra metadata items 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","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","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","Select","space=3");  //  browse thumb button
   zdialog_add_widget(zd,"label","labgreet3","hbthumb1",greet3,"space=5");
   zdialog_add_widget(zd,"hbox","hbthumb2","dialog");
   zdialog_add_widget(zd,"zentry","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","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
   textwidget_set_eventfunc(widget,index_callbackfunc);                          //    for top folders text window

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

   snprintf(sthumbfolder,200,"%s/thumbnails",get_zhomedir());                    //  default thumbnails folder
   zdialog_stuff(zd,"sthumbfolder",sthumbfolder);                                //    /home/<user>/.fotocx/thumbnails

   xmeta_keys[0] = 0;                                                            //  default no extra indexed metadata

   fid = fopen(image_folders_file,"r");                                          //  read index config file,
   if (fid) {                                                                    //    stuff data into dialog widgets
      textwidget_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 textwidget_append(widget,0," X  %s\n",buff);                       //  stuff " X  /dir1/dir2..."
      }
      fclose(fid);
   }

   zdialog_resize(zd,500,500);                                                   //  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);                                                //  index not finished
      Fblock(0);
      return;
   }

   fid = fopen(image_folders_file,"w");                                          //  open/write index config file
   if (! fid) {                                                                  //  fatal error
      zmessageACK(Mwin,"index config file: \n %s",strerror(errno));
      quitxx();
   }

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

   for (line = 0; ; line++) {
      pp = textwidget_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 config 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);                                         //  thumbnails folder >> config file

   fclose(fid);
   zdialog_free(zd);                                                             //  close dialog
   Fblock(0);

   index_rebuild(2,1);                                                           //  build image index and thumbnails

   return;
}


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

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

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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   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 = textwidget_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++;
   }

   textwidget_clear(widget);

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

   gdk_window_thaw_updates(gdkwin);
   return;
}


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

//  index dialog event and completion function

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

   int         ii, nn, yn;
   GtkWidget   *widget;
   ch          **flist, *pp, *sthumbfolder;
   zlist_t     *mlist;
   ch          *topmess = "Choose top image folders";
   ch          *thumbmess = "Choose thumbnail folder";
   ch          *xmetamess = "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++) {
         textwidget_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 items
      if (! mlist) mlist = zlist_new(0);
      nn = select_meta_keys(mlist,xmetamaxkeys,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,"userguide","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 + new files
//  Called from main() when Fotocx is started (index level from user setting)
//  Called from menu function m_index() (index level = 2)

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

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

   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;
   STATB       statB;
   double      startime;
   int         Nraw, Nimage;
   double      rawgb, imagegb;

   ch   *indexmess =  "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 =  "Thumbnails folder: %s \n"
                      "Please remove.";

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

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

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

   viewmode('F');                                                                //  24.10

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

   xxrec_old = xxrec_new = 0;                                                    //  initz.
   Xstatus = Tstatus = 0;
   zd_indexlog = 0;
   Findexvalid = 0;

   //  create log window for reporting status and statistics

   if (zd_indexlog) zdialog_free(zd_indexlog);                                   //  make dialog for output log
   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");                      //  24.20
   
   //  get current top image folders and thumbnails folder

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

   Ntop = Nthumb = 0;

   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,"index config file error: %s",strerror(errno));
            quitxx();
         }
         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);
         zfree(pp);
         if (err) quitxx();
      }

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

      Ntopfolders = 0;                                                           //  no top image folders
      indexlog("no image index: reports disabled \n");                           //  no image index
      Findexvalid = 0;
      Fblock(0);
      return;
   }

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

   if (Nthumb != 1) {                                                            //  0 or >1 thumbnail folders
      zmessageACK(Mwin,"specify 1 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);
      zfree(pp);
      if (err) quitxx();
   }

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

   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("top image folders: \n");
   for (ii = 0; ii < Ntopfolders; ii++)                                          //  log top folders
      indexlog(" %s\n",topfolders[ii]);

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

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

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

   xmeta_keys[0] = 0;
   mlist = zlist_from_file(meta_index_file);                                     //  get extra indexed metadata items
   if (mlist) {
      indexlog("extra metadata index: ");
      for (ii = 0; ii < zlist_count(mlist); ii++) {                              //  log extra items
         xmeta_keys[ii] = zlist_get(mlist,ii);
         indexlog("%s, ",xmeta_keys[ii]);
      }
      xmeta_keys[ii] = 0;
      indexlog("\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("blacklist file: %s \n",pp);                                   //  log blacklist
         blacklist[Nblacklist] = zstrdup(pp,"blacklist");
         Nblacklist++;
         if (Nblacklist == 1000) {
            Plog(0,"blacklist limit reached \n");
            break;
         }
      }

      fclose(fid);
   }
   else indexlog("no blacklist files \n");

   if (indexlev == 1) {
      indexlog("old image index: reports will omit new files \n");               //  image index has old files only
      index_rebuild_old();
      goto cleanup;                                                              //  24.70
   }

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

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

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

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

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

   while (true)
   {
      xxrec = read_xxrec_seq(ftf);                                               //  read curr. index 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,"exceeded max. images: %d",maximages);
         quitxx();
      }
   }

   indexlog("image index records found: %d \n",Nold);

   //  sort old index 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 index recs

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

   cc = maximages * sizeof(xxrec_t *);
   xxrec_new = (xxrec_t **) zmalloc(cc,"xxrec-new");                             //  "new" image index recs
   Nnew = 0;
   
   Funcbusy(+1,1);                                                               //  24.60

   for (ii = 0; ii < Ntopfolders; ii++)                                          //  loop top folders
   {
      err = find_imagefiles(topfolders[ii],1+16+32,flist,NF);                    //  find image files, recurse folders,
      if (err) {                                                                 //    omit symlinks
         zmessageACK(Mwin,"find_imagefiles() failure \n");
         quitxx();
      }

      if (Nnew + NF > maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d",maximages);
         quitxx();
      }

      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);
         compact_time(statB.st_mtime,xxrec_new[nrec]->fdate);                    //  file mod date
      }                                                                          //  remaining data left null

      if (flist) zfree(flist);
   }
   
   Funcbusy(-1);

   indexlog("image files found: %d \n",Nnew);

   //  sort new index recs in order of file name and file mod date

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

   //  create image index table in memory

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

      zfree(xxrec_tab);
   }

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

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

   nrec = orec = xrec = 0;

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

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

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

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

      else {                                                                     //  old and new index recs match
         xxrec_tab[xrec] = xxrec_old[orec];                                      //  copy old index rec to output
         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,
         if (! thumbfile_OK(xxrec_tab[xrec]->file))                              //    test and mark thumbnail
            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,"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("index updates needed: %d  thumbnails: %d \n\n",
                              index_updates,thumb_updates);

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

   Funcbusy(+1,1);
   Fescape = 0;

   index_updates = thumb_updates = thumb_deletes = 0;

   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;
      textwidget_replace(wlog,0,-1,"%d %d \n",index_updates,thumb_updates);      //  update log window
      zmainsleep(0.01);
   }

   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;
      textwidget_replace(wlog,0,-1,"%d %d \n",index_updates,thumb_updates);      //  update log window
      zmainsleep(0.01);
   }

bailout:

   while (index_thread_busy || thumb_thread_busy) zmainsleep(0.1);

   Funcbusy(-1);

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

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

   //  write updated index records to image index file

   indexlog("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("all image files, including unmounted folders: %d \n",Nxxrec);

   //  keep index 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
         indexlog("blacklist: %s \n",xxrec_tab[ii]->file);
         continue;
      }

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

   Nxxrec = jj;                                                                  //  new count

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

   //  get RAW and image file counts and megabytes

   Nraw = Nimage = 0;
   rawgb = imagegb = 0;

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

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

   //  find orphan thumbnails and delete them

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

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

   indexlog("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("thumbnails deleted: %d \n",thumb_deletes);

   if (Nxxrec) {
      Findexvalid = 2;                                                           //  image index is complete
      Findexlev = 2;                                                             //  fotocx command: index new files
      FMindexlev = 1;                                                            //  file manager: old index only
      FfullindexReq = 0;                                                         //  cancel user full index reqest
   }

   indexlog("index completed \n"                                                 //  log elapsed time
            "time: %.1f seconds \n",get_seconds() - startime);

cleanup:                                                                         //  free allocated memory

   if (Fescape) {
      zsleep(1);                                                                 //  user aborted
      Findexvalid = 0;
      Fescape = 0;
   }

   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;

   if (zd_indexlog && ! keepopen)                                                //  kill index log window
      zdialog_send_event(zd_indexlog,"exitlog");

   Fblock(0);
   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) zsleep(0.001);                //  wait for last thread
   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
   threads_completed++;
   return 0;
}


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

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

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

   int      cc, xcc, acc;
   int      NF = 0, ff, kk, fkk;
   int      ww, hh;
   int      xrec;
   ch       **files = 0, *file;
   float    flati, flongi;
   ch       xmetarec[xmetaXcc];                                                  //  max. extra metadata cc
   STATB    statB;
   xxrec_t  **xxrecs = 0, *xxrec;

   ch       *metadate = 0, *metatags = 0, *metarating = 0;
   ch       *metaww = 0, *metahh = 0, *metatitle = 0, *metadesc = 0;
   ch       *metalocation = 0, *metacountry = 0, *metalat = 0, *metalong = 0;

   ch       *kname[100] = {
            meta_date_key, meta_tags_key,                                        //  first NK keys are fixed
            meta_rating_key, meta_ww_key, meta_hh_key,
            meta_title_key, meta_description_key,
            meta_city_key, meta_country_key, meta_lati_key, meta_longi_key };
   int      NK0 = 11, NK = 11;

   ch       **kdata = 0;

   index_thread_busy = 1;

   if (! Nxxrec) goto exit_thread;

   for (kk = 0; kk < xmetamaxkeys; kk++) {                                       //  add keys for extra indexed metadata
      if (! xmeta_keys[kk]) break;
      kname[NK] = xmeta_keys[kk];
      NK++;                                                                      //  total keys
   }

   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];
      files[ff] = xxrecs[ff]->file;                                              //  image file
      ff++;
   }

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

   cc = NF * NK * sizeof(ch *);                                                  //  get space for returned key data
   kdata = (ch **) zmalloc(cc,"index_thread");

   meta_getN(files,NF,kname,kdata,NK);                                           //  get metadata for all files

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

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

      if (! regfile(file,&statB)) {
         Plog(0,"index_thread: FNF %s \n",file);
         continue;
      }

      xxrec = xxrecs[ff];

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

      metadate = kdata[fkk+0];                                                   //  metadata returned
      metatags = kdata[fkk+1];
      metarating = kdata[fkk+2];
      metaww = kdata[fkk+3];
      metahh = kdata[fkk+4];
      metatitle = kdata[fkk+5];
      metadesc = kdata[fkk+6];
      metalocation = kdata[fkk+7];
      metacountry = kdata[fkk+8];
      metalat = kdata[fkk+9];
      metalong = kdata[fkk+10];

      if (metadate && strlen(metadate) > 3)                                      //  photo date
         meta_tagdate(metadate,xxrec->pdate);

      if (metarating && strlen(metarating)) {                                    //  rating '0' - '5'
         xxrec->rating[0] = *metarating;
         xxrec->rating[1] = 0;
      }
      else strcpy(xxrec->rating,"0");                                            //  not present

      if (metaww && metahh) {
         convSI(metaww,ww);
         convSI(metahh,hh);
         if (ww > 0 && hh > 0) {
            xxrec->ww = ww;
            xxrec->hh = hh;
         }
      }

      xxrec->fsize = statB.st_size;                                              //  file size

      if (metatags && strlen(metatags))                                          //  tags
         xxrec->tags = zstrdup(metatags,"index_thread");

      if (metatitle && strlen(metatitle))                                        //  title
         xxrec->title = zstrdup(metatitle,"index_thread");

      if (metadesc && strlen(metadesc))                                          //  description
         xxrec->desc = zstrdup(metadesc,"index_thread");

      if (metalocation && strlen(metalocation))
         xxrec->location = zstrdup(metalocation,"index_thread");                 //  location (city)

      if (metacountry && strlen(metacountry))                                    //  country
         xxrec->country = zstrdup(metacountry,"index_thread");

      if (metalat && metalong) {                                                 //  geocoordinates
         flati = atofz(metalat);
         flongi = atofz(metalong);
         if (flati < -90.0 || flati > 90.0) flati = flongi = 0;
         if (flongi < -180.0 || flongi > 180.0) flati = flongi = 0;
         xxrec->flati = flati;
         xxrec->flongi = flongi;
      }

      xcc = 0;

      for (kk = NK0; kk < NK; kk++)                                              //  extra indexed metadata if any
      {
         if (! kdata[fkk+kk]) continue;
         acc = strlen(kname[kk]) + strlen(kdata[fkk+kk]) + 3;
         if (acc + xcc >= xmetaXcc-2) {                                          //  limit key and data < xmetaXcc
            Plog(0,"indexed metadata too big: %s  %s \n",kname[kk],file);
            break;
         }
         strcpy(xmetarec+xcc,kname[kk]);                                         //  construct series
         xcc += strlen(kname[kk]);                                               //    "keyname=keydata^ "
         xmetarec[xcc++] = '=';
         strcpy(xmetarec+xcc,kdata[fkk+kk]);
         xcc += strlen(kdata[fkk+kk]);
         strcpy(xmetarec+xcc,"^ ");
         xcc += 2;
      }

      if (xcc > 0) xxrec->xmeta = zstrdup(xmetarec,"index_thread");

      if (! xxrec->pdate[0])                                                     //  missing values are "null"
         strcpy(xxrec->pdate,"null");
      if (! xxrec->tags)
         xxrec->tags = zstrdup("null","index_thread");
      if (! xxrec->title)
         xxrec->title = zstrdup("null","index_thread");
      if (! xxrec->desc)
         xxrec->desc = zstrdup("null","index_thread");
      if (! xxrec->location)
         xxrec->location = zstrdup("null","index_thread");
      if (! xxrec->country)
         xxrec->country = zstrdup("null","index_thread");
      if (! xxrec->xmeta)
         xxrec->xmeta = zstrdup("null","index_thread");
   }

exit_thread:

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

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

   index_thread_busy = 0;
   return 0;
}


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

//  index log window dialog response function

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

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

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

   if (Nfuncbusy) {
      Fescape = 1;                                                               //  [kill] index process                  24.50
      zmainsleep(1);
   }
   
   zdialog_free(zd);
   zd_indexlog = 0;

   return 1;
}


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

void indexlog(ch *format, ...)                                                   //  24.70
{
   using namespace index_names;

   va_list  arglist;
   ch       text[200];

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

   textwidget_append2(wlog,0,text);                                              //  output to report window
   Plog(1,text);                                                                 //  output to log file

   return;
}


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

//  sort compare function - compare index 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;
}


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

//  quick incremental index rebuild with no user interface

void m_quick_index(GtkWidget *, ch *)
{
   ch       *galleryname = 0;

   F1_help_topic = "quick index";

   Plog(1,"m_quick_index \n");

   if (navi::galleryname && navi::gallerytype == FOLDER)                         //  save current gallery
      galleryname = zstrdup(navi::galleryname,"quick-index");

   index_rebuild(2,0);                                                           //  run incremental index

   if (galleryname) {
      gallery(galleryname,"init",0);                                             //  restore gallery and scroll position
      zfree(galleryname);
      gallery_memory("get");
      gallery(0,"paint",-1);
   }

   return;
}


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

//  Rebuild image index table from existing image index file
//    without searching for new and modified files.
//  The only caller is index_rebuild(). 

void index_rebuild_old()
{
   using namespace index_names;

   int      ftf, cc, rec, rec2, Ntab;

   Findexvalid = 0;

   //  read image index file and build table of index records

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

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

   //  sort index 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) Findexvalid = 1;                                                  //  index OK but missing new files
   return;
}


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

//  user preferences and settings dialog

namespace settings
{
   void  get_raw_commands(zdialog *zd);

   ch       *startopt[8][2] = {
               { "recent", "Recent Files Gallery" },                             //  fotocx startup view options
               { "newest", "Newest Files Gallery" },
               { "specG",  "Specific Gallery" },
               { "album",  "Album Gallery" },
               { "prevG",  "Previous Gallery" },
               { "prevF",  "Previous File" },
               { "specF",  "Specific File" },
               { "blank",  "Blank Window" } };
   int      NSO = 8;

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

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

   F1_help_topic = "settings";

   Plog(1,"m_settings \n");
   
   if (Fblock("settings")) return;

/***
       _____________________________________________________________________
      |                Preferences and Settings                             |
      |                                                                     |
      | 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]                    |
      |  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 ] diameter in pixels                         |
      |  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>        |
      |  Log Level      |  (o) errors only  (o) +info  (o) +dialog inputs   |
      |  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);
   zdialog_add_widget(zd,"scrwin","swmain","dialog",0,"expand");                 //  scrolling window

   //  left and right vertical boxes
   zdialog_add_widget(zd,"hbox","hb1","swmain");
   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");

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

   //  background colors
   zdialog_add_widget(zd,"label","background colors","vb1","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","Menu Style");
   zdialog_add_widget(zd,"hbox","hbms","vb2");
   zdialog_add_widget(zd,"radio","icons","hbms","Icons","space=3");
   zdialog_add_widget(zd,"radio","text","hbms","Text","space=3");
   zdialog_add_widget(zd,"radio","both","hbms","Both","space=3");
   zdialog_add_widget(zd,"label","space","hbms",0,"space=8");
   zdialog_add_widget(zd,"label","labis","hbms","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","Menu Colors");
   zdialog_add_widget(zd,"hbox","hbmc","vb2");
   zdialog_add_widget(zd,"label","labmb","hbmc","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","Background","space=8");
   zdialog_add_widget(zd,"colorbutt","MBrgb","hbmc");

   //  dialog font
   zdialog_add_widget(zd,"label","dialog font","vb1","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","Choose","space=5");

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

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

   //  JPEG save quality
   zdialog_add_widget(zd,"label","jpeg qual","vb1","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","quality level (70+)","space=10");

   //  TIFF compression method
   zdialog_add_widget(zd,"label","tiff comp","vb1","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","compression method","space=10");

   //  curve edit node separation
   zdialog_add_widget(zd,"label","curve node","vb1","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","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","diameter in pixels","space=10");

   //  overlay text line length range
   zdialog_add_widget(zd,"label","overlay text","vb1","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","text on image line wrap range","space=10");

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

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

   //  index level
   zdialog_add_widget(zd,"label","index levels","vb1","Index Level");
   zdialog_add_widget(zd,"hbox","hbxlev","vb2");
   zdialog_add_widget(zd,"zspin","indexlev","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|1","size=3");
   zdialog_add_widget(zd,"label","labfmxlev2","hbxlev","$ fotocx <filename> (1)","space=5");

   //  log level
   zdialog_add_widget(zd,"label","log level","vb1","Log Level");
   zdialog_add_widget(zd,"hbox","hbloglev","vb2");
   zdialog_add_widget(zd,"radio","logerrs","hbloglev","errors only");
   zdialog_add_widget(zd,"radio","loginfo","hbloglev","+ info messages","space=6");
   zdialog_add_widget(zd,"radio","loginputs","hbloglev","+ dialog inputs","space=6");

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

//  stuff dialog fields with current settings

   for (ii = 0; ii < NSO; ii++)
      zdialog_stuff(zd,"startopt",startopt[ii][1]);                              //  startup view option list

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

   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",dialog_font);                                         //  curr. dialog font

   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,"indexlev",Findexlev);                                       //  index level, always
   zdialog_stuff(zd,"fmindexlev",FMindexlev);                                    //  index level, file manager call

   zdialog_stuff(zd,"logerrs",0);                                                //  message log level
   zdialog_stuff(zd,"loginfo",0);
   zdialog_stuff(zd,"loginputs",0);
   if (zfuncs::Floglevel == 0) zdialog_stuff(zd,"logerrs",1);
   if (zfuncs::Floglevel == 1) zdialog_stuff(zd,"loginfo",1);
   if (zfuncs::Floglevel == 2) zdialog_stuff(zd,"loginputs",1);

   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)                                //  simplifications
{
   using namespace settings;

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

   if (zd->zstat) return 1;                                                      //  [OK] or [x]

   if (strmatch(event,"startopt"))                                               //  set startup view
   {
      zdialog_fetch(zd,"startopt",temp,200);
      for (ii = 0; ii < NSO; ii++) {
         if (strmatch(temp,startopt[ii][1]))
            zstrcopy(startdisplay,startopt[ii][0],"settings");
      }
   }

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

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

      if (strmatch(startdisplay,"album")) {                                      //  specific album
         pp = zgetfile("Select startup album",MWIN,"file",albums_folder);
         if (! pp) return 1;
         if (! strstr(pp,albums_folder)) {
            zmessageACK(Mwin,"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("select font",MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),temp);
      gtk_dialog_run(GTK_DIALOG(font_dialog));
      pp = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(font_dialog));
      gtk_widget_destroy(font_dialog);
      if (! pp) return 1;
      zdialog_stuff(zd,"font",pp);
      zsetfont(pp);
      dialog_font = zstrdup(pp,"settings");
      g_free(pp);
   }

   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("indexlev fmindexlev",event))                                      //  index level,
   {
      zdialog_fetch(zd,"indexlev",Findexlev);                                    //  fotocx started directly
      zdialog_fetch(zd,"fmindexlev",FMindexlev);                                 //  fotocx started via file manager
      if (Findexlev < FMindexlev) Findexlev = FMindexlev;                        //  disallow F < FM
   }

   if (strstr("logerrs loginfo loginputs",event))                                //  log message level
   {
      zdialog_fetch(zd,"logerrs",nn);
      if (nn == 1) zfuncs::Floglevel = 0;                                        //  0/1/2 = errors/info/inputs
      zdialog_fetch(zd,"loginfo",nn);
      if (nn == 1) zfuncs::Floglevel = 1;
      zdialog_fetch(zd,"loginputs",nn);
      if (nn == 1) zfuncs::Floglevel = 2;
   }

   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                24.30
      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];

   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 = 22;                                                   //  reserved shortcuts (hard coded)
   ch          *reserved[22] = {
      "K", "H", "+", "=", "-", "Z", "F1", "F2", "F3", "F4", "F10", "F11",
      "Escape", "Delete", "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 = "KB shortcuts";

   Plog(1,"m_KB_shortcuts \n");

/***
          ____________________________________________
         |          Keyboard Shortcuts                |
         |                                            |
         | Reserved Shortcuts                         |
         |  K              KB Shortcuts               |
         |  H              User Guide, Context Help   |
         |  +              Zoom-in                    |
         |  -              Zoom-out                   |
         |  Z              Toggle 1x / fit window     |
         |  F1             User Guide, Context Help   |
         |  F2/F3/F4       File/Gallery/Map View      |
         |  F10/F11        Full Screen / no menus     |
         |  Escape         Quit dialog, Quit Fotocx   |
         |  Delete         Delete/Trash file          |
         |  Arrow keys     Previous/Next file         |
         |  Home/End       Gallery start/end          |
         |  Page keys      Gallery page up/down       |
         |                                            |
         | Custom Shortcuts                           |
         |  F              File View                  |
         |  G              Gallery View               |
         |  M              Map View                   |
         |  R              Rename                     |
         |  T              Trim/Rotate                |
         | ...             ...                        |
         |                                            |
         |                                 [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

   textwidget_append(widget,1,"Reserved Shortcuts \n");
   textwidget_append(widget,0," K           KB shortcuts \n");
   textwidget_append(widget,0," H           User Guide, Context Help \n");
   textwidget_append(widget,0," +           Zoom-in \n");
   textwidget_append(widget,0," -           Zoom-out \n");
   textwidget_append(widget,0," Z           Toggle 1x / fit window \n");
   textwidget_append(widget,0," F1          User Guide, Context Help \n");
   textwidget_append(widget,0," F2/F3/F4    File/Gallery/Map View \n");
   textwidget_append(widget,0," F10/F11     Full Screen / no menus \n");
   textwidget_append(widget,0," Escape      Quit dialog / Quit Fotocx \n");
   textwidget_append(widget,0," Delete      Delete/Trash file \n");
   textwidget_append(widget,0," Arrow keys  Previous/Next file \n");
   textwidget_append(widget,0," Page keys   Gallery page up/down \n");
   textwidget_append(widget,0," Home/End    Gallery start/end \n");
   textwidget_append(widget,0,"\n");
   textwidget_append(widget,1,"Custom Shortcuts \n");

   for (ii = 0; ii < Nkbsu; ii++)                                                //  list custom shortcuts
      textwidget_append(widget,0," %-14s %s \n",
                     kbsutab[ii].key, 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;

   void KBshorts_callbackfunc1(GtkWidget *widget, int line, int pos, int kbkey);
   void KBshorts_callbackfunc2(GtkWidget *widget, int line, int pos, int kbkey);
   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                                  |
         |_________________________________________________________________|
         | Alt+G           Grid Settings           |  Blur                 |
         | Alt+U           Undo                    |  Bookmarks            |
         | Alt+R           Redo                    |  Captions             |
         | T               Trim/Rotate             |  Color Depth          |
         | V               View Main               |  Color Sat            |
         | Ctrl+Shift+V    View All                |  Copy to Cache        |
         | ...             ...                     |  ...                  |
         |_________________________________________|_______________________|
         |                                                                 |
         | shortcut key: (enter key)  (no selection)                       |
         |                                                                 |
         |                                         [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","shortcut key:","space=5");
   zdialog_add_widget(zd,"label","shortkey","hbshort","(enter key)","size=10");
   zdialog_add_widget(zd,"label","shortfunc","hbshort","(no selection)","space=5");

   for (ii = 0; ii < Nkbsu; ii++) {                                              //  copy current shortcuts list
      kbsutab2[ii].key = zstrdup(kbsutab[ii].key,"KB_shortcuts");
      kbsutab2[ii].menu = zstrdup(kbsutab[ii].menu,"KB_shortcuts");
   }
   Nkbsu2 = Nkbsu;

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

   textwidget_set_eventfunc(widget,KBshorts_callbackfunc1);                      //  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
   textwidget_clear(widget);

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

   textwidget_set_eventfunc(widget,KBshorts_callbackfunc2);                      //  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"

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

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

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txline = textwidget_line(widget,line,1);                                      //  get clicked line
   if (! txline || ! *txline) return;
   textwidget_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;
}


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

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

   ch       *txline;

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

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

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


//  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       keyname[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(keyname,"F1");
      keyname[1] += ii;
   }

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

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

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

   zdialog_stuff(zd,"shortkey",keyname);                                         //  stuff key name into dialog
   zdialog_stuff(zd,"shortfunc","(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);
   void KBshorts_callbackfunc2(GtkWidget *widget, int line, int pos, int kbkey);

   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,"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
      textwidget_clear(widget);

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  show updated shortcuts in dialog
         textwidget_append2(widget,0,"%-14s %s \n",
                              kbsutab2[ii].key,kbsutab2[ii].menu);
      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
      textwidget_clear(widget);

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

      zdialog_stuff(zd,"shortkey","");                                           //  clear entered key and menu
      zdialog_stuff(zd,"shortfunc","(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.
//  zdialog completion button KB shortcuts removed                               //  24.20

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) {
         Plog(0,"Reserved KB shortcut ignored: %s \n",key);
         continue;
      }

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

      kbsutab[ii].key = key;                                                     //  add to shortcuts table
      kbsutab[ii].menu = menu;
      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;

   Plog(1,"m_RGB_hist \n");

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

   if (! curr_file) {
      zmessageACK(Mwin,"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 = "RGB histogram";

   zd = zdialog_new("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) {
            Plog(1,"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 = "Drag mouse on image. \n"
                 "Left click to cancel.";

   F1_help_topic = "magnify image";

   Plog(1,"m_magnify \n");

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

***/

   if (! curr_file) {
      zmessageACK(Mwin,"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","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","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 = "Click image to select pixels";

   F1_help_topic = "measure image";

   Plog(1,"m_measure_image \n");

   if (! curr_file) {
      zmessageACK(Mwin,"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 = "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 = "show RGB";

   Plog(1,"m_show_RGB \n");

   if (! curr_file) {
      zmessageACK(Mwin,"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
      curr_file_bpc = f_load_bpc;
   }

/***
    ____________________________________________
   |                                            |
   |  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,"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
      curr_file_bpc = f_load_bpc;
   }

   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 = "grid settings";

   Plog(1,"m_grid_settings \n");

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

/***
       ____________________________________________
      |             Grid Settings                  |
      |                                            |
      | x-spacing [____]    y-spacing [____]       |
      |  x-count  [____]     y-count  [____]       |
      | x-enable  [_]       y-enable  [_]          |
      |                                            |
      | x-offset =================[]=============  |
      | y-offset ==============[]================  |
      |                                            |
      |                                       [OK] |
      |____________________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hb0","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb0",0,"homog");
   zdialog_add_widget(zd,"vbox","vbspace","hb0",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb3","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb0",0,"homog");

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

   zdialog_add_widget(zd,"zspin","spacex","vb2","20|200|1|50","space=2");
   zdialog_add_widget(zd,"zspin","countx","vb2","0|100|1|2","space=2");
   zdialog_add_widget(zd,"check","enablex","vb2",0);

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

   zdialog_add_widget(zd,"zspin","spacey","vb4","20|200|1|50");
   zdialog_add_widget(zd,"zspin","county","vb4","0|100|1|2");
   zdialog_add_widget(zd,"check","enabley","vb4",0);

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

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

   zdialog_stuff(zd,"enablex",gridsettings[GX]);                                 //  current settings >> dialog widgets
   zdialog_stuff(zd,"enabley",gridsettings[GY]);
   zdialog_stuff(zd,"spacex",gridsettings[GXS]);
   zdialog_stuff(zd,"spacey",gridsettings[GYS]);
   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)
{
   if (zd->zstat)                                                                //  done or cancel
   {
      if (zd->zstat != 1) return 1;
      if (gridsettings[GX] || gridsettings[GY])
         gridsettings[GON] = 1;
      else gridsettings[GON] = 0;
      Fpaint2();
      return 1;
   }

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

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

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

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

   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[GX] || gridsettings[GY])                                     //  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 = "grid settings";

   Plog(1,"m_toggle_grid \n");

   gridsettings[GON] = 1 - gridsettings[GON];
   if (gridsettings[GON])
      if (! gridsettings[GX] && ! gridsettings[GY])                              //  if grid on and x/y both off,
         gridsettings[GX] = gridsettings[GY] = 1;                                //    set both grids on
   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 = "line color";

   Plog(1,"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","Black","space=3");               //  add radio button per color
   zdialog_add_widget(zd,"radio","White","hb1","White","space=3");
   zdialog_add_widget(zd,"radio","Red","hb1","Red","space=3");
   zdialog_add_widget(zd,"radio","Green","hb1","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 = "dark/bright pixels";

   Plog(1,"m_darkbrite \n");

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

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

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

***/

   zdialog *zd = zdialog_new("Dark/Bright Pixels",Mwin," X ",null);              //  darkbrite dialog                      24.20
   zdialog_add_widget(zd,"hbox","hbD","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labD","hbD","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","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               24.20
      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                   24.20
      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 = "Brightness should show a gradual ramp \n"
                          "extending all the way to the edges.";

   F1_help_topic = "monitor color";

   Plog(1,"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
   }  Tdata_t;

   Tdata_t     **Tdata = 0;

   F1_help_topic = "find duplicates";

   Plog(1,"m_duplicates \n");

   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      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("Find Duplicate Images",Mwin,"Proceed"," X ",null);

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

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfiles","hbfiles","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","Thumbnail size","space=3");
   zdialog_add_widget(zd,"zspin","thumbsize","hbthumb","32|512|16|256","space=3");
   zdialog_add_widget(zd,"zbutton","calculate","hbthumb","Calculate","space=5");

   zdialog_add_widget(zd,"hbox","hbdiff","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labdiff","hbdiff","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","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","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","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," <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(Tdata_t);                                                //  allocate memory for thumbnail data
   Tdata = (Tdata_t **) zmalloc(cc,"duplicates");

   Nfuncbusy = 1;                                                                //  24.10
   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;

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

      kk = thumbsize;
      gerror = 0;                                                                //  read thumbnail >> pixbuf
      pxb = gdk_pixbuf_new_from_file_at_size(Tdata[ii]->file,kk,kk,&gerror);
      if (! pxb) {
         Plog(0,"file: %s \n %s",Tdata[ii]->file,gerror->message);
         zfree(Tdata[ii]->file);
         zfree(Tdata[ii]);
         Tdata[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);

      Tdata[ii]->ww = ww;                                                        //  thumbnail dimensions
      Tdata[ii]->hh = hh;
      Tdata[ii]->rs = rs;
      cc = rs * hh;
      Tdata[ii]->pixels = (uint8 *) zmalloc(cc,"duplicates");
      memcpy(Tdata[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];
      }
      Tdata[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(10);                                                             //  keep GTK alive
   }

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

   for (ii = 0; ii < Nfiles; ii++)                                               //  replace thumbnail filespecs
   {                                                                             //    with corresp. image filespecs
      if (! Tdata[ii]) continue;
      pp = thumb2imagefile(Tdata[ii]->file);
      zfree(Tdata[ii]->file);
      Tdata[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(10);

      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 (! Tdata[ii]) continue;                                                 //  removed from list

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

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

      ndup = 0;                                                                  //  file duplicates

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

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

         pixelsjj = Tdata[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",Tdata[ii]->file);                                 //  first duplicate, output file name
            ndup++;
            Ndups++;
         }

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

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

         zmainloop(10);
      }
   }

   fclose(fid);
   fid = 0;

   if (Ndups == 0) {
      zmessageACK(Mwin,"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 (Tdata) {
      for (ii = 0; ii < Nfiles; ii++) {
         if (! Tdata[ii]) continue;
         zfree(Tdata[ii]->file);
         zfree(Tdata[ii]->pixels);
         zfree(Tdata[ii]);
      }
      zfree(Tdata);
   }

   Nfuncbusy = 0;                                                                //  bugfix   24.10
   Fescape = 0;
   return;

filerror:
   zmessageACK(Mwin,"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,"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;

      Plog(1," %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 *)                                              //  rewrite  24.20
{
   using namespace resources_names;
   
   int  resources_dialog_event(zdialog *zd, ch *event);

   zdialog   *zd;

   F1_help_topic = "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("Resources",Mwin,"Sample"," X ",0);
   
   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;
}


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

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

void m_zmalloc_report(GtkWidget *, ch *)
{
   static zdialog    *zdpop = 0;

   if (! zdpop || ! zdialog_valid(zdpop,"zmalloc"))                              //  open new popup report
      zdpop = popup_report_open("zmalloc",Mwin,500,400,0,0,0,"X",0);
   zmalloc_report(zdpop);                                                        //  generate report
   popup_report_bottom(zdpop);
   return;
}


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

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

void m_zmalloc_growth(GtkWidget *, ch *)
{
   static zdialog    *zdpop = 0;

   if (! zdpop || ! zdialog_valid(zdpop,"zmalloc growth"))                       //  open new popup report
      zdpop = popup_report_open("zmalloc growth",Mwin,500,400,0,0,0,"X",0);
   zmalloc_growth(zdpop);                                                        //  generate report
   popup_report_bottom(zdpop);
   return;
}


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

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

void m_mouse_events(GtkWidget *, ch *)
{
   Fmousevents = 1 - Fmousevents;
   return;
}


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

//  audit that all F1 help topics are present in user guide

void m_audit_userguide(GtkWidget *, ch *)
{
   showz_docfile(Mwin,"userguide","validate");
   return;
}


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

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

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

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