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

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   Contact: kornelix2@googlemail.com
   
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   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/.

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

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


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

   Fotoxx image edit - functions for navigation of image files

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


   GtkWidget      *wing = 0, *vboxx, *scrwing, *layout;                    //  image gallery and drawing windows
   GtkWidget      *pwing = 0;                                              //  parent window
   GtkAdjustment  *scrollbar;
   GdkGC          *gdkgc2 = 0;                                             //  graphics context

namespace image_navi 
{
   #define thumbnail_cachesize 10000                                       //  max thumbnails cached in memory
   #define imagetypes ".jpeg .jpg .png .tif .tiff .bmp .gif .svg .xpm"     //  supported image file types

   #define TEXTWIN GTK_TEXT_WINDOW_TEXT                                    //  GDK window of GTK text view
   #define NODITHER GDK_RGB_DITHER_NONE
   #define SCROLLWIN GTK_SCROLLED_WINDOW
   #define NEVER GTK_POLICY_NEVER
   #define ALWAYS GTK_POLICY_ALWAYS
   #define interp GDK_INTERP_BILINEAR
   #define colorspace GDK_COLORSPACE_RGB

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

   char        *galleryname = 0;                                           //  image directory or file list name
   int         gallerytype = 0;                                            //  1/2/3 = directory/file-list/metadata
   int         nfiles = 0;                                                 //  total file count (incl. subdirks)
   int         nimages = 0;                                                //  image file count
   char        **flist = 0;                                                //  image file list
   char        **mdlist = 0;                                               //  corresp. metadata

   typedef void ufunc(int Nth, int button);                                //  callback function for clicked image
   ufunc       *userfunc;

   int         xwinW = 1000, xwinH = 700;                                  //  gallery window initial size
   int         xwinX, xwinY;                                               //  gallery window initial position
   int         thumbsize = thumbfilesize;                                  //  thumbnail image <= thumbnail file
   int         thumbW, thumbH;                                             //  gallery window thumbnail cell size
   int         xrows, xcols;                                               //  gallery window thumbnail rows, cols
   int         margin = 5;                                                 //  cell margin from left and top edge
   int         genthumbs = 0;                                              //  count newly generated thumbnails
   int         scrollposn;                                                 //  gallery window scroll position
   int         maxscroll;                                                  //  max. scroll position
   int         fpresent = 0;                                               //  force gallery window to z-top
   int         targposn = 0;                                               //  scroll-to file position (Nth)
   
   //  private functions
   int    gallery_paint(GtkWidget *, GdkEventExpose *);                    //  gallery window paint function
   int    gallery_paint_metadata(GdkEventExpose *expose);                  //   "" for metadata report
   void   draw_text(GtkWidget *win, char *text, int x, int y, int ww);     //  draw text in gallery window
   void   gallery_destroy();                                               //  gallery window destroy event function
   void   menufuncx(GtkWidget *win, cchar *menu);                          //  function for gallery window buttons
   void   mouse_xevent(GtkWidget *, GdkEventButton *, void *);             //  gallery window mouse event function
	int    KBxpress(GtkWidget *, GdkEventKey *, void *);                    //  gallery window key press event func.
	int    KBxrelease(GtkWidget *, GdkEventKey *, void *);                  //  gallery window key release event
   char * image_navigate(cchar *filez, cchar *action, int Nth = 0);        //  image file list setup and navigate
   int    image_fcomp(cchar *file1, cchar *file2);                         //  file name compare (special sort)
}


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

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

   Make window scrolling window of thumbnails for a list of files
   Handle window buttons (up row, down page, open file, etc.)
   Call ufunc() when thumbnail image is clicked

   char * image_gallery(cchar *filez, cchar *action, int Nth, 
                         void ufunc(int Nth, int button), GtkWidget *parent)

   filez: image file or directory of image files 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
            paint1:  create or refresh gallery window, anchor = Nth file
            paint2:  refresh gallery window if present, anchor = Nth file
            close    close gallery window
   
   Nth: file to return (action = find) or file to scroll-to (action = paint)

   void ufunc(int Nth, int button):
      - returns Nth of clicked thumbnail (0 to last)
      - returns -1 if gallery window is closed
      - returns -2 if key F1 is pressed (for context help)
      - button is mouse button used on clicked thumbnail
      
   parent: optional parent window
           if present, gallery window will overlay the parent window
           (window placement can be undone by the user)

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

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

char * image_gallery(cchar *filez, cchar *action, int Nth, 
                     void ufunc(int Nth, int button), GtkWidget *parent)
{
   using namespace image_navi;

   GtkWidget      *tbarx;

   zthreadcrash();                                                         //  thread usage not allowed

   if (ufunc) userfunc = ufunc;                                            //  save callback function
   if (parent) pwing = parent;                                             //  save parent window

   if (strstr("init initF sort insert delete find",action))
      return image_navigate(filez,action,Nth);                             //  create or navigate image file list
      
   if (strEqu(action,"close")) {
      if (wing) gtk_widget_destroy(wing);
      return 0;
   }
   
   if (! strnEqu(action,"paint",5))                                        //  must be paint1 or paint2
      zappcrash("image_gallery action: %s",action);

   if (strEqu(action,"paint2") && ! wing) return 0;                        //  refresh but window not active
   if (strEqu(action,"paint1")) fpresent++;                                //  bring window to z-top

   if (Nth >= 0) targposn = Nth;                                           //  scroll-to file position
   else if (filez) targposn = image_gallery_position(filez,0);             //  or use filez if present
   if (targposn > nfiles-1) targposn = nfiles-1;                           //  (-1 for no change)

   if (wing) {                                                             //  repaint existing gallery window
      gallery_paint(0,0);
      return 0;
   }

   wing = gtk_window_new(GTK_WINDOW_TOPLEVEL);                             //  create new gallery window
   
   if (pwing) {
      gtk_window_get_size(GTK_WINDOW(pwing),&xwinW,&xwinH);                //  overlay parent window 
      gtk_window_get_position(GTK_WINDOW(pwing),&xwinX,&xwinY);
      gtk_window_set_default_size(GTK_WINDOW(wing),xwinW,xwinH);
      gtk_window_move(GTK_WINDOW(wing),xwinX,xwinY);
   }
   else {
      gtk_window_set_default_size(GTK_WINDOW(wing),xwinW,xwinH+56);        //  + toolbar size to stop shrinkage
      gtk_window_set_position(GTK_WINDOW(wing),GTK_WIN_POS_MOUSE); 
   }

   vboxx = gtk_vbox_new(0,0);                                              //  vertical packing box
   gtk_container_add(GTK_CONTAINER(wing),vboxx);                           //  add to main window

   tbarx = create_toolbar(vboxx,24);                                       //  add toolbar and buttons 
   gtk_toolbar_set_style(GTK_TOOLBAR(tbarx),GTK_TOOLBAR_BOTH);

   add_toolbar_button(tbarx, ZTX("bigger"), ZTX("increase thumbnail size"), "gtk-zoom-in", menufuncx);
   add_toolbar_button(tbarx, ZTX("smaller"), ZTX("reduce thumbnail size"), "gtk-zoom-out", menufuncx);
   add_toolbar_button(tbarx, ZTX("parent"), ZTX("parent directory"), "folder.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("first page"), ZTX("jump to first file"), "first-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev page"), ZTX("previous page"), "prev-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev row"), ZTX("previous row"), "prev-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next row"), ZTX("next row"), "next-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next page"), ZTX("ttip::next page"), "next-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("last page"), ZTX("jump to last file"), "last-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("close"), ZTX("close image gallery"), "gtk-close", menufuncx);

   gtk_toolbar_set_style(GTK_TOOLBAR(tbarx),GTK_TOOLBAR_BOTH);             //  set toolbar style         v.12.01
   if (strEqu(tbar_style,"icons"))
      gtk_toolbar_set_style(GTK_TOOLBAR(tbarx),GTK_TOOLBAR_ICONS);
   if (strEqu(tbar_style,"text"))
      gtk_toolbar_set_style(GTK_TOOLBAR(tbarx),GTK_TOOLBAR_TEXT);
   
   scrwing = gtk_scrolled_window_new(0,0);                                 //  create scrolled window
   gtk_container_add(GTK_CONTAINER(vboxx),scrwing);                        //  add to main window
   layout = gtk_layout_new(0,0);                                           //  create drawing window
   gtk_layout_set_size(GTK_LAYOUT(layout),xwinW,xwinH);                    //  initial size              v.12.01
   gtk_container_add(GTK_CONTAINER(scrwing),layout);                       //  add to scrolled window
   gtk_scrolled_window_set_policy(SCROLLWIN(scrwing),NEVER,ALWAYS);        //  vertical scroll bar
   scrollbar = gtk_layout_get_vadjustment(GTK_LAYOUT(layout));

   G_SIGNAL(wing,"destroy",gallery_destroy,0);                             //  connect window events
   G_SIGNAL(layout,"expose-event",gallery_paint,0);
   gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK);                    //  connect mouse events
   G_SIGNAL(layout,"button-press-event",mouse_xevent,0);
   G_SIGNAL(wing,"key-press-event",KBxpress,0);                           	//  connect KB events
   G_SIGNAL(wing,"key-release-event",KBxrelease,0);

   gtk_widget_show_all(wing);                                              //  show all widgets
   gdkgc2 = gdk_gc_new(layout->window);                                    //  initz. graphics context
   gallery_paint(0,0);                                                     //  repaint
   gtk_window_present(GTK_WINDOW(wing));                                   //  bring gallery window to top
   zmainloop();

   return 0;
}


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

int image_navi::gallery_paint(GtkWidget *, GdkEventExpose *expose)
{
   using namespace image_navi;

   GdkPixbuf         *pxbT;
   GdkRectangle      rect;
   int               ii, nrows, row1, row2;
   int               currscrollposn;
   int               layoutW, layoutH;
   int               expy1, expy2, row, col;
   int               thumx, thumy, orgx, orgy, ww, hh;
   int               stat, popup = 0;
   char              *pp, *fname;
   char              wintitle[200];
   char              filep0;
   
   if (gallerytype == 3) {
      stat = gallery_paint_metadata(expose);                               //  v.12.01
      return stat;
   }
   
   xwinW = layout->allocation.width;                                       //  curr. gallery window size
   xwinH = layout->allocation.height;
   
   thumbW = thumbsize + 10;                                                //  thumbnail cell size
   thumbH = thumbsize + 30;

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

   xrows = int(0.2 + 1.0 * xwinH / thumbH);                                //  get thumbnail rows and cols that
   xcols = int(0.3 + 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;                                               //  bugfix

   layoutW = xcols * thumbW + margin + 10;                                 //  layout size for entire file list
   layoutH = (nrows+1) * thumbH + margin;                                  //  last row + 1
   if (layoutH < xwinH) layoutH = xwinH;                                   //  bugfix
   gtk_layout_set_size(GTK_LAYOUT(layout),layoutW,layoutH);

   maxscroll = layoutH - xwinH;                                            //  scroll to end of layout
   if (maxscroll < 0) maxscroll = 0;

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

   currscrollposn = gtk_adjustment_get_value(scrollbar);                   //  current scroll position
   scrollposn = currscrollposn;

   if (targposn >= 0) {
      scrollposn = targposn / xcols * thumbH;                              //  set initial scroll position from 
      if (scrollposn > maxscroll) scrollposn = maxscroll;                  //   target file position if defined,
      scrollposn = scrollposn / thumbH * thumbH;                           //    then disable it for local control 
   }                                                                       //     of scrolling from gallery window
   targposn = -1;                                                          

   if (scrollposn > maxscroll) scrollposn = maxscroll;                     //  bugfix 

   if (scrollposn != currscrollposn)
      gtk_adjustment_set_value(scrollbar,scrollposn);                      //  will cause re-entrance

   if (! expose)                                                           //  initial window or navigation button
   {
      snprintf(wintitle,199,"%s  %d files",galleryname,nfiles);            //  window title: gallery name and file count
      gtk_window_set_title(GTK_WINDOW(wing),wintitle);
      gdk_window_invalidate_rect(wing->window,0,1);                        //  will cause re-entrance
      if (fpresent) gtk_window_present(GTK_WINDOW(wing));                  //  bring window to top
      zmainloop();
      fpresent = 0;
      return 0;
   }

   rect = expose->area;                                                    //  exposed area to refresh
   expy1 = rect.y;
   expy2 = expy1 + rect.height;
   row1 = expy1 / thumbH;
   row2 = expy2 / thumbH;

   ii = row1 * xcols;                                                      //  1st image file in top row
   if (ii >= nfiles) return 1;

   for (row = row1; row <= row2; row++)                                    //  draw file thumbnails
   for (col = 0; col < xcols; col++)
   {
      if (genthumbs == 1 && ! popup) {                                     //  inform user of delay
         write_popup_text("open","please wait",200,10,wing);
         write_popup_text("write","\n generating thumbnails");             //  1st pass gtk bug, no text ////
         genthumbs = 2;
         popup = 1;
      }
      if (genthumbs < 5) zmainloop();                                      //  mysterious, necessary     ////

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

      filep0 = *flist[ii];
      *flist[ii] = '/';                                                    //  get filespec, replace ! with /

      if (thumy < expy2 && thumy+14 > expy1) {                             //  if in exposed area,
         pp = (char *) strrchr(flist[ii],'/');                             //    draw file name 
         if (pp) fname = pp + 1;
         else fname = flist[ii];
         draw_text(layout,fname,thumx,thumy,thumbW-5);
      }

      if (thumbsize)                                                       //  zero >> list view
         pxbT = image_thumbnail(flist[ii],thumbsize);                      //  get thumbnail
      else pxbT = 0;

      *flist[ii] = filep0;                                                 //  restore posn. 0   bugfix  v.12.01

      if (pxbT) {
         thumy = thumy + 16;                                               //  thumbnail 16 pixels down from text
         orgx = 0;
         orgy = 0;
         ww = gdk_pixbuf_get_width(pxbT);
         hh = gdk_pixbuf_get_height(pxbT);

         if (thumy < expy1) {
            orgy = orgy + (expy1 - thumy);
            hh = hh - (expy1 - thumy);
         }
         if (thumy + orgy + hh > expy2)
            hh = hh - (thumy + orgy + hh - expy2);
         
         if (orgy >= 0 && hh > 0)
            gdk_draw_pixbuf(GTK_LAYOUT(layout)->bin_window,0,pxbT,
                             orgx,orgy,thumx+orgx,thumy+orgy,ww,hh,NODITHER,0,0); 
         g_object_unref(pxbT);
      }

      if (++ii == nfiles) goto thumbsdone;                                 //  EOL
   }

thumbsdone:

   if (popup) {
      write_popup_text("close");
      genthumbs = 0;
   }

   return 1;
}


//  expose event private function for metadata gallery report
//  paint gallery window - draw all thumbnail images that can fit

int image_navi::gallery_paint_metadata(GdkEventExpose *expose)             //  v.12.01
{
   using namespace image_navi;

   GdkPixbuf         *pxbT;
   GdkRectangle      rect;
   int               ii, nrows, row1, row2;
   int               currscrollposn;
   int               layoutW, layoutH;
   int               expy1, expy2, row, col;
   int               thumx, thumy, orgx, orgy, ww, hh;
   int               popup = 0, textww;
   char              wintitle[200];
   
   xwinW = layout->allocation.width;                                       //  curr. gallery window size
   xwinH = layout->allocation.height;
   
   if (thumbsize < 64) thumbsize = 64;
   thumbW = thumbsize + 10;                                                //  thumbnail layout size
   thumbH = thumbsize + 20;

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

   layoutW = xwinW;                                                        //  layout size for entire file list
   if (layoutW < 800) layoutW = 800;
   layoutH = (nrows+1) * thumbH + margin;                                  //  last row + 1
   if (layoutH < xwinH) layoutH = xwinH;
   gtk_layout_set_size(GTK_LAYOUT(layout),layoutW,layoutH);

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

   maxscroll = layoutH - xwinH;                                            //  scroll to end of layout
   if (maxscroll < 0) maxscroll = 0;

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

   currscrollposn = gtk_adjustment_get_value(scrollbar);                   //  current scroll position
   scrollposn = currscrollposn;

   if (targposn >= 0) {
      scrollposn = targposn / xcols * thumbH;                              //  set initial scroll position from 
      if (scrollposn > maxscroll) scrollposn = maxscroll;                  //   target file position if defined,
      scrollposn = scrollposn / thumbH * thumbH;                           //    then disable it for local control 
   }                                                                       //     of scrolling from gallery window
   targposn = -1;                                                          

   if (scrollposn > maxscroll) scrollposn = maxscroll;

   if (scrollposn != currscrollposn)
      gtk_adjustment_set_value(scrollbar,scrollposn);                      //  will cause re-entrance

   if (! expose)                                                           //  initial window or navigation button
   {
      snprintf(wintitle,199,"%s  %d files",galleryname,nfiles);            //  window title: gallery name and file count
      gtk_window_set_title(GTK_WINDOW(wing),wintitle);
      gdk_window_invalidate_rect(wing->window,0,1);                        //  will cause re-entrance
      if (fpresent) gtk_window_present(GTK_WINDOW(wing));                  //  bring window to top
      zmainloop();
      fpresent = 0;
      return 0;
   }
   
   rect = expose->area;                                                    //  exposed area to refresh
   expy1 = rect.y;
   expy2 = expy1 + rect.height;
   row1 = expy1 / thumbH;
   row2 = expy2 / thumbH;

   ii = row1 * xcols;                                                      //  1st image file in top row
   if (ii >= nfiles) return 1;

   for (row = row1; row <= row2; row++)                                    //  draw file thumbnails
   for (col = 0; col < xcols; col++)
   {
      if (genthumbs == 1 && ! popup) {                                     //  inform user of delay
         write_popup_text("open","please wait",200,10,wing);
         write_popup_text("write","\n generating thumbnails");             //  1st pass gtk bug, no text ////
         genthumbs = 2;
         popup = 1;
      }
      if (genthumbs < 5) zmainloop();                                      //  mysterious, necessary     ////

      thumx = col * thumbW + margin;                                       //  upper left corner in window space
      thumy = row * thumbH + margin;
      
      pxbT = image_thumbnail(flist[ii],thumbsize);                         //  get thumbnail

      if (pxbT) {
         orgx = 0;
         orgy = 0;
         ww = gdk_pixbuf_get_width(pxbT);
         hh = gdk_pixbuf_get_height(pxbT);

         if (thumy < expy1) {                                              //  position in layout
            orgy = orgy + (expy1 - thumy);
            hh = hh - (expy1 - thumy);
         }
         if (thumy + orgy + hh > expy2)
            hh = hh - (thumy + orgy + hh - expy2);
         
         if (orgy >= 0 && hh > 0)                                          //  write in layout
            gdk_draw_pixbuf(GTK_LAYOUT(layout)->bin_window,0,pxbT,
                             orgx,orgy,thumx+orgx,thumy+orgy,ww,hh,NODITHER,0,0); 
         g_object_unref(pxbT);
      }

      draw_text(layout,flist[ii],thumbW+margin,thumy,textww);              //  write filespec to right of thumbnail
      
      if (mdlist && mdlist[ii])                                            //  write metadata if present
         draw_text(layout,mdlist[ii],thumbW+margin,thumy+20,textww);
      
      if (++ii == nfiles) goto thumbsdone;                                 //  EOL
   }

thumbsdone:

   if (popup) {
      write_popup_text("close");
      genthumbs = 0;
   }

   return 1;
}


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

void image_navi::draw_text(GtkWidget *win, char *text, int x, int y, int ww)
{
   using namespace image_navi;

   static PangoFontDescription   *pfont = 0;
   static PangoLayout            *playout = 0;
   
   int            nn;
   static int     thumbsize2 = 0;
   static char    thumbfont[12] = "";
   
   if (thumbsize != thumbsize2) {                                          //  scale file name font to thumbnail size
      thumbsize2 = thumbsize;
      nn = 7 + thumbsize / 128;                                            //  font size from 7 to 10
      if (thumbsize == 0) nn = 9;                                          //  list view, use 9
      sprintf(thumbfont,"sans %d",nn);
      pfont = pango_font_description_from_string(thumbfont);
      playout = gtk_widget_create_pango_layout(win,0);
      pango_layout_set_font_description(playout,pfont);
   }

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

   gdk_draw_layout(GTK_LAYOUT(win)->bin_window,gdkgc2,x,y,playout);
   return;
}


//  private function
//  gallery window destroy event - mark window not active.

void image_navi::gallery_destroy()
{
   using namespace image_navi;
   
   wing = 0;                                                               //  no window
   if (userfunc) userfunc(-1,0);                                           //  tell caller
   return;
}


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

void image_navi::menufuncx(GtkWidget *win, cchar *menu)
{
   using namespace image_navi;

   int         ii, yn, Gwarn, scrollp;
   char        *filez, *pp;
      
   if (strEqu(menu,ZTX("close"))) {                                        //  close image gallery window
      gtk_widget_destroy(wing);                                            //  destroy event function calls userfunc(-1)
      return;
   }

   if (strEqu(menu,ZTX("bigger")))  {                                      //  next bigger thumbnail size
      for (ii = 0; ii < thumbxx; ii++) 
         if (thumbsize == thumbx[ii]) break;
      if (ii == 0) return;
      thumbsize = thumbx[ii-1];
      targposn = scrollposn / thumbH * xcols;                              //  keep top row position
      gallery_paint(0,0);
      return;
   }

   if (strEqu(menu,ZTX("smaller")))  {                                     //  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 == 3)                            //  min. size for metadata report   v.12.01
         thumbsize = 128;
      targposn = scrollposn / thumbH * xcols;                              //  keep top row position
      gallery_paint(0,0);
      return;
   }

   scrollp = scrollposn;

   if (strEqu(menu,ZTX("parent"))) {
      Gwarn = 0;
      if (image_navi::gallerytype > 1) Gwarn = 1;                          //  warn if discarding search or
      pp = image_navi::galleryname;                                        //    collection type gallery
      if (pp && strEqu(pp,"Recent Files")) Gwarn = 0;                      //  v.12.01
      if (Gwarn) {                                                         //  if gallery not from a directory,
         yn = zmessageYN(wing,Bdiscard,image_navi::galleryname);           //    warn user, gallery will be discarded
         if (! yn) return;                                                 //  do not discard
      }
      if (galleryname && gallerytype == 1) 
         filez = strdupz(galleryname,0,"image_navi");
      else filez = strdupz(curr_dirk,0,"image_navi");                      //  v.12.01
      pp = strrchr(filez,'/');                                             //  go up to '/' and stop
      if (pp && pp > filez) *pp = 0;
      else filez = strdupz("/",0,"image_navi");                            //  v.12.01
      image_navigate(filez,"init");                                        //  get new file list
      gallery_paint(0,0);
      zfree(filez);
      scrollp = 0; 
   }

   if (strEqu(menu,ZTX("prev row"))) scrollp -= thumbH;
   if (strEqu(menu,ZTX("next row"))) scrollp += thumbH;
   if (strEqu(menu,ZTX("prev page"))) scrollp -= thumbH * xrows;
   if (strEqu(menu,ZTX("next page"))) scrollp += thumbH * xrows;
   if (strEqu(menu,ZTX("first page"))) scrollp = 0;
   if (strEqu(menu,ZTX("last page"))) scrollp = maxscroll;

   if (scrollp < 0) scrollp = 0;                                           //  enforce limits
   if (scrollp > maxscroll) scrollp = maxscroll;
   scrollp = scrollp / thumbH * thumbH;                                    //  align top row

   if (scrollp != scrollposn)
      gtk_adjustment_set_value(scrollbar,scrollp);

   return;
}


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

void image_navi::mouse_xevent(GtkWidget *, GdkEventButton *event, void *)
{
   using namespace image_navi;

   int            mousex, mousey, mousebutt;
   int            row, col, nrows, ii, err;
   char           *filez;
   struct stat    statb;
   
   if (! nfiles) return;                                                   //  empty window
   
   mousex = int(event->x);
   mousey = int(event->y);
   mousebutt = event->button;

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

   nrows = 1 + (nfiles-1) / xcols;                                         //  total thumbnail rows, 1 or more
   if (col < 0 || col >= xcols) return;
   if (row < 0 || row >= nrows) return;

   ii = xcols * row + col;
   if (ii >= nfiles) return;

   filez = strdupz(flist[ii],0,"image_navi");                              //  selected file
   *filez = '/';
   
   err = stat(filez,&statb);
   if (err) {                                                              //  file is gone?
      zfree(filez);
      return;
   }

   if (S_ISDIR(statb.st_mode)) {                                           //  if directory, go there
      image_navigate(filez,"init");
      gallery_paint(0,0);
      zfree(filez);
      return;
   }
   
   if (userfunc) userfunc(ii,mousebutt);                                   //  clicked file position to user
   return;
}


//  private function
//  KB event function - respond to keyboard navigation keys
//  key definitions: /usr/include/gtk-2.0/gdk/gdkkeysyms.h

int image_navi::KBxpress(GtkWidget *win, GdkEventKey *event, void *)       //  prevent propagation of key-press
{                                                                          //    events to toolbar buttons 
   return 1;
}

int image_navi::KBxrelease(GtkWidget *win, GdkEventKey *event, void *)
{
   using namespace image_navi;

   int      KBkey;
   
   KBkey = event->keyval;

   if (KBkey == GDK_plus) menufuncx(win,ZTX("bigger"));                    //  +/- = bigger/smaller thumbnails
   if (KBkey == GDK_equal) menufuncx(win,ZTX("bigger"));
   if (KBkey == GDK_minus) menufuncx(win,ZTX("smaller"));
   if (KBkey == GDK_KP_Add) menufuncx(win,ZTX("bigger"));                  //  keypad +/- also 
   if (KBkey == GDK_KP_Subtract) menufuncx(win,ZTX("smaller"));

   if (KBkey == GDK_Left) menufuncx(win,ZTX("prev page"));                 //  left arrow = previous page
   if (KBkey == GDK_Right) menufuncx(win,ZTX("next page"));                //  right arrow = next page
   if (KBkey == GDK_Up) menufuncx(win,ZTX("prev row"));                    //  up arrow = previous row
   if (KBkey == GDK_Down) menufuncx(win,ZTX("next row"));                  //  down arrow = next row
   
   if (KBkey == GDK_Home) menufuncx(win,ZTX("first page"));                //  keys added 
   if (KBkey == GDK_End) menufuncx(win,ZTX("last page"));
   if (KBkey == GDK_Page_Up) menufuncx(win,ZTX("prev page"));
   if (KBkey == GDK_Page_Down) menufuncx(win,ZTX("next page"));

   if (KBkey == GDK_Escape) gtk_widget_destroy(win);                       //  Escape = cancel gallery window
   
   if (KBkey == GDK_F1)
      showz_userguide(zfuncs::F1_help_topic);
   
   return 1;
}


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

int image_gallery_position(cchar *file, int Nth)
{
   using namespace image_navi;
   
   int      ii;
   
   if (! nfiles) return -1;

   if (Nth >= 0 && Nth < nfiles) ii = Nth;
   else ii = 0;
   
   if (strEqu(file+1,flist[ii]+1)) return ii;                              //  1st flist[ii] char. may be !

   for (ii = 0; ii < nfiles; ii++)
      if (strEqu(file+1,flist[ii]+1)) break;

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


//  public function
//  determine if a file is a directory or a supported image file type
//  return: 0 = error, 1 = directory, 2 = image file, 3 = other

int image_file_type(cchar *file)
{
   using namespace image_navi;

   int            err, cc;
   cchar          *pp;
   struct stat    statbuf;

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

   if (S_ISDIR(statbuf.st_mode)) {                                         //  directory
      cc = strlen(file);
      if (cc > 12) {
         pp = file + cc - 12;
         if (strEqu(pp,"/.thumbnails")) return 3;                          //  .thumbnails
      }
      return 1;
   }

   if (S_ISREG(statbuf.st_mode)) {                                         //  reg. file
      pp = strrchr(file,'.');
      if (! pp) return 3;
      pp = strcasestr(imagetypes,pp);                                      //  supported image type
      if (pp) return 2;
   }
   
   return 3;
}


//  Public function
//  Get thumbnail filespec for the given image file.
//  If missing or stale, add or update in /.thumbnails directory.
//  Returned filespec is subject for zfree().
//  Use 4 threads to build missing thumbnails asynchrounously
//  (and hopefully ahead of need).

namespace image_thumbs 
{
   char * image_thumbfile_1x(char *imagefile);
   void * image_thumbfile_thread(void *arg);
   int    image_thumbfile_lock(char *imagefile, int lock);
   char     *imagefilex;
   char     *thumbfilex;
   char     *directoryx;
   char     **filelistx;
   int      Nfilesx;
   int      indexx;
   int      busythreads;
   int      stopthreads;
   char     *lockfiles[5];
   mutex    filelock = PTHREAD_MUTEX_INITIALIZER;
   mutex    thumblock = PTHREAD_MUTEX_INITIALIZER;
}


char * image_thumbfile(char *imagefile)
{
   using namespace image_thumbs;

   char           *thumbfile, *directory;
   char           *pfile, *bfile, *buff;
   cchar          *findcommand = "find \"%s\" -maxdepth 1";
   struct stat    statf, statb;
   int            err, ftyp, contx = 0;

   zthreadcrash();                                                         //  thread usage not allowed

   err = stat(imagefile,&statf);
   if (err) return 0;
   if (! S_ISREG(statf.st_mode)) return 0;                                 //  not a regular file
   if (image_file_type(imagefile) != 2) return 0;                          //  unsupported image type
   
   pfile = strrchr(imagefile,'/');                                         //  get .../filename.xxx
   if (! pfile) return 0;
   if (pfile > imagefile+12 && strnEqu(pfile-12,"/.thumbnails/",13)) {     //  .../.thumbnails/filename.xxx ??
      thumbfile = strdupz(imagefile,0,"image_thumbfile");                  //  yes, a thumbnail file
      return thumbfile;                                                    //  return same file
   }
   
   thumbfile = strdupz(imagefile,20,"image_thumbfile");                    //  construct thumbnail file
   bfile = thumbfile + (pfile - imagefile);                                //    .../.thumbnails/filename.xxx.png
   strcpy(bfile,"/.thumbnails");
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   err = stat(thumbfile,&statb);                                           //  thumbnail file exists ??
   if (! err) {                                                            //  yes
      if (statb.st_mtime >= statf.st_mtime) return thumbfile;              //  up to date, return it
      zfree(thumbfile);
      thumbfile = image_thumbfile_1x(imagefile);                           //  refresh stale thumbnail
      return thumbfile;                                                    //  return it
   }

   zfree(thumbfile);

   directory = strdupz(imagefile,0,"image_thumbfile");                     //  thumbnail does not exist
   pfile = strrchr(directory,'/');
   pfile[1] = 0;                                                           //  image directory/
   
   if (! directoryx) directoryx = directory;                               //  first use
   else if (strNeq(directoryx,directory)) {                                //  new directory to search
      stopthreads = 1;
      while (busythreads) zsleep(0.001);                                   //  stop prior search if any
      zfree(directoryx);
      directoryx = directory;                                              //  directory for new search
   }
   else zfree(directory);                                                  //  no change

   imagefilex = imagefile;                                                 //  target thumbnail to get

   while (busythreads) {                                                   //  if ongoing search, request thumbnail
      if (! imagefilex) return thumbfilex;                                 //      from existing search threads
      zsleep(0.001);                                                       //  (this can fail rarely)
   }

   if (filelistx)                                                          //  set up empty file list
      for (int ii = 0; ii < Nfilesx; ii++) zfree(filelistx[ii]);
   else 
      filelistx = (char **) zmalloc(MAXIMAGES * sizeof(char *),"image_thumbfile");
   Nfilesx = 0;

   while ((buff = command_output(contx,findcommand,directoryx)))           //  search image file directory
   {
      if (Nfilesx == MAXIMAGES) {                                          //  no room
         zmessageACK(0,Btoomanyfiles,MAXIMAGES);
         break;
      }

      ftyp = image_file_type(buff);
      if (ftyp == 2) {                                                     //  supported image file type
         filelistx[Nfilesx] = buff;                                        //  add to file list
         Nfilesx++;
      }
      else zfree(buff);
   }
   
   busythreads = 4;                                                        //  start search threads for this directory
   stopthreads = 0;
   indexx = -1;

   for (int ii = 0; ii < 4; ii++)
      start_detached_thread(image_thumbfile_thread,0);
   
   while (busythreads) {                                                   //  return as soon as target thumbnail
      if (! imagefilex) return thumbfilex;                                 //    has been generated
      zsleep(0.001);
   }

   if (! imagefilex) return thumbfilex;
   printf("image_thumbfile failed %s \n",imagefile);                       //  failure
   return 0;
}


//  private function
//  four threads each processing 1/4 of the entries in filelistx

void * image_thumbs::image_thumbfile_thread(void *arg)
{
   using namespace image_thumbs;

   int      ii;
   char     *thumbfile;

   while (indexx < Nfilesx)
   {
      if (imagefilex) {                                                    //  if thumbnail request is waiting
         mutex_lock(&thumblock);                                           //    take care of it first
         if (imagefilex) 
            thumbfilex = image_thumbfile_1x(imagefilex);
         imagefilex = 0;
         mutex_unlock(&thumblock);
      }
      
      if (stopthreads) break;                                              //  stop before done
     
      ii = zadd_locked(indexx,+1);                                         //  get next file from list
      if (ii >= Nfilesx) break;
      
      thumbfile = image_thumbfile_1x(filelistx[ii]);                       //  make thumbnail file if needed
      if (thumbfile) zfree(thumbfile);
   }

   zadd_locked(busythreads,-1);                                            //  decrement busy count
   pthread_exit(0);                                                        //  "return" cannot be used here
}


//  Private function to create a single thumbnail file.
//  Get thumbnail file for the given image file.
//  If missing or stale, add or update in /.thumbnails directory.
//  Returned filespec is subject for zfree().

char * image_thumbs::image_thumbfile_1x(char *imagefile)
{
   using namespace image_thumbs;

   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   char              *pfile, *bfile, *thumbfile;
   int               err, sizew, sizeh;
   struct stat       statf, statb;
   
   err = stat(imagefile,&statf);
   if (err) return 0;
   if (! S_ISREG(statf.st_mode)) return 0;                                 //  not a regular file
   if (image_file_type(imagefile) != 2) return 0;                          //  unsupported image type
   
   pfile = strrchr(imagefile,'/');                                         //  get .../filename.xxx
   if (! pfile) return 0;
   if (pfile > imagefile+12 && strnEqu(pfile-12,"/.thumbnails/",13)) {     //  .../.thumbnails/filename.xxx ??
      thumbfile = strdupz(imagefile,0,"image_thumbfile");                  //  yes, a thumbnail file
      return thumbfile;                                                    //  return same file
   }
   
   thumbfile = strdupz(imagefile,20,"image_thumbfile");                    //  construct thumbnail file
   bfile = thumbfile + (pfile - imagefile);                                //    .../.thumbnails/filename.xxx.png
   strcpy(bfile,"/.thumbnails");
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   while (! image_thumbfile_lock(imagefile,1)) zsleep(0.001);              //  lock file for me

   err = stat(thumbfile,&statb);                                           //  thumbnail file exists ??
   if (err || (statb.st_mtime < statf.st_mtime)) 
   {                                                                       //  does not exist or stale
      *bfile = 0;
      err = stat(thumbfile,&statb);
      if (err) err = mkdir(thumbfile,0751);                                //  create .thumbnails directory
      if (err) {
         image_thumbfile_lock(imagefile,0);
         return 0;
      }
      *bfile = *pfile;
      sizew = sizeh = image_navi::thumbfilesize;                           //  create thumbnail pixbuf
      thumbpxb = gdk_pixbuf_new_from_file_at_size(imagefile,sizew,sizeh,&gerror);
      if (! thumbpxb) {
         printf("gdk_pixbuf_new error: %s \n",gerror->message);            //  diagnose error 
         image_thumbfile_lock(imagefile,0);
         return 0;
      }
      gdk_pixbuf_save(thumbpxb,thumbfile,"png",&gerror,null);              //  save in /.thumbnails/ directory
      g_object_unref(thumbpxb);
      image_navi::genthumbs++;                                             //  count generated thumbnails
   }
   
   image_thumbfile_lock(imagefile,0);                                      //  return thumbfile
   return thumbfile;
}


//  prevent interference from multiple threads working on the same image file

int image_thumbs::image_thumbfile_lock(char *imagefile, int lock)
{
   using namespace image_thumbs;

   int         ii, jj = -1;
   
   mutex_lock(&filelock);

   if (lock)                                                               //  lock file if possible
   {
      for (ii = 0; ii < 5; ii++) {
         if (! lockfiles[ii]) jj = ii;                                     //  remember 1st empty slot
         else if (strEqu(lockfiles[ii],imagefile)) {
            mutex_unlock(&filelock);                                       //  file is already locked
            return 0;
         }
      }

      if (jj < 0) zappcrash("image_thumbfile_lock > 5");
      lockfiles[jj] = imagefile;                                           //  lock the file
      mutex_unlock(&filelock);
      return 1;
   }

   else                                                                    //  unlock file
   {
      for (ii = 0; ii < 5; ii++) {
         if (! lockfiles[ii]) continue;
         if (strEqu(lockfiles[ii],imagefile)) {
            lockfiles[ii] = 0;
            mutex_unlock(&filelock);
            return 1;
         }
      }
      zappcrash("image_thumbfile_lock gone");
      return 0;
   }
}


//  Public function
//  Get thumbnail image for given image file, from .thumbnails directory.
//  Add thumbnail file if missing, or update it if older than image file.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.

GdkPixbuf * image_thumbnail(char *fpath, int size)
{
   using namespace zfuncs;
   using namespace image_navi;

   GdkPixbuf         *thumbpxb;
   GError            *gerror = 0;
   int               ii, err;
   char              *bpath;
   time_t            mtime;
   struct stat       statf;
   const int         cachesize = thumbnail_cachesize;                      //  shorthand

   static int           nextcache, ftf = 1;
   static int           sizecache[cachesize];
   static time_t        mtimecache[cachesize];
   static char          *fpathcache[cachesize];
   static GdkPixbuf     *pixbufcache[cachesize];
   
   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 (S_ISDIR(statf.st_mode)) {                                           //  if directory, return folder image
      bpath = zmalloc(500);
      strncatv(bpath,499,zicondir,"/folder256.png",null);
      thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);
      zfree(bpath);
      return thumbpxb;
   }
   
   mtime = statf.st_mtime;                                                 //  last modification time
   
   if (! size) size = thumbfilesize;                                       //  default thumb size
   
   for (ii = nextcache; ii >= 0; ii--)                                     //  get cached pixbuf if found 
      if (fpathcache[ii] && strEqu(fpath,fpathcache[ii]) &&
          sizecache[ii] == size && mtime == mtimecache[ii]) break;         //  check mtime (bugfix)
   if (ii >= 0) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);
      return thumbpxb;
   }
   for (ii = cachesize-1; ii > nextcache; ii--)                            //  continue search
      if (fpathcache[ii] && strEqu(fpath,fpathcache[ii]) &&
          sizecache[ii] == size && mtime == mtimecache[ii]) break;
   if (ii > nextcache) {
      thumbpxb = gdk_pixbuf_copy(pixbufcache[ii]);
      return thumbpxb;
   }
   
   if (size > thumbfilesize) {                                             //  support huge thumbnails
      thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror);
      goto addcache;
   }
   
   bpath = image_thumbfile(fpath);                                         //  get thumbnail file
   if (! bpath) return 0;
   thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror);   //  get thumbnail
   zfree(bpath);
   goto addcache;

addcache:
   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]);
   }
   fpathcache[ii] = strdupz(fpath,0,"thumbnail_cache");                    //  add new occupant
   pixbufcache[ii] = gdk_pixbuf_copy(thumbpxb);                            //  this memory is not tracked
   sizecache[ii] = size;
   mtimecache[ii] = mtime;

   return thumbpxb;                                                        //  return pixbuf to caller
}


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

   private function - manage list of image files within a directory

   get an image file in the same directory as given file or directory

   char * image_navi::image_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 (0 base) or null if Nth > last

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

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

char * image_navi::image_navigate(cchar *filez, cchar *action, int Nth)
{
   using namespace image_navi;

   char           *buff;
   cchar          *findcommand = "find \"%s\" -maxdepth 1";
   char           *pp, *file2;
   int            err, ii, cc, ftyp, contx = 0, fposn;
   FILE           *fid;
   struct stat    statbuf;
   
   if (! strstr("init initF sort insert delete find",action))
         zappcrash("image_navigate %s",action);

   if (strnEqu(action,"init",4))                                           //  init or initF
   {
      if (flist) {
         for (ii = 0; ii < nfiles; ii++) zfree(flist[ii]);                 //  free prior file list
         zfree(flist);
      }

      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(char *);      
      flist = (char **) zmalloc(cc,"image_navi");                          //  list of file pointers

      nfiles = nimages = 0;                                                //  no files
      fposn = 0;
      gallerytype = 0;                                                     //  v.12.01
   }

   if (strEqu(action,"init"))                                              //  initialize from given directory
   {
      if (! filez) return 0;                                               //  should not happen

      if (galleryname) zfree(galleryname);
      galleryname = strdupz(filez,0,"image_navi");
      gallerytype = 1;                                                     //  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;
      }

      while ((buff = command_output(contx,findcommand,galleryname)))       //  find all files
      {
         if (strEqu(buff,galleryname)) {                                   //  skip self directory
            zfree(buff);
            continue;
         }
         
         if (nfiles == MAXIMAGES) {                                        //  message, not crash 
            zmessageACK(0,Btoomanyfiles,MAXIMAGES);
            break;
         }

         ftyp = image_file_type(buff);

         if (ftyp == 1) {                                                  //  subdirectory
            flist[nfiles] = buff;                                          //  add to file list
            flist[nfiles][0] = '!';                                        //  if directory, make it sort first
            nfiles++;
         }

         else if (ftyp == 2) {                                             //  supported image file type
            flist[nfiles] = buff;                                          //  add to file list
            nfiles++;
            nimages++; 
         }

         else {
            zfree(buff);                                                   //  (.thumbnails not ftyp 1)
            continue;
         }
      }

      if (nfiles > 1) 
         HeapSort(flist,nfiles,image_fcomp);                               //  Heap Sort - pointers to strings

      return 0;
   }

   if (strEqu(action,"initF"))                                             //  initialize from given list
   {
      if (galleryname) zfree(galleryname);
      galleryname = strdupz(filez,0,"image_navi");
      gallerytype = 2;                                                     //  gallery type = file list
      
      fid = fopen(galleryname,"r");                                        //  open file
      if (! fid) return 0;

      buff = zmalloc(maxfcc);
      
      while (true)                                                         //  read list of files
      {
         if (nfiles == MAXIMAGES) {
            zmessageACK(0,Btoomanyfiles,MAXIMAGES);
            break;
         }
         pp = fgets_trim(buff,maxfcc-1,fid,1);
         if (! pp) break;
         err = stat(pp,&statbuf);                                          //  check file exists
         if (err) continue;
         flist[nfiles] = strdupz(buff,0,"image_navi");                     //  add files to memory list
         nfiles++;
         nimages++;
      }

      fclose(fid);
      zfree(buff);                                                         //  sort removed
      return 0;
   }
   
   if (strEqu(action,"sort"))                                              //  sort the list from initF 
   {
      if (gallerytype == 3) return 0;                                      //  v.12.01
      if (nfiles < 2) return 0;
      HeapSort(flist,nfiles,image_fcomp);                                  //  Heap Sort - pointers to strings
      return 0;
   }

   if (strEqu(action,"insert"))                                            //  insert new file into list
   {
      if (gallerytype == 3) return 0;                                      //  metadata report     v.12.01
      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(0,Btoomanyfiles,MAXIMAGES);
         return 0;
      }

      for (ii = nfiles; ii > fposn; ii--)                                  //  create hole is list
         flist[ii] = flist[ii-1];

      flist[fposn] = strdupz(filez,0,"image_navi");                        //  put new file in hole
      nfiles++;
      nimages++;                                                           //  bugfix
   }

   if (strEqu(action,"delete"))                                            //  delete file from list
   {
      if (gallerytype == 3) return 0;                                      //  metadata report     v.12.01
      fposn = Nth;                                                         //  file position from caller must be OK
      if (fposn < 0 || fposn > nfiles-1) return 0;
      nfiles--;
      if (*flist[fposn] != '!') nimages--;                                 //  not a directory              v.12.01
      zfree(flist[fposn]);                                                 //  remove file from list
      for (ii = fposn; ii < nfiles; ii++) {                                //  close the hole
         if (nfiles < 0) printf("meaningless reference %d",ii);            //  stop g++ optimization bug    ////
         flist[ii] = flist[ii+1];
      }
   }
   
   if (strEqu(action,"find"))
   {
      fposn = Nth;                                                         //  file position from caller must be OK
      if (fposn < 0 || fposn > nfiles-1) return 0;
      file2 = strdupz(flist[fposn],0,"image_navi");                        //  get Nth file
      file2[0] = '/';                                                      //  restore initial '/'
      err = stat(file2,&statbuf);
      if (! err) return file2;
      zfree(file2);
   }
   
   return 0;
}


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

int image_navi::image_fcomp(cchar *file1, cchar *file2)
{
   int      nn;
   nn = strcasecmp(file1,file2);                                           //  compare ignoring case
   if (nn != 0) return nn;
   nn = strcmp(file1,file2);                                               //  if equal, do normal compare
   return nn;
}


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

//  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 image_gallery() callback function is changed and must be restored by caller.

int  imgl_showthumb();
void imgl_insert_file(cchar *imagefile);
void imgl_gallery_file(int Nth, int button);

GtkWidget      *imgl_drawarea = 0;
GtkWidget      *imgl_files = 0;
cchar          *imgl_font = "Monospace 8";
zdialog        *imgl_zd = 0;
int            imgl_fontheight = 14;
int            imgl_cursorpos = 0;


char ** image_gallery_getfiles(char *startdir, GtkWidget *parent)
{
   int  imgl_dialog_event(zdialog *zd, cchar *event);
   int  imgl_mouseclick(GtkWidget *, GdkEventButton *event, void *);

   PangoLanguage           *plang;
   PangoFontDescription    *pfontdesc;
   PangoContext            *pcontext;
   PangoFont               *pfont;
   PangoFontMetrics        *pmetrics;
   GdkCursor               *cursor;
   GdkWindow               *gdkwin;
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   int               fontascent, fontdescent;
   int               line, nlines, ii;
   char              *imagefile, **filelist;
   
   zthreadcrash();                                                         //  thread usage not allowed

   image_gallery(startdir,"paint1",0,imgl_gallery_file,parent);            //  activate image gallery window

   imgl_zd = zdialog_new(ZTX("Select Files"),wing,ZTX("done"),ZTX("cancel"),null);
   zdialog_add_widget(imgl_zd,"hbox","hb1","dialog",0,"expand|space=5");
   zdialog_add_widget(imgl_zd,"frame","fr11","hb1",0,"expand");
   zdialog_add_widget(imgl_zd,"scrwin","scrwin","fr11",0,"expand");
   zdialog_add_widget(imgl_zd,"edit","files","scrwin");
   zdialog_add_widget(imgl_zd,"vbox","vb12","hb1");
   zdialog_add_widget(imgl_zd,"frame","fr12","vb12");
   zdialog_add_widget(imgl_zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(imgl_zd,"button","delete","hb2",ZTX("delete"),"space=8");
   zdialog_add_widget(imgl_zd,"button","insert","hb2",ZTX("insert"),"space=8");
   zdialog_add_widget(imgl_zd,"button","addall","hb2",ZTX("add all"),"space=30");

   GtkWidget *textbox = zdialog_widget(imgl_zd,"files");                   //  disable text wrap 
   gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textbox),GTK_WRAP_NONE);

   GtkWidget *frame = zdialog_widget(imgl_zd,"fr12");                      //  drawing area for thumbnail image
   imgl_drawarea = gtk_drawing_area_new();
   gtk_widget_set_size_request(imgl_drawarea,256,258);                     //  increased
   gtk_container_add(GTK_CONTAINER(frame),imgl_drawarea);
   
   imgl_files = zdialog_widget(imgl_zd,"files");                           //  activate mouse-clicks for
   gtk_widget_add_events(imgl_files,GDK_BUTTON_PRESS_MASK);                //    file list widget
   G_SIGNAL(imgl_files,"button-press-event",imgl_mouseclick,0);

   pfontdesc = pango_font_description_from_string(imgl_font);              //  set default font for files window
   gtk_widget_modify_font(imgl_files,pfontdesc);

   plang = pango_language_get_default();                                   //  get font metrics (what a mess)
   pcontext = gtk_widget_get_pango_context(imgl_files);
   pfont = pango_context_load_font(pcontext,pfontdesc);
   pmetrics = pango_font_get_metrics(pfont,plang);
   fontascent = pango_font_metrics_get_ascent(pmetrics) / PANGO_SCALE;
   fontdescent = pango_font_metrics_get_descent(pmetrics) / PANGO_SCALE;
   imgl_fontheight = fontascent + fontdescent;                             //  effective line height

   zdialog_resize(imgl_zd,600,0);                                          //  start dialog
   zdialog_run(imgl_zd,imgl_dialog_event);

   cursor = gdk_cursor_new(GDK_TOP_LEFT_ARROW);                            //  arrow cursor for file list widget
   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(imgl_files),TEXTWIN);   //  (must be done after window realized)
   gdk_window_set_cursor(gdkwin,cursor);
   imgl_cursorpos = 0;

   zdialog_wait(imgl_zd);                                                  //  wait for completion

   if (imgl_zd->zstat != 1) {                                              //  cancelled
      zdialog_free(imgl_zd);                                               //  kill dialog
      image_gallery(0,"close");                                            //  close image gallery window
      return 0;
   }

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   nlines = gtk_text_buffer_get_line_count(textBuff);
   
   filelist = (char **) zmalloc((nlines+1) * sizeof(char *),"imgl.getfiles");

   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] = strdupz(imagefile,0,"imgl.getfiles");              //  >> next file in list
         free(imagefile);
         ii++;
      }
   }
   filelist[ii] = 0;                                                       //  mark EOL
   
   zdialog_free(imgl_zd);                                                  //  kill dialog
   image_gallery(0,"close");                                               //  close image gallery window

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

   return filelist;                                                        //  return file list
}


//  imgl dialog event function

int imgl_dialog_event(zdialog *zd, cchar *event)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   char           *ftemp;
   static char    *imagefile = 0;
   int            line, posn;
   
   if (strEqu(event,"delete"))                                             //  delete file at cursor position
   {
      if (imagefile) zfree(imagefile);
      imagefile = 0;

      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
      line = imgl_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 || *ftemp != '/') {
         if (ftemp) free(ftemp);
         return 0;
      }

      imagefile = strdupz(ftemp,0,"imgl.getfiles");                        //  save deleted file for poss. insert
      free(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)

      imgl_showthumb();                                                    //  thumbnail = next file
   }

   if (strEqu(event,"insert"))                                             //  insert last deleted file
   {                                                                       //    at current cursor position
      if (! imagefile) return 0;
      imgl_insert_file(imagefile);
      zfree(imagefile);
      imagefile = 0;
   }

   if (strEqu(event,"addall"))                                             //  insert all files in image gallery
   {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
      posn = 0;

      while (true)
      {
         imagefile = image_gallery(0,"find",posn);                         //  get first or next file
         if (! imagefile) break;
         posn++;
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,imgl_cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,"\n",1);                   //  insert new blank line
         gtk_text_buffer_get_iter_at_line(textBuff,&iter1,imgl_cursorpos);
         gtk_text_buffer_insert(textBuff,&iter1,imagefile,-1);             //  insert image file
         zfree(imagefile);
         imgl_cursorpos++;                                                 //  advance cursor position
      }
   }

   return 0;
}


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

void imgl_insert_file(cchar *imagefile)
{
   GtkTextIter    iter;
   GtkTextBuffer  *textBuff;
   
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,imgl_cursorpos);
   gtk_text_buffer_insert(textBuff,&iter,"\n",1);                          //  insert new blank line
   gtk_text_buffer_get_iter_at_line(textBuff,&iter,imgl_cursorpos);
   gtk_text_buffer_insert(textBuff,&iter,imagefile,-1);                    //  insert image file

   imgl_showthumb();                                                       //  update thumbnail
   imgl_cursorpos++;                                                       //  advance 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 imgl_gallery_file(int Nth, int button)
{
   int            ftyp;
   char           *imagefile;
   
   if (Nth == -2) {
      showz_userguide(zfuncs::F1_help_topic);                              //  F1 context help
      return;
   }

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

   ftyp = image_file_type(imagefile);                                      //  ignore directories
   if (ftyp == 2) imgl_insert_file(imagefile);                             //  insert file at current position
   zfree(imagefile);

   return;
}


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

int imgl_mouseclick(GtkWidget *, GdkEventButton *event, void *)
{
   int            mpy;
   GtkWidget      *scrollwin;
   GtkAdjustment  *scrolladj;
   double         scrollpos;

   if (event->type != GDK_BUTTON_PRESS) return 0;
   mpy = int(event->y);
   scrollwin = zdialog_widget(imgl_zd,"scrwin");                           //  window scroll position
   scrolladj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scrollwin));
   scrollpos = gtk_adjustment_get_value(scrolladj);
   imgl_cursorpos = (mpy + scrollpos) / imgl_fontheight;                   //  line selected
   imgl_showthumb();                                                       //  show thumbnail image
   return 0;
}


//  show thumbnail for file at current cursor position

int imgl_showthumb()
{
   int            line;
   char           *imagefile;
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   GdkPixbuf      *thumbnail = 0;

   gdk_window_clear(imgl_drawarea->window);

   line = imgl_cursorpos;
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imgl_files));
   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 != '/') {
      free(imagefile);
      return 0;
   }

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

   if (thumbnail) {
      gdk_draw_pixbuf(imgl_drawarea->window,0,thumbnail,0,0,0,0,-1,-1,NODITHER,0,0);
      g_object_unref(thumbnail);
   }
   return 0;
}


