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

   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 - folder navigation and thumbnail gallery functions

   gallery()                  create/update/search/display an image gallery

   navi::
      gallery_comp            gallery sort compare function
      filename_comp           gallery sort file name compare function
      gallery_paint           paint thumbnail gallery, resizable thumbnails
      gallery_paintmeta       same, metadata report from search function
      gallery_paintmeta2      same, thumbnails with basic metadata
      dir_filecount           paint - get folder subdir and image file counts
      draw_text               paint - write text over thumbnail images
      gallery_navibutts       add folder navigation buttons
      menufuncx               gallery menu: sort/zoom/scroll ...
      gallery_scroll          scroll gallery in rows and pages
      navibutt_clicked        open new gallery from clicked folder
      newtop                  open new gallery from clicked TOP entry
      newalbum                open new gallery from clicked album entry
      mouse_event             process gallery thumbnail clicks
      gallery_dragfile        process thumbnail dragged
      gallery_dropfile        process thumbnail dropped (add new file, arrange album)
      KBaction                process gallery KB actions: zoom, page, top, end

   gallery_memory             save/recall gallery sort and position
   set_gwin_title             update the gallery window title bar
   prev_next_file             get previous/next file from curr_file in current gallery
   prev_next_gallery          get gallery's previous or next gallery
   file_position              get gallery file position

   image_file_type            determine file type (folder, image, RAW, thumbnail, other)
   thumb2imagefile            get corresponding image file for given thumbnail file
   image2thumbfile            get corresponding thumbnail file for given image file
   thumbfile_OK               check if thumbnail file missing or stale
   get_folder_pixbuf          get 'folder' pixbuf image
   get_broken_pixbuf          get 'broken file' pixbuf image
   update_thumbfile           refresh thumbnail file if missing or stale
   thumbfile_set_mod_time     set thumbnail file mod time from image file
   delete_thumbfile           delete thumbnail disk file
   get_thumb_pixbuf           get thumbnail pixbuf for given image file

   gallery_popimage           popup a larger image from a clicked thumbnail
   select_files1              single gallery file selection function and dialog
   select_files               multiple gallery file selection function and dialog

   m_thumbview                gallery thumbnail view
   m_metaview                 gallery metadata view
   m_recentfiles              show gallery of recently viewed files
   add_recent_file            add an image file to the list of recent files
   m_newfiles                 show gallery of newest image files
   m_gallery_sort             sort gallery by file, date, size ...
   m_gallery_screen           screen gallery for newest, oldest, text match
   m_current_folder           set gallery to folder of current image file
   m_recent_folders           set gallery to selected recently used folder
   add_recent_folder          add current folder to top of recent folder list
   m_folder_tree              folder tree view, expand/contract, click for gallery view
   m_select_files             select files for album and batch functions
   m_rename_folder            rename the current folder
   m_add_subfolder            add subfolder to current gallery
   m_bookmarks                goto selected bookmark, edit bookmarks
   m_edit_bookmarks           edit bookmarks

   m_thumbframe               set the video frame for a video thumbnail
   m_show_hidden              dummy menu for KB shortcut "Show Hidden Files"
   m_current_album            dummy menu for KB shortcut "Current Album"

   popup_image                show an image in a small popup window

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

#define EX extern                                                                //  disable extern declarations
#include "fotocx.h"                                                              //  (variables in fotocx.h are refs)

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

/***                                                gallery index in fotocx.h
      typedef struct {                              current gallery file list in memory
         ch       *file;                            /folder.../filename
         int      folder;                           flag, file is a folder
         ch       fdate[16];                        file date: yyyymmddhhmmss
         ch       pdate[16];                        photo date: yyyymmddhhmmss
         int      fsize;                            file size, bytes
         int      psize;                            image size, pixels
         ch       *mdata;                           metadata (text)
      }  Gindex_t;
***/

namespace navi
{
   #define maxgallerylevs 60                                                     //  max gallery navigation levels
   #define TEXTWIN GTK_TEXT_WINDOW_TEXT                                          //  GDK window of GTK text view
   #define NEVER GTK_POLICY_NEVER
   #define ALWAYS GTK_POLICY_ALWAYS

   #define     thumbxx 5                                                         //  thumbx array size
   int         thumbx[5] = { 512, 360, 256, 180, 128 };                          //  thumbnail sizes
   int         fontx[5] =  {   0,  -1,  -1,  -2,  -3  };                         //  font size wrt appfont
   Gindex_t    *Gindex = 0;                                                      //  gallery index
   int         GFlock = 0;                                                       //  gallery index lock
   int         TClock = 0;                                                       //  thumbnail cache lock
   int         Gfiles = 0;                                                       //  gallery files (incl. subfolders)
   int         Gfolders = 0;                                                     //  gallery subfolder count
   int         Gimages = 0;                                                      //  gallery image file count
   int         preload_trigger = 0;                                              //  kick preload_thumb() thread
   int         preload_start = 0;                                                //  1st file in gallery to check
   int         Gmdrows = 0;                                                      //  text rows in metadata list
   ch          *galleryname = 0;                                                 //  physical folder or file list filename
   GTYPE       gallerytype;                                                      //  gallery type: folder, recent, etc.
   GSORT       gallerysort;                                                      //  gallery sort: f.name, f.date, p.date
   GSEQ        galleryseq;                                                       //  gallery sort: ascending/descending
   int         galleryposn = 0;                                                  //  scroll-to file position (Nth)
   int         Fthumbview = 1;                                                   //  gallery view mode 1/2 = normal/metadata
   GtkWidget   *gallerybutt[60];                                                 //  gallery navi buttons [aaa] [bbb] ...
   ch          *gallerypath[60];                                                 //  corresp. folder names  aaa bbb ...
   GtkWidget   *gallerylabel = 0;                                                //  gallery label (album name ...)
   int         gallerypainted = 0;                                               //  gallery is finished painting
   int         xwinW, xwinH;                                                     //  gallery window current size
   int         thumbsize = 256;                                                  //  initial thumbnail image size
   int         thumbW, thumbH;                                                   //  gallery window thumbnail cell size
   int         fontsize = 10;                                                    //  font size for gallery text
   int         texthh;                                                           //  vertical space req. for metadata text
   int         xrows, xcols;                                                     //  gallery window thumbnail rows, cols
   int         margin = 5;                                                       //  cell margin from left and top edge
   int         genthumbs = 0;                                                    //  count newly generated thumbnails
   int         scrollposn = 0;                                                   //  scroll position
   int         maxscroll;                                                        //  max. scroll position
   ch          *drag_file = 0;                                                   //  selected thumbnail (file)
   int         drag_posn = -1;                                                   //  drag from position
   int         gallery_scrollgoal = -1;                                          //  gallery scroll goal position
   int         gallery_scrollspeed = 0;                                          //  gallery scroll speed pixels/second
   
   zlist_t     *ZLrecent_folders = 0;                                            //  list of recently used folders         24.60

   //  private functions
   int    gallery_comp(ch *rec1, ch *rec2);                                      //  gallery record compare for sort options
   int    filename_comp(ch *file1, ch *file2);                                   //  special file name compare function
   int    gallery_paint(GtkWidget *, cairo_t *);                                 //  gallery window paint, resizable thumbnails
   int    gallery_paintmeta(GtkWidget *Gdrawin, cairo_t *cr);                    //  same, metadata report from search func.
   int    gallery_paintmeta2(GtkWidget *Gdrawin, cairo_t *cr);                   //  same, thumbnails with basic metadata
   void   dir_filecount(ch *dirname, int &ndir, int &nfil);                      //  get subdir and image file counts
   void   draw_text(cairo_t *cr, ch *text, int x, int y, int ww);                //  draw text in gallery window
   void   gallery_navibutts();                                                   //  create navigation buttons in top panel
   void   menufuncx(GtkWidget *, ch *menu);                                      //  function for gallery window buttons
   void   gallery_scroll(int position, int speed);                               //  start gallery slow scroll to position
   int    gallery_scrollfunc(void *);                                            //  gallery scroll timer function
   void   navibutt_clicked(GtkWidget *, int *level);                             //  set gallery via click navigation button
   void   newtop(GtkWidget *widget, GdkEventButton *event);                      //  function for [TOP] button
   void   newtop_menu_event(GtkWidget *, ch *);                                  //  [TOP] menu response function
   void   newalbum(GtkWidget *widget, GdkEventButton *event);                    //  function for [Album] button
   void   newalbum_menu_event(GtkWidget *, ch *);                                //  [Album] menu response function
   int    mouse_event(GtkWidget *widget, GdkEvent *event, void *);               //  gallery window mouse event function
   ch   * gallery_dragfile();                                                    //  start drag-drop, set the file
   void   gallery_dropfile(int mousex, int mousey, ch *file);                    //  accept drag-drop file at position
   int    KBpress(GtkWidget *, GdkEventKey *, void *);                           //  gallery window key press event
}

using namespace zfuncs;
using namespace navi;


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

   ch * gallery(ch *filez, ch *action, int Nth)

   public function to create/update image gallery (thumbnail window)

   Make a scrolling window of thumbnails for a folder or list of files
   Handle window buttons (up row, down page, open file, etc.)
   Call external functions in response to thumbnail mouse-clicks.

   filez: image file or folder, or file with list of image files

      filez    action   Nth
      -----    ------   ---
      file     init      *       folder gallery, file name sort (folders first, last vers. option)
      file     initF     *       file list gallery, no sort

      *        sort     -2       sort and position via gallery_memory()
      *        sort     -1       sort and position from current params

      file     paint     N       paint, position = file (use N if valid for file)
      0        paint     N       paint, position = N
      0        paint    -1       paint, no change in position

      file     insert    N       add file to gallery at position N (folder or unsorted album)
      *        delete    N       delete Nth file in gallery list
      *        get       N       return file N in gallery list or null if N > last
      file     update    *       update gallery index (Gindex[*]) from image index in memory

   Returned values:
      get:  filespec             returned filespec is subject for zfree().
      getR: filespec reference   no zfree() needed

   thumbnail click functions:
      gallery_Lclick_func()                        default function (open file)
      gallery_Rclick_popup()                       default function (popup menu)
      select_files_Lclick_func()                   select_files active
      select_files_Rclick_func()                   select_files active
      edit_bookmarks_Lclick_func                   edit bookmarks active

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

ch * gallery(ch *filez, ch *action, int Nth)
{
   ch          *file, *file2;
   ch          *pp, *pp1, *pp2;
   ch          buff[XFCC];
   ch          **Flist = 0;
   xxrec_t     *xxrec;
   static int  ftf = 1;
   int         err, ii, jj, kk;
   int         cc, cc1, cc2, fposn;
   int         NF, flags;
   FTYPE       ftype;
   FILE        *fid;

   while (! resource_lock(GFlock)) zsleep(0.0001);                               //  lock gallery file list

   if (ftf) {
      ftf = 0;
      cc = maxgallery * sizeof(Gindex_t);
      Gindex = (Gindex_t *) zmalloc(cc,"gallery");                               //  allocate gallery index
      Gfiles = Gfolders = Gimages = 0;                                           //  no files yet
   }

   if (strmatchN(action,"init",4))                                               //  init or initF
   {
      if (! filez && gallerytype == FOLDER) filez = galleryname;                 //  null file: refresh current gallery
      if (! filez) goto return0;

      zstrcopy(galleryname,filez,"gallery");                                     //  new gallery name

      NF = Gfiles;                                                               //  current file count
      Gfiles = Gfolders = Gimages = 0;                                           //  no new files yet

      for (ii = 0; ii < NF; ii++) {                                              //  free prior gallery index
         if (Gindex[ii].file) zfree(Gindex[ii].file);                            //  memory leak
         if (Gindex[ii].mdata1) zfree(Gindex[ii].mdata1);
         if (Gindex[ii].mdata2) zfree(Gindex[ii].mdata2);
         Gindex[ii].file = Gindex[ii].mdata1 = Gindex[ii].mdata2 = 0;
         Gindex[ii].folder = 0;
      }
   }

   if (strmatch(action,"init"))                                                  //  initz. from given file or folder
   {
      if (! dirfile(galleryname)) {                                              //  bad folder? (may be member file)
         pp = (ch *) strrchr(galleryname,'/');                                   //  try parent folder
         if (pp) *pp = 0;
         if (! dirfile(galleryname)) goto return0;                               //  give up, empty file list
      }

      cc = strlen(galleryname) - 1;                                              //  remove trailing '/'
      if (cc > 1 && galleryname[cc] == '/') galleryname[cc] = 0;                 //  but not if root folder '/'

      flags = 1 + 8;                                                             //  images + folders, one level
      if (Fshowhidden) flags += 4;                                               //  include hidden files

      err = find_imagefiles(galleryname,flags,Flist,NF);                         //  find all image files in folder
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         goto return0;
      }

      for (ii = 0; ii < NF; ii++)
      {
         file = Flist[ii];

         pp = strrchr(file,'/');
         if (pp && (strmatch(pp,"/.") || strmatch(pp,"/.."))) {                  //  omit folders  /.  and  /..
            zfree(file);
            continue;
         }

         if (Gfiles == maxgallery) {                                             //  too many files
            zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);
            break;
         }

         ftype = image_file_type(file);

         if (ftype == FDIR)                                                      //  subfolder
         {
            Gindex[Gfiles].file = file;                                          //  add to gallery index
            Gindex[Gfiles].folder = 1;                                           //  mark folder 
            Gindex[Gfiles].fdate[0] = 0;                                         //  no file date
            Gindex[Gfiles].pdate[0] = 0;                                         //  no photo date
            Gindex[Gfiles].psize = 0;                                            //  no image size
            Gfiles++;
            Gfolders++;
         }

         else if (ftype == IMAGE || ftype == RAW || ftype == VIDEO)              //  supported image file type
         {
            for (jj = 0; jj < Nblacklist; jj++)                                  //  omit blacklisted file
               if (MatchWild(blacklist[jj],file) == 0) break;
            if (jj < Nblacklist) {
               zfree(file);
               continue;
            }

            xxrec = get_xxrec(file);
            if (! xxrec) {
               zfree(file);
               continue;
            }

            Gindex[Gfiles].file = file;                                          //  add to gallery index
            Gindex[Gfiles].folder = 0;
            strcpy(Gindex[Gfiles].fdate,xxrec->fdate);
            strcpy(Gindex[Gfiles].pdate,xxrec->pdate);
            Gindex[Gfiles].fsize = xxrec->fsize;                                 //  file size, MB
            Gindex[Gfiles].psize = xxrec->ww * xxrec->hh;                        //  image size, pixels
            Gfiles++;
            Gimages++;
         }

         else zfree(file);                                                       //  thumbnail or other kind of file

         if (Gfiles == maxgallery) {                                             //  too many files
            zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);
            break;
         }
      }

      if (Flist) zfree(Flist);

      gallerytype = FOLDER;                                                      //  gallery type = folder
      gallerysort = FNAME;                                                       //  sort Gindex by file name ascending
      galleryseq = ASCEND;
      galleryposn = 0;
      
      add_recent_folder(galleryname);                                            //  add gallery to recent list            24.60

      if (Gfiles > 1)
         HeapSort((ch *) Gindex, sizeof(Gindex_t), Gfiles, gallery_comp);

      if (Flastversion)                                                          //  keep last versions only (user setting)
      {
         ii = Gfolders;                                                          //  skip folders
         jj = ii + 1;
         kk = 0;

         while (jj < Gfiles)
         {
            pp1 = strrchr(Gindex[jj].file,'.');                                  //  /.../filename.vNN.ext
            if (pp1 && pp1[-4] == '.' && pp1[-3] == 'v') {                       //                |
               cc1 = pp1 - Gindex[jj].file - 3;
               pp2 = strrchr(Gindex[ii].file,'.');
               if (! pp2) cc2 = 0;
               else {
                  cc2 = pp2 - Gindex[ii].file + 1;                               //  /.../filename.ext
                  if (pp2[-4] == '.' && pp2[-3] == 'v') cc2 -= 4;                //                |
               }
               if (cc1 == cc2 && strmatchN(Gindex[jj].file,Gindex[ii].file,cc1))
               {
                  zfree(Gindex[ii].file);                                        //  if match to "/.../filename."
                  Gindex[ii] = Gindex[jj];                                       //    replace with later version
                  jj++;
                  kk++;
                  continue;
               }
            }
            ii++;
            Gindex[ii] = Gindex[jj];
            jj++;
         }

         Gfiles -= kk;
         Gimages -= kk;
      }

      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"initF"))                                                 //  gallery from given file list
   {
      if (gallerytype == TNONE || gallerytype == FOLDER)                         //  gallery type from caller:
         zappcrash("gallery() initF gallerytype %d",gallerytype);                //    SEARCH META RECENT NEWEST ALBUM

      fid = fopen(galleryname,"r");                                              //  open file
      if (! fid) goto return0;

      while (true)                                                               //  read list of files
      {
         file = fgets_trim(buff,XFCC-1,fid,1);
         if (! file) break;

         xxrec = get_xxrec(file);                                                //  get index record
         if (! xxrec) continue;

         Gindex[Gfiles].file = zstrdup(file,"gallery");                          //  add to gallery index
         Gindex[Gfiles].folder = 0;
         strcpy(Gindex[Gfiles].fdate,xxrec->fdate);                              //  file date
         strcpy(Gindex[Gfiles].pdate,xxrec->pdate);                              //  photo date
         Gindex[Gfiles].fsize = xxrec->fsize;                                    //  file size, MB
         Gindex[Gfiles].psize = xxrec->ww * xxrec->hh;                           //  image size, pixels
         Gfiles++;
         if (Gfiles == maxgallery) {                                             //  too many files
            zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);
            break;
         }
      }

      fclose(fid);

      Gimages = Gfiles;                                                          //  no folders
      gallerysort = SNONE;                                                       //  no initial sort
      galleryseq = QNONE;
      galleryposn = 0;
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"sort"))                                                  //  sort the gallery index
   {
      if (Nth == -2) gallery_memory("get");                                      //  recall prior sort and posn.
      if (Gfiles > 1 && gallerysort != SNONE)                                    //  sort using current or prior params
         HeapSort((ch *) Gindex, sizeof(Gindex_t), Gfiles, gallery_comp);
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"paint"))                                                 //  paint gallery window
   {
      if (filez) Nth = file_position(filez,Nth);                                 //  use or get valid Nth for filez
      if (Nth >= 0) galleryposn = Nth;                                           //  filez position or caller Nth
      else gallery_memory("get");
      gtk_widget_queue_draw(Gdrawin);                                            //  draw gallery window
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"insert"))                                                //  insert new file into list
   {
      fposn = Nth;                                                               //  file position from caller
      if (fposn < 0) fposn = 0;                                                  //  limit to allowed range
      if (fposn > Gfiles) fposn = Gfiles;

      if (Gfiles == maxgallery) {                                                //  no room
         zmessageACK(Mwin,"gallery truncated to %d images",maxgallery);
         goto return0;
      }

      xxrec = get_xxrec(filez);
      if (! xxrec) {
         zmessageACK(Mwin,"file not indexed: %s",filez);
         goto return0;
      }

      for (ii = Gfiles; ii > fposn; ii--)                                        //  create hole in gallery index
         Gindex[ii] = Gindex[ii-1];

      Gindex[fposn].file = zstrdup(filez,"gallery");                             //  put new file in hole
      Gindex[Gfiles].folder = 0;
      strcpy(Gindex[fposn].fdate,xxrec->fdate);
      strcpy(Gindex[fposn].pdate,xxrec->pdate);
      Gindex[fposn].fsize = xxrec->fsize;                                        //  file size, MB
      Gindex[fposn].psize = xxrec->ww * xxrec->hh;                               //  image size, pixels
      Gfiles++;
      Gimages++;
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"delete"))                                                //  delete file from gallery index
   {
      fposn = Nth;                                                               //  file position from caller
      if (fposn < Gfolders || fposn > Gfiles-1) goto return0;                    //  not image file
      Gfiles--;                                                                  //  decr. total files
      Gimages--;                                                                 //  decr. image files
      zfree(Gindex[fposn].file);                                                 //  remove Gindex record
      if (Gindex[fposn].mdata1) zfree(Gindex[fposn].mdata1);                     //     and metadata
      if (Gindex[fposn].mdata2) zfree(Gindex[fposn].mdata2);
      for (ii = fposn; ii < Gfiles; ii++)                                        //  close the hole
         Gindex[ii] = Gindex[ii+1];
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"get"))                                                   //  return Nth folder/file in gallery index
   {
      fposn = Nth;                                                               //  file position from caller
      if (fposn < 0 || fposn > Gfiles-1) goto return0;
      file2 = zstrdup(Gindex[fposn].file,"gallery");                             //  get Nth file
      if (regfile(file2)) goto return2;                                          //  return file2
      if (dirfile(file2)) goto return2;
      zfree(file2);
      goto return0;
   }

   if (strmatch(action,"getR"))                                                  //  return file reference (no zstrdup)
   {
      fposn = Nth;                                                               //  file position from caller
      if (fposn < 0 || fposn > Gfiles-1) goto return0;
      file2 = Gindex[fposn].file;                                                //  get Nth file
      if (regfile(file2)) goto return2;                                          //  return file2
      if (dirfile(file2)) goto return2;
      goto return0;
   }

   if (strmatch(action,"update"))                                                //  update Gindex file from image index
   {                                                                             //  (from update_image_index())
      for (ii = 0; ii < Gfiles; ii++)
         if (strmatch(filez,Gindex[ii].file)) break;
      if (ii == Gfiles) goto return0;                                            //  not found

      xxrec = get_xxrec(filez);
      if (! xxrec) goto return0;
      strcpy(Gindex[ii].fdate,xxrec->fdate);
      strcpy(Gindex[ii].pdate,xxrec->pdate);
      Gindex[ii].fsize = xxrec->fsize;                                           //  file size, MB
      Gindex[ii].psize = xxrec->ww * xxrec->hh;                                  //  image size, pixels
      gallerypainted = 0;
      goto return0;
   }

   zappcrash("navigate %s",action);                                              //  invalid action

return0:
   resource_unlock(GFlock);                                                      //  unlock gallery file list
   return 0;

return2:
   resource_unlock(GFlock);
   return file2;
}


//  private function, gallery sort compare
//  folders sort first and upper/lower case is ignored

int navi::gallery_comp(ch *rec1, ch *rec2)
{
   int         nn;
   ch          *pp1, *pp2;
   double      d1, d2;
   Gindex_t    *grec1, *grec2;

   grec1 = (Gindex_t *) rec1;
   grec2 = (Gindex_t *) rec2;

   if (grec1->folder) {                                                          //  folder sort
      if (! grec2->folder) return -1;                                            //  folder :: image file 
      nn = strcasecmp(grec1->file,grec2->file);                                  //  folder :: folder
      if (nn) return nn;
      nn = strcmp(grec1->file,grec2->file);                                      //  if equal, use utf8 compare
      return nn;
   }
   else if (grec2->folder) return +1;                                            //  image file :: folder 

   if (galleryseq == DESCEND) {                                                  //  descending, switch inputs
      grec1 = (Gindex_t *) rec2;
      grec2 = (Gindex_t *) rec1;
   }

   switch (gallerysort) {

      case FNAME: {                                                              //  file name
         nn = strcasecmp(grec1->file,grec2->file);                               //  ignore case
         if (nn) return nn;
         nn = strcmp(grec1->file,grec2->file);                                   //  if equal, use utf8 compare
         return nn;
      }

      case FNUMBER: {                                                            //  file name/number
         nn = filename_comp(grec1->file,grec2->file);
         if (nn) return nn;
         nn = strcmp(grec1->file,grec2->file);                                   //  if equal, use utf8 compare
         return nn;
      }

      case FDATE: {                                                              //  file mod date/time
         nn = strcmp(grec1->fdate,grec2->fdate);
         if (nn) return nn;
         goto tiebreak;
      }

      case PDATE: {                                                              //  photo date/time
         nn = strcmp(grec1->pdate,grec2->pdate);                                 //  (metadata DateTimeOriginal)
         if (nn) return nn;
         goto tiebreak;
      }

      case FSIZE: {                                                              //  file size
         nn = grec1->fsize - grec2->fsize;
         if (nn) return nn;
         goto tiebreak;
      }

      case PSIZE: {                                                              //  image pixel size
         nn = grec1->psize - grec2->psize;
         if (nn) return nn;
         goto tiebreak;
      }

      case MDATA:                                                                //  user selected metadata
      {
         pp1 = grec1->mdata2;                                                    //  (from search function, metadata report)
         pp2 = grec2->mdata2;

         while (true)                                                            //  loop name1: data1 \n name2: data2 ...
         {
            if (! pp1 && ! pp2) goto tiebreak;                                   //  no data
            if (pp1 && ! pp2) return 1;                                          //  something > nothing
            if (pp2 && ! pp1) return -1;

            pp1 = strchr(pp1,':');                                               //  look for metaname: metadata
            pp2 = strchr(pp2,':');
            if (! pp1 && ! pp2) goto tiebreak;                                   //  no data
            if (pp1 && ! pp2) return 1;                                          //  something > nothing
            if (pp2 && ! pp1) return -1;

            d1 = atof(pp1+1);                                                    //  try numeric comparison
            d2 = atof(pp2+1);
            if (d1 > 0 && d2 > 0) {                                              //  success
               if (d1 < d2) return -1;
               if (d1 > d2) return +1;
               goto nextdata;                                                    //  equal
            }

            nn = zstrcmp(pp1,pp2);                                               //  strcmp() but \n terminates like null
            if (nn) return nn;                                                   //  not equal

         nextdata:                                                               //  equal
            pp1 = strchr(pp1,'\n');
            pp2 = strchr(pp2,'\n');                                              //  loop next name: data pair
         }
      }

      default: return 0;

   tiebreak:                                                                     //  tie breaker
      nn = strcasecmp(grec1->file,grec2->file);                                  //  file name without case
      if (nn) return nn;
      nn = strcmp(grec1->file,grec2->file);                                      //  if equal, use utf8 compare
      return nn;
   }
}


/****

   compare function for sorting a list of file names with embedded numbers
      + folder names are compared normally using strcmp()
      + file base names are compared ignoring string case: 'a' = 'A'
      + file base names with embedded numbers starting in the same position
        are compared arithmetically: '5' < '40' < '300' < '1000'

****/

int navi::filename_comp(ch *file1, ch *file2)
{
   ch       *pp1, *pp2;
   ch       ch1, ch2;
   int      nn;
   int64    nn1, nn2;

   pp1 = strrchr(file1,'/');                                                     //  get end of folders
   pp2 = strrchr(file2,'/');

   if (pp1 && pp2)
   {                                                                             //  compare folders
      *pp1 = *pp2 = 0;
      nn = strcmp(file1,file2);
      *pp1 = *pp2 = '/';
      if (nn != 0) return nn;                                                    //  different, done
   }

   else
   {                                                                             //  no folders
      pp1 = file1 - 1;                                                           //  point to base file names -1
      pp2 = file2 - 1;
   }

   while (true)                                                                  //  loop chars. in file names
   {
      pp1++; pp2++;                                                              //  next chars. to compare

      if (*pp1 == *pp2)
      {                                                                          //  chars. are equal
         if (*pp1 == 0) return 0;                                                //  both '0', strings are equal, done
         if (*pp1 < '0' || *pp1 > '9') continue;                                 //  if either is not a number 0-9,
         if (*pp2 < '0' || *pp2 > '9') continue;                                 //    continue to compare next chars.
         goto number;                                                            //  both are numbers 0-9
      }

      if (*pp1 == 0) return -1;                                                  //  chars. are not equal
      if (*pp2 == 0) return +1;                                                  //  if either is '0', done

      if (*pp1 < '0' || *pp1 > '9' || *pp2 < '0' || *pp2 > '9')
      {                                                                          //  either is not a number 0-9
         ch1 = *pp1;
         ch2 = *pp2;
         if (ch1 >= 'A' && ch1 <= 'Z') ch1 = ch1 | 0x20;                         //  make A-Z into a-z to ignore case
         if (ch2 >= 'A' && ch2 <= 'Z') ch2 = ch2 | 0x20;
         if (ch1 > ch2) return +1;                                               //  compare chars.
         if (ch2 > ch1) return -1;                                               //  if not equal, done
         continue;                                                               //  equal, continue to next chars.
      }

   number:

      nn1 = atol(pp1);                                                           //  compare two numbers
      nn2 = atol(pp2);                                                           //  note: '123' and '0123' compare equal
      if (nn1 > nn2) return +1;
      if (nn2 > nn1) return -1;

      while (pp1[1] >= '0' && pp1[1] <= '9') pp1++;                              //  leave pp1/pp2 at last digit 0-9
      while (pp2[1] >= '0' && pp2[1] <= '9') pp2++;

      continue;
   }
}


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

//  private function for gallery 'thumb view'
//  paint gallery window - draw thumbnail images and limited metadata

int navi::gallery_paint(GtkWidget *drwin, cairo_t *cr)
{
   GdkRGBA     rgba;
   PIXBUF      *pixbuf;
   FTYPE       ftype;
   GtkWidget   *Ftext;
   xxrec_t     *xxrec;
   double      x1, y1, x2, y2;
   int         ii, Fmark;
   int         nrows, row, col;
   int         textlines;
   int         row1, row2, ww, hh, cc, fsize;
   int         drwingW, drwingH;
   int         thumx, thumy;
   int         ndir, nfil, convdate;
   ch          *pp, *fspec, *fname;
   ch          *ppd, ffolder[60];
   ch          pdt[24], text[200];
   static int  prows = 0;

   gallerypainted = 0;

   if (! galleryname) return 1;                                                  //  no gallery

   set_gwin_title();                                                             //  main window title = gallery name
   gallery_navibutts();                                                          //  set navigation buttons on top panel

   rgba.red = 0.00392 * GBrgb[0];                                                //  window background color
   rgba.green = 0.00392 * GBrgb[1];                                              //  0 - 255  -->  0.0 - 1.0
   rgba.blue  = 0.00392 * GBrgb[2];
   rgba.alpha = 1.0;
   gdk_cairo_set_source_rgba(cr,&rgba);
   cairo_paint(cr);

   if (gallerytype == META) {                                                    //  search, metadata report
      gallery_paintmeta(drwin,cr);
      return 1;
   }

   if (Fthumbview == 2) {                                                        //  thumbnails with basic metadata
      gallery_paintmeta2(drwin,cr);
      return 1;
   }

   for (ii = 0; ii < thumbxx; ii++)                                              //  largest ... smallest thumb size
      if (thumbsize >= thumbx[ii]) break;
   if (ii == thumbxx) ii = thumbxx-1;                                            //  (fix poss. bad parameter)
   thumbsize = thumbx[ii];                                                       //  thumbnail size
   fontsize = appfontsize + fontx[ii];                                           //  corresp. font size

   if (! Findexvalid) textlines = 1;                                             //  file name only
   else textlines = 2;                                                           //  file name + date + wwhh
   if (gallerytype != FOLDER) textlines++;                                       //  add folder name above file name
   texthh = textlines * 1.6 * fontsize + 4;                                      //  vertical space required

   thumbW = thumbsize + 10;                                                      //  thumbnail cell size
   thumbH = thumbsize + texthh + thumbsize/24 + 10;

   xwinW = gtk_widget_get_allocated_width(Gscroll);                              //  drawing window size
   xwinH = gtk_widget_get_allocated_height(Gscroll);

   xrows = int(0.1 + 1.0 * xwinH / thumbH);                                      //  get thumbnail rows and cols that
   xcols = int(0.1 + 1.0 * xwinW / thumbW);                                      //    (almost) fit in window
   if (xrows < 1) xrows = 1;
   if (xcols < 1) xcols = 1;
   nrows = (Gfiles+xcols-1) / xcols;                                             //  total thumbnail rows, 1 or more
   if (nrows < 1) nrows = 1;

   drwingW = xcols * thumbW + margin + 10;                                       //  layout size for entire gallery
   drwingH = (nrows + 1) * thumbH;                                               //  (+ empty row for visual end)
   if (drwingH <= xwinH) drwingH = xwinH + 1;                                    //  at least window size + 1

   if (drwingH < 7000000) prows = 0;                                             //  stay within undocumented limit
   else {
      nrows = 7000000 / thumbH;
      drwingH = nrows * thumbH;
      if (nrows * xcols != prows) {
         zmessage_post(Mwin,"20/20",3,"gallery truncated to %d images",nrows*xcols);
         prows = nrows * xcols;
      }
   }

   gtk_widget_get_size_request(drwin,&ww,&hh);                                   //  current size
   if (ww != drwingW || hh != drwingH)
      gtk_widget_set_size_request(drwin,-1,drwingH);                             //  needs to change

   maxscroll = nrows * thumbH;                                                   //  too far but necessary
   if (maxscroll < xwinH) maxscroll = xwinH;                                     //  compensate GTK bug
   gtk_adjustment_set_upper(Gadjust,maxscroll);

   gtk_adjustment_set_step_increment(Gadjust,thumbH);                            //  scrollbar works in row steps
   gtk_adjustment_set_page_increment(Gadjust,thumbH * xrows);                    //  and in page steps

   if (galleryposn >= 0) {                                                       //  new target file position (Nth)
      scrollposn = galleryposn / xcols * thumbH;                                 //  scroll position for target file
      if (scrollposn > maxscroll) scrollposn = maxscroll;                        //    >> top row of window
      gtk_adjustment_set_value(Gadjust,scrollposn);
   }

   scrollposn = gtk_adjustment_get_value(Gadjust);
   galleryposn = (scrollposn + thumbH/2) / thumbH * xcols;                       //  remember gallery position
   gallery_memory("put");
   galleryposn = -1;                                                             //  disable

   cairo_clip_extents(cr,&x1,&y1,&x2,&y2);                                       //  window region to paint
   row1 = y1 / thumbH;
   row2 = y2 / thumbH;

   for (row = row1; row <= row2; row++)                                          //  draw visible rows
   for (col = 0; col < xcols; col++)                                             //  draw all columns in row
   {
      ii = row * xcols + col;                                                    //  next file
      if (ii >= Gfiles) goto endloops;                                           //  exit 2 nested loops

      fspec = Gindex[ii].file;                                                   //  folder/file name
      pp = strrchr(fspec,'/');                                                   //  get file name only
      if (pp) fname = pp + 1;
      else fname = fspec;

      strcpy(ffolder,"/");
      if (gallerytype != FOLDER) {                                               //  files from mixed folders
         if (pp && pp > fspec) {
            for (ppd = pp-1; ppd >= fspec && *ppd != '/'; ppd--);                //  get last folder level
            cc = pp - ppd + 1;                                                   //  (include null to be added)
            if (cc > 60) cc = 60;
            strncpy0(ffolder,ppd,cc);
         }
      }

      thumx = col * thumbW + margin;                                             //  drawing area position
      thumy = row * thumbH + margin;

      Fmark = 0;

      if (zd_select_files) {                                                     //  gallery select is active
         Ftext = zdialog_gtkwidget(zd_select_files,"files");
         if (textwidget_find(Ftext,fspec,0,0) >= 0) Fmark = 1;                   //  mark selected file
      }

      else if (curr_file && strmatch(fspec,curr_file)) Fmark = 1;                //  if file is current file, mark it

      if (Fmark) {
         cairo_set_source_rgb(cr,1,1,0.5);                                       //  mark file with yellow background
         cairo_rectangle(cr,thumx-3,thumy-3,thumbW-3,texthh);
         cairo_fill(cr);
      }

      ftype = image_file_type(fspec);                                            //  folder/image/RAW file

      if (ftype == FDIR) {                                                       //  folder
         dir_filecount(fspec,ndir,nfil);                                         //  get subdir and file counts
         snprintf(text,200,"%s\n%d + %d images",fname,ndir,nfil);                //  dir name, subdirs + image files
      }

      else                                                                       //  image/RAW/VIDEO file
      {
         xxrec = get_xxrec(fspec);                                               //  get index record
         if (! xxrec) continue;

         convdate = 1;
         if (gallerysort == FDATE) strncpy0(pdt,xxrec->fdate,15);                //  use file or photo date
         else {                                                                  //    based on gallery sort
            convdate = 0;
            strncpy0(pdt,xxrec->pdate,15);                                       //  photo date
            if (xxrec->ww == 0) strcpy(pdt,"not indexed");
            else if (strmatch(pdt,"null")) strcpy(pdt,"undated");
            else convdate = 1;
         }
         if (convdate) {
            memmove(pdt+17,pdt+12,2);                                            //  convert yyyymmddhhmmss
            memmove(pdt+14,pdt+10,2);                                            //    to yyyy-mm-dd hh:mm:ss
            memmove(pdt+11,pdt+8,2);
            memmove(pdt+8,pdt+6,2);
            memmove(pdt+5,pdt+4,2);
            pdt[19] = 0;
            pdt[16] = pdt[13] = ':';
            pdt[10] =' ';
            pdt[4] = pdt[7] = '-';
            if (thumbsize <= 256) pdt[10] = 0;                                   //  truncate date/time to date only
         }

         ww = xxrec->ww;                                                         //  width, height, file size
         hh = xxrec->hh;
         fsize = xxrec->fsize;

         cc = 0;

         if (gallerytype != FOLDER) {                                            //  mixed folders (search results)
            snprintf(text,200,"%s\n",ffolder);                                   //  output folder name + \n
            cc = strlen(text);
         }

         snprintf(text+cc,200-cc,"%s\n",fname);                                  //  output file name + \n
         cc = cc + strlen(fname) + 1;

         if (Findexvalid) {                                                      //  valid index, metadata available
            snprintf(text+cc,200-cc,"%s",pdt);                                   //  output date [ time ]
            cc += strlen(pdt);

            if (ww == 0)                                                         //  file not indexed
               snprintf(text+cc,200-cc,"  %.2fmb",fsize/MEGA);                   //  output file size only
            else {
               if (thumbsize > 180)
                  snprintf(text+cc,200-cc,"  %dx%d  %.2fmb",ww,hh,fsize/MEGA);   //  output width, height, file size
               else if (thumbsize > 128)
                  snprintf(text+cc,200-cc,"  %dx%d",ww,hh);                      //  output only width, height
            }
         }

         draw_text(cr,text,thumx,thumy,thumbW-5);                                //  paint text first
         thumy += texthh;                                                        //  position thumbnail below text
      }

      pixbuf = get_thumb_pixbuf(fspec);                                          //  get thumbnail                         24.10
      if (pixbuf) {
         ww = gdk_pixbuf_get_width(pixbuf);
         ww = (thumbsize - ww) / 4;                                              //  shift margin if smaller width
         gdk_cairo_set_source_pixbuf(cr,pixbuf,thumx+ww,thumy);
         cairo_paint(cr);                                                        //  paint
      }

      if (ftype == FDIR) {                                                       //  folder
         thumy += thumbsize/3 + 10;                                              //  overlay thumbnail with text
         draw_text(cr,text,thumx+ww+thumbW/6,thumy,thumbW-5);
      }
   }

endloops:

   gallerypainted = 1;                                                           //  mark gallery display complete
   preload_start = ii;                                                           //  start file for preload_thread()
   preload_trigger = 1;                                                          //  trigger preload_thread()
   return 1;
}


//  private function for search images 'metadata report'
//  paint metadata report - draw thumbnail images + metadata text

int navi::gallery_paintmeta(GtkWidget *drwin, cairo_t *cr)
{
   PIXBUF      *pixbuf;
   double      x1, y1, x2, y2;
   int         ii, nrows, row;
   int         row1, row2, ww, hh;
   int         cc1, cc2;
   int         drwingW, drwingH;
   int         thumx, thumy, textww;
   ch          *fspec, *mtext;
   static int  prows = 0;

   thumbsize = 256;

   for (ii = 0; ii < thumbxx; ii++)
      if (thumbsize == thumbx[ii]) break;                                        //  tolerate bad parameter value
   if (ii >= thumbxx) thumbsize = thumbx[2];                                     //    inherited from prior release

   fontsize = appfontsize;

   thumbW = thumbsize + 10;                                                      //  thumbnail layout size
   thumbH = thumbsize + 20;

   texthh = Gmdrows * fontsize * 1.8 + 20;                                       //  space for metadata text
   if (texthh > thumbH) thumbH = texthh;

   xwinW = gtk_widget_get_allocated_width(Gscroll);                              //  drawing window size
   xwinH = gtk_widget_get_allocated_height(Gscroll);

   xrows = int(0.1 + 1.0 * xwinH / thumbH);                                      //  get thumbnail rows fitting in window
   if (xrows < 1) xrows = 1;
   xcols = 1;                                                                    //  force cols = 1
   nrows = Gfiles;                                                               //  thumbnail rows
   if (! nrows) nrows = 1;

   drwingW = xwinW;                                                              //  layout size for entire gallery
   drwingH = (nrows + 1) * thumbH;                                               //  last row
   if (drwingH < xwinH) drwingH = xwinH;

   if (drwingH < 7000000) prows = 0;                                             //  stay within undocumented limit
   else {
      nrows = 7000000 / thumbH;
      drwingH = nrows * thumbH;
      if (nrows != prows) {
         zmessage_post(Mwin,"20/20",3,"gallery truncated to %d images",nrows);
         prows = nrows;
      }
   }

   gtk_widget_get_size_request(drwin,&ww,&hh);                                   //  current size
   if (ww != drwingW || hh != drwingH)
      gtk_widget_set_size_request(drwin,-1,drwingH);                             //  needs to change

   maxscroll = nrows * thumbH;                                                   //  too far but necessary
   if (maxscroll < xwinH) maxscroll = xwinH;                                     //  compensate GTK bug
   gtk_adjustment_set_upper(Gadjust,maxscroll);

   gtk_adjustment_set_step_increment(Gadjust,thumbH);                            //  scrollbar works in row steps
   gtk_adjustment_set_page_increment(Gadjust,thumbH * xrows);                    //  and in page steps

   if (galleryposn >= 0) {                                                       //  new target file position (Nth)
      scrollposn = galleryposn / xcols * thumbH;                                 //  scroll position for target file
      if (scrollposn > maxscroll) scrollposn = maxscroll;                        //    >> top row of window
      gtk_adjustment_set_value(Gadjust,scrollposn);
   }

   scrollposn = gtk_adjustment_get_value(Gadjust);                               //  save gallery sort and position
   galleryposn = (scrollposn + thumbH/2) / thumbH * xcols;
   gallery_memory("put");
   galleryposn = -1;                                                             //  disable

   cairo_clip_extents(cr,&x1,&y1,&x2,&y2);                                       //  window region to paint
   row1 = y1 / thumbH;
   row2 = y2 / thumbH;

   textww = drwingW - thumbW - 2 * margin;                                       //  space for text right of thumbnail

   for (row = row1; row <= row2; row++)                                          //  draw file thumbnails + text
   {
      ii = row;                                                                  //  next file
      if (ii >= Gfiles) break;

      fspec = Gindex[ii].file;                                                   //  filename

      thumx = margin;                                                            //  drawing area position
      thumy = row * thumbH + margin;

      pixbuf = get_thumb_pixbuf(fspec);                                          //  get thumbnail                         24.10
      if (pixbuf) {
         gdk_cairo_set_source_pixbuf(cr,pixbuf,thumx,thumy);
         cairo_paint(cr);
      }

      draw_text(cr,fspec,thumbW+margin,thumy,textww);                            //  write filespec to right of thumbnail

      cc1 = cc2 = 0;
      if (Gindex[ii].mdata1) cc1 = strlen(Gindex[ii].mdata1);                    //  standard metadata text
      if (Gindex[ii].mdata2) cc2 = strlen(Gindex[ii].mdata2);                    //  user selected metadata text
      if (cc1 + cc2 == 0) continue;                                              //  no text

      if (cc1 && Gindex[ii].mdata1[cc1-1] == '\n') {                             //  delete sporadic trailing \n
         Gindex[ii].mdata1[cc1-1] = 0;
         cc1--;
      }

      if (cc2 && Gindex[ii].mdata2[cc2-1] == '\n') {                             //  delete sporadic trailing \n
         Gindex[ii].mdata2[cc2-1] = 0;
         cc2--;
      }

      mtext = (ch *) zmalloc(cc1 + cc2 + 4,"paintmeta");                         //  space for both

      if (cc1) strcpy(mtext,Gindex[ii].mdata1);                                  //  standard text

      if (cc2) {
         strcpy(mtext + cc1,"\n\n");
         cc1 += 2;
         strcpy(mtext+cc1,Gindex[ii].mdata2);
      }

      draw_text(cr,mtext,thumbW+margin,thumy+20,textww);                         //  draw text to window
      zfree(mtext);
   }

   gallerypainted = 1;
   return 1;
}


//  private function for 'meta view'
//  paint gallery window: thumbnail, file name, basic metadata,
//  extra indexed metadata (user defined in image index function).

int navi::gallery_paintmeta2(GtkWidget *drwin, cairo_t *cr)
{
   PIXBUF      *pixbuf;
   FTYPE       ftype;
   double      x1, y1, x2, y2;
   int         ii, nn, cc, nrows, row, col;
   int         row1, row2, ww, hh;
   int         drwingW, drwingH;
   int         thumx, thumy, textx, textww;
   int         ndir, nfil;
   ch          *fspec, *fspec2;
   xxrec_t     *xxrec;
   ch          fdate[20], pdate[20], *pp;
   ch          *rating, *tags, *title, *desc, *location, *country;
   ch          wwhh[16], fsize[16];
   ch          text[1000], xtext1[xmetaXcc+xmetamaxkeys], **xtext2;
   static int  prows = 0;

   thumbsize = 256;                                                              //  thumbnail size
   fontsize = appfontsize;

   textww = fontsize * 60;                                                       //  text width limit for max. 60 chars.
   if (gallerytype != FOLDER) textww = fontsize * 200;                           //  more if full path is written

   thumbW = thumbsize + 10 + textww;                                             //  thumbnail + text layout size
   thumbH = thumbsize + 20;

   texthh = 15 * fontsize * 1.8 + 20;                                            //  space for 15 lines of metadata text
   if (texthh > thumbH) thumbH = texthh;

   xwinW = gtk_widget_get_allocated_width(Gscroll);                              //  drawing window size
   xwinH = gtk_widget_get_allocated_height(Gscroll);

   xrows = int(0.1 + 1.0 * xwinH / thumbH);                                      //  thumbnail rows that fit in window
   if (xrows < 1) xrows = 1;
   xcols = 1;                                                                    //  1 thumbnail column
   nrows = (Gfiles+xcols-1) / xcols;                                             //  thumbnail rows, 1 or more
   if (nrows < 1) nrows = 1;

   drwingW = xcols * thumbW + margin + 10;                                       //  layout size for entire gallery
   drwingH = (nrows + 1) * thumbH;                                               //  (+ empty row for visual end)
   if (drwingH <= xwinH) drwingH = xwinH + 1;                                    //  at least window size + 1

   if (drwingH < 7000000) prows = 0;                                             //  stay within undocumented limit
   else {
      nrows = 7000000 / thumbH;
      drwingH = nrows * thumbH;
      if (nrows != prows) {
         zmessage_post(Mwin,"20/20",3,"gallery truncated to %d images",nrows);
         prows = nrows;
      }
   }

   gtk_widget_get_size_request(drwin,&ww,&hh);                                   //  current size
   if (ww != drwingW || hh != drwingH)
      gtk_widget_set_size_request(drwin,-1,drwingH);                             //  needs to change

   maxscroll = nrows * thumbH;                                                   //  too far but necessary
   if (maxscroll < xwinH) maxscroll = xwinH;                                     //  compensate GTK bug
   gtk_adjustment_set_upper(Gadjust,maxscroll);

   gtk_adjustment_set_step_increment(Gadjust,thumbH);                            //  scrollbar works in row steps
   gtk_adjustment_set_page_increment(Gadjust,thumbH * xrows);                    //  and in page steps

   if (galleryposn >= 0) {                                                       //  new target file position (Nth)
      scrollposn = galleryposn / xcols * thumbH;                                 //  scroll position for target file
      if (scrollposn > maxscroll) scrollposn = maxscroll;                        //    >> top row of window
      gtk_adjustment_set_value(Gadjust,scrollposn);
   }

   scrollposn = gtk_adjustment_get_value(Gadjust);                               //  save gallery sort and position
   galleryposn = (scrollposn + thumbH/2) / thumbH * xcols;
   gallery_memory("put");
   galleryposn = -1;                                                             //  disable

   cairo_clip_extents(cr,&x1,&y1,&x2,&y2);                                       //  window region to paint
   row1 = y1 / thumbH;
   row2 = y2 / thumbH;

   for (row = row1; row <= row2; row++)                                          //  draw file thumbnails
   for (col = 0; col < xcols; col++)                                             //  draw all columns in row
   {
      ii = row * xcols + col;                                                    //  next file
      if (ii >= Gfiles) goto endloops;                                           //  exit 2 nested loops

      fspec = Gindex[ii].file;
      fspec2 = fspec;
      if (gallerytype == FOLDER) {                                               //  folder gallery, write only
         pp = strrchr(fspec,'/');                                                //    base file names
         if (pp) fspec2 = pp+1;
      }

      thumx = col * thumbW + margin;                                             //  drawing area position
      thumy = row * thumbH + margin;

      pixbuf = get_thumb_pixbuf(fspec);                                          //  get thumbnail                         24.10
      if (pixbuf) {
         gdk_cairo_set_source_pixbuf(cr,pixbuf,thumx,thumy);                     //  paint thumbnail
         cairo_paint(cr);
      }

      textx = thumx + margin + thumbsize;                                        //  text position

      if (curr_file && strmatch(fspec,curr_file)) {                              //  yellow background for curr. file name
         ww = fontsize * strlen(fspec2);
         if (ww > textww) ww = textww;
         cairo_set_source_rgb(cr,1,1,0.5);
         cairo_rectangle(cr,textx,thumy,ww,fontsize*1.6);
         cairo_fill(cr);
      }

      ftype = image_file_type(fspec);                                            //  file type

      if (ftype == FDIR) {                                                       //  subfolder
         dir_filecount(fspec,ndir,nfil);                                         //  get subfolder and file counts
         snprintf(text,1000,"\n %s \n %d folders + %d images \n",                //  paint 2 lines:
                                         fspec2, ndir, nfil);                    //  folder name, file counts
         draw_text(cr,text,textx,thumy,textww);
      }

      else if (Findexvalid)                                                      //  if image index is valid
      {
         xxrec = get_xxrec(fspec);                                               //  get indexed metadata for file
         if (! xxrec) continue;

         strncpy0(fdate,xxrec->fdate,16);                                        //  construct metadata text report
         strncpy0(pdate,xxrec->pdate,16);
         rating = xxrec->rating;
         snprintf(wwhh,16,"%dx%d",xxrec->ww,xxrec->hh);
         snprintf(fsize,16,"%.2fmb",xxrec->fsize/MEGA);
         tags = xxrec->tags;
         title = xxrec->title;
         desc = xxrec->desc;
         location = xxrec->location;
         country = xxrec->country;

         pp = fdate;                                                             //  file date
         memmove(pp+17,pp+12,2);                                                 //  :ss added
         memmove(pp+14,pp+10,2);                                                 //  yyyymmddhhmmss to yyyy-mm-dd hh:mm:ss
         memmove(pp+11,pp+8,2);
         memmove(pp+8,pp+6,2);
         memmove(pp+5,pp+4,2);
         pp[19] = 0;
         pp[16] = pp[13] = ':';
         pp[10] =' ';
         pp[4] = pp[7] = '-';

         pp = pdate;                                                             //  photo date
         if (xxrec->ww == 0) strcpy(pp,"not indexed");
         else if (strmatch(pp,"null")) strcpy(pp,"undated");
         else {
            memmove(pp+17,pp+12,2);                                              //  :ss added
            memmove(pp+14,pp+10,2);                                              //  yyyymmddhhmmss to yyyy-mm-dd hh:mm:ss
            memmove(pp+11,pp+8,2);
            memmove(pp+8,pp+6,2);
            memmove(pp+5,pp+4,2);
            pp[19] = 0;
            pp[16] = pp[13] = ':';
            pp[10] =' ';
            pp[4] = pp[7] = '-';
         }

         if (! strmatchN(xxrec->xmeta,"null",4))                                 //  extra indexed metadata present
         {
            nn = breakup_text(xxrec->xmeta,xtext2,"^",10,60);
            for (ii = cc = 0; ii < nn && ii < 9; ii++) {
               repl_1str(xtext2[ii],xtext1+cc,99,"^"," ");
               cc += strlen(xtext2[ii]);
               strcpy(xtext1+cc,"\n ");
               cc += 2;
            }
            xtext1[cc] = 0;

            for (ii = 0; ii < nn; ii++)
               zfree(xtext2[ii]);
            zfree(xtext2);

            snprintf(text,1000,                                                  //  report basic metadata in 6 lines
               " %s \n photo date: %s  file date: %s \n"
               " rating: %s  size: %s %s  location: %s %s \n"
               " tags: %s \n title: %s \n description: %s \n"
               " ----------------------- \n %s",                                 //  extra indexed metadata in <10 lines
               fspec2, pdate, fdate, rating, wwhh, fsize,
                       location, country, tags, title, desc, xtext1);
         }

         else
         {
            snprintf(text,1000,                                                  //  report basic metadata in 6 lines
               " %s \n photo date: %s  file date: %s \n"
               " rating: %s  size: %s %s  location: %s %s \n"
               " tags: %s \n title: %s \n description: %s ",
               fspec2, pdate, fdate, rating, wwhh, fsize,
                       location, country, tags, title, desc);
         }
      }

      else snprintf(text,1000,"\n %s",fspec2);                                   //  no index, no metadata, file name only

      draw_text(cr,text,textx,thumy,textww);                                     //  paint text
   }
   endloops:

   gallerypainted = 1;
   return 1;
}


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

//  private function
//  find the number of subdirs and image files within a given folder

void navi::dir_filecount(ch *dirname, int &ndir, int &nfil)
{
   ch       *file, **flist;
   int      ii, cc, err, NF;
   int      dcount = 0, fcount = 0;
   FTYPE    ftype;

   #define NC 1000                                                               //  cache capacity

   static int     ftf = 1;
   static ch      *dirnamecache[NC];                                             //  cache for recent folder data
   static time_t  modtimecache[NC];
   static int     dcountcache[NC];
   static int     fcountcache[NC];
   static int     pcache = 0;
   STATB          statB;

   if (ftf) {
      ftf = 0;
      cc = NC * sizeof(ch *);                                                    //  first call, clear cache
      memset(dirnamecache,0,cc);
   }

   ndir = nfil = 0;

   if (! dirfile(dirname,&statB)) return;

   for (ii = 0; ii < NC; ii++) {                                                 //  look for folder in cache
      if (! dirnamecache[ii]) break;
      if (strmatch(dirname,dirnamecache[ii]) &&
          statB.st_mtime == modtimecache[ii]) {
         ndir = dcountcache[ii];                                                 //  found, not stale
         nfil = fcountcache[ii];
         return;
      }
   }

   err = find_imagefiles(dirname,1+8,flist,NF);                                  //  find image files + folders
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   for (ii = 0; ii < NF; ii++)
   {
      file = flist[ii];
      ftype = image_file_type(file);
      zfree(file);
      if (ftype == FDIR) dcount++;                                               //  folder count
      else if (ftype == IMAGE || ftype == RAW || ftype == VIDEO) fcount++;       //  image file count
   }

   if (NF) zfree(flist);

   ii = pcache++;                                                                //  add to cache, replace oldest
   if (pcache == NC) pcache = 0;
   if (dirnamecache[ii]) zfree(dirnamecache[ii]);
   dirnamecache[ii] = zstrdup(dirname,"filecount");
   modtimecache[ii] = statB.st_mtime;
   ndir = dcountcache[ii] = dcount;
   nfil = fcountcache[ii] = fcount;
   return;
}


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

//  private function
//  write text block at px/py location with width limit ww

void navi::draw_text(cairo_t *cr, ch *text, int px, int py, int ww)
{
   static PangoFontDescription   *pfont = 0;
   static PangoLayout            *playout = 0;

   static int     pfontsize = -1;
   static ch      thumbfont[20] = "";

   if (fontsize != pfontsize) {                                                  //  adjust for curr. font size
      pfontsize = fontsize;
      snprintf(thumbfont,20,"sans bold %d",fontsize);
      if (pfont) pango_font_description_free(pfont);
      pfont = pango_font_description_from_string(thumbfont);
      if (playout) g_object_unref(playout);
      playout = pango_cairo_create_layout(cr);
      pango_layout_set_font_description(playout,pfont);
   }

   pango_layout_set_width(playout,ww*PANGO_SCALE);                               //  limit width to avail. space
   pango_layout_set_ellipsize(playout,PANGO_ELLIPSIZE_END);
   pango_layout_set_text(playout,text,-1);

   cairo_move_to(cr,px,py);
   cairo_set_source_rgb(cr,0,0,0);                                               //  text = black
   pango_cairo_show_layout(cr,playout);
   return;
}


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

//  private function
//  create a row of navigation buttons in gallery top panel

void navi::gallery_navibutts()
{
   ch          labtext[100];
   int         ii, cc, max = maxgallerylevs;
   ch          *pp1, *pp2;

   for (ii = 0; ii < max; ii++) {                                                //  clear old navi buttons if any
      if (gallerypath[ii]) {
         zfree(gallerypath[ii]);
         gallerypath[ii] = 0;
         gtk_widget_destroy(gallerybutt[ii]);
      }
   }

   if (gallerylabel) gtk_widget_destroy(gallerylabel);                           //  clear gallery label if any
   gallerylabel = 0;

   sprintf(labtext,"no gallery");
   if (gallerytype == SEARCH) sprintf(labtext,"search results");                 //  search results
   if (gallerytype == META) sprintf(labtext,"search results");                   //  search results (metadata report)
   if (gallerytype == RECENT) sprintf(labtext,"recent images");                  //  recent images
   if (gallerytype == NEWEST) sprintf(labtext,"newest images");                  //  newest images

   if (gallerytype == ALBUM) {                                                   //  album gallery
      pp1 = strrchr(galleryname,'/');
      if (pp1) pp1++;
      else pp1 = galleryname;
      snprintf(labtext,100,"album: %s",pp1);                                     //  album: album-name
   }

   if (gallerytype != FOLDER) {                                                  //  not a folder gallery
      gallerylabel = gtk_label_new(labtext);                                     //  show gallery label
      gtk_box_pack_start(GTK_BOX(Gpanel),gallerylabel,0,0,0);
      gtk_widget_show_all(Gpanel);
      return;
   }

   ii = 0;
   pp1 = galleryname;

   while (pp1 && *pp1)                                                           //  construct new buttons
   {
      pp2 = strchr(pp1+1,'/');                                                   //  /aaaaaa/bbbbbb/cccccc
      if (pp2) cc = pp2 - pp1;                                                   //         |      |
      else cc = strlen(pp1);                                                     //         pp1    pp2
      gallerypath[ii] = (ch *) zmalloc(cc,"navibutts");
      strncpy0(gallerypath[ii],pp1+1,cc);                                        //  bbbbbb
      gallerybutt[ii] = gtk_button_new_with_label(gallerypath[ii]);
      gtk_box_pack_start(GTK_BOX(Gpanel),gallerybutt[ii],0,0,3);
      G_SIGNAL(gallerybutt[ii],"clicked",navibutt_clicked,&Nval[ii]);
      pp1 = pp1 + cc;                                                            //  next folder level /cccccc
      if (! *pp1) break;                                                         //  null = end
      if (*pp1 == '/' && ! *(pp1+1)) break;                                      //  / + null = end
      if (++ii == max) break;                                                    //  limit of folder levels
   }

   gtk_widget_show_all(Gpanel);
   return;
}


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

//  private function - menu function for gallery window
//    - scroll window as requested
//    - jump to new file or folder as requested

void navi::menufuncx(GtkWidget *, ch *menu)
{
   int         ii, scroll1, scroll2;

   viewmode('G');

   scrollposn = gtk_adjustment_get_value(Gadjust);                               //  current scroll position

   if (strmatch(menu,"Zoom+"))                                                   //  next bigger thumbnail size
   {
      F1_help_topic = "zoom";
      if (Fthumbview != 1) return;                                               //  not resizable thumb view
      for (ii = 0; ii < thumbxx; ii++)                                           //  set next greater thumbnail size
         if (thumbsize == thumbx[ii]) break;
      if (ii == 0) return;                                                       //  already maximum, no change
      thumbsize = thumbx[ii-1];
      galleryposn = scrollposn / thumbH * xcols;                                 //  keep top row position
      gallery(0,"paint",-1);                                                     //  paint gallery
      return;
   }

   if (strmatch(menu,"Zoom-"))                                                   //  next smaller thumb size
   {
      F1_help_topic = "zoom";
      if (Fthumbview != 1) return;                                               //  not resizable thumb view
      for (ii = 0; ii < thumbxx; ii++)                                           //  set next smaller thumb size
         if (thumbsize == thumbx[ii]) break;
      if (ii >= thumbxx-1) return;                                               //  already minimum, no change
      thumbsize = thumbx[ii+1];                                                  //  next smaller
      galleryposn = scrollposn / thumbH * xcols;                                 //  keep top row position
      gallery(0,"paint",-1);                                                     //  paint gallery
      return;
   }

   if (strmatch(menu,"Row Up")) {                                                //  scroll 1 row up
      scroll1 = scrollposn / thumbH * thumbH;
      scroll2 = scroll1 - thumbH;
      if (scroll2 < 0) scroll2 = 0;
      gallery_scroll(scroll2,800);
      return;
   }

   if (strmatch(menu,"Row Down")) {                                              //  scroll 1 row down
      scroll1 = scrollposn / thumbH * thumbH;
      scroll2 = scroll1 + thumbH;
      if (scroll2 > maxscroll) scroll2 = maxscroll;
      gallery_scroll(scroll2,800);
      return;
   }

   if (strmatch(menu,"Page Up")) {                                               //  scroll 1 page up
      scroll1 = scrollposn / thumbH * thumbH;
      scroll2 = scroll1 - thumbH * xrows;
      if (scroll2 < 0) scroll2 = 0;
      gallery_scroll(scroll2,1600);
      return;
   }

   if (strmatch(menu,"Page Down")) {                                             //  scroll 1 page down
      scroll1 = scrollposn / thumbH * thumbH;
      scroll2 = scroll1 + thumbH * xrows;
      if (scroll2 > maxscroll) scroll2 = maxscroll;
      gallery_scroll(scroll2,1600);
      return;
   }

   if (strmatch(menu,"Home")) {                                                  //  jump to top
      gallery_scroll(-1,0);                                                      //  stop scrolling
      scrollposn = 0;
      galleryposn = scrollposn / thumbH * xcols;
      gallery(0,"paint",0);
      return;
   }

   if (strmatch(menu,"End")) {                                                   //  jump to bottom
      gallery_scroll(-1,0);                                                      //  stop scrolling
      scrollposn = maxscroll;
      galleryposn = scrollposn / thumbH * xcols;
      gallery(0,"paint",galleryposn);
      return;
   }

   Plog(0,"unknown gallery function: %s \n",menu);
   return;
}


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

//  private function
//  scroll gallery page up or down to goal scroll position
//  position:  N  goal scroll position, 0 to maxscroll
//            -1  stop scrolling immediately
//     speed:  N  scroll N pixels/second

#define scrollms 4                                                               //  timer period, millisecs
#define scrollsec (0.001 * scrollms)

void navi::gallery_scroll(int position, int speed)
{
   if (position < 0) {                                                           //  stop scrolling
      gallery_scrollgoal = -1;
      gallery_scrollspeed = 0;
      return;
   }

   if (gallery_scrollgoal < 0) {                                                 //  start scrolling
      gallery_scrollgoal = position;
      gallery_scrollspeed = speed;
      g_timeout_add(scrollms,gallery_scrollfunc,0);                              //  timer period, milliseconds
      return;
   }

   gallery_scrollgoal = position;                                                //  continue scrolling with
   gallery_scrollspeed = speed;                                                  //    possible goal/speed change
   return;
}


//  private function
//  timer function, runs every 'scrollms' milliseconds

int navi::gallery_scrollfunc(void *)
{
   float    cumscroll = 0;
   int      newscrollposn;

   if (gallery_scrollgoal < 0) return 0;                                         //  stop scrolling

   if (FGM != 'G') {                                                             //  not gallery view
      gallery_scrollgoal = -1;                                                   //  stop scrolling
      return 0;
   }

   if (scrollposn == gallery_scrollgoal) {                                       //  goal reached, stop
      gallery_scrollgoal = -1;
      return 0;
   }

   cumscroll = scrollsec * gallery_scrollspeed;                                  //  based on timer period

   scrollposn = gtk_adjustment_get_value(Gadjust);
   newscrollposn = scrollposn;

   if (scrollposn < gallery_scrollgoal) {                                        //  adjust scroll position
      newscrollposn += cumscroll;
      if (newscrollposn > gallery_scrollgoal)
         newscrollposn = gallery_scrollgoal;
   }

   if (scrollposn > gallery_scrollgoal) {
      newscrollposn -= cumscroll;
      if (newscrollposn < gallery_scrollgoal)
         newscrollposn = gallery_scrollgoal;
   }

   gtk_adjustment_set_value(Gadjust,newscrollposn);
   return 1;
}


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

//  private function
//  gallery top panel folder button clicked, open corresponding folder

void navi::navibutt_clicked(GtkWidget *widget, int *lev)
{
   ch     gallerydir[XFCC], *pp;

   gallery_scroll(-1,0);                                                         //  stop scrolling

   pp = gallerydir;

   for (int ii = 0; ii <= *lev; ii++)
   {
      *pp = '/';
      strcpy(pp+1,gallerypath[ii]);
      pp = pp + strlen(pp);
   }

   gallery(gallerydir,"init",0);                                                 //  new gallery
   gallery(0,"sort",-2);                                                         //  recall sort and position
   gallery(0,"paint",-1);                                                        //  paint
   return;
}


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

//  private function - [TOP] button: select new top folder

void navi::newtop(GtkWidget *widget, GdkEventButton *event)
{
   static GtkWidget   *popmenu = 0;

   gallery_scroll(-1,0);                                                         //  stop scrolling

   if (popmenu) gtk_widget_destroy(popmenu);
   popmenu = create_popmenu();

   for (int ii = 0; ii < Ntopfolders; ii++)                                      //  insert all top image folders
      add_popmenu_item(popmenu,topfolders[ii],newtop_menu_event,0,0);

   add_popmenu_item(popmenu,"Folder Tree",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"/",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"HOME",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"Desktop",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"Fotocx home",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"Saved Areas",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"recent images",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"newest images",newtop_menu_event,0,0);

   popup_menu(Mwin,popmenu);
   return;
}

void navi::newtop_menu_event(GtkWidget *, ch *menu)                              //  menu event function
{
   ch     folder[200];

   if (! menu) return;

   strncpy0(folder,menu,200);

   if (strmatch(menu,"Folder Tree")) {
      m_folder_tree(0,0);
      return;
   }

   if (strmatch(menu,"recent images")) {
      m_recentfiles(0,0);
      return;
   }

   if (strmatch(menu,"newest images")) {
      m_newfiles(0,0);
      return;
   }

   if (strmatch(menu,"HOME"))                                                    //  user home folder
      strncpy0(folder,getenv("HOME"),200);

   if (strmatch(menu,"Desktop"))                                                 //  user desktop
      snprintf(folder,200,"%s/%s",getenv("HOME"),desktopname);                   //  locale-specific desktop name

   if (strmatch(menu,"Fotocx home"))                                             //  fotocx home folder
      strncpy0(folder,get_zhomedir(),200);

   if (strmatch(menu,"Saved Areas"))                                             //  saved areas folder
      snprintf(folder,200,"%s/saved_areas",get_zhomedir());

   gallery(folder,"init",0);                                                     //  new gallery
   gallery(0,"sort",-2);                                                         //  recall sort and position
   gallery(0,"paint",-1);                                                        //  paint
   return;
}


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

//  private function - [Album] button: select new album

void navi::newalbum(GtkWidget *widget, GdkEventButton *event)
{
   static GtkWidget  *popmenu = 0;

   ch          **flist, *pp;
   ch          findcomm[200];
   ch          *albums[200];
   int         ii, NF, count = 0;

   gallery_scroll(-1,0);                                                         //  stop scrolling

   snprintf(findcomm,200,"%s/*",albums_folder);                                  //  find all album names
   zfind(findcomm,flist,NF);

   for (ii = 0; ii < NF && count < 200; ii++)
   {
      pp = strrchr(flist[ii],'/');
      if (! pp) continue;
      albums[count] = zstrdup(pp+1,"navi-album");
      count++;
   }

   for (int ii = 0; ii < NF; ii++)
      zfree(flist[ii]);
   zfree(flist);

   if (! count) {
      zmessageACK(Mwin,"no albums found");
      return;
   }

   if (count > 1)                                                                //  sort album names
      HeapSort(albums,count);

   if (popmenu) gtk_widget_destroy(popmenu);
   popmenu = create_popmenu();

   add_popmenu_item(popmenu,"Current Album",newalbum_menu_event,0,0);            //  add "current album"

   for (int ii = 0; ii < count; ii++)                                            //  insert all known albums
      add_popmenu_item(popmenu,albums[ii],newalbum_menu_event,0,0);

   popup_menu(widget,popmenu);
   return;
}


void navi::newalbum_menu_event(GtkWidget *, ch *menu)                            //  menu event function
{
   ch  albumfile[200];

   if (! menu || strmatch(menu,"Current Album"))
   {
      if (! curr_album || ! *curr_album) {
         zmessageACK(Mwin,"no current album");
         return;
      }
      album_show();
      return;
   }

   snprintf(albumfile,200,"%s/%s",albums_folder,menu);                           //  show the album gallery
   album_show(albumfile);
   return;
}


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

//  private function
//  mouse event function for gallery window - get selected thumbnail and file
//  user function receives clicked file, which is subject for zfree()

int navi::mouse_event(GtkWidget *widget, GdkEvent *event, void *)
{
   GdkEventButton *eventB;
   PIXBUF         *pixbuf;
   int            evtype, mousex, mousey, mousebutt;
   int            row, col, nrows, tww, thh, marg;
   int            Nth, poswidth, posheight, err, ftype;
   static int     Fmyclick = 0;
   ch             *filez = 0;
   STATB          statB;

   if (! Gfiles) return 1;                                                       //  empty gallery
   if (! gallerypainted) return 1;                                               //  not initialized

   eventB = (GdkEventButton *) event;
   evtype = eventB->type;
   mousex = int(eventB->x);
   mousey = int(eventB->y);
   mousebutt = eventB->button;
   if (mousex < margin) return 1;
   if (mousey < margin) return 1;

   KBcontrolkey = KBshiftkey = KBaltkey = 0;
   if (eventB->state & GDK_CONTROL_MASK) KBcontrolkey = 1;
   if (eventB->state & GDK_SHIFT_MASK) KBshiftkey = 1;
   if (eventB->state & GDK_MOD1_MASK) KBaltkey = 1;

   if (mousebutt == 1 && KBaltkey) mousebutt = 3;                                //  left butt + ALT key >> right butt

   row = (mousey - margin) / thumbH;                                             //  find selected row, col
   col = (mousex - margin) / thumbW;

   if (thumbsize) {
      poswidth = (mousex - margin) - thumbW * col;                               //  mouse position within thumbnail
      poswidth = 100 * poswidth / thumbsize;                                     //  0-100 = left to right edge
      posheight = (mousey - texthh - margin) - thumbH * row;
      posheight = 100 * posheight / thumbsize;                                   //  0-100 = top to bottom edge
   }
   else poswidth = posheight = 0;

   if (! xcols) return 1;
   nrows = 1 + (Gfiles-1) / xcols;                                               //  total thumbnail rows, 1 or more
   if (col < 0 || col >= xcols) return 1;                                        //  mouse not on a thumbnail
   if (row < 0 || row >= nrows) return 1;
   Nth = xcols * row + col;                                                      //  mouse at this thumbnail (image file)
   if (Nth >= Gfiles) return 1;

   filez = zstrdup(Gindex[Nth].file,"navi-mouse");                               //  file (thumbnail) at mouse posn.

   if (evtype == GDK_BUTTON_PRESS)
   {
      gallery_scroll(-1,0);                                                      //  stop scrolling

      Fmyclick = 1;                                                              //  button press is mine

      if (drag_file) zfree(drag_file);
      drag_file = 0;
      ftype = image_file_type(filez);                                            //  save for poss. drag-drop
      if (ftype == IMAGE || ftype == RAW || ftype == VIDEO) {
         drag_file = zstrdup(filez,"navi-mouse");                                //  save file and position in gallery
         drag_posn = Nth;
      }
      goto cleanup;
   }

   if (evtype == GDK_BUTTON_RELEASE)
   {
      gallery_scroll(-1,0);                                                      //  stop scrolling

      if (! Fmyclick) goto cleanup;                                              //  ignore unmatched button release
      Fmyclick = 0;                                                              //    (from vanished popup window)

      err = stat(filez,&statB);
      if (err) goto cleanup;                                                     //  file is gone?

      if (S_ISDIR(statB.st_mode)) {                                              //  if folder, go there
         gallery(filez,"init",0);                                                //  new gallery
         gallery(0,"sort",-2);                                                   //  recall sort and position
         gallery(0,"paint",-1);                                                  //  paint
         goto cleanup;
      }

      if (clicked_file) zfree(clicked_file);                                     //  save clicked file and gallery position
      clicked_file = zstrdup(filez,"navi-mouse");
      clicked_posn = Nth;
      clicked_width = poswidth;                                                  //  normalized 0-100
      clicked_height = posheight;

      if (thumbsize) {
         pixbuf = get_thumb_pixbuf(filez);                                       //  get thumbnail image                   24.10
         if (pixbuf) {
            tww = gdk_pixbuf_get_width(pixbuf);                                  //  thumbnail width and height
            thh = gdk_pixbuf_get_height(pixbuf);
            marg = (thumbsize - tww) / 4;                                        //  allow for margin offset
            poswidth -= 100 * marg/thumbsize;
            clicked_width = poswidth * thumbsize / tww;                          //  clicked position is relative
            clicked_height = posheight * thumbsize / thh;                        //    to thumbnail dimensions
            if (clicked_width < 0) clicked_width = 0;
            if (clicked_width > 100) clicked_width = 100;
            if (clicked_height < 0) clicked_height = 0;
            if (clicked_height > 100) clicked_height = 100;
         }
      }

      if (mousebutt == 1)                                                        //  left click
      {
         if (zd_select_files) select_files_Lclick_func(Nth);                     //  send to select_files()
         else if (zd_select_files1) select_files1_Lclick_func(Nth);              //  send to select_files1()
         else if (zd_album_update) update_albums_Lclick_func(Nth);               //  send to album_replacefile()
         else if (zd_edit_bookmarks) edit_bookmarks_Lclick_func(Nth);            //  send to bookmarks editor
         else if (zd_ss_imageprefs) ss_imageprefs_Lclick_func(Nth);              //  send to slide show editor
         else if (zd_copymove) m_copy_move(0,0);                                 //  update copy/move dialog
         else if (zd_rename) m_rename(0,0);                                      //  update rename dialog
         else if (zd_permissions) m_permissions(0,0);                            //  update permissions dialog
         else if (zd_deltrash) m_delete_trash(0,0);                              //  update delete/trash dialog
         else if (zd_deletemeta) m_meta_delete(0,0);                             //  update delete metadata dialog
         else if (zd_editmeta || zd_editanymeta || zd_metaview) {                //  (any/all can be active)
            if (zd_editmeta) m_meta_edit_main(0,0);                              //  update edit metadata dialog
            if (zd_editanymeta) m_meta_edit_any(0,0);                            //  update edit any metadata dialog
            if (zd_metaview) meta_view(0);                                       //  update metadata view window
         }
         else if (KBshiftkey) gallery_popimage();                                //  popup large image
         else gallery_Lclick_func(Nth);                                          //  open the file
      }

      if (mousebutt == 2) gallery_popimage();                                    //  middle click, popup large image

      if (mousebutt == 3) {                                                      //  right click
         if (zd_select_files) select_files_Rclick_func(Nth);                     //  send to select_files()
         else gallery_Rclick_popup(Nth);                                         //  send to gallery thumbnail popup menu
      }
   }

cleanup:
   if (filez) zfree(filez);
   return 0;                                                                     //  must be 0 for drag/drop to work
}


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

//  this function is called if a drag-drop is initiated from the gallery window

ch * navi::gallery_dragfile()
{
   ch       dragfile[200];
   FILE     *fid;

   snprintf(dragfile,200,"%s/drag_from_folder",get_zhomedir());                  //  save source gallery name
   fid = fopen(dragfile,"w");                                                    //    and position
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 0;
   }

   fprintf(fid,"%s\n",galleryname);
   fprintf(fid,"%d\n",drag_posn);
   fclose(fid);

   return drag_file;
}


//  this function is called if a drag-drop file is dragged or dropped on the gallery window

void navi::gallery_dropfile(int mousex, int mousey, ch *file)
{
   int      err, top, mpos, speed;
   int      row, col, nrows, Nth, cc;
   int      poswidth, from_posn = -1;
   ch       dragfile[200], buff[200];
   ch       *pp1, *pp2, *from_gallery = 0, *newfile;
   FILE     *fid;

   if (gallerytype != FOLDER && gallerytype != ALBUM) return;                    //  reject others (search, recent ...)

   if (! file)                                                                   //  drag motion underway
   {
      if (! mousex) {                                                            //  drag leave event
         gallery_scroll(-1,0);                                                   //  stop scroll
         return;
      }

      top = gtk_adjustment_get_value(Gadjust);                                   //  mouse vertical posn in window
      mpos = 100 * (mousey - top) / xwinH;                                       //  0 - 100

      if (mpos < 15 && top > 0) {                                                //  mouse position from window top to bottom
         if (mpos < 0) mpos = 0;                                                 //      0 .... 15 .......... 85 .... 100
         speed = 200 * (15 - mpos);                                              //  corresponding scroll speed
         gallery_scroll(0,speed);                                                //    4000 ... 200 ... 0 ... 200 ... 4000
      }                                                                          //    down     down           up      up

      if (mpos > 85 && top < maxscroll) {
         if (mpos >= 100) mpos = 100;
         speed = 200 * (mpos - 85);
         gallery_scroll(maxscroll,speed);
      }

      if (mpos >= 15 && mpos <= 85)                                              //  stop scrolling in mid-window range
         gallery_scroll(-1,0);

      return;
   }

   gallery_scroll(-1,0);                                                         //  stop scrolling

   if (gallerytype == FOLDER)                                                    //  folder gallery, add new file
   {
      err = cp_copy(file,galleryname);
      if (err) {
         zfree(file);
         zmessageACK(Mwin,strerror(err));
         return;
      }

      pp1 = strrchr(file+2,'/');                                                 //  isolate filename.ext
      if (! pp1) return;
      cc = strlen(pp1);
      newfile = zstrdup(galleryname,"navi-dropfile",cc+2);                       //  construct galleryname/filename
      strcat(newfile,pp1);
      load_filemeta(file);                                                       //  update image index
      save_filemeta(newfile);
      zfree(file);
      zfree(newfile);
      if (curr_file) load_filemeta(curr_file);                                   //  restore curr. metadata

      gallery(0,"init",0);                                                       //  refresh gallery
      gallery(0,"sort",-2);                                                      //  sort, keep position
      gallery(0,"paint",-1);                                                     //  paint
      return;
   }

   row = (mousey - margin) / thumbH;                                             //  find drop location: row, col
   col = (mousex - margin) / thumbW;

   if (col < 0) col = 0;
   if (col >= xcols) col = xcols-1;

   if (xcols) nrows = 1 + (Gfiles-1) / xcols;                                    //  total thumbnail rows, 1 or more
   else nrows = 1;
   if (nrows < 1) nrows = 1;
   if (row < 0) row = 0;
   if (row >= nrows) row = nrows-1;

   if (thumbsize) {
      poswidth = (mousex - margin) - thumbW * col;                               //  mouse position within thumbnail
      poswidth = 100 * poswidth / thumbsize;                                     //  0-100 = left to right edge
   }
   else poswidth = 0;

   Nth = xcols * row + col;                                                      //  mouse at this thumbnail (image file)
   if (poswidth > 50) Nth++;                                                     //  drop after this position
   if (Nth > Gfiles) Nth = Gfiles;                                               //  last + 1


   snprintf(dragfile,200,"%s/drag_from_folder",get_zhomedir());                  //  get source gallery name
   fid = fopen(dragfile,"r");                                                    //    and position
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   pp1 = fgets_trim(buff,200,fid);                                               //  source gallery name
   if (pp1) from_gallery = zstrdup(pp1,"navi-dropfile");
   pp2 = fgets_trim(buff,200,fid);                                               //  source gallery position
   if (pp2) from_posn = atoi(pp2);
   fclose(fid);

   if (! pp1 || from_posn < 0) {
      Plog(0,"drag_from data not available \n");
      return;
   }

   if (from_gallery && strmatch(galleryname,from_gallery)) {                     //  drag-drop in same gallery
      album_move_file(from_posn,Nth);                                            //  move file position in gallery
   }
   else album_add_file(file,Nth);                                                //  insert new file at position

   zfree(file);
   if (from_gallery) zfree(from_gallery);

   album_show();
   return;
}


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

//  Private function - respond to keyboard navigation keys.
//  KBpress() for main window calls this function when G view is active.

int navi::KBaction(ch *action)
{
   int      ii;
   int      row1, row2, rowf;

   Plog(2,"KBaction: %s \n",action);

   if (strmatch(action,"Open")) {                                                //  goto F-view
      viewmode('F');
      return 1;
   }

   if (strmatch(action,"Zoom+") || strmatch(action,"Zoom-in")) {
      menufuncx(0,"Zoom+");
      return 1;
   }

   if (strmatch(action,"Zoom-") || strmatch(action,"Zoom-out")) {
      menufuncx(0,"Zoom-");
      return 1;
   }

   if (strmatch(action,"Up")) {
      menufuncx(0,"Row Up");
      return 1;
   }

   if (strmatch(action,"Down")) {
      menufuncx(0,"Row Down");
      return 1;
   }

   if (strmatch(action,"Home")) {
      menufuncx(0,"Home");
      return 1;
   }

   if (strmatch(action,"End")) {
      menufuncx(0,"End");
      return 1;
   }

   if (strmatch(action,"Page_Up")) {
      menufuncx(0,"Page Up");
      return 1;
   }

   if (strmatch(action,"Page_Down")) {
      menufuncx(0,"Page Down");
      return 1;
   }

   if (strmatch(action,"Left")) {                                                //  left arrow - previous image
      m_prev(0,0);
      row1 = scrollposn / thumbH;
      rowf = curr_file_posn / xcols;
      if (rowf < row1) menufuncx(0,"Row Up");
      return 1;
   }

   if (strmatch(action,"Right")) {                                               //  right arrow - next image
      m_next(0,0);
      row1 = scrollposn / thumbH;
      row2 = row1 + xwinH / thumbH - 1;
      rowf = curr_file_posn / xcols;
      if (rowf == 0) menufuncx(0,"Home");
      if (rowf > row2) menufuncx(0,"Row Down");
      return 1;
   }

   if (strmatch(action,"Delete")) {                                              //  delete key - delete/trash dialog
      m_delete_trash(0,0);
      return 1;
   }

   if (strmatch(action,"Show Hidden")) {
      Fshowhidden = 1 - Fshowhidden;
      gallery(0,"init",0);
      gallery(0,"paint",-1);
      return 1;
   }

//  check for menu function KB shortcut

   for (ii = 0; ii < Nkbsf; ii++)
      if (strmatchcase(action,kbsftab[ii].menu)) break;

   if (ii == Nkbsf) {
      Plog(0,"shortcut not found: %s \n",action);
      return 1;
   }

   kbsftab[ii].func(0,kbsftab[ii].arg);                                          //  call the menu function
   return 1;
}


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

//  save and restore gallery sort and top file position

#define RGmax 100                                                                //  max. recent galleries to remember

typedef struct {                                                                 //  gallery memory data
   int      galleryposn;                                                         //  top file position (scroll position)
   int      gallerysort;                                                         //  sort galleryname/filedate/photodate
   int      galleryseq;                                                          //  sort ascending/descending
   ch       *galleryname;                                                        //  gallery name /.../filename
}
gallerymem_t;

gallerymem_t   gallerymem[RGmax];                                                //  array of gallery memory
gallerymem_t   Tgallerymem;

int            NGmem;                                                            //  current entries <= RGmax


void gallery_memory(ch *action)
{
   FILE        *fid;
   ch          buff[XFCC], *pp;
   int         ii, nn, err;

   if (strmatch(action,"reset"))                                                 //  clear gallery memory data
   {
      NGmem = 0;
      return;
   }

   if (strmatch(action,"load"))                                                  //  load gallery memory from file
   {                                                                             //    at Fotocx startup time
      NGmem = 0;

      fid = fopen(gallerymem_file,"r");                                          //  open gallery memory file
      if (fid)
      {
         for (ii = 0; ii < RGmax; ii++)
         {
            pp = fgets_trim(buff,XFCC,fid);                                      //  read gallery memory record
            if (! pp) break;                                                     //  NNNNNN N N /folder/name/...

            err = convSI(pp,nn,0,999999);                                        //  NNNNNN = gallery top file position
            if (err) break;
            gallerymem[ii].galleryposn = nn;

            err = convSI(pp+7,nn,0,3);                                           //  N = 0/1/2/3 = sort
            if (err) break;                                                      //    none / f.name / f.date / photo-date
            gallerymem[ii].gallerysort = nn;

            err = convSI(pp+9,nn,0,2);                                           //  N = 0/1/2 = none / ascend / descend
            if (err) break;
            gallerymem[ii].galleryseq = nn;

            pp += 11;                                                            //  gallery name (folder)
            if (*pp != '/') break;
            gallerymem[ii].galleryname = zstrdup(pp,"gallery-memory");
         }

         fclose(fid);
         NGmem = ii;                                                             //  memory entry count
      }

      return;
   }

   if (strmatch(action,"save"))                                                  //  save gallery memory at shutdown
   {
      fid = fopen(gallerymem_file,"w");
      if (! fid) return;
      for (ii = 0; ii < NGmem; ii++)
         fprintf(fid,"%06d %1d %1d %s\n",
                     gallerymem[ii].galleryposn, gallerymem[ii].gallerysort,
                     gallerymem[ii].galleryseq, gallerymem[ii].galleryname);
      fclose(fid);
      return;
   }

   if (! galleryname) return;

   if (strmatch(action,"get"))                                                   //  get gallery data from memory
   {
      for (ii = 0; ii < NGmem; ii++)                                             //  search for gallery in memory
         if (strmatch(galleryname,gallerymem[ii].galleryname)) break;

      if (ii < NGmem) {
         galleryposn = gallerymem[ii].galleryposn;                               //  found, restore top file posn and sort
         gallerysort = (GSORT) gallerymem[ii].gallerysort;
         galleryseq = (GSEQ) gallerymem[ii].galleryseq;
      }
      else {                                                                     //  not found, use defaults
         if (gallerytype == FOLDER) {
            gallerysort = FNAME;
            galleryseq = ASCEND;
            galleryposn = 0;
         }
         else {
            gallerysort = SNONE;
            galleryseq = QNONE;
            galleryposn = 0;
         }
      }

      return;
   }

   if (strmatch(action,"put"))                                                   //  save gallery data in memory
   {
      if (! galleryname) return;

      for (ii = 0; ii < NGmem; ii++)                                             //  search for gallery in memory
         if (strmatch(galleryname,gallerymem[ii].galleryname)) break;

      if (NGmem == 0 || ii == NGmem)                                             //  not found
      {
         if (NGmem == RGmax) {                                                   //  gallery memory full
            zfree(gallerymem[ii-1].galleryname);                                 //    remove last entry
            NGmem--;
         }

         for (ii = NGmem; ii > 0; ii--)                                          //  push all entries up
            gallerymem[ii] = gallerymem[ii-1];                                   //    to free entry [0]

         NGmem++;                                                                //  one more entry
         gallerymem[0].galleryname = zstrdup(galleryname,"gallery-memory");      //  entry [0] is mine
      }

      else if (ii > 0)                                                           //  found at entry [ii]
      {
         Tgallerymem = gallerymem[0];                                            //  exchange entries [0] and [ii]
         gallerymem[0] = gallerymem[ii];
         gallerymem[ii] = Tgallerymem;
      }

      gallerymem[0].galleryposn = galleryposn;                                   //  update entry [0]
      gallerymem[0].gallerysort = gallerysort;
      gallerymem[0].galleryseq = galleryseq;

      return;
   }

   zappcrash("gallery_memory() %s",action);                                      //  bad action
   return;
}


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

//  set the window title for the gallery window
//  window title = gallery name

void set_gwin_title()
{
   ch     *pp, title[200];

   if (FGM != 'G') return;

   if (gallerytype == FOLDER)
      snprintf(title,200,"%s   FOLDER %s   %d folders  %d files",                //  use Frelease
                           Frelease,galleryname,Gfolders,Gimages);

   else if (gallerytype == SEARCH || gallerytype == META)
      snprintf(title,200,"%s   SEARCH RESULTS   %d files",Frelease,Gimages);

   else if (gallerytype == ALBUM) {
      pp = strrchr(galleryname,'/');
      if (! pp) pp = galleryname;
      else pp++;
      snprintf(title,200,"%s   ALBUM %s   %d files",Frelease,pp,Gimages);
   }

   else if (gallerytype == RECENT)
      snprintf(title,200,"%s   RECENT FILES",Frelease);

   else if (gallerytype == NEWEST)
      snprintf(title,200,"%s   NEWEST FILES",Frelease);

   else snprintf(title,200,"%s   UNKNOWN",Frelease);

   gtk_window_set_title(MWIN,title);
   return;
}


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

//  Return previous or next image file from curr_file in the gallery file list.
//  If lastver is set, only the last edit version is returned.
//  (gallery must be sorted by file name (version sequence)).
//  Returns null if no previous/next file found.

ch * prev_next_file(int index, int lastver)
{
   int      Nth;
   ch       *rootname1 = 0, *rootname2 = 0;                                      //  file names without .vNN and .ext
   ch       *file = 0, *filever = 0;

   Nth = curr_file_posn;

   if (index == +1)                                                              //  get next file
   {
      while (true)
      {
         Nth += 1;
         if (Nth >= Gfiles) {                                                    //  no more files this gallery
            if (filever) break;                                                  //  return last file version
            goto retnull;                                                        //  no more files
         }
         file = gallery(0,"getR",Nth);                                           //  get next file 
         if (! file) goto retnull;
         if (! lastver) goto retfile;                                            //  return all versions
         if (! filever) {
            filever = file;                                                      //  potential last version
            file = 0;
            rootname1 = file_rootname(filever);                                  //  save rootname
            continue;
         }
         if (rootname2) zfree(rootname2);
         rootname2 = file_rootname(file);
         if (! strmatch(rootname1,rootname2)) break;                             //  new rootname, filever was last version
         filever = file;                                                         //  save last file with same rootname
         file = 0;
      }

      file = filever;
      goto retfile;
   }

   if (index == -1)                                                              //  get previous file
   {
      if (curr_file) rootname1 = file_rootname(curr_file);                       //  current file rootname
      while (true)
      {
         Nth -= 1;
         if (Nth < Gfolders) goto retnull;                                       //  no more files
         file = gallery(0,"getR",Nth);                                           //  get previous file 
         if (! file) goto retnull;
         if (! lastver) goto retfile;                                            //  return all versions
         if (! rootname1) goto retfile;                                          //  no current file - return previous file
         if (rootname2) zfree(rootname2);
         rootname2 = file_rootname(file);
         if (! strmatch(rootname1,rootname2)) goto retfile;                      //  new rootname, return this file
      }
   }

   retnull:
   file = 0;

   retfile:
   if (rootname1) zfree(rootname1);
   if (rootname2) zfree(rootname2);
   return file;
}


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

//  Find the previous or next gallery from the current gallery.
//  (previous/next defined by subfolder sequence in parent folder)
//  returned gallery is subject for zfree().

ch * prev_next_gallery(int index)
{
   int      nn, Nth;
   ch       *parentdir = 0, *olddir = 0, *newdir = 0, *file = 0;

   if (gallerytype != FOLDER) goto errret;                                       //  gallery not a physical folder

   olddir = zstrdup(galleryname,"prev-next-gallery");                            //  olddir = current gallery / folder
   if (! olddir) goto errret;
   nn = strlen(olddir) - 1;
   if (olddir[nn] == '/') olddir[nn] = 0;
   parentdir = zstrdup(olddir,"prev-next-gallery");                              //  get parent folder
   for (NOP; nn && parentdir[nn] != '/'; nn--)
   if (! nn) goto errret;
   parentdir[nn] = 0;
   gallery(parentdir,"init",0);                                                  //  gallery = parent

   for (Nth = 0; Nth < Gfolders; Nth++) {                                        //  find olddir in parent
      if (file) zfree(file);
      file = gallery(0,"get",Nth);
      if (! file) goto errret;
      if (strmatch(file,olddir)) break;
   }

   Nth += index;                                                                 //  previous or next folder
   if (Nth < 0 || Nth >= Gfolders) goto errret;
   newdir = gallery(0,"get",Nth);
   if (newdir) goto okret;

errret:
   if (newdir) zfree(newdir);
   newdir = 0;

okret:
   if (olddir) {
      gallery(olddir,"init",0);                                                  //  restore old folder
      gallery(0,"sort",-2);                                                      //  recall sort and position
      zfree(olddir);
      gallerypainted = 1;                                                        //  no need to paint
   }
   if (parentdir) zfree(parentdir);
   if (file) zfree(file);
   return newdir;
}


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

//  Get file position in gallery file list.
//  If Nth position matches file, this is returned.
//  Otherwise the list is searched from position 0.
//  Position 0-last is returned if found, or -1 if not.

int file_position(ch *file, int Nth)
{
   int      ii;

   if (! Gimages) return -1;
   if (! file) return -1;

   if (Nth >= Gfolders && Nth < Gfiles)
      if (strmatch(file,Gindex[Nth].file)) return Nth;

   for (ii = Gfolders; ii < Gfiles; ii++)
      if (strmatch(file,Gindex[ii].file)) break;

   if (ii < Gfiles) return ii;
   return -1;
}


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

//  Determine if a file is a folder or a supported image file type

FTYPE image_file_type(ch *file)
{
   int         err, xcc;
   static int  ftf = 1, tdcc = 0;
   ch          *ppx;
   ch          ppx2[8], *RP;
   STATB       statB;
   static ch   myRAWtypes[1000] = " ";                                           //  room for 200 file types
   static ch   myVIDEOtypes[1000] = " ";                                         //  ditto

   if (ftf) {
      if (thumbfolder && *thumbfolder == '/')                                    //  thumbnail top folder
         tdcc = strlen(thumbfolder);                                             //  save length
      ftf = 0;
   }

   if (! file) return FNF;
   RP = f_realpath(file);                                                        //  use real path
   if (! RP) return FNF;

   err = stat(RP,&statB);                                                        //  file not found
   if (err) {
      zfree(RP);
      return FNF;
   }

   if (S_ISDIR(statB.st_mode)) {                                                 //  folder
      zfree(RP);
      return FDIR;
   }

   if (! S_ISREG(statB.st_mode)) {                                               //  not a regular file
      zfree(RP);
      return OTHER;
   }

   if (tdcc && strmatchN(RP,thumbfolder,tdcc)) {                                 //  a fotocx thumbnail
      zfree(RP);
      return THUMB;
   }

   ppx = strrchr(RP,'.');
   if (! ppx) {                                                                  //  no file .ext
      zfree(RP);
      return OTHER;
   }

   xcc = strlen(ppx);                                                            //  file .ext > 6 chars.
   if (xcc > 6) {
      zfree(RP);
      return OTHER;
   }

   strcpy(ppx2,ppx);                                                             //  add trailing blank: ".ext "
   strcpy(ppx2+xcc," ");

   zfree(RP);

   if (strcasestr(imagefiletypes,ppx2)) return IMAGE;                            //  supported image type
   if (strcasestr(myRAWtypes,ppx2))return RAW;                                   //  one of my RAW types
   if (strcasestr(myVIDEOtypes,ppx2)) return VIDEO;                              //  one of my VIDEO types

   if (strcasestr(RAWfiletypes,ppx2)) {                                          //  found in known RAW types 
      strcat(myRAWtypes,ppx2);                                                   //  add to my RAW types
      return RAW;
   }

   if (! Ffmpeg) return OTHER;                                                   //  missing ffmpeg program

   if (strcasestr(VIDEOfiletypes,ppx2)) {                                        //  found in known VIDEO types
      strcat(myVIDEOtypes,ppx2);                                                 //  add to my VIDEO types
      return VIDEO;
   }

   return OTHER;                                                                 //  not a known image file type
}


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

//  Given a thumbnail file, get the corresponding image file.
//  Returns null if no image file found.
//  Returned file is subject for zfree().
//  image file:  /image/folder/file.xxx
//  thumb folder:  /thumb/folder
//  thumb file:  /thumb/folder/image/folder/file.xxx.jpeg

ch * thumb2imagefile(ch *thumbfile)                                              //  simplified
{
   uint        cc;
   ch          *imagefile;
   static int  Fdone = 0;

   if (! thumbfolder || *thumbfolder != '/') {
      if (! Fdone) Plog(0,"%s \n","no thumbnail folder");
      Fdone++;
      return 0;
   }

   cc = strlen(thumbfolder);
   if (cc > strlen(thumbfile) - 12) {                                            //  /thumbfolder/imagefolder/file.xxx.jpeg
      Plog(0,"invalid thumbfile: %s \n",thumbfile);
      return 0;
   }

   imagefile = zstrdup(thumbfile+cc,"thumb2imagefile");                          //  /imagefolder/file.xxx.jpeg
   cc = strlen(imagefile);
   imagefile[cc-5] = 0;                                                          //  /imagefolder/file.xxx
   if (regfile(imagefile)) return imagefile;                                     //  return if exists
   zfree(imagefile);                                                             //  not found
   return 0;
}


//  Given an image file, get the corresponding thumbnail file.
//  The filespec is returned whether or not the file exists.
//  Returned file is subject for zfree().

ch * image2thumbfile(ch *imagefile)                                              //  simplified
{
   int         cc1, cc2;
   ch          *RP, *thumbfile;
   static int  Fdone = 0;

   if (! thumbfolder || *thumbfolder != '/') {
      if (! Fdone++) Plog(0,"%s \n","no thumbnail folder");
      return 0;
   }

   RP = f_realpath(imagefile);                                                   //  use real path
   if (! RP) return 0;
   if (! regfile(RP)) { zfree(RP); return 0; }
   cc1 = strlen(thumbfolder);
   cc2 = strlen(RP);
   thumbfile = (ch *) zmalloc(cc1+cc2+6,"image2thumbfile");
   strcpy(thumbfile,thumbfolder);                                                //  /thumb/folder
   strcpy(thumbfile+cc1,RP);                                                     //  .../image/folder/file.xxx
   strcpy(thumbfile+cc1+cc2,".jpeg");                                            //  .../image/folder/file.xxx.jpeg
   zfree(RP);
   return thumbfile;
}


//  version to avoid use of zmalloc() & zfree()
//  output: ch thumbfile[XFCC]
//  returns 0 if OK, 1 if image file not found

int image2thumbfile2(ch *imagefile, ch *thumbfile)
{
   int         cc1, cc2;
   ch          *RP;
   static int  Fdone = 0;

   *thumbfile = 0;

   if (! thumbfolder || *thumbfolder != '/') {
      if (! Fdone++) Plog(0,"no thumbnail folder\n");
      return 1;
   }

   RP = f_realpath(imagefile);                                                   //  use real path
   if (! RP) return 1;
   if (! regfile(RP)) { zfree(RP); return 1; }
   cc1 = strlen(thumbfolder);
   cc2 = strlen(RP);
   strcpy(thumbfile,thumbfolder);                                                //  /thumb/folder
   strcpy(thumbfile+cc1,RP);                                                     //  .../image/folder/file.xxx
   strcpy(thumbfile+cc1+cc2,".jpeg");                                            //  .../image/folder/file.xxx.jpeg
   zfree(RP);
   return 0;
}


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

//  check if thumbnail file is missing or stale.
//  returns 1  new thumbnail NOT needed
//          0  new thumbnail needed

int thumbfile_OK(ch *imagefile)
{
   int         ftype;
   STATB       statF, statB;
   ch          thumbfile[XFCC];

   if (! regfile(imagefile,&statF)) return 1;                                    //  bad image file

   ftype = image_file_type(imagefile);
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) return 1;               //  not an image file or RAW file

   image2thumbfile2(imagefile,thumbfile);                                        //  get thumbnail file for image file
   if (! regfile(thumbfile,&statB)) return 0;                                    //  thumbfile FNF
   if (statB.st_mtime != statF.st_mtime) return 0;                               //  thumbnail is stale
   return 1;                                                                     //  thumbnail is up to date
}


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

//  get "folder" or "broken" pixbuf using cached pixbuf image
//  caller must avoid gdk_object_unref() of returned pixbuf

PIXBUF * get_folder_pixbuf()
{
   static int        ftf = 1, err;
   static PIXBUF     *bigpixbuf = 0;
   static PIXBUF     *pixbuf = 0;
   static int        psize = -1;
   static ch         thumbfile[300] = "";
   GError            *gerror = 0;
   STATB             statB;

   if (ftf) {
      ftf = 0;
      snprintf(thumbfile,300,"%s/folder.png",get_zhomedir());                    //  look for user folder.png
      err = stat(thumbfile,&statB);
      if (err) snprintf(thumbfile,300,"%s/folder.png",get_zimagedir());          //  no, get package folder.png
      bigpixbuf = gdk_pixbuf_new_from_file(thumbfile,&gerror);
      if (! bigpixbuf) {
         Plog(0,"cannot make folder pixbuf: ");
         if (gerror) Plog(0,"%s \n",gerror->message);
      }
   }

   if (! bigpixbuf) return 0;
   if (thumbsize == psize) return pixbuf;

   pixbuf = gdk_pixbuf_scale_simple(bigpixbuf,thumbsize,thumbsize,BILINEAR);
   psize = thumbsize;
   return pixbuf;
}


PIXBUF * get_broken_pixbuf()
{
   static int        ftf = 1;
   static PIXBUF     *bigpixbuf = 0;
   static PIXBUF     *pixbuf = 0;
   static int        psize = -1;
   static ch         thumbfile[300] = "";
   GError            *gerror = 0;

   if (ftf) {
      ftf = 0;
      strncatv(thumbfile,300,zfuncs::zimagedir,"/broken.png",0);
      bigpixbuf = gdk_pixbuf_new_from_file(thumbfile,&gerror);
      if (! bigpixbuf) {
         Plog(0,"cannot make broken pixbuf: ");
         if (gerror) Plog(0,"%s \n",gerror->message);
      }
   }

   if (! bigpixbuf) return 0;
   if (thumbsize == psize) return pixbuf;

   pixbuf = gdk_pixbuf_scale_simple(bigpixbuf,thumbsize,thumbsize,BILINEAR);
   psize = thumbsize;
   return pixbuf;
}


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

//  create or replace thumbnail file if missing or stale.
//  returns  0  thumbnail exists already, nothing was done (OK)
//           1  new thumbnail file created (OK)
//           2  cannot create PXB or make thumbnail file (error)

int update_thumbfile(ch *imagefile)
{
   ch          *pp, *tf, thumbfile[XFCC];
   PXB         *imagepxb = 0, *thumbpxb = 0;
   STATB       statB;
   int         err, size, retstat;
   timeval     thumbtimes[2];

   if (thumbfile_OK(imagefile)) return 0;                                        //  thumbnail file exists, not stale

   Funcbusy(+1);                                                                 //  "working"  24.10

   err = image2thumbfile2(imagefile,thumbfile);                                  //  get thumbnail file for image file
   if (err) { retstat = 2; goto returnx; }

   pp = strrchr(thumbfile+1,'/');                                                //  folder
   if (! pp) { retstat = 2; goto returnx; }
   *pp = 0;
   if (! dirfile(thumbfile)) {                                                   //  create thumbnail folder if needed
      tf = zescape_quotes(thumbfile);
      err = zshell(0,"mkdir -p -m 0750 \"%s\"",tf);
      zfree(tf);
   }
   *pp = '/';
   if (err && errno != EEXIST) {                                                 //  happens, threads
      Plog(0,"thumbnail mkdir failure: %s\n %s\n",thumbfile,strerror(errno));
      retstat = 2;
      goto returnx;
   }

   pp = (ch *) strrchr(imagefile,'.');
   if (! pp) { retstat = 2; goto returnx; }

   size = thumbfilesize;

   if (strcasestr(".jpg .jpeg",pp))
      thumbpxb = JPG_PXB_load(imagefile,size);                                   //  JPEG file to PXB (reduced)

   else if (image_file_type(imagefile) == RAW)                                   //  RAW file
      thumbpxb = RAW_thumb_pxb(imagefile);                                       //  get thumb PXB from embedded image

   if (! thumbpxb) {
      imagepxb = PXB_load(imagefile,0);                                          //  any image file to PXB
      if (imagepxb) thumbpxb = PXB_rescale(imagepxb,size);                       //  rescale PXB to thumbnail size
   }

   if (! thumbpxb) { retstat = 2; goto returnx; }

   err = PXB_JPG_save(thumbpxb,thumbfile,80);                                    //  save to JPEG thumbnail file
   if (err) { retstat = 2; goto returnx; }

   if (! regfile(imagefile,&statB)) { retstat = 2; goto returnx; }               //  get image file mod time

   thumbtimes[0].tv_sec = thumbtimes[1].tv_sec = statB.st_mtim.tv_sec;           //  thumbnail mod time
   thumbtimes[0].tv_usec = thumbtimes[1].tv_usec = 0;                            //    = image file mod time
   utimes(thumbfile,thumbtimes);
   retstat = 1;

returnx:
   if (retstat > 1) Plog(0,"update_thumbfile() failure: %s \n",imagefile);
   if (imagepxb) PXB_free(imagepxb);
   if (thumbpxb) PXB_free(thumbpxb);
   Funcbusy(-1);
   return retstat;
}


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

//  Update thumbnail mod time to match corresponding image file
//  (avoid thumbnail refresh when image file metadata is modified)

int thumbfile_set_mod_time(ch *imagefile)
{
   STATB       statB;
   ch          thumbfile[XFCC];
   int         err;
   timeval     thumbtimes[2];

   if (! regfile(imagefile,&statB)) return ENOENT;                               //  get image file mod time

   image2thumbfile2(imagefile,thumbfile);                                        //  get thumbnail file for image file
   if (! regfile(thumbfile)) return ENOENT;

   thumbtimes[0].tv_sec = thumbtimes[1].tv_sec = statB.st_mtim.tv_sec;           //  thumbnail mod time
   thumbtimes[0].tv_usec = thumbtimes[1].tv_usec = 0;                            //    = image file mod time
   err = utimes(thumbfile,thumbtimes);
   return err;
}


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

//  Remove thumbnail from disk (for deleted or renamed image file).

void delete_thumbfile(ch *imagefile)
{
   int         err;
   ch          thumbfile[XFCC];

   err = image2thumbfile2(imagefile,thumbfile);                                  //  get thumbnail file for image file
   if (err) return;

   remove(thumbfile);
   return;
}


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

//  get thumbnail pixbuf for given image file
//  DO NOT g_object_unref() returned pixbuf.

PIXBUF * get_thumb_pixbuf(ch *imagefile)                                         //  simplify   24.10
{
   ch          thumbfile[XFCC];
   int         ii, err;
   int         ww, hh, size;
   FTYPE       ftype;
   STATB       statB;
   PIXBUF      *tempixbuf;
   static PIXBUF *pixbuf = 0;
   GError      *gerror = 0;

   if (pixbuf) g_object_unref(pixbuf);                                           //  free prior pixbuf memory
   pixbuf = 0;

   if (thumbsize == 0) return 0;                                                 //  should not happen

   if (! regfile(imagefile,&statB))                                              //  check file exists
      if (! dirfile(imagefile,&statB)) return 0;

   ftype = image_file_type(imagefile);

   if (ftype == FDIR)                                                            //  'folder' image
      return get_folder_pixbuf();

   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO)                         //  not an image or RAW or VIDEO file
      return get_broken_pixbuf();                                                //  'broken' image

   ii = update_thumbfile(imagefile);                                             //  make/refresh thumbnail file
   if (ii > 1) return 0;                                                         //  fail

   if (thumbsize <= thumbfilesize) {
      err = image2thumbfile2(imagefile,thumbfile);                               //  small, use thumbnail file
      if (! err) pixbuf = gdk_pixbuf_new_from_file(thumbfile,&gerror);
   }
   else pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror);                    //  large, use image file
   if (! pixbuf) {
      Plog(0,"cannot make pixbuf: %s %s \n",gerror->message,imagefile);
      return 0;
   }

   ww = gdk_pixbuf_get_width(pixbuf);                                            //  get thumbnail actual size
   hh = gdk_pixbuf_get_height(pixbuf);
   size = ww;
   if (hh > ww) size = hh;
   if (size != thumbsize) {                                                      //  rescale to thumbnail display size
      ww = ww * thumbsize / size;
      hh = hh * thumbsize / size;
      tempixbuf = gdk_pixbuf_scale_simple(pixbuf,ww,hh,BILINEAR);
      g_object_unref(pixbuf);
      pixbuf = tempixbuf;
   }

   return pixbuf;
}


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

//  popup a new window with a larger image of a clicked thumbnail

void gallery_popimage()
{
   static int   ftf = 1, ii;
   static ch  *popfiles[20];                                                     //  last 20 files
   int          ftype;

   if (ftf) {
      ftf = 0;                                                                   //  initz. empty file memory
      for (ii = 0; ii < 20; ii++)
         popfiles[ii] = 0;
      ii = 0;
   }

   if (! clicked_file) return;

   ftype = image_file_type(clicked_file);                                        //  exclude RAW etc.
   if (ftype != IMAGE) return;

   ii++;                                                                         //  use next file memory position
   if (ii == 20) ii = 0;
   if (popfiles[ii]) zfree(popfiles[ii]);
   popfiles[ii] = clicked_file;                                                  //  save clicked_file persistently
   clicked_file = 0;                                                             //  reset clicked_file

   popup_image(popfiles[ii],MWIN,1,512);                                         //  popup window with image
   return;
}


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

//  select one image file by clicking a gallery thumbnail
//  returned file is subject for zfree()

ch  galsel1_filename[XFCC];

ch * select_files1(ch *gfolder)
{
   zdialog     *zd;
   int         zstat;
   ch          *cfolder;
   ch          fgm;

   fgm = FGM;                                                                    //  save current view mode
   cfolder = zstrdup(navi::galleryname,"gallery-select");                        //  and gallery folder

   if (gfolder) {
      gallery(gfolder,"init",0);                                                 //  switch to caller's gallery
      gallery(0,"sort",-2);                                                      //  recall sort and position
      gallery(0,"paint",-1);                                                     //  paint
   }

   *galsel1_filename = 0;                                                        //  no selection

   viewmode('G');

/***
          _________________________________________
         |             Image File                  |
         |                                         |
         |  Click thumbnail to select file         |
         |                                         |
         | Image File [__________________________] |
         |                                         |
         |                                [OK] [X] |
         |_________________________________________|

***/

   zd = zdialog_new("Image File",Mwin,"OK"," X ",null);                          //  dialog to select a thumbnail
   zd_select_files1 = zd;
   zdialog_add_widget(zd,"label","labtip","dialog","Click thumbnail to select file");
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labf","hbf","Image File","space=3");
   zdialog_add_widget(zd,"zentry","filename","hbf",0,"space=3|expand");

   zdialog_resize(zd,400,0);
   zdialog_run(zd,0,"parent");                                                   //  run dialog and wait for completion
   zstat = zdialog_wait(zd);                                                     //    via mouse click function below

   zdialog_free(zd);
   zd_select_files1 = 0;

   if (cfolder) {
      gallery(cfolder,"init",0);                                                 //  restore view mode
      gallery(0,"sort",-2);
      zfree(cfolder);
   }
   viewmode(fgm);                                                                //  may paint

   if (zstat != 1) return 0;                                                     //  no selection
   if (*galsel1_filename != '/') return 0;
   if (! regfile(galsel1_filename)) return 0;                                    //  FNF

   return f_realpath(galsel1_filename);                                          //  use real path
}


void select_files1_Lclick_func(int Nth)                                          //  called by gallery mouse function
{
   ch       *imagefile = 0, *pp;
   int      ftype;

   if (! zd_select_files1) return;                                               //  should not happen
   if (Nth < 0) return;

   imagefile = gallery(0,"get",Nth);                                             //  get file at clicked position
   if (! imagefile) return;

   ftype = image_file_type(imagefile);                                           //  must be image or RAW file
   if (ftype != IMAGE && ftype != RAW) {
      zfree(imagefile);
      return;
   }

   pp = strrchr(imagefile,'/') + 1;
   zdialog_stuff(zd_select_files1,"filename",pp);                                //  stuff file name in dialog

   strncpy0(galsel1_filename,imagefile,XFCC);                                    //  save full path
   zfree(imagefile);
   return;
}


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

//  Select multiple image files from thumbnail gallery window.
//    SelFiles[*]: list of selected image files, SFcount entries.
//  Pre-selected files are passed in the same list, which is updated.
//  The dialog shows the list of files selected and can be edited.

namespace galselnames
{
   int  dialog_event(zdialog *zd, ch *event);
   int  find_file(ch *imagefile);
   void add_file(ch *imagefile);
   void remove_file(ch *imagefile);
   void Xclick_func(int Nth, ch LR);
   void callbackfunc(GtkWidget *textwidget, int line, int posn, int KBkey);
   void showthumb();

   int            Fsetcurr = 0;
   GtkWidget      *drawarea = 0;
   GtkWidget      *Ftext = 0;
   int            currline;
   ch             *imagefile;
   int            GSselect;                                                      //  count of files in select dialog
};


void select_files(int fclear, int fsetcurr)                                      //  add fsetcurr (def. 0)                 24.20
{
   using namespace galselnames;

   int      ii, kk, dups = 0, yn = 0;;

/***
          _________________________________________________________________
         |                  Select Image Files                             |
         |  __________________________________   ________________________  |
         | |                                  | |                        | |
         | |      list of selected files      | | image of current file  | |
         | |                                  | |  selected in file list | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |__________________________________| |________________________| |
         |                                                                 |
         | [Remove] [Clear] [Add All] [Add Current]              [Disable] |
         |                                                                 |
         |                                                        [OK] [X] |
         |_________________________________________________________________|

***/

   zdialog *zd = zdialog_new("Select Image Files",Mwin,"OK"," X ",null);
   zd_select_files = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"scrwin","scrwin","hb1",0,"expand");
   zdialog_add_widget(zd,"text","files","scrwin");
   zdialog_add_widget(zd,"frame","fr12","hb1",0,"space=5");                      //  for thumbnail - added later
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","remove","hb2","Remove","space=8");
   zdialog_add_widget(zd,"button","clear","hb2","Clear","space=8");
   zdialog_add_widget(zd,"button","addall","hb2","Add All","space=8");
   zdialog_add_widget(zd,"button","addcurr","hb2","Add Current","space=8");
   zdialog_add_widget(zd,"label","space","hb2",0,"expand");
   zdialog_add_widget(zd,"button","disable","hb2","Disable","space=8");
   zdialog_add_ttip(zd,"remove","remove selected file from list");
   zdialog_add_ttip(zd,"clear","remove all files in list");
   zdialog_add_ttip(zd,"addall","add all gallery files to list");
   zdialog_add_ttip(zd,"addcurr","add current file to list");
   zdialog_add_ttip(zd,"disable","disable thumbnail selection to enable popup menus etc. \n"
                                 "use [Enable] to resume thumbnail selection");

   Ftext = zdialog_gtkwidget(zd,"files");
   textwidget_set_eventfunc(Ftext,callbackfunc);                                 //  set mouse/KB event function

   GtkWidget *frame = zdialog_gtkwidget(zd,"fr12");                              //  drawing area for thumbnail image
   drawarea = gtk_drawing_area_new();
   gtk_widget_set_size_request(drawarea,256,258);
   gtk_container_add(GTK_CONTAINER(frame),drawarea);
   G_SIGNAL(drawarea,"draw",showthumb,0);

   zdialog_resize(zd,600,0);                                                     //  start dialog
   zdialog_run(zd,dialog_event,"save");                                          //  keep relative position

   textwidget_clear(Ftext);
   GSselect = 0;
   currline = -1;
   imagefile = 0;

   if (fclear) {                                                                 //  clear prior list if requested
      for (ii = 0; ii < SFcount; ii++)
         zfree(SelFiles[ii]);
      SFcount = 0;
   }
   
   Fsetcurr = fsetcurr;                                                          //  set current file if requested         24.20

   for (ii = 0; ii < SFcount; ii++)                                              //  move prev. selected files to dialog
   {                                                                             //    and clear selected files list
      textwidget_append(Ftext,0,"%s\n",SelFiles[ii]);
      zfree(SelFiles[ii]);
   }

   GSselect = SFcount;
   SFcount = 0;

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

   zdialog_wait(zd);                                                             //  wait for dialog completion

   if (zd->zstat != 1)                                                           //  cancel
   {
      zdialog_free(zd);                                                          //  kill dialog - no selectd files
      zd_select_files = 0;
      return;
   }

   for (ii = 0; ii < GSselect; ii++)                                             //  [OK]
   {
      imagefile = textwidget_line(Ftext,ii,1);                                   //  get selected files from dialog
      if (! imagefile || ! *imagefile) continue;
      SelFiles[SFcount++] = imagefile;
   }

   for (ii = kk = 0; ii < SFcount; ii++)                                         //  look for duplicates
      for (kk = ii + 1; kk < SFcount; kk++)
         if (strmatch(SelFiles[ii],SelFiles[kk])) dups++;

   if (dups)
      yn = zmessageYN(Mwin,"remove %d duplicates?",dups);                        //  ask user

   if (yn)
   {
      for (ii = kk = 0; ii < SFcount; ii++) {                                    //  remove duplicates
         if (! SelFiles[ii]) continue;
         for (kk = ii + 1; kk < SFcount; kk++) {
            if (! SelFiles[kk]) continue;
            if (strmatch(SelFiles[ii],SelFiles[kk])) {
               zfree(SelFiles[kk]);
               SelFiles[kk] = 0;
            }
         }
      }

      for (ii = kk = 0; ii < SFcount; ii++)
         if (SelFiles[ii])
            SelFiles[kk++] = SelFiles[ii];
      SFcount = kk;
   }

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


//  gallery getfiles dialog event function

int galselnames::dialog_event(zdialog *zd, ch *event)
{
   using namespace galselnames;

   ch             *file, *ftemp;
   int            Nth;
   FTYPE          ftype;

   if (zd->zstat) {
      if (zd->zstat < 0) {
         zd_select_files = 0;                                                    //  [x] kill dialog
         gallery(0,"paint",-1);
      }
      return 1;
   }

   if (strmatch(event,"disable"))                                                //  toggle disable/enable status
   {
      if (zd_select_files) {
         zdialog_put_data(zd,"disable","Enable");
         zd_select_files = 0;                                                    //  suspend, no capture gallery clicks
      }                                                                          //  (button is the opposite)
      else {
         zdialog_put_data(zd,"disable","Disable");                               //  resume, capture gallery clicks
         zd_select_files = zd;
      }
      return 1;
   }

   if (strmatch(event,"remove"))                                                 //  remove file from list
   {
      if (! GSselect) return 1;
      if (currline < 0) return 1;
      ftemp = textwidget_line(Ftext,currline,0);                                 //  file (text line) to remove (keep \n)
      if (! ftemp) return 1;
      textwidget_delete(Ftext,currline);                                         //  remove from dialog list
      GSselect--;
      if (currline > GSselect-1) currline = GSselect - 1;
      textwidget_highlight_line(Ftext,currline);                                 //  next line
      showthumb();                                                               //  show thumbnail
      gallery(0,"paint",-1);
   }

   if (strmatch(event,"clear")) {                                                //  clear all files
      textwidget_clear(Ftext);
      GSselect = 0;
      currline = -1;
      showthumb();                                                               //  blank thumbnail
      gallery(0,"paint",-1);
   }

   if (strmatch(event,"addall"))                                                 //  insert all files in image gallery
   {
      for (Nth = 0; ; Nth++)
      {
         if (GSselect == SFmax) {
            zmessageACK(Mwin,"exceed %d selected files",SFmax);
            break;
         }

         ftemp = gallery(0,"getR",Nth);                                          //  next file
         if (! ftemp) break;

         ftype = image_file_type(ftemp);                                         //  must be image or RAW file
         if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) continue;

         file = f_realpath(ftemp);                                               //  use real path
         if (! file) continue;

         GSselect++;
         textwidget_append(Ftext,0,"%s\n",file);                                 //  append - could be insert
         zfree(file);
      }

      textwidget_scroll(Ftext,-1);                                               //  scroll to end
      currline = GSselect - 1;                                                   //  position at last file
      showthumb();
      gallery(0,"paint",-1);
   }

   if (strmatch(event,"addcurr"))                                                //  add current file to selection 
      add_file(curr_file);

   return 1;
}


//  See if image file is in the file list already or not.
//  Return the last matching line number or -1 if not found.

int galselnames::find_file(ch *imagefile)
{
   using namespace galselnames;

   int      line;
   ch       *ftemp;

   for (line = GSselect-1; line >= 0; line--)
   {
      ftemp = textwidget_line(Ftext,line,1);                                     //  get file without \n
      if (! ftemp) continue;
      if (strmatch(ftemp,imagefile)) {
         zfree(ftemp);
         return line;
      }
      zfree(ftemp);
   }

   return -1;
}


//  add image file to list, set thumbnail = file

void galselnames::add_file(ch *imagefile)
{
   using namespace galselnames;

   int      ftype;
   ch       *RP;

   if (GSselect == SFmax) {
      zmessageACK(Mwin,"exceed %d selected files",SFmax);
      return;
   }

   ftype = image_file_type(imagefile);                                           //  must be image or RAW file
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) return;
   RP = f_realpath(imagefile);                                                   //  use real path
   if (! RP) return;
   textwidget_append2(Ftext,0,"%s\n",RP);                                        //  append file to list
   zfree(RP);
   GSselect++;
   currline = GSselect-1;
   textwidget_highlight_line(Ftext,currline);
   showthumb();                                                                  //  update thumbnail
   if (FGM == 'G') gallery(0,"paint",-1);                                        //  highlight in gallery
   if (Fsetcurr) f_open(imagefile);                                              //  make current                          24.20
   return;
}


//  external access to add a file to the current selection

void add_file_to_selection(ch *imagefile)                                        //  24.20
{
   using namespace galselnames;
   if (! zd_select_files) return;
   add_file(imagefile);
   return;
}


//  remove image file at last position found, set thumbnail = next
//  called when gallery thumbnail is right-clicked

void galselnames::remove_file(ch *imagefile)
{
   using namespace galselnames;

   int      line;

   line = find_file(imagefile);                                                  //  find last instance
   if (line < 0) return;
   currline = line;
   showthumb();
   textwidget_delete(Ftext,currline);
   GSselect--;
   if (currline > GSselect-1) currline = GSselect - 1;
   textwidget_highlight_line(Ftext,currline);
   showthumb();
   gallery(0,"paint",-1);
   return;
}


//  called from image gallery window when a thumbnail is clicked
//  add image file to list at current position, set thumbnail = file

void select_files_Lclick_func(int Nth)                                           //  left click, add
{
   galselnames::Xclick_func(Nth,'L');
   if (clicked_file) zfree(clicked_file);
   clicked_file = 0;
   return;
}

void select_files_Rclick_func(int Nth)                                           //  right click, find and remove
{
   galselnames::Xclick_func(Nth,'R');
   if (clicked_file) zfree(clicked_file);
   clicked_file = 0;
   return;
}

void galselnames::Xclick_func(int Nth, ch LR)
{
   using namespace galselnames;

   ch             *imagefile;
   FTYPE          ftype;
   static int     pNth = -1;                                                     //  previously clicked file
   int            nn, incr;

   if (! zd_select_files) return;                                                //  should not happen
   if (Nth < 0) return;                                                          //  gallery gone ?

   imagefile = gallery(0,"getR",Nth);                                            //  get file at clicked position
   if (! imagefile) {
      pNth = -1;
      return;
   }

   ftype = image_file_type(imagefile);                                           //  must be image or RAW file
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) {
      pNth = -1;
      return;
   }

   if (LR == 'R') {                                                              //  right click, unselect
      remove_file(imagefile);
      return;
   }

   if (! KBshiftkey)                                                             //  no shift key
   {
      pNth = Nth;                                                                //  possible start of range
      add_file(imagefile);                                                       //  add to list
      return;
   }

   if (KBshiftkey)                                                               //  shift key, end of range
   {
      if (pNth < 0) return;                                                      //  no start of range, ignore
      if (pNth > Nth) incr = -1;                                                 //  range is descending
      else incr = +1;                                                            //  ascending

      for (nn = pNth+incr; nn != Nth+incr; nn += incr)                           //  add all files from pNth to Nth
      {                                                                          //    excluding pNth (already added)
         imagefile = gallery(0,"get",nn);
         if (! imagefile) continue;
         ftype = image_file_type(imagefile);                                     //  only image and RAW files
         if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) continue;
         add_file(imagefile);
      }
      pNth = -1;                                                                 //  no prior
      return;
   }

   return;
}


//  process mouse or key click in file list
//  set new current position and set thumbnail = clicked file

void galselnames::callbackfunc(GtkWidget *widget, int line, int posn, int KBkey)
{
   if (KBkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   if (! GSselect) return;                                                       //  no lines

   if (KBkey >= 0xfd00) {
      if (KBkey == GDK_KEY_Up) currline--;                                       //  KB arrow key navigation
      if (KBkey == GDK_KEY_Down) currline++;
      if (KBkey == GDK_KEY_Page_Up) currline -= 10;
      if (KBkey == GDK_KEY_Page_Down) currline += 10;
      if (KBkey == GDK_KEY_Home) currline = 0;
      if (KBkey == GDK_KEY_End) currline = GSselect - 1;
      if (currline < 0) currline = 0;
      if (currline > GSselect-1) currline = GSselect - 1;
      textwidget_highlight_line(Ftext,currline);                                 //  highlight line
      textwidget_scroll(Ftext,currline);
   }

   if (line >= 0) {                                                              //  line clicked
      textwidget_highlight_line(Ftext,line);                                     //  highlight
      currline = line;
      showthumb();
      return;
   }

   showthumb();
   return;
}


//  show thumbnail for file at current position in select list

void galselnames::showthumb()
{
   using namespace galselnames;

   GdkWindow      *gdkwin;
   ch             thumbfile[XFCC];
   PIXBUF         *thumbpxb = 0;
   GError         *gerror = 0;
   int            err;

   static draw_context_t   draw_context;
   cairo_t                 *cr;

   if (! GSselect) return;
   if (currline < 0) return;

   gdkwin = gtk_widget_get_window(drawarea);
   cr = draw_context_create(gdkwin,draw_context);

   imagefile = textwidget_line(Ftext,currline,1);                                //  get curr. image without \n

   if (imagefile) {
      err = image2thumbfile2(imagefile,thumbfile);                               //  use thumbnail file
      if (! err)
         thumbpxb = gdk_pixbuf_new_from_file_at_size(thumbfile,256,256,&gerror);
   }

   if (thumbpxb) {
      cairo_set_source_rgb(cr,1,1,1);                                            //  white background
      cairo_paint(cr);
      gdk_cairo_set_source_pixbuf(cr,thumbpxb,0,0);                              //  + thumbnail
      cairo_paint(cr);
      g_object_unref(thumbpxb);
   }
   else {
      cairo_set_source_rgb(cr,1,1,1);                                            //  white background only
      cairo_paint(cr);
   }

   draw_context_destroy(draw_context);
   return;
}


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

//  gallery menu, resizable thumbnail view

void m_thumbview(GtkWidget *, ch *menu)
{
   F1_help_topic = "thumb view";
   Plog(1,"m_thumbview \n");
   Fthumbview = 1;
   if (thumbsize < 256) thumbsize = 256;
   viewmode('G');
   gallery(0,"paint",-1);
   return;
}


//  gallery menu, thumbnail with basic metadata view

void m_metaview(GtkWidget *, ch *menu)
{
   F1_help_topic = "meta view";
   Plog(1,"m_metaview \n");
   Fthumbview = 2;
   viewmode('G');
   gallery(0,"paint",-1);
   return;
}


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

//  Show recently viewed image files.

void m_recentfiles(GtkWidget *, ch *menu)
{
   F1_help_topic = "recent files";

   Plog(1,"m_recentfiles \n");

   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   navi::gallerytype = RECENT;                                                   //  gallery type = recent files
   gallery(recentfiles_file,"initF",0);                                          //  generate gallery of recent files
   gallery(0,"paint",0);
   viewmode('G');
   return;
}


//  add a new file to the list of recent files, first position
//  ( < 1 millisec. typical at 1000 file limit)

void add_recent_file(ch *newfile)
{
   zlist_t  *ZLfiles;
   int      ii;

   ZLfiles = zlist_from_file(recentfiles_file);
   if (! ZLfiles) ZLfiles = zlist_new(0);
   ii = zlist_find(ZLfiles,newfile,0);                                           //  if file already present, remove it
   if (ii >= 0) zlist_remove(ZLfiles,ii);
   zlist_insert(ZLfiles,newfile,0);                                              //  insert in 1st position
   zlist_clear(ZLfiles,1000);                                                    //  truncate to 1000 files                24.30
   zlist_to_file(ZLfiles,recentfiles_file);
   zlist_free(ZLfiles);
   return;
}


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

//  Report the newest or most recently modified image files,
//  based on metadata photo date or file mod date.

namespace newfiles
{
   struct newfile_t  {                                                           //  new file record
      ch          *file;                                                         //  image file
      ch          fdate[16];                                                     //  date-time, yyyymmddhhmmss
   };
}


//  menu function

void m_newfiles(GtkWidget *, ch *menu)
{
   using namespace newfiles;

   int newfile_comp(ch *rec1, ch *rec2);

   ch   *mess = "Use metadata photo date or \n file modification date?";

   int         ii, jj, cc, sort, Nxrec;
   xxrec_t     *xxrec;
   FILE        *fid;
   newfile_t    *newfile = 0;

   F1_help_topic = "newest files";

   Plog(1,"m_newfiles \n");

   if (Findexvalid == 0) {
      zmessageACK(Mwin,"image index disabled");                                  //  no image index
      return;
   }

   if (Nxxrec == 0) {
      zmessageACK(Mwin,"no files found");
      return;
   }

   cc = Nxxrec * sizeof(newfile_t);                                              //  allocate memory
   newfile = (newfile_t *) zmalloc(cc,"newfiles");

   if (menu && strmatch(menu,"metadata")) sort = 1;
   else if (menu && strmatch(menu,"file")) sort = 2;
   else sort = zdialog_choose(Mwin,"mouse",mess,"metadata","File",null);
   if (sort < 1) return;                                                         //  cancel

   for (ii = jj = 0; ii < Nxxrec; ii++)                                          //  loop image index table
   {
      xxrec = xxrec_tab[ii];
      newfile[jj].file = xxrec->file;                                            //  image file

      if (sort == 1) {
         if (strmatch(xxrec->pdate,"null")) continue;                            //  use metadata photo date
         strncpy0(newfile[jj].fdate,xxrec->pdate,15);                            //  ignore images without photo date
      }
      else strncpy0(newfile[jj].fdate,xxrec->fdate,15);                          //  else use file mod date

      jj++;                                                                      //  selected files
   }

   Nxrec = jj;                                                                   //  final count

   if (Nxrec > 1)                                                                //  sort index recs. by file date
      HeapSort((ch *) newfile, sizeof(newfile_t), Nxrec, newfile_comp);

   fid = fopen(searchresults_file,"w");                                          //  open output file
   if (! fid) {
      zmessageACK(Mwin,"file error: %s",strerror(errno));
      goto cleanup;
   }

   for (ii = 0; ii < 1000 && ii < Nxrec; ii++)                                   //  output newest 1000 image files
      fprintf(fid,"%s\n",newfile[ii].file);

   fclose(fid);

cleanup:

   zfree(newfile);                                                               //  free memory
   navi::gallerytype = NEWEST;                                                   //  newest files
   gallery(searchresults_file,"initF",0);                                        //  generate gallery of files
   gallery(0,"paint",0);
   viewmode('G');

   return;
}


//  Compare 2 newfile records by file date-time
//  return <0 =0 >0  for  rec2 < = > rec1 (descending sequence)

using namespace newfiles;

int newfile_comp(ch *rec1, ch *rec2)
{
   ch *date1 = ((newfile_t *) rec1)->fdate;
   ch *date2 = ((newfile_t *) rec2)->fdate;
   return strcmp(date2,date1);
}


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

//  menu function
//  dialog to choose gallery sort order and sort the gallery

void m_gallery_sort(GtkWidget *, ch *menu)
{
   zdialog     *zd;
   int         zstat, nn;
   ch          albumfile[200], *pp, *pp2;

   ch     *resetmess = " Reset all galleries\n to file name ascending";

   F1_help_topic = "gallery sort";

   Plog(1,"m_gallery_sort \n");

/***
          _________________________________
         |         Gallery Sort            |
         |                                 |
         |  (o) File Name                  |
         |  (o) File Name/Number           |
         |  (o) File Mod Date/Time         |
         |  (o) Photo Date/Time (metadata) |
         |  (o) File Size (bytes)          |
         |  (o) Image Size (pixels)        |
         |  (o) Metadata from Search       |
         |  (o) ascending  (o) descending  |
         |                                 |
         |  [x] reset all galleries        |
         |      to file name ascending     |
         |                                 |
         |                     [Apply] [X] |
         |_________________________________|

***/

   zd = zdialog_new("Gallery Sort",Mwin,"Apply"," X ",null);                     //  user dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1");
   zdialog_add_widget(zd,"radio","filename","vb1","File Name");
   zdialog_add_widget(zd,"radio","filenumber","vb1","File Name/Number");
   zdialog_add_widget(zd,"radio","filedate","vb1","File Mod Date/Time");
   zdialog_add_widget(zd,"radio","photodate","vb1","Photo Date/Time (metadata)");
   zdialog_add_widget(zd,"radio","filesize","vb1","File Size (bytes)");
   zdialog_add_widget(zd,"radio","pixelsize","vb1","Image Size (pixels)");
   zdialog_add_widget(zd,"radio","metadata","vb1","Metadata from Search Function");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","ascending","hb2","ascending","space=4");
   zdialog_add_widget(zd,"radio","descending","hb2","descending","space=2");
   zdialog_add_widget(zd,"hbox","hbreset","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","reset","hbreset",resetmess,"space=4");

   zdialog_add_ttip(zd,"metadata","metadata from Search Function, metadata report");

   zdialog_stuff(zd,"filename",0);                                               //  all buttons off
   zdialog_stuff(zd,"filenumber",0);
   zdialog_stuff(zd,"filedate",0);                                               //  GTK radio buttons not reliable
   zdialog_stuff(zd,"photodate",0);                                              //  (vbox works, hbox does not)
   zdialog_stuff(zd,"filesize",0);
   zdialog_stuff(zd,"pixelsize",0);
   zdialog_stuff(zd,"metadata",0);
   zdialog_stuff(zd,"metadata",0);
   zdialog_stuff(zd,"descending",0);
   zdialog_stuff(zd,"reset",0);

   if (gallerysort == FNAME || gallerysort == SNONE)
      zdialog_stuff(zd,"filename",1);

   if (gallerysort == FNUMBER)
      zdialog_stuff(zd,"filenumber",1);

   if (gallerysort == FDATE)
      zdialog_stuff(zd,"filedate",1);

   if (gallerysort == PDATE)
      zdialog_stuff(zd,"photodate",1);

   if (gallerysort == FSIZE)
      zdialog_stuff(zd,"filesize",1);

   if (gallerysort == PSIZE)
      zdialog_stuff(zd,"pixelsize",1);

   if (gallerysort == MDATA)
      zdialog_stuff(zd,"metadata",1);

   if (galleryseq == ASCEND || galleryseq == QNONE)
      zdialog_stuff(zd,"ascending",1);

   if (galleryseq == DESCEND)
      zdialog_stuff(zd,"descending",1);

   zdialog_set_modal(zd);
   zdialog_run(zd,0,"mouse");                                                    //  run dialog, wait for completion
   zstat = zdialog_wait(zd);

   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"filename",nn);                                              //  get user sort type
   if (nn) gallerysort = FNAME;
   zdialog_fetch(zd,"filenumber",nn);
   if (nn) gallerysort = FNUMBER;
   zdialog_fetch(zd,"filedate",nn);
   if (nn) gallerysort = FDATE;
   zdialog_fetch(zd,"photodate",nn);
   if (nn) gallerysort = PDATE;
   zdialog_fetch(zd,"filesize",nn);
   if (nn) gallerysort = FSIZE;
   zdialog_fetch(zd,"pixelsize",nn);
   if (nn) gallerysort = PSIZE;
   zdialog_fetch(zd,"metadata",nn);
   if (nn) gallerysort = MDATA;

   zdialog_fetch(zd,"ascending",nn);                                             //  get ascending/descending
   if (nn) galleryseq = ASCEND;
   else galleryseq = DESCEND;

   zdialog_fetch(zd,"reset",nn);                                                 //  reset all gallery sort memory
   if (nn) {                                                                     //  (revert to file name ascending)
      gallery_memory("reset");
      gallerysort = FNAME;
      galleryseq = ASCEND;
   }

   zdialog_free(zd);

   gallery(0,"sort",-1);                                                         //  sort the gallery
   gallery(0,"paint",0);                                                         //  paint, position = 0

   if (gallerytype == ALBUM)                                                     //  an album was sorted
   {
      pp = strrchr(galleryname,'/');                                             //  get album name
      if (pp) pp++;
      else pp = galleryname;
      pp2 = strstr(pp,"-sorted");                                                //  append "-sorted"
      if (pp2) *pp2 = 0;                                                         //  avoid "-sorted-sorted"
      snprintf(albumfile,200,"%s/%s-sorted",albums_folder,pp);
      album_create_from_gallery(albumfile);                                      //  new album, oldname-sorted
      album_show(albumfile);
   }

   return;
}


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

//  screen the current gallery for matching files
//  output matching files to album "gallery_screen"

namespace gallery_screen_names
{
   int      Fall, Forg, Fver, Flast, Frating, Ftags, Ftext;
   int      lorat, hirat;
   ch       tags[100], text[100];
}


//  menu function

void m_gallery_screen(GtkWidget *, ch *menu)                                     //  revamped 24.60
{
   using namespace gallery_screen_names;

   zdialog  *zd;
   int      zstat, ii, kk1, kk2, Nfiles, Fkeep;
   ch       albumfile[AFCC];
   ch       *file, *pp1, *pp2;
   FTYPE    ftype;
   FILE     *fid;
   xxrec_t  *xxrec;
   int      rating;

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

   F1_help_topic = "gallery screen";

   Plog(1,"m_gallery_screen \n");

   if (Gfiles < 1) {
      zmessageACK(Mwin,"gallery is empty");
      return;
   }

/***
       _________________________________________________________________
      |                     Gallery Screen                              |
      |                                                                 |
      |  [_] all files  [_] originals  [_] versions  [_] last versions  |
      |                                                                 |
      |  [_] rating range [__] [__]                                     |     inlcude if rating within range
      |  [_] tag names [______________________________________________] |     include if any tag present (comma separated)
      |  [_] filename text [__________________________________________] |     include if any text present (comma separated) 
      |                                                                 |
      |                                                        [OK] [X] |
      |_________________________________________________________________|

***/

   zd = zdialog_new("Gallery Screen",Mwin,"OK"," X ",null);

   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","ckall","hbfiles","all files");
   zdialog_add_widget(zd,"check","ckorg","hbfiles","originals");
   zdialog_add_widget(zd,"check","ckver","hbfiles","versions");
   zdialog_add_widget(zd,"check","cklast","hbfiles","last versions");

   zdialog_add_widget(zd,"hbox","hbrat","dialog");
   zdialog_add_widget(zd,"check","ckrat","hbrat","rating range");
   zdialog_add_widget(zd,"zspin","lorat","hbrat","0|5|1|0","size=3|space=5");
   zdialog_add_widget(zd,"zspin","hirat","hbrat","0|5|1|5","size=3");

   zdialog_add_widget(zd,"hbox","hbtags","dialog");
   zdialog_add_widget(zd,"check","cktags","hbtags","tag names");
   zdialog_add_widget(zd,"zentry","tags","hbtags",0,"space=5|expand");

   zdialog_add_widget(zd,"hbox","hbtext","dialog");
   zdialog_add_widget(zd,"check","cktext","hbtext","filename text");
   zdialog_add_widget(zd,"zentry","text","hbtext",0,"space=5|expand");

   zdialog_add_ttip(zd,"ckall","include all files");
   zdialog_add_ttip(zd,"ckorg","include original files");
   zdialog_add_ttip(zd,"ckver","include edited versions");
   zdialog_add_ttip(zd,"cklast","include last versions only");
   zdialog_add_ttip(zd,"ckrat","get images within rating range");
   zdialog_add_ttip(zd,"cktags","get images with matching tags (comma separated)");
   zdialog_add_ttip(zd,"cktext","get images having text in filename (comma separated)");

   zdialog_load_inputs(zd);
   zdialog_resize(zd,400,0);
   zdialog_run(zd,gallery_screen_dialog_event,"mouse");                          //  start dialog
   zdialog_send_event(zd,"init");
   zstat = zdialog_wait(zd);                                                     //  wait

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

   zdialog_fetch(zd,"ckall",Fall);                                               //  get all inputs
   zdialog_fetch(zd,"ckorg",Forg);  
   zdialog_fetch(zd,"ckver",Fver);  
   zdialog_fetch(zd,"cklast",Flast);
   zdialog_fetch(zd,"ckrat",Frating); 
   zdialog_fetch(zd,"cktags",Ftags);
   zdialog_fetch(zd,"cktext",Ftext);
   zdialog_fetch(zd,"lorat",lorat);
   zdialog_fetch(zd,"hirat",hirat);
   zdialog_fetch(zd,"tags",tags,100);
   zdialog_fetch(zd,"text",text,100);

   zdialog_free(zd);

   snprintf(albumfile,AFCC,"%s/gallery_screen",albums_folder);                   //  output to album "gallery_screen"

   fid = fopen(albumfile,"w");                                                   //  open/write album file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   Nfiles = 0;

   for (ii = 0; ii < Gfiles; ii++)                                               //  loop files in current gallery
   {
      file = gallery(0,"getR",ii);
      if (! file) break;
      ftype = image_file_type(file);                                             //  must be image type file
      if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) continue;
      
      Fkeep = 0;
      
      if (Fall) Fkeep = 1;                                                       //  keep all files
      
      if (! Fkeep && Forg) {
         pp1 = strrchr(file,'/');                                                //  /.../filename.vNN.jpg
         if (pp1) pp1 = strrchr(pp1,'.');                                        //                   |
         if (pp1 && ! strmatchN(pp1-4,".v",2)) Fkeep = 1;                        //                   pp1
      }

      if (! Fkeep && Fver) {
         pp1 = strrchr(file,'/');                                                //  /.../filename.vNN.jpg
         if (pp1) pp1 = strrchr(pp1,'.');                                        //                   |
         if (pp1 && strmatchN(pp1-4,".v",2)) Fkeep = 1;                          //                   pp1
      }
      
      if (! Fkeep && Flast) {
         pp1 = file_newest_version(file);
         if (strmatch(pp1,file)) Fkeep = 1;
      }
      
      if (! Fkeep) continue;
      
      if (Frating) {
         xxrec = get_xxrec(file);                                                //  get image rating
         if (! xxrec) {
            Plog(1,"file not indexed: %s \n",file);
            continue;
         }
         rating = xxrec->rating[0] - '0';                                        //  numeric 0-5
         if (rating < lorat || rating > hirat) continue;                         //  test within limits
      }

      if (Ftags) {
         xxrec = get_xxrec(file);                                                //  get image tags
         if (! xxrec) {
            Plog(1,"file not indexed: %s \n",file);
            continue;
         }

         for (kk1 = 1; ; kk1++)                                                  //  loop tags to search
         {
            pp1 = substring(tags,',',kk1);
            if (! pp1) goto nomatch;                                             //  no match found

            for (kk2 = 1; ; kk2++) {                                             //  loop image tags
               pp2 = substring(xxrec->tags,',',kk2);
               if (! pp2) break;
               if (strmatchcase(pp1,pp2)) goto tagmatch;
            }
         }
      }
      tagmatch:

      if (Ftext) {
         for (kk1 = 1; ; kk1++) {                                                //  get text strings to match
            pp1 = substring(text,',',kk1);
            if (! pp1) goto nomatch;                                             //  no match found
            pp2 = strrchr(file,'/');
            if (strcasestr(pp2,pp1)) goto textmatch;
         }
      }
      textmatch:

      fprintf(fid,"%s\n",file);                                                  //  screened output file

      Nfiles++;
      if (Nfiles == maxalbumfiles) {
         zmessageACK(Mwin,"max. album files reached, results truncated");
         break;
      }
      
      nomatch: continue;                                                         
   }


   fclose(fid);
   
   if (Nfiles) {
      zstrcopy(curr_album,albumfile,"albums");                                   //  make current album
      zmessage_post(Mwin,"20/20",3,"new album created");
      album_show();
   }
   else {
      zmessageACK(Mwin,"no matching files found");
      remove(albumfile);
      return;
   }

   return;
}


//  dialog event and completion function

int gallery_screen_dialog_event(zdialog *zd, ch *event)
{
   using namespace gallery_screen_names;
   
   if (strmatch(event,"ckall")) {                                                //  if all, clear org, ver, last
      zdialog_fetch(zd,"ckall",Fall);
      if (Fall) Forg = Fver = Flast = 0;
   }

   if (strmatch(event,"ckorg")) {                                                //  if org, clear all
      zdialog_fetch(zd,"ckorg",Forg);
      if (Forg) Fall = 0;
   }

   if (strmatch(event,"ckver")) {                                                //  if ver, clear all, last
      zdialog_fetch(zd,"ckver",Fver);
      if (Fver) Fall = Flast = 0;
   }
   
   if (strmatch(event,"cklast")) {                                               //  if last, clear all, ver
      zdialog_fetch(zd,"cklast",Flast);
      if (Flast) Fall = Fver = 0;
   }

   if (Forg && Fver) {                                                           //  if org and ver, clear both
      Fall = 1;                                                                  //    and set all
      Forg = Fver = 0;
   }
   
   if (! Forg && ! Fver && ! Flast) Fall = 1;                                    //  default
   
   zdialog_stuff(zd,"ckall",Fall);                                               //  update dialog 
   zdialog_stuff(zd,"ckorg",Forg);
   zdialog_stuff(zd,"ckver",Fver);
   zdialog_stuff(zd,"cklast",Flast);
   zdialog_fetch(zd,"ckrat",Frating);
   zdialog_fetch(zd,"cktags",Ftags);
   zdialog_fetch(zd,"cktext",Ftext);
   zdialog_fetch(zd,"lorat",lorat);
   zdialog_fetch(zd,"hirat",hirat);
   zdialog_fetch(zd,"tags",tags,100);
   zdialog_fetch(zd,"text",text,100);

   return 1;
}


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

//  set the gallery from the current image file folder

void m_current_folder(GtkWidget*, ch *menu)
{
   F1_help_topic = "current folder";
   Plog(1,"m_current_folder \n");

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

   gallery(curr_file,"init",0);                                                  //  new gallery
   gallery(curr_file,"paint",0);                                                 //  position at curr. file
   curr_file_posn = file_position(curr_file,0);                                  //  file position in gallery list
   viewmode('G');
   return;
}


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

//  set the gallery from a recently used file folder

void m_recent_folders(GtkWidget*, ch *menu)                                      //  24.60
{
   void  recent_folders_choose(GtkWidget *, ch *);

   int         nf = 0;
   ch          *folder;

   F1_help_topic = "recent folders";
   Plog(1,"m_recent_folders \n");
   
   if (! ZLrecent_folders) ZLrecent_folders = zlist_from_file(recent_folders_file);
   if (ZLrecent_folders) nf = zlist_count(ZLrecent_folders);
   if (! nf) {
      zmessageACK(Mwin,"no saved folders");
      return;
   }

   folder = zlist_choose(ZLrecent_folders);
   if (! folder) return;

   gallery(folder,"init",0);                                                     //  set new gallery
   gallery(0,"sort",-2);                                                         //  last sort and position
   gallery(0,"paint",-1);                                                        //  show
   viewmode('G');

   return;
}


//  add a new folder to the list of recent folders, first position

void add_recent_folder(ch *foldername)                                           //  24.60
{
   int      ii;

   if (! ZLrecent_folders) ZLrecent_folders = zlist_from_file(recent_folders_file);
   if (! ZLrecent_folders) ZLrecent_folders = zlist_new(0);

   ii = zlist_find(ZLrecent_folders,foldername,0);
   if (ii >= 0) zlist_remove(ZLrecent_folders,ii);
   zlist_insert(ZLrecent_folders,foldername,0); 
   zlist_clear(ZLrecent_folders,10);
   zlist_to_file(ZLrecent_folders,recent_folders_file);

   return;
}


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

//  generate a clickable list of all image folders
//  show gallery for any folder clicked

namespace folder_tree
{
   #define  maxdirs 10000

   int         Nlines = 0, Fall = 0;
   int         currline = 0;
   zdialog     *zdpop;

   typedef struct {
      ch       *name;                                 //  /dir1/dir2/.../dirN       folder name
      int      Nsubs;                                 //  subfolder count           0-N
      int8     lev;                                   //  folder level              0-N, top/sub/sub ...
      int8     exp;                                   //  folder status             0/1 = collapsed/expanded
      int16    line;                                  //  textwidget line           -1 = not displayed
   }  dlist_t;

   dlist_t  *dlist;
   int      drecl = sizeof(dlist_t);

   int  compfunc(ch *rec1, ch *rec2);
   void callbackfunc(GtkWidget *, int line, int pos, int kbkey);
   void writetext();
}


//  menu function

void m_folder_tree(GtkWidget *, ch *)
{
   using namespace folder_tree;

   int         ii, jj, cc, pcc, NF, err;
   ch          *dir, *pdir, **Flist;

   F1_help_topic = "folder tree";

   Plog(1,"m_folder_tree \n");

/***
          ________________________________
         |       Folder Tree              |
         |  ____________________________  |
         | |                            | |
         | | [+] topdir1                | |
         | | [-] topdir2                | |
         | |         subdir1            | |
         | |     [+] subdir2            | |
         | |         subdir3            | |
         | | [+] topdir3                | |
         | |         ...                | |
         | |____________________________| |
         |                                |
         |                           [OK] |
         |________________________________|

***/

   if (dlist) goto report;                                                       //  already done

   cc = drecl * maxdirs;
   dlist = (dlist_t *) zmalloc(cc,"folder_tree");                                //  memory for folder list
   Fall = 0;

   for (ii = 0; ii < Ntopfolders; ii++)                                          //  loop all top image folders
   {
      dlist[Fall].name = topfolders[ii];
      Fall++;
      if (Fall == maxdirs) break;

      err = find_imagefiles(topfolders[ii],8+16,Flist,NF);                       //  folders, all levels
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         continue;
      }

      for (jj = 0; jj < NF; jj++)                                                //  add to folders list
      {
         dlist[Fall].name = zstrdup(Flist[jj],"folder_tree");
         Fall++;
         if (Fall == maxdirs) break;
      }

      if (NF) zfree(Flist);
      if (Fall == maxdirs) break;
   }

   if (Fall > 1)
      HeapSort((ch *) dlist,drecl,Fall,compfunc);                                //  sort alphabetically

   for (ii = 0; ii < Fall; ii++)                                                 //  loop all folders
   {
      dir = dlist[ii].name;                                                      //  this folder name
      for (jj = ii-1; jj >= 0; jj--) {                                           //  search backwards for parent
         pdir = dlist[jj].name;                                                  //  previous folder name
         pcc = strlen(pdir);
         if (strmatchN(dir,pdir,pcc) && dir[pcc] == '/') break;                  //  this dir = prev dir + /...
      }
      if (jj >= 0) {                                                             //  parent found
         dlist[ii].lev = dlist[jj].lev + 1;                                      //  level = parent level + 1
         dlist[jj].Nsubs++;                                                      //  add parent subdir count
      }
      else dlist[ii].lev = 0;                                                    //  no parent, level = 0
   }

   for (ii = 0; ii < Fall; ii++)                                                 //  loop all folders
      dlist[ii].exp = 0;                                                         //  expand = no

report:

   zdpop = popup_report_open("Folders",Mwin,300,400,0,0,callbackfunc,"X",0);     //  open report window

   writetext();                                                                  //  write top folders to window
   currline = 0;                                                                 //  first entry

   return;
}


//  sort compare function

int folder_tree::compfunc(ch *rec1, ch *rec2)
{
   dlist_t   *dir1 = (dlist_t *) rec1;
   dlist_t   *dir2 = (dlist_t *) rec2;
   int         nn;

   nn = strcasecmp(dir1->name,dir2->name);
   if (nn) return nn;
   nn = strcmp(dir1->name,dir2->name);
   return nn;
}


//  folder list mouse function

void folder_tree::callbackfunc(GtkWidget *textwidget, int line, int pos, int kbkey)
{
   using namespace folder_tree;

   int         ii;
   ch          *pline, *pp;
   static int  Fbusy = 0;

   if (Fbusy++) return;                                                          //  stop re-entry

   if (line < 0)                                                                 //  KB key
   {
      if (kbkey == GDK_KEY_F1) {                                                 //  key F1 pressed, show help
         showz_docfile(Mwin,"userguide",F1_help_topic);
         goto returnx;
      }

      line = currline;
      if (kbkey == GDK_KEY_Up) line--;                                           //  KB arrow key navigation
      if (kbkey == GDK_KEY_Down) line++;
      if (line < 0) line = 0;
      if (line > Nlines-1) line = Nlines - 1;
      zmainsleep(0.1);                                                           //  restrain to 10/sec.
   }

   pline = textwidget_line(textwidget,line,1);                                   //  textwidget line: ... [x] dirname
   if (! pline || ! *pline) goto returnx;
   textwidget_highlight_line(textwidget,line);
   textwidget_scroll(textwidget,line);
   currline = line;

   for (ii = 0; ii < Fall; ii++)                                                 //  find dlist[] rec corresponding
      if (line == dlist[ii].line) break;                                         //    to textwidget line clicked
   if (ii == Fall) goto returnx;

   if (kbkey == GDK_KEY_Return) {                                                //  Enter key, look for [+] or [-]
      pp = strchr(pline,'[');
      if (pp) pos = pp - pline;                                                  //  simulate click on [*]
   }

   if (pos > 0)                                                                  //  clicked position
   {
      pp = strchr(pline,'[');
      if (pp) {                                                                  //  [+] or [-] button present
         if (pos < (pp-pline)) goto returnx;                                     //  click position before button
         if (pos >= (pp-pline) && pos <= (pp+2-pline)) {                         //  on the button
            if (pp[1] == '+') dlist[ii].exp = 1;                                 //  set expand flag
            if (pp[1] == '-') dlist[ii].exp = 0;
            writetext();                                                         //  refresh textwidget
            textwidget_highlight_line(textwidget,line);
            textwidget_scroll(textwidget,line);
            goto returnx;
         }
      }
   }

   viewmode('G');
   gallery(dlist[ii].name,"init",0);                                             //  folder name clicked
   gallery(0,"sort",-2);                                                         //  recall sort and position
   gallery(0,"paint",-1);                                                        //  show gallery

returnx:
   Fbusy = 0;
   return;
}


//  write all visible folders to text window

void folder_tree::writetext()
{
   using namespace folder_tree;

   int      ii = 0, jj, line = 0;
   ch       *pp, indent[100];
   ch       *expbutt;

   memset(indent,' ',100);

   popup_report_clear(zdpop);

   while (ii < Fall)                                                             //  loop all folders
   {
      jj = dlist[ii].lev * 4;                                                    //  indent 4 blanks per folder level
      if (jj > 99) jj = 99;
      indent[jj] = 0;
      if (dlist[ii].Nsubs == 0) expbutt = "   ";                                 //  no subdirs, no expand button
      else if (dlist[ii].exp) expbutt = "[-]";                                   //  prepare [+] or [-]
      else expbutt = "[+]";
      pp = strrchr(dlist[ii].name,'/');
      if (! pp) continue;
      popup_report_write2(zdpop,0,"%s %s %s \n",indent,expbutt,pp+1);            //  ... [x] dirname
      indent[jj] = ' ';
      dlist[ii].line = line;                                                     //  text line for this folder
      line++;

      if (dlist[ii].exp) {                                                       //  if folder expanded, continue
         ii++;
         continue;
      }

      for (jj = ii + 1; jj < Fall; jj++) {                                       //  not expanded, find next folder
         if (dlist[jj].lev <= dlist[ii].lev) break;                              //    at same or lower level
         dlist[jj].line = -1;                                                    //  no text line for skipped folders
      }
      ii = jj;
   }

   Nlines = line;
   return;
}


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

//  menu function to pre-select files for feeding album, batch and script functions

void m_select_files(GtkWidget *, ch *)
{
   ch       albumfile[200];
   FILE     *fid;
   int      ii;

   F1_help_topic = "select image files";

   Plog(1,"m_select_files \n");

   snprintf(albumfile,200,"%s/selected_files",albums_folder);                    //  "selected files" album

   select_files(0,1);                                                            //  curr file = last selection            24.20
   if (SFcount == 0) {
      remove(albumfile);                                                         //  nothing selected
      return;
   }

   fid = fopen(albumfile,"w");                                                   //  create "selected files" album
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   for (ii = 0; ii < SFcount; ii++)                                              //  fill with selected files
      fprintf(fid,"%s\n",SelFiles[ii]);

   fclose(fid);
   return;
}


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

//  rename the current folder

void m_rename_folder(GtkWidget *, ch *menu)
{
   int  rename_folder_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      zstat, err;
   ch       oldname[XFCC], newname[XFCC];

   F1_help_topic = "rename folder";

   Plog(1,"m_rename_folder \n");

   if (! galleryname) {
      zmessageACK(Mwin,"no current gallery");
      return;
   }

   if (gallerytype != FOLDER) {
      zmessageACK(Mwin,"current gallery is not a folder (file directory)");
      return;
   }

   strcpy(oldname,galleryname);
   strcpy(newname,galleryname);

/***
       _________________________________________
      |             Rename Folder               |
      |                                         |
      |  current folder [_____________________] |
      |      new name   [_____________________] |
      |                                         |
      |                             [Apply] [X] |
      |_________________________________________|

***/

   zd = zdialog_new("Rename Folder",Mwin,"Apply"," X ",null);
   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcf","hbcf","current folder:","space=3");
   zdialog_add_widget(zd,"label","oldname","hbcf",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbnn","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labnn","hbnn","new name","space=3");
   zdialog_add_widget(zd,"zentry","newname","hbnn",0,"space=3|expand");

   zdialog_stuff(zd,"oldname",oldname);                                          //  stuff current folder name
   zdialog_stuff(zd,"newname",newname);                                          //  also basis for new name

   zdialog_run(zd,rename_folder_dialog_event);

   zstat = zdialog_wait(zd);

   zdialog_fetch(zd,"newname",newname,XFCC);                                     //  get new folder name
   zdialog_free(zd);
   if (zstat != 1) return;

   if (strmatch(oldname,newname)) {
      zmessageACK(Mwin,"name not changed");
      return;
   }

   err = rename(oldname,newname);                                                //  do folder rename
   if (err) {
      zmessageACK(Mwin,"rename failed: %s",strerror(errno));
      return;
   }

   zmessageACK(Mwin,"folder renamed: \n %s",newname);

   zmessage_post(Mwin,"parent",3,"updating index and albums");

   m_quick_index(0,0);                                                           //  re-index after folder rename

   album_folder_rename(oldname,newname);                                         //  update filenames in album files

   gallery(newname,"init",0);                                                    //  refresh gallery
   gallery(0,"paint",-1);
   return;
}


//  zdialog event and completion function

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


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

//  create a new subfolder under the current folder

void m_add_subfolder(GtkWidget *, ch *menu)
{
   int  add_subfolder_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      zstat, err;
   ch       pathname[XFCC], subfolder[100];

   F1_help_topic = "add subfolder";

   Plog(1,"m_add_subfolder \n");

   if (! galleryname) {
      zmessageACK(Mwin,"no current gallery");
      return;
   }

   if (gallerytype != FOLDER) {
      zmessageACK(Mwin,"current gallery is not a folder (file directory)");
      return;
   }

/***
       _________________________________________
      |             Add Subfolder               |
      |                                         |
      |  current folder [_____________________] |
      |   new subfolder [_____________________] |
      |                                         |
      |                            [Create] [X] |
      |_________________________________________|

***/

   zd = zdialog_new("Add Subfolder",Mwin,"Create"," X ",null);
   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcf","hbcf","current folder:","space=3");
   zdialog_add_widget(zd,"label","currfol","hbcf",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbnsf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labnsf","hbnsf","new subfolder","space=3");
   zdialog_add_widget(zd,"zentry","subfolder","hbnsf",0,"space=3|expand");

   zdialog_stuff(zd,"currfol",galleryname);

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

   zdialog_fetch(zd,"subfolder",subfolder,100);
   zdialog_free(zd);
   if (zstat != 1) return;

   if (*subfolder < ' ' || strchr(subfolder,'/')) {
      zmessageACK(Mwin,"subfolder name is invalid");
      return;
   }

   *pathname = 0;
   strncatv(pathname,XFCC,galleryname,"/",subfolder,0);

   err = mkdir(pathname,0764);
   if (err) {
      zmessageACK(Mwin,"create folder failed: %s",strerror(errno));
      return;
   }

   gallery(galleryname,"init",0);
   gallery(0,"paint",-1);
   zmessageACK(Mwin,"new subfolder created: \n %s",pathname);

   return;
}


//  zdialog event and completion function

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


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

namespace bookmarknames
{
   #define     maxbmks 50
   ch          *bookmarks[maxbmks];                                              //  bookmark names and files
   int         Nbmks;                                                            //  count of entries
   int         bmkposn;                                                          //  current entry, 0-last
   zdialog     *zd_bookmark;
   GtkWidget   *textwidget;
}

void bookmarks_load();
void bookmarks_refresh();


//  select a bookmark and jump gallery to selected bookmark thumbnail

void m_bookmarks(GtkWidget *, ch *)
{
   using namespace bookmarknames;

   int  bookmarks_dialog_event(zdialog *zd, ch *event);
   void bookmarks_callbackfunc(GtkWidget *, int line, int pos, int kbkey);

   zdialog     *zd;

/***
          _______________________________________________
         |                Bookmarks                      |
         |-----------------------------------------------|
         | bookmarkname1      /topdir/.../filename1.jpg  |
         | bookmarkname2      /topdir/.../filename2.jpg  |
         | bookmarkname3      /topdir/.../filename3.jpg  |
         | bookmarkname4      /topdir/.../filename4.jpg  |
         | bookmarkname5      /topdir/.../filename5.jpg  |
         | bookmarkname6      /topdir/.../filename6.jpg  |
         |                                               |
         |                          [Edit Bookmarks] [X] |
         |_______________________________________________|
***/

   F1_help_topic = "bookmarks";

   Plog(1,"m_bookmarks \n");

   if (zd_edit_bookmarks) return;                                                //  already busy
   if (zd_bookmark) return;

   zd = zdialog_new("Bookmarks",Mwin,"Edit Bookmarks"," X ",null);
   zd_bookmark = zd;
   zdialog_add_widget(zd,"frame","frame","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"scrwin","scrwin","frame");
   zdialog_add_widget(zd,"text","bmklist","scrwin");

   textwidget = zdialog_gtkwidget(zd,"bmklist");                                 //  set mouse/KB event function
   textwidget_set_eventfunc(textwidget,bookmarks_callbackfunc);

   bookmarks_load();                                                             //  get bookmarks from bookmarks file
   bookmarks_refresh();                                                          //  update bookmarks list in dialog

   zdialog_resize(zd,400,300);
   zdialog_run(zd,bookmarks_dialog_event,"mouse");                               //  run dialog
   return;
}


//  dialog event and completion function

int bookmarks_dialog_event(zdialog *zd, ch *event)
{
   using namespace bookmarknames;

   int      zstat;

   zstat = zd->zstat;
   if (! zstat) return 1;                                                        //  wait for completion
   zdialog_free(zd);
   zd_bookmark = 0;
   if (zstat == 1) m_edit_bookmarks(0,0);                                        //  [edit bookmarks] button
   return 1;
}


//  mouse click function to receive clicked bookmarks

void bookmarks_callbackfunc(GtkWidget *, int line, int pos, int kbkey)
{
   using namespace bookmarknames;

   ch     *file;

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

   if (! zd_bookmark) return;

   bmkposn = line;                                                               //  get clicked line
   if (bmkposn < 0 || bmkposn > Nbmks-1) return;
   file = bookmarks[bmkposn] + 32;
   if (! regfile(file)) {
      zmessageACK(Mwin,"file not found");
      return;
   }

   f_open(file,0);                                                               //  open file (for yellow highlight)
   gallery(file,"init",0);                                                       //  go to gallery and file position
   gallery(file,"paint",0);
   viewmode('G');

   return;
}


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

//  edit bookmarks

void m_edit_bookmarks(GtkWidget *, ch *)
{
   using namespace bookmarknames;

   int edit_bookmarks_dialog_event(zdialog *zd, ch *event);
   void edit_bookmarks_callbackfunc(GtkWidget *, int line, int pos, int kbkey);

   zdialog     *zd;
   ch          *bmk_add = "Add bookmark: click on a gallery thumbnail. \n"       //  24.10
                          "Rename bookmark: click it and press [Rename]";

/***
          _______________________________________________
         |           Edit Bookmarks                      |
         |                                               |
         | Click list position. Click thumbnail to add.  |
         |-----------------------------------------------|
         | bookmarkname1      /topdir/.../filename1.jpg  |
         | bookmarkname2      /topdir/.../filename2.jpg  |
         | bookmarkname3      /topdir/.../filename3.jpg  |
         | bookmarkname4      /topdir/.../filename4.jpg  |
         | bookmarkname5      /topdir/.../filename5.jpg  |
         | bookmarkname6      /topdir/.../filename6.jpg  |
         |-----------------------------------------------|
         | [bookmarkname...] [rename] [delete]           |
         |                                      [OK] [X] |
         |_______________________________________________|

***/

   F1_help_topic = "bookmarks";

   Plog(1,"m_edit_bookmarks \n");

   if (zd_edit_bookmarks) return;                                                //  already busy

   zd = zdialog_new("Edit Bookmarks",Mwin,"OK"," X ",null);
   zd_edit_bookmarks = zd;
   zdialog_add_widget(zd,"hbox","hbtip","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtip","hbtip",bmk_add,"space=5");
   zdialog_add_widget(zd,"scrwin","scrwin","dialog",0,"expand");
   zdialog_add_widget(zd,"text","bmklist","scrwin");
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=5");
   zdialog_add_widget(zd,"zentry","bmkname","hbname",0,"space=5|size=30");
   zdialog_add_widget(zd,"button","rename","hbname","Rename","space=5");
   zdialog_add_widget(zd,"button","delete","hbname","Delete","space=5");

   textwidget = zdialog_gtkwidget(zd,"bmklist");                                 //  set mouse/KB event function
   textwidget_set_eventfunc(textwidget,edit_bookmarks_callbackfunc);

   bookmarks_load();                                                             //  load bookmarks from bookmarks file
   bookmarks_refresh();                                                          //  update bookmarks list in dialog

   zdialog_resize(zd,500,400);
   zdialog_run(zd,edit_bookmarks_dialog_event,"save");                           //  run dialog, parallel

   viewmode('G');                                                                //  show current gallery
   return;
}


//  load bookmarks list from bookmarks file

void bookmarks_load()
{
   using namespace bookmarknames;

   ch          buff[XFCC];
   ch          *pp, *pp2;
   FILE        *fid;

   Nbmks = 0;
   fid = fopen(bookmarks_file,"r");
   if (fid) {
      while (true) {
         pp = fgets_trim(buff,XFCC,fid,1);                                       //  next bookmark rec.
         if (! pp) break;
         if (strlen(pp) < 40) continue;
         pp2 = strchr(pp+32,'/');                                                //  verify bookmark
         if (! pp2) continue;
         if (! regfile(pp2)) continue;
         bookmarks[Nbmks] = zstrdup(pp,"bookmarks");                             //  fill bookmark list
         if (++Nbmks == maxbmks) break;
      }
      fclose(fid);
   }

   bmkposn = Nbmks;                                                              //  next free position
   return;
}


//  mouse click function to select existing bookmark from list

void edit_bookmarks_callbackfunc(GtkWidget *, int line, int pos, int kbkey)
{
   using namespace bookmarknames;

   ch     bookmarkname[32];

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

   if (! zd_edit_bookmarks) return;
   if (Nbmks < 1) return;
   if (line < 0) line = 0;
   if (line > Nbmks-1) line = Nbmks-1;
   bmkposn = line;
   strncpy0(bookmarkname,bookmarks[bmkposn],31);
   strTrim(bookmarkname);
   zdialog_stuff(zd_edit_bookmarks,"bmkname",bookmarkname);
   return;
}


//  mouse click function to receive clicked thumbnails for new/revised bookmarks

void edit_bookmarks_Lclick_func(int Nth)
{
   using namespace bookmarknames;

   ch       *imagefile, *newbookmark;
   ch       *pp, bookmarkname[32];
   int      cc;

   if (! zd_edit_bookmarks) return;
   if (Nth < 0) return;                                                          //  gallery gone ?
   imagefile = gallery(0,"getR",Nth);                                            //  get file at clicked position
   if (! imagefile) return;

   pp = strrchr(imagefile,'/');                                                  //  get file name or last subfolder name
   if (! pp) return;                                                             //    to use as default bookmark name
   strncpy0(bookmarkname,pp+1,31);                                               //  max. 30 chars. + null

   cc = strlen(imagefile) + 34;                                                  //  construct bookmark record:
   newbookmark = (ch *) zmalloc(cc,"bookmarks");                                 //    filename  /folders.../filename
   snprintf(newbookmark,cc,"%-30s  %s",bookmarkname,imagefile);

   if (Nbmks == maxbmks) {                                                       //  if list full, remove first
      zfree(bookmarks[0]);
      Nbmks--;
      for (int ii = 0; ii < Nbmks; ii++)
         bookmarks[ii] = bookmarks[ii+1];
   }

   if (Nbmks == 0) bmkposn = 0;                                                  //  1st bookmark --> 0
   else bmkposn++;                                                               //  else clicked position + 1
   if (bmkposn < 0) bmkposn = 0;
   if (bmkposn > Nbmks) bmkposn = Nbmks;

   for (int ii = Nbmks; ii > bmkposn; ii--)                                      //  make hole to insert new bookmark
      bookmarks[ii] = bookmarks[ii-1];

   bookmarks[bmkposn] = newbookmark;                                             //  insert
   Nbmks++;

   bookmarks_refresh();                                                          //  update bookmarks list in dialog

   zdialog_stuff(zd_edit_bookmarks,"bmkname",bookmarkname);

   return;
}


//  dialog event and completion function

int edit_bookmarks_dialog_event(zdialog *zd, ch *event)
{
   using namespace bookmarknames;

   ch          bookmarkname[32];
   FILE        *fid;
   int         cc;

   if (strmatch(event,"delete"))                                                 //  delete bookmark at position
   {
      if (bmkposn < 0 || bmkposn > Nbmks-1) return 1;
      for (int ii = bmkposn; ii < Nbmks-1; ii++)
         bookmarks[ii] = bookmarks[ii+1];
      Nbmks--;
      zdialog_stuff(zd,"bmkname","");                                            //  clear name field
      bookmarks_refresh();                                                       //  update bookmarks list in dialog
   }

   if (strmatch(event,"rename"))                                                 //  apply new name to bookmark
   {
      if (bmkposn < 0 || bmkposn > Nbmks-1) return 1;
      zdialog_fetch(zd,"bmkname",bookmarkname,31);                               //  get name from dialog
      cc = strlen(bookmarkname);
      if (cc < 30) memset(bookmarkname+cc,' ',30-cc);                            //  blank pad to 30 chars.
      bookmarkname[30] = 0;
      memcpy(bookmarks[bmkposn],bookmarkname,30);                                //  replace name in bookmarks list
      bookmarks_refresh();                                                       //  update bookmarks list in dialog
   }

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

   if (zd->zstat == 1)                                                           //  done
   {
      fid = fopen(bookmarks_file,"w");                                           //  write bookmarks file
      if (! fid)
         zmessageACK(Mwin,"unable to save bookmarks file");
      else {
         for (int ii = 0; ii < Nbmks; ii++)
            fprintf(fid,"%s\n",bookmarks[ii]);
         fclose(fid);
      }
   }

   for (int ii = 0; ii < Nbmks; ii++)                                            //  free memory
      zfree(bookmarks[ii]);

   zdialog_free(zd);
   zd_edit_bookmarks = 0;

   return 1;
}


//  private function to update dialog widget with new bookmarks list

void bookmarks_refresh()
{
   using namespace bookmarknames;

   ch       bookmarkline[XFCC+32];
   ch       blanks[33] = "                                ";
   int      cc;

   if (! zd_edit_bookmarks && ! zd_bookmark) return;
   textwidget_clear(textwidget);                                                 //  clear bookmarks list
   for (int ii = 0; ii < Nbmks; ii++) {                                          //  write bookmarks list
      strncpy0(bookmarkline,bookmarks[ii],31);
      cc = utf8len(bookmarkline);                                                //  compensate multibyte chars.
      strncat(bookmarkline,blanks,32-cc);
      strcat(bookmarkline,bookmarks[ii]+32);
      textwidget_append(textwidget,0,"%s\n",bookmarkline);
   }
   return;
}


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

//  set the video frame for a video thumbnail
//  (video file popup menu)

void m_thumbframe(GtkWidget *, ch *)
{
   zdialog  *zd;
   STATB    statB;
   int      ftype, zstat, err;
   int      minutes, seconds;
   ch       *pp, framefile[200];
   ch       *vf, *tf;
   ch       *videofile = 0, thumbfile[XFCC];
   PXB      *framepxb = 0, *thumbpxb = 0;
   timeval  thumbtimes[2];

   ch          *tip = "Play video and stop at desired frame \n"
                      "Note playback time in minutes and seconds";

   F1_help_topic = "video files";

   Plog(1,"m_thumbframe \n");

   if (clicked_file) {                                                           //  use clicked file if present
      videofile = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      videofile = zstrdup(curr_file,"thumbframe");
   else return;

   ftype = image_file_type(videofile);
   if (ftype != VIDEO) goto cleanup;

/**
       ____________________________________________
      |         Set Video Thumbnail Frame          |
      |                                            |
      | Play video and stop at desired frame.      |
      | Playback time: minutes [__] seconds [__]   |
      |                                            |
      |                                   [OK] [X] |
      |____________________________________________|

**/

   zd = zdialog_new("Set Video Thumbnail Frame",Mwin,"OK"," X ",null);
   zdialog_add_widget(zd,"label","labtip","dialog",tip);
   zdialog_add_widget(zd,"hbox","hbtime","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtime","hbtime","Playback time:","space=3");
   zdialog_add_widget(zd,"label","space","hbtime",0,"space=3");
   zdialog_add_widget(zd,"label","labmin","hbtime","minutes","space=3");
   zdialog_add_widget(zd,"zspin","minutes","hbtime","0|999|1|0","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbtime",0,"space=3");
   zdialog_add_widget(zd,"label","labsec","hbtime","seconds","space=3");
   zdialog_add_widget(zd,"zspin","seconds","hbtime","0|59|1|0","space=3|size=3");

   zdialog_run(zd,0,"parent");                                                   //  run dialog and wait for completion
   zstat = zdialog_wait(zd);

   if (zstat != 1) {                                                             //  cancel
      zdialog_free(zd);
      goto cleanup;
   }

   zdialog_fetch(zd,"minutes",minutes);                                          //  get playback time inputs
   zdialog_fetch(zd,"seconds",seconds);
   seconds = 60 * minutes + seconds;
   zdialog_free(zd);

   snprintf(framefile,200,"%s/videoframe.jpg",temp_folder);                      //  get frame file from ffmpeg

   vf = zescape_quotes(videofile);
   err = zshell("ack","ffmpeg -ss %d -i \"%s\" -v 8 -frames 1 -y \"%s\" ",
                                                   seconds, vf, framefile);
   zfree(vf);

   if (err) goto cleanup;

   if (! regfile(framefile)) {
      zmessageACK(Mwin,"cannot get video frame: %s",strerror(errno));
      goto cleanup;
   }

   err = image2thumbfile2(videofile,thumbfile);                                  //  get thumbnail file for video file
   if (err) {
      zmessageACK(Mwin,"cannot create thumbnail file");
      goto cleanup;
   }

   pp = strrchr(thumbfile+1,'/');
   *pp = 0;
   if (! dirfile(thumbfile)) {                                                   //  create thumbnail folder if needed
      tf = zescape_quotes(thumbfile);
      err = zshell("ack","mkdir -p -m 0750 \"%s\"",tf);
      zfree(tf);
      if (err) goto cleanup;
   }
   *pp = '/';

   framepxb = PXB_load(framefile,1);                                             //  video frame file >> PXB image
   if (! framepxb) goto cleanup;

   thumbpxb = PXB_rescale(framepxb,thumbfilesize);                               //  rescale PXB to thumbnail size
   if (! thumbpxb) goto cleanup;

   err = PXB_JPG_save(thumbpxb,thumbfile,80);                                    //  save to JPEG thumbnail file
   if (err) goto cleanup;

   if (! regfile(videofile,&statB)) goto cleanup;                                //  get video file mod time

   thumbtimes[0].tv_sec = thumbtimes[1].tv_sec = statB.st_mtim.tv_sec;           //  thumbnail mod time
   thumbtimes[0].tv_usec = thumbtimes[1].tv_usec = 0;                            //    = video file mod time
   utimes(thumbfile,thumbtimes);

   gallery(0,"paint",-1);                                                        //  repaint gallery

cleanup:
   remove(framefile);
   if (videofile) zfree(videofile);
   if (framepxb) PXB_free(framepxb);
   if (thumbpxb) PXB_free(thumbpxb);
   return;
}


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

//  dummy menu entry as target for "Show Hidden Files" KB shortcut

void m_show_hidden(GtkWidget *, ch *)
{
   Plog(1,"m_show_hidden \n");
   KBaction("Show Hidden");
   return;
}


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

//  dummy menu entry as target for "Current Album" KB shortcut

void m_current_album(GtkWidget *, ch *)
{
   Plog(1,"m_current_album \n");
   navi::newalbum_menu_event(0,0);
   return;
}


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

//  Show an image file in a popup window at mouse position.
//  Re-use most recent window or create a new one if Fnewin != 0.
//  Returns 0 if OK, +N otherwise.

namespace popup_image_names
{
   GtkWidget  *window[10], *drawarea[10];                                        //  up to 10 popup windows open
   ch         *filex[10], reqfull[10], isfull[10];
   float      winc = 1.26, wdec = 0.80;                                          //  window upsize/downsize ratios
   int        Nw = 0;
}


int popup_image(ch *file, GtkWindow *parent, int Fnewin, int size)
{
   using namespace popup_image_names;

   static int  ftf = 1;
   ch         *pp;
   ch         *tipmess("zoom via mouse wheel or Keys +/=/-/↑/↓");

   int popup_image_draw(GtkWidget *, cairo_t *, int &Nw);
   int popup_image_scroll(GtkWidget *, GdkEvent *event, int &Nw);
   int popup_image_KBevent(GtkWidget *, GdkEventKey *event, int &Nw);
   int popup_image_mousebutt(GtkWidget *, GdkEvent *event, int &Nw);
   int popup_image_state_event(GtkWidget *, GdkEvent *, int &Nw);

   if (! main_thread()) zappcrash("popup_image() called from thread");

   if (ftf) {
      ftf = 0;
      poptext_mouse(tipmess,0,0,0,8);
   }

   if (Fnewin)
      if (++Nw == 10) Nw = 0;                                                    //  new window, re-use oldest up to 10
   if (! Fnewin)
      while (Nw > 0 && window[Nw] == 0) Nw--;                                    //  else re-use latest still active

   if (window[Nw]) {
      gtk_widget_destroy(drawarea[Nw]);
      drawarea[Nw] = 0;
      zfree(filex[Nw]);
      filex[Nw] = 0;
   }
   else {
      window[Nw] = gtk_window_new(GTK_WINDOW_TOPLEVEL);                          //  create new popup window
      if (! window[Nw]) return 1;
      if (! size) size = 512;
      gtk_window_set_default_size(GTK_WINDOW(window[Nw]),size,size);
      pp = strrchr(file,'/');                                                    //  window title = file name
      gtk_window_set_title(GTK_WINDOW(window[Nw]),pp+1);

      if (parent) {
         gtk_window_set_transient_for(GTK_WINDOW(window[Nw]),parent);
         gtk_window_set_destroy_with_parent(GTK_WINDOW(window[Nw]),1);
      }
      gtk_window_set_position(GTK_WINDOW(window[Nw]),GTK_WIN_POS_MOUSE);
   }

   filex[Nw] = zstrdup(file,"popup_image");
   drawarea[Nw] = gtk_drawing_area_new();                                        //  new drawing area always required
   if (! drawarea[Nw]) return 2;
   gtk_container_add(GTK_CONTAINER(window[Nw]),drawarea[Nw]);
   reqfull[Nw] = isfull[Nw] = 0;                                                 //  not fullscreen

   gtk_widget_add_events(window[Nw],GDK_SCROLL_MASK);
   gtk_widget_add_events(window[Nw],GDK_KEY_PRESS_MASK);
   gtk_widget_add_events(window[Nw],GDK_BUTTON_RELEASE_MASK);

   G_SIGNAL(window[Nw],"destroy",gtk_widget_destroyed,&window[Nw]);              //  set window = null if destroyed
   G_SIGNAL(window[Nw],"draw",popup_image_draw,&Nval[Nw]);
   G_SIGNAL(window[Nw],"scroll-event",popup_image_scroll,&Nval[Nw]);             //  connect events
   G_SIGNAL(window[Nw],"key-press-event",popup_image_KBevent,&Nval[Nw]);
   G_SIGNAL(window[Nw],"button-release-event",popup_image_mousebutt,&Nval[Nw]);
   G_SIGNAL(window[Nw],"window-state-event",popup_image_state_event,&Nval[Nw]);

   gtk_widget_show_all(window[Nw]);

   return 0;
}


//  rescale image and repaint window to fit

int popup_image_draw(GtkWidget *window, cairo_t *cr, int &nn)
{
   using namespace popup_image_names;

   PXB         *pxb1, *pxb2;
   int         ww1, hh1, ww2, hh2;
   double      area;
   ch          *file;

   file = filex[nn];
   if (! file) return 1;

   pxb1 = PXB_load(file,1);
   if (! pxb1) return 1;

   ww1 = pxb1->ww;                                                               //  image dimensions
   hh1 = pxb1->hh;

   gtk_window_get_size(GTK_WINDOW(window),&ww2,&hh2);                            //  current window dimensions

   area = ww2 * hh2;
   ww2 = sqrt(area * ww1 / hh1);                                                 //  fit window to image, keeping same area
   hh2 = area / ww2;

   pxb2 = PXB_rescale(pxb1,ww2,hh2);                                             //  rescale image to window

   gtk_window_resize(GTK_WINDOW(window),ww2,hh2);

   gdk_cairo_set_source_pixbuf(cr,pxb2->pixbuf,0,0);                             //  draw image
   cairo_paint(cr);

   PXB_free(pxb1);
   PXB_free(pxb2);

   return 1;
}


//  respond to mouse scroll button and zoom window larger or smaller

int popup_image_scroll(GtkWidget *window, GdkEvent *event, int &nn)
{
   using namespace popup_image_names;

   int         scroll, ww, hh;
   double      ff = 1.0;

   if (event->type == GDK_SCROLL) {                                              //  mouse wheel event
      scroll = ((GdkEventScroll *) event)->direction;
      if (scroll == GDK_SCROLL_UP) ff = winc;
      if (scroll == GDK_SCROLL_DOWN) ff = wdec;
   }

   gtk_window_get_size(GTK_WINDOW(window),&ww,&hh);                              //  current window dimensions
   ww *= ff;                                                                     //  new dimensions
   hh *= ff;

   if (ww > monitor_ww || hh > monitor_hh) {                                     //  request > screen size, fullscreen
      reqfull[nn] = 1;
      gtk_window_fullscreen(GTK_WINDOW(window));
      return 1;
   }

   reqfull[nn] = 0;
   gtk_window_unfullscreen(GTK_WINDOW(window));

   if (ww + hh > 512)
      gtk_window_resize(GTK_WINDOW(window),ww,hh);                               //  rescale up or down
   else
      gtk_widget_destroy(window);                                                //  if very small, delete window

   return 1;
}


//  respond to KB events F11 (fullscreen/unfullscreen)
//                       Escape (destroy)
//                       +/= or up-arrow  make image larger
//                       - or down-arrow  make image smaller

int popup_image_KBevent(GtkWidget *window, GdkEventKey *event, int &nn)
{
   using namespace popup_image_names;

   int      KBkey = event->keyval;
   int      ww, hh;
   double   ff = 0;

   if (KBkey == GDK_KEY_Escape) gtk_widget_destroy(window);

   if (KBkey == GDK_KEY_F11)
   {
      if (reqfull[nn]) {
         reqfull[nn] = 0;
         gtk_window_unfullscreen(GTK_WINDOW(window));
      }
      else {
         reqfull[nn] = 1;
         gtk_window_fullscreen(GTK_WINDOW(window));
      }
   }

   if (KBkey == GDK_KEY_plus || KBkey == GDK_KEY_KP_Add || KBkey == GDK_KEY_equal || KBkey == GDK_KEY_Up)
      ff = winc;

   if (KBkey == GDK_KEY_minus || KBkey == GDK_KEY_KP_Subtract || KBkey == GDK_KEY_Down)
      ff = wdec;

   if (ff)
   {
      gtk_window_get_size(GTK_WINDOW(window),&ww,&hh);                           //  current window dimensions
      ww *= ff;                                                                  //  new dimensions
      hh *= ff;

      if (ww > monitor_ww || hh > monitor_hh) {                                  //  request > screen size, fullscreen
         reqfull[nn] = 1;
         gtk_window_fullscreen(GTK_WINDOW(window));
         return 1;
      }

      reqfull[nn] = 0;
      gtk_window_unfullscreen(GTK_WINDOW(window));

      if (ww + hh > 512)
         gtk_window_resize(GTK_WINDOW(window),ww,hh);                            //  rescale up or down
      else
         gtk_widget_destroy(window);                                             //  if very small, delete window
   }

   return 1;
}


//  respond to mouse button - destroy window

int popup_image_mousebutt(GtkWidget *window, GdkEvent *event, int &nn)
{
   gtk_widget_destroy(window);
   return 1;
}


//  track window fullscreen state

int popup_image_state_event(GtkWidget *window, GdkEvent *event, int &nn)
{
   using namespace popup_image_names;

   int state = ((GdkEventWindowState *) event)->new_window_state;
   if (state & GDK_WINDOW_STATE_FULLSCREEN) isfull[nn] = 1;
   else isfull[nn] = 0;

   if (isfull[nn] != reqfull[nn]) {                                              //  compensate GTK bug:    FIXME
      if (reqfull[nn]) gtk_window_fullscreen(GTK_WINDOW(window));                //   the window fullscreens itself after
      else gtk_window_unfullscreen(GTK_WINDOW(window));                          //    being requested to unfullscreen
   }

   return 1;
}
