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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

   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.

   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.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see http://www.gnu.org/licenses/.

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

   Fotoxx image edit - directory navigation and thumbnail gallery functions

   gallery                    create/update/search/paint an image gallery
   set_gwin_title             update the gallery window title bar
   gallery_getnext            get next file from current gallery position
   gallery_getlastver         get the last version of a file in a gallery
   gallery_position           get a file position within a gallery
   image_file_type            determine file type (directory, image, RAW, thumbnail, other)
   thumb2imagefile            get corresponding image file for given thumbnail file
   image2thumbfile            get corresponding thumbnail file for given image file
   image_thumbfile            retrieve or create the thumbnail file for given image file
   image_thumbnail            get thumbnail pixbuf for an image file from cache or disk
   get_thumbnail_pixbuf       make thumbnail pixbuf from an image file
   popimage                   popup a larger image from a clicked thumbnail
   gallery_monitor            monitor directory for file changes and update gallery
   gallery_getfiles           gallery file selection function and dialog
   m_edit_bookmarks           edit bookmarks
   m_goto_bookmark            jump to a gallery/bookmark position

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

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

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

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

   #define     thumbxx 6                                                         //  thumbx array size
   int         thumbx[6] = { 512, 360, 256, 180, 128, 90 };                      //  thumbnail sizes

   typedef struct {                                                              //  current gallery list in memory
      char     *file;                                                            //  /directory.../filename
      char     fdate[16];                                                        //  file date: yyyymmddhhmmss
      char     pdate[16];                                                        //  photo date: yyyy:mm:dd
      char     size[16];                                                         //  image size: 2345x1234
   }  glist_t;

   glist_t     *glist;

   GTYPE       gallerytype = NONE;                                               //  gallery type
   char        *galleryname = 0;                                                 //  directory or file list name
   GtkWidget   *gallerybutt[60];                                                 //  gallery navi buttons [aaa] [bbb] ...
   char        *gallerypath[60];                                                 //  corresp. directory names  aaa bbb ...
   GtkWidget   *gallerylabel = 0;                                                //  gallery label (album name ...)
   int         galleryseq = 1;                                                   //  1/2 = ascending/descending
   int         gallerypainted = 0;                                               //  gallery is finished painting
   int         Nfiles = 0;                                                       //  gallery file count (incl. subdirks)
   int         Ndirs = 0;                                                        //  gallery subdirectory count         16.02
   int         Nimages = 0;                                                      //  gallery image file count
   char        **mdlist = 0;                                                     //  corresp. metadata list
   int         mdrows = 0;                                                       //  text rows in metadata list
   int         xwinW = 1000, xwinH = 700;                                        //  gallery window initial size
   int         xwinX, xwinY;                                                     //  gallery window initial position
   int         thumbsize = 128;                                                  //  initial thumbnail image size
   int         thumbW, thumbH;                                                   //  gallery window thumbnail cell size
   int         fontsize = 10;                                                    //  font size for text in gallery window
   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         scrollp = 0;                                                      //  scroll position
   int         maxscroll;                                                        //  max. scroll position
   int         topfileposn = 0;                                                  //  scroll-to file position (Nth)
   char        *drag_file = 0;                                                   //  selected thumbnail (file)          16.08
   char        *drag_gallery = 0;                                                //  drag from gallery                  16.08
   int         drag_posn = -1;                                                   //  drag from position                 16.08
   int         gallery_scrollgoal = -1;                                          //  gallery scroll goal position
   int         gallery_scrollspeed = 0;                                          //  gallery scroll speed pixels/second
   double      thumbcache_MB = 0;                                                //  total thumbnail cache size in MB
   double      thumbcache_max = 1000;                                            //  cache limit: 1 GB                  15.08

   //  private functions
   char * navigate(cchar *filez, cchar *action, int Nth = 0);                    //  image file list setup and navigate
   int    gallery_comp(cchar *rec1, cchar *rec2);                                //  gallery record compare for sort options
   int    gallery_paint(GtkWidget *, cairo_t *);                                 //  gallery window paint function
   int    gallery_paintmeta(GtkWidget *Gdrawin, cairo_t *cr);                    //  same, for metadata report
   void   gallery_navibutts();                                                   //  create navigation buttons in top panel
   void   dir_filecount(char *dirname, int &ndir, int &nfil);                    //  get directory subdir and image file counts
   void   draw_text(cairo_t *cr, char *text, int x, int y, int ww);              //  draw text in gallery window
   void   menufuncx(GtkWidget *win, cchar *menu);                                //  function for gallery window buttons
   void   gallery_scroll(int position, int speed);                               //  start gallery slow scroll to position
   int    gallery_scrollfunc(void *);                                            //  gallery scroll timer function
   void   navibutt_clicked(GtkWidget *, int *level);                             //  set gallery via click on navigation button
   void   newtop(GtkWidget *widget, GdkEventButton *event);                      //  function for top image directory change
   int    newtop_dialog_event(zdialog *zd, cchar *event);                        //   "" dialog event function
   void   newalbum(GtkWidget *widget, GdkEventButton *event);                    //  function for top image directory change
   void   getalbum();                                                            //  function to get gallery from an album
   int    getalbum_dialog_event(zdialog *zd, cchar *event);                      //   "" dialog event function
   void   gallery_sort();                                                        //  choose gallery sort order
   int    gallery_sort_dialog_event(zdialog *zd, cchar *event);                  //   "" dialog event function
   void   mouse_event(GtkWidget *widget, GdkEvent *event, void *);               //  gallery window mouse event function
   char * gallery_dragfile();                                                    //  start drag-drop, set the file      16.08
   void   gallery_dropfile(int mousex, int mousey, char *file);                  //  accept drag-drop file at position  16.08
   int    KBrelease(GtkWidget *, GdkEventKey *, void *);                         //  gallery window key release event
}

using namespace zfuncs;
using namespace navi;


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

   char * gallery(cchar *filez, cchar *action, int Nth)

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

   Make a scrolling window of thumbnails for a 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 directory, or file with list of image files

   action:  init:     filez = initial file or directory
            initF:    filez = file with list of image files to use
            sort:     sort the file list, directories first, ignore case
            insert:   insert filez into file list at position Nth (0 to last+1)
            delete:   delete Nth file in list
            find:     return Nth file (0 base) or null if Nth > last
            get1st:   return 1st image file or null if none
            paint:    paint gallery window, filez in top row, Nth position if match
                      (if filez null and Nth < 0, no change in rop row)

   Nth: file position in gallery (action = insert/delete/find/paint) (0 base)
   (Nth is optional argument, default = 0)

   thumbnail click functions:
      gallery_Lclick_func()                     default function (open file)
      gallery_Rclick_popup()                    default function (popup menu)
      gallery_getfiles_Lclick_func()            gallery_getfiles active
      gallery_getfiles_Rclick_func()            gallery_getfiles active
      bookmarks_Lclick_func                     edit bookmarks active
      passes Nth of clicked thumbnail (0 to last)
      passes -1 if gallery window is closed
      passes -2 if key F1 is pressed (for context help)

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

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

char * gallery(cchar *filez, cchar *action, int Nth)                             //  overhauled
{
   int      nth = Nth;

   zthreadcrash();                                                               //  thread usage not allowed
   
   if (strstr("init initF",action)) {                                            //  generate new gallery file list
      gallerypainted = 0;                                                        //    from directory/file
      navigate(filez,action);
      return 0;
   }

   if (strstr("sort insert delete",action)) {                                    //  revise gallery file list
      navigate(filez,action,Nth);
      gallerypainted = 0;
      return 0;
   }

   if (strmatch(action,"find"))                                                  //  find Nth file in file list
      return navigate(0,action,Nth);

   if (strmatch(action,"get1st"))                                                //  get 1st image file in file list
      return navigate(0,action);

   if (strmatch(action,"paint")) {                                               //  paint gallery window
      gallerypainted = 0;
      if (filez) nth = gallery_position(filez,Nth);                              //  use Nth if valid, else find Nth
      topfileposn = nth;                                                         //  filez or -1 or caller Nth
      gtk_widget_queue_draw(Gdrawin);                                            //  draw gallery window
      return 0;
   }

   zappcrash("gallery() action: %s",action);                                     //  bad call
   return 0;
}


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

   private function - manage list of image files within a directory

   char * navi::navigate(cchar *filez, cchar *action, int Nth)

   action:  init:    file list = directories and image files in directory filez
            initF:   file list = filez = list of image files to use (cchar **)
            sort:    sort the file list, directories first, ignore case
            insert:  insert filez into file list at position Nth (0 to last+1)
            delete:  delete Nth file in list
            find:    returns Nth file or null if Nth > last
            get1st:  return 1st image file or null if none

   Nth: file to find/insert/delete (0 base)
   (used for initF galleries: album or search results)

   Returned values:
      find: filespec, else null
      The returned file belongs to caller and is subject for zfree().

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

char * navi::navigate(cchar *filez, cchar *action, int Nth)
{
   char        *file, *findname;
   cchar       *findcommand = "find -L \"%s\" -maxdepth 1";
   char        *pp, *file2, fdate[16], pdate[16], size[16];
   int         err, ii, cc, contx = 0, fposn;
   FTYPE       ftype;
   FILE        *fid;
   STATB       statbuf;

   if (! strstr("init initF sort insert delete find get1st",action))
         zappcrash("navigate %s",action);

   if (strmatchN(action,"init",4))                                               //  init or initF
   {
      if (! filez && gallerytype == GDIR) filez = galleryname;                   //  refresh gallery                    15.03
      if (! filez) return 0;

      if (glist) {
         for (ii = 0; ii < Nfiles; ii++)                                         //  free prior gallery list
            zfree(glist[ii].file);
         zfree(glist);
         glist = 0;
      }

      if (mdlist) {                                                              //  clear prior metadata if any
         for (ii = 0; ii < Nfiles; ii++)
            if (mdlist[ii]) zfree(mdlist[ii]);
         zfree(mdlist);
         mdlist = 0;
      }

      cc = maximages * sizeof(glist_t);
      glist = (glist_t *) zmalloc(cc);                                           //  gallery file list

      Nfiles = Ndirs = Nimages = 0;                                              //  no files
      fposn = 0;

      if (galleryname && filez != galleryname)                                   //  don't destroy input
         zfree(galleryname);
      galleryname = zstrdup(filez);
   }

   if (strmatch(action,"init"))                                                  //  initialize from given directory
   {
      gallerytype = GDIR;                                                        //  gallery type = directory

      err = stat(galleryname,&statbuf);
      if (err) {
         pp = (char *) strrchr(galleryname,'/');                                 //  bad file, check directory part
         if (! pp) return 0;
         pp[1] = 0;
         err = stat(galleryname,&statbuf);
         if (err) return 0;                                                      //  give up, empty file list
      }

      if (S_ISREG(statbuf.st_mode)) {                                            //  if a file, get directory part
         pp = (char *) strrchr(galleryname,'/');
         if (! pp) return 0;
         pp[1] = 0;
      }

      if (strchr(galleryname,'"')) {                                             //  if galleryname has embedded quotes,
         cc = strlen(galleryname);                                               //    they must be escaped for "find"
         findname = (char *) zmalloc(cc+40);
         repl_1str(galleryname,findname,"\"","\\\"");
      }
      else findname = zstrdup(galleryname);

      while ((file = command_output(contx,findcommand,findname)))                //  find all files
      {
         if (strmatch(file,galleryname)) {                                       //  skip self directory
            zfree(file);
            continue;
         }

         if (Nfiles == maximages) {
            zmessageACK(Mwin,Btoomanyfiles,maximages);
            break;
         }

         ftype = image_file_type(file);

         if (ftype == FDIR) {                                                    //  subdirectory
            if (! Fshowhidden) {
               pp = strrchr(file,'/');                                           //  suppress hidden directories        15.04
               if (pp && pp[1] == '.') continue;
            }
            glist[Nfiles].file = file;                                           //  add to file list
            glist[Nfiles].file[0] = '!';                                         //  if directory, make it sort first
            glist[Nfiles].fdate[0] = 0;                                          //  no file date
            glist[Nfiles].pdate[0] = 0;                                          //  no photo date
            Nfiles++;
            Ndirs++;                                                             //  16.02
         }

         else if (ftype == IMAGE || ftype == RAW) {                              //  supported image or RAW file
            err = stat(file,&statbuf);
            if (err) continue;
            glist[Nfiles].file = file;                                           //  add to file list
            err = get_sxrec_min(file,fdate,pdate,size);                          //  file date, photo date, size
            strcpy(glist[Nfiles].fdate,fdate);                                   //  (overhead 0.3 ms per file)
            strcpy(glist[Nfiles].pdate,pdate);
            Nfiles++;
            Nimages++;
         }

         else {
            zfree(file);                                                         //  (thumbnails)
            continue;
         }
      }

      zfree(findname);

      if (Nfiles > 1)                                                            //  sort the glist records
         HeapSort((char *) glist, sizeof(glist_t), Nfiles, gallery_comp);

      gallery_monitor("start");                                                  //  monitor gallery file changes
      curr_file_count = Nimages;                                                 //  gallery image file count
      return 0;
   }

   if (strmatch(action,"initF"))                                                 //  initialize from given file list
   {
      if (gallerytype == NONE || gallerytype == GDIR)                            //  gallery type from caller
         zappcrash("gallerytype %d",gallerytype);

      fid = fopen(galleryname,"r");                                              //  open file
      if (! fid) return 0;

      file = (char *) zmalloc(XFCC);

      while (true)                                                               //  read list of files
      {
         pp = fgets_trim(file,XFCC-1,fid,1);
         if (! pp) break;
         err = stat(pp,&statbuf);                                                //  check file exists
         if (err) continue;
         glist[Nfiles].file = zstrdup(pp);                                       //  add to file list
         get_sxrec_min(pp,fdate,pdate,size);                                     //  file date, photo date, size
         strcpy(glist[Nfiles].fdate,fdate);
         strcpy(glist[Nfiles].pdate,pdate);
         Nfiles++;
         if (Nfiles == maximages) {
            zmessageACK(Mwin,Btoomanyfiles,maximages);
            break;
         }
      }

      fclose(fid);
      zfree(file);

      Nimages = Nfiles;
      curr_file_count = Nimages;                                                 //  gallery image file count

      if (curr_file) {
         Nth = gallery_position(curr_file,0);                                    //  current file in the new gallery?
         curr_file_posn = Nth;                                                   //  set curr. file posn. or -1 if not
      }

      return 0;
   }

   if (strmatch(action,"sort"))                                                  //  sort the file list from init
   {
      if (Nfiles < 2) return 0;
      HeapSort((char *) glist, sizeof(glist_t), Nfiles, gallery_comp);           //  sort the glist records
      return 0;
   }

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

      if (Nfiles == maximages-1) {                                               //  no room
         zmessageACK(Mwin,Btoomanyfiles,maximages);
         return 0;
      }

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

      glist[fposn].file = zstrdup(filez);                                        //  put new file in hole
      get_sxrec_min(filez,fdate,pdate,size);                                     //  file date, photo date, size
      strcpy(glist[Nfiles].fdate,fdate);
      strcpy(glist[Nfiles].pdate,pdate);
      Nfiles++;
      Nimages++;                                                                 //  bugfix
   }

   if (strmatch(action,"delete"))                                                //  delete file from list
   {
      fposn = Nth;                                                               //  file position from caller must be OK
      if (fposn < 0 || fposn > Nfiles-1) return 0;
      Nfiles--;
      if (*glist[fposn].file == '!') Ndirs--;                                    //  reduce directory count             16.02
      else Nimages--;                                                            //  reduce image file count
      zfree(glist[fposn].file);                                                  //  remove glist record
      for (ii = fposn; ii < Nfiles; ii++)                                        //  close the hole
         glist[ii] = glist[ii+1];                                                //  gcc bug workaround removed         16.01
      if (mdlist) {
         if (mdlist[fposn]) zfree(mdlist[fposn]);                                //  delete corresp. metadata
         for (ii = fposn; ii < Nfiles; ii++)                                     //  close the hole
            mdlist[ii] = mdlist[ii+1];                                           //  gcc bug workaround removed         16.01
      }
   }

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

   if (strmatch(action,"get1st"))                                                //  return 1st image file in gallery
   {
      for (Nth = 0; Nth < Nfiles; Nth++)
      {
         if (glist[Nth].file[0] == '!') continue;                                //  subdirectory
         file2 = zstrdup(glist[Nth].file);                                       //  get Nth file
         err = stat(file2,&statbuf);
         if (! err) return file2;
         zfree(file2);
      }
      return 0;
   }

   return 0;
}


//  private function for special file name compare
//  directories sort first and upper/lower case is ignored

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

   if (galleryseq == 1) {                                                        //  ascending
      grec1 = (glist_t *) rec1;
      grec2 = (glist_t *) rec2;
   }
   else {                                                                        //  descending
      grec1 = (glist_t *) rec2;
      grec2 = (glist_t *) rec1;
   }

   switch (gallerysort) {

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

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

      case 3: {                                                                  //  photo date/time
         nn = strcmp(grec1->pdate,grec2->pdate);                                 //  (EXIF DateTimeOriginal)
         return nn;
      }

      default: return 0;
   }
}


//  private function
//  paint gallery window - draw all thumbnail images that can fit

int navi::gallery_paint(GtkWidget *drwin, cairo_t *cr)
{
   GdkRGBA     rgba;
   PIXBUF      *pxbT;
   double      x1, y1, x2, y2;
   int         ii, nrows, row, col;
   int         row1, row2, ww, hh;
   int         drwingW, drwingH;
   int         thumx, thumy;
   int         ndir, nfil;
   FTYPE       ftype;
   char        *pp, *fname, p0;
   char        text[200], fdate[16], pdate[20], size[16];

   gallerypainted = 0;

   if (! galleryname) {
      if (! curr_dirk) return 0;
      gallery(curr_dirk,"init");
   }

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

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

   if (gallerytype == METADATA) {                                                //  metadata report
      gallery_paintmeta(drwin,cr);
      return 0;
   }
   
   fontsize = appfontsize - 3 + thumbsize / 128;                                 //  font size = appfontsize -3 to +1   16.05
   if (! thumbsize) fontsize = appfontsize - 1;                                  //  text only view
   texthh = 3.5 * fontsize + 4;                                                  //  two text lines
   thumbW = thumbsize + 10;                                                      //  thumbnail cell size
   thumbH = thumbsize + texthh + thumbsize/24 + 10;

   if (! thumbsize) {
      thumbW = 400;                                                              //  zero, list view
      thumbH = 40;
   }

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

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

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

   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_step_increment(Gadjust,thumbH);                            //  scrollbar works in row steps
   gtk_adjustment_set_page_increment(Gadjust,thumbH * xrows);                    //  and in page steps
   
   if (topfileposn >= 0) {                                                       //  new target file position (Nth)
      scrollp = topfileposn / xcols * thumbH;                                    //  scroll position for target file
      if (scrollp > maxscroll) scrollp = maxscroll;                              //    in top row of window
      gtk_adjustment_set_upper(Gadjust,maxscroll);
      gtk_adjustment_set_value(Gadjust,scrollp);                                 //  will cause re-entrance
      gtk_widget_queue_draw(drwin);
      topfileposn = -1;                                                          //  keep scroll position next time
      return 0;
   }
   else {
      cairo_clip_extents(cr,&x1,&y1,&x2,&y2);                                    //  window region to paint
      row1 = y1 / thumbH;
      row2 = y2 / thumbH;
   }
   
   for (row = row1; row <= row2; row++)                                          //  draw file thumbnails
   for (col = 0; col < xcols; col++)                                             //  draw all columns in row
   {
      ii = row * xcols + col;                                                    //  next file
      if (ii >= Nfiles) goto endloops;                                           //  exit 2 nested loops

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

      fname = glist[ii].file;                                                    //  filespec
      pp = strrchr(fname,'/');                                                   //  get file name only
      if (pp) fname = pp + 1;
      else fname = (char *) "?";

      thumx = col * thumbW + margin;                                             //  upper left corner in drawing area
      thumy = row * thumbH + margin;

      if (curr_file && strmatch(glist[ii].file,curr_file)) {                     //  yellow background for curr. image
         cairo_set_source_rgb(cr,1,1,0.5);
         cairo_rectangle(cr,thumx-3,thumy-3,thumbW-3,texthh);
         cairo_fill(cr);
      }

      ftype = image_file_type(glist[ii].file);                                   //  directory/image/RAW file

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

      if (ftype == IMAGE || ftype == RAW) {                                      //  image or RAW file
         get_sxrec_min(glist[ii].file,fdate,pdate,size);                         //  filename, file/photo date, size (WxH)
         if (gallerysort == 2) pp = fdate;
         else pp = pdate;                                                        //  use file or photo date based on sort
         if (! strmatch(pp,"undated")) {
            memcpy(pp+14,pp+10,2);                                               //  convert yyyymmddhhmm to yyyy-mm-dd hh:mm
            memcpy(pp+11,pp+8,2);
            memcpy(pp+8,pp+6,2);
            memcpy(pp+5,pp+4,2);
            pp[16] = 0;
            pp[13] = ':';
            pp[10] =' ';
            pp[4] = pp[7] = '-';

         }
         snprintf(text,200,"%s\n%-16s  %s",fname,pp,size);                       //  text is name, date, size
      }

      if (thumbsize)                                                             //  thumbnails view
      {
         if (ftype == IMAGE || ftype == RAW) {                                   //  image or RAW file
            draw_text(cr,text,thumx,thumy,thumbW-5);                             //  paint text first
            thumy += texthh;                                                     //  position thumbnail below text
         }

         pxbT = image_thumbnail(glist[ii].file,thumbsize);                       //  get thumbnail
         if (pxbT) {
            ww = gdk_pixbuf_get_width(pxbT);
            ww = (thumbsize - ww) / 4;                                           //  shift margin if smaller width      15.01
            gdk_cairo_set_source_pixbuf(cr,pxbT,thumx+ww,thumy);
            cairo_paint(cr);                                                     //  paint
            g_object_unref(pxbT);
         }

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

      if (! thumbsize)                                                           //  list view, no thumbnails
         draw_text(cr,text,thumx,thumy,thumbW-5);                                //  paint text

      *glist[ii].file = p0;                                                      //  restore '!'
   }

   endloops:
   gallerypainted = 1;
   return 0;
}


//  private function
//  paint metadata report - draw thumbnail images + metadata

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

   if (thumbsize < 128) thumbsize = 128;

   fontsize = appfontsize - 3 + thumbsize / 128;                                 //  font size = appfontsize -3 to +1   16.05

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

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

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

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

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

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

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

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

   if (topfileposn >= 0) {                                                       //  new target file position (Nth)
      scrollp = topfileposn / xcols * thumbH;                                    //  scroll position for target file
      if (scrollp > maxscroll) scrollp = maxscroll;                              //    in top row of window
      gtk_adjustment_set_upper(Gadjust,maxscroll);
      gtk_adjustment_set_value(Gadjust,scrollp);                                 //  will cause re-entrance
      gtk_widget_queue_draw(drwin);
      topfileposn = -1;                                                          //  keep scroll position next time
      return 1;
   }

   else {
      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
   {
      for (col = 0; col < xcols; col++)
      {
         ii = row * xcols + col;                                                 //  next file
         if (ii >= Nfiles) goto endloops;                                        //  exit 2 nested loops

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

         thumx = col * thumbW + margin;                                          //  upper left corner in window space
         thumy = row * thumbH + margin;

         pxbT = image_thumbnail(glist[ii].file,thumbsize);                       //  get thumbnail
         if (pxbT) {
            gdk_cairo_set_source_pixbuf(cr,pxbT,thumx,thumy);
            cairo_paint(cr);
            g_object_unref(pxbT);
         }

         draw_text(cr,glist[ii].file,thumbW+margin,thumy,textww);                //  write filespec to right of thumbnail

         if (mdlist && mdlist[ii])                                               //  write metadata if present
            draw_text(cr,mdlist[ii],thumbW+margin,thumy+20,textww);

         *glist[ii].file = p0;                                                   //  restore '!'
      }
   }

   endloops:
   gallerypainted = 1;
   return 1;
}


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

void navi::gallery_navibutts()                                                   //  15.07
{
   char        labtext[100];
   int         ii, cc, max = maxgallerylevs;
   char        *pp1, *pp2;
   
   for (ii = 0; ii < max; ii++) {                                                //  clear old navi buttons if any
      if (gallerypath[ii]) {
         zfree(gallerypath[ii]);
         gallerypath[ii] = 0;
         gtk_widget_destroy(gallerybutt[ii]);
      }
   }
   
   if (gallerylabel) gtk_widget_destroy(gallerylabel);                           //  clear gallery label if any         16.05
   gallerylabel = 0;
   
   if (gallerytype == SEARCH) sprintf(labtext,"search results");                 //  search results
   if (gallerytype == METADATA) sprintf(labtext,"search results");               //  search results (metadata report)
   if (gallerytype == RECENT) sprintf(labtext,"recent images");                  //  recent images
   if (gallerytype == NEWEST) sprintf(labtext,"newest images");                  //  newest images
   
   if (gallerytype == ALBUM) {                                                   //  album gallery
      pp1 = strrchr(galleryname,'/');
      if (pp1) pp1++;
      else pp1 = galleryname;
      snprintf(labtext,100,"album: %s",pp1);                                     //  album: album-name
   }

   if (gallerytype != GDIR) {                                                    //  not a directory 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 (true)                                                                  //  construct new buttons
   {
      pp2 = strchr(pp1+1,'/');                                                   //  /aaaaaa/bbbbbb/cccccc
      if (pp2) cc = pp2 - pp1;                                                   //         |      |
      else cc = strlen(pp1);                                                     //         pp1    pp2
      gallerypath[ii] = (char *) zmalloc(cc);
      strncpy0(gallerypath[ii],pp1+1,cc);                                        //  bbbbbb
      gallerybutt[ii] = gtk_button_new_with_label(gallerypath[ii]);
      gtk_box_pack_start(GTK_BOX(Gpanel),gallerybutt[ii],0,0,3);
      G_SIGNAL(gallerybutt[ii],"clicked",navibutt_clicked,&Nval[ii]);
      pp1 = pp1 + cc;                                                            //  next directory level /cccccc
      if (! *pp1) break;                                                         //  null = end
      if (*pp1 == '/' && ! *(pp1+1)) break;                                      //  / + null = end
      if (++ii == max) break;                                                    //  limit of directory levels
   }
   
   gtk_widget_show_all(Gpanel);
   return;
}


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

void navi::dir_filecount(char *dirname, int &ndir, int &nfil)                    //  15.11
{
   char     *file;
   cchar    *findcommand = "find -L \"%s\" -maxdepth 1";
   int      ii, cc, err;
   int      dcount = 0, fcount = 0, contx = 0;
   FTYPE    ftype;

   #define NC 1000

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

   if (ftf) {
      ftf = 0;
      cc = NC * sizeof(char *);
      memset(dirnamecache,0,cc);
   }

   err = stat(dirname,&statb);
   if (err) return;

   for (ii = 0; ii < NC; ii++) {
      if (! dirnamecache[ii]) break;
      if (strmatch(dirname,dirnamecache[ii]) &&
          statb.st_mtime == modtimecache[ii]) {
         ndir = dcountcache[ii];
         nfil = fcountcache[ii];
         return;
      }
   }

   while ((file = command_output(contx,findcommand,dirname))) {                  //  count included files and directories
      ftype = image_file_type(file);
      zfree(file);
      if (ftype == FDIR) dcount++;                                               //  directory count
      else if (ftype == IMAGE || ftype == RAW) fcount++;                         //  image file count
   }

   dcount--;                                                                     //  remove self-count

   ii = pcache++;
   if (pcache == NC) pcache = 0;
   if (dirnamecache[ii]) zfree(dirnamecache[ii]);
   dirnamecache[ii] = zstrdup(dirname);
   modtimecache[ii] = statb.st_mtime;
   ndir = dcountcache[ii] = dcount;
   nfil = fcountcache[ii] = fcount;
   return;
}


//  private function
//  write text for thumbnail limited by width of thumbnail

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

   static int     pfontsize = -1;
   static char    thumbfont[12] = "";

   if (fontsize != pfontsize) {                                                  //  adjust for curr. font size
      pfontsize = fontsize;
      snprintf(thumbfont,12,"sans %d",fontsize);
      if (pfont) pango_font_description_free(pfont);                             //  free memory 
      pfont = pango_font_description_from_string(thumbfont);
      if (playout) g_object_unref(playout);                                      //  free memory
      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);
   pango_cairo_show_layout(cr,playout);
   return;
}


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

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

   if (FGWM == 'G' && ! gallerypainted) return;                                  //  wait for pending paint             16.06
   if (! strmatch(menu,ZTX("Scroll"))) gallery_scroll(-1,0);                     //  stop scrolling

   scrollp = gtk_adjustment_get_value(Gadjust);                                  //  current scroll position
   
   if (strmatch(menu,ZTX("Sync Gallery")))                                       //  curr. file directory --> gallery   15.12
   {
      F1_help_topic = "sync_gallery";                                            //  15.07
      if (! curr_file) return;
      gallery(curr_file,"init");
      m_viewmode(0,"G");
      gallery(curr_file,"paint");
      return;
   }

   if (strmatch(menu,ZTX("Open"))) {                                             //  change directory
      file = zgetfile(ZTX("change directory"),MWIN,"folder",curr_dirk);
      if (! file) return;
      gallery(file,"init");
      gallery(0,"paint",0);
      zfree(file);
      return;
   }

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

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

   if (strmatch(menu,ZTX("Zoom+")))  {                                           //  next bigger thumbnail size
      for (ii = 0; ii < thumbxx; ii++)
         if (thumbsize == thumbx[ii]) break;
      if (ii == 0) return;
      thumbsize = thumbx[ii-1];
      topfileposn = scrollp / thumbH * xcols;                                    //  keep top row position
      gtk_widget_queue_draw(Gdrawin);
      return;
   }

   if (strmatch(menu,ZTX("Zoom-")))  {                                           //  next smaller
      for (ii = 0; ii < thumbxx; ii++)
         if (thumbsize == thumbx[ii]) break;
      if (ii >= thumbxx-1) thumbsize = 0;                                        //  no thumbs, list view
      else  thumbsize = thumbx[ii+1];
      if (thumbsize < 128 && gallerytype == METADATA)                            //  min. for metadata report
         thumbsize = 128;
      topfileposn = scrollp / thumbH * xcols;                                    //  keep top row position
      gtk_widget_queue_draw(Gdrawin);
      return;
   }

   if (strmatch(menu,ZTX("Row↑"))) {                                            //  scroll 1 row up
      scroll1 = scrollp / thumbH * thumbH;
      scroll2 = scroll1 - thumbH;
      if (scroll2 < 0) scroll2 = 0;
      gallery_scroll(scroll2,3000);
      return;
   }

   if (strmatch(menu,ZTX("Row↓"))) {                                             //  scroll 1 row down
      scroll1 = scrollp / thumbH * thumbH;
      scroll2 = scroll1 + thumbH;
      if (scroll2 > maxscroll) scroll2 = maxscroll;
      gallery_scroll(scroll2,3000);
      return;
   }

   if (strmatch(menu,ZTX("Page↑"))) {                                            //  scroll 1 page up 
      scroll1 = scrollp / thumbH * thumbH;
      scroll2 = scroll1 - thumbH * xrows;
      if (scroll2 < 0) scroll2 = 0;
      gallery_scroll(scroll2,5000);
      return;
   }

   if (strmatch(menu,ZTX("Page↓"))) {                                            //  scroll 1 page down
      scroll1 = scrollp / thumbH * thumbH;
      scroll2 = scroll1 + thumbH * xrows;
      if (scroll2 > maxscroll) scroll2 = maxscroll;
      gallery_scroll(scroll2,5000);
      return;
   }

   if (strmatch(menu,ZTX("Scroll"))) {                                           //  start/stop slow scroll down
      if (gallery_scrollgoal >= 0) gallery_scroll(-1,0);
      else gallery_scroll(maxscroll,500);
      return;
   }

   if (strmatch(menu,ZTX("First"))) scrollp = 0;                                 //  jump to top or bottom
   if (strmatch(menu,ZTX("Last"))) scrollp = maxscroll;

   topfileposn = scrollp / thumbH * xcols;
   gtk_widget_queue_draw(Gdrawin);
   return;
}


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

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

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

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


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

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

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

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

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

   cumscroll += 0.002 * gallery_scrollspeed;                                     //  based on 2 millisec. timer
   if (cumscroll < 1.0) return 1;                                                //  not yet one pixel

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

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

   gtk_adjustment_set_value(Gadjust,scrollp);

   cumscroll = 0;
   return 1;
}


//  private function
//  gallery navigation button clicked, open corresponding directory

void navi::navibutt_clicked(GtkWidget *widget, int *lev)                         //  15.07
{
   char     gallerydir[XFCC], *pp;
   
   pp = gallerydir;
   
   for (int ii = 0; ii <= *lev; ii++)
   {
      *pp = '/';
      strcpy(pp+1,gallerypath[ii]);
      pp = pp + strlen(pp);
   }
   
   gallery(gallerydir,"init");                                                   //  initialize new gallery
   gallery(0,"paint",0);                                                         //  paint new gallery
   return;
}


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

void navi::newtop(GtkWidget *widget, GdkEventButton *event)
{
   zdialog     *zd;
   char        dirk[200], *pp;

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

   zd = zdialog_new(ZTX("Choose image directory"),Mwin,null);                    //  popup dialog with top image directories
   zdialog_set_decorated(zd,0);                                                  //  15.07
   zdialog_add_widget(zd,"combo","top","dialog");
   for (int ii = 0; ii < Ntopdirks; ii++)                                        //  insert all top image directories
      zdialog_cb_app(zd,"top",topdirks[ii]);
   zdialog_cb_app(zd,"top","/");                                                 //  add "/" and "HOME"
   zdialog_cb_app(zd,"top","HOME");
   zdialog_cb_app(zd,"top",ZTX("recent images"));                                //  add "recent" and "newest"
   zdialog_cb_app(zd,"top",ZTX("newest images"));
   zdialog_resize(zd,200,0);
   zdialog_run(zd,newtop_dialog_event,"mouse");                                  //  run dialog, wait for response
   zdialog_cb_popup(zd,"top");
   zdialog_wait(zd);

   if (zd->zstat == 1)
      zdialog_fetch(zd,"top",dirk,200);                                          //  get user choice
   else *dirk = 0;
   zdialog_free(zd);
   if (! *dirk) return;

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

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

   if (strmatch(dirk,"HOME"))                                                    //  if HOME, get /home/<user> directory
      if ((pp = getenv("HOME")))
         if (pp) strncpy0(dirk,pp,200);

   gallery(dirk,"init");
   gallery(0,"paint",0);                                                         //  paint new gallery
   return;
}

int navi::newtop_dialog_event(zdialog *zd, cchar *event)                         //  dialog event function
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   if (strmatch(event,"top")) zd->zstat = 1;                                     //  finish dialog when choice is made
   else zd->zstat = 2;
   return 1;
}


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

void navi::newalbum(GtkWidget *widget, GdkEventButton *event)                    //  15.08
{
   gallery_scroll(-1,0);                                                         //  stop scrolling
   getalbum();
   return;
}


//  private function
//  show a list of albums and choose a one which becomes current gallery
//  called by [Top] button menu in gallery view

void navi::getalbum()
{
   cchar          *findcomm = "find -L \"%s\" -type f";
   char           *albums[100], albumname[100], albumfile[200];
   char           *file, *pp;
   int            ii, contx = 0, count = 0;
   zdialog        *zd;

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

   while ((file = command_output(contx,findcomm,albums_dirk)))                   //  find all album files
   {
      if (count > 99) {
         zmessageACK(Mwin,Btoomanyfiles,100);
         break;
      }

      pp = strrchr(file,'/');
      if (! pp) continue;
      albums[count] = zstrdup(pp+1);
      zfree(file);
      count++;
   }

   if (file) command_kill(contx);

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

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

   zd = zdialog_new(ZTX("Choose album"),Mwin,null);                              //  popup dialog with albums list
   zdialog_set_decorated(zd,0);                                                  //  15.07
   zdialog_add_widget(zd,"combo","albums","dialog");

   for (ii = 0; ii < count; ii++)                                                //  insert album file names
      zdialog_cb_app(zd,"albums",albums[ii]);

   zdialog_resize(zd,250,0);
   zdialog_run(zd,getalbum_dialog_event,"mouse");                                //  run dialog, wait for response
   zdialog_cb_popup(zd,"albums");
   zdialog_wait(zd);

   if (zd->zstat != 1) {
      zdialog_free(zd);
      for (ii = 0; ii < count; ii++)
         zfree(albums[ii]);
      return;
   }

   zdialog_fetch(zd,"albums",albumname,100);                                     //  get user choice
   zdialog_free(zd);
   for (ii = 0; ii < count; ii++) zfree(albums[ii]);
   
   snprintf(albumfile,200,"%s/%s",albums_dirk,albumname);                        //  show the album gallery             16.04
   album_show(albumfile);
   return;
}

int navi::getalbum_dialog_event(zdialog *zd, cchar *event)                       //  dialog event function
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   if (strmatch(event,"albums")) zd->zstat = 1;                                  //  finish dialog when choice is made
   else zd->zstat = 2;
   return 1;
}


//  private function
//  gallery sort by file name, file date, or photo date (exif)

void navi::gallery_sort()
{
   zdialog     *zd;
   int         zstat, nn;

   gallery_scroll(-1,0);                                                         //  stop scrolling
   
   if (gallerytype == ALBUM) return;                                             //  do not sort album galleries        16.08

   zd = zdialog_new(ZTX("Gallery Sort"),Mwin,Bapply,null);                       //  user dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1");
   zdialog_add_widget(zd,"radio","filename","vb1",ZTX("File Name"));
   zdialog_add_widget(zd,"radio","filedate","vb1",ZTX("File Mod Date/Time"));
   zdialog_add_widget(zd,"radio","photodate","vb1",ZTX("Photo Date/Time (EXIF)"));
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","ascending","hb2",ZTX("ascending"),"space=4");
   zdialog_add_widget(zd,"radio","descending","hb2",ZTX("descending"),"space=2");

   if (gallerysort == 1) zdialog_stuff(zd,"filename",1);                         //  stuff current sort order
   if (gallerysort == 2) zdialog_stuff(zd,"filedate",1);
   if (gallerysort == 3) zdialog_stuff(zd,"photodate",1);

   if (galleryseq == 1) zdialog_stuff(zd,"ascending",1);                         //  ascending/descending
   else zdialog_stuff(zd,"descending",1);

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

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

   zdialog_fetch(zd,"filename",nn);                                              //  get user choice
   if (nn) gallerysort = 1;
   zdialog_fetch(zd,"filedate",nn);
   if (nn) gallerysort = 2;
   zdialog_fetch(zd,"photodate",nn);
   if (nn) gallerysort = 3;
   zdialog_fetch(zd,"ascending",nn);
   if (nn) galleryseq = 1;
   else galleryseq = 2;

   zdialog_free(zd);

   gallery(0,"sort");                                                            //  sort the gallery
   gallery(0,"paint",0);
   return;
}


//  dialog event and completion function

int navi::gallery_sort_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   return 1;
}


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

void navi::mouse_event(GtkWidget *widget, GdkEvent *event, void *)
{
   GdkEventButton *eventB;
   PIXBUF         *pxbT;
   int            evtype, mousex, mousey, mousebutt;
   int            row, col, nrows, tww, thh, marg;
   int            Nth, poswidth, posheight, err, ftype;
   char           *filez = 0;
   STATB          statb;
   
   if (! Nfiles) return;                                                         //  empty gallery
   if (! gallerypainted) return;                                                 //  not initialized
   
   eventB = (GdkEventButton *) event;
   evtype = eventB->type;
   mousex = int(eventB->x);
   mousey = int(eventB->y);
   mousebutt = eventB->button;
   if (mousex < margin) return;
   if (mousey < margin) return;

   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;                              //  button 1 + Alt handled by window manager

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

   if (thumbsize) {                                                              //  bugfix
      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;
   nrows = 1 + (Nfiles-1) / xcols;                                               //  total thumbnail rows, 1 or more
   if (col < 0 || col >= xcols) return;                                          //  mouse not on a thumbnail
   if (row < 0 || row >= nrows) return;
   Nth = xcols * row + col;                                                      //  mouse at this thumbnail (image file)
   if (Nth >= Nfiles) return;

   filez = zstrdup(glist[Nth].file);                                             //  file (thumbnail) at mouse posn.    16.08
   *filez = '/';
   
   if (evtype == GDK_BUTTON_PRESS)
   {
      gallery_scroll(-1,0);                                                      //  stop scrolling

      if (drag_file) zfree(drag_file);
      drag_file = 0;
      ftype = image_file_type(filez);                                            //  if image file, save for possible
      if (ftype == IMAGE) {                                                      //    drag-drop operation
         drag_file = zstrdup(filez);                                             //  save file and position in gallery
         drag_posn = Nth;
      }
      goto cleanup;
   }

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

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

      if (S_ISDIR(statb.st_mode)) {                                              //  if directory, go there
         gallery(filez,"init");
         gallery(0,"paint",0);                                                   //  paint new gallery
         goto cleanup;
      }

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

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

      if (mousebutt == 1) {                                                      //  left click
         if (zd_gallery_getfiles) gallery_getfiles_Lclick_func(Nth);             //  send to gallery_getfiles()
         else if (zd_edit_bookmarks) bookmarks_Lclick_func(Nth);                 //  send to bookmarks editor
         else if (zd_ss_imageprefs) ss_imageprefs_Lclick_func(Nth);              //  send to slide show editor 
         else if (KBshiftkey) popup_image(filez,MWIN,1,0);                       //  popup enlarged image
         
         else if (zdexifview) meta_view(0);                                      //  EXIF/IPTC data view window
         else if (zdcopymove) m_copy_move(0,0);                                  //  copy/move dialog
         else if (zdrename) m_rename(0,0);                                       //  rename dialog
         else if (zddeltrash) m_delete_trash(0,0);                               //  delete/trash dialog
         else if (zdeditmeta) m_edit_metadata(0,0);                              //  edit metadata dialog
         else if (zdexifedit) m_meta_edit_any(0,0);                              //  edit any metadata dialog
         else if (zdupright) m_upright(0,0);                                     //  upright image

         else gallery_Lclick_func(Nth);                                          //  open the file
         goto cleanup;
      }

      if (mousebutt == 3) {                                                      //  right click
         if (zd_gallery_getfiles) gallery_getfiles_Rclick_func(Nth);             //  send to gallery_getfiles()
         else gallery_Rclick_popup(Nth);                                         //  send to gallery thumbnail popup menu
         goto cleanup;
      }
   }

cleanup:
   zfree(filez);
   return;
}


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

char * navi::gallery_dragfile()
{
   drag_gallery = galleryname;
   return drag_file;
}


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

void navi::gallery_dropfile(int mousex, int mousey, char *file)                  //  16.08
{
   int      top, mpos, speed;
   int      row, col, nrows, Nth;
   int      poswidth, posheight;
   
   if (gallerytype != GDIR && gallerytype != ALBUM) return;                      //  reject others (search, recent ...)

   if (! file)                                                                   //  drag motion underway
   {
      top = gtk_adjustment_get_value(Gadjust);                                   //  mouse vertical posn in window
      mpos = 100 * (mousey - top) / xwinH;                                       //  0 - 100

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

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

      if (mpos >= 20 && mpos <= 80)                                              //  stop scrolling in mid-window range
         gallery_scroll(-1,0);
      
      return;
   }
   
   if (gallerytype == GDIR)                                                      //  directory gallery, add new file
   {
      shell_quiet("cp \"%s\" \"%s\"",file,galleryname);
      gallery(0,"paint",0);
      return;
   }
                                                                                 //  album gallery
   row = (mousey - margin) / thumbH;                                             //  find drop location: row, col
   col = (mousex - margin) / thumbW;

   if (thumbsize) {                                                              //  bugfix
      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;
   nrows = 1 + (Nfiles-1) / xcols;                                               //  total thumbnail rows, 1 or more
   if (col < 0 || col >= xcols) return;                                          //  mouse not on a thumbnail
   if (row < 0 || row >= nrows) return;
   Nth = xcols * row + col;                                                      //  mouse at this thumbnail (image file)
   if (Nth >= Nfiles) return;
   if (poswidth > 50) Nth++;                                                     //  drop after this position

   if (drag_gallery && strmatch(galleryname,drag_gallery)) {                     //  drag-drop in same gallery
      album_movefile(drag_posn,Nth);                                             //  move file position in gallery
      drag_gallery = 0;
   }
   else album_pastefile(file,Nth);                                               //  insert new file at position

   album_show();
   return;
}


//  Private function - respond to keyboard navigation keys.
//  KBrelease() for main window calls this function when G view is active.
//  key definitions: /usr/include/gtk-2.0/gdk/gdkkeysyms.h

int navi::KBrelease(GtkWidget *win, GdkEventKey *event, void *)
{
   int      KBkey;

   KBkey = event->keyval;

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

   if (KBkey == GDK_KEY_plus) menufuncx(win,ZTX("Zoom+"));                       //  +/- = bigger/smaller thumbnails
   if (KBkey == GDK_KEY_equal) menufuncx(win,ZTX("Zoom+"));
   if (KBkey == GDK_KEY_minus) menufuncx(win,ZTX("Zoom-"));
   if (KBkey == GDK_KEY_KP_Add) menufuncx(win,ZTX("Zoom+"));                     //  keypad +/- also
   if (KBkey == GDK_KEY_KP_Subtract) menufuncx(win,ZTX("Zoom-"));

   if (KBkey == GDK_KEY_Left) menufuncx(win,ZTX("Page↑"));                       //  left arrow = previous page
   if (KBkey == GDK_KEY_Right) menufuncx(win,ZTX("Page↓"));                      //  right arrow = next page
   if (KBkey == GDK_KEY_Up) menufuncx(win,ZTX("Row↑"));                          //  up arrow = previous row
   if (KBkey == GDK_KEY_Down) menufuncx(win,ZTX("Row↓"));                        //  down arrow = next row

   if (KBkey == GDK_KEY_Home) menufuncx(win,ZTX("First"));                       //  keys added
   if (KBkey == GDK_KEY_End) menufuncx(win,ZTX("Last"));
   if (KBkey == GDK_KEY_Page_Up) menufuncx(win,ZTX("Page↑"));
   if (KBkey == GDK_KEY_Page_Down) menufuncx(win,ZTX("Page↓"));

   return 1;
}


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

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

   if (FGWM != 'G') return;

   if (gallerytype == GDIR)
      snprintf(title,200,"DIRECTORY   %s  %d files",galleryname,Nfiles);

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

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

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

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

   else strcpy(title,"UNKNOWN");

   gtk_window_set_title(MWIN,title);
   return;
}


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

char * gallery_getnext(int index, int lastver)                                   //  overhauled                         16.07
{
   char * get_rootname(char *file);
   char * prev_next_gallery(int index);

   int      Nth;
   char     *rootname1 = 0, *rootname2 = 0;                                      //  file names without .vNN and .ext
   char     *file = 0, *filever = 0;
   
   Nth = curr_file_posn;
   
   if (index == +1)                                                              //  get next file
   {
      while (true) 
      {
         Nth += 1;
         if (Nth >= Nfiles) {                                                    //  no more files this gallery
            if (filever) break;                                                  //  return last file version
            goto retnull;                                                        //  no more files
         }
         if (file) zfree(file);
         file = gallery(0,"find",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 = get_rootname(filever);                                   //  save rootname
            continue;
         }
         if (rootname2) zfree(rootname2);
         rootname2 = get_rootname(file);
         if (! strmatch(rootname1,rootname2)) break;                             //  new rootname, filever was last version
         zfree(filever);
         filever = file;                                                         //  save last file with same rootname
         file = 0;
      }

      if (file) zfree(file);
      file = filever;
      goto retfile;
   }
   
   if (index == -1)                                                              //  get previous file
   {
      if (curr_file) rootname1 = get_rootname(curr_file);                        //  current file rootname
      while (true) 
      {
         Nth -= 1;
         if (Nth < Ndirs) goto retnull;                                          //  no more files
         if (file) zfree(file);
         file = gallery(0,"find",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 = get_rootname(file);
         if (! strmatch(rootname1,rootname2)) goto retfile;                      //  new rootname, return this file
      }
   }
   
   retnull:
   if (file) zfree(file);
   file = 0;

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


//  get the root file name of a file: /.../filename  without .vNN and .ext

char * get_rootname(char *file)                                                  //  16.02
{
   char  *rootname, *pp;

   rootname = zstrdup(file);                                                     //  /.../filename.ext (or) filename.vNN.ext
   pp = strrchr(rootname,'.');                                                   //               |                 |
   if (! pp) return rootname;                                                    //               pp                pp
   if (strmatchN(pp-4,".v",2)) pp -= 4;
   *pp = 0;
   return rootname;
}


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

char * prev_next_gallery(int index)                                              //  overhauled                         16.07
{
   int      nn, Nth;
   char     *parentdir = 0, *olddir = 0, *newdir = 0, *file = 0;
   
   if (gallerytype != GDIR) goto errret;                                         //  gallery not a physical directory
   
   olddir = zstrdup(galleryname);                                                //  olddir = current gallery / directory
   if (! olddir) goto errret;
   nn = strlen(olddir) - 1;
   if (olddir[nn] == '/') olddir[nn] = 0;
   parentdir = zstrdup(olddir);                                                  //  get parent directory
   for ( ; nn && parentdir[nn] != '/'; nn--)
   if (! nn) goto errret;
   parentdir[nn] = 0;
   gallery(parentdir,"init");                                                    //  gallery = parent
   
   for (Nth = 0; Nth < Ndirs; Nth++) {                                           //  find olddir in parent
      if (file) zfree(file);
      file = gallery(0,"find",Nth);
      if (! file) goto errret;
      if (strmatch(file,olddir)) break;
   }
   
   Nth += index;                                                                 //  previous or next directory
   if (Nth < 0 || Nth >= Ndirs) goto errret;
   newdir = gallery(0,"find",Nth);
   if (newdir) goto okret;

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

okret:
   if (olddir) {
      gallery(olddir,"init");                                                    //  restore old directory
      zfree(olddir);
   }
   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 gallery_position(cchar *file, int Nth)
{
   int      ii;

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

   if (Nth >= 0 && Nth < Nfiles) ii = Nth;
   else ii = 0;

   if (strmatch(file+1,glist[ii].file+1)) return ii;                             //  file[0] may be !

   for (ii = 0; ii < Nfiles; ii++)
      if (strmatch(file+1,glist[ii].file+1)) break;

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


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

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

   if (! file) return FNF;
   err = stat(file,&statbuf);
   if (err) return FNF;

   if (S_ISDIR(statbuf.st_mode)) return FDIR;                                    //  directory

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

   if (ftf) {
      if (thumbdirk && *thumbdirk == '/')
         tdcc = strlen(thumbdirk);
      myRAWtypes = zstrdup(" ");
      ftf = 0;
   }

   if (tdcc && strmatchN(file,thumbdirk,tdcc)) return THUMB;                     //  fotoxx thumbnail

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

   xcc = strlen(ppx);
   if (xcc > 5) return OTHER;                                                    //  file .ext > 5 chars.

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

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

   if (strcasestr(RAWfiletypes,ppx2)) {                                          //  found in list of known RAW types
      tcc = strlen(myRAWtypes) + xcc + 2;
      ppt = (char *) zmalloc(tcc);                                               //  add to cache of my RAW types
      strcpy(ppt,ppx2);
      strcpy(ppt+xcc+1,myRAWtypes);
      zfree(myRAWtypes);
      myRAWtypes = ppt;
      return RAW;
   }

   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/dirk/file.xxx                                            //  .jpg .png .tif etc.
//  thumb dirk:  /thumb/dirk                                                     //  if thumb dirk defined
//  thumb file:  /thumb/dirk/image/dirk/file.xxx.jpeg                            //     thumb file is like this
//  thumb file:  /image/dirk/.thumbnails/file.xxx.jpeg                           //  else like this

char * thumb2imagefile(cchar *thumbfile)
{
   STATB    statb;
   int      err, cc;
   char     *imagefile;
   char     *pp;

   if (thumbdirk && *thumbdirk == '/')                                           //  remove /thumb/dirk from the front
   {
      cc = strlen(thumbdirk);
      imagefile = zstrdup(thumbfile+cc);                                         //  have /image/dirk/file.xxx.jpeg
      cc = strlen(imagefile);
      imagefile[cc-5] = 0;                                                       //  /image/dirk/file.xxx
   }

   else                                                                          //  remove /.thumbnails from between
   {
      pp = (char *) strrchr(thumbfile,'/');                                      //  /image/dirk/.thumbnails/file.xxx.jpeg
      if (! pp) return 0;                                                        //             |           |             |
      pp = pp - 12;                                                              //             pp          +12           +cc
      if (! strmatchN(pp,"/.thumbnails/",13)) return 0;
      cc = strlen(pp+12);
      if (! strmatch(pp+12+cc-5,".jpeg")) return 0;
      imagefile = zstrdup(thumbfile);
      pp = imagefile + (pp - thumbfile);
      memmove((char *) pp,pp+12,cc-5);                                           //  /image/dirk/file.xxx
      pp[cc-5] = 0;
   }

   err = stat(imagefile,&statb);                                                 //  check file exists
   if (! err) return imagefile;                                                  //  return image file
   zfree(imagefile);                                                             //  not found
   return 0;
}


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

char * image2thumbfile(cchar *imagefile)
{
   int      cc, cc1, cc2;
   char     *pp, *thumbfile;

   if (thumbdirk && *thumbdirk == '/')                                           //  use defined thumbnail directory
   {
      cc1 = strlen(thumbdirk);
      cc2 = strlen(imagefile);
      thumbfile = (char *) zmalloc(cc1+cc2+6);
      strcpy(thumbfile,thumbdirk);                                               //  /thumb/dirk
      strcpy(thumbfile+cc1,imagefile);                                           //  /thumb/dirk/image/dirk/file.xxx
      strcpy(thumbfile+cc1+cc2,".jpeg");                                         //  /thumb/dirk/image/dirk/file.xxx.jpeg
      return thumbfile;
   }

   else                                                                          //  use /image/dirk/.thumbnails/
   {
      thumbfile = zstrdup(imagefile,18);                                         //  /image/dirk/file.xxx
      pp = strrchr(thumbfile,'/');                                               //             |        |
      if (! pp) return 0;                                                        //             pp       +cc
      cc = strlen(pp);
      memmove(pp+12,pp,cc);                                                      //  /image/dirk............/file.xxx
      strncpy(pp,"/.thumbnails",12);                                             //  /image/dirk/.thumbnails/file.xxx
      strcpy(pp+12+cc,".jpeg");                                                  //  /image/dirk/.thumbnails/file.xxx.jpeg
      return thumbfile;
   }
}


//  Get thumbnail file for the given image file.
//  If missing or stale, add or update thumbnail file on disk.
//  Returned filespec is subject for zfree().
//  Optional arg 'ind' is returned 1 if thumb file was created.

char * image_thumbfile(char *imagefile, int *ind)
{
   PIXBUF      *thumbpxb;
   GError      *gerror = 0;
   char        *thumbfile, *pp;
   int         err;
   FTYPE       ftype;
   STATB       statf, statb;
   static int  Fmkdirerr = 0;

   zthreadcrash();

   if (ind) *ind = 0;

   err = stat(imagefile,&statf);
   if (err) return 0;                                                            //  image file not found

   ftype = image_file_type(imagefile);

   if (ftype == THUMB) {                                                         //  thumbnail file
      thumbfile = zstrdup(imagefile);                                            //  return same file
      return thumbfile;
   }

   if (ftype != IMAGE && ftype != RAW) return 0;                                 //  not an image file or RAW file

   thumbfile = image2thumbfile(imagefile);                                       //  get thumbnail file for image file
   if (! thumbfile) return 0;

   err = stat(thumbfile,&statb);                                                 //  thumbfile exists, up to date ??
   if (! err && statb.st_mtime >= statf.st_mtime) return thumbfile;              //  yes, return it

   pp = strrchr(thumbfile,'/');                                                  //  check if thumbnail directory exists
   *pp = 0;
   err = stat(thumbfile,&statb);
   *pp = '/';
   if (err) {                                                                    //  no
      pp = thumbfile;
      while (true) {
         pp = strchr(pp+1,'/');                                                  //  check each directory level
         if (! pp) break;
         *pp = 0;
         err = stat(thumbfile,&statb);
         if (err) {
            err = mkdir(thumbfile,0750);                                         //  if missing, try to create
            if (err) {
               if (! Fmkdirerr++)                                                //  if unable, print error once only
                  printz("create thumbnail: %s \n",strerror(errno));
               zfree(thumbfile);
               return 0;
            }
         }
         *pp = '/';
      }
   }

   thumbpxb = get_thumbnail_pixbuf(imagefile,thumbfilesize);                     //  generate thumbnail pixbuf from image file
   if (! thumbpxb) return 0;

   gdk_pixbuf_save(thumbpxb,thumbfile,"jpeg",&gerror,"quality","80",null);       //  save as .jpeg file in thumbnail directory
   g_object_unref(thumbpxb);
   if (ind) *ind = 1;                                                            //  return indicator
   return thumbfile;
}


//  Get thumbnail image (pixbuf) for given image file.
//  Use pixbuf cached in memory if available.
//  Create pixbuf from thumbnail file if size <= thumbnail file size.
//  Create pixbuf from image file if size > thumbnail file size.
//  Add pixbuf to memory cache if not there already.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.
//  Returns null if thumbnail not found on disk.

namespace thumbnail_cache 
{
   const int      cachesize = thumbnail_cachesize;                               //  shorthand
   static int     nextcache = 0, ftf = 1;
   static int     sizecache[cachesize];                                          //  128/256/etc.
   static time_t  mtimecache[cachesize];                                         //  file mod time
   static char    *fpathcache[cachesize];                                        //  file pathname
   static PIXBUF  *pixbufcache[cachesize];                                       //  thumbnail pixbuf
   static int     bytescache[cachesize];                                         //  thumbnail ww x hh x 3
}

PIXBUF * image_thumbnail(char *fpath, int size)
{
   using namespace thumbnail_cache;

   PIXBUF      *thumbpxb;
   GError      *gerror = 0;
   int         ii, err, ww, hh;
   char        *bpath;
   time_t      mtime;
   STATB       statf;

   zthreadcrash();                                                               //  thread usage not allowed

   if (ftf) {                                                                    //  first call
      for (ii = 0; ii < cachesize; ii++)
         fpathcache[ii] = 0;                                                     //  clear cache
      ftf = 0;
   }

   err = stat(fpath,&statf);                                                     //  fpath status info
   if (err) return 0;

   if (! size) size = thumbfilesize;                                             //  default thumb size

   if (S_ISDIR(statf.st_mode)) {                                                 //  if directory, return folder image
      thumbpxb = get_thumbnail_pixbuf(fpath,size);
      return thumbpxb;
   }

   mtime = statf.st_mtime;                                                       //  last modification time

   for (ii = nextcache; ii >= 0; ii--)                                           //  search cache
      if (fpathcache[ii] && strmatch(fpath,fpathcache[ii]) &&                    //  match on path, size, mod time
          sizecache[ii] == size && mtime == mtimecache[ii]) break;
   if (ii >= 0) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);                               //  return matching thumbnail
      return thumbpxb;
   }
   for (ii = cachesize-1; ii > nextcache; ii--)                                  //  continue search
      if (fpathcache[ii] && strmatch(fpath,fpathcache[ii]) &&
          sizecache[ii] == size && mtime == mtimecache[ii]) break;
   if (ii > nextcache) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);
      return thumbpxb;
   }

   if (size > thumbfilesize)                                                     //  get thumbnail from image file
      thumbpxb = get_thumbnail_pixbuf(fpath,size);
   else {
      bpath = image_thumbfile(fpath);                                            //  get thumbnail from thumbnail file
      if (! bpath) return 0;                                                     //  (create thumbnail file if missing)
      thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);
      zfree(bpath);
   }
   if (! thumbpxb) return 0;

   nextcache++;                                                                  //  next cache slot (oldest)
   if (nextcache == cachesize) nextcache = 0;
   ii = nextcache;
   if (fpathcache[ii]) {                                                         //  free prior occupant
      zfree(fpathcache[ii]);
      g_object_unref(pixbufcache[ii]);
      navi::thumbcache_MB -= bytescache[ii]; 
   }
   fpathcache[ii] = zstrdup(fpath);                                              //  add new occupant
   pixbufcache[ii] = gdk_pixbuf_copy(thumbpxb);                                  //  (memory not tracked in zmalloc())
   sizecache[ii] = size;
   mtimecache[ii] = mtime;
   ww = gdk_pixbuf_get_width(thumbpxb);                                          //  track total cache MB               15.08
   hh = gdk_pixbuf_get_height(thumbpxb);
   bytescache[ii] += ww * hh * 3;
   navi::thumbcache_MB += 0.000001 * bytescache[ii];
   
   while (navi::thumbcache_MB > navi::thumbcache_max) {                          //  purge cache to stay within MB limit
      ii++;
      if (ii == cachesize) ii = 0;
      if (fpathcache[ii]) {
         zfree(fpathcache[ii]);
         fpathcache[ii] = 0;
         g_object_unref(pixbufcache[ii]);
         navi::thumbcache_MB -= 0.000001 * bytescache[ii]; 
      }
   }

   return thumbpxb;                                                              //  return pixbuf to caller
}


//  Remove thumbnail from disk and memory cache.
//  This is required for deleted or renamed image files.

void image_thumbnail_delete(char *fpath)                                         //  15.12
{
   using namespace thumbnail_cache;

   int         ii, err;
   STATB       statf;
   char        *tpath;

   zthreadcrash();                                                               //  thread usage not allowed

   if (ftf) {                                                                    //  first call
      for (ii = 0; ii < cachesize; ii++)
         fpathcache[ii] = 0;                                                     //  clear cache
      ftf = 0;
      return;
   }

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

   for (ii = nextcache; ii >= 0; ii--)                                           //  search cache
      if (fpathcache[ii] && strmatch(fpath,fpathcache[ii])) break;
   if (ii < 0) {
      for (ii = cachesize-1; ii > nextcache; ii--)                               //  continue search
         if (fpathcache[ii] && strmatch(fpath,fpathcache[ii])) break;
      if (ii == nextcache) return;
   }

   zfree(fpathcache[ii]);                                                        //  free cache position
   fpathcache[ii] = 0;
   g_object_unref(pixbufcache[ii]);
   navi::thumbcache_MB -= bytescache[ii];                                        //  update cache MB used

   return;
}


//  Make a thumbnail pixbuf from the image file.
//  File can be a regular supported image file (jpeg etc.)
//    or a supported RAW file type.

PIXBUF * get_thumbnail_pixbuf(char *imagefile, int size)
{
   PIXBUF * get_thumbnail_pixbuf_raw(char *rawfile, int size);

   FTYPE          ftype;
   PIXBUF         *thumbpxb = 0;
   GError         *gerror = 0;
   static int     ftf = 1;
   static char    folderthumb[300], brokenthumb[300];

   zthreadcrash();

   if (ftf) {
      ftf = 0;
      strcpy(folderthumb,zfuncs::zicondir);                                      //  folder icon
      strcat(folderthumb,"/folder.png");
      strcpy(brokenthumb,zfuncs::zicondir);                                      //  broken thumbnail icon
      strcat(brokenthumb,"/broken.png");
   }

   ftype = image_file_type(imagefile);

   if (ftype == FDIR)                                                            //  directory file
      thumbpxb = gdk_pixbuf_new_from_file_at_size(folderthumb,size,size,&gerror);
   else if (ftype == IMAGE)                                                      //  supported image type (jpeg etc.)
      thumbpxb = gdk_pixbuf_new_from_file_at_size(imagefile,size,size,&gerror);
   else if (ftype == RAW)                                                        //  supported RAW file type
      thumbpxb = get_thumbnail_pixbuf_raw(imagefile,size);
   else return 0;                                                                //  not a supported image file type

   if (thumbpxb) return thumbpxb;

   printz("cannot make thumbnail: %s \n",imagefile);                             //  use broken image thumbnail
   if (gerror) printz(" %s \n",gerror->message);
   gerror = 0;
   thumbpxb = gdk_pixbuf_new_from_file_at_size(brokenthumb,size,size,&gerror);
   return thumbpxb;
}


//  Make a thumbnail pixbuf from a supported RAW file type

PIXBUF * get_thumbnail_pixbuf_raw(char *rawfile, int size)                       //  16.07
{
   PIXBUF   *thumbpxb = 0;
   PXB      *rawpxb;
   int      ww1, hh1, ww2, hh2;

   rawpxb = RAW_PXB_load(rawfile);
   if (! rawpxb) return 0;

   ww1 = rawpxb->ww;
   hh1 = rawpxb->hh;

   if (ww1 > hh1) {
      ww2 = size;
      hh2 = ww2 * hh1 / ww1;
   }
   else {
      hh2 = size;
      ww2 = hh2 * ww1 / hh1;
   }
   
   thumbpxb = gdk_pixbuf_scale_simple(rawpxb->pixbuf,ww2,hh2,BILINEAR);
   PXB_free(rawpxb);
   return thumbpxb;
}


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

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

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

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

   if (! clicked_file) return;

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

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


//  Monitor a gallery directory for file changes and refresh the file list.
//  Action is "start" or "stop".
//  Called only for gallerytype = directory.

void gallery_monitor(cchar *action)
{
   void gallery_changed(void *, GFile *, void *, GFileMonitorEvent);

   static GFile          *gfile_gallery = 0;                                     //  directory being monitored
   static GFileMonitor   *gallerymon = 0;
   GError                *gerror = 0;

   if (gfile_gallery) {
      g_file_monitor_cancel(gallerymon);
      g_object_unref(gallerymon);
      g_object_unref(gfile_gallery);
      gfile_gallery = 0;
   }

   if (strmatch(action,"stop")) return;
   if (gallerytype != GDIR) return;

   gfile_gallery = g_file_new_for_path(galleryname);
   gallerymon = g_file_monitor_directory(gfile_gallery,(GFileMonitorFlags) 0,0,&gerror);
   if (gallerymon)
      G_SIGNAL(gallerymon,"changed",gallery_changed,0);
   else {
      printz("monitor directory failure: %s \n",galleryname);                    //  it happens
      printz("%s\n",gerror->message);
      g_object_unref(gfile_gallery);
      gfile_gallery = 0;
   }
   return;
}

void gallery_changed(void *, GFile *, void *, GFileMonitorEvent event)
{
   if (gallerytype != GDIR) {                                                    //  precaution
      gallery_monitor("stop");
      return;
   }

   if (event == G_FILE_MONITOR_EVENT_DELETED) {
      gallery(galleryname,"init");                                               //  refresh file list
      if (FGWM == 'G') gallery(0,"paint",-1);                                    //  repaint from same position
   }

   else if (event == G_FILE_MONITOR_EVENT_CREATED) {
      gallery(galleryname,"init");
      if (FGWM == 'G') gallery(0,"paint",-1);
   }

   return;
}


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

//  Select files from the image gallery window, return list of files selected.
//  The dialog shows the list of files selected and can be edited.
//  The returned file list belongs to caller and is subject for zfree().
//  The file list EOL is marked with null.
//  The gallery() callback function is restored to caller's function.

namespace ggfnames
{
   int  dialog_event(zdialog *zd, cchar *event);
   int  find_file(cchar *imagefile);
   void insert_file(cchar *imagefile);
   void remove_file(cchar *imagefile);
   void Xclick_func(int Nth, char LR);
   int  mouseclick(GtkWidget *, GdkEventButton *event, void *);
   int  showthumb();

   GtkWidget   *drawarea = 0;
   GtkWidget   *Fwin = 0;
   int         cursorpos = 0;
   char        **initfiles = 0;
};


char ** gallery_getfiles(char **initfiles0)
{
   using namespace ggfnames;

   GdkCursor               *cursor;
   GdkWindow               *gdkwin;
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   int      line, nlines, ii;
   char     *imagefile = 0, **filelist = 0;

   zthreadcrash();

   if (initfiles0)                                                               //  initial files for dialog window
      initfiles = initfiles0;
   else initfiles = 0;

   zdialog *zd = zdialog_new(ZTX("Select Files"),Mwin,Bdone,Bcancel,null);
   zd_gallery_getfiles = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand|space=3");
   zdialog_add_widget(zd,"frame","fr11","hb1",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrwin","fr11",0,"expand");
   zdialog_add_widget(zd,"edit","files","scrwin");
   zdialog_add_widget(zd,"vbox","vb12","hb1");
   zdialog_add_widget(zd,"frame","fr12","vb12");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","delete","hb2",Bdelete,"space=8");
   zdialog_add_widget(zd,"button","insert","hb2",Binsert,"space=8");
   zdialog_add_widget(zd,"button","clear","hb2",Bclear,"space=8");
   zdialog_add_widget(zd,"button","addall","hb2",Baddall,"space=8");

   Fwin = zdialog_widget(zd,"files");                                            //  disable text wrap in file list 
   gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(Fwin),GTK_WRAP_NONE);

   gtk_widget_add_events(Fwin,GDK_BUTTON_PRESS_MASK);                            //  activate mouse clicks in file list
   G_SIGNAL(Fwin,"button-press-event",mouseclick,0);

   GtkWidget *frame = zdialog_widget(zd,"fr12");                                 //  drawing area for thumbnail image
   drawarea = gtk_drawing_area_new();
   gtk_widget_set_size_request(drawarea,256,258);                                //  increased
   gdkwin = gtk_widget_get_window(drawarea);                                     //  gtk3
   gtk_container_add(GTK_CONTAINER(frame),drawarea);

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

   cursor = gdk_cursor_new_for_display(display,GDK_TOP_LEFT_ARROW);              //  cursor for file list widget
   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(Fwin),TEXTWIN);               //  (do after window realized)
   gdk_window_set_cursor(gdkwin,cursor);
   cursorpos = 0;

   if (gallerytype == NONE) {                                                    //  if no gallery, start with
      gallery(topdirks[0],"init");                                               //    top directory
      gallery(0,"paint",0);
   }

   m_viewmode(0,"G");                                                            //  open gallery window
   if (initfiles) zdialog_send_event(zd,"initfiles");                            //  stuff initial files
   zdialog_wait(zd);                                                             //  wait for dialog completion

   if (zd->zstat != 1) {                                                         //  cancelled
      zdialog_free(zd);                                                          //  kill dialog
      zd_gallery_getfiles = 0;
      return 0;
   }

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
   nlines = gtk_text_buffer_get_line_count(textBuff);

   filelist = (char **) zmalloc((nlines+1) * sizeof(char *));

   for (ii = line = 0; line < nlines; line++)                                    //  get list of files from dialog
   {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                    //  iter at line start
      iter2 = iter1;
      gtk_text_iter_forward_to_line_end(&iter2);
      imagefile = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);            //  get line of text
      if (imagefile && *imagefile == '/') {
         filelist[ii] = zstrdup(imagefile);                                      //  >> next file in list
         zfree(imagefile);
         ii++;
      }
   }
   filelist[ii] = 0;                                                             //  mark EOL

   zdialog_free(zd);                                                             //  kill dialog
   zd_gallery_getfiles = 0;

   if (! ii) {
      zfree(filelist);                                                           //  file list is empty
      return 0;
   }

   return filelist;                                                              //  return file list
}


//  gallery getfiles dialog event function

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

   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   char           *ftemp, *imagefile;
   static char    *deletedfiles[100];                                            //  last 100 files deleted
   static int     Ndeleted = 0;
   int            ii, line, Nth;
   FTYPE          ftype;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"focus")) showthumb();                                     //  GTK bug? thumbnail disappears

   if (strmatch(event,"initfiles"))                                              //  insert all files in initial list
   {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));

      for (ii = 0; initfiles[ii]; ii++)
      {
         imagefile = initfiles[ii];
         ftype = image_file_type(imagefile);                                     //  must be image or RAW file
         if (ftype != IMAGE && ftype != RAW) continue;
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,"\n",1);                         //  insert new blank line
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,imagefile,-1);                   //  insert image file
         cursorpos++;                                                            //  advance cursor position
      }
   }

   if (strmatch(event,"delete"))                                                 //  delete file at cursor position
   {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
      line = cursorpos;
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                    //  iter at line start
      iter2 = iter1;
      gtk_text_iter_forward_to_line_end(&iter2);                                 //  iter at line end

      ftemp = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);                //  get selected file
      if (*ftemp != '/') {
         zfree(ftemp);
         return 1;
      }

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

      deletedfiles[Ndeleted] = zstrdup(ftemp);                                   //  save deleted file for poss. insert
      Ndeleted++;
      zfree(ftemp);

      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                            //  delete file text
      gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line+1);
      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                            //  delete empty line (\n)

      showthumb();                                                               //  thumbnail = next file
   }

   if (strmatch(event,"insert"))                                                 //  insert first deleted file
   {                                                                             //    at current cursor position
      if (! Ndeleted) return 1;
      insert_file(deletedfiles[0]);
      zfree(deletedfiles[0]);                                                    //  remove file from the deleted list
      for (ii = 0; ii < Ndeleted-1; ii++)
         deletedfiles[ii] = deletedfiles[ii+1];
      Ndeleted--;
   }

   if (strmatch(event,"clear")) {                                                //  clear all files
      gtk_text_view_set_buffer(GTK_TEXT_VIEW(Fwin),null);
      cursorpos = 0;
   }

   if (strmatch(event,"addall"))                                                 //  insert all files in image gallery
   {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
      Nth = 0;

      while (true)
      {
         imagefile = gallery(0,"find",Nth);                                      //  get first or next file
         if (! imagefile) break;
         Nth++;
         ftype = image_file_type(imagefile);                                     //  must be image or RAW file
         if (ftype != IMAGE && ftype != RAW) continue;
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,"\n",1);                         //  insert new blank line
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,imagefile,-1);                   //  insert image file
         zfree(imagefile);
         cursorpos++;                                                            //  advance cursor position
      }
   }

   return 1;
}


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

int ggfnames::find_file(cchar *imagefile)
{
   using namespace ggfnames;

   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   char           *ftemp;
   int            line, last = -1, more;

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));

   for (line = 0; ; line++)
   {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                    //  iter at line start
      iter2 = iter1;
      more = gtk_text_iter_forward_to_line_end(&iter2);                          //  iter at line end
      ftemp = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);                //  included text
      if (strmatch(ftemp,imagefile)) last = line;                                //  remember last entry found
      zfree(ftemp);
      if (! more) break;
   }

   return last;
}


//  add image file to list at current cursor position, set thumbnail = file

void ggfnames::insert_file(cchar *imagefile)
{
   using namespace ggfnames;

   GtkTextIter    iter;
   GtkTextBuffer  *textBuff;

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,cursorpos);
   gtk_text_buffer_insert(textBuff,&iter,imagefile,-1);                          //  insert image file
   gtk_text_buffer_insert(textBuff,&iter,"\n",1);                                //  insert new line
   gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(Fwin),&iter,0,0,0,0);              //  insure visible

   showthumb();                                                                  //  update thumbnail
   cursorpos++;                                                                  //  advance cursor position

   return;
}


//  remove image file at last position found, set thumbnail = next

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

   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   int            line;

   line = find_file(imagefile);
   if (line < 0) return;

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                       //  iter at line start
   iter2 = iter1;
   gtk_text_iter_forward_to_line_end(&iter2);                                    //  iter at line end
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                               //  delete file text
   gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line+1);                     //  next line
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                               //  delete empty line (\n)

   showthumb();                                                                  //  thumbnail = curr. position
   if (cursorpos > 0) cursorpos--;                                               //  backup cursor position
   return;
}


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

void gallery_getfiles_Lclick_func(int Nth)
{
   ggfnames::Xclick_func(Nth,'L');
   zfree(clicked_file);                                                          //  bugfix                             16.06
   clicked_file = 0;
   return;
}

void gallery_getfiles_Rclick_func(int Nth)
{
   ggfnames::Xclick_func(Nth,'R');
   zfree(clicked_file);
   clicked_file = 0;
   return;
}

void ggfnames::Xclick_func(int Nth, char LR)
{
   using namespace ggfnames;

   FTYPE          ftype;
   static int     pNth = -1;                                                     //  previously clicked file
   char           *imagefile;
   int            control, shift;
   int            line, nn, incr;

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

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

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

   control = shift = 0;                                                          //  left click, select
   if (KBcontrolkey) control = 1;
   if (KBshiftkey) shift = 1;

   if (! control && ! shift)                                                     //  no control or shift keys
   {
      pNth = Nth;                                                                //  possible start of range
      insert_file(imagefile);                                                    //  insert file at current position
      zfree(imagefile);
      return;
   }

   if (control && ! shift)                                                       //  control key
   {
      pNth = -1;                                                                 //  add or remove single file
      line = find_file(imagefile);
      if (line < 0) insert_file(imagefile);                                      //  not found, add
      else remove_file(imagefile);                                               //  found, remove
      zfree(imagefile);
      return;
   }

   if (! control && shift)                                                       //  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,"find",nn);
         if (! imagefile) continue;
         ftype = image_file_type(imagefile);                                     //  only image and RAW files
         if (ftype != IMAGE && ftype != RAW) {
            zfree(imagefile);
            continue;
         }
         insert_file(imagefile);
         zfree(imagefile);
      }
      pNth = -1;                                                                 //  no prior
      return;
   }

   if (control && shift)                                                         //  both control and shift keys
      return;                                                                    //  ignore

   return;
}


//  process mouse click in files window:
//  set new cursor position and set thumbnail = clicked file

int ggfnames::mouseclick(GtkWidget *widget, GdkEventButton *event, void *)
{
   using namespace ggfnames;
   int            mpx, mpy, tbx, tby;
   GtkTextIter    iter1;
   
   #define VIEW GTK_TEXT_VIEW
   #define TEXT GTK_TEXT_WINDOW_TEXT
   
   if (event->type != GDK_BUTTON_PRESS) return 0;                                //  reworked                           15.10
   mpx = int(event->x);
   mpy = int(event->y);
   gtk_text_view_window_to_buffer_coords(VIEW(widget),TEXT,mpx,mpy,&tbx,&tby);
   gtk_text_view_get_iter_at_location(VIEW(widget),&iter1,tbx,tby);
   cursorpos = gtk_text_iter_get_line(&iter1);                                   //  clicked line
   showthumb();                                                                  //  show thumbnail image
   return 0;
}


//  show thumbnail for file at current cursor position

int ggfnames::showthumb()
{
   using namespace ggfnames;

   int            line;
   char           *imagefile;
   GdkWindow      *gdkwin;
   cairo_t        *cr;
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   PIXBUF         *thumbnail = 0;

   gtk_widget_grab_focus(drawarea);                                              //  GTK bug?
   zmainloop();                                                                  //  stop thumbnail disappearing

   gdkwin = gtk_widget_get_window(drawarea);
   cr = gdk_cairo_create(gdkwin);

   line = cursorpos;
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(Fwin));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line);                       //  iter at line start
   iter2 = iter1;
   gtk_text_iter_forward_to_line_end(&iter2);                                    //  iter at line end

   imagefile = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);               //  get selected file
   if (*imagefile != '/') {
      zfree(imagefile);
      cairo_set_source_rgb(cr,1,1,1);                                            //  white background
      cairo_paint(cr);
      return 0;
   }

   thumbnail = image_thumbnail(imagefile,256);                                   //  get thumbnail
   zfree(imagefile);

   if (thumbnail) {
      cairo_set_source_rgb(cr,1,1,1);                                            //  white background
      cairo_paint(cr);
      gdk_cairo_set_source_pixbuf(cr,thumbnail,0,0);                             //  paint thumbnail
      cairo_paint(cr);
      g_object_unref(thumbnail);
   }

   return 0;
}


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

//  edit bookmarks

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

void bookmarks_load();
void bookmarks_listclick(GtkWidget *widget, int line, int pos);
int  bookmarks_dialog_event(zdialog *zd, cchar *event);
void bookmarks_refresh();


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

   zdialog     *zd;
   cchar       *bmk_add = ZTX("Click list position. Click thumbnail to add.");

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

***/

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

   zd = zdialog_new(ZTX("Edit Bookmarks"),Mwin,Bdone,null);
   zd_edit_bookmarks = zd;
   zdialog_add_widget(zd,"hbox","hbtip","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtip","hbtip",bmk_add,"space=5");
   zdialog_add_widget(zd,"hbox","hbbmk","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"frame","frbmk","hbbmk",0,"space=5|expand");
   zdialog_add_widget(zd,"text","bmklist","frbmk",0,"expand");
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=5");
   zdialog_add_widget(zd,"entry","bmkname","hbname",0,"space=5|size=30");
   zdialog_add_widget(zd,"button","rename","hbname",Brename,"space=5");
   zdialog_add_widget(zd,"button","delete","hbname",Bdelete,"space=5");

   bmkswidget = zdialog_widget(zd,"bmklist");                                    //  connect mouse to bookmark list
   textwidget_set_clickfunc(bmkswidget,bookmarks_listclick);

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

   zdialog_resize(zd,300,300);
   zdialog_run(zd,bookmarks_dialog_event,"save");                                //  run dialog, parallel

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


//  load bookmarks list from bookmarks file

void bookmarks_load()                                                            //  15.05
{
   using namespace bookmarknames;

   int         err;
   char        buff[XFCC], bmkfile[200];
   char        *pp, *pp2;
   FILE        *fid;
   STATB       stbuff;

   Nbmks = 0;
   err = locale_filespec("user","bookmarks",bmkfile);                            //  read bookmarks file
   if (! err) {
      fid = fopen(bmkfile,"r");
      if (fid) {
         while (true) {
            pp = fgets_trim(buff,XFCC,fid,1);                                    //  next bookmark rec.
            if (! pp) break;
            if (strlen(pp) < 40) continue;
            pp2 = strchr(pp+32,'/');                                             //  verify bookmark                    15.05
            if (! pp2) continue;
            err = stat(pp2,&stbuff);
            if (err) continue;
            bookmarks[Nbmks] = zstrdup(pp);                                      //  fill bookmark list
            if (++Nbmks == maxbmks) break;
         }
         fclose(fid);
      }
   }
   
   bmkposn = Nbmks;                                                              //  next free position                 15.05
   return;
}


//  mouse click function to select existing bookmark from list

void bookmarks_listclick(GtkWidget *widget, int line, int pos)
{
   using namespace bookmarknames;

   char     bookmarkname[32];

   if (! zd_edit_bookmarks) return;
   if (line < 0) line = 0;
   if (line > Nbmks-1) line = Nbmks-1;
   bmkposn = line;
   strncpy0(bookmarkname,bookmarks[bmkposn],31);
   strTrim(bookmarkname);
   zdialog_stuff(zd_edit_bookmarks,"bmkname",bookmarkname);
   return;
}


//  mouse click function to receive clicked bookmarks

void bookmarks_Lclick_func(int Nth)
{
   using namespace bookmarknames;

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

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

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

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

   if (Nbmks == maxbmks) {                                                       //  if list full, remove first
      zfree(bookmarks[0]);
      Nbmks--;
      for (int ii = 0; ii < Nbmks; ii++)
         bookmarks[ii] = bookmarks[ii+1];
   }
   
   if (Nbmks == 0) bmkposn = 0;                                                  //  1st bookmark --> 0
   else bmkposn++;                                                               //  else clicked position + 1          15.05
   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 bookmarks_dialog_event(zdialog *zd, cchar *event)
{
   using namespace bookmarknames;

   char        bmkfile[200];
   char        bookmarkname[32];
   FILE        *fid;
   int         cc;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   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;
      strncpy(bookmarks[bmkposn],bookmarkname,30);                               //  replace name in bookmarks list
      bookmarks_refresh();                                                       //  update bookmarks list in dialog
   }

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]

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

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

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

   zdialog_free(zd);
   zd_edit_bookmarks = 0;
   return 1;
}


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

void bookmarks_refresh()
{
   using namespace bookmarknames;
   
   char     bookmarkline[XFCC+32];
   char     blanks[33] = "                                ";
   int      cc;

   if (! zd_edit_bookmarks && ! zd_goto_bookmark) return;
   wclear(bmkswidget);                                                           //  clear bookmarks list
   for (int ii = 0; ii < Nbmks; ii++) {                                          //  write bookmarks list
      strncpy0(bookmarkline,bookmarks[ii],31);
      cc = utf8len(bookmarkline);                                                //  compensate multibyte chars.        15.05
      strncat(bookmarkline,blanks,32-cc);
      strcat(bookmarkline,bookmarks[ii]+32);
      wprintf(bmkswidget,0,bookmarkline);
      wprintf(bmkswidget,0,"\n");
   }
   return;
}


//  select a bookmark and jump gallery to selected bookmark
//  connected to gallery menu "GoTo" button

void m_goto_bookmark(GtkWidget *, cchar *)
{
   using namespace bookmarknames;

   void goto_bookmark_listclick(GtkWidget *widget, int line, int pos);
   int  goto_bookmark_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;

/***
                  Gallery Button: [GoTo]
          _______________________________________________
         |             Go to Bookmark                    |
         |-----------------------------------------------|
         | bookmarkname1      /topdir/.../filename1.jpg  |
         | bookmarkname2      /topdir/.../filename2.jpg  |
         | bookmarkname3      /topdir/.../filename3.jpg  |
         | bookmarkname4      /topdir/.../filename4.jpg  |
         | bookmarkname5      /topdir/.../filename5.jpg  |
         | bookmarkname6      /topdir/.../filename6.jpg  |
         |                                               |
         |                              [edit bookmarks] |
         |_______________________________________________|
***/

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

   zd = zdialog_new(ZTX("Go To Bookmark"),Mwin,ZTX("Edit Bookmarks"),null);
   zd_goto_bookmark = zd;

   zdialog_add_widget(zd,"hbox","hbbmk","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"frame","frbmk","hbbmk",0,"space=5|expand");
   zdialog_add_widget(zd,"text","bmklist","frbmk",0,"expand");

   bmkswidget = zdialog_widget(zd,"bmklist");                                    //  connect mouse to bookmark list
   textwidget_set_clickfunc(bmkswidget,goto_bookmark_listclick);

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

   zdialog_resize(zd,300,0);
   zdialog_run(zd,goto_bookmark_dialog_event,"mouse");                           //  run dialog
   return;
}


//  dialog event and completion function

int goto_bookmark_dialog_event(zdialog *zd, cchar *event)
{
   using namespace bookmarknames;
   
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   int zstat = zd->zstat;
   if (! zstat) return 1;                                                        //  wait for completion
   zdialog_free(zd);
   zd_goto_bookmark = 0;
   if (zstat == 1) m_edit_bookmarks(0,0);                                        //  [edit bookmarks] button
   return 1;
}


//  mouse click function to receive clicked bookmarks

void goto_bookmark_listclick(GtkWidget *widget, int line, int pos)
{
   using namespace bookmarknames;

   char     *file;
   int      err;
   STATB    sbuff;

   if (checkpend("all")) return;

   if (! zd_goto_bookmark) return;
   zdialog_free(zd_goto_bookmark);                                               //  kill dialog
   zd_goto_bookmark = 0;

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

   gallery(file,"init");                                                         //  go to gallery and file position
   m_viewmode(0,"G");
   gallery(file,"paint");

   return;
}



