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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 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.

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

   Fotoxx image edit - folder navigation and thumbnail gallery functions

   gallery                    create/update/search/paint an image gallery
   navi::
      gallery_comp            gallery sort compare function
      gallery_paint           paint thumbnail gallery
      gallery_paintmeta       same, metadata report from search function
      gallery_listview1       same, small list view, no metadata
      gallery_listview2       same, big list view with 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
      gallery_sort            choose gallery sort order and sort gallery
      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
   
   m_thumbview                gallery thumbnail view
   m_metaview                 gallery metadata view
   m_listview                 gallery list view

   add_recent_file            add an image file to the list of recent files
   m_recentfiles              show gallery of recently viewed files
   m_newfiles                 show gallery of newest image files

   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
   delete_thumbfile           delete thumbnail disk file
   get_cache_thumb            get thumbnail from cache, create and add if missing
   check_cache_thumb          test if a thumbnail is in the cache already
   preload_thumbs             preload thumbnails beyond gallery into thumbnail cache

   gallery_popimage           popup a larger image from a clicked thumbnail
   gallery_select1            single gallery file selection function and dialog
   gallery_select             multiple gallery file selection function and dialog
   m_select_files             select files for album and batch functions

   m_bookmarks                goto selected bookmark, edit bookmarks
   m_edit_bookmarks           edit bookmarks
   m_alldirs                  folding folder tree, click for gallery view
   m_source_folder            set gallery to folder of current image file
   m_show_hidden              dummy menu for KB shortcut "Show Hidden Files"
   m_current_album            dummy menu for KB shortcut "Current Album"

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

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

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

/***                                                defined in fotoxx.h
      typedef struct {                              current gallery file list in memory
         char     *file;                            /folder.../filename
         char     fdate[16];                        file date: yyyymmddhhmmss
         char     pdate[16];                        photo date: yyyymmddhhmmss
         int      fsize;                            file size, bytes                                                    19.0
         int      psize;                            image size, pixels                                                  19.0
         int      mdindex;                          index to metadata list Gmdlist[*]
      }  GFlist_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              20.0
   GFlist_t    *GFlist = 0;                                                      //  gallery file list 
   int         GFlock = 0;                                                       //  gallery file list lock 
   int         TClock = 0;                                                       //  thumbnail cache lock
   int         Nfiles = 0;                                                       //  gallery files (incl. subfolders)
   int         Nfolders = 0;                                                     //  gallery subfolder count
   int         Nimages = 0;                                                      //  gallery image file count
   int         preload_trigger = 0;                                              //  kick preload_thumb() thread        20.0
   int         preload_start = 0;                                                //  1st file in gallery to check       20.0
   char        **Gmdlist = 0;                                                    //  corresp. metadata list
   int         Gmdrows = 0;                                                      //  text rows in metadata list
   char        *galleryname = 0;                                                 //  folder or file list name
   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         Flistview = 0;                                                    //  gallery list view mode 0/1/2
   GtkWidget   *gallerybutt[60];                                                 //  gallery navi buttons [aaa] [bbb] ...
   char        *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
   char        *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

   //  private functions
   int    gallery_comp(cchar *rec1, cchar *rec2);                                //  gallery record compare for sort options
   int    gallery_paint(GtkWidget *, cairo_t *);                                 //  gallery window paint function
   int    gallery_paintmeta(GtkWidget *Gdrawin, cairo_t *cr);                    //  same, metadata report from search func.
   int    gallery_listview1(GtkWidget *Gdrawin, cairo_t *cr);                    //  same, small list view, no metadata
   int    gallery_listview2(GtkWidget *Gdrawin, cairo_t *cr);                    //  same, big list view with metadata
   void   dir_filecount(char *dirname, int &ndir, int &nfil);                    //  get subdir and image file counts
   void   draw_text(cairo_t *cr, char *text, int x, int y, int ww);              //  draw text in gallery window
   void   gallery_navibutts();                                                   //  create navigation buttons in top panel
   void   menufuncx(GtkWidget *, cchar *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 *, cchar *);                               //  [TOP] menu response function
   void   newalbum(GtkWidget *widget, GdkEventButton *event);                    //  function for [Album] button
   void   newalbum_menu_event(GtkWidget *, cchar *);                             //  [Album] menu response function
   void   gallery_sort();                                                        //  choose sort order and sort gallery
   int    mouse_event(GtkWidget *widget, GdkEvent *event, void *);               //  gallery window mouse event function
   char * gallery_dragfile();                                                    //  start drag-drop, set the file
   void   gallery_dropfile(int mousex, int mousey, char *file);                  //  accept drag-drop file at position
   int    KBpress(GtkWidget *, GdkEventKey *, void *);                           //  gallery window key press event
}

using namespace zfuncs;
using namespace navi;


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

   char * gallery(cchar *filez, cchar *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 (subdirs first, last version 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 file params (GFlist[*]) from image index in memory
      *        get1st    *       return 1st image file in gallery or null if none      

   Returned values:
      get or get1st: filespec     others: null
      The returned file belongs to caller and is subject for zfree().

   thumbnail click functions:
      gallery_Lclick_func()                        default function (open file)
      gallery_Rclick_popup()                       default function (popup menu)
      gallery_select_Lclick_func()                 gallery_select active
      gallery_select_Rclick_func()                 gallery_select active
      edit_bookmarks_Lclick_func                   edit bookmarks active

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


char * gallery(cchar *filez, cchar *action, int Nth)
{
   char        *file, *file2;
   char        *pp, *pp1, *pp2;
   char        buff[XFCC];
   char        **Flist = 0;                                                      //  19.0
   xxrec_t     *xxrec;
   static int  ftf = 1, Gbusy = 0;
   int         err, ii, jj, kk;
   int         cc, cc1, cc2, fposn;
   int         NF, flags;
   FTYPE       ftype;
   FILE        *fid;
   STATB       statb;
   
   if (ftf) {
      ftf = 0;
      cc = maxgallery * sizeof(GFlist_t);
      GFlist = (GFlist_t *) zmalloc(cc);                                         //  allocate gallery file list
      Nfiles = Nfolders = Nimages = 0;                                           //  no files yet
   }
   
   if (Gbusy++) {
      printz("gallery re-enter \n");                                             //  prevent GUI lockup          bugfix 19.3
      return 0;
   }
   
   while (! resource_lock(GFlock)) zsleep(0.001);                                //  lock gallery file list             20.0

   if (strmatchN(action,"init",4))                                               //  init or initF
   {
      if (! filez && gallerytype == GDIR) filez = galleryname;                   //  refresh gallery
      if (! filez) goto return0;
      
      NF = Nfiles;                                                               //  current file count
      Nfiles = Nfolders = Nimages = 0;                                           //  no new files yet

      if (Gmdlist) {
         for (ii = 0; ii < NF; ii++) {                                           //  free prior metadata list
            kk = GFlist[ii].mdindex;
            if (Gmdlist[kk]) zfree(Gmdlist[kk]);
         }
         zfree(Gmdlist);
         Gmdlist = 0;
      }

      for (ii = 0; ii < NF; ii++)                                                //  free prior gallery list
         zfree(GFlist[ii].file);

      galleryname = zstrdup(filez);                                              //  may be folder or file
   }
   
   if (strmatch(action,"init"))                                                  //  initz. from given file or folder
   {
      err = stat(galleryname,&statb);
      if (err) {
         pp = (char *) strrchr(galleryname,'/');                                 //  bad file, check folder part
         if (! pp) goto return0;
         pp[0] = 0;
         err = stat(galleryname,&statb);
         if (err) goto return0;                                                  //  give up, empty file list
      }

      if (S_ISREG(statb.st_mode)) {                                              //  if a file, get folder part
         pp = (char *) strrchr(galleryname,'/');
         if (! pp) goto return0;
         pp[0] = 0;
      }

      gallerytype = GDIR;                                                        //  gallery type = folder

      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,"/.."))) {
            zfree(file);
            continue;
         }

         err = stat(file,&statb);
         if (err) {
            zfree(file);
            continue;
         }

         if (Nfiles == maxgallery) {                                             //  too many files
            zmessageACK(Mwin,Bgallerytruncated,maxgallery);
            break;
         }

         ftype = image_file_type(file);

         if (ftype == FDIR) {                                                    //  subfolder
            GFlist[Nfiles].file = file;                                          //  add to file list
            GFlist[Nfiles].file[0] = '!';                                        //  if folder, make it sort first
            GFlist[Nfiles].fdate[0] = 0;                                         //  no file date
            GFlist[Nfiles].pdate[0] = 0;                                         //  no photo date
            GFlist[Nfiles].psize = 0;                                            //  no image size                      19.13
            Nfiles++;
            Nfolders++;
         }

         else if (ftype == IMAGE || ftype == RAW || ftype == VIDEO) {            //  supported image file type
            xxrec = get_xxrec(file);                                             //  19.0
            if (! xxrec) {
               zfree(file);
               continue;
            }
            GFlist[Nfiles].file = file;                                          //  add to file list
            strcpy(GFlist[Nfiles].fdate,xxrec->fdate);
            strcpy(GFlist[Nfiles].pdate,xxrec->pdate);
            GFlist[Nfiles].fsize = xxrec->fsize;                                 //  file size, MB
            GFlist[Nfiles].psize = xxrec->ww * xxrec->hh;                        //  image size, pixels
            Nfiles++;
            Nimages++;
         }

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

      if (Flist) zfree(Flist);

      gallerysort = FNAME;                                                       //  sort GFlist by file name ascending
      galleryseq = ASCEND;
      galleryposn = 0;
      
      if (Nfiles > 1)
         HeapSort((char *) GFlist, sizeof(GFlist_t), Nfiles, gallery_comp);

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

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

         Nfiles -= kk;
         Nimages -= kk;
      }

      curr_file_count = Nimages;                                                 //  gallery image file count
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"initF"))                                                 //  gallery from given file list
   {
      if (gallerytype == TNONE || gallerytype == GDIR)                           //  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;
         err = stat(file,&statb);                                                //  check file exists
         if (err) continue;

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

         GFlist[Nfiles].file = zstrdup(file);                                    //  add to file list                   19.13
         compact_time(statb.st_mtime,GFlist[Nfiles].fdate);                      //  file date, "yyyymmddhhmmss"
         GFlist[Nfiles].fsize = statb.st_size;                                   //  file size, bytes
         strcpy(GFlist[Nfiles].pdate,xxrec->pdate);                              //  photo date
         GFlist[Nfiles].psize = xxrec->ww * xxrec->hh;                           //  image size, pixels

         Nfiles++;
         if (Nfiles == maxgallery) {                                             //  too many files
            zmessageACK(Mwin,Bgallerytruncated,maxgallery);
            break;
         }
      }

      fclose(fid);

      Nimages = Nfiles;                                                          //  no folders
      curr_file_count = Nimages;                                                 //  gallery image file count
      
      gallerysort = SNONE;                                                       //  no initial sort
      galleryseq = QNONE;
      galleryposn = 0;
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"sort"))                                                  //  sort the gallery file list      
   {  
      if (Nth == -2) gallery_memory("get");                                      //  recall prior sort and posn.
      if (Nfiles > 1 && gallerysort != SNONE)                                    //  sort using current or prior params
         HeapSort((char *) GFlist, sizeof(GFlist_t), Nfiles, 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
      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 > Nfiles) fposn = Nfiles;

      if (Nfiles == maxgallery-1) {                                              //  no room
         zmessageACK(Mwin,Bgallerytruncated,maxgallery);
         goto return0;
      }

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

      for (ii = Nfiles; ii > fposn; ii--)                                        //  create hole in list
         GFlist[ii] = GFlist[ii-1];

      GFlist[fposn].file = zstrdup(filez);                                       //  put new file in hole
      strcpy(GFlist[fposn].fdate,xxrec->fdate);
      strcpy(GFlist[fposn].pdate,xxrec->pdate);
      GFlist[fposn].fsize = xxrec->fsize;                                        //  file size, MB                      19.0
      GFlist[fposn].psize = xxrec->ww * xxrec->hh;                               //  image size, pixels
      Nfiles++;
      Nimages++;
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"delete"))                                                //  delete file from list
   {
      fposn = Nth;                                                               //  file position from caller must be OK
      if (fposn < Nfolders || fposn > Nfiles-1) goto return0;
      Nfiles--;                                                                  //  decr. total files
      Nimages--;                                                                 //  decr. image files
      zfree(GFlist[fposn].file);                                                 //  remove GFlist record
      for (ii = fposn; ii < Nfiles; ii++)                                        //  close the hole
         GFlist[ii] = GFlist[ii+1];                                              //  gcc bug workaround removed
      if (Gmdlist) {
         kk = GFlist[fposn].mdindex;                                             //  remove corresp. Gmdlist data 
         if (Gmdlist[kk]) zfree(Gmdlist[kk]);
         Gmdlist[kk] = 0;
      }
      gallerypainted = 0;
      goto return0;
   }

   if (strmatch(action,"get"))                                                   //  return Nth file in gallery
   {
      fposn = Nth;                                                               //  file position from caller must be OK
      if (fposn < 0 || fposn > Nfiles-1) goto return0;
      file2 = zstrdup(GFlist[fposn].file);                                       //  get Nth file
      file2[0] = '/';                                                            //  restore initial '/'
      err = stat(file2,&statb);
      if (! err) goto return2;                                                   //  return file2
      zfree(file2);
      goto return0;
   }

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

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

   if (strmatch(action,"get1st"))                                                //  return 1st image file in gallery
   {
      for (ii = Nfolders; ii < Nfiles; ii++) {                                   //  loop from 1st image file           19.0
         err = stat(GFlist[ii].file,&statb);                                     //  file gone?
         if (err) continue;
      }
      if (ii == Nfiles) goto return0;                                            //  no files in gallery
      file2 = zstrdup(GFlist[ii].file);                                          //  1st file
      goto return2;                                                              //  return file2
   }

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

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

return2:
   resource_unlock(GFlock);
   Gbusy = 0;
   return file2;;
}


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

int navi::gallery_comp(cchar *rec1, cchar *rec2)
{
   int      nn;
   GFlist_t  *grec1, *grec2;

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

   if (grec1->file[0] == '!') {                                                  //  folder sort 
      if (grec2->file[0] != '!') 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->file[0] == '!') return +1;                                    //  image file :: folder

   if (galleryseq == DESCEND) {                                                  //  descending, switch inputs
      grec1 = (GFlist_t *) rec2;
      grec2 = (GFlist_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 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);                                 //  (EXIF DateTimeOriginal)
         if (nn) return nn;
         goto tiebreak;
      }
      
      case FSIZE: {                                                              //  file size                          19.0
         nn = grec1->fsize - grec2->fsize;
         if (nn) return nn;
         goto tiebreak;
      }

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

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


//  private function
//  paint gallery window - draw thumbnail images and limited metadata

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

   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) {                                                    //  metadata report (search function)
      gallery_paintmeta(drwin,cr);
      return 1;
   }
   
   if (Flistview == 0) {} ///////////
   
   if (Flistview == 1) {                                                         //  small list view report             19.0
      gallery_listview1(drwin,cr);
      return 1;
   }

   if (Flistview == 2) {                                                         //  big list view, with metadata       20.0
      gallery_listview2(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                 20.0
   
   if (! Findexvalid) textlines = 1;                                             //  file name only
   else textlines = 2;                                                           //  file name + date + wwhh
   if (gallerytype != GDIR) 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 = (Nfiles+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
   
   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 file thumbnails
   for (col = 0; col < xcols; col++)                                             //  draw all columns in row
   {
      ii = row * xcols + col;                                                    //  next file
      if (ii >= Nfiles) goto endloops;                                           //  exit 2 nested loops

      fspec = GFlist[ii].file;                                                   //  folder/file name
      p0 = *fspec;                                                               //  replace possible '!' with '/'
      *fspec = '/';

      pp = strrchr(fspec,'/');                                                   //  get file name only
      if (pp) fname = pp + 1;
      else fname = fspec;

      strcpy(ffolder,"/");
      if (gallerytype != GDIR) {                                                 //  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;

      if (curr_file && strmatch(fspec,curr_file)) {                              //  yellow background for curr. image
         cairo_set_source_rgb(cr,1,1,0.5);
         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             19.13
         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           19.0
         hh = xxrec->hh;
         fsize = xxrec->fsize;

         cc = 0;

         if (gallerytype != GDIR) {                                              //  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                   19.13
               snprintf(text+cc,200-cc,"  %.2fmb",fsize/FMEGA);                  //  output file size only              19.13
            else {
               if (thumbsize > 180) 
                  snprintf(text+cc,200-cc,"  %dx%d  %.2fmb",ww,hh,fsize/FMEGA);  //  output width, height, file size    19.0
               else if (thumbsize > 128)
                  snprintf(text+cc,200-cc,"  %dx%d",ww,hh);                      //  output only width, height          19.0
            }
         }

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

      pxbT = get_cache_thumb(fspec,0);                                           //  get thumbnail
      if (pxbT) {
         ww = gdk_pixbuf_get_width(pxbT);
         ww = (thumbsize - ww) / 4;                                              //  shift margin if smaller width
         gdk_cairo_set_source_pixbuf(cr,pxbT,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);
      }

      *fspec = p0;                                                               //  restore '!'
   }
endloops:

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


//  private function
//  paint metadata report - draw thumbnail images + metadata
//  metadata is in Gmdlist[*] from search_images() metadata report

int navi::gallery_paintmeta(GtkWidget *drwin, cairo_t *cr)
{
   PIXBUF      *pxbT;
   double      x1, y1, x2, y2;
   int         ii, kk, nrows, row;
   int         row1, row2, ww, hh;
   int         drwingW, drwingH;
   int         thumx, thumy, textww;
   char        *fspec, p0;

   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 = Nfiles;                                                               //  thumbnail rows
   if (! nrows) nrows = 1;

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

   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
   {
      ii = row;                                                                  //  next file
      if (ii >= Nfiles) break;

      fspec = GFlist[ii].file;
      p0 = *fspec;                                                               //  replace possible ! with /
      *fspec = '/';

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

      pxbT = get_cache_thumb(fspec,0);                                           //  get thumbnail
      if (pxbT) {
         gdk_cairo_set_source_pixbuf(cr,pxbT,thumx,thumy);
         cairo_paint(cr);
      }

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

      if (Gmdlist) {                                                             //  write corresp. metadata if any
         kk = GFlist[ii].mdindex;
         draw_text(cr,Gmdlist[kk],thumbW+margin,thumy+20,textww);                //  bugfix (kk=0 omitted)
      }

      *fspec = p0;                                                               //  restore '!'
   }

   gallerypainted = 1;
   return 1;
}


//  private function
//  paint gallery window small list view: tiny thumbnail, file name only

int navi::gallery_listview1(GtkWidget *drwin, cairo_t *cr)                       //  20.0
{
   PIXBUF      *pxbT;
   FTYPE       ftype;
   double      x1, y1, x2, y2;
   int         ii, nrows, row, col;
   int         row1, row2, ww, hh;
   int         drwingW, drwingH;
   int         thumx, thumy, textx, textww;
   int         ndir, nfil;
   char        *fspec, *fspec2, p0;
   xxrec_t     *xxrec;
   char        fdate[20], pdate[20], *pp;
   char        text[1000];

   thumbsize = 48;                                                               //  small list view thumbnail size
   fontsize = appfontsize;

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

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

   texthh = 2 * fontsize * 1.8 + 20;                                             //  space for 2 lines of 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 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;
   if (gallerytype != GDIR) xcols = 1;                                           //  not folder gallery, full file path
   nrows = (Nfiles+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

   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 >= Nfiles) goto endloops;                                           //  exit 2 nested loops

      fspec = GFlist[ii].file;
      p0 = *fspec;                                                               //  replace possible ! with /
      *fspec = '/';

      fspec2 = fspec;
      if (gallerytype == GDIR) {                                                 //  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;

      pxbT = get_cache_thumb(fspec,0);                                           //  get thumbnail
      if (pxbT) {
         gdk_cairo_set_source_pixbuf(cr,pxbT,thumx,thumy);                       //  paint thumbnail
         cairo_paint(cr);
      }
      
      textx = thumx + margin + thumbsize;

      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," %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)                                                      //  image file
      {
         xxrec = get_xxrec(fspec);
         if (! xxrec) continue;

         strncpy0(fdate,xxrec->fdate,16);
         strncpy0(pdate,xxrec->pdate,16);

         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] = '-';
         }

         snprintf(text,1000," %s \n photo date: %s  file date: %s \n",           //  file name, photo date, file date
                                       fspec2, pdate, fdate);
      }
      
      else snprintf(text,1000,"\n %s",fspec2);                                   //  no index, no metadata, file name only

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

      *fspec = p0;                                                               //  restore '!'
   } 
   endloops:

   gallerypainted = 1;
   return 1;
}


//  private function
//  paint gallery window big list view: small thumbnail, file name, basic metadata

int navi::gallery_listview2(GtkWidget *drwin, cairo_t *cr)
{
   PIXBUF      *pxbT;
   FTYPE       ftype;
   double      x1, y1, x2, y2;
   int         ii, nrows, row, col;
   int         row1, row2, ww, hh;
   int         drwingW, drwingH;
   int         thumx, thumy, textx, textww;
   int         ndir, nfil;
   char        *fspec, *fspec2, p0;
   xxrec_t     *xxrec;
   char        fdate[20], pdate[20], *pp;
   char        *rating, *tags, *capt, *comms, *location, *country;
   char        wwhh[16], fsize[16];
   char        text[1000];
   
   thumbsize = 128;                                                              //  list view thumbnail size 
   fontsize = appfontsize;

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

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

   texthh = 6 * fontsize * 1.8 + 20;                                             //  space for 6 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);                                      //  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;
   if (gallerytype != GDIR) xcols = 1;                                           //  not folder gallery, full file path
   nrows = (Nfiles+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

   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 >= Nfiles) goto endloops;                                           //  exit 2 nested loops

      fspec = GFlist[ii].file;
      p0 = *fspec;                                                               //  replace possible ! with /
      *fspec = '/';

      fspec2 = fspec;
      if (gallerytype == GDIR) {                                                 //  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;

      pxbT = get_cache_thumb(fspec,0);                                           //  get thumbnail
      if (pxbT) {
         gdk_cairo_set_source_pixbuf(cr,pxbT,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)
      {
         xxrec = get_xxrec(fspec);
         if (! xxrec) continue;                                                  //  19.13

         strncpy0(fdate,xxrec->fdate,16);                                        //  19.13
         strncpy0(pdate,xxrec->pdate,16);
         rating = xxrec->rating;
         snprintf(wwhh,16,"%dx%d",xxrec->ww,xxrec->hh);                          //  19.0
         snprintf(fsize,16,"%.2fmb",xxrec->fsize/FMEGA);                         //  19.0
         tags = xxrec->tags;
         capt = xxrec->capt;
         comms = xxrec->comms;
         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                         19.13
         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] = '-';
         }

         snprintf(text,1000,
            " %s \n photo date: %s  file date: %s \n"
            " rating: %s  size: %s %s  location: %s %s \n"
            " tags: %s \n caption: %s \n comments: %s",
            fspec2, pdate, fdate, rating, wwhh, fsize, 
                    location, country, tags, capt, comms);
      }
      
      else snprintf(text,1000,"\n %s",fspec2);                                   //  no index, no metadata, file name only

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

      *fspec = p0;                                                               //  restore '!'
   }
   endloops:
   
   gallerypainted = 1;
   return 1;
}


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

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

   #define NC 1000                                                               //  cache capacity

   static int     ftf = 1;
   static char    *dirnamecache[NC];                                             //  cache for recent folder data
   static time_t  modtimecache[NC];
   static int     dcountcache[NC];
   static int     fcountcache[NC];
   static int     pcache = 0;
   struct stat    statb;

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

   err = stat(dirname,&statb);
   if (err) 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);
   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, char *text, int px, int py, int ww)
{
   static PangoFontDescription   *pfont = 0;
   static PangoLayout            *playout = 0;

   static int     pfontsize = -1;
   static char    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()
{
   char        labtext[100];
   int         ii, cc, max = maxgallerylevs;
   char        *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");                                                //  20.0
   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 != GDIR) {                                                    //  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              20.0
   {
      pp2 = strchr(pp1+1,'/');                                                   //  /aaaaaa/bbbbbb/cccccc
      if (pp2) cc = pp2 - pp1;                                                   //         |      |
      else cc = strlen(pp1);                                                     //         pp1    pp2
      gallerypath[ii] = (char *) zmalloc(cc);
      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 *, cchar *menu)
{
   int         ii, scroll1, scroll2;
   
   if (FGWM != 'G') return;
   gallery_scroll(-1,0);                                                         //  stop scrolling
   if (! gallerypainted) return;                                                 //  wait for pending paint

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

   if (strmatch(menu,"GoTo")) {
      F1_help_topic = "bookmarks";
      m_bookmarks(0,0);
      return;
   }

   if (strmatch(menu,"Sort")) {                                                  //  choose sort order and sort gallery
      F1_help_topic = "sort gallery";
      gallery_sort();
      return;
   }

   if (strmatch(menu,"Zoom+"))                                                   //  next bigger thumbnail size         20.0
   {
      if (Flistview == 1) Flistview = 2;                                         //  small list view --> big list view
      else if (Flistview == 2) {                                                 //  big list view --> thumbnail gallery
         Flistview = 0;
         thumbsize = thumbx[thumbxx-1];                                          //  set smallest thumbnail size
      }
      else {                                                                     //  thumbnail gallery
         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            20.0
   {
      if (Flistview == 1) return;                                                //  small list view, no change
      else if (Flistview == 2) Flistview = 1;                                    //  big list view --> small list view
      else {                                                                     //  thumbnail gallery
         for (ii = 0; ii < thumbxx; ii++)                                        //  set next smaller thumb size
            if (thumbsize == thumbx[ii]) break;
         if (ii >= thumbxx-1) Flistview = 2;                                     //  < minimum, --> big list view
         else 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,1500);
      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,1500);
      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,3000);
      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,3000);
      return;
   }

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

   if (strmatch(menu,"Last")) {                                                  //  jump to bottom
      scrollposn = maxscroll;
      galleryposn = scrollposn / thumbH * xcols;
      gallery(0,"paint",-1);
      return;
   }
   
   printz("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

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(4,gallery_scrollfunc,0);                                     //  4 millisec. timer period 
      return;
   }

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


//  private function
//  timer function, runs every 4 milliseconds

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

   if (gallery_scrollgoal < 0) {                                                 //  stop scrolling
      gallery_scrollspeed = 0;
      cumscroll = 0;
      return 0;
   }

   if (FGWM != 'G') {                                                            //  not gallery view
      gallery_scrollgoal = -1;                                                   //  stop scrolling
      gallery_scrollspeed = 0;
      cumscroll = 0;
      return 0;
   }
   
   if (scrollposn == gallery_scrollgoal) {                                       //  goal reached, stop
      gallery_scrollgoal = -1;
      gallery_scrollspeed = 0;
      cumscroll = 0;
      return 0;
   }
   
   cumscroll = 0.004 * gallery_scrollspeed;                                      //  based on 4 millisec. timer

   scrollposn = gtk_adjustment_get_value(Gadjust);

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

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

   gtk_adjustment_set_value(Gadjust,scrollposn);

   return 1;
}


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

void navi::navibutt_clicked(GtkWidget *widget, int *lev)
{
   char     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,"ALL",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,"Fotoxx home",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,"Saved Areas",newtop_menu_event,0,0);
   add_popmenu_item(popmenu,E2X("recent images"),newtop_menu_event,0,0);
   add_popmenu_item(popmenu,E2X("newest images"),newtop_menu_event,0,0);
   
   popup_menu(Mwin,popmenu);
   return;
}

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

   if (! menu) return;

   strncpy0(folder,menu,200);

   if (strmatch(menu,"ALL")) {
      m_alldirs(0,0);
      return;
   }

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

   if (strmatch(menu,E2X("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,"Fotoxx home"))                                             //  fotoxx 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;

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

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

   snprintf(findcomm,200,"%s/*",albums_folder);                                  //  find all album names               20.0
   zfind(findcomm,flist,NF);
   
   for (ii = 0; ii < NF && count < 200; ii++)
   {
      pp = strrchr(flist[ii],'/');
      if (! pp) continue;
      pp++;
      if (*pp == '.') continue;
      albums[count] = zstrdup(pp);
      count++;
   }

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

   if (! count) {
      zmessageACK(Mwin,E2X("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,E2X("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 *, cchar *menu)                         //  menu event function
{
   char  albumfile[200];
   
   if (! menu || strmatch(menu,E2X("Current Album"))) 
   {
      if (! curr_album || ! *curr_album) {
         zmessageACK(Mwin,E2X("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
//  dialog to choose gallery sort order and sort the gallery

void navi::gallery_sort()
{
   zdialog     *zd;
   int         zstat, nn;
   char        albumfile[200], *pp, *pp2;
   
   cchar  *resetmess = E2X(" Reset all galleries\n to file name ascending");

/***
          ________________________________
         |     Gallery Sort               |
         |                                |
         |  (o) File Name                 |
         |  (o) File Mod Date/Time        |
         |  (o) Photo Date/Time (EXIF)    |
         |  (o) File Size (bytes)         |                                      //  19.0
         |  (o) Image Size (pixels)       |
         |  (o) ascending  (o) descending |
         |                                |
         |  [x] reset all galleries       |
         |      to file name ascending    |
         |                                |
         |                        [Apply] |
         |________________________________|

***/

   zd = zdialog_new(E2X("Gallery Sort"),Mwin,Bapply,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",E2X("File Name"));
   zdialog_add_widget(zd,"radio","filedate","vb1",E2X("File Mod Date/Time"));
   zdialog_add_widget(zd,"radio","photodate","vb1",E2X("Photo Date/Time (EXIF)"));
   zdialog_add_widget(zd,"radio","filesize","vb1",E2X("File Size (bytes)"));
   zdialog_add_widget(zd,"radio","pixelsize","vb1",E2X("Image Size (pixels)"));
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","ascending","hb2",E2X("ascending"),"space=4");
   zdialog_add_widget(zd,"radio","descending","hb2",E2X("descending"),"space=2");
   zdialog_add_widget(zd,"hbox","hbreset","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","reset","hbreset",resetmess,"space=4");

   zdialog_stuff(zd,"filename",0);                                               //  all buttons off
   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,"ascending",0);
   zdialog_stuff(zd,"descending",0);
   zdialog_stuff(zd,"reset",0);
   
   if (gallerysort == FNAME || gallerysort == SNONE)                             //  19.0
      zdialog_stuff(zd,"filename",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 (galleryseq == ASCEND || galleryseq == QNONE)                              //  19.0
      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,"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,"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;
}


//  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         *pxbT;
   int            evtype, mousex, mousey, mousebutt;
   int            row, col, nrows, tww, thh, marg;
   int            Nth, poswidth, posheight, err, ftype;
   static int     Fmyclick = 0;
   char           *filez = 0;
   STATB          statb;
   
   if (! Nfiles) 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;

   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 + (Nfiles-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 >= Nfiles) return 1;

   filez = zstrdup(GFlist[Nth].file);                                            //  file (thumbnail) at mouse posn.
   *filez = '/';

   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);                                             //  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);
      clicked_posn = Nth;
      clicked_width = poswidth;                                                  //  normalized 0-100
      clicked_height = posheight;

      if (thumbsize) {
         pxbT = get_cache_thumb(filez,0);                                        //  get thumbnail image
         if (pxbT) {
            tww = gdk_pixbuf_get_width(pxbT);                                    //  thumbnail width and height
            thh = gdk_pixbuf_get_height(pxbT);
            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_gallery_select) gallery_select_Lclick_func(Nth);                 //  send to gallery_select()
         else if (zd_gallery_select1) gallery_select1_Lclick_func(Nth);          //  send to gallery_select1()
         else if (zd_album_update) update_albums_Lclick_func(Nth);               //  send to album_replacefile()        19.0
         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 (KBshiftkey) popup_image(filez,MWIN,1,0);                       //  popup enlarged image
         else if (zd_metaview) meta_view(0);                                     //  EXIF/IPTC data view window
         else if (zd_copymove) m_copy_move(0,0);                                 //  copy/move dialog
         else if (zd_rename) m_rename(0,0);                                      //  rename dialog
         else if (zd_permissions) m_permissions(0,0);                            //  permissions dialog                 20.0
         else if (zd_deltrash) m_delete_trash(0,0);                              //  delete/trash dialog
         else if (zd_editmeta) m_meta_edit_main(0,0);                            //  edit metadata dialog
         else if (zd_editanymeta) m_meta_edit_any(0,0);                          //  edit any metadata dialog
         else if (zd_deletemeta) m_meta_delete(0,0);                             //  delete metadata dialog
         else gallery_Lclick_func(Nth);                                          //  open the file
      }

      if (mousebutt == 2) {                                                      //  middle click
         ftype = image_file_type(clicked_file);
         if (ftype == IMAGE) gallery_popimage();
         if (ftype == RAW) gallery_popimage();
      }

      if (mousebutt == 3) {                                                      //  right click
         if (zd_gallery_select) gallery_select_Rclick_func(Nth);                 //  send to gallery_select()
         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    19.0
}


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

char * navi::gallery_dragfile()
{
   char     dragfile[200];
   FILE     *fid;
   
   snprintf(dragfile,200,"%s/%s",get_zhomedir(),"drag_from_gallery");            //  save source gallery name           19.0
   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, char *file)
{
   int      err, top, mpos, speed;
   int      row, col, nrows, Nth, cc;
   int      poswidth, from_posn = -1;
   char     dragfile[200], buff[200];
   char     *pp1, *pp2, *from_gallery = 0, *newfile;
   FILE     *fid;
   
   if (gallerytype != GDIR && gallerytype != ALBUM) return;                      //  reject others (search, recent ...)

   if (! file)                                                                   //  drag motion underway
   {
      if (! mousex) {                                                            //  drag leave event                   19.0
         gallery_scroll(-1,0);                                                   //  stop scroll               bugfix
         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 == GDIR)                                                      //  folder gallery, add new file
   {
      err = copyFile(file,galleryname);
      if (err) {
         zfree(file);
         zmessageACK(Mwin,strerror(err));
         return;
      }
      
      pp1 = strrchr(file+2,'/');                                                 //  isolate filename.ext               20.0
      if (! pp1) return;
      cc = strlen(pp1);
      newfile = zstrdup(galleryname,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 + (Nfiles-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 > Nfiles) Nth = Nfiles;                                               //  last + 1
   

   snprintf(dragfile,200,"%s/%s",get_zhomedir(),"drag_from_gallery");            //  get source gallery name            19.0
   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);
   pp2 = fgets_trim(buff,200,fid);                                               //  source gallery position
   if (pp2) from_posn = atoi(pp2);
   fclose(fid);

   if (! pp1 || from_posn < 0) {
      printz("drag_from data not available \n");
      return;
   }
   
   if (from_gallery && strmatch(galleryname,from_gallery)) {                     //  drag-drop in same gallery   bugfix 19.0
      album_movefile(from_posn,Nth);                                             //  move file position in gallery      19.0
   }
   else album_addfile(file,Nth);                                                 //  insert new file at position
   
   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(cchar *action)
{
   int      ii;
   int      row1, row2, rowf;

   gallery_scroll(-1,0);                                                         //  stop scrolling
   
   if (strmatch(action,"Zoom+")) {
      menufuncx(0,"Zoom+");
      return 1;
   }

   if (strmatch(action,"Zoom-")) {
      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,"First")) {
      menufuncx(0,"First");
      return 1;
   }

   if (strmatch(action,"Last")) {
      menufuncx(0,"Last");
      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,"First");
      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) {
      printz("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
   char     *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(cchar *action)
{
   FILE        *fid;
   char        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 Fotoxx startup time
      NGmem = 0;

      fid = fopen(gallerymem_file,"r");                                          //  open gallery memiry 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);
         }

         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 == GDIR) {
            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);                       //  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()
{
   char     *pp, title[200];

   if (FGWM != 'G') return;

   if (gallerytype == GDIR)
      snprintf(title,200,"Fotoxx   FOLDER %s   %d folders  %d files",
                           galleryname,Nfolders,Nimages);

   else if (gallerytype == SEARCH || gallerytype == META)
      snprintf(title,200,"Fotoxx   SEARCH RESULTS   %d files",Nimages);

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

   else if (gallerytype == RECENT)
      strcpy(title,"Fotoxx   RECENT FILES");

   else if (gallerytype == NEWEST)
      strcpy(title,"Fotoxx   NEWEST FILES");

   else strcpy(title,"Fotoxx   UNKNOWN");

   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.
//  returned file is subject for zfree().

char * prev_next_file(int index, int lastver)                                    //  overhauled
{
   char * prev_next_gallery(int index);

   int      Nth;
   char     *rootname1 = 0, *rootname2 = 0;                                      //  file names without .vNN and .ext
   char     *file = 0, *filever = 0;

   Nth = curr_file_posn;

   if (index == +1)                                                              //  get next file
   {
      while (true)
      {
         Nth += 1;
         if (Nth >= Nfiles) {                                                    //  no more files this gallery
            if (filever) break;                                                  //  return last file version
            goto retnull;                                                        //  no more files
         }
         if (file) zfree(file);
         file = gallery(0,"get",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
         zfree(filever);
         filever = file;                                                         //  save last file with same rootname
         file = 0;
      }

      if (file) zfree(file);
      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 < Nfolders) goto retnull;                                       //  no more files
         if (file) zfree(file);
         file = gallery(0,"get",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:
   if (file) zfree(file);
   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().

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

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

   olddir = zstrdup(galleryname);                                                //  olddir = current gallery / folder
   if (! olddir) goto errret;
   nn = strlen(olddir) - 1;
   if (olddir[nn] == '/') olddir[nn] = 0;
   parentdir = zstrdup(olddir);                                                  //  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 < Nfolders; 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 >= Nfolders) 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(cchar *file, int Nth)
{
   int      ii;

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

   if (Nth >= Nfolders && Nth < Nfiles) 
      if (strmatch(file,GFlist[Nth].file)) return Nth;

   for (ii = Nfolders; ii < Nfiles; ii++)
      if (strmatch(file,GFlist[ii].file)) break;

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


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

//  gallery menu, thumbnail view

void m_thumbview(GtkWidget *, cchar *menu)                                       //  20.04
{
   F1_help_topic = "thumb view";
   Flistview = 0;
   if (thumbsize < 256) thumbsize = 256;
   m_viewmode(0,"G");
   gallery(0,"paint",-1);
   return;
}


//  gallery menu, metadata view

void m_metaview(GtkWidget *, cchar *menu)                                        //  20.0
{
   F1_help_topic = "meta view";
   Flistview = 2;
   m_viewmode(0,"G");
   gallery(0,"paint",-1);
   return;
}


//  gallery menu, list view

void m_listview(GtkWidget *, cchar *menu)                                        //  20.0
{
   F1_help_topic = "list view";
   Flistview = 1;
   m_viewmode(0,"G");
   gallery(0,"paint",-1);
   return;
}


//  Show recently viewed image files.

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

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

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


//  add a new file to the list of recent files, first position

void add_recent_file(cchar *newfile)
{
   int      ii;
   char     *pp;

   linedit_open(recentfiles_file);
   linedit_put(newfile);                                                         //  add new file as first in list
   
   for (ii = 0; ii < 500; ii++) {                                                //  read remaining list, up to 500
      pp = linedit_get();
      if (! pp) break;
      if (strmatch(pp,newfile)) continue;                                        //  omit new file if elsewhere in list
      linedit_put(pp);
   }
   
   linedit_close();
   return;
}


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

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

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


//  menu function

void m_newfiles(GtkWidget *, cchar *menu)
{
   using namespace newfiles;
   
   int newfile_comp(cchar *rec1, cchar *rec2);
   
   cchar  *mess = E2X("Use EXIF photo date or \n file modification date?");

   F1_help_topic = "newest";

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

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }
   
   if (Nxxrec == 0) {
      zmessageACK(Mwin,"no files found");
      return;
   }

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

   if (menu && strmatch(menu,"EXIF")) sort = 1;
   else if (menu && strmatch(menu,"file")) sort = 2;
   else sort = zdialog_choose(Mwin,"mouse",mess,"EXIF",E2X("File"),null);
   
   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 EXIF photo date                19.13
         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((char *) 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);
   m_viewmode(0,"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(cchar *rec1, cchar *rec2)
{
   char *date1 = ((newfile_t *) rec1)->fdate;
   char *date2 = ((newfile_t *) rec2)->fdate;
   return strcmp(date2,date1);
}


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

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

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

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

   if (! clicked_file) 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;
}


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

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

FTYPE image_file_type(cchar *file)
{
   int         err, xcc, tcc;
   static int  ftf = 1, tdcc = 0;
   cchar       *ppx;
   char        ppx2[8], *ppt, *RP;
   STATB       statbuf;

   if (ftf) {
      if (thumbfolder && *thumbfolder == '/')                                    //  thumbnail top folder 
         tdcc = strlen(thumbfolder);
      myRAWtypes = zstrdup(" ");                                                 //  clear cache of found file types
      myVIDEOtypes = zstrdup(" ");
      ftf = 0;
   }

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

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

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

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

   if (tdcc && strmatchN(RP,thumbfolder,tdcc)) {                                 //  fotoxx 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 list of known RAW types
      tcc = strlen(myRAWtypes) + xcc + 2;
      ppt = (char *) zmalloc(tcc);                                               //  add to cache of my RAW types
      strcpy(ppt,ppx2);
      strcpy(ppt+xcc+1,myRAWtypes);
      zfree(myRAWtypes);
      myRAWtypes = ppt;
      return RAW;
   }

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

   if (strcasestr(VIDEOfiletypes,ppx2)) {                                        //  found in known VIDEO types
      tcc = strlen(myVIDEOtypes) + xcc + 2;
      ppt = (char *) zmalloc(tcc);                                               //  add to cache of my VIDEO types
      strcpy(ppt,ppx2);
      strcpy(ppt+xcc+1,myVIDEOtypes);
      zfree(myVIDEOtypes);
      myVIDEOtypes = ppt;
      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

char * thumb2imagefile(cchar *thumbfile)                                         //  simplified
{
   STATB       statb;
   int         err;
   uint        cc;
   char        *imagefile;
   static int  Fdone = 0;
   
   if (! thumbfolder || *thumbfolder != '/') {
      if (! Fdone) printz("%s \n","no thumbnail folder");
      Fdone++;
      return 0;
   }

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

   imagefile = zstrdup(thumbfile+cc);                                            //  /imagefolder/file.xxx.jpeg
   cc = strlen(imagefile);
   imagefile[cc-5] = 0;                                                          //  /imagefolder/file.xxx
   err = stat(imagefile,&statb);                                                 //  check file exists
   if (! err) return imagefile;                                                  //  return image file
   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().

char * image2thumbfile(cchar *imagefile)                                         //  simplified
{
   int         cc1, cc2, err;
   char        *RP, *thumbfile;
   static int  Fdone = 0;
   STATB       statb;
   
   if (! thumbfolder || *thumbfolder != '/') {
      if (! Fdone++) printz("%s \n","no thumbnail folder");
      return 0;
   }
   
   RP = f_realpath(imagefile);                                                   //  use real path                      20.0
   if (! RP) return 0;
   err = stat(RP,&statb);
   if (err) return 0;
   cc1 = strlen(thumbfolder);
   cc2 = strlen(RP);
   thumbfile = (char *) zmalloc(cc1+cc2+6);
   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;
}


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

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

int thumbfile_OK(cchar *imagefile)
{
   int         err, ftype;
   STATB       statf, statb;
   char        *thumbfile;

   err = stat(imagefile,&statf);
   if (err) return 1;                                                            //  file does not exist

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

   thumbfile = image2thumbfile(imagefile);                                       //  get thumbnail file for image file
   if (! thumbfile) return 0;                                                    //  should not happen
   err = stat(thumbfile,&statb);                                                 //  thumbfile exists?
   if (err) return 0;                                                            //  no
   if (statb.st_mtime != statf.st_mtime) return 0;                               //  thumbnail is stale                 19.5
   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;
   static PIXBUF     *bigpixbuf = 0;
   static PIXBUF     *pixbuf = 0;
   static int        psize = -1;
   static char       thumbfile[300] = "";
   GError            *gerror = 0;
   
   if (ftf) {
      ftf = 0;
      strncatv(thumbfile,300,zfuncs::zimagedir,"/folder.png",0);
      bigpixbuf = gdk_pixbuf_new_from_file(thumbfile,&gerror);
      if (! bigpixbuf) {
         printz("cannot make folder pixbuf: ");
         if (gerror) printz("%s",gerror->message);
         printz("\n");
      }
   }
   
   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 char       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) {
         printz("cannot make broken pixbuf: ");
         if (gerror) printz("%s",gerror->message);
         printz("\n");
      }
   }
   
   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(cchar *imagefile)                                           //  19.0
{
   char        *thumbfile = 0, *pp;
   PXB         *imagepxb = 0, *thumbpxb = 0;
   STATB       statb;
   int         err, size, retstat;
   timeval     thumbtimes[2];
   
   if (thumbfile_OK(imagefile)) return 0;                                        //  thumbnail file exists

   thumbfile = image2thumbfile(imagefile);                                       //  get thumbnail file for image file
   if (! thumbfile) { retstat = 2; goto returnx; }
   
   pp = strrchr(thumbfile,'/');                                                  //  check if thumbnail folder exists
   *pp = 0;
   err = stat(thumbfile,&statb);
   *pp = '/';
   if (err) {                                                                    //  no
      pp = thumbfile;
      while (true) {
         pp = strchr(pp+1,'/');                                                  //  check each folder level
         if (! pp) break;
         *pp = 0;
         err = stat(thumbfile,&statb);
         if (err) {
            err = mkdir(thumbfile,0750);                                         //  if missing, try to create
            if (err && errno != EEXIST) {                                        //  this happens (parallel threads)
               retstat = 2;
               goto returnx;
            }
         }
         *pp = '/';
      }
   }

   size = thumbfilesize;
   
   pp = (char *) strrchr(imagefile,'.');                                         //  GDK pixbufs no longer used         19.0
   if (pp && strcasestr(".jpg .jpeg",pp)) {
      thumbpxb = JPG_PXB_load(imagefile,size);                                   //  load JPEG file to PXB (reduced)
      if (! thumbpxb) { retstat = 2; goto returnx; }
   }
   else {
      if (image_file_type(imagefile) == RAW)                                     //  load RAW image file to PXB
         imagepxb = RAW_PXB_load_dcraw_half(imagefile);                          //  use dcraw_half for speed           20.0
      else imagepxb = PXB_load(imagefile,0);                                     //  load other image file to PXB
      if (! imagepxb) { retstat = 2; goto returnx; }
      thumbpxb = PXB_resize(imagepxb,size);                                      //  resize 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; }

   err = stat(imagefile,&statb);                                                 //  get image file mod time
   if (err) { retstat = 2; goto returnx; }
   
   thumbtimes[0].tv_sec = thumbtimes[1].tv_sec = statb.st_mtim.tv_sec;           //  thumbnail mod time                 19.5
   thumbtimes[0].tv_usec = thumbtimes[1].tv_usec = 0;                            //    = image file mod time
   utimes(thumbfile,thumbtimes);
   retstat = 1;

returnx:
   if (retstat > 1) printz("update_thumbfile() failure: %s \n",imagefile);
   if (thumbfile) zfree(thumbfile);
   if (imagepxb) PXB_free(imagepxb);
   if (thumbpxb) PXB_free(thumbpxb);
   return retstat;
}


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

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

void delete_thumbfile(cchar *imagefile)
{
   int         err;
   STATB       statf;
   char        *tpath;

   tpath = image2thumbfile(imagefile);
   if (! tpath) return;
   err = stat(tpath,&statf);                                                     //  remove from disk
   if (! err && S_ISREG(statf.st_mode)) remove(tpath);
   zfree(tpath);
   return;
}


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

   Get thumbnail image (pixbuf) for given image file.
   Use cached thumbnail image if available and not stale.
   Use thumbnail file if available and not stale.
   Create or replace thumbnail file if missing or stale.
   Add thumbnail file to cache if not already there.
   Return thumbnail pixbuf or null if error.
   Returned pixbuf is a cache entry (DO NOT g_object_unref()).
   3000 x 512 x 400 x 3 = 1.7 GB for 512 pixel thumbnail size.
   This function is also called in thread preload_thumbs().

   thumb file --> get_cache_thumb() <--> thumb cache 
                         V           --> PIXBUF returned
   image file --> update_thumbfile() --> thumb file

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

namespace thumbnail_cache
{
   int         ftf = 1;
   char        lockname[200];
   int         cachesize = 1000;                                                 //  max thumbnails cached in memory
   int         maxhash = 10 * cachesize;                                         //  hash table = 10 * cache size
   int         hashw = 20;                                                       //  hash search width

   typedef struct {
      char     *imagefile;
      PXB      *pxb;                                                             //  PXB with congruent pixbuf
      time_t   mtime;
      int      size;
   }  thumbtab_t;

   char        **filetab;                                                        //  cached imagefiles
   int         *indextab;                                                        //  corresp. thumbtab indexes
   thumbtab_t  *thumbtab;                                                        //  corresp. cached thumbtab
   int         nextcache = 0;
}


//  look ahead of gallery page and preload thumbnails into cache

void preload_thumbs_start()                                                      //  initialize, start the threads      20.0
{
   void * preload_thumbs_thread(void *);
   start_detached_thread(preload_thumbs_thread,0);
   return;
}


//  thread function - add thumbnails to thumbnail cache ahead of need

void * preload_thumbs_thread(void *)                                             //  thread runs continuously           20.0
{
   int check_cache_thumb(cchar *imagefile);

   int      ii, jj, err, fnn, size;
   char     *imagefile, *thumbfile;
   PXB      *tempxb, *thumbpxb;
   static int  last_preload_start = 0;
   
   while (true)                                                                  //  loop forever
   {
      while (preload_trigger == 0) zsleep(0.001);                                //  wait for gallery page change
      preload_trigger = 0;

      if (preload_start == last_preload_start) continue;
      last_preload_start = preload_start;
      
      for (ii = 0; ii < 50; ii++)   
      {
         if (FGWM != 'G') break;                                                 //  not gallery view

         fnn = preload_start + ii;                                               //  gallery page last file + ii
         if (fnn > Nfiles-1) break;                                              //  beyond last gallery file
         imagefile = GFlist[fnn].file;
         if (! imagefile) break;

         while (! resource_lock(GFlock)) zsleep(0.0001);                         //  gallery is changing
         jj = check_cache_thumb(imagefile);                                      //  thumbnail in cache already?
         resource_unlock(GFlock);
         if (jj) continue;                                                       //  yes

         err = update_thumbfile(imagefile);                                      //  check/create thumbnail file        20.0
         if (err > 1) continue;                                                  //  fail

         if (thumbsize <= thumbfilesize) {                                       //  thumbnail size
            thumbfile = image2thumbfile(imagefile);                              //  small, use thumbnail file
            tempxb = JPG_PXB_load(thumbfile);
            zfree(thumbfile);
         }
         else tempxb = JPG_PXB_load(imagefile);                                  //  large, use image file
         if (! tempxb) continue;                                                 //  fail

         size = tempxb->ww;                                                      //  get actual size
         if (tempxb->hh > size) size = tempxb->hh;
         if (size == thumbsize) thumbpxb = tempxb;                               //  current thumb size?
         else {
            thumbpxb = PXB_resize(tempxb,thumbsize);                             //  no, downsize
            PXB_free(tempxb);
         }
         if (! thumbpxb) continue;

         while (! resource_lock(GFlock)) zsleep(0.0001);                         //  wait if gallery is locked
         get_cache_thumb(imagefile,thumbpxb);                                    //  add to thumbnail cache
         resource_unlock(GFlock);
      }
   }
}


//  initialize cache at first call

void init_cache_thumb()
{
   using namespace thumbnail_cache;

   int      cc;

   ftf = 0;
   
   cc = (maxhash + hashw) * sizeof(char *);                                      //  allocate table space and clear
   filetab = (char **) zmalloc(cc);                                              //  (+ hashw to avoid wraparound logic)
   memset(filetab,0,cc);

   cc = (maxhash + hashw) * sizeof(int);
   indextab = (int *) zmalloc(cc);
   memset(indextab,-1,cc);

   cc = cachesize * sizeof(thumbtab_t);
   thumbtab = (thumbtab_t *) zmalloc(cc);
   memset(thumbtab,0,cc);
   
   preload_thumbs_start();                                                       //  start preload thumbs threads       20.0
   
   return;
}


//  get thumbnail from cache, or add to cache if missing
//  use given thumbnail PXB if not null
//  create new thumbnail file if necessary, create thumbnail PXB

PIXBUF * get_cache_thumb(cchar *imagefile, PXB *thumbpxb)
{
   using namespace thumbnail_cache;

   PXB         *tempxb;
   char        *thumbfile, *purgefile;
   int         Fii, Tii, Pii;
   int         ii, err;
   FTYPE       ftype;
   time_t      mtime;
   STATB       statf;
   
   if (ftf) init_cache_thumb();                                                  //  first call
   
   if (thumbsize == 0) return 0;                                                 //  should not happen
   
   err = stat(imagefile,&statf);                                                 //  check file exists
   if (err) return 0;
   mtime = statf.st_mtime;                                                       //  last modification time

   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

   while (! resource_lock(TClock)) zsleep(0.001);                                //  lock thumbnail cache

   Fii = strHash(imagefile,maxhash);                                             //  find imagefile in filetab
   for (ii = 0; ii < hashw; ii++) {
      if (filetab[Fii+ii] == 0) continue;                                        //  empty position
      if (strmatch(imagefile,filetab[Fii+ii])) break;                            //  found
   }
   
   Tii = -1;                                                                     //  no thumbtab cache position

   if (ii < hashw) {                                                             //  found in cache
      Fii += ii;                                                                 //  filetab entry
      Tii = indextab[Fii];                                                       //  corresp. thumbtab entry
      if (Tii == -1) goto bug0;                                                  //  must exist
      if (! strmatch(imagefile,thumbtab[Tii].imagefile)) goto bug1;              //  must match
      if (thumbsize != thumbtab[Tii].size) goto add_to_cache;                    //  not current thumb size
      if (mtime != thumbtab[Tii].mtime) goto add_to_cache;                       //  thumbtab is stale
      resource_unlock(TClock);                                                   //  unlock thumbnail cache
      return thumbtab[Tii].pxb->pixbuf;                                          //  return reference to cache
   }

   for (ii = 0; ii < hashw; ii++)                                                //  not found in cache
      if (filetab[Fii+ii] == 0) break;                                           //  get first empty position
   if (ii == hashw) goto bug2;
   Fii += ii;                                                                    //  use this position for new entry

add_to_cache:                                                                    //  not found, not size, or stale

   if (! thumbpxb)
   {
      ii = update_thumbfile(imagefile);                                          //  make/refresh thumbnail file
      if (ii > 1) goto ret0;                                                     //  fail

      if (thumbsize <= thumbfilesize) {
         thumbfile = image2thumbfile(imagefile);                                 //  small, use thumbnail file
         tempxb = JPG_PXB_load(thumbfile);
         zfree(thumbfile);
      }
      else tempxb = JPG_PXB_load(imagefile);                                     //  large, use image file

      if (! tempxb) goto ret0;                                                   //  fail

      ii = tempxb->ww;                                                           //  get actual size
      if (tempxb->hh > ii) ii = tempxb->hh;
      if (ii == thumbsize) thumbpxb = tempxb;                                    //  current thumbnail size ?
      else {
         thumbpxb = PXB_resize(tempxb,thumbsize);                                //  no, resize
         PXB_free(tempxb);
      }

      if (! thumbpxb) goto ret0;                                                 //  fail
   }

   if (Tii == -1) {                                                              //  add new thumbtab entry
      nextcache++;                                                               //  next cache slot (oldest)
      if (nextcache == cachesize) nextcache = 0;
      Tii = nextcache;
   }

   purgefile = thumbtab[Tii].imagefile;                                          //  prior occupant of thumbtab
   if (purgefile) {
      PXB_free(thumbtab[Tii].pxb);                                               //  free thumbtab entry
      Pii = strHash(purgefile,maxhash);                                          //  find purgefile in filetab
      for (ii = 0; ii < hashw; ii++) {
         if (filetab[Pii+ii] == 0) continue;                                     //  skip empty position
         if (strmatch(purgefile,filetab[Pii+ii])) break;                         //  found
      }
      if (ii == hashw) goto bug3;                                                //  not found
      Pii += ii;
      zfree(filetab[Pii]);                                                       //  free filetab entry
      filetab[Pii] = 0;
      indextab[Pii] = -1;                                                        //  free indextab entry
      zfree(purgefile);
   }

   thumbtab[Tii].imagefile = zstrdup(imagefile);                                 //  add thumbnail PXB to cache
   thumbtab[Tii].pxb = thumbpxb;
   thumbtab[Tii].size = thumbsize;
   thumbtab[Tii].mtime = mtime;
   filetab[Fii] = zstrdup(imagefile);                                            //  add filetab and indextab entries
   indextab[Fii] = Tii;

   resource_unlock(TClock);                                                      //  unlock thumbnail cache
   return thumbtab[Tii].pxb->pixbuf;                                             //  return reference to cache

ret0:
   resource_unlock(TClock);                                                      //  unlock thumbnail cache
   return 0;

bug0:
   resource_unlock(TClock);                                                      //  unlock thumbnail cache
   printz("get_cache_thumb() indextab entry missing \n");
   return get_broken_pixbuf();                                                   //  return ref. to 'broken' pixbuf

bug1:
   resource_unlock(TClock);
   printz("get_cache_thumb() indextab thumbtab no match \n");
   return get_broken_pixbuf();

bug2: 
   resource_unlock(TClock);
   printz("get_cache_thumb() hash table failure \n");
   return get_broken_pixbuf();

bug3:
   resource_unlock(TClock);
   printz("get_cache_thumb() purgefile not in filetab \n");
   return get_broken_pixbuf();
}


//  check if thumbnail and thumbsize is currently in the thumbnail cache
//  return 0 if not found
//  return 1 if found, or not needed (not image, not found, folder)
//  used by preload_thumbs()

int check_cache_thumb(cchar *imagefile) 
{
   using namespace thumbnail_cache;

   int         Fii, Tii;
   int         ii, err;
   FTYPE       ftype;
   time_t      mtime;
   STATB       statf;
   
   if (ftf) init_cache_thumb();                                                  //  first call
   
   ftype = image_file_type(imagefile);
   if (ftype == FDIR)  return 1;                                                 //  'folder' image
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) return 1;               //  not an image or RAW or VIDEO file 
   
   err = stat(imagefile,&statf);                                                 //  check file exists
   if (err) return 1;
   mtime = statf.st_mtime;                                                       //  last modification time

   Fii = strHash(imagefile,maxhash);                                             //  find imagefile in filetab
   for (ii = 0; ii < hashw; ii++) {
      if (filetab[Fii+ii] == 0) continue;                                        //  skip empty position
      if (strmatch(imagefile,filetab[Fii+ii])) break;                            //  found
   }

   if (ii == hashw) return 0;                                                    //  not found
   
   Fii += ii;                                                                    //  filetab entry
   Tii = indextab[Fii];                                                          //  corresp. thumbtab entry
   if (Tii == -1) goto bug0;                                                     //  must exist
   if (! strmatch(imagefile,thumbtab[Tii].imagefile)) goto bug1;                 //  must match
   if (thumbsize != thumbtab[Tii].size) return 0;                                //  thumbtab not current size
   if (mtime != thumbtab[Tii].mtime) return 0;                                   //  thumbtab is stale
   return 1;                                                                     //  thumbtab OK

bug0:
   printz("check_cache_thumb() index tab entry missing \n");
   return 0;

bug1:
   printz("check_cache_thumb() indextab thumbtab no match \n");
   return 0;
}


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

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

char * gallery_select1(cchar *gfolder)
{
   zdialog     *zd;
   int         zstat;
   static char filename[XFCC];
   char        *cfolder;
   char        fgwm[4];

   fgwm[0] = FGWM;                                                               //  save current view mode
   fgwm[1] = 0;
   cfolder = navi::galleryname;                                                  //  and gallery folder                 20.0

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

   m_viewmode(0,"G");

/***
          _________________________________________
         |             Image File                  |
         |                                         |
         |  click on thumbnail to select file      |
         |                                         |
         | Image File [__________________________] |
         |                                         |
         |                         [Done] [Cancel] |
         |_________________________________________|

***/

   zd = zdialog_new(E2X("Image File"),Mwin,Bdone,Bcancel,null);                  //  dialog to select a thumbnail
   zd_gallery_select1 = zd;
   zdialog_add_widget(zd,"label","labtip","dialog",Bclickthumbtoselect);         //  20.0
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labf","hbf",E2X("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_fetch(zd,"filename",filename,XFCC);                                   //  get selected file
   zdialog_destroy(zd);
   zd_gallery_select1 = 0;

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

   if (zstat != 1) return 0;                                                     //  no selection
   if (*filename != '/') return 0;

   return f_realpath(filename);                                                  //  use real path                      20.0
}


void gallery_select1_Lclick_func(int Nth)                                        //  called by gallery mouse function
{
   char     *imagefile = 0;
   int      ftype;

   if (! zd_gallery_select1) 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;
   }

   zdialog_stuff(zd_gallery_select1,"filename",imagefile);                       //  stuff file name in dialog
   zfree(imagefile);
   return;
}


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

//  Select multiple image files from thumbnail gallery window.
//    GSfiles[*]: list of selected image files, GScount 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.
//  Returned status:  0 = OK, 1 = user cancel, 2 = internal error

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

   GtkWidget      *drawarea = 0;
   GtkWidget      *Ftext = 0;
   int            currline;
   int            Nselect = 0;
   char           *imagefile;
};


void gallery_select_clear()
{
   using namespace galselnames;

   for (int ii = 0; ii < GScount; ii++) zfree(GSfiles[ii]);
   GScount = 0;
   return;
}


int gallery_select()
{
   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 | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |                                  | |                        | |
         | |__________________________________| |________________________| |
         |                                                                 |
         | [Delete] [Remove] [Insert] [Clear] [Add All]                    |
         |                                                 [Done] [Cancel] |
         |_________________________________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Select Image Files"),Mwin,Bdone,Bcancel,null);
   zd_gallery_select = 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","delete","hb2",Bdelete,"space=8");
   zdialog_add_widget(zd,"button","remove","hb2",Bremove,"space=8");
   zdialog_add_widget(zd,"button","insert","hb2",Binsert,"space=8");
   zdialog_add_widget(zd,"button","clear","hb2",Bclear,"space=8");
   zdialog_add_widget(zd,"button","addall","hb2",Baddall,"space=8");
   zdialog_add_ttip(zd,"delete","delete selected file from list");
   zdialog_add_ttip(zd,"remove","remove and save selected file from list");
   zdialog_add_ttip(zd,"insert","insert next removed and saved file here");
   zdialog_add_ttip(zd,"clear","delete all files in list");
   zdialog_add_ttip(zd,"addall","add all gallery files to list");
   
   Ftext = zdialog_widget(zd,"files");
   textwidget_set_eventfunc(Ftext,callbackfunc);                                 //  set mouse/KB event function

   GtkWidget *frame = zdialog_widget(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);
   Nselect = 0;
   currline = -1;
   imagefile = 0;

   for (ii = 0; ii < GScount; ii++)                                              //  add pre-selected files to dialog
   {
      textwidget_append(Ftext,0,"%s\n",GSfiles[ii]);
      Nselect++;
   }

   m_viewmode(0,"G");                                                            //  open gallery window

   if (gallerytype == TNONE) {                                                   //  if no gallery, start with
      gallery(topfolders[0],"init",0);                                           //    top folder
      gallery(0,"sort",-2);                                                      //  recall sort and position
      gallery(0,"paint",-1);                                                     //  paint
   }

   zdialog_wait(zd);                                                             //  wait for dialog completion
   
   if (zd->zstat != 1) {                                                         //  cancelled
      zdialog_free(zd);                                                          //  kill dialog
      zd_gallery_select = 0;                                                     //  selected files unchanged
      return 1;
   }

   gallery_select_clear();                                                       //  clear gallery_select() file list

   for (ii = kk = 0; ii < Nselect; ii++)                                         //  get list of files from dialog
   {
      imagefile = textwidget_line(Ftext,ii,1);
      if (! imagefile || ! *imagefile) continue;
      GSfiles[kk] = imagefile;                                                   //  /.../filename.ext
      kk++;
      if (kk == GSmax) {
         zmessageACK(Mwin,E2X("exceed %d selected files"),GSmax);
         break;
      }
   }

   GScount = kk;                                                                 //  selected files
   
   for (ii = kk = 0; ii < GScount; ii++)                                         //  look for duplicates                20.0
      for (kk = ii + 1; kk < GScount; kk++)
         if (strmatch(GSfiles[ii],GSfiles[kk])) dups++;

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

   if (yn) {
      for (ii = kk = 0; ii < GScount; ii++) {
         if (! GSfiles[ii]) continue;
         for (kk = ii + 1; kk < GScount; kk++) {
            if (! GSfiles[kk]) continue;
            if (strmatch(GSfiles[ii],GSfiles[kk])) {
               zfree(GSfiles[kk]);
               GSfiles[kk] = 0;
            }
         }
      }

      for (ii = kk = 0; ii < GScount; ii++)
         if (GSfiles[ii])
            GSfiles[kk++] = GSfiles[ii];
      GScount = kk;
   }

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


//  gallery getfiles dialog event function

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

   char           *file, *ftemp;
   static char    *removedfiles[100];                                            //  last 100 files removed
   static int     Nremoved = 0;
   int            ii, Nth;
   FTYPE          ftype;

   if (strmatch(event,"delete"))                                                 //  delete file from list              20.0
   {
      if (! Nselect) 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
      Nselect--;
      if (currline > Nselect-1) currline = Nselect - 1;
      textwidget_highlight_line(Ftext,currline);                                 //  next line
      showthumb();                                                               //  show thumbnail
   }

   if (strmatch(event,"remove"))                                                 //  remove file at curr. position
   {                                                                             //    and save for later insertion
      if (! Nselect) return 1;
      if (currline < 0) return 1;

      if (Nremoved == 100) {                                                     //  capacity reached
         zfree(removedfiles[0]);                                                 //  remove oldest entry
         for (ii = 0; ii < 99; ii++)
            removedfiles[ii] = removedfiles[ii+1];
         Nremoved = 99;
      }

      ftemp = textwidget_line(Ftext,currline,0);                                 //  file (text line) to remove (keep \n)
      if (! ftemp) return 1;
      removedfiles[Nremoved] = ftemp;                                            //  save removed file for poss. insert
      Nremoved++;

      textwidget_delete(Ftext,currline);                                         //  remove from dialog list
      Nselect--;
      if (currline > Nselect-1) currline = Nselect - 1;
      textwidget_highlight_line(Ftext,currline);                                 //  next line
      showthumb();                                                               //  show thumbnail
   }

   if (strmatch(event,"insert"))                                                 //  insert first removed file
   {                                                                             //    at current position
      if (! Nremoved) return 1;
      textwidget_insert(Ftext,0,currline,"%s",removedfiles[0]);
      Nselect++;
      currline++;
      textwidget_highlight_line(Ftext,currline);                                 //  next line
      textwidget_scroll(Ftext,currline);
      showthumb();                                                               //  update thumbnail

      zfree(removedfiles[0]);                                                    //  remove file from the removed list
      for (ii = 0; ii < Nremoved-1; ii++)
         removedfiles[ii] = removedfiles[ii+1];
      Nremoved--;
   }

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

   if (strmatch(event,"addall"))                                                 //  insert all files in image gallery
   {
      for (Nth = 0; ; Nth++)
      {
         ftemp = gallery(0,"get",Nth);                                           //  next file
         if (! ftemp) break;

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

         file = f_realpath(ftemp);                                               //  use real path                      20.0
         if (! file) {
            zfree(ftemp);
            continue;
         }

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

      textwidget_scroll(Ftext,-1);                                               //  scroll to end
      currline = Nselect - 1;                                                    //  position at last file
      showthumb();
   }

   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(cchar *imagefile)
{
   using namespace galselnames;

   int      line;
   char     *ftemp;

   for (line = Nselect-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 at current position, set thumbnail = file

void galselnames::insert_file(cchar *imagefile)
{
   using namespace galselnames;
   
   int      ftype;
   char     *RP;

   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                      20.0
   if (! RP) return;
   textwidget_insert(Ftext,0,currline,"%s\n",RP);
   zfree(RP);
   Nselect++;
   currline++;
   textwidget_highlight_line(Ftext,currline);
   textwidget_scroll(Ftext,currline);
   showthumb();                                                                  //  update thumbnail
   return;
}


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

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

   int      line;

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


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

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

void gallery_select_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, char LR)
{
   using namespace galselnames;

   char           *imagefile;
   FTYPE          ftype;
   static int     pNth = -1;                                                     //  previously clicked file
   int            nn, incr;
   
   if (! zd_gallery_select) return;                                              //  should not happen
   if (Nth < 0) return;                                                          //  gallery gone ?

   imagefile = gallery(0,"get",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) {
      zfree(imagefile);
      pNth = -1;
      return;
   }

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

   if (! KBshiftkey)                                                             //  no shift key
   {
      pNth = Nth;                                                                //  possible start of range
      insert_file(imagefile);                                                    //  insert file at current position
      zfree(imagefile);
      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) {
            zfree(imagefile);
            continue;
         }
         insert_file(imagefile);      
         zfree(imagefile);
      }
      pNth = -1;                                                                 //  no prior
      return;
   }

   return;
}


//  process mouse or key click in files window:
//  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 (! Nselect) 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 = Nselect - 1;
      if (currline < 0) currline = 0;
      if (currline > Nselect-1) currline = Nselect - 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;
   char           *thumbfile;
   PIXBUF         *thumbpxb = 0;
   GError         *gerror = 0;

   static draw_context_t   draw_context;
   cairo_t                 *cr;
   
   if (! Nselect) 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) {
      thumbfile = image2thumbfile(imagefile);                                    //  use thumbnail file
      if (thumbfile)
         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);
      zfree(thumbfile);
   }
   else {
      cairo_set_source_rgb(cr,1,1,1);                                            //  white background only
      cairo_paint(cr);
   }

   draw_context_destroy(draw_context);
   return;
}


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

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

void m_select_files(GtkWidget *, cchar *)                                        //  20.0
{
   char     albumfile[200];
   FILE     *fid;
   int      err, ii;

   F1_help_topic = "select image files";

   err = gallery_select();
   if (err) return;
   if (GScount == 0) return;

   snprintf(albumfile,200,"%s/selected files",albums_folder);                    //  save search results to album
                                                                                 //    "selected files"   
   fid = fopen(albumfile,"w");
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }
   
   for (ii = 0; ii < GScount; ii++)
      fprintf(fid,"%s\n",GSfiles[ii]);
   
   fclose(fid);
   return;
}


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

namespace bookmarknames
{
   #define     maxbmks 50
   char        *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 *, cchar *)
{
   using namespace bookmarknames;

   int  bookmarks_dialog_event(zdialog *zd, cchar *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] |
         |_______________________________________________|
***/

   F1_help_topic = "bookmarks";
   if (zd_edit_bookmarks) return;                                                //  already busy
   if (zd_bookmark) return;

   zd = zdialog_new(E2X("Bookmarks"),Mwin,E2X("Edit Bookmarks"),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_widget(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_set_modal(zd);
   zdialog_run(zd,bookmarks_dialog_event,"mouse");                               //  run dialog
   return;
}


//  dialog event and completion function

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

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

   char     *file;
   int      err;
   STATB    sbuff;

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

   if (checkpend("all")) return;
   if (! zd_bookmark) return;

   bmkposn = line;                                                               //  get clicked line
   if (bmkposn < 0 || bmkposn > Nbmks-1) return;
   file = bookmarks[bmkposn] + 32;
   err = stat(file,&sbuff);
   if (err) {
      zmessageACK(Mwin,Bfilenotfound);
      return;
   }

   f_open(file,0);                                                               //  open bookmarked image file
   gallery(file,"init",0);                                                       //  go to gallery and file position
   gallery(file,"paint",0);
   m_viewmode(0,"G");
                                                                                 //  no close zdialog
   return;
}


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

//  edit bookmarks

void m_edit_bookmarks(GtkWidget *, cchar *)
{
   using namespace bookmarknames;
   
   int edit_bookmarks_dialog_event(zdialog *zd, cchar *event);
   void edit_bookmarks_callbackfunc(GtkWidget *, int line, int pos, int kbkey);

   zdialog     *zd;
   cchar       *bmk_add = E2X("Click a list position. Click a gallery thumbnail for the new bookmark.\n"
                              "Bookmark for thumbnail will be added. Change the name 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]           |
         |                                        [done] |
         |_______________________________________________|

***/

   F1_help_topic = "bookmarks";
   if (zd_edit_bookmarks) return;                                                //  already busy 
   if (checkpend("all")) return;

   zd = zdialog_new(E2X("Edit Bookmarks"),Mwin,Bdone,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,"frame","frbmk","dialog",0,"space=5|expand"); 
   zdialog_add_widget(zd,"scrwin","scrwin","frbmk");
   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",Brename,"space=5");
   zdialog_add_widget(zd,"button","delete","hbname",Bdelete,"space=5");

   textwidget = zdialog_widget(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

   m_viewmode(0,"G");                                                            //  show current gallery
   return;
}


//  load bookmarks list from bookmarks file

void bookmarks_load()
{
   using namespace bookmarknames;

   int         err;
   char        buff[XFCC], bmkfile[200];
   char        *pp, *pp2;
   FILE        *fid;
   STATB       stbuff;
   
   Nbmks = 0;
   err = locale_filespec("user","bookmarks",bmkfile);                            //  read bookmarks file
   if (! err) {
      fid = fopen(bmkfile,"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;
            err = stat(pp2,&stbuff);
            if (err) continue;
            bookmarks[Nbmks] = zstrdup(pp);                                      //  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;

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

   char     *imagefile, *newbookmark;
   char     *pp, bookmarkname[32];
   int      cc;

   if (! zd_edit_bookmarks) return;
   if (Nth < 0) return;                                                          //  gallery gone ?
   imagefile = gallery(0,"get",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 = (char *) zmalloc(cc);                                           //    filename  /folders.../filename
   snprintf(newbookmark,cc,"%-30s  %s",bookmarkname,imagefile);
   zfree(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, cchar *event)
{
   using namespace bookmarknames;

   char        bmkfile[200];
   char        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     19.0
      bookmarks_refresh();                                                       //  update bookmarks list in dialog
   }

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

   if (zd->zstat == 1)                                                           //  done
   {
      locale_filespec("user","bookmarks",bmkfile);                               //  write bookmarks file
      fid = fopen(bmkfile,"w");
      if (! fid)
         zmessageACK(Mwin,E2X("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;

   char     bookmarkline[XFCC+32];
   char     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;
}


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

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

namespace alldirs
{
   #define  maxdirs 10000

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

   typedef struct {
      char     *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(cchar *rec1, cchar *rec2);
   void callbackfunc(GtkWidget *, int line, int pos, int kbkey);
   void writetext();
}


//  menu function

void m_alldirs(GtkWidget *, cchar *) 
{
   using namespace alldirs;
   
   int         ii, jj, cc, pcc, NF, err;
   char        *dir, *pdir, **Flist;
   
   F1_help_topic = "all folders";
   
/***   
          ________________________________
         |       All Folders              |
         |  ____________________________  |
         | |                            | |
         | | [+] topdir1                | |
         | | [-] topdir2                | |
         | |         subdir1            | |
         | |     [+] subdir2            | |
         | |         subdir3            | |
         | | [+] topdir3                | |
         | |         ...                | |
         | |____________________________| |
         |                                |
         |                         [Done] |
         |________________________________|

***/

   if (dlist) goto report;                                                       //  already done 

   cc = drecl * maxdirs;
   dlist = (dlist_t *) zmalloc(cc);                                              //  memory for folder list
   memset(dlist,0,cc);
   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]);
         Fall++;
         if (Fall == maxdirs) break;
      }
      
      if (NF) zfree(Flist);
      if (Fall == maxdirs) break;
   }

   if (Fall > 1) 
      HeapSort((char *) 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:

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

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

   return;
}


//  sort compare function

int alldirs::compfunc(cchar *rec1, cchar *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 alldirs::callbackfunc(GtkWidget *textwidget, int line, int pos, int kbkey)
{
   using namespace alldirs;

   int         ii;
   char        *pline, *pp;
   static int  Fbusy = 0;
   
   if (checkpend("all")) return;                                                 //  check nothing pending

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

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

   m_viewmode(0,"G");                                                            //  19.0
   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 alldirs::writetext()
{
   using namespace alldirs;

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

   memset(indent,' ',100);
   
   popup_report_clear(zd);

   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_write(zd,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;
}


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

//  set the gallery from the current image file physical folder

void m_source_folder(GtkWidget*, cchar *menu)
{
   F1_help_topic = "source folder";
   if (! curr_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
   m_viewmode(0,"G");
   return;
}


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

//  dummy menu entry as target for "Show Hidden Files" KB shortcut

void m_show_hidden(GtkWidget *, cchar *)
{
   KBaction("Show Hidden");
   return;
}


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

//  dummy menu entry as target for "Current Album" KB shortcut

void m_current_album(GtkWidget *, cchar *)                                       //  20.0
{
   navi::newalbum_menu_event(0,0);
   return;
}


