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

   Fotoxx      edit photos and manage collections

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

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

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

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

   Fotoxx image edit - file menu and file functions

   m_new_session           start a new parallel session of Fotoxx
   m_cycle2files           cycle 2 previous image files
   m_cycle3files           cycle 3 previous image files
   m_view360               view a 360 degree panorama image
   m_rename                rename current image file or clicked thumbnail
   m_permissions           show and change file permissions                      20.0
   f_open                  open and display an image file
   f_open_saved            open the last image file saved
   f_preload               preload image files ahead of need
   x_prev_next             open prev/next file in current or prev/next gallery
   m_prev                  menu function - open previous
   m_next                  menu function - open next
   m_prev_next             menu button function - open previous or next file
   m_zoom_menu             menu button function - zoom image/thumb (F/G view)
   m_blank_image           create a new monocolor image
   create_blank_file       callable function to create a new monocolor image
   m_blank_window          blank/restore window
   play_gif                play current GIF file animation

   m_copy_move             copy or move an image file to a new location
   m_copyto_desktop        copy an image file to the desktop
   m_copyto_clip           copy clicked file or current file to the clipboard
   m_wallpaper             set current image file as wallpaper
   m_delete_trash          delete or trash an image file

   m_print                 print an image file
   m_print_calibrated      print an image file with calibrated colors
   m_quit                  menu quit
   quitxx                  callable quit
   m_help                  help menu

   m_file_save             save a (modified) image file to disk
   m_file_save_replace     save file (replace) for KB shortcut
   m_file_save_version     save file (new version) for KB shortcut

   file_rootname           get root file name /.../filename
   file_basename           get base file name /.../filename.ext
   file_all_versions       get base file and all versions /.../filename.vNN.ext
   file_new_version        get next available version /.../filename.vNN.ext
   file_newest_version     get newest file version or base file name if none
   file_prior_version      get prior version or base file name for given file

   f_save                  save an image file to disk (replace, new version, new file)
   f_save_as               dialog to save an image file with a designated file name

   linedit                 text file line edit functions
   find_imagefiles         find all image files under a given folder path
   raw_to_tiff             convert a RAW file name to equivalent .tif name
   set_permissions         dialog to show and set file permissions               //  20.0
   conv_permissions        conv. permission formats: external <> mode_t          //  20.0
   f_realpath              get real path for filename having symlinks            //  20.0
   

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

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

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


//  start a new parallel session of fotoxx
//  new window is slightly down and right from old window
//  current language locale is preserved
//  args = optional command line args
//  current file is appended if it exists

void m_new_session(GtkWidget *, cchar *)
{
   char     *pp = curr_file;
   int      cc;

   if (pp && strchr(pp,' ')) {                                                   //  if embedded blanks, enclose in quotes
      cc = strlen(pp);
      pp = (char *) zmalloc(cc+4);
      pp[0] = '"';
      strncpy(pp+1,curr_file,cc);
      pp[cc+1] = '"';
      pp[cc+2] = 0;
      new_session(pp);
      zfree(pp);
   }

   else new_session(pp);

   return;
}


//  callable new session function

void new_session(cchar *args)
{
   int      cc, xx, yy, ww, hh;
   char     progexe[300], command[1000];

   gtk_window_get_position(MWIN,&xx,&yy);                                        //  get window position and size
   gtk_window_get_size(MWIN,&ww,&hh);
   xx += 100;                                                                    //  shift down and right
   yy += 100;

   cc = readlink("/proc/self/exe",progexe,300);                                  //  get own program path
   if (cc <= 0) {
      zmessageACK(Mwin,"cannot get /proc/self/exe");
      return;
   }
   progexe[cc] = 0;
   
   if (! args) args = "";

   snprintf(command,999,"%s -c %d %d %d %d %s",                                  //  remove -lang <zfuncs::zlocale>     19.0
                     progexe, xx, yy, ww, hh, args);

   printz("\n %s \n",command);
   shell_asynch(command);

   return;
}


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

//  open the previous file opened (not the same as toolbar [prev] button)
//  repeated use will alternate the two most recent files

void m_cycle2files(GtkWidget *, cchar *)
{
   FILE     *fid;
   char     *file, buff[XFCC];
   int      Nth = 0, err;
   float    gzoom;

   F1_help_topic = "cycle 2";
   if (checkpend("busy block mods")) return;

   m_viewmode(0,"F");
   if (! Cstate) return;
   gzoom = Cstate->fzoom;

   fid = fopen(recentfiles_file,"r");
   if (! fid) return;

   file = fgets_trim(buff,XFCC,fid,1);                                           //  skip over first most recent file

   while (true)
   {
      file = fgets_trim(buff,XFCC,fid,1);                                        //  find next most recent file
      if (! file) break;
      err = f_open(file,Nth,0,0,0);
      if (! err) break;
   }

   fclose(fid);

   Cstate->fzoom = gzoom;                                                        //  keep same image scale
   Fpaint2();
   return;
}


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

//  open the 2nd previous file opened
//  repeated use will alternate the 3 most recent files

void m_cycle3files(GtkWidget *, cchar *) 
{
   FILE     *fid;
   char     *file, buff[XFCC];
   int      Nth = 0, err;
   float    gzoom;

   F1_help_topic = "cycle 3";
   if (checkpend("busy block mods")) return;

   m_viewmode(0,"F");
   if (! Cstate) return;
   gzoom = Cstate->fzoom;

   fid = fopen(recentfiles_file,"r");
   if (! fid) return;

   file = fgets_trim(buff,XFCC,fid,1);                                           //  skip over 2 most recent files
   file = fgets_trim(buff,XFCC,fid,1);

   while (true)
   {
      file = fgets_trim(buff,XFCC,fid,1);                                        //  find next most recent file
      if (! file) break;
      err = f_open(file,Nth,0,0,0);
      if (! err) break;
   }

   fclose(fid);

   Cstate->fzoom = gzoom;                                                        //  keep same image scale
   Fpaint2();
   return;
}


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

//   view a 360 degree panorama image
//   center of view direction can be any angle 0-360 degrees
//   image width is assumed to correspond to a 360 degree view

namespace view360 
{
   void   show();
   void * show_thread(void *arg);
   void   mouse();
   void   KB_func(int key);
   void   quit();

   PXB      *filepxb, *viewpxb;
   PIXBUF   *viewpixbuf;
   char     *filename = 0;
   int      projection = 1;
   int      Frecalc;
   int      fww, fhh;                                                            //  file dimensions
   int      dww, dhh;                                                            //  window dimensions
   int      pdww, pdhh;
   int      cx, cy;
   float    *Fx, *Fy, zoom;
   zdialog  *zd;
}


//  menu function

void m_view360(GtkWidget *, const char *)
{
   using namespace view360;
   
   F1_help_topic = "view 360° pano";
   if (checkpend("busy block mods")) return;

   if (clicked_file) {                                                           //  use clicked file if present
      filename = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else use current file
      filename = zstrdup(curr_file);
   else return;

   filepxb = PXB_load(filename,1);
   zfree(filename);

   if (! filepxb) return;
   
   zd = zmessage_post_bold(Mwin,"20/20",8,
            "Mouse drag: pan image 360° \n"
            "L/R arrow keys: pan image 360° \n"
            "L/R mouse click: zoom in/out \n"
            "Key 1 or 2: change projection \n"
            "Key Q: quit panorama view");

   Vmenu_block(1);                                                               //  block menus   
   Fview360 = 1;                                                                 //  flag for main window KBpress() 

   fww = filepxb->ww;
   fhh = filepxb->hh;
   cx = fww/2;                                                                   //  initial view center = middle
   cy = fhh/2;
   zoom = 0.5;                                                                   //  initial zoom
   Frecalc = 1;                                                                  //  initial calculate
   Fx = Fy = 0;                                                                  //  no allocated constants
   pdww = pdhh = 0;
   
   m_viewmode(0,"F");
   gdk_window_freeze_updates(gdkwin);
   takeMouse(mouse,0);
   show();
   return;
}


//  show current view region on window

void view360::show()
{
   using namespace view360;

   int      ii, px, py, dx, dy;
   float    sx, sy, R, C, Tx, Ty;

   if (! Fview360) return;

   dww = gdk_window_get_width(gdkwin);                                           //  drawing window size
   dhh = gdk_window_get_height(gdkwin);
   if (dww < 20 || dhh < 20) return;                                             //  too small
   
   if (dww != pdww || dhh != pdhh) {
      pdww = dww;
      pdhh = dhh;
      if (Fx) zfree(Fx);
      if (Fy) zfree(Fy);
      Fx = (float *) zmalloc(dww * dhh * sizeof(float));
      Fy = (float *) zmalloc(dww * dhh * sizeof(float));
      Frecalc = 1;
   }

   viewpxb = PXB_make(dww,dhh,3);
   viewpixbuf = viewpxb->pixbuf;
   gdk_pixbuf_fill(viewpixbuf,0);

   if (Frecalc && projection == 1)                                               //  flat projection
   {
      for (py = 0; py < dhh; py++)                                               //  loop window pixels
      for (px = 0; px < dww; px++)
      {
         dx = px - dww/2;                                                        //  distance from center
         dy = py - dhh/2;
         dx = dx / zoom;                                                         //  scale for zoom
         dy = dy / zoom;

         Tx = PI * dx / fww;                                                     //  x view angle
         Ty = PI * dy / fww;                                                     //  y view angle
         if (Tx > PI/3 || Tx < -PI/3 || Ty > PI/4 || Ty < -PI/4) {               //  limit to +-60 and +-45 deg. view
            ii = py * dww + px;
            Fx[ii] = Fy[ii] = 0;
            continue;
         }

         Tx = 0.4 * Tx;
         sx = dx * cosf(Tx);
         sy = dy * cosf(Ty) * cosf(Tx);
         
         ii = py * dww + px;
         Fx[ii] = sx;
         Fy[ii] = sy;
      }
   }
   
   if (Frecalc && projection == 2)                                               //  spherical projection
   {
      for (py = 0; py < dhh; py++)                                               //  loop window pixels
      for (px = 0; px < dww; px++)
      {
         dx = px - dww/2;                                                        //  distance from center of window
         dy = py - dhh/2;
         dx = dx / zoom;                                                         //  scale for zoom
         dy = dy / zoom;

         sx = 6.28 * dx / fww;                                                   //  scale (dx,dy) so that fww/4
         sy = 6.28 * dy / fww;                                                   //    (90 deg.) is PI/2

         if (sx > 1.0 || sx < -1.0 || sy > 0.7 || sy < -0.7) {                   //  limit +-90 and +-63 deg. view
            ii = py * dww + px;
            Fx[ii] = Fy[ii] = 0;
            continue;
         }

         R = sqrtf(sx*sx + sy*sy);                                               //  conv. (dx,dy) in rectilinear proj.
         C = atanf(R);                                                           //     to spherical coordinates
         Tx = atanf(sx);
         Ty = asinf(sy/R * sinf(C));
         if (R == 0.0) Tx = Ty = 0;                                              //  avoid Ty = nan                     19.1
         
         sx = 0.25 * fww * Tx;                                                   //  conv. spherical coordinate (Ty,Tx) 
         sy = 0.25 * fww * Ty * cosf(Tx);                                        //    to flat coordinates (sx,sy)

         ii = py * dww + px;
         Fx[ii] = sx;
         Fy[ii] = sy;
      }
   }
   
   Frecalc = 0;
   
   do_wthreads(show_thread,NWT);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   gdk_cairo_set_source_pixbuf(cr,viewpxb->pixbuf,0,0);
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   PXB_free(viewpxb);
   return;
}


//  thread function to generate the image

void * view360::show_thread(void *arg)
{
   using namespace view360;

   int      index = *((int *) (arg));
   int      ii, stat, px, py;
   float    sx, sy;
   uint8    pixs[4], *pixss, *pixp;
   
   for (py = index; py < dhh; py += NWT)                                         //  loop window pixels
   for (px = 0; px < dww; px++)
   {
      ii = py * dww + px;
      sx = Fx[ii];
      sy = Fy[ii];
      if (sx == 0 && sy == 0) continue;

      sx += cx;                                                                  //  add back center
      sy += cy;

      if (sy < 0) continue;                                                      //  off the image
      if (sy >= fhh) continue;

      if (sx < 0) sx = fww + sx;                                                 //  handle x wrap-around
      if (sx >= fww) sx = sx - fww;

      pixp = PXBpix(viewpxb,px,py);                                              //  dest. pixel

      stat = vpixel(filepxb,sx,sy,pixs);                                         //  source pixel interpolated
      if (stat) memcpy(pixp,pixs,3);                                             //  fails at image left/right edge
      else {
         pixss = PXBpix(filepxb,int(sx),int(sy));                                //  use no interpolation
         memcpy(pixp,pixss,3);
      }
   }

   pthread_exit(0);
   return 0;
}


//  mouse function - move center of view with mouse drag

void view360::mouse()
{
   using namespace view360;
   
   int      Fshow = 0;
   
   if (Mxdrag || Mydrag) {                                                       //  change center of view
      cx += 2 * Mwdragx;
      if (cx < 0) cx = fww + cx;
      if (cx >= fww) cx = cx - fww;
      Mxdrag = Mydrag = 0;
      Fshow = 1;
   }
   
   if (LMclick) {                                                                //  zoom-in
      LMclick = 0;
      zoom = zoom * 1.4142;
      if (zoom > 1.99) zoom = 2;
      Fshow = 1;
      Frecalc = 1;
   }
   
   if (RMclick) {                                                                //  zoom-out
      RMclick = 0;
      zoom = zoom / 1.4142;
      if (zoom < 0.36) zoom = 0.354;
      Frecalc = 1;
      Fshow = 1;
   }

   if (Fshow) {
      if (zd) zdialog_free(zd);
      zd = 0;
      show();
   }

   return;
}


//  keyboard function

void view360::KB_func(int key)
{
   using namespace view360;
   
   static int  busy = 0;
   
   while (busy) zmainloop();
   busy++;

   if (zd) zdialog_free(zd);
   zd = 0;   

   if (key == GDK_KEY_Escape || key == GDK_KEY_q) quit();
   
   if (key == GDK_KEY_1) {
      projection = 1;
      Frecalc = 1;
   }
   
   if (key == GDK_KEY_2) {
      projection = 2;
      Frecalc = 1;
   }

   if (key == GDK_KEY_Left) {
      cx -= 20;
      if (cx < 0) cx = fww + cx;
   }

   if (key == GDK_KEY_Right) {
      cx += 20;
      if (cx >= fww) cx = cx - fww;
   }

   show();
   busy = 0;
   return;
}


//  quit and free resources

void view360::quit()
{
   if (! Fview360) return;
   freeMouse();
   PXB_free(filepxb);
   PXB_free(viewpxb);
   if (Fx) zfree(Fx);
   if (Fy) zfree(Fy);
   Fview360 = 0;
   Vmenu_block(0);
   gdk_window_thaw_updates(gdkwin);
   Fpaint2();
}


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

//  rename menu function
//  activate rename dialog, stuff data from current or clicked file
//  dialog remains active when new file is opened

char     rename_old[200] = "";
char     rename_new[200] = "";
char     rename_prev[200] = "";
char     *rename_file = 0;

void m_rename(GtkWidget *, cchar *menu)
{
   int rename_dialog_event(zdialog *zd, cchar *event);

   char     *pdir, *pfile, *pext;

   F1_help_topic = "rename";

   if (rename_file) zfree(rename_file);
   rename_file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      rename_file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      rename_file = zstrdup(curr_file);
   else return;

   if (checkpend("all")) return;
   
   if (FGWM != 'F' && FGWM != 'G') m_viewmode(0,"F");                            //  19.0

/***
       ______________________________________
      |         Rename Image File            |
      |                                      |
      | Old Name  [_______________________]  |
      | New Name  [_______________________]  |
      |           [previous name] [Add 1]    |
      |                                      |
      | [x] keep this dialog open            |
      |                                      |
      |                   [apply] [cancel]   |
      |______________________________________|

***/

   if (! zd_rename)                                                              //  restart dialog
   {
      zd_rename = zdialog_new(E2X("Rename Image File"),Mwin,Bapply,Bcancel,null);
      zdialog *zd = zd_rename;

      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
      zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand");

      zdialog_add_widget(zd,"label","Lold","vb1",E2X("Old Name"));
      zdialog_add_widget(zd,"label","Lnew","vb1",E2X("New Name"));
      zdialog_add_widget(zd,"label","space","vb1");

      zdialog_add_widget(zd,"hbox","hb2","vb2");
      zdialog_add_widget(zd,"label","oldname","hb2");
      zdialog_add_widget(zd,"label","space","hb2",0,"expand");

      zdialog_add_widget(zd,"zentry","newname","vb2",0,"size=20");
      zdialog_add_widget(zd,"hbox","hb3","vb2",0,"space=3");
      zdialog_add_widget(zd,"button","prev","hb3",E2X("previous name"));
      zdialog_add_widget(zd,"button","Badd1","hb3",E2X("Add 1"),"space=8");

      zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
      zdialog_add_widget(zd,"check","keepopen","hb4",E2X("keep this dialog open"),"space=3");

      zdialog_restore_inputs(zd);
      zdialog_run(zd,rename_dialog_event,"parent");                              //  run dialog
   }

   zdialog *zd = zd_rename;
   parsefile(rename_file,&pdir,&pfile,&pext);
   strncpy0(rename_old,pfile,199);
   strncpy0(rename_new,pfile,199);
   zdialog_stuff(zd,"oldname",rename_old);                                       //  current file name
   zdialog_stuff(zd,"newname",rename_new);                                       //  entered file name (same)

   return;
}


//  dialog event and completion callback function

int rename_dialog_event(zdialog *zd, cchar *event)
{
   char     *pp, *pdir, *pfile, *pext, suffix[16];
   char     *newfile = 0, *nextfile = 0, namever[200];
   int      nseq, digits, ccp, ccn, ccx, err, Fkeep;
   STATB    statb;
   
   if (strmatch(event,"prev"))                                                  //  previous name >> new name
   {
      if (! *rename_prev) return 1;
      parsefile(rename_prev,&pdir,&pfile,&pext);                                 //  get previous rename name
      pp = strrchr(rename_prev,'.');
      if (pp && strlen(pp) == 4 && pp[1] == 'v'                                  //  look for file version .vNN
         && pp[2] >= '0' && pp[2] <= '9'                                         //    and remove if found
         && pp[3] >= '0' && pp[3] <= '9') *pp = 0;
      zdialog_stuff(zd,"newname",rename_prev);                                   //  stuff prev rename name into dialog

      if (! rename_file) return 1;
      parsefile(rename_file,&pdir,&pfile,&pext);                                 //  curr. file to be renamed
      pp = strrchr(pfile,'.');                                                   //  look for file version .vNN
      if (pp && strlen(pp) == 4 && pp[1] == 'v'
         && pp[2] >= '0' && pp[2] <= '9'
         && pp[3] >= '0' && pp[3] <= '9') 
      {
         *namever = 0;                                                           //  prev rename name + curr file version
         strncatv(namever,200,rename_prev,pp,null);
         zdialog_stuff(zd,"newname",namever);                                    //  stuff into dialog
      }
   }     

   if (strmatch(event,"Badd1"))                                                  //  increment sequence number
   {
      zdialog_fetch(zd,"newname",rename_new,188);                                //  get entered filename
      pp = strrchr(rename_new,'.');                                              //  find .ext
      if (pp && strlen(pp) < 8) {
         if (strmatchN(pp-4,".v",2) &&                                           //  find .vNN.ext
            (pp[2] >= '0' && pp[2] <= '9') && 
            (pp[3] >= '0' && pp[3] <= '9')) pp -= 4;
         strncpy0(suffix,pp,16);                                                 //  save .ext or .vNN.ext
      }
      else {
         pp = rename_new + strlen(rename_new);
         *suffix = 0;
      }

      digits = 0;
      while (pp[-1] >= '0' && pp[-1] <= '9') {
         pp--;                                                                   //  look for NNN in filenameNNN
         digits++;
      }
      nseq = 1;
      if (digits) nseq += atoi(pp);                                              //  NNN + 1
      if (nseq > 9999) nseq = 1;
      if (digits < 1) digits = 1;
      if (nseq > 9 && digits < 2) digits = 2;
      if (nseq > 99 && digits < 3) digits = 3;
      if (nseq > 999 && digits < 4) digits = 4;
      sprintf(pp,"%0*d",digits,nseq);                                            //  replace NNN with NNN + 1
      strncat(rename_new,suffix,199);                                            //  append .ext or .vNN.ext
      zdialog_stuff(zd,"newname",rename_new);
   }
   
   if (zd->zstat == 0) return 1;                                                 //  not finished
   if (zd->zstat != 1) goto KILL;                                                //  canceled

   zd->zstat = 0;                                                                //  apply - keep dialog active
   
   if (! rename_file) return 1;
   if (checkpend("all")) return 1;

   parsefile(rename_file,&pdir,&pfile,&pext);                                    //  existing /folders/file.ext

   zdialog_fetch(zd,"newname",rename_new,194);                                   //  new file name from user

   ccp = strlen(pdir);                                                           //  length of /folders/
   ccn = strlen(rename_new);                                                     //  length of file
   if (pext) ccx = strlen(pext);                                                 //  length of .ext
   else ccx = 0;

   newfile = (char *) zmalloc(ccp + ccn + ccx + 1);                              //  put it all together
   strncpy(newfile,rename_file,ccp);                                             //   /folders.../newfilename.ext
   strcpy(newfile+ccp,rename_new);
   if (ccx) strcpy(newfile+ccp+ccn,pext);

   err = stat(newfile,&statb);                                                   //  check if new name exists
   if (! err) {
      zmessageACK(Mwin,Bfileexists);
      zfree(newfile);
      newfile = 0;
      return 1;
   }

   if (FGWM == 'F')
      nextfile = gallery(0,"get",curr_file_posn+1);                              //  save next file, before rename
   else nextfile = 0;

   err = rename(rename_file,newfile);                                            //  rename the file
   if (err) {
      zmessageACK(Mwin,"file error: %s",strerror(errno));
      goto KILL;
   }
   
   load_filemeta(newfile);                                                       //  add new file to image index
   update_image_index(newfile);
   delete_image_index(rename_file);                                              //  delete old file in image index

   delete_thumbfile(rename_file);                                                //  remove thumbnails, disk and cache
   delete_thumbfile(newfile);                                                    //  (will auto add when referenced)

   add_recent_file(newfile);                                                     //  first in recent files list
   
   strncpy0(rename_prev,rename_new,199);                                         //  save new name to previous name

   if (curr_file && strmatch(rename_file,curr_file))                             //  current file exists no more
      free_resources();

   if (navi::gallerytype == GDIR) {                                              //  refresh gallery list
      gallery(0,"init",0);
      gallery(0,"sort",-2);                                                      //  recall sort and position
      gallery(0,"paint",-1);
   }

   zdialog_fetch(zd,"keepopen",Fkeep);
   if (Fkeep) {                                                                  //  keep dialog open
      if (FGWM == 'F' && nextfile) {
         f_open(nextfile);                                                       //  will call m_rename()
         gtk_window_present(MWIN);                                               //  keep focus on main window
      }
      if (nextfile) zfree(nextfile);
      nextfile = 0;
      if (newfile) zfree(newfile);
      newfile = 0;
      return 1;
   }
   else {                                                                        //  close dialog
      f_open(newfile);                                                           //  open renamed file
      goto KILL;
   }

KILL:
   if (nextfile) zfree(nextfile);
   nextfile = 0;
   if (newfile) zfree(newfile);
   newfile = 0;
   zdialog_free(zd);                                                             //  kill dialog
   zd_rename = 0;
   return 1;
}


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

//  show and change file permissions

char  *permissions_file = 0;

void m_permissions(GtkWidget *, cchar *menu)                                     //  20.0
{
   int permissions_dialog_event(zdialog *zd, cchar *event);

   char     permissions[100];
   cchar    *pp;
   STATB    statb;
   mode_t   mode;
   int      ftype;

   F1_help_topic = "permissions";

   if (FGWM != 'F' && FGWM != 'G') return;

   if (permissions_file) zfree(permissions_file);
   permissions_file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      permissions_file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else current file
      permissions_file = zstrdup(curr_file);

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

/***
       ______________________________________
      |       File Permissions               |
      |                                      |
      | File: [____________________________] |        file name
      |                                      |  
      | owner [read+write |v]                |
      | group [read only  |v]                |        combo box entries: read+write, read only, no access
      | other [no access  |v]                |
      |                                      |
      | [x] keep this dialog open            |
      |                                      |
      |                     [apply] [cancel] |
      |______________________________________|

***/

   if (! zd_permissions)                                                         //  restart dialog
   {
      zd_permissions = zdialog_new(E2X("File Permissions"),Mwin,Bapply,Bcancel,null);
      zdialog *zd = zd_permissions;
      
      zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labfile","hbfile",E2X("File:"),"space=3");
      zdialog_add_widget(zd,"label","filename","hbfile",0,"space=3");
      
      zdialog_add_widget(zd,"hbox","hbperm","dialog",0,"space=2");
      zdialog_add_widget(zd,"label","space","hbperm",0,"space=3");
      zdialog_add_widget(zd,"vbox","vb1","hbperm",0,"homog");
      zdialog_add_widget(zd,"label","space","hbperm",0,"space=6");
      zdialog_add_widget(zd,"vbox","vb2","hbperm",0,"homog");
      
      zdialog_add_widget(zd,"label","labown","vb1",ownertran,"space=3");
      zdialog_add_widget(zd,"combo","permown","vb2",0,"space=3");
      zdialog_cb_app(zd,"permown",RWtran);
      zdialog_cb_app(zd,"permown",ROtran);
      zdialog_cb_app(zd,"permown",NOtran);

      zdialog_add_widget(zd,"label","labgrp","vb1",grouptran,"space=3");
      zdialog_add_widget(zd,"combo","permgrp","vb2",0,"space=3");
      zdialog_cb_app(zd,"permgrp",RWtran);
      zdialog_cb_app(zd,"permgrp",ROtran);
      zdialog_cb_app(zd,"permgrp",NOtran);

      zdialog_add_widget(zd,"label","laboth","vb1",othertran,"space=3");
      zdialog_add_widget(zd,"combo","permoth","vb2",0,"space=3");
      zdialog_cb_app(zd,"permoth",RWtran);
      zdialog_cb_app(zd,"permoth",ROtran);
      zdialog_cb_app(zd,"permoth",NOtran);

      zdialog_add_widget(zd,"hbox","hbkeep","dialog",0,"space=3");
      zdialog_add_widget(zd,"check","keepopen","hbkeep",E2X("keep this dialog open"),"space=3");

      zdialog_resize(zd,280,0);
      zdialog_run(zd,permissions_dialog_event,"parent");                         //  run dialog
   }

   zdialog *zd = zd_permissions;
   
   pp = strrchr(permissions_file,'/');                                           //  stuff file name into dialog
   if (pp) pp++;
   else pp = permissions_file;
   zdialog_stuff(zd,"filename",pp);
   
   stat(permissions_file,&statb);                                                //  stuff curr. permissions
   mode = statb.st_mode;
   conv_permissions(mode,permissions);

   pp = strField(permissions,',',1);
   if (pp) zdialog_stuff(zd,"permown",pp);

   pp = strField(permissions,',',2);
   if (pp) zdialog_stuff(zd,"permgrp",pp);

   pp = strField(permissions,',',3);
   if (pp) zdialog_stuff(zd,"permoth",pp);

   return;
}


//  dialog event and completion callback function

int permissions_dialog_event(zdialog *zd, cchar *event)
{
   int         Fkeep;
   mode_t      mode;
   char        permown[20], permgrp[20], permoth[20];
   char        permissions[100];
   
   if (zd->zstat == 0) return 1;                                                 //  wait for dialog finished
   if (zd->zstat != 1) goto KILL;                                                //  [cancel] or [x]
   
   zdialog_fetch(zd,"permown",permown,20);                                       //  [apply]
   zdialog_fetch(zd,"permgrp",permgrp,20);                                       //  construct permissions string
   zdialog_fetch(zd,"permoth",permoth,20);                                       //  e.g. "read+write, read only, no access"
   snprintf(permissions,100,"%s, %s, %s",permown,permgrp,permoth);

   conv_permissions(permissions,mode);                                           //  convert to mode_t
   chmod(permissions_file,mode);                                                 //  set file permissions

   zdialog_fetch(zd,"keepopen",Fkeep);
   if (! Fkeep) goto KILL;
   zd->zstat = 0;                                                                //  keep dialog active
   gtk_window_present(MWIN);                                                     //  keep focus on main window 
   return 1;

KILL:
   if (permissions_file) zfree(permissions_file);                                //  free memory
   permissions_file = 0;
   zdialog_free(zd);                                                             //  kill dialog
   zd_permissions = 0;
   return 1;
}


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

//  Open a file and initialize Fpxb pixbuf.
//
//  Nth:    if Nth matches file position in current gallery, curr_file_posn
//          is set to Nth, otherwise it is searched and set correctly.
//          (a file can be present multiple times in an album gallery).
//  Fkeep:  edit undo/redo stack is not purged, and current edits are kept
//          after opening the new file (used by file_save()).
//  Fack:   failure will cause a popup ACK dialog.
//  fzoom:  keep current zoom level and position, otherwise fit window.
//
//  Following are set: curr_file_type, curr_file_bpc, curr_file_size.
//  Returns: 0 = OK, +N = error.
//  errors: 1  reentry (bug)
//          2  curr. edit function cannot be restarted or canceled
//          3  file not found or user cancel
//          4  unsupported file type or PXB_load() failure

int f_open(cchar *filespec, int Nth, int Fkeep, int Fack, int fzoom)
{
   PXB            *tempxb = 0;
   int            ii, err, fposn, retcode = 0;
   FTYPE          ftype;
   static int     Freent = 0;
   char           *pp, *file;
   void           (*menufunc)(GtkWidget *, cchar *);
   STATB          statb;
   static char    *last_folder = 0;
   char           *curr_folder;
   
   if (Freent++) {                                                               //  stop re-entry
      printz("f_open() re-entry \n");
      goto ret1;
   }

   if (CEF && ! CEF->Frestart) goto ret2;                                        //  edit function not restartable

   if (CEF) menufunc = CEF->menufunc;                                            //  active edit, save menu function
   else menufunc = 0;                                                            //    for possible edit restart

   if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"cancel");                     //  cancel if possible
   if (CEF) goto ret2;                                                           //  cannot

   sa_clear();                                                                   //  clear area if any

   file = 0;

   if (filespec)
      file = zstrdup(filespec);                                                  //  use passed filespec
   else {
      pp = curr_file;                                                            //  use file open dialog
      if (! pp && navi::gallerytype == GDIR) pp = navi::galleryname;             //  20.0
      file = zgetfile(E2X("Open Image File"),MWIN,"file",pp);
   }

   if (! file) goto ret3;

   err = stat(file,&statb);                                                      //  check file exists
   if (err) {
      if (Fack) zmessage_post(Mwin,"20/20",4,"%s \n %s",file,strerror(errno));
      zfree(file);
      goto ret3;
   }

   ftype = image_file_type(file);                                                //  reject thumbnail
   if (ftype == THUMB) {
      if (Fack) zmessageACK(Mwin,"thumbnail file");
      goto ret4;
   }

   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) {                       //  must be supported image file type
      if (Fack) zmessageACK(Mwin,E2X("unknown file type"));                      //    or RAW file or VIDEO file
      zfree(file);
      goto ret4;
   }

   Ffuncbusy = 1;                                                                //  may be large or RAW file, slow CPU
   tempxb = PXB_load(file,1);                                                    //  load image as PXB pixbuf
   Ffuncbusy = 0;

   if (! tempxb) {                                                               //  PXB_load() messages user
      zfree(file);
      goto ret4;
   }

   free_resources(Fkeep);                                                        //  free resources for old image file
   
   curr_file = file;                                                             //  new current file

   Fpxb = tempxb;                                                                //  pixmap for current image

   strcpy(curr_file_type,f_load_type);                                           //  set curr_file_xxx from f_load_xxx
   curr_file_bpc = f_load_bpc;
   curr_file_size = f_load_size;

   load_filemeta(curr_file);                                                     //  load metadata used by fotoxx

   fposn = file_position(file,Nth);                                              //  file position in gallery list
   if (fposn < 0) {                                                              //  not there
      gallery(curr_file,"init",0);                                               //  generate new gallery list
      gallery(0,"sort",-2);                                                      //  recall sort and position 
      gallery(curr_file,"paint",0);                                              //  position at curr. file
      fposn = file_position(curr_file,0);                                        //  position and count in gallery list
   }
   curr_file_posn = fposn;                                                       //  keep track of file position

   URS_reopen_pos = 0;                                                           //  not f_open_saved()

   if (! fzoom) {                                                                //  discard zoom state
      Fzoom = 0;                                                                 //  zoom level = fit window
      zoomx = zoomy = 0;                                                         //  no zoom center
   }

   set_mwin_title();                                                             //  set win title from curr_file info

   add_recent_file(curr_file);                                                   //  most recent file opened

   if (zd_rename) m_rename(0,0);                                                 //  update active rename dialog
   if (zd_permissions) m_permissions(0,0);                                       //    "  permissions dialog
   if (zd_copymove) m_copy_move(0,0);                                            //    "  copy/move dialog
   if (zd_deltrash) m_delete_trash(0,0);                                         //    "  delete/trash dialog
   if (zd_metaview) meta_view(0);                                                //    "  EXIF/IPTC data view window
   if (zd_editmeta) m_meta_edit_main(0,0);                                       //    "  edit metadata dialog
   if (zd_editanymeta) m_meta_edit_any(0,0);                                     //    "  edit any metadata dialog
   if (zd_deletemeta) m_meta_delete(0,0);                                        //    "  delete metadata dialog
   if (Fcaptions) m_meta_captions(0,0);                                          //  show caption/comments at top

   if (menufunc) menufunc(0,0);                                                  //  restart edit function
   
   for (ii = 0; ii < Ntopfolders; ii++)                                          //  check file under top folders
      if (strstr(file,topfolders[ii])) break;
   if (ii == Ntopfolders) {
      curr_folder = zstrdup(file);                                               //  not                                20.0
      pp = strrchr(curr_folder+1,'/');
      if (pp) *pp = 0;
//    if (! last_folder || ! strmatch(curr_folder,last_folder))                  //  notify, one time this folder
//       zmessage_post_bold(Mwin,"5/5",1,E2X("outside top folders"),null);       //  removed  20.0
      if (last_folder) zfree(last_folder);
      last_folder = curr_folder;
   }

   if (ftype == VIDEO)                                                           //  offer to play video file
      zmessage_post_bold(Mwin,"20/30",3,"VIDEO  press P to play");
   
   pp = strrchr(curr_file,'.');                                                  //  offer to play animated GIF file    20.0
   if (pp && strstr(".gif .GIF",pp))
      zmessage_post_bold(Mwin,"20/30",3,"GIF  press P to play");
  
   gallery(0,"paint",-1);                                                        //  paint gallery
   goto ret0; 

   ret4: retcode++;
   ret3: retcode++;
   ret2: retcode++;
   ret1: retcode++;
   ret0:
   Freent = 0;

   return retcode;
}


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

//  Open a file that was just saved. Used by file_save().
//  The edit undo/redo stack is not purged and current edits are kept.
//  Following are set:
//    curr_file  *_folder  *_file_posn  *_file_type  *_file_bpc  *_file_size
//  Returns: 0 = OK, +N = error.

int f_open_saved()
{
   int      Nth = -1, fposn;
   
   if (! f_save_file) return 1;

   if (E0pxm) {                                                                  //  edits were made
      PXB_free(Fpxb);                                                            //  new window image
      Fpxb = PXM_PXB_copy(E0pxm);
   }
   
   if (curr_file) zfree(curr_file);
   curr_file = zstrdup(f_save_file);                                             //  new current file

   fposn = file_position(curr_file,Nth);                                         //  file position in gallery list
   if (fposn < 0) {                                                              //  not there
      gallery(curr_file,"init",0);                                               //  generate new gallery list
      gallery(0,"sort",-2);                                                      //  recall sort and position
      gallery(curr_file,"paint",-1);                                             //  position at current file
      fposn = file_position(curr_file,0);                                        //  position and count in gallery list
   }
   curr_file_posn = fposn;                                                       //  keep track of file position

   strcpy(curr_file_type,f_save_type);                                           //  set curr_file_xxx from f_save_xxx
   curr_file_bpc = f_save_bpc;
   curr_file_size = f_save_size;

   URS_reopen_pos = URS_pos;                                                     //  track undo/redo stack position

   zoomx = zoomy = 0;                                                            //  no zoom center
   set_mwin_title();                                                             //  set win title from curr_file info

   if (zd_metaview) meta_view(0);                                                //  update EXIF/IPTC view window
   return 0;
}


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

//  Function to preload image files hopefully ahead of need.
//  Usage: f_preload(next)
//    next = -1 / +1  to read previous / next image file in curr. gallery
//    preload_file will be non-zero if and when preload_pxb is available.

void f_preload(int next)
{
   int      fd;
   char     *file;

   if (! curr_file) return;

   file = prev_next_file(next,Flastversion);
   if (! file) return;

   if (strmatch(file,curr_file)) {
      zfree(file);
      return;
   }

   fd = open(file,O_RDONLY);
   if (fd >= 0) {
      posix_fadvise(fd,0,0,POSIX_FADV_WILLNEED);                                 //  preload file in kernel cache
      close(fd);
   }

   zfree(file);
   return;
}


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

//  Open previous or next file in current gallery list, 
//  or jump to previous or next gallery when at the limits.
//  index is -1 or +1

void x_prev_next(int index)
{
   using namespace navi;

   char            *pp, *newfile = 0, *newgallery = 0;
   int             err, Nth = 0;
   static int      xbusy = 0;
   static int      jumpgallery = 0;
   static zdialog  *zd = 0;

   if (! curr_file) return;                                                      //  19.0
   if (FGWM != 'F' && FGWM != 'G') return;

   cchar    *mess1 = E2X("Start of gallery, preceding gallery: %s");
   cchar    *mess2 = E2X("End of gallery, following gallery: %s");
   
   if (zd && zdialog_valid(zd,"post")) zdialog_free(zd);                         //  clear prior popup message          19.0
   zd = 0;
   
   if (checkpend("busy block mods")) return;
   if (xbusy) return;                                                            //  avoid "function busy"
   xbusy = 1;

   newfile = prev_next_file(index,Flastversion);                                 //  get prev/next file (last version)
   if (newfile) {
      Nth = curr_file_posn + index;                                              //  albums can have repeat files
      err = f_open(newfile,Nth,0,1,1);                                           //  open image or RAW file             19.0
      if (! err) f_preload(index);                                               //  preload next image
      jumpgallery = 0;                                                           //  19.0
      goto returnx;
   }

   if (jumpgallery) {                                                            //  jump to prev/next gallery
      jumpgallery = 0;
      newgallery = prev_next_gallery(index);
      if (! newgallery) goto returnx;
      gallery(newgallery,"init",0);                                              //  load gallery
      gallery(0,"sort",-2);                                                      //  preserve sort 
      if (Nfiles - Nfolders > 0) {                                               //  at least one image file present
         if (index == +1) Nth = Nfolders;                                        //  get first or last image file
         if (index == -1) Nth = Nfiles - 1;
         newfile = gallery(0,"get",Nth);
         err = f_open(newfile,Nth,0,1,0);                                        //  open image or RAW file             19.0
         if (! err) gallery(newfile,"paint",Nth);
         if (! err) f_preload(index);                                            //  preload next image
         goto returnx;
      }
   }
   
   newgallery = prev_next_gallery(index);                                        //  check for prev/next gallery
   if (newgallery) {
      pp = strrchr(newgallery,'/');
      if (pp) pp++;
      if (index == -1) zd = zmessage_post_bold(Mwin,"5/5",3,mess1,pp,0);         //  prepare jump to prev/next gallery
      if (index == +1) zd = zmessage_post_bold(Mwin,"5/5",3,mess2,pp,0);         //    if user tries prev/next again
      jumpgallery = 1;
   }
   else {
      if (index == -1) zd = zmessage_post_bold(Mwin,"5/5",3,mess1,Bnone,0);      //  no prev/next gallery
      if (index == +1) zd = zmessage_post_bold(Mwin,"5/5",3,mess2,Bnone,0);
   }

returnx:
   if (newfile) zfree(newfile);
   if (newgallery) zfree(newgallery);
   Fpaint2();
   zmainloop();                                                                  //  refresh window (holding arrow key)
   xbusy = 0;
   return;
}


void m_prev(GtkWidget *, cchar *menu)
{
   F1_help_topic = "prev/next";
   x_prev_next(-1);                                                              //  search from curr_file -1
   return;                                                                       //    to first file
}


void m_next(GtkWidget *, cchar *menu)
{
   F1_help_topic = "prev/next";
   x_prev_next(+1);                                                              //  search from curr_file +1
   return;                                                                       //    to last file
}


void m_prev_next(GtkWidget *, cchar *menu)                                       //  left/right mouse click >>          20.0
{                                                                                //    previous/next image file
   F1_help_topic = "prev/next";

   if (FGWM != 'F') return;
   int button = zfuncs::vmenuclickbutton;
   if (button == 1) m_prev(0,0);
   else m_next(0,0);
   return;
}


void m_zoom_menu(GtkWidget *, cchar *menu)                                       //  left/right mouse click             20.0
{
   F1_help_topic = "[+-]";                                                       //  UG button name change              20.04 

   int button = zfuncs::vmenuclickbutton;

   if (FGWM == 'G') {                                                            //  G view, larger/smaller thumbnails
      if (button == 1) navi::menufuncx(0,"Zoom+");
      else navi::menufuncx(0,"Zoom-");
   }
   
   if (FGWM == 'F') {                                                            //  F view, zoom image in/out
      if (button == 1) m_zoom(0,"Zoom+");
      else m_zoom(0,"Zoom-");
   }
   
   return;
}


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

//  create a new blank image file with desired background color

void m_blank_image(GtkWidget *, cchar *pname)
{
   char        color[20], fname[100], fext[8], *filespec;
   int         zstat, err, cc, ww, hh, RGB[3];
   zdialog     *zd;
   cchar       *pp;
   char        *curr_folder = 0;

   F1_help_topic = "blank image";

   if (checkpend("all")) return;

   if (navi::gallerytype == GDIR) curr_folder = navi::galleryname;               //  20.0
   else curr_folder = topfolders[0];
   if (! curr_folder) {
      printz("no top image folders defined \n");                                 //  should not happen
      return;
   }

   Fblock = 1;

   m_viewmode(0,"F");                                                            //  19.0

/***
       __________________________________________________
      |           Create Blank Image                     |
      |                                                  |
      | file name [___________________________] [.jpg|v] |
      | width [____]  height [____] (pixels)             |
      | color [____]                                     |
      |                                                  |
      |                                  [Done] [Cancel] |
      |__________________________________________________|

***/


   zd = zdialog_new(E2X("Create Blank Image"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labf","hbf",E2X("file name"),"space=3");
   zdialog_add_widget(zd,"zentry","file","hbf",0,"space=3|expand");
   zdialog_add_widget(zd,"combo","ext","hbf",".jpg");
   zdialog_add_widget(zd,"hbox","hbz","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labw","hbz",Bwidth,"space=5");
   zdialog_add_widget(zd,"zspin","width","hbz","100|30000|1|1600");
   zdialog_add_widget(zd,"label","space","hbz",0,"space=5");
   zdialog_add_widget(zd,"label","labh","hbz",Bheight,"space=5");
   zdialog_add_widget(zd,"zspin","height","hbz","100|16000|1|1000");
   zdialog_add_widget(zd,"label","labp","hbz","(pixels)","space=3");
   zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labc","hbc",Bcolor,"space=5");
   zdialog_add_widget(zd,"colorbutt","color","hbc","200|200|200");

   zdialog_cb_app(zd,"ext",".jpg");
   zdialog_cb_app(zd,"ext",".png");
   zdialog_cb_app(zd,"ext",".tif");

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_stuff(zd,"file","");                                                  //  force input of new name

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

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

   zdialog_fetch(zd,"file",fname,92);                                            //  get new file name
   strTrim2(fname);
   if (*fname <= ' ') {
      zmessageACK(Mwin,E2X("supply a file name"));
      zdialog_free(zd);
      Fblock = 0;
      m_blank_image(0,0);                                                        //  retry
      return;
   }

   zdialog_fetch(zd,"ext",fext,8);                                               //  add extension
   strcat(fname,fext);
   
   cc = strlen(fname);
   filespec = zstrdup(curr_folder,cc+4);                                         //  make full filespec
   strcat(filespec,"/");
   strcat(filespec,fname);

   zdialog_fetch(zd,"width",ww);                                                 //  get image dimensions
   zdialog_fetch(zd,"height",hh);

   RGB[0] = RGB[1] = RGB[2] = 255;
   zdialog_fetch(zd,"color",color,19);                                           //  get image color
   pp = strField(color,"|",1);
   if (pp) RGB[0] = atoi(pp);
   pp = strField(color,"|",2);
   if (pp) RGB[1] = atoi(pp);
   pp = strField(color,"|",3);
   if (pp) RGB[2] = atoi(pp);

   zdialog_free(zd);

   err = create_blank_file(filespec,ww,hh,RGB);
   if (! err) f_open(filespec);                                                  //  make it the current file
   Fblock = 0;
   return;
}


//  function to create a new blank image file
//  file extension must be one of: .jpg .tif .png
//  RGB args are in the range 0 - 255
//  if file exists it is overwritten
//  returns 0 if OK, 1 if file exists, +N if error

int create_blank_file(cchar *file, int ww, int hh, int RGB[3])
{
   cchar          *pp;
   cchar          *fext;
   int            err, cstat;
   PXB            *tempxb;
   GError         *gerror = 0;
   uint8          *pixel;
   STATB          statb;

   err = stat(file,&statb);
   if (! err) {                                                                  //  file already exists
      zmessageACK(Mwin,Bfileexists);
      return 1;
   }

   pp = strrchr(file,'.');                                                       //  get file .ext
   if (! pp || strlen(pp) > 4) return 1;

   if (strmatch(pp,".jpg")) fext = "jpeg";                                       //  validate and set pixbuf arg.
   else if (strmatch(pp,".png")) fext = "png";
   else if (strmatch(pp,".tif")) fext = "tif";
   else return 2;

   tempxb = PXB_make(ww,hh,3);                                                   //  create pixbuf image

   for (int py = 0; py < hh; py++)                                               //  fill with color
   for (int px = 0; px < ww; px++)
   {
      pixel = PXBpix(tempxb,px,py);
      pixel[0] = RGB[0];
      pixel[1] = RGB[1];
      pixel[2] = RGB[2];
   }

   cstat = gdk_pixbuf_save(tempxb->pixbuf,file,fext,&gerror,null);
   if (! cstat) {
      zmessageACK(Mwin,"error: %s",gerror->message);
      PXB_free(tempxb);
      return 3;
   }

   PXB_free(tempxb);
   return 0;
}


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

//  blank or restore window

void m_blank_window(GtkWidget *, cchar *menu)
{
   static int     Fblank = 0;
   static char    *savefile = 0;

   F1_help_topic = "blank window";
   if (checkpend("all")) return;
   if (FGWM != 'F') m_viewmode(0,"F");                                           //  insure file view mode

   if (! Fblank) 
   {
      if (curr_file) {
         if (savefile) zfree(savefile);
         savefile = zstrdup(curr_file);
      }
      free_resources();
   }

   else if (savefile) {
      f_open(savefile);
      zfree(savefile);
      savefile = 0;
   }
   
   Fblank = 1 - Fblank;
   return;
}


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

//  animate given GIF file if possible

void play_gif(cchar *file)                                                       //  20.0
{
   int gif_animations_dialog_event(zdialog *, cchar *event);

   FILE        *fid;
   char        command[300], buff[20] = "", *pp = 0;
   zdialog     *zd;
   GtkWidget   *image;
   GdkPixbufAnimation *animation;
   
   snprintf(command,300,"exiftool -s3 -framecount \"%s\"",file);
   fid = popen(command,"r");
   if (fid) {
      pp = fgets(buff,20,fid);
      pclose(fid);
   }
   if (! pp || atoi(pp) < 2) return;

   zd = zdialog_new("GIF animation",Mwin,0);
   zdialog_add_widget(zd,"image","gif","dialog");
   image = zdialog_widget(zd,"gif");
   animation = gdk_pixbuf_animation_new_from_file(curr_file,0);
   gtk_image_set_from_animation(GTK_IMAGE(image),animation);
   zdialog_run(zd,gif_animations_dialog_event,"desktop");
   zdialog_can_focus(zd,0);  

   return;
}


//  dialog event and completion function

int gif_animations_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat) zdialog_free(zd); 
   return 1;
}


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

  right-click popup menu functions

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

//  copy or move an image file to a new location

char     *copymove_file = 0;

void m_copy_move(GtkWidget *, cchar *)                                           //  combine copy and move
{
   int copymove_dialog_event(zdialog *zd, cchar *event);

   cchar    *title = E2X("Copy or Move Image File");
   char     newloc[200];

   F1_help_topic = "copy/move";

   if (copymove_file) zfree(copymove_file);
   copymove_file = 0;

   if (clicked_file) {                                                           //  use clicked file if present
      copymove_file = clicked_file;
      clicked_file = 0;
   }
   else if (curr_file)                                                           //  else use current file
      copymove_file = zstrdup(curr_file);
   else return;

   if (checkpend("all")) return;
   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

/***
          ________________________________________________________
         |               Copy or Move Image File                  |
         |                                                        |
         | Image File: [_______________________________________]  |
         | New Location: [____________________________] [browse]  |
         | New Name: [___________________________________] [prev] |              //  19.0
         |                                                        |
         | (o) copy (duplicate file)  (o) move (remove original)  |
         | [x] keep this dialog open                              | 
         |                                                        |
         |                                      [apply] [cancel]  |
         |________________________________________________________|
***/

   if (! zd_copymove)
   {
      zd_copymove = zdialog_new(title,Mwin,Bapply,Bcancel,null);
      zdialog *zd = zd_copymove;

      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labf","hb1",E2X("Image File:"),"space=5");
      zdialog_add_widget(zd,"label","file","hb1");
      zdialog_add_widget(zd,"label","space","hb1",0,"expand");

      zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=2");
      zdialog_add_widget(zd,"label","labl","hb2",E2X("New Location:"),"space=5");
      zdialog_add_widget(zd,"zentry","newloc","hb2",0,"expand");
      zdialog_add_widget(zd,"button","browse","hb2",Bbrowse,"space=5");
      
      zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=2");
      zdialog_add_widget(zd,"label","labl","hb3",E2X("New Name:"),"space=5");
      zdialog_add_widget(zd,"zentry","newname","hb3",0,"expand");
      zdialog_add_widget(zd,"button","prev","hb3",Bprev,"space=5");
      
      zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
      zdialog_add_widget(zd,"radio","copy","hb4",E2X("copy (duplicate file)"),"space=5");
      zdialog_add_widget(zd,"radio","move","hb4",E2X("move (remove original)"),"space=5");

      zdialog_add_widget(zd,"hbox","hb5","dialog");
      zdialog_add_widget(zd,"check","keepopen","hb5",E2X("keep this dialog open"),"space=3");
      
      zdialog_add_ttip(zd,"prev",Bprevtip); 

      zdialog_stuff(zd,"copy",1);
      zdialog_stuff(zd,"move",0);
      zdialog_stuff(zd,"keepopen",0);

      zdialog_restore_inputs(zd);
      zdialog_fetch(zd,"newloc",newloc,200);
      if (*newloc != '/') zdialog_stuff(zd,"newloc",topfolders[0]); 
      
      zdialog_resize(zd,400,0);
      zdialog_run(zd,copymove_dialog_event,"parent");                            //  run dialog
   }

   zdialog *zd = zd_copymove;

   char *pp = strrchr(copymove_file,'/');                                        //  initz. newname from copymove_file
   if (pp) pp++;
   else pp = copymove_file;
   zdialog_stuff(zd,"file",pp);
   zdialog_stuff(zd,"newname",pp);                                               //  19.0

   if (copymove_loc && *copymove_loc == '/') 
      zdialog_stuff(zd,"newloc",copymove_loc);                                   //  last used 

   return;
}


//  dialog event and completion callback function

int copymove_dialog_event(zdialog *zd, cchar *event)
{
   int      Fcopy, Fmove;
   char     *newloc, *newfile = 0, *nextfile = 0;
   char     *pp1, *pp2;
   int      cc, err, Nth, Fkeep;
   STATB    statb;
   static char  newname[100], prevname[100];
   
   if (strmatch(event,"browse")) {                                               //  browse for new location
      if (! copymove_loc) {         
         copymove_loc = (char *) zmalloc(XFCC);
         zdialog_fetch(zd,"newloc",copymove_loc,XFCC);
      }
      newloc = zgetfile(E2X("Select folder"),MWIN,"folder",copymove_loc);
      if (! newloc) goto cleanup;
      zdialog_stuff(zd,"newloc",newloc);
      if (copymove_loc) zfree(copymove_loc);
      copymove_loc = newloc;
      goto cleanup;
   }
   
   if (strmatch(event,"prev"))                                                   //  [prev] button pressed              19.0
      if (*prevname) zdialog_stuff(zd,"newname",prevname);
   
   gtk_window_activate_focus(MWIN);                                              //  return focus to main window        19.0

   if (zd->zstat == 0) goto cleanup;                                             //  wait for dialog finished

   if (zd->zstat != 1) {                                                         //  cancel or [x]
      zdialog_free(zd);                                                          //  kill dialog
      zd_copymove = 0;
      goto cleanup;
   }

   zd->zstat = 0;                                                                //  apply - keep dialog active

   if (! copymove_file) goto cleanup;
   if (checkpend("all")) goto cleanup;

   err = stat(copymove_file,&statb);                                             //  source file not found              20.0
   if (err) {
      zmessageACK(Mwin,Bfilenotfound2,copymove_file);
      goto cleanup;
   }

   zdialog_fetch(zd,"copy",Fcopy);                                               //  one of these must be set           19.0
   zdialog_fetch(zd,"move",Fmove);
   if (Fcopy + Fmove != 1) goto cleanup;                                         //  should not happen

   if (copymove_loc) zfree(copymove_loc);                                        //  get new location from dialog
   copymove_loc = (char *) zmalloc(XFCC);
   zdialog_fetch(zd,"newloc",copymove_loc,XFCC);

   stat(copymove_loc,&statb);                                                    //  check for valid folder
   if (! S_ISDIR(statb.st_mode)) {
      zmessageACK(Mwin,E2X("new location is not a folder"));
      goto cleanup;
   }

   zdialog_fetch(zd,"newname",newname,100);
   cc = strlen(copymove_loc) + strlen(newname) + 2;                              //  new file = /new/location/newname.ext
   newfile = (char *) zmalloc(cc);
   strcpy(newfile,copymove_loc);
   strcat(newfile,"/");
   strcat(newfile,newname);

   pp1 = strrchr(copymove_file,'.');                                             //  check if user erased               20.0
   pp2 = strrchr(newfile,'.');                                                   //    or modified file .ext
   if (!pp1 || !pp2 || !strmatchcase(pp1,pp2)) {
      if (pp1) {
         if (! pp2) pp2 = newfile + strlen(newfile);
         strcpy(pp2,pp1);
      }
      else {
         zmessageACK(Mwin,E2X("new file extension missing or changed"));
         goto cleanup;
      }
   }

   err = stat(newfile,&statb);                                                   //  check if new file exists
   if (! err) {
      zmessageACK(Mwin,Bfileexists);
      goto cleanup;
   }

   err = copyFile(copymove_file,newfile);                                        //  copy source file to new file
   if (err) {
      zmessageACK(Mwin,strerror(err));
      zdialog_free(zd);
      zd_copymove = 0;
      goto cleanup;
   }

   load_filemeta(newfile);                                                       //  update image index for new file
   update_image_index(newfile);                                                  //  (memory metadata now invalid)
   
   add_recent_file(newfile);                                                     //  20.0
   
   strcpy(prevname,newname);                                                     //  set new 'previous' name            19.0

   if (FGWM == 'F') {                                                            //  if F-view, get next file
      Nth = curr_file_posn + 1;
      nextfile = gallery(0,"get",Nth);
   }

   if (Fmove) {                                                                  //  move - delete source file
      err = remove(copymove_file);
      if (err) {
         zmessageACK(Mwin,E2X("delete failed: \n %s"),strerror(errno));
         zdialog_free(zd);
         zd_copymove = 0;
         goto cleanup;
      }

      delete_image_index(copymove_file);                                         //  delete in image index
      delete_thumbfile(copymove_file);                                           //  delete thumbnail file and cache
      if (curr_file && strmatch(curr_file,copymove_file))                        //  current file gone
         free_resources();
   }
   
   if (Fcopy) 
      if (curr_file && strmatch(curr_file,copymove_file))                        //  current file was copied
         load_filemeta(curr_file);                                               //  restore curr. metadata             19.0

   if (FGWM == 'F') {                                                            //  F-view
      if (nextfile) f_open(nextfile);                                            //  open next file
      else {                                                                     //  end of gallery
         if (Fmove) free_resources();                                            //  no curr. file
         zdialog_free(zd);                                                       //  kill dialog
         zd_copymove = 0;
      }
      gtk_window_present(MWIN);                                                  //  keep focus on main window 
   }
   
   if (FGWM == 'G' && Fmove)                                                     //  G-view and file is gone
      zdialog_stuff(zd,"file","");

   if (navi::gallerytype == GDIR) {                                              //  refresh gallery
      gallery(0,"init",0);
      gallery(0,"sort",-2);                                                      //  recall sort and position
      gallery(0,"paint",-1);
   }
   
   zdialog_fetch(zd,"keepopen",Fkeep);
   if (Fkeep) goto cleanup;                                                      //  keep dialog open

   zdialog_free(zd);                                                             //  kill dialog
   zd_copymove = 0;
   goto cleanup;

cleanup:
   if (nextfile) zfree(nextfile);                                                //  free memory                        20.0
   if (newfile) zfree(newfile);
   return 1;
}


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

//  copy selected image file to the desktop

void m_copyto_desktop(GtkWidget *, cchar *)
{
   char     *file, *pp, newfile[XFCC];
   STATB    statbuff;
   int      err;
   
   F1_help_topic = "copy to desktop";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   
   file = clicked_file;
   if (! file) file = curr_file;
   if (! file) return;
   
   snprintf(newfile,100,"%s/%s",getenv("HOME"),desktopname);                     //  locale specific desktop name
   pp = strrchr(file,'/');
   if (! pp) return;
   strncatv(newfile,XFCC,pp,0);                                                  //  new desktop file name
   
   err = stat(newfile,&statbuff);                                                //  check if file exists
   if (! err) {
      int yn = zmessageYN(Mwin,E2X("Overwrite file? \n %s"),newfile);            //  confirm overwrite
      if (! yn) goto cleanup;
   }
   
   if (curr_file && strmatch(file,curr_file))                                    //  current file is copied      bugfix 19.0
      f_save(newfile,curr_file_type,curr_file_bpc,0,1);                          //  preserve edits
   
   else {
      err = copyFile(file,newfile);                                              //  copy other file
      if (err) {
         zmessageACK(Mwin,strerror(err));
         goto cleanup;
      }
      load_filemeta(newfile);                                                    //  update image index          bugfix 19.0
      update_image_index(newfile);
   }

cleanup:
   if (clicked_file) zfree(clicked_file);
   clicked_file = 0;
   return;
}


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

//  copy selected image file to the clipboard

void m_copyto_clip(GtkWidget *, cchar *)
{
   int file_copytoclipboard(char *file);
   
   char     tempfile[200];
   int      err;
   
   F1_help_topic = "copy to clipboard";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (clicked_file) {
      file_copytoclipboard(clicked_file);                                        //  get file behind thumbnail
      zfree(clicked_file);
      clicked_file = 0;
      return;
   }

   if (! curr_file) return;                                                      //  no current file
   
   snprintf(tempfile,200,"%s/clipboard.jpg",temp_folder);                        //  may be edited and unsaved
   err = f_save(tempfile,"jpg",8,0,1);
   if (err) return;
   file_copytoclipboard(tempfile);
   remove(tempfile);

   return;
}


//  copy an image file to the clipboard (as pixbuf)
//  any prior clipboard image is replaced
//  supports copy/paste to other apps (not used in fotoxx)
//  returns 1 if OK, else 0

int file_copytoclipboard(char *file)
{
   GtkClipboard   *clipboard;
   PIXBUF         *pixbuf;
   GError         *gerror = 0;

   clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
   if (! clipboard) return 0;
   gtk_clipboard_clear(clipboard);

   pixbuf = gdk_pixbuf_new_from_file(file,&gerror);
   if (! pixbuf) return 0;

   gtk_clipboard_set_image(clipboard,pixbuf);
   g_object_unref(pixbuf);
   
   return 1;
}


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

//  set current file as desktop wallpaper (GNOME only)

void m_wallpaper(GtkWidget *, cchar *)                                           //  19.0
{
   cchar *  key = "gsettings set org.gnome.desktop.background";
   cchar *  id = "picture-uri";

   F1_help_topic = "set wallpaper";

   if (! curr_file) return;   
   shell_ack("%s %s \"file://%s\" ",key,id,curr_file);
   return;
}


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

//  Delete or Trash an image file.
//  Use the Linux standard trash function.
//  If not available, show diagnostic and do nothing.

char  *delete_trash_file = 0;

void m_delete_trash(GtkWidget *, cchar *)                                        //  combined delete/trash function
{
   int delete_trash_dialog_event(zdialog *zd, cchar *event);

   cchar  *title = E2X("Delete/Trash Image File");
   
   F1_help_topic = "delete/trash";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (delete_trash_file) zfree(delete_trash_file);
   delete_trash_file = 0;
   
   if (clicked_file) {                                                           //  use clicked file if present 
      delete_trash_file = clicked_file;
      clicked_file = 0;
   }

   else if (curr_file)                                                           //  else current file
      delete_trash_file = zstrdup(curr_file);

   if (checkpend("all")) return;

/***
          ______________________________________
         |        Delete/Trash Image File       |
         |                                      |
         | Image File: [______________________] |
         |                                      |
         | [x] keep this dialog open            |
         |                                      |
         |            [delete] [trash] [cancel] |
         |______________________________________|

***/

   if (! zd_deltrash)                                                            //  start dialog if not already
   {
      zd_deltrash = zdialog_new(title,Mwin,Bdelete,Btrash,Bcancel,null);
      zdialog *zd = zd_deltrash;

      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labf","hb1",E2X("Image File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hb1",0,"space=3");
      zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
      zdialog_add_widget(zd,"check","keepopen","hb2",E2X("keep this dialog open"),"space=3");

      zdialog_resize(zd,300,0);
      zdialog_restore_inputs(zd);
      zdialog_run(zd,delete_trash_dialog_event,"parent");                        //  run dialog
   }
   
   if (delete_trash_file) 
   {
      char *pp = strrchr(delete_trash_file,'/');
      if (pp) pp++;
      else pp = delete_trash_file;
      zdialog_stuff(zd_deltrash,"file",pp);
   }
   else zdialog_stuff(zd_deltrash,"file","");

   return;
}


//  dialog event and completion callback function

int delete_trash_dialog_event(zdialog *zd, cchar *event)
{
   int         err = 0, yn = 0;
   int         Nth, gstat, ftype, Fkeep;
   char        *file;
   GError      *gerror = 0;
   GFile       *gfile = 0;
   STATB       statb;

   cchar    *trashmess = E2X("GTK g_file_trash() function failed");

   if (zd->zstat == 0) return 1;                                                 //  wait for dialog finished
   if (zd->zstat == 1) goto PREP;                                                //  [delete] button
   if (zd->zstat == 2) goto PREP;                                                //  [trash] button
   goto KILL;                                                                    //  [cancel] or [x]

PREP:

   if (! delete_trash_file) goto KILL;                                           //  no file to delete or trash

   err = stat(delete_trash_file,&statb);                                         //  check file exists
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      goto KILL;
   }

   ftype = image_file_type(delete_trash_file);
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) {                       //  must be image or RAW or VIDEO 
      zmessageACK(Mwin,E2X("not a known image file"));
      goto KILL;
   }

   if (! (statb.st_mode & S_IWUSR)) {                                            //  check permission
      if (zd->zstat == 1)
         yn = zmessageYN(Mwin,E2X("Delete read-only file?"));
      if (zd->zstat == 2)
         yn = zmessageYN(Mwin,E2X("Trash read-only file?"));
      if (! yn) goto NEXT;                                                       //  keep file
      statb.st_mode |= S_IWUSR;                                                  //  make writable if needed
      err = chmod(delete_trash_file,statb.st_mode);
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         goto KILL;
      }
   }
   
   if (checkpend("all")) return 1;                                               //  check for conflicts

   if (zd->zstat == 1) goto DELETE;                                              //  [delete] button
   if (zd->zstat == 2) goto TRASH;                                               //  [trash] button
   
DELETE:

   err = remove(delete_trash_file);                                              //  delete file
   if (! err) goto BOTH;
   zmessageACK(Mwin,E2X("delete failed: \n %s"),strerror(errno));
   goto KILL;

TRASH:

   gfile = g_file_new_for_path(delete_trash_file);
   gstat = g_file_trash(gfile,0,&gerror);                                        //  move file to trash
   g_object_unref(gfile);
   if (gstat) goto BOTH;
   zmessageACK(Mwin,trashmess);
   if (gerror) printz("%s\n",gerror->message);
   goto KILL;

BOTH:

   delete_image_index(delete_trash_file);                                        //  delete file in image index
   delete_thumbfile(delete_trash_file);                                          //  delete thumbnail file and cache

   Nth = file_position(delete_trash_file,0);                                     //  find in gallery list
   if (Nth >= 0) gallery(0,"delete",Nth);                                        //  delete from gallery list

   if (curr_file && strmatch(delete_trash_file,curr_file)) {                     //  current file was removed
      last_file_posn = curr_file_posn;                                           //  remember for prev/next use 
      free_resources();
   }

   if (FGWM == 'F') {                                                            //  F window
      file = gallery(0,"get",curr_file_posn);                                    //  new current file = next
      if (file) {
         f_open(file);
         zfree(file);
      }
      else m_viewmode(0,"G");                                                    //  no next, show gallery              20.0

      zdialog_fetch(zd,"keepopen",Fkeep);
      if (! Fkeep) goto KILL;

      zd->zstat = 0;                                                             //  keep dialog active
      gtk_window_present(MWIN);                                                  //  keep focus on main window
      return 1;
   }

NEXT:

   if (FGWM == 'G') {                                                            //  gallery view
      gallery(0,"init",0);                                                       //  refresh for removed file
      gallery(0,"sort",-2);                                                      //  recall sort and position 
      gallery(0,"paint",-1);                                                     //  paint
      zdialog_stuff(zd,"file","");                                               //  erase file in dialog
   }

   if (FGWM == 'F') m_next(0,0);                                                 //  file view mode, open next image
   
   zdialog_fetch(zd,"keepopen",Fkeep);
   if (Fkeep) {
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

KILL:

   if (delete_trash_file) zfree(delete_trash_file);                              //  free memory
   delete_trash_file = 0;
   zdialog_free(zd);                                                             //  kill dialog
   zd_deltrash = 0;
   return 1;
}


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

//  print image file

void m_print(GtkWidget *, cchar *)                                               //  use GTK print
{
   int print_addgrid(PXB *Ppxb);

   int      pstat;
   char     *printfile = 0;
   PXB      *Ppxb = 0;
   GError   *gerror = 0;

   F1_help_topic = "print";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (clicked_file) Ppxb = PXB_load(clicked_file,1);                            //  clicked thumbnail
   else if (E0pxm) Ppxb = PXM_PXB_copy(E0pxm);                                   //  current edited file
   else if (curr_file) Ppxb = PXB_load(curr_file,1);                             //  current file
   clicked_file = 0;
   if (! Ppxb) return;

   print_addgrid(Ppxb);                                                          //  add grid lines if wanted

   printfile = zstrdup(temp_folder,20);                                          //  make temp print file:
   strcat(printfile,"/printfile.bmp");                                           //    /.../fotoxx-nnnnnn/printfile.bmp

   pstat = gdk_pixbuf_save(Ppxb->pixbuf,printfile,"bmp",&gerror,null);           //  bmp much faster than png
   if (! pstat) {
      zmessageACK(Mwin,"error: %s",gerror->message);
      PXB_free(Ppxb);
      zfree(printfile);
      return;
   }

   print_image_file(Mwin,printfile);

   PXB_free(Ppxb);
   zfree(printfile);
   return;
}


//  add grid lines to print image if wanted

int print_addgrid(PXB *Ppxb)
{
   uint8    *pix;
   int      px, py, ww, hh;
   int      startx, starty, stepx, stepy;
   int      G = currgrid;

   if (! gridsettings[G][GON]) return 0;                                         //  grid lines off

   ww = Ppxb->ww;
   hh = Ppxb->hh;

   stepx = gridsettings[G][GXS];                                                 //  space between grid lines
   stepy = gridsettings[G][GYS];

   stepx = stepx / Mscale;                                                       //  window scale to image scale
   stepy = stepy / Mscale;

   if (gridsettings[G][GXC])                                                     //  if line counts specified,
      stepx = ww / (1 + gridsettings[G][GXC]);                                   //    set spacing accordingly
   if (gridsettings[G][GYC])
      stepy = hh / ( 1 + gridsettings[G][GYC]);

   startx = stepx * gridsettings[G][GXF] / 100;                                  //  variable offsets
   if (startx < 0) startx += stepx;

   starty = stepy * gridsettings[G][GYF] / 100;
   if (starty < 0) starty += stepy;

   if (gridsettings[G][GX]) {                                                    //  x-grid enabled
      for (px = startx; px < ww-1; px += stepx)
      for (py = 0; py < hh; py++)
      {
         pix = PXBpix(Ppxb,px,py);
         pix[0] = pix[1] = pix[2] = 255;
         pix[3] = pix[4] = pix[5] = 0;
      }
   }

   if (gridsettings[G][GY]) {                                                    //  y-grid enabled
      for (py = starty; py < hh-1; py += stepy)
      for (px = 0; px < ww; px++)
      {
         pix = PXBpix(Ppxb,px,py);
         pix[0] = pix[1] = pix[2] = 255;
         pix = PXBpix(Ppxb,px,py+1);
         pix[0] = pix[1] = pix[2] = 0;
      }
   }

   return 1;
}


//  print calibrated image
//  menu function calling print_calibrated() in f.tools.cc

void m_print_calibrated(GtkWidget *, cchar *)
{
   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   print_calibrated();
   return;
}


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

//  normal quit menu function
//  does not return

void m_quit(GtkWidget *, cchar *)
{
   zdialog  *zd;
   int      busy;

   if (checkpend("mods")) return;                                                //  allow user bailout

   if (zfuncs::zdialog_busy) {
      for (int ii = 0; ii < zfuncs::zdialog_count; ii++) {
         zd = zfuncs::zdialog_list[ii];
         if (strmatch(zd->title,"userguide")) continue;                          //  omit this dialog                   20.0
         zdialog_show(zd,1);                                                     //  un-hide minimized dialog           20.0
         int yn = zmessageYN(Mwin,E2X("Kill active dialog? %s"),zd->title);      //  allow user bailout
         if (! yn) return;
         zdialog_free(zd);
      }
   }

   printz("quit \n");

   Fshutdown++;

   for (int ii = 0; ii < 20; ii++) {                                             //  wait up to 2 secs. if something running
      busy = checkpend("all quiet");
      if (! busy) break;
      zmainsleep(0.1);
   }

   if (busy) printz("busy function killed \n");

   quitxx();                                                                     //  does not return
}


//  used also for main window destroy event
//  does not return

void quitxx()
{
   Fshutdown++;
   gtk_window_get_position(MWIN,&mwgeom[0],&mwgeom[1]);                          //  get last window position
   gtk_window_get_size(MWIN,&mwgeom[2],&mwgeom[3]);                              //    and size for next session
   zdialog_inputs("save");                                                       //  save dialog inputs
   zdialog_geometry("save");                                                     //  save dialogs position/size
   gallery_memory("save");                                                       //  save recent gallery positions
   free_resources();                                                             //  free memory
   shell_quiet("rm -R -f %s",temp_folder);                                       //  delete temp files
   save_params();                                                                //  save state for next session
   fflush(null);                                                                 //  flush stdout, stderr
   /// gtk_main_quit();                                                          //  exit GTK
   zexit("Fotoxx normal exit");
}


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

//  help menu function

void m_help(GtkWidget *, cchar *menu)
{
   char     *pp;

   if (strmatch(menu,E2X("User Guide")))
      showz_docfile(Mwin,"userguide",0);                                         //  20.0
   
   if (strmatch(menu,E2X("Recent Changes")))
      showz_docfile(Mwin,"userguide","recent changes");                          //  20.0

   if (strmatch(menu,E2X("Edit Functions Overview")))                            //  20.0
      showz_docfile(Mwin,"userguide","edit overview");

   if (strmatch(menu,"Validate User Guide"))                                     //  developer function                 20.0
      showz_docfile(Mwin,"userguide","validate");

   if (strmatch(menu,E2X("Change Log")))
      showz_textfile("doc","changelog",Mwin);

   if (strmatch(menu,E2X("Log File")))
      showz_logfile(Mwin);                                                       //  20.0
      
   if (strmatch(menu,"Man Page"))
      showz_textfile("doc","fotoxx.man",Mwin);

   if (strmatch(menu,E2X("Command Params")))
      showz_docfile(Mwin,"userguide","command parameters");                      //  20.0

   if (strmatch(menu,E2X("Translations")))
      showz_docfile(Mwin,"userguide","translations");                            //  20.0

   if (strmatch(menu,E2X("Home Page")))
      showz_html(Fhomepage);

   if (strmatch(menu,E2X("License")))                                            //  19.0
      showz_textfile("doc","license",Mwin);

   if (strmatch(menu,E2X("Privacy")))                                            //  19.1
      showz_html("https://gitlab.com/fotoxx/fotoxx/wikis/privacy");

   if (strmatch(menu,E2X("About"))) {
      if (zfuncs::appimagexe) pp = zfuncs::appimagexe;
      else pp = zfuncs::progexe;
      zmessageACK(Mwin," %s  %s \n %s \n\n"" %s    %s \n %s \n\n"" %s \n",
         Frelease, zfuncs::build_date_time, pp,
         Fhomepage, Fcontact, Fgitlab, Ftranslators);
   }

   if (strmatch(menu,E2X("Help")))                                               //  menu button
      showz_docfile(Mwin,"userguide",F1_help_topic);                             //  show topic if there, or page 1

   return;
}


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

//  save (modified) image file to disk

void m_file_save(GtkWidget *, cchar *menu)
{
   int  file_save_dialog_event(zdialog *zd, cchar *event);

   cchar          *pp;
   zdialog        *zd;

   F1_help_topic = "file save";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

   if (! curr_file) {
      if (zd_filesave) zdialog_destroy(zd_filesave);
      zd_filesave = 0;
      return;
   }
   
   if (strmatch(curr_file_type,"other"))                                         //  if unsupported type, use jpg
      strcpy(curr_file_type,"jpg");

/***
          _______________________________________________
         |        Save Image File                        |
         |                                               |
         |   filename.jpg                                |
         |                                               |
         |  [new version] save as new file version       |
         |  [new file ...] save as new file name or type |
         |  [replace file] replace file (OVERWRITE)      |
         |                                               |
         |                                    [cancel]   |
         |_______________________________________________|

***/

   if (! zd_filesave)
   {
      zd = zdialog_new(E2X("Save Image File"),Mwin,Bcancel,null);
      zd_filesave = zd;

      zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","filename","hbf",0,"space=10");

      zdialog_add_widget(zd,"hbox","hb0","dialog");
      zdialog_add_widget(zd,"vbox","vb1","hb0",0,"space=3|homog");
      zdialog_add_widget(zd,"vbox","vb2","hb0",0,"space=5|homog");

      zdialog_add_widget(zd,"button","newvers","vb1",E2X("new version"));
      zdialog_add_widget(zd,"button","newfile","vb1",E2X("new file ..."));
      zdialog_add_widget(zd,"button","replace","vb1",E2X("replace file"));

      zdialog_add_widget(zd,"hbox","hb1","vb2");
      zdialog_add_widget(zd,"hbox","hb2","vb2");
      zdialog_add_widget(zd,"hbox","hb3","vb2");

      zdialog_add_widget(zd,"label","labvers","hb1",E2X("save as new file version"));
      zdialog_add_widget(zd,"label","labfile","hb2",E2X("save as new file name or type"));
      zdialog_add_widget(zd,"icon","warning","hb3","warning.png","size=30");
      zdialog_add_widget(zd,"label","labrepl","hb3",E2X("replace old file (OVERWRITE)"),"space=3");
   }

   zd = zd_filesave;

   pp = strrchr(curr_file,'/');
   if (pp) zdialog_stuff(zd,"filename",pp+1);

   zdialog_run(zd,file_save_dialog_event,"mouse");
   return;
}


//  dialog event and completion function

int file_save_dialog_event(zdialog *zd, cchar *event)
{
   char     *RP = 0, *newfilename = 0;
   int      err;
   
   if (zd->zstat) goto retx;                                                     //  cancel
   if (checkpend("busy block")) goto retx;
   if (! zstrstr("newvers newfile replace",event)) return 1;                     //  ignore other events

   if (strmatch(event,"newvers"))                                                //  new version
   {
      RP = f_realpath(curr_file);                                                //  use real path                      20.0
      if (! RP) {
         zmessageACK(Mwin,Bfilenotfound2,curr_file);
         goto retx;
      }
      
      newfilename = file_new_version(RP);                                        //  get next avail. file version name
      if (! newfilename) goto retx;
      
      if (strmatch(curr_file_type,"RAW"))                                        //  if RAW file, save as 16-bit TIFF   20.0
         err = f_save(newfilename,"tif",16,0,1);
      else if (strmatch(curr_file_type,"heic"))                                  //  if .heic file, save as JPEG        20.0
         err = f_save(newfilename,"jpg",8,0,1);
      else if (strmatch(curr_file_type,"jp2"))                                   //  if .jp2 file, save as PNG          20.0
         err = f_save(newfilename,"png",8,0,1);
      else 
         err = f_save(newfilename,curr_file_type,curr_file_bpc,0,1);             //  save with same file type

      if (! err) f_open_saved();                                                 //  open saved file with edit hist
      goto retx;
   }

   if (strmatch(event,"replace"))                                                //  replace
   {
      if (strmatch(curr_file_type,"RAW")) {
         zmessageACK(Mwin,E2X("cannot replace RAW file"));
         goto retx;
      }

      if (strmatch(curr_file_type,"heic")) {                                     //  20.0
         zmessageACK(Mwin,E2X("cannot replace HEIC file"));
         goto retx;
      }

      if (strmatch(curr_file_type,"jp2")) {                                      //  20.0
         zmessageACK(Mwin,E2X("cannot replace JP2 file"));
         goto retx;
      }

      RP = f_realpath(curr_file);                                                //  use real path                      20.0
      if (! RP) {
         zmessageACK(Mwin,Bfilenotfound2,curr_file);
         goto retx;
      }

      err = f_save(RP,curr_file_type,curr_file_bpc,0,1);                         //  save file with curr. edits applied
      if (err) goto retx;
      curr_file_size = f_save_size;
   }

   if (strmatch(event,"newfile"))
      err = f_save_as();                                                         //  save-as file chooser dialog

retx:
   if (RP) zfree(RP);
   if (newfilename) zfree(newfilename);
   zdialog_free(zd);                                                             //  kill dialog                        20.0
   zd_filesave = 0;
   return 1;
}


//  menu entry for KB shortcut Save File (replace)

void m_file_save_replace(GtkWidget *, cchar *menu)
{
   int      err;
   char     *RP;

   RP = f_realpath(curr_file);                                                   //  use real path                      20.0
   if (! RP) {
      zmessageACK(Mwin,Bfilenotfound2,curr_file);
      return;
   }

   err = f_save(RP,curr_file_type,curr_file_bpc,0,1);                            //  save file
   if (! err) f_open_saved();                                                    //  open saved file with edit hist
   zfree(RP); 
   return;
}


//  menu entry for KB shortcut Save File Version

void m_file_save_version(GtkWidget *, cchar *menu)
{
   char     *newfilename = 0, *RP = 0;
   char     newfiletype[8];
   int      err;

   RP = f_realpath(curr_file);                                                   //  use real path                      20.0
   if (! RP) {
      zmessageACK(Mwin,Bfilenotfound2,curr_file);
      return;
   }

   strncpy0(newfiletype,curr_file_type,6);
   newfilename = file_new_version(RP);                                           //  get next avail. file version name
   if (! newfilename) goto retx;
   err = f_save(newfilename,newfiletype,curr_file_bpc,0,1);                      //  save file
   if (! err) f_open_saved();                                                    //  open saved file with edit hist

retx:
   if (RP) zfree(RP);
   if (newfilename) zfree(newfilename);
   return;
}


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

//  get the root name of an image file, without version and extension
//     /.../filename.V03.png  -->  /.../filename
//  returned root name has extra length to add .vNN.exxxxt
//  returned root name is subject for zfree()

char * file_rootname(cchar *file)
{
   char  *rootname, *pp1, *pp2;
   
   rootname = zstrdup(file,16);
   pp1 = strrchr(rootname,'/');                                                  //  find file .ext
   if (! pp1) pp1 = rootname;
   pp2 = strrchr(pp1,'.');                                                       //  pp2 --> .ext or ending null
   if (! pp2) pp2 = pp1 + strlen(pp1);

   if (pp2[-4] == '.' && pp2[-3] == 'v' &&                                       //  look for .vNN.ext
       pp2[-2] >= '0' && pp2[-2] <= '9' &&
       pp2[-1] >= '0' && pp2[-1] <= '9') pp2 -= 4;                               //  pp2 -->  .ext or .vNN.ext
   *pp2 = 0;                                                                     //  pp2 -->  new ending null

   return rootname;
}


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

//  return base name for given file = original file name /.../filename.ext
//  returns null if base file does not exist.
//  returned base name has extra length to add .vNN.exxxxt
//  returned base name is subject for zfree()

char * file_basename(cchar *file)                                                //  19.0
{
   char     *rootname, **flist, *pp = 0;
   int      ii, cc, nf;
   
   flist = file_all_versions(file,nf);                                           //  get base file and all versions
   if (! nf) return 0;

   rootname = file_rootname(file);
   cc = strlen(rootname);
   zfree(rootname);
   
   for (ii = 0; ii < nf; ii++) 
   {
      pp = flist[ii];
      if (strmatchN(pp+cc,".v",2)) continue;
      if (pp[cc] == '.') break;
   }
   
   if (ii < nf) pp = zstrdup(pp,16);
   else pp = 0;
   
   for (ii = 0; ii < nf; ii++)
      zfree(flist[ii]);
   zfree(flist);
   
   return pp;
}


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

//  Get all versions of a given file name in sequence, including base file.
//  Returned list of file names is subject for zfree()
//  (each flist[*] member and flist itself)
//  returns null if nothing found

char ** file_all_versions(cchar *file, int &NF)                                  //  19.0
{
   char     **flist1, **flist2, *rootname;
   int      ii, jj, err, cc;
   
   rootname = file_rootname(file);                                               //  /.../fname.vNN.ext  -->  /.../fname
   if (! rootname) return 0;
   cc = strlen(rootname);
   strcpy(rootname + cc,".*");
   err = zfind(rootname,flist1,NF);                                              //  find /.../fname.*
   zfree(rootname);
   if (err || NF < 1) return 0;

   flist2 = (char **) zmalloc(NF * sizeof(char *));                              //  purge non-image type files         20.0
   for (ii = jj = 0; ii < NF; ii++)
      if (image_file_type(flist1[ii]) <= VIDEO) 
         flist2[jj++] = flist1[ii];
   zfree(flist1);
   NF = jj;

   if (NF == 0) {
      zfree(flist2);
      return 0;
   }

   if (NF == 1) return flist2;

   if (NF > 99) {                                                                //  screen out problem file
      zmessageACK(Mwin,E2X("file: %s \n exceed 99 versions"),file);
      zfree(flist2);
      return 0;
   }

   HeapSort(flist2,NF);                                                           //  sort in base, version order
   return flist2;
}


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

//  Get next available version  /.../filename.vNN.ext  for given file.
//  Returns  "/.../filename.v01.ext"  if no versions are found.
//  Returns null if bad file name or 99 versions already exist.
//  Returned file name is subject for zfree().

char * file_new_version(cchar *file)                                             //  19.0
{
   char     *retname, *pp, pext[8];
   char     **flist = 0;
   int      ii, cc, NF = 0, vers;
   
   pp = (char *) strrchr(file,'/');                                              //  find .ext for file
   if (! pp) return 0;
   pp = strrchr(pp,'.');
   if (pp) strncpy0(pext,pp,8);
   else strcpy(pext,".jpg");
   
   flist = file_all_versions(file,NF);                                           //  get base file and all versions
   if (! NF) {                                                                   //  nothing exists
      retname = file_rootname(file);                                             //  get /.../filename
      retname = zstrdup(retname,12);
      cc = strlen(retname);
      strcpy(retname+cc,".v01");                                                 //  return /.../filename.v01.ext
      strcpy(retname+cc+4,pext);
      return retname;
   }
   
   retname = zstrdup(flist[NF-1],12);                                            //  get last version found

   for (ii = 0; ii < NF; ii++)                                                   //  free memory
      zfree(flist[ii]);
   zfree(flist);

   pp = strrchr(retname,'/');                                                    //  find .ext
   if (! pp) return 0;
   pp = strrchr(pp,'.');
   if (! pp) {                                                                   //  none
      strcat(retname,"v.01.jpg");                                                //  return /.../filename.v01.jpg
      return retname;
   }
   
   if (strmatchN(pp-4,".v",2)) {                                                 //  find /.../filename.vNN.ext
      if (pp[-2] >= '0' && pp[-2] <= '9' &&                                      //                        |
          pp[-1] >= '0' && pp[-1] <= '9')                                        //                        pp
      {
         vers = 10 * (pp[-2] - '0') + pp[-1] - '0';
         if (vers >= 99) {
            zmessageACK(Mwin,E2X("file: %s \n exceed 99 versions"),file);
            return 0;
         }
         vers++;                                                                 //  add 1 to .vNN, leave same .ext
         pp[-2] = vers/10 + '0';
         pp[-1] = vers - 10 * (vers/10) + '0';
         return retname;                                                         //  return /.../filename.vNN.ext
      }
   }

   strncpy0(pext,pp,8);                                                          //  keep .ext
   strcpy(pp,".v01");                                                            //  return /.../filename.v01.ext
   strcpy(pp+4,pext);
   return retname;
}


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

//  Get the newest version  /.../filename.vNN.ext  for the given file.
//  Returns unversioned file  /.../filename.ext  if found by itself.
//  Returns null if bad file name.
//  Returned file name is subject for zfree().

char * file_newest_version(cchar *file)                                          //  19.0
{
   char     *retname;
   char     **flist;
   int      ii, NF;
   
   flist = file_all_versions(file,NF);                                           //  get base file and all versions
   if (! NF) return 0;

   retname = zstrdup(flist[NF-1],12);                                            //  get last version found

   for (ii = 0; ii < NF; ii++)                                                   //  free memory
      zfree(flist[ii]);
   zfree(flist);

   return retname;
}


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

//  Get the prior version  /.../filename.vNN.ext  for the given file.
//  Returns unversioned file  /.../filename.ext  if no prior .vNN found.
//  Returns null if bad file name or no prior version.

char * file_prior_version(cchar *file)                                           //  19.0
{
   char     *retname;
   char     **flist;
   int      ii, NF;
   
   flist = file_all_versions(file,NF);                                           //  get base file and all versions
   if (! NF) return 0;
   
   for (ii = 0; ii < NF; ii++)                                                   //  find input file in list
      if (strmatch(file,flist[ii])) break;
   
   if (ii == 0) return 0;                                                        //  input is base file (no prior)
   if (ii == NF) return 0;                                                       //  should not happen

   retname = zstrdup(flist[ii-1]);                                               //  prior file in list

   for (ii = 0; ii < NF; ii++)                                                   //  free memory
      zfree(flist[ii]);
   zfree(flist);

   return retname;
}


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

//  save current image to specified disk file (same or new).
//  update image index file and thumbnail file.
//  set f_save_type, f_save_bpc, f_save_size
//  returns 0 if OK, else +N.
//  If Fack is true, failure will cause a popup ACK dialog.

int f_save(char *outfile, cchar *outype, int outbpc, int qual, int Fack)
{
   cchar    *exifkey[5], *exifdata[5];
   char     *ppv[1], *tempfile, *pext, *outfile2;
   char     edithist[exif_maxcc];                                                //  edit history trail
   int      nkeys, err, cc1, cc2, ii, ii0;
   int      Fmod, Fcopy, Ftransp, Fnewfile;
   int      px, py;
   void     (*menufunc)(GtkWidget *, cchar *);
   STATB    statb;
   mode_t   mode;

   cchar    *warnalpha = E2X("Transparency map will be lost.\n"
                             "save to PNG file to retain.");
   
   if (! curr_file) return 1;
   
   err = stat(curr_file,&statb);                                                 //  get current file permissions       20.0
   if (err) return 1;                                                            //  (default for replace or new version)
   mode = statb.st_mode;

   if (strmatch(outype,"RAW")) {                                                 //  disallow saving as RAW type
      zmessageACK(Mwin,E2X("cannot save as RAW type"));
      return 1;
   }

   Fmod = 0;
   menufunc = 0;
   if (! qual) qual = jpeg_def_quality;                                          //  use default jpeg compression       19.0

   if (CEF) {                                                                    //  edit function active
      if (CEF->Fmods) Fmod = 1;                                                  //  active edits pending
      menufunc = CEF->menufunc;                                                  //  save menu function for restart
      if (CEF->zd) zdialog_send_event(CEF->zd,"done");                           //  tell it to finish
      if (CEF) return 1;                                                         //  failed (HDR etc.)
   }

   if (URS_pos > 0 && URS_saved[URS_pos] == 0) Fmod = 1;                         //  completed edits not saved
   
   if (strmatch(outfile,curr_file)) Fnewfile = 0;                                //  replace current file
   else Fnewfile = 1;                                                            //  new file (name/.ext/location)
   
   if (! Fnewfile) {
      err = access(outfile,W_OK);                                                //  test file can be written by me     19.1
      if (err) {
         zmessageACK(Mwin,"%s: %s",Bnowriteperm,outfile);
         return 1;
      }
   }

   outfile2 = f_realpath(outfile);                                               //  resolve symlinks in file path      20.0
   if (! outfile2) {
      zmessageACK(Mwin,Bfilenotfound2,outfile);
      return 2;
   }   
   
   if (Fnewfile) {                                                               //  if new file, force .ext
      pext = strrchr(outfile2,'/');                                              //    to one of: .jpg .png .tif
      if (pext) pext = strrchr(pext,'.');
      if (! pext) pext = outfile2 + strlen(outfile2);
      pext[0] = '.';
      strcpy(pext+1,outype);
   }

   if (! Fnewfile && ! Fmod) {                                                   //  no edit changes to file
      Fcopy = 1;                                                                 //  direct copy to output file?
      if (E0pxm) Fcopy = 0;                                                      //  no, non-edit change (upright)
      if (curr_file_bpc != outbpc) Fcopy = 0;                                    //  no, BPC change
      if (qual < jpeg_def_quality) Fcopy = 0;                                    //  no, higher compression wanted
      if (Fcopy) {
         err = copyFile(curr_file,outfile2);                                     //  copy unchanged file to output
         if (! err) goto updateindex;
         zmessageACK(Mwin,strerror(err));
         return 1;
      }
   }
   
   Ffuncbusy = 1;                                                                //  may be large file, slow CPU

   if (! E0pxm) {
      E0pxm = PXM_load(curr_file,1);                                             //  no prior edits, load image file
      if (! E0pxm) {
         zfree(outfile2);
         Ffuncbusy = 0;                                                          //  PXM_load() diagnoses error
         return 1;
      }
   }

   tempfile = zstrdup(outfile2,20);                                              //  temp file in same folder
   strcat(tempfile,"-temp.");
   strcat(tempfile,outype);
   
   if (E0pxm->nc == 4 && ! strstr("tif png",outype))                             //  alpha channel will be lost         19.0
   {
      Ftransp = 0;
      for (py = 2; py < E0pxm->hh-2; py += 2)                                    //  ignore extreme edges
      for (px = 2; px < E0pxm->ww-2; px += 2)
         if (PXMpix(E0pxm,px,py)[3] < 250) {                                     //  20.0
            Ftransp = 1;
            goto breakout;
         }
   breakout:                                                                     //  warn transparency lost
      if (Ftransp) {
         ii = zdialog_choose(Mwin,"mouse",warnalpha,E2X("save anyway"),Bcancel,null);
         if (ii != 1) {
            remove(tempfile);                                                    //  user canceled
            zfree(tempfile);
            zfree(outfile2);
            Ffuncbusy = 0;
            return 0;
         }
      }
   }

   if (! strstr("tif png",outype)) outbpc = 8;                                   //  force 8 if 16 not supported
   
   err = PXM_save(E0pxm,tempfile,outbpc,qual,Fack);                              //  19.0
   if (err) {
      remove(tempfile);                                                          //  failure, clean up
      zfree(tempfile);
      zfree(outfile2);
      Ffuncbusy = 0;
      return 1;
   }

   exifkey[0] = exif_editlog_key;
   exif_get(curr_file,&exifkey[0],ppv,1);                                        //  get prior edit history in EXIF
   if (ppv[0]) {
      strncpy0(edithist,ppv[0],exif_maxcc-2);
      zfree(ppv[0]);
      cc1 = strlen(edithist);                                                    //  edits made before this file was opened
   }
   else cc1 = 0;                                                                 //  none

   if ((CEF && CEF->Fmods) || URS_pos > 0)                                       //  active or completed unsaved edits
   {
      strcpy(edithist+cc1," ");                                                  //  update edit history
      strcpy(edithist+cc1+1,"Fotoxx:");                                          //  append " Fotoxx:"
      cc1 += 8;

      if (URS_reopen_pos > 0) ii0 = URS_reopen_pos + 1;                          //  if last file saved was kept open,
      else ii0 = 1;                                                              //    these edits are already in history

      for (ii = ii0; ii <= URS_pos; ii++)                                        //  append list of edits from undo/redo stack
      {                                                                          //  (omit index 0 = initial image)
         cc2 = strlen(URS_funcs[ii]);
         strcpy(edithist+cc1,URS_funcs[ii]);
         strcpy(edithist+cc1+cc2,"|");
         cc1 += cc2 + 1;
      }

      if (CEF && CEF->Fmods) {                                                   //  save during active edit function
         cc2 = strlen(CEF->funcname);                                            //  add curr. edit to history list
         strcpy(edithist+cc1,CEF->funcname);
         strcpy(edithist+cc1+cc2,"|");
         cc1 += cc2 + 1;
      }
   }

   exifkey[0] = exif_orientation_key;                                            //  remove EXIF orientation 
   exifdata[0] = "";                                                             //  (assume saved file is fixed)
   nkeys = 1;

   if (cc1) {                                                                    //  prior and/or curr. edit history
      exifkey[1] = exif_editlog_key;                                             //    will be added to EXIF
      exifdata[1] = edithist;
      nkeys = 2;
   }

   err = exif_copy(curr_file,tempfile,exifkey,exifdata,nkeys);                   //  copy all EXIF/IPTC data to
   if (err && Fack)                                                              //    temp file with above revisions
      zmessageACK(Mwin,E2X("Unable to copy EXIF/IPTC data"));
   if (err && ! Fack)
      printz("%s \n",E2X("Unable to copy EXIF/IPTC data"));                      //  20.0
   
   err = rename(tempfile,outfile2);                                              //  rename temp file to output file
   if (err) {
      if (Fack) zmessageACK(Mwin,strerror(err));
      remove(tempfile);                                                          //  delete temp file
      zfree(tempfile);
      zfree(outfile2);
      Ffuncbusy = 0;
      return 2;                                                                  //  could not save
   }
   
   zfree(tempfile);                                                              //  free memory

   for (ii = 0; ii <= URS_pos; ii++)                                             //  mark all prior edits as saved
      URS_saved[ii] = 1;

updateindex:

   update_thumbfile(outfile2);                                                   //  refresh thumbnail file

   load_filemeta(outfile2);                                                      //  load output file metadata

   if (Fmod) {
      set_meta_wwhh(E0pxm->ww,E0pxm->hh);                                        //  set w/h in metadata
      add_tag_fotoxx(outfile2);                                                  //  edited file has "fotoxx" tag
      save_filemeta(outfile2);                                                   //  includes update_image_index() 
   }
   else update_image_index(outfile2);                                            //  add to image index                 20.0

   if (samefolder(outfile2,navi::galleryname)) {                                 //  if saving into current gallery
      gallery(curr_file,"init",0);                                               //  update curr. gallery list
      gallery(0,"sort",-2);                                                      //  recall sort and position
      set_mwin_title();                                                          //  update posn, count in title
   }

   add_recent_file(outfile2);                                                    //  first in recent files list

   if (f_save_file) zfree(f_save_file);
   f_save_file = outfile2;

   chmod(outfile2,mode);                                                         //  set permissions from input file    20.0

   stat(outfile2,&statb);                                                        //  set output file size, type, bit depth
   f_save_size = statb.st_size;
   strcpy(f_save_type,outype);
   f_save_bpc = outbpc;

   Ffuncbusy = 0;
   if (menufunc) menufunc(0,0);                                                  //  restart edit function
   return 0;
}


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

//  save (modified) image to new file name or type
//  confirm if overwrite of existing file
//  returns 0 if OK, 1 if cancel or error

GtkWidget      *saveas_fchooser;

int f_save_as()
{
   int  f_save_as_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;
   char           *save_folder = 0;
   cchar          *type;
   char           *newfile, *fname;
   char           *outfile = 0, *outfile2 = 0, *pp, *pext;
   char           permissions[100];
   int            ii, zstat, err;
   int            bpc, mkcurr = 0;
   int            jpgqual = jpeg_def_quality;
   STATB          statbuf;
   mode_t         mode;

/***
       _____________________________________________________
      |   Save as New File Name or Type                     |
      |   ________________________________________________  |
      |  |                                                | |
      |  |       file chooser dialog                      | |
      |  |                                                | |
      |  |                                                | |
      |  |                                                | |
      |  |                                                | |
      |  |                                                | |
      |  |________________________________________________| |
      |                                                     |
      |  (o) tif  (o) png  (o) jpg  [90] jpg quality        |                    //  sep. file type and color depth
      |  color depth: (o) 8-bit  (o) 16-bit                 |
      |  [x] make current (new file becomes current file)   |
      |  permissions: [__________________________] [change] |                    //  20.0
      |                                                     |
      |                                    [save] [cancel]  |
      |_____________________________________________________|

***/

   zd = zdialog_new(E2X("save as new file name or type"),Mwin,Bsave,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbfc","dialog",0,"expand");
   saveas_fchooser = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_SAVE);
   gtk_container_add(GTK_CONTAINER(zdialog_widget(zd,"hbfc")),saveas_fchooser);

   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbft","dialog");
   zdialog_add_widget(zd,"radio","tif","hbft","tif","space=4");
   zdialog_add_widget(zd,"radio","png","hbft","png","space=4");
   zdialog_add_widget(zd,"radio","jpg","hbft","jpg","space=2");
   zdialog_add_widget(zd,"zspin","jpgqual","hbft","10|100|1|90","size=3");
   zdialog_add_widget(zd,"label","labqual","hbft",E2X("jpg quality"),"space=6");

   zdialog_add_widget(zd,"hbox","hbcd","hbft");
   zdialog_add_widget(zd,"label","space","hbcd","","space=8");
   zdialog_add_widget(zd,"label","labdepth","hbcd",E2X("color depth:"),"space=3");
   zdialog_add_widget(zd,"radio","8-bit","hbcd","8-bit","space=4");
   zdialog_add_widget(zd,"radio","16-bit","hbcd","16-bit","space=4");

   zdialog_add_widget(zd,"hbox","hbmc","dialog");
   zdialog_add_widget(zd,"check","mkcurr","hbmc",0,"space=8");
   zdialog_add_widget(zd,"label","labmc","hbmc",E2X("make current"));
   zdialog_add_widget(zd,"label","labmc2","hbmc",E2X("(new file becomes current file)"),"space=5");

   zdialog_add_widget(zd,"hbox","hbperm","dialog");                              //  20.0
   zdialog_add_widget(zd,"label","labperm","hbperm",E2X("permissions:"),"space=8");
   zdialog_add_widget(zd,"label","permissions","hbperm");
   zdialog_add_widget(zd,"button","change","hbperm",Bchange,"space=8");

   zdialog_labelfont(zd,"labmc","sans bold",E2X("make current"));

   save_folder = f_realpath(curr_file);                                          //  use real path                      20.0
   if (! save_folder) save_folder = zstrdup(curr_file);
   pp = strrchr(save_folder,'/');
   if (pp) *pp = 0;

   gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(saveas_fchooser),save_folder);
   zfree(save_folder);
   newfile = file_new_version(curr_file);                                        //  suggest next version
   if (! newfile) newfile = zstrdup(curr_file);
   fname = strrchr(newfile,'/') + 1;
   gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(saveas_fchooser),fname);
   zfree(newfile);

   zdialog_stuff(zd,"tif",0);                                                    //  no file type selected
   zdialog_stuff(zd,"png",0);
   zdialog_stuff(zd,"jpg",0);
   zdialog_stuff(zd,"8-bit",0);
   zdialog_stuff(zd,"16-bit",0);

   zdialog_stuff(zd,"jpgqual",jpeg_def_quality);                                 //  default jpeg quality, user setting

   if (strmatch(curr_file_type,"tif")) {                                         //  if curr. file type is tif,
      zdialog_stuff(zd,"tif",1);                                                 //    set corresp. type and bit depth
      if (curr_file_bpc == 16)
         zdialog_stuff(zd,"16-bit",1);
      else zdialog_stuff(zd,"8-bit",1);
   }

   else if (strmatch(curr_file_type,"png")) {                                    //  same for png
      zdialog_stuff(zd,"png",1);
      if (curr_file_bpc == 16)
         zdialog_stuff(zd,"16-bit",1);
      else zdialog_stuff(zd,"8-bit",1);
   }

   else {                                                                        //  same for jpg
      zdialog_stuff(zd,"jpg",1);
      zdialog_stuff(zd,"8-bit",1);
   }

   zdialog_stuff(zd,"mkcurr",0);                                                 //  deselect "make current"
   
   err = stat(curr_file,&statbuf);                                               //  current file permissions           20.0
   conv_permissions(statbuf.st_mode,permissions);                                //   >> default new file permissions
   zdialog_stuff(zd,"permissions",permissions);

   zdialog_resize(zd,700,500);
   zdialog_run(zd,f_save_as_dialog_event,"parent");

zdialog_wait:

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

   outfile2 = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(saveas_fchooser));
   if (! outfile2) {
      zd->zstat = 0;
      goto zdialog_wait;
   }
   
   if (outfile) zfree(outfile);
   outfile = zstrdup(outfile2,12);                                               //  add space for possible .vNN and .ext
   g_free(outfile2);

   type = "";
   bpc = -1;
   
   zdialog_fetch(zd,"tif",ii);                                                   //  get selected file type
   if (ii) type = "tif";

   zdialog_fetch(zd,"png",ii);
   if (ii) type = "png";

   zdialog_fetch(zd,"jpg",ii);
   if (ii) type = "jpg";
   
   zdialog_fetch(zd,"8-bit",ii);                                                 //  get selected color depth
   if (ii) bpc = 8;
   
   zdialog_fetch(zd,"16-bit",ii);
   if (ii) bpc = 16;
   
   if (! strstr("tif png jpg",type)) goto zdialog_wait;                          //  should not happen
   if (bpc != 8 && bpc != 16) goto zdialog_wait;
   if (strmatch(type,"jpg") && bpc != 8) goto zdialog_wait;

   zdialog_fetch(zd,"jpgqual",jpgqual);                                          //  jpeg compression level

   pext = strrchr(outfile,'/');                                                  //  locate file .ext
   if (pext) pext = strrchr(pext,'.');

   if (pext) {                                                                   //  validate .ext OK for type
      if (strmatch(type,"jpg") && ! strcasestr(".jpg .jpeg",pext)) *pext = 0;
      if (strmatch(type,"tif") && ! strcasestr(".tif .tiff",pext)) *pext = 0;
      if (strmatch(type,"png") && ! strcasestr(".png",pext)) *pext = 0;
   }

   if (! pext || ! *pext) {
      pext = outfile + strlen(outfile);                                          //  wrong or missing, add new .ext
      *pext = '.';                                                               //  NO replace .JPG with .jpg etc.
      strcpy(pext+1,type);
   }

   zdialog_fetch(zd,"mkcurr",mkcurr);                                            //  get make current option

   err = stat(outfile,&statbuf);                                                 //  check if file exists
   if (! err) {
      int yn = zmessageYN(Mwin,E2X("Overwrite file? \n %s"),outfile);            //  confirm overwrite
      if (! yn) {
         zd->zstat = 0;
         goto zdialog_wait;
      }
   }
   
   zdialog_fetch(zd,"permissions",permissions,100);                              //  get permissions from dialog        20.0
   conv_permissions(permissions,mode);

   zdialog_free(zd);                                                             //  zdialog_free(zd);

   err = f_save(outfile,type,bpc,jpgqual,1);                                     //  save the file
   if (err) {
      zfree(outfile);
      return 1;
   }
   
   chmod(outfile,mode);                                                          //  set permissions                    20.0
   
   if (samefolder(outfile,navi::galleryname)) {                                  //  if saving into current gallery
      gallery(outfile,"init",0);                                                 //    refresh gallery list
      gallery(0,"sort",-2);                                                      //    recall sort and position 
      curr_file_posn = file_position(curr_file,curr_file_posn);                  //    update curr. file position
      set_mwin_title();                                                          //    update window title (file count)
   }

   if (mkcurr) f_open_saved();                                                   //  open saved file with edit hist

   zfree(outfile);
   return 0;
}


//  dialog event and completion function

int f_save_as_dialog_event(zdialog *zd, cchar *event)
{
   int      ii, err;
   char     *filespec, *filename, *pp;
   char     oldperms[100], newperms[100];
   char     ext[4];
   
   if (zd->zstat) return 1;                                                      //  [done] or [cancel]

   if (strmatch(event,"jpgqual")) {                                              //  if jpg quality edited, set jpg .ext
      zdialog_stuff(zd,"jpg",1);
      event = "jpg";
   }

   if (zstrstr("tif png jpg",event)) {                                           //  file type selection
      zdialog_stuff(zd,"tif",0);                                                 //  turn off all types
      zdialog_stuff(zd,"png",0);
      zdialog_stuff(zd,"jpg",0);
      zdialog_stuff(zd,event,1);                                                 //  turn on selected type
   }
   
   if (zstrstr("8-bit 16-bit",event)) {                                          //  color depth selection
      zdialog_stuff(zd,"8-bit",0);                                               //  turn off all depths
      zdialog_stuff(zd,"16-bit",0);
      zdialog_stuff(zd,event,1);                                                 //  turn on selected depth
   }
   
   zdialog_fetch(zd,"jpg",ii);                                                   //  if jpg, force 8-bit
   if (ii) {
      zdialog_stuff(zd,"16-bit",0);
      zdialog_stuff(zd,"8-bit",1);
   }
   
   zdialog_fetch(zd,"tif",ii);                                                   //  get chosen file type "tif" ...
   if (ii) strcpy(ext,"tif");                                                    //  set corresp. extension ".tif" ...
   zdialog_fetch(zd,"png",ii);
   if (ii) strcpy(ext,"png");
   zdialog_fetch(zd,"jpg",ii);
   if (ii) strcpy(ext,"jpg");

   filespec = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(saveas_fchooser));
   if (! filespec) return 1;
   filename = strrchr(filespec,'/');                                             //  revise file .ext in chooser dialog
   if (! filename) return 1;
   filename = zstrdup(filename+1,6);
   pp = strrchr(filename,'.');
   if (! pp || strlen(pp) > 5) pp = filename + strlen(filename);
   *pp = '.';
   strcpy(pp+1,ext);
   gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(saveas_fchooser),filename);
   zfree(filename);
   g_free(filespec);
   
   if (strmatch(event,"change")) {                                               //  change saved file permissions      20.0
      zdialog_fetch(zd,"permissions",oldperms,100);
      err = set_permissions(zd->dialog,"",oldperms,newperms);
      if (! err) zdialog_stuff(zd,"permissions",newperms);
   }

   return 1;
}


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

   text file line edit function

   open a text file:   err = linedit_open(filespec)
   get next record:    record = linedit_get()         null = EOF
   put next record:    err = linedit_put(record)
   close file:         err = linedit_close()

   resulting file contains only the 'put' records and replaces input file
   trailing '\n' is removed from records read and added to records written
   linedit+get() returns pointer to internal buffer - DO NOT zfree()
   
   returns: 0 = OK, +N = error

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

namespace linedit
{
   FILE     *fidr = 0, *fidw = 0;
   char     filespec[202], copyfile[208];
}


int linedit_open(cchar *file)
{
   using namespace linedit;
   
   if (strlen(file) > 200) {
      printz("linedit() filespec > 200: %s \n",file);
      return 1;
   }

   strcpy(filespec,file);
   snprintf(copyfile,206,"%s-copy",filespec);
   
   fidr = fopen(filespec,"r");                                                   //  open/read input file (may be null)
   
   fidw = fopen(copyfile,"w");                                                   //  open/write copy file
   if (! fidw) {
      printz("linedit(): %s \n %s \n",file,strerror(errno));
      if (fidr) fclose(fidr);
      fidr = 0;
      return 1;
   }
   
   return 0;
}


char * linedit_get()
{
   using namespace linedit;

   static char    buff[1000], *prec;

   if (! fidr) return 0;                                                         //  new file
   prec = fgets_trim(buff,1000,fidr);                                            //  read next record
   if (! prec) return 0;

   if (strlen(prec) > 998) {
      printz("linedit() record > 998: %s \n",filespec);
      return 0;
   }

   return prec;
}


int linedit_put(cchar *record)
{
   using namespace linedit;

   int      nn;

   if (! fidw) return 1;
   nn = fprintf(fidw,"%s\n",record);                                             //  write next record
   if (nn < 0) {
      printz("linedit(): %s \n %s \n",filespec,strerror(errno));
      return 1;
   }

   return 0;
}


int linedit_close()
{
   using namespace linedit;

   int      err;

   if (fidr) fclose(fidr);                                                       //  close input and copy files
   fidr = 0;
   if (! fidw) return 1;
   err = fclose(fidw);
   fidw = 0;

   if (err) {
      printz("linedit(): %s \n %s \n",filespec,strerror(errno));
      return 1;
   }

   err = rename(copyfile,filespec);                                              //  replace input file with copy
   if (err) {
      printz("linedit(): %s \n %s \n",filespec,strerror(errno));
      return 1;
   }

   return 0;
}


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

   Find all image files within given path.

   int find_imagefiles(cchar *folder, int flags, char **&flist, int &NF)

      folder      folder path to search
      flags       sum of the following:
                     1  include image files (+RAW +video)
                     2  include thumbnails
                     4  include hidden files 
                     8  include folders
                    16  recurse folders
                    32  omit symlinks                                            //  20.0
      NF          count of files returned
      flist       list of files returned

   Returns 0 if OK, +N if error (errno is set).
   flist and flist[*] are subjects for zfree().

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

namespace find_imagefiles_names
{
   char  **fif_filelist;                  //  list of filespecs returned
   char  *lockfile = 0;                   //  global lock file
   char  lockname[200];                   //  lock file name
   int   fif_max;                         //  filelist slots allocated
   int   fif_count;                       //  filelist slots filled
   int   fd = -1;                         //  global lock file descriptor
}


int find_imagefiles(cchar *folder, int flags, char **&flist, int &NF, int Finit) 
{
   using namespace find_imagefiles_names;

   static int  ftf = 1;
   int         globflags, Fimages, Fthumbs, Fdirs, Frecurse, Fnolinks;
   int         err1, err2, cc;
   FTYPE       ftype;
   char        *file, *mfolder, **templist;
   glob_t      globdata;
   STATB       statB;
   
   if (ftf) {
      ftf = 0;
      snprintf(lockname,200,"%s/find_imagefiles_lock",temp_folder);
      make_global_lockfile(lockname,&lockfile);
   }

   if (Finit) {                                                                  //  initial call (OMIT, default 1)
      while ((fd = global_lock(lockfile)) < 0) zsleep(0.001);                    //  19.0
      fif_max = fif_count = 0;
   }

   globflags = GLOB_NOSORT;
   Fimages = Fthumbs = Fdirs = Frecurse = Fnolinks = 0;
   
   if (flags & 1) Fimages = 1;
   if (flags & 2) Fthumbs = 1;
   if (flags & 4) globflags += GLOB_PERIOD;
   if (flags & 8) Fdirs = 1;
   if (flags & 16) Frecurse = 1;
   if (flags & 32) Fnolinks = 1;
   
   if (Fdirs && ! Fimages && ! Fthumbs)
      globflags += GLOB_ONLYDIR;

   globdata.gl_pathc = 0;                                                        //  glob() setup
   globdata.gl_offs = 0;
   globdata.gl_pathc = 0;

   NF = 0;                                                                       //  empty output
   flist = 0;

   mfolder = zstrdup(folder,4);                                                  //  append /* to input folder
   strcat(mfolder,"/*");

   err1 = glob(mfolder,globflags,null,&globdata);                                //  find all files in folder
   if (err1) {
      if (err1 == GLOB_NOMATCH) err1 = 0;
      else if (err1 == GLOB_ABORTED) err1 = 1;
      else if (err1 == GLOB_NOSPACE) err1 = 2;
      else err1 = 3;
      goto fif_return;
   }

   for (uint ii = 0; ii < globdata.gl_pathc; ii++)                               //  loop found files
   {
      file = globdata.gl_pathv[ii];

      if (Fnolinks) {                                                            //  detect and omit symlinks           20.0
         err1 = lstat(file,&statB);
         if (err1) continue;
         if (S_ISLNK(statB.st_mode)) continue;
      }
      else err1 = stat(file,&statB);
      if (err1) continue;                                                        //  should not happen

      if (Frecurse && S_ISDIR(statB.st_mode)) {                                  //  folder
         err1 = find_imagefiles(file,flags,flist,NF,0);                          //  process member files
         if (err1) goto fif_return;
      }
      
      ftype = image_file_type(file);
      if (ftype == OTHER) continue;                                              //  unknown file type
      
      if (ftype == FDIR && ! Fdirs) continue;
      if (ftype == THUMB && ! Fthumbs) continue;
      if (ftype == IMAGE || ftype == RAW || ftype == VIDEO)
         if (! Fimages) continue;
      
      if (fif_count == fif_max) {                                                //  output list is full
         if (fif_max == 0) {
            fif_max = 1000;                                                      //  initial space, 1000 files
            cc = fif_max * sizeof(char *);
            fif_filelist = (char **) zmalloc(cc);
         }
         else {
            templist = fif_filelist;                                             //  expand by 2x each time needed
            cc = fif_max * sizeof(char *);
            fif_filelist = (char **) zmalloc(cc+cc);
            memcpy(fif_filelist,templist,cc);
            memset(fif_filelist+fif_max,0,cc);
            zfree(templist);
            fif_max *= 2;
         }
      }

      fif_filelist[fif_count] = zstrdup(file);                                   //  add file to output list
      fif_count += 1;
   }

   err1 = 0;

fif_return:

   err2 = errno;                                                                 //  preserve Linux errno

   globfree(&globdata);                                                          //  free memory
   zfree(mfolder);

   if (Finit) {                                                                  //  user call
      NF = fif_count;
      if (NF) flist = fif_filelist;
      global_unlock(fd,lockfile);                                                //  19.0
   }

   errno = err2;                                                                 //  return err1 and preserve errno
// zmainloop();                                                                  //  NO - causes re-entrance bugs
   return err1;
}


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

//  get the equivalent .tif file name for a given RAW file
//  returned file name is subject to zfree()

char * raw_to_tiff(cchar *rawfile)
{
   char     *ptiff, *pext;

   ptiff = zstrdup(rawfile,8);
   pext = strrchr(ptiff,'.');
   if (! pext) pext = ptiff + strlen(ptiff);
   strcpy(pext,".tif");
   return ptiff;
}


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

//  Show a permissions setting, edit and return changed permissions.
//  'fname' is a file name or permission category, e.g. 'default'
//  Permissions are strings: "read+write", "read only", "no access".
//  'p1' and 'p2' are input and output permissions strings, which are
//    any combination of three of the above strings, comma separated,
//     which define the access permissions for user, group, other.
//  p1 and p2 are char[100] strings to allow for fat translations.
//  Returns 1 if user cancel or error, 0 if changes were made OK.

int set_permissions(GtkWidget *parent, cchar *fname, cchar *p1, char *p2)        //  20.0
{
   char           operm[50], gperm[50], wperm[50];
   const char     *pp;
   zdialog        *zd;
   int            zstat;

/***
          ________________________________
         |           permissions          |
         |                                |
         |  owner    [read+write |v]      |
         |  group    [read only  |v]      |
         |  other    [no access  |v]      |
         |                                |
         |               [apply] [cancel] |
         |________________________________|

***/

   zd = zdialog_new(E2X("Permissions"),parent,Bapply,Bcancel,null);
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labowner","vb1",E2X("owner"));
   zdialog_add_widget(zd,"label","labgroup","vb1",E2X("group"));
   zdialog_add_widget(zd,"label","labother","vb1",E2X("other"));
   zdialog_add_widget(zd,"combo","ownerperm","vb2");
   zdialog_add_widget(zd,"combo","groupperm","vb2");
   zdialog_add_widget(zd,"combo","otherperm","vb2");
   
   zdialog_stuff(zd,"ownerperm",RWtran);                                         //  initz. combo boxes
   zdialog_stuff(zd,"ownerperm",ROtran);
   zdialog_stuff(zd,"ownerperm",NOtran);
   
   zdialog_stuff(zd,"groupperm",RWtran);
   zdialog_stuff(zd,"groupperm",ROtran);
   zdialog_stuff(zd,"groupperm",NOtran);
   
   zdialog_stuff(zd,"otherperm",RWtran);
   zdialog_stuff(zd,"otherperm",ROtran);
   zdialog_stuff(zd,"otherperm",NOtran);
   
   pp = strField(p1,',',1);                                                      //  initz. dialog from input permissions
   if (pp) zdialog_stuff(zd,"ownerperm",pp);

   pp = strField(p1,',',2);
   if (pp) zdialog_stuff(zd,"groupperm",pp);

   pp = strField(p1,',',3);
   if (pp) zdialog_stuff(zd,"otherperm",pp);
   
   zdialog_run(zd,0,0);                                                          //  run dialog

   zstat = zdialog_wait(zd);                                                     //  wait for completion
   if (zstat != 1) {
      zdialog_free(zd);                                                          //  [cancel]
      return 1;                                                                  //  error return
   }
   
   *p2 = 0;

   zdialog_fetch(zd,"ownerperm",operm,50);                                       //  get permissions from dialog
   zdialog_fetch(zd,"groupperm",gperm,50);
   zdialog_fetch(zd,"otherperm",wperm,50);
   
   snprintf(p2,100,"%s, %s, %s",operm,gperm,wperm);                              //  output permissions
   
   zdialog_free(zd);
   return 0;                                                                     //  normal return
}


//  convert between permission formats
//  user format: "read+write, read only, no access"  (also translatable)
//  internal format: mode_t   bitmap of permissions, e.g. 0640

int conv_permissions(cchar *perms, mode_t &mode)                                 //  20.0
{
   int      err = 0;
   cchar    *pp;

   mode = 0;

   pp = strField(perms,',',1);
   if (pp) {
      if (strmatch(pp,RWtran)) mode += S_IRUSR + S_IWUSR;
      else if (strmatch(pp,ROtran)) mode += S_IRUSR;
      else if (strmatch(pp,NOtran)); 
      else err = 1;
   }
   else err = 1;
   
   pp = strField(perms,',',2);
   if (pp) {
      if (strmatch(pp,RWtran)) mode += S_IRGRP + S_IWGRP;
      else if (strmatch(pp,ROtran)) mode += S_IRGRP;
      else if (strmatch(pp,NOtran)); 
      else err = 1;
   }
   else err = 1;
   
   pp = strField(perms,',',3);
   if (pp) {
      if (strmatch(pp,RWtran)) mode += S_IROTH + S_IWOTH;
      else if (strmatch(pp,ROtran)) mode += S_IROTH;
      else if (strmatch(pp,NOtran)); 
      else err = 1;
   }
   else err = 1;
   
   return err;
}


//  output perms is char[100] to allow for fat translations

int conv_permissions(mode_t mode, char *perms)                                   //  20.0
{
   int      err = 0;

   *perms = 0;

   if (mode & S_IRUSR && mode & S_IWUSR) strcat(perms,RWtran);
   else if (mode & S_IRUSR) strcat(perms,ROtran);
   else strcat(perms,NOtran);
   strcat(perms,", ");

   if (mode & S_IRGRP && mode & S_IWGRP) strcat(perms,RWtran);
   else if (mode & S_IRGRP) strcat(perms,ROtran);
   else strcat(perms,NOtran);
   strcat(perms,", ");

   if (mode & S_IROTH && mode & S_IWOTH) strcat(perms,RWtran);
   else if (mode & S_IROTH) strcat(perms,ROtran);
   else strcat(perms,NOtran);
   
   return err;
}


//  get real path for filename possibly having symlinks
//  all folders in input filename path must exist
//  final file name need not exist (f_save() caller)
//  returns real path or null if error
//  returned path is subject for zfree()

char * f_realpath(cchar * infile)                                                //  20.0
{
   int      cc;
   char     *pp, *RP, *outfile;
   char     tempfile[XFCC];

   RP = realpath(infile,null);                                                   //  try full file path
   if (RP) {
      outfile = zstrdup(RP);                                                     //  OK
      free(RP);
      return outfile;
   }

   strcpy(tempfile,infile);                                                      //  fail, try folders only 
   pp = strrchr(tempfile + 2,'/');
   if (! pp) return 0;
   *pp = 0;
   RP = realpath(tempfile,null);
   *pp = '/';
   if (! RP) return 0;                                                           //  fail

   cc = strlen(pp);                                                              //  return folders/file
   outfile = zstrdup(RP,cc+2);
   strcat(outfile,pp);
   free(RP);
   return outfile;
}


