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

   Fotocx - edit photos and manage collections

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

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

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   See the GNU General Public License for more details.

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

   Fotocx image edit - folder navigation and thumbnail gallery functions

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

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

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

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

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

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

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

   popup_image                show an image in a small popup window

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

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

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

/***     gallery index in fotocx.h

      typedef struct {                                current gallery file list in memory
         ch       *file;                              /folder.../filename
         int      folder;                             flag, file is a folder
         ch       fdate[16];                          file date: yyyymmddhhmmss
         ch       pdate[16];                          photo date: yyyymmddhhmmss
         uint     fsize;                              file size, bytes
         uint     psize;                              image size, pixels
         ch       *mdata1;                            fixed text for metadata report
         ch       *mdata2;                            variable text for metadata report
      }  Gindex_t;
***/

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

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

   zlist_t     *ZLrecent_folders = 0;                                                  //  list of recently used folders

   //  private functions

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

using namespace zfuncs;
using namespace navi;


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      err = find_imagefiles(galleryname,1+8+32,Flist,NF);                              //  find folders + files, one level       25.2
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         goto return0;
      }

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

         pp = strrchr(file,'/');
         if (pp && (strmatch(pp,"/.") || strmatch(pp,"/.."))) {                        //  omit folders  /.  and  /..
            zfree(file);
            continue;
         }
         
         if (! Fshowhidden && pp && *(pp+1) == '.') continue;                          //  suppress hidden files                 26.4

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

         ftype = image_file_type(file);

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

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

            xxrec = get_xxrec(file);
            if (! xxrec) {                                                             //  should not happen
               zfree(file);
               continue;
            }

            Gindex[Gfiles].file = file;
            Gindex[Gfiles].folder = 0;
            xxrec_to_gindex(xxrec,&Gindex[Gfiles]);                                    //  get data from file xxrec_tab[]

            Gfiles++;
            Gimages++;
         }

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

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

      if (Flist) zfree(Flist);

      gallerytype = FOLDER;                                                            //  gallery type = folder
      gallerysort = FNAME;                                                             //  sort Gindex by file name ascending
      galleryseq = ASCEND;
      galleryposn = 0;

      add_recent_folder(galleryname);                                                  //  add gallery to recent list

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

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

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

         Gfiles -= kk;
         Gimages -= kk;
      }

      gallerypainted = 0;
      goto return0;
   }

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

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

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

         xxrec = get_xxrec(file);                                                      //  get xxrec_tab[]
         if (! xxrec) continue;                                                        //  deleted, not image file

         Gindex[Gfiles].file = zstrdup(file,"gallery");                                //  add to gallery index
         Gindex[Gfiles].folder = 0;
         xxrec_to_gindex(xxrec,&Gindex[Gfiles]);                                       //  get data from file xxrec_tab[]

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

      fclose(fid);

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

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

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

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

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

      xxrec = get_xxrec(filez);
      if (! xxrec) goto return0;                                                       //  deleted, not image file

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

      Gindex[fposn].file = zstrdup(filez,"gallery");                                   //  put new file in hole
      Gindex[Gfiles].folder = 0;
      xxrec_to_gindex(xxrec,&Gindex[fposn]);                                           //  get data from file xxrec_tab[]

      Gfiles++;
      Gimages++;
      gallerypainted = 0;
      goto return0;
   }

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

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

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

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

      xxrec = get_xxrec(filez);
      if (! xxrec) goto return0;                                                       //  deleted, not image file

      xxrec_to_gindex(xxrec,&Gindex[Gfiles]);                                          //  get data from file xxrec_tab[]

      gallerypainted = 0;
      goto return0;
   }

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

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

return2:
   resource_unlock(GFlock);
   return file2;
}


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

//  private function
//  fill Gindex[] record data from xxrec_tab[] data

void  navi::xxrec_to_gindex(xxrec_t *xxrec, Gindex_t *gindex)
{
   uint        ww, hh;
   ch          fdate[16], pdate[16];
   static ch   *strings[2];
   static int  ftf = 1;

   if (ftf) {
      ftf = 0;
      strings[0] = (ch *) malloc(12);                                                  //  pre-allocate strings
      strings[1] = (ch *) malloc(12);
   }

   gindex->fsize = atoi(xxrec->fsize);                                                 //  file size

   get_substrings(xxrec->psize,' ',2,12,strings);                                      //  pixel size
   ww = atoi(strings[0]);
   hh = atoi(strings[1]);
   gindex->psize = ww * hh;

   strncpy(fdate+0,xxrec->fdate+0,4);                                                  //  convert  yyyy:mm:dd hh:mm:ss
   strncpy(fdate+4,xxrec->fdate+5,2);                                                  //    to  yyyymmddhhmmss
   strncpy(fdate+6,xxrec->fdate+8,2);
   strncpy(fdate+8,xxrec->fdate+11,2);
   strncpy(fdate+10,xxrec->fdate+14,2);
   strncpy(fdate+12,xxrec->fdate+17,2);
   fdate[14] = 0;
   strcpy(gindex->fdate,fdate);                                                        //  file date, yyyymmddhhmmss

   if (*xxrec->pdate) {
      strncpy(pdate+0,xxrec->pdate+0,4);                                               //  convert  yyyy:mm:dd hh:mm:ss
      strncpy(pdate+4,xxrec->pdate+5,2);                                               //    to  yyyymmddhhmmss
      strncpy(pdate+6,xxrec->pdate+8,2);
      strncpy(pdate+8,xxrec->pdate+11,2);
      strncpy(pdate+10,xxrec->pdate+14,2);
      strncpy(pdate+12,xxrec->pdate+17,2);
      pdate[14] = 0;
      strcpy(gindex->pdate,pdate);                                                     //  photo date, yyyymmddhhmmss
   }
   else *pdate = 0;                                                                    //  no photo date

   if (gindex->mdata1) zfree(gindex->mdata1);
   if (gindex->mdata2) zfree(gindex->mdata2);
   gindex->mdata1 = gindex->mdata2 = 0;
}


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

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

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

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

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

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

   switch (gallerysort) {

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

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

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

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

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

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

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

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

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

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

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

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

      default: return 0;

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


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

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

****/

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

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

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

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

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

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

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

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

   number:

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

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

      continue;
   }
}


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

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

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

   gallerypainted = 0;

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

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

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

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

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

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

   if (Xindexlev < 1) textlines = 1;                                                   //  no index, file name only
   else textlines = 2;                                                                 //  file name + date + wwhh
   if (gallerytype != FOLDER) textlines++;                                             //  add folder name above file name
   texthh = textlines * 1.6 * fontsize + 5;                                            //  vertical space required               26.0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      Fmark = 0;

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

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

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

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

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

      else                                                                             //  image/RAW/VIDEO file
      {
         xxrec = get_xxrec(fspec);                                                     //  get xxrec_tab[] record
         if (! xxrec) continue;                                                        //  deleted, not image file

         fsize2 = atof(xxrec->fsize)/MEGA;                                             //  file size, mb
         strncpy0(pdate,xxrec->pdate,20);                                              //  photo date   yyyy:mm:dd hh:mm:ss
         strncpy0(psize2,xxrec->psize,20);                                             //  pixel size, 12345 6789
         pp = strchr(psize2,' ');
         if (pp && psize2 - pp < (int) strlen(psize2) - 2) *pp = 'x';                  //  12345x6789

         if (gallerysort == FDATE) strncpy0(pdate,xxrec->fdate,20);                    //  use file or photo date

         if (thumbsize <= 256) pdate[10] = 0;                                          //  truncate date/time to date only

         cc = 0;

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

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

         snprintf(text+cc,200-cc,"%s",pdate);                                          //  output date [ time ]
         cc += strlen(pdate);

         if (thumbsize > 180)
            snprintf(text+cc,200-cc,"  %s  %.2fmb",psize2,fsize2);                     //  output pixel size, file size (mb)
         else if (thumbsize > 128)
            snprintf(text+cc,200-cc,"  %s",psize2);                                    //  output only pixel size

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

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

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

endloops:

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


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

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

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

   thumbsize = 360;                                                                    //  25.1

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

   fontsize = appfontsize;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   gallerypainted = 1;
   return 1;
}


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

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

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

   thumbsize = 512;                                                                    //  25.1
   fontsize = appfontsize;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      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 (Xindexlev > 0)                                                          //  if image index is valid
      {
         xxrec = get_xxrec(fspec);                                                     //  get indexed metadata for file
         if (! xxrec) continue;                                                        //  deleted, not image file

         fdate = xxrec->fdate;                                                         //  construct metadata text report
         fsize2 = atof(xxrec->fsize)/MEGA;                                             //  file size, mb
         pdate = xxrec->pdate;
         rating = xxrec->rating;
         keywords = xxrec->keywords;
         title = xxrec->title;
         if (xxrec->desc) strncpy0(desc,xxrec->desc,200);                              //  limit size                            26.0
         else *desc = 0;
         location = xxrec->location;
         country = xxrec->country;

         strncpy0(psize2,xxrec->psize,20);                                             //  pixel size, 12345 6789
         pp = strchr(psize2,' ');
         if (pp && psize2 - pp < (int) strlen(psize2) - 2) *pp = 'x';                  //  12345x6789

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

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

            snprintf(text,1000,                                                        //  report basic metadata in 6 lines
               TX("%s\nPhoto Date: %s File Date: %s \n"
                  "Rating: %s Size: %s %.2fmb \n"
                  "Location: %s %s \n"
                  "Keywords: %s \nTitle: %s \nDescription: %s \n"
                  "----------------------- \n%s"),                                     //  extra indexed metadata in <10 lines
               fspec2, pdate, fdate, rating, psize2, fsize2,
                       location, country, keywords, title, desc, xtext1);
         }

         else
         {
            snprintf(text,1000,                                                        //  report basic metadata in 6 lines
               TX("%s\nPhoto Date: %s File Date: %s \n"
                  "Rating: %s Size: %s %.2fmb \n"
                  "Location: %s %s \n"
                  "Keywords: %s \nTitle: %s \nDescription: %s"),
               fspec2, pdate, fdate, rating, psize2, fsize2,
                       location, country, keywords, title, desc);
         }
      }

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

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

   gallerypainted = 1;
   return 1;
}


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

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

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

   #define NC 1000                                                                     //  cache capacity

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

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

   ndir = nfil = 0;

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

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

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

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

   if (NF) zfree(flist);

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

   ii = 0;
   pp1 = galleryname;

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

   gtk_widget_show_all(Gpanel);
   return;
}


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

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

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

   viewmode('G');

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

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

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

   if (strmatch(menu,"Row Up")) {                                                      //  scroll 1 row up
      scroll1 = scrollposn / thumbH * thumbH;
      scroll2 = scroll1 - thumbH;
      if (scroll2 < 0) scroll2 = 0;
      gallery_scroll(scroll2,2000);
      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,2000);
      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,8000);
      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,8000);
      return;
   }

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

   scrollposn = gtk_adjustment_get_value(Gadjust);
   newscrollposn = scrollposn;

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

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

   gtk_adjustment_set_value(Gadjust,newscrollposn);
   return 1;
}


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

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

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

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

   pp = gallerydir;

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

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


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

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

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

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

   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(popmenu,topfolders[ii],newtop_menu_event,0,0);

   add_popmenu(popmenu,TX("Folder Tree"),newtop_menu_event,0,0);                           //  additional menu entries
   add_popmenu(popmenu,"/",newtop_menu_event,0,0);
   add_popmenu(popmenu,"HOME",newtop_menu_event,0,0);
   add_popmenu(popmenu,TX("Desktop"),newtop_menu_event,0,0);
   add_popmenu(popmenu,TX("Fotocx home"),newtop_menu_event,0,0);
   add_popmenu(popmenu,TX("Saved Areas"),newtop_menu_event,0,0);
   add_popmenu(popmenu,TX("recent files"),newtop_menu_event,0,0);
   add_popmenu(popmenu,TX("newest files"),newtop_menu_event,0,0);

   popup_menu(Mwin,popmenu);
   return;
}


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

   if (! menu) return;

   strncpy0(folder,menu,200);

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

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

   if (strmatch(menu,TX("newest files"))) {
      m_newest_files(0,0);
      return;
   }

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

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

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

   if (strmatch(menu,TX("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
//  mouse event function for gallery window - get selected thumbnail and file
//  user function receives clicked file, which is subject for zfree()

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

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

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

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

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

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

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

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

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

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

      Fmyclick = 1;                                                                    //  button press is mine

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

   return drag_file;
}


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

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

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

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

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

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

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

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

      return;
   }

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

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

      pp1 = strrchr(file+2,'/');                                                       //  isolate filename.ext
      if (! pp1) return;
      cc = strlen(pp1);
      newfile = zstrdup(galleryname,"navi-dropfile",cc+2);                             //  construct galleryname/filename
      strcat(newfile,pp1);
      file_to_xxrec(newfile);                                                          //  update xxrec_tab[] and thumbnail
      add_recent_file(newfile);
      zfree(file);
      zfree(newfile);

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

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

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

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

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

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


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

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

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

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

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

   album_show();
   return;
}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

//  check for menu function KB shortcut

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

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

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


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

//  save and restore gallery sort and top file position

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

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

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

int            NGmem;                                                                  //  current entries <= RGmax


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

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

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

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

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

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

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

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

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

      return;
   }

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

   if (! galleryname) return;

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

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

      return;
   }

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

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

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

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

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

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

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

      return;
   }

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


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

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

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

   if (FGM != 'G') return;

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

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

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

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

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

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

   gtk_window_set_title(MWIN,title);
   return;
}


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

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

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

   Nth = curr_file_posn;

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

      file = filever;
      goto retfile;
   }

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

   retnull:
   file = 0;

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

int   thumbfoldercc = 0;

void image_file_type_init()                                                            //  25.1
{                                                                                      //  initialize from image index function
   if (thumbfolder && *thumbfolder == '/')
   thumbfoldercc = strlen(thumbfolder);
}


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

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

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

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

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

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

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

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

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

   zfree(RP);

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

   *thumbfile = 0;

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

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


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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

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

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

   zadd_locked(NFbusy,+1);                                                             //  "working"

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

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

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

   size = thumbfilesize;

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

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

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

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

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

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

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

returnx:
   if (retstat > 1) printf("*** update_thumbfile() failure: %s \n",imagefile);
   if (imagepxb) PXB_free(imagepxb);
   if (thumbpxb) PXB_free(thumbpxb);
   zadd_locked(NFbusy,-1);
   return retstat;
}


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

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

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

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

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

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


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

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

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

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

   remove(thumbfile);
   return;
}


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

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

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

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

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

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

   ftype = image_file_type(imagefile);

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

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

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

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

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

   return pixbuf;
}


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

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

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

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

   if (! clicked_file) return;

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

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

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


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

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

ch  galsel1_filename[XFCC];

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

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

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

   *galsel1_filename = 0;                                                              //  no selection

   viewmode('G');

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

***/

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

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

   zdialog_free(zd);
   zd_select_files1 = 0;

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

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

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


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

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

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

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

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

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


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

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

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

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


void select_files(int fclear)                                                          //  remove fsetcurr                       25.1
{
   using namespace galselnames;

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

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

***/

   if (zd_select_files) return;                                                        //  already active                        26.0

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

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

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

   zdialog_resize(zd,600,0);                                                           //  start dialog
   zdialog_run(zd,dialog_event,"save");                                                //  keep relative position
   
   txwidget_clear(Ftext);
   GSselect = 0;
   currline = -1;
   imagefile = 0;

   if (fclear) {                                                                       //  clear prior list if requested
      for (ii = 0; ii < SFcount; ii++)
         zfree(SelFiles[ii]);
      SFcount = 0;
   }

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

   GSselect = SFcount;
   SFcount = 0;

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

   zdialog_wait(zd);                                                                   //  wait for dialog completion

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

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

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

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

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

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

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


//  gallery getfiles dialog event function

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

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

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

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

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

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

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

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

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

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

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

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

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

   return 1;
}


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

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

   int      line;
   ch       *ftemp;

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

   return -1;
}


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

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

   int      ftype;
   ch       *RP;

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

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


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

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


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

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

   int      line;

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


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

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

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

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

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

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

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

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

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

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

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

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

   return;
}


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

int galselnames::CBfunc(GtkWidget *widget, int line, int posn, ch *input)
{
   if (*input == GDK_KEY_F1) {                                                         //  key F1 pressed, show help
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   if (! GSselect) return 0;                                                           //  no lines

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

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

   showthumb();
   return 1;
}


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

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

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

   static draw_context_t   draw_context;
   cairo_t                 *cr;

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

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

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

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

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

   draw_context_destroy(draw_context);
   return;
}


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

//  gallery menu, resizable thumbnail view

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


//  gallery menu, thumbnail with basic metadata view

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


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

//  Show recently viewed image files.

void m_recentfiles(GtkWidget *, ch *menu)
{
   F1_help_topic = TX("RECENT FILES");

   printf("m_recentfiles \n");

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


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

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

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

   pp = strrchr(newfile,'/');                                                          //  update recent folders list
   if (pp) *pp = 0;
   add_recent_folder(newfile);
   if (pp) *pp = '/';

   return;
}


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

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

namespace newest_files_names
{
   struct newfile_t  {                                                                 //  new file record
      ch          *file;                                                               //  image file
      ch          fdate[20];                                                           //  date-time, yyyy:mm:dd hh:mm:ss
   };
}


//  menu function

void m_newest_files(GtkWidget *, ch *menu)
{
   using namespace newest_files_names;

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

   ch   *mess = TX("Use metadata photo date or file modification date?");

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

   F1_help_topic = TX("NEWEST FILES");

   printf("m_newest_files \n");

   if (Xindexlev < 2 || Findexnew > 0) {                                               //  check if full index needed            25.1
      index_rebuild(2,0);                                                              //  full index, get new/mod files
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

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

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

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

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

      jj++;                                                                            //  selected files
   }

   Nxrec = jj;                                                                         //  final count

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

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

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

   fclose(fid);

cleanup:

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

   return;
}


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

using namespace newest_files_names;

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


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

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

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

   ch     *resetmess = TX("Reset all galleries to file name ascending");

   F1_help_topic = TX("GALLERY SORT");

   printf("m_gallery_sort \n");

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

***/

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

   zdialog_add_ttip(zd,"metadata",TX("metadata from Search Function, metadata report"));
   zdialog_add_ttip(zd,"filenumber",TX("embedded numbers will sort by value"));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   zdialog_free(zd);

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

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

   return;
}


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

//  filter the current gallery for matching files
//  output matching files to album "gallery_filter"

namespace gallery_filter_names
{
   int      Fall, Forg, Fver, Flast, Frating, Fkeywords, Ftext;
   int      lorat, hirat;
   ch       keywords[100], text[100];
}


//  menu function

void m_gallery_filter(GtkWidget *, ch *menu)
{
   using namespace gallery_filter_names;

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

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

   F1_help_topic = TX("GALLERY FILTER");

   printf("m_gallery_filter \n");

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

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

***/

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

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

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

   zdialog_add_widget(zd,"hbox","hbkeywords","dialog");
   zdialog_add_widget(zd,"check","ckkeywords","hbkeywords",TX("keyword names"));
   zdialog_add_widget(zd,"zentry","keywords","hbkeywords",0,"space=5|expand");

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

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

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

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

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

   zdialog_free(zd);

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

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

   Nfiles = 0;

   for (ii = 0; ii < Gfiles; ii++)                                                     //  loop files in current gallery
   {
      file = gallery(0,"getR",ii);
      if (! file) break;
      ftype = image_file_type(file);                                                   //  must be image type file
      if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) continue;

      Fkeep = 0;

      if (Fall) Fkeep = 1;                                                             //  keep all files

      if (! Fkeep && Forg) {
         pp1 = strrchr(file,'/');                                                      //  /.../filename.vNN.jpg
         if (pp1) pp1 = strrchr(pp1,'.');                                              //                   |
         if (pp1 && ! strmatchN(pp1-4,".v",2)) Fkeep = 1;                              //                   pp1
      }

      if (! Fkeep && Fver) {
         pp1 = strrchr(file,'/');                                                      //  /.../filename.vNN.jpg
         if (pp1) pp1 = strrchr(pp1,'.');                                              //                   |
         if (pp1 && strmatchN(pp1-4,".v",2)) Fkeep = 1;                                //                   pp1
      }

      if (! Fkeep && Flast) {
         pp1 = file_newest_version(file);
         if (strmatch(pp1,file)) Fkeep = 1;
      }

      if (! Fkeep) continue;

      if (Frating) {
         xxrec = get_xxrec(file);                                                      //  get image rating
         if (! xxrec) continue;                                                        //  deleted, not image file
         rating = xxrec->rating[0] - '0';                                              //  numeric 0-5
         if (rating < lorat || rating > hirat) continue;                               //  test within limits
      }

      if (Fkeywords) {
         xxrec = get_xxrec(file);                                                      //  get image keywords
         if (! xxrec) continue;                                                        //  deleted, not image file

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

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

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

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

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

      nomatch: continue;
   }


   fclose(fid);

   if (Nfiles) {
      zstrcopy(curr_album,albumfile,"albums");                                         //  make current album
      panelmessage(1,3,TX("new album created"));                                           //  25.1
      album_show();
   }
   else {
      zmessageACK(Mwin,TX("no matching files found"));
      remove(albumfile);
      return;
   }

   return;
}


//  dialog event and completion function

int gallery_filter_dialog_event(zdialog *zd, ch *event)
{
   using namespace gallery_filter_names;

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

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

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

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

   if (Forg && Fver) {                                                                 //  if org and ver, clear both
      Fall = 1;                                                                        //    and set all
      Forg = Fver = 0;
   }

   if (! Forg && ! Fver && ! Flast) Fall = 1;                                          //  default

   zdialog_stuff(zd,"ckall",Fall);                                                     //  update dialog
   zdialog_stuff(zd,"ckorg",Forg);
   zdialog_stuff(zd,"ckver",Fver);
   zdialog_stuff(zd,"cklast",Flast);
   zdialog_fetch(zd,"ckrat",Frating);
   zdialog_fetch(zd,"ckkeywords",Fkeywords);
   zdialog_fetch(zd,"cktext",Ftext);
   zdialog_fetch(zd,"lorat",lorat);
   zdialog_fetch(zd,"hirat",hirat);
   zdialog_fetch(zd,"keywords",keywords,100);
   zdialog_fetch(zd,"text",text,100);

   return 1;
}


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

//  set the gallery from the current image file folder

void m_current_folder(GtkWidget*, ch *menu)
{
   F1_help_topic = TX("CURRENT FOLDER");
   printf("m_current_folder \n");

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

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


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

//  set the gallery from a recently used file folder

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

   int         nf = 0;
   ch          *folder;

   F1_help_topic = TX("RECENT FOLDERS");
   printf("m_recent_folders \n");

   if (! ZLrecent_folders) ZLrecent_folders = zlist_from_file(recent_folders_file);
   if (ZLrecent_folders) nf = zlist_count(ZLrecent_folders);
   if (! nf) {
      zmessageACK(Mwin,TX("no saved folders"));
      return;
   }

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

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

   return;
}


//  add a new folder to the list of recent folders, first position
//  execution time is under 1 milllisecond

void add_recent_folder(ch *foldername)
{
   int      ii;

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

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

   return;
}


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

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

namespace folder_tree
{
   #define  maxdirs 10000

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

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

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

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


//  menu function

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

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

   F1_help_topic = TX("FOLDER TREE");

   printf("m_folder_tree \n");

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

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

***/

   if (dlist) goto report;                                                             //  already done

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

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

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

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

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

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

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

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

report:

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

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

   return;
}


//  sort compare function

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

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


//  folder list mouse function

int folder_tree::CBfunc(GtkWidget *txwidget, int line, int pos, ch *input)
{
   using namespace folder_tree;

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

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

   if (line < 0)                                                                       //  KB key
   {
      line = currline;
      if (strmatch(input,"up")) line--;                                                //  KB arrow key navigation
      if (strmatch(input,"down")) line++;
      if (line < 0) line = 0;
      if (line > Nlines-1) line = Nlines - 1;
      zmainsleep(0.1);                                                                 //  restrain to 10/sec.
   }

   pline = txwidget_line(txwidget,line,1);                                             //  txwidget line: ... [x] dirname
   if (! pline || ! *pline) goto returnx;
   txwidget_highlight_line(txwidget,line);
   txwidget_scroll(txwidget,line);
   currline = line;

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

   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 txwidget
            txwidget_highlight_line(txwidget,line);
            txwidget_scroll(txwidget,line);
            goto returnx;
         }
      }
   }

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

returnx:
   Fbusy = 0;
   if (input && strstr("up down",input)) return 1;                                     //  input processed by me
   else return 0;                                                                      //  not
}


//  write all visible folders to text window

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

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

   memset(indent,' ',100);

   popup_report_clear(zdpop);

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

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

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

   Nlines = line;
   return;
}


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

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

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

   F1_help_topic = TX("SELECT IMAGE FILES");

   printf("m_select_files \n");

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

   select_files(0);

   if (SFcount == 0) {
      remove(albumfile);                                                               //  nothing selected
      return;
   }

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

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

   fclose(fid);
   return;
}


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

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

void bookmarks_load();
void bookmarks_refresh();


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

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

   int  bookmarks_dialog_event(zdialog *zd, ch *event);
   int bookmarks_CBfunc(GtkWidget *, int line, int pos, ch *input);

   zdialog     *zd;

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

   F1_help_topic = TX("BOOKMARKS");

   printf("m_bookmarks \n");

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

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

   txwidget = zdialog_gtkwidget(zd,"bmklist");                                         //  set mouse/KB event function
   txwidget_set_eventfunc(txwidget,bookmarks_CBfunc);

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

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


//  dialog event and completion function

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

   int      zstat;

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


//  mouse click function to receive clicked bookmarks

int bookmarks_CBfunc(GtkWidget *, int line, int pos, ch *input)
{
   using namespace bookmarknames;

   ch     *file;

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

   if (! zd_bookmark) return 1;

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

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

   return 1;
}


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

//  edit bookmarks

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

   int edit_bookmarks_dialog_event(zdialog *zd, ch *event);
   int edit_bookmarks_CBfunc(GtkWidget *, int line, int pos, ch *input);

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

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

***/

   F1_help_topic = TX("BOOKMARKS");

   printf("m_edit_bookmarks \n");

   if (zd_edit_bookmarks) return;                                                      //  already busy

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

   txwidget = zdialog_gtkwidget(zd,"bmklist");                                         //  set mouse/KB event function
   txwidget_set_eventfunc(txwidget,edit_bookmarks_CBfunc);

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

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

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


//  load bookmarks list from bookmarks file

void bookmarks_load()
{
   using namespace bookmarknames;

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

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

   bmkposn = Nbmks;                                                                    //  next free position
   return;
}


//  mouse click function to select existing bookmark from list

int edit_bookmarks_CBfunc(GtkWidget *, int line, int pos, ch *input)
{
   using namespace bookmarknames;

   ch     bookmarkname[32];

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

   if (! zd_edit_bookmarks) return 1;
   if (Nbmks < 1) return 1;
   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 1;
}


//  mouse click function to receive clicked thumbnails for new/revised bookmarks

void edit_bookmarks_Lclick_func(int Nth)
{
   using namespace bookmarknames;

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

   if (! zd_edit_bookmarks) return;
   if (Nth < 0) return;                                                                //  gallery gone ?
   imagefile = gallery(0,"getR",Nth);                                                  //  get file at clicked position
   if (! imagefile) return;

   pp = strrchr(imagefile,'/');                                                        //  get file name or last subfolder name
   if (! pp) return;                                                                   //    to use as default bookmark name
   strncpy0(bookmarkname,pp+1,31);                                                     //  max. 30 chars. + null

   cc = strlen(imagefile) + 34;                                                        //  construct bookmark record:
   newbookmark = (ch *) zmalloc(cc,"bookmarks");                                       //    filename  /folders.../filename
   snprintf(newbookmark,cc,"%-30s  %s",bookmarkname,imagefile);

   if (Nbmks == maxbmks) {                                                             //  if list full, remove first
      zfree(bookmarks[0]);
      Nbmks--;
      for (int ii = 0; ii < Nbmks; ii++)
         bookmarks[ii] = bookmarks[ii+1];
   }

   if (Nbmks == 0) bmkposn = 0;                                                        //  1st bookmark --> 0
   else bmkposn++;                                                                     //  else clicked position + 1
   if (bmkposn < 0) bmkposn = 0;
   if (bmkposn > Nbmks) bmkposn = Nbmks;

   for (int ii = Nbmks; ii > bmkposn; ii--)                                            //  make hole to insert new bookmark
      bookmarks[ii] = bookmarks[ii-1];

   bookmarks[bmkposn] = newbookmark;                                                   //  insert
   Nbmks++;

   bookmarks_refresh();                                                                //  update bookmarks list in dialog

   zdialog_stuff(zd_edit_bookmarks,"bmkname",bookmarkname);

   return;
}


//  dialog event and completion function

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

   ch          bookmarkname[32];
   FILE        *fid;
   int         cc;

   if (strmatch(event,"delete"))                                                       //  delete bookmark at position
   {
      if (bmkposn < 0 || bmkposn > Nbmks-1) return 1;
      for (int ii = bmkposn; ii < Nbmks-1; ii++)
         bookmarks[ii] = bookmarks[ii+1];
      Nbmks--;
      zdialog_stuff(zd,"bmkname","");                                                  //  clear name field
      bookmarks_refresh();                                                             //  update bookmarks list in dialog
   }

   if (strmatch(event,"rename"))                                                       //  apply new name to bookmark
   {
      if (bmkposn < 0 || bmkposn > Nbmks-1) return 1;
      zdialog_fetch(zd,"bmkname",bookmarkname,31);                                     //  get name from dialog
      cc = strlen(bookmarkname);
      if (cc < 30) memset(bookmarkname+cc,' ',30-cc);                                  //  blank pad to 30 chars.
      bookmarkname[30] = 0;
      memcpy(bookmarks[bmkposn],bookmarkname,30);                                      //  replace name in bookmarks list
      bookmarks_refresh();                                                             //  update bookmarks list in dialog
   }

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

   if (zd->zstat == 1)                                                                 //  done
   {
      fid = fopen(bookmarks_file,"w");                                                 //  write bookmarks file
      if (! fid)
         zmessageACK(Mwin,TX("unable to save bookmarks file"));
      else {
         for (int ii = 0; ii < Nbmks; ii++)
            fprintf(fid,"%s\n",bookmarks[ii]);
         fclose(fid);
      }
   }

   for (int ii = 0; ii < Nbmks; ii++)                                                  //  free memory
      zfree(bookmarks[ii]);

   zdialog_free(zd);
   zd_edit_bookmarks = 0;

   return 1;
}


//  private function to update dialog widget with new bookmarks list

void bookmarks_refresh()
{
   using namespace bookmarknames;

   ch       bookmarkline[XFCC+32];
   ch       blanks[33] = "                                ";
   int      cc;

   if (! zd_edit_bookmarks && ! zd_bookmark) return;
   txwidget_clear(txwidget);                                                           //  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);
      txwidget_append(txwidget,0,"%s\n",bookmarkline);
   }
   return;
}


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

//  set the video frame for a video thumbnail
//  (video file popup menu)

void m_thumbframe(GtkWidget *, ch *)
{
   zdialog  *zd;
   STATB    statB;
   int      ftype, zstat, err;
   int      minutes, seconds;
   ch       *pp, framefile[200];
   ch       *vf, *tf;
   ch       *videofile = 0, thumbfile[XFCC];
   PXB      *framepxb = 0, *thumbpxb = 0;
   timeval  thumbtimes[2];

   ch          *tip = TX("Play video and stop at desired frame \n"
                         "Note playback time in minutes and seconds");

   F1_help_topic = TX("VIDEO FILES");

   printf("m_thumbframe \n");

   if (clicked_file) {                                                                 //  use clicked file if present
      videofile = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                                 //  else current file
      videofile = zstrdup(curr_file,"thumbframe");
   else return;

   ftype = image_file_type(videofile);
   if (ftype != VIDEO) goto cleanup;

/***
       ____________________________________________
      |         Set Video Thumbnail Frame          |
      |                                            |
      | Play video and stop at desired frame.      |
      | Playback time: minutes [__] seconds [__]   |
      |                                            |
      |                                   [OK] [X] |
      |____________________________________________|

***/

   zd = zdialog_new(TX("Set Video Thumbnail Frame"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"label","labtip","dialog",tip);
   zdialog_add_widget(zd,"hbox","hbtime","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtime","hbtime",TX("Playback time:"),"space=3");
   zdialog_add_widget(zd,"label","space","hbtime",0,"space=3");
   zdialog_add_widget(zd,"label","labmin","hbtime",TX("minutes"),"space=3");
   zdialog_add_widget(zd,"zspin","minutes","hbtime","0|999|1|0","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbtime",0,"space=3");
   zdialog_add_widget(zd,"label","labsec","hbtime",TX("seconds"),"space=3");
   zdialog_add_widget(zd,"zspin","seconds","hbtime","0|59|1|0","space=3|size=3");

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

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

   zdialog_fetch(zd,"minutes",minutes);                                                //  get playback time inputs
   zdialog_fetch(zd,"seconds",seconds);
   seconds = 60 * minutes + seconds;
   zdialog_free(zd);

   snprintf(framefile,200,"%s/videoframe.jpg",temp_folder);                            //  get frame file from ffmpeg

   vf = zescape_quotes(videofile);
   err = zshell("ack","ffmpeg -ss %d -i \"%s\" -v 8 -frames 1 -y \"%s\" ",
                                                   seconds, vf, framefile);
   zfree(vf);

   if (err) goto cleanup;

   if (! regfile(framefile)) {
      zmessageACK(Mwin,TX("cannot get video frame: %s"),strerror(errno));
      goto cleanup;
   }

   err = image2thumbfile2(videofile,thumbfile);                                        //  get thumbnail file for video file
   if (err) {
      zmessageACK(Mwin,TX("cannot create thumbnail file"));
      goto cleanup;
   }

   pp = strrchr(thumbfile+1,'/');
   *pp = 0;
   if (! dirfile(thumbfile)) {                                                         //  create thumbnail folder if needed
      tf = zescape_quotes(thumbfile);
      err = zshell("ack","mkdir -p -m 0750 \"%s\"",tf);
      zfree(tf);
      if (err) goto cleanup;
   }
   *pp = '/';

   framepxb = PXB_load(framefile,1);                                                   //  video frame file >> PXB image
   if (! framepxb) goto cleanup;

   thumbpxb = PXB_rescale(framepxb,thumbfilesize);                                     //  rescale PXB to thumbnail size
   if (! thumbpxb) goto cleanup;

   err = PXB_JPG_save(thumbpxb,thumbfile,80);                                          //  save to JPEG thumbnail file
   if (err) goto cleanup;

   if (! regfile(videofile,&statB)) goto cleanup;                                      //  get video file mod time

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

   gallery(0,"paint",-1);                                                              //  repaint gallery

cleanup:
   remove(framefile);
   if (videofile) zfree(videofile);
   if (framepxb) PXB_free(framepxb);
   if (thumbpxb) PXB_free(thumbpxb);
   return;
}


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

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

void m_show_hidden(GtkWidget *, ch *)
{
   printf("m_show_hidden \n");
   KBaction("Show Hidden");
   return;
}


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

//  Show an image file in a popup window at mouse position.
//  Re-use most recent window or create a new one if Fnewin != 0.
//  Returns 0 if OK, +N otherwise.

namespace popup_image_names
{
   GtkWidget  *window[10], *drawarea[10];                                              //  up to 10 popup windows open
   ch         *filex[10], reqfull[10], isfull[10];
   float      winc = 1.26, wdec = 0.80;                                                //  window upsize/downsize ratios
   int        Nw = 0;
}


int popup_image(ch *file, GtkWindow *parent, int Fnewin, int size)
{
   using namespace popup_image_names;

   static int  ftf = 1;
   ch         *pp;
   ch         *tipmess(TX("zoom via mouse wheel or Keys +/=/-/↑/↓"));

   int popup_image_draw(GtkWidget *, cairo_t *, int &Nw);
   int popup_image_scroll(GtkWidget *, GdkEvent *event, int &Nw);
   int popup_image_KBevent(GtkWidget *, GdkEventKey *event, int &Nw);
   int popup_image_mousebutt(GtkWidget *, GdkEvent *event, int &Nw);

   if (! main_thread()) zappcrash("popup_image() called from thread");

   if (ftf) {
      ftf = 0;
      poptext_mouse(tipmess,0,0,0,8);
   }

   if (Fnewin)
      if (++Nw == 10) Nw = 0;                                                          //  new window, re-use oldest up to 10
   if (! Fnewin)
      while (Nw > 0 && window[Nw] == 0) Nw--;                                          //  else re-use latest still active

   if (window[Nw]) {
      gtk_widget_destroy(drawarea[Nw]);
      drawarea[Nw] = 0;
      zfree(filex[Nw]);
      filex[Nw] = 0;
   }
   else {
      window[Nw] = gtk_window_new(GTK_WINDOW_TOPLEVEL);                                //  create new popup window
      if (! window[Nw]) return 1;
      if (! size) size = 512;
      gtk_window_set_default_size(GTK_WINDOW(window[Nw]),size,size);
      pp = strrchr(file,'/');                                                          //  window title = file name
      gtk_window_set_title(GTK_WINDOW(window[Nw]),pp+1);

      if (parent) {
         gtk_window_set_transient_for(GTK_WINDOW(window[Nw]),parent);
         gtk_window_set_destroy_with_parent(GTK_WINDOW(window[Nw]),1);
      }
      gtk_window_set_position(GTK_WINDOW(window[Nw]),GTK_WIN_POS_MOUSE);
   }

   filex[Nw] = zstrdup(file,"popup_image");
   drawarea[Nw] = gtk_drawing_area_new();                                              //  new drawing area always required
   if (! drawarea[Nw]) return 2;
   gtk_container_add(GTK_CONTAINER(window[Nw]),drawarea[Nw]);
   reqfull[Nw] = isfull[Nw] = 0;                                                       //  not fullscreen

   gtk_widget_add_events(window[Nw],GDK_SCROLL_MASK);
   gtk_widget_add_events(window[Nw],GDK_KEY_PRESS_MASK);
   gtk_widget_add_events(window[Nw],GDK_BUTTON_RELEASE_MASK);

   G_SIGNAL(window[Nw],"destroy",gtk_widget_destroyed,&window[Nw]);                    //  set window = null if destroyed
   G_SIGNAL(window[Nw],"draw",popup_image_draw,&Nval[Nw]);
   G_SIGNAL(window[Nw],"scroll-event",popup_image_scroll,&Nval[Nw]);                   //  connect events
   G_SIGNAL(window[Nw],"key-press-event",popup_image_KBevent,&Nval[Nw]);

   gtk_widget_show_all(window[Nw]);

   return 0;
}


//  rescale image and repaint window to fit

int popup_image_draw(GtkWidget *window, cairo_t *cr, int &nn)
{
   using namespace popup_image_names;

   PXB         *pxb1, *pxb2;
   int         ww1, hh1, ww2, hh2;
   double      area;
   ch          *file;

   file = filex[nn];
   if (! file) return 1;

   pxb1 = PXB_load(file,1);
   if (! pxb1) return 1;

   ww1 = pxb1->ww;                                                                     //  image dimensions
   hh1 = pxb1->hh;

   gtk_window_get_size(GTK_WINDOW(window),&ww2,&hh2);                                  //  current window dimensions

   area = ww2 * hh2;
   ww2 = sqrt(area * ww1 / hh1);                                                       //  fit window to image, keeping same area
   hh2 = area / ww2;

   pxb2 = PXB_rescale(pxb1,ww2,hh2);                                                   //  rescale image to window

   gtk_window_resize(GTK_WINDOW(window),ww2,hh2);

   gdk_cairo_set_source_pixbuf(cr,pxb2->pixbuf,0,0);                                   //  draw image
   cairo_paint(cr);

   PXB_free(pxb1);
   PXB_free(pxb2);

   return 1;
}


//  respond to mouse scroll button and zoom window larger or smaller

int popup_image_scroll(GtkWidget *window, GdkEvent *event, int &nn)
{
   using namespace popup_image_names;

   int         scroll, ww, hh;
   double      ff = 1.0;

   if (event->type == GDK_SCROLL) {                                                    //  mouse wheel event
      scroll = ((GdkEventScroll *) event)->direction;
      if (scroll == GDK_SCROLL_UP) ff = winc;
      if (scroll == GDK_SCROLL_DOWN) ff = wdec;
   }

   gtk_window_get_size(GTK_WINDOW(window),&ww,&hh);                                    //  current window dimensions
   ww *= ff;                                                                           //  new dimensions
   hh *= ff;

   if (ww > monitor_ww || hh > monitor_hh) {                                           //  request > screen size, fullscreen
      reqfull[nn] = 1;
      gtk_window_fullscreen(GTK_WINDOW(window));
      return 1;
   }

   reqfull[nn] = 0;
   gtk_window_unfullscreen(GTK_WINDOW(window));

   if (ww + hh > 512)
      gtk_window_resize(GTK_WINDOW(window),ww,hh);                                     //  rescale up or down
   else
      gtk_widget_destroy(window);                                                      //  if very small, delete window

   return 1;
}


//  respond to KB events F11 (fullscreen/unfullscreen)
//                       Escape (destroy)
//                       +/= or up-arrow  make image larger
//                       - or down-arrow  make image smaller

int popup_image_KBevent(GtkWidget *window, GdkEventKey *event, int &nn)
{
   using namespace popup_image_names;

   int      KBkey = event->keyval;
   int      ww, hh;
   double   ff = 0;

   if (KBkey == GDK_KEY_Escape) gtk_widget_destroy(window);

   if (KBkey == GDK_KEY_F11)
   {
      if (reqfull[nn]) {
         reqfull[nn] = 0;
         gtk_window_unfullscreen(GTK_WINDOW(window));
      }
      else {
         reqfull[nn] = 1;
         gtk_window_fullscreen(GTK_WINDOW(window));
      }
   }

   if (KBkey == GDK_KEY_plus || KBkey == GDK_KEY_KP_Add || KBkey == GDK_KEY_equal || KBkey == GDK_KEY_Up)
      ff = winc;

   if (KBkey == GDK_KEY_minus || KBkey == GDK_KEY_KP_Subtract || KBkey == GDK_KEY_Down)
      ff = wdec;

   if (ff)
   {
      gtk_window_get_size(GTK_WINDOW(window),&ww,&hh);                                 //  current window dimensions
      ww *= ff;                                                                        //  new dimensions
      hh *= ff;

      if (ww > monitor_ww || hh > monitor_hh) {                                        //  request > screen size, fullscreen
         reqfull[nn] = 1;
         gtk_window_fullscreen(GTK_WINDOW(window));
         return 1;
      }

      reqfull[nn] = 0;
      gtk_window_unfullscreen(GTK_WINDOW(window));

      if (ww + hh > 512)
         gtk_window_resize(GTK_WINDOW(window),ww,hh);                                  //  rescale up or down
      else
         gtk_widget_destroy(window);                                                   //  if very small, delete window
   }

   return 1;
}


