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

   Fotoxx      edit photos and manage collections  

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

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

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

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

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

   main                    main program, set up defaults
   initzfunc               initializations, carry out command line options

   delete_event            response function for main window delete event
   destroy_event           response function for main window destroy event
   state_event             response function for main window fullscreen state change

   gtimefunc               periodic function
   update_Fpanel           update status parameters on F window top panel

   paintlock               block window updates from main thread or created thread
   Fpaint                  main / drawing window refresh (draw signal response function)
   Fpaintnow               immediate Fpaint, not callable from threads
   Fpaint2                 queue Fpaint, callable from threads
   Fpaint3                 update drawing window section from updated E3 section
   Fpaint4                 update drawing window section (direct write)

   mouse_event             mouse event response function
   mouse_convert           convert mouse/window space to image space
   m_zoom                  main window zoom in/out function
   KBstate                 send KB key from dialog to main window
   KBpress                 KB key press event function
   KBrelease               KB key release event function
   win_fullscreen          set main window full screen status
   win_unfullscreen        restore main window to former size
   set_mwin_title          update the main window title bar

   draw_pixel              draw one overlay pixel using image space
   erase_pixel             erase one pixel
   draw_line               draw overlay line in image space
   erase_line              erase line
   draw_toplines           draw or redraw a set of overlay lines
   draw_gridlines          draw grid lines over image
   add_toptext             add to set of overlay text strings
   draw_toptext            draw set of overlay text strings
   erase_toptext           remove text from set of overlay text strings
   draw_text               draw text on window in image space
   add_topcircle           add a circle to set of overlay circles
   draw_topcircles         draw the set of overlay circles in window space
   erase_topcircles        erase the set of overlay circles
   draw_mousecircle        draw a circle around pointer in image space
   draw_mousecircle2       2nd instance for paint/clone tracking circle
   draw_mousearc           draw an ellipse around pointer in image space

   splcurve_init           set up a spline curve drawing area
   splcurve_adjust         mouse event function to manipulate curve nodes
   splcurve_addnode        add an anchor point to a curve
   splcurve_draw           draw curve through nodes
   splcurve_generate       generate x/y table of values from curve
   splcurve_yval           get curve y-value for given x-value
   splcurve_load           load curve data from a saved file
   splcurve_save           save curve data to a file

   edit_setup              start an image edit function
   CEF_invalid             diagnostic
   edit_cancel             cancle image edit
   edit_done               finish image edit
   edit_fullsize           convert preview to full size edit
   edit_undo               undo current edit (reset)
   edit_redo               redo current edit
   edit_reset              reset all edit changes
   edit_load_widgets       load zdialog widgets and curves from a file
   edit_save_widgets       save last-used widgets (for [done] buttons)
   edit_load_prev_widgets  load last-used widgets (for [prev] buttons)
   edit_save_last_widgets  save last-used widgets (for [prev] buttons)

   m_undo_redo             undo/redo depending on mouse button
   undo_redo_choice        popup menu response function
   m_undo                  restore previous edit in undo/redo stack
   m_redo                  restore next edit in undo/redo stack
   undo_all                undo all edits for image
   redo_all                redo all edits for image
   save_undo               save image in the undo stack
   load_undo               load image from the undo stack
   checkpend               check status: edit mods busy block any quiet
   takeMouse               set mouse event function and special cursor
   freeMouse               remove mouse event function, set normal cursor

   start_thread            start thread running
   signal_thread           signal thread that work is pending
   wait_thread_idle        wait for pending work complete
   wrapup_thread           wait for thread exit or command thread exit
   thread_idle_loop        wait for pending work, exit if commanded
   thread_exit             exit thread unconditionally
   start_wthread           start a working thread per SMP processor
   exit_wthread            exit a working thread
   wait_wthreads           wait for all working threads to exit

   save_params             save parameters when fotoxx exits
   load_params             load parameters at fotoxx startup
   free_resources          free resources for the current image file
   sigdiff                 compare 2 floats with margin of significance

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

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

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


//  fotoxx main program

using namespace zfuncs;

int main(int argc, char *argv[])
{
   char     lang[8] = "";
   int      Fclone=0, cloxx=0, cloyy=0, cloww=0, clohh=0;
   int      err;
   STATB    statb;

   if (argc > 1 && strmatchV(argv[1],"-ver","-v",0)) {
      printz(Frelease "\n");                                                     //  print version and exit
      return 0;
   }

   if (gtk_clutter_init(&argc,&argv) != CLUTTER_INIT_SUCCESS) return 1;          //  init. gtk and clutter (OSM)        16.05

   zinitapp("fotoxx");                                                           //  initz. app directories
   err = chdir(get_zuserdir());                                                  //  set to /home/<user>/.fotoxx

   //  initialize externals to default values (saved parameters will override)

   strcpy(zfuncs::zappname,Frelease);                                            //  app name and version
   Ffirsttime = 1;                                                               //  first time startup (params override)
   Prelease = zstrdup("unknown");                                                //  prev. fotoxx release (params override)
   mwgeom[0] = mwgeom[1] = 100;                                                  //  default main window geometry
   mwgeom[2] = 1200; mwgeom[3] = 800;
   *paneltext = 0;                                                               //  no status bar text
   trimsize[0] = 1600;                                                           //  default initial image trim size
   trimsize[1] = 1000;
   trimbuttons[0] = zstrdup("5:4");  trimratios[0] = zstrdup("5:4");             //  default trim ratio buttons
   trimbuttons[1] = zstrdup("4:3");  trimratios[1] = zstrdup("4:3");
   trimbuttons[2] = zstrdup("8:5");  trimratios[2] = zstrdup("8:5");
   trimbuttons[3] = zstrdup("16:9");  trimratios[3] = zstrdup("16:9");
   trimbuttons[4] = zstrdup("2:1");  trimratios[4] = zstrdup("2:1");
   trimbuttons[5] = zstrdup("gold");  trimratios[5] = zstrdup("1.62:1");
   editresize[0] = 1600;                                                         //  default initial resize size
   editresize[1] = 1200;
   currgrid = 0;                                                                 //  default initial grid settings
   gridsettings[0][GON] = 0;                                                     //  grid off
   gridsettings[0][GX] = gridsettings[0][GY] = 1;                                //  x/y grid lines enabled
   gridsettings[0][GXS] = gridsettings[0][GYS] = 200;                            //  x/y spacing
   gridsettings[0][GXC] = gridsettings[0][GYC] = 5;                              //  x/y count
   menu_style = zstrdup("icons");                                                //  default menu style (icons)
   Fbgcolor[0] = Fbgcolor[1] = Fbgcolor[2] = 50;                                 //  F view background color            16.07
   Gbgcolor[0] = Gbgcolor[1] = Gbgcolor[2] = 200;                                //  G view background color            16.07
   gallerysort = 1;                                                              //  default gallery sort (alphabetic)  16.01
   dialog_font = zstrdup("Sans 11");                                             //  default dialog font                16.05
   iconsize = 32;                                                                //  default icon size                  16.07
   splcurve_minx = 5;                                                            //  default curve node separation %    15.10
   startdisplay = zstrdup("prevI");                                              //  start with previous image
   Fdragopt = 1;                                                                 //  image drag with mouse              15.02
   zoomcount = 2;                                                                //  zooms to reach 2x image size
   zoomratio = sqrtf(2);                                                         //  corresp. zoom ratio
   map_dotsize = 8;                                                              //  map dot size, mouse capture dist   16.05
   curr_file = curr_dirk = 0;                                                    //  no curr. file or directory         15.11
   copymove_loc = 0;                                                             //  copy/move target directory         16.07
   thumbdirk = 0;                                                                //  no thumbnail directory
   thumbfilesize = 256;                                                          //  thumbnail files, default pixels    15.02
   navi::thumbsize = 128;                                                        //  gallery window thumbnail size      16.07
   startmenu = 0;                                                                //  start with menu function
   startalbum = 0;                                                               //  start with album gallery 
   initial_file = 0;                                                             //  start with image file or directory
   jpeg_def_quality = 90;                                                        //  default .jpeg save quality
   jpeg_1x_quality = 90;                                                         //  default for 1-time jpeg quality
   lens_mm = 35;                                                                 //  pano lens parameter
   colormapfile = zstrdup("undefined");                                          //  printer calibration color map      15.10
   RAWfiletypes = zstrdup(".arw .crw .cr2 .dng .erf .iiq .mef .mos "             //  some of the known RAW file types
                          ".mpo .nef .nrw .orf .pef .ptx .raw "                  //  (case is not significant here)
                          ".rw2 .rwl .srf .srw .sr2 ");
   imagefiletypes = zstrdup(".jpg .jpeg .png .tif .tiff .bmp .ico "              //  supported image file types
                            ".gif .svg .xpm .tga ");

   BLACK[0] = BLACK[1] = BLACK[2] = 0;                                           //  define RGB colors
   WHITE[0] = WHITE[1] = WHITE[2] = 255;
   RED[0] = 255; RED[1] = RED[2] = 0;
   GREEN[1] = 255; GREEN[0] = GREEN[2] = 0;
   BLUE[2] = 255; BLUE[0] = BLUE[1] = 0;
   LINE_COLOR = RED;                                                             //  initial foreground drawing color

   for (int ii = 0; ii < 100; ii++) Nval[ii] = ii;                               //  static integer values 0-99

   //  file and directory names in user directory /home/<user>/.fotoxx/*

   snprintf(index_dirk,199,"%s/image_index",get_zuserdir());                     //  image index directory
   snprintf(tags_defined_file,199,"%s/tags_defined",get_zuserdir());             //  defined tags file
   snprintf(recentfiles_file,199,"%s/recent_files",get_zuserdir());              //  recent files file (index func)
   snprintf(saved_areas_dirk,199,"%s/saved_areas",get_zuserdir());               //  saved areas directory
   snprintf(albums_dirk,199,"%s/albums",get_zuserdir());                         //  albums directory
   snprintf(filecachefile,199,"%s/albums/file_cache",get_zuserdir());            //  albums, file cache file            16.07
   snprintf(saved_curves_dirk,199,"%s/saved_curves",get_zuserdir());             //  saved curves directory
   snprintf(writetext_dirk,199,"%s/write_text",get_zuserdir());                  //  write_text directory
   snprintf(favorites_dirk,199,"%s/favorites",get_zuserdir());                   //  favorites directory
   snprintf(mashup_dirk,199,"%s/mashup",get_zuserdir());                         //  mashup projects directory
   locale_filespec("data","quickstart.html",quickstart_file);                    //  quickstart html file
   snprintf(slideshow_dirk,199,"%s/slideshows",get_zuserdir());                  //  slide show directory
   snprintf(slideshow_trans_dirk,199,"%s/slideshow_trans",get_zuserdir());       //  slide show transitions
   snprintf(pattern_dirk,199,"%s/patterns",get_zuserdir());                      //  pattern files directory 
   snprintf(retouch_combo_dirk,199,"%s/retouch_combo",get_zuserdir());           //  retouch combo settings directory
   snprintf(custom_kernel_dirk,199,"%s/custom_kernel",get_zuserdir());           //  custom kernel files directory
   snprintf(printer_color_dirk,199,"%s/printer_color",get_zuserdir());           //  printer calibration directory      15.10
   snprintf(edit_scripts_dirk,199,"%s/edit_scripts",get_zuserdir());             //  edit script files directory        15.10
   snprintf(searchresults_file,199,"%s/search_results",get_zuserdir());          //  output of image search function
   snprintf(maps_dirk,199,"/usr/share/fotoxx-maps/data");                        //  map files in fotoxx-maps package
   snprintf(user_maps_dirk,199,"%s/user_maps",get_zuserdir());                   //  map files made by user             15.12
   
   err = stat(index_dirk,&statb);                                                //  create missing directories
   if (err) mkdir(index_dirk,0750);
   err = stat(saved_areas_dirk,&statb);
   if (err) mkdir(saved_areas_dirk,0750);
   err = stat(albums_dirk,&statb);
   if (err) mkdir(albums_dirk,0750);
   err = stat(saved_curves_dirk,&statb);
   if (err) mkdir(saved_curves_dirk,0750);
   err = stat(writetext_dirk,&statb);
   if (err) mkdir(writetext_dirk,0750);
   err = stat(mashup_dirk,&statb);
   if (err) mkdir(mashup_dirk,0750);
   err = stat(slideshow_dirk,&statb);
   if (err) mkdir(slideshow_dirk,0750);
   err = stat(slideshow_trans_dirk,&statb);
   if (err) mkdir(slideshow_trans_dirk,0750);
   err = stat(printer_color_dirk,&statb);
   if (err) mkdir(printer_color_dirk,0750);
   err = stat(edit_scripts_dirk,&statb);
   if (err) mkdir(edit_scripts_dirk,0750);
   err = stat(maps_dirk,&statb);
   if (err) mkdir(maps_dirk,0750);
   err = stat(user_maps_dirk,&statb);
   if (err) mkdir(user_maps_dirk,0750);
   
   //  following are initialized in initzfunc() below:
   //  pattern_dirk  retouch_combo_dirk  custom_kernel_dirk  favorites_dirk

   load_params();                                                                //  restore parameters from last session

   for (int ii = 1; ii < argc; ii++)                                             //  command line parameters
   {
      char *pp = argv[ii];

      if (strmatchV(pp,"-debug","-d",0))                                         //  -d -debug
         Fdebug = 1;
      else if (strmatchV(pp,"-lang","-l",0) && argc > ii+1)                      //  -l -lang lc_RC   language/region code
         strncpy0(lang,argv[++ii],7);
      else if (strmatchV(pp,"-clone","-c",0) && argc > ii+4) {                   //  -c -clone        clone new instance
         Fclone = 1;
         cloxx = atoi(argv[ii+1]);                                               //  window position and size
         cloyy = atoi(argv[ii+2]);                                               //    passed from parent instance
         cloww = atoi(argv[ii+3]);
         clohh = atoi(argv[ii+4]);
         ii += 4;
      }
      else if (strmatchV(pp,"-recent","-r",0))                                   //  -r -recent       recent files gallery
         Frecent = 1;
      else if (strmatchV(pp,"-new","-n",0))                                      //  -n -new          new added files gallery
         Fnew = 1;
      else if (strmatchV(pp,"-prev","-p",0))                                     //  -p -prev         open previous file
         Fprev = 1;
      else if (strmatchV(pp,"-blank","-b",0))                                    //  -b -blank        start with blank window
         Fblank = 1;
      else if (strmatch(pp,"-noindex"))                                          //  -noindex         no index update   15.02
         Fnoindex = 1;
      else if (strmatchV(pp,"-menu","-m",0) && argc > ii+1)                      //  -m -menu func    start with menu func
         startmenu = zstrdup(argv[++ii]);
      else if (strmatchV(pp,"-album","-a",0) && argc > ii+1)                     //  -a -album <name> start with album  16.02
         startalbum = zstrdup(argv[++ii]);
      else if (strmatch(pp,"-cycledesktop") && argc > ii+2) {                    //  -cycledesktop album seconds        15.06
         run_cycledesktop(argv[ii+1],argv[ii+2]);                                //  run in background without GUI
         gtk_main();
         return 0;
      }
      else  initial_file = zstrdup(pp);                                          //  must be initial file or directory
   }

   ZTXinit(lang);                                                                //  setup locale and translations
   setlocale(LC_NUMERIC,"en_US.UTF-8");                                          //  stop comma decimal points

   build_widgets();                                                              //  build window widgets and menus

   arrowcursor = gdk_cursor_new_for_display(display,GDK_TOP_LEFT_ARROW);         //  cursor for selection
   dragcursor = gdk_cursor_new_for_display(display,GDK_CROSSHAIR);               //  cursor for dragging
   drawcursor = gdk_cursor_new_for_display(display,GDK_PENCIL);                  //  cursor for drawing lines
   blankcursor = gdk_cursor_new_for_display(display,GDK_BLANK_CURSOR);           //  invisible cursor

   if (! zfuncs::screen || ! zfuncs::mouse) {
      zpopup_message(0,"GTK/GDK failure to find hardware");                      //  15.09
      m_quit(0,0);
      exit(1);
   }

   m_viewmode(0,"F");                                                            //  set F mode initially

   if (Fclone) {                                                                 //  clone: open new window
      gtk_window_move(MWIN,cloxx+10,cloyy+10);                                   //    slightly offset from old window
      gtk_window_resize(MWIN,cloww,clohh);
   }
   else {
      gtk_window_move(MWIN,mwgeom[0],mwgeom[1]);                                 //  main window geometry
      gtk_window_resize(MWIN,mwgeom[2],mwgeom[3]);                               //  defaults or last session params
   }

   g_timeout_add(100,initzfunc,0);                                               //  initz. call from gtk_main()
   gtk_main();                                                                   //  start processing window events
   return 0;
}


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

//  Initial function called from gtk_main() at startup.
//  This function MUST return 0.

int initzfunc(void *)
{
   int      Fexiftool = 0, Fxdgopen = 0;
   int      ii, err, npid, contx;
   int      Fdirk = 0;
   FTYPE    ftype;
   char     *pp, *pp2;
   char     procfile[20], buff[200];
   char     KBshortsU[200], KBshortsI[200];
   char     tonefile[200], badnews[200];
   char     metalistfile[200], albumfile[200];
   char     favoritesfile[200];
   double   freememory, cachememory;
   float    exifver = 0;
   FILE     *fid;
   STATB    statb;

   printz(Frelease "\n");                                                        //  print Fotoxx release version

   int v1 = gtk_get_major_version();                                             //  get GTK release version
   int v2 = gtk_get_minor_version();
   int v3 = gtk_get_micro_version();
   printz("GTK version: %d.%02d.%02d \n",v1,v2,v3);
   
   //  check that necessary programs are installed

   fid = popen("exiftool -ver","r");                                             //  check exiftool version
   if (fid) {
      ii = fscanf(fid,"%s",buff);
      pclose(fid);                                                               //  bugfix: accept period or comma
      convSF(buff,exifver);
      printz("exiftool version: %.2f \n",exifver);
      if (exifver >= 8.60) Fexiftool = 1;
   }

   err = shell_quiet("which xdg-open");                                          //  check for xdg-open
   if (! err) Fxdgopen = 1;
   err = shell_quiet("which rawtherapee");                                       //  check for Raw Therapee
   if (! err) Frawtherapee = 1;
   err = shell_quiet("which growisofs");                                         //  check for growisofs                16.02
   if (! err) Fgrowisofs = 1;

   err = shell_quiet("which hugin");                                             //  check for pano tools (hugin versions)
   if (! err) {
      PTtools = 1;
      err = shell_quiet("which hugin_executor");                                 //  2015 release
      if (! err) PTtools = 2;
   }

   if (Fexiftool + Fxdgopen < 2) {                                               //  check mandatory dependencies
      strcpy(badnews,ZTX("Please install missing programs:"));
      if (! Fexiftool) strcat(badnews,"\n exiftool 8.6 or later");               //  refuse to start if any are missing
      if (! Fxdgopen) strcat(badnews,"\n xdg-utils");
      zmessageACK(Mwin,badnews);
      m_quit(0,0);
   }

   if (! Frawtherapee) printz("Raw Therapee not installed \n");
   if (! Fgrowisofs) printz("growisofs not installed \n");
   if (! PTtools) printz("Panorama Tools (Hugin) not installed \n");

   //  check for first time Fotoxx install or new release install

   if (Ffirsttime) {                                                             //  new fotoxx install
      showz_html(quickstart_file);
      Prelease = zstrdup(Frelease);
   }

   if (! Ffirsttime && ! strmatch(Prelease,Frelease)) {                          //  Fotoxx release change
      zmessageACK(Mwin,"fotoxx new release %s",Frelease);
      Prelease = zstrdup(Frelease);
      showz_textfile("doc","changelog");
   }

   Ffirsttime = 0;                                                               //  reset first time flag

   //  delete fotoxx tempdir files if owner process is no longer running

   contx = 0;
   while ((pp = command_output(contx,"find /tmp/fotoxx-*",0))) {
      pp2 = strchr(pp,'-');
      if (! pp2) continue;
      npid = atoi(pp2+1);                                                        //  pid of fotoxx owner process
      snprintf(procfile,20,"/proc/%d",npid);
      err = stat(procfile,&statb);
      if (! err) continue;                                                       //  pid is active, keep
      shell_quiet("rm -R -f -v %s",pp);                                          //  delete
   }

   //  set up temp directory /tmp/fotoxx-nnnnnn-XXXXXX

   snprintf(tempdir,100,"/tmp/fotoxx-%06d-XXXXXX",getpid());                     //  use PID + random chars.
   pp = mkdtemp(tempdir);
   if (! pp) {
      zmessageACK(Mwin,"%s \n %s",tempdir,strerror(errno));
      exit(1);
   }
   strncpy0(tempdir,pp,100);
   printz("tempdir: %s \n",tempdir);

   //  file name template for undo/redo files

   snprintf(URS_filename,100,"%s/undo_nn",tempdir);                              //  /tmp/fotoxx-nnnnnn/undo_nn

   //  check free memory and suggest image size limits

   parseprocfile("/proc/meminfo","MemFree:",&freememory,0);                      //  get amount of free memory
   parseprocfile("/proc/meminfo","Cached:",&cachememory,0);
   freememory = (freememory + cachememory) / 1024;                               //  megabytes
   printz("free memory: %.0f MB \n",freememory);
   printz("image size limits for good performance: \n");
   printz("  view: %.0f megapixels \n",(freememory-100)/6);                      //  F + preview, 3 bytes/pixel each
   printz("  edit: %.0f megapixels \n",(freememory-100)/54);                     //  + E0/E1/E3/ER, 12 bytes/pixel each

   //  update KB shortcuts if needed

   err = locale_filespec("user","KB-shortcuts",KBshortsU);                       //  user KB shortcuts
   if (err) {                                                                    //  not found
      locale_filespec("data","KB-shortcuts",KBshortsI);
      shell_quiet("cp %s %s",KBshortsI,KBshortsU);                               //  copy installed KB shortcuts
      printz("keyboard shortcuts installed \n");
   }
   KBshortcuts_load();                                                           //  load KB shortcuts from file

   //  copy default data files to user directories if not already

   err = stat(pattern_dirk,&statb);                                              //  pattern files
   if (err) {
      printz("copy default pattern files \n");
      mkdir(pattern_dirk,0750);
      shell_quiet("cp -n %s/patterns/* %s",get_zdatadir(),pattern_dirk);
   }

   err = stat(retouch_combo_dirk,&statb);                                        //  retouch combo settings
   if (err) {
      printz("copy default retouch_combo files \n");
      mkdir(retouch_combo_dirk,0750);
      shell_quiet("cp -n %s/retouch_combo/* %s",get_zdatadir(),retouch_combo_dirk);
   }

   err = stat(custom_kernel_dirk,&statb);                                        //  custom convolution kernels
   if (err) {
      printz("copy default custom_kernel files \n");
      mkdir(custom_kernel_dirk,0750);
      shell_quiet("cp -n %s/custom_kernel/* %s",get_zdatadir(),custom_kernel_dirk);
   }

   
   snprintf(favoritesfile,200,"%s/menu-config",favorites_dirk);                  //  favorites menu                     16.08
   err = stat(favoritesfile,&statb);
   if (err) {
      printz("copy default favorites menu \n");
      mkdir(favorites_dirk,0750);
      shell_quiet("cp -n %s/favorites/* %s",get_zdatadir(),favorites_dirk);
   }

   err = stat(tags_defined_file,&statb);                                         //  tags_defined file
   if (err) {
      printz("create default tags_defined file \n");
      shell_quiet("cp %s/tags_defined %s",get_zdatadir(),tags_defined_file);
   }

   snprintf(tonefile,200,"%s/slideshow-tone.oga",slideshow_dirk);                //  default slide show pause tone
   err = stat(tonefile,&statb);
   if (err) shell_quiet("cp %s/slideshow-tone.oga %s",get_zdatadir(),tonefile);

   snprintf(metalistfile,200,"%s/metadata_short_list",get_zuserdir());           //  metadata keys short list           15.10
   err = stat(metalistfile,&statb);
   if (err) shell_quiet("cp %s/metadata_short_list %s",get_zdatadir(),metalistfile);

   //  miscellaneous

   screen = gtk_window_get_screen(MWIN);                                         //  get monitor pixel dimensions
   screenww = gdk_screen_get_width(screen);
   screenhh = gdk_screen_get_height(screen);
   printz("screen width: %d  height: %d \n",screenww,screenhh);

   NWT = get_nprocs();                                                           //  get SMP CPU count
   if (NWT <= 0) NWT = 2;
   if (NWT > max_threads) NWT = max_threads;                                     //  compile time limit
   printz("using %d threads \n",NWT);

   zdialog_inputs("load");                                                       //  load saved dialog inputs
   zdialog_positions("load");                                                    //  load saved dialog positions

   exif_server(0,0,0);                                                           //  kill orphan exiftool process

   //  create or update image index file
   //  cannot continue until completed OK

   index_rebuild(0);                                                             //  quits if cannot complete
   printz("RAW file types found: %s \n",myRAWtypes);                             //  list RAW types found

   //  set current file and gallery from command line if present

   if (topdirks[0]) curr_dirk = zstrdup(topdirks[0]);                            //  default 1st top image directory    15.11
   else curr_dirk = zstrdup(getenv("HOME")); 
   
   if (startalbum) {                                                             //  16.02
      snprintf(albumfile,200,"%s/albums/%s",get_zuserdir(),startalbum);
      err = stat(albumfile,&statb);
      if (err) {
         printz("invalid album file: %s \n",albumfile);
         startalbum = 0;
      }
   }

   if (initial_file) {                                                           //  from command line
      pp = realpath(initial_file,0);                                             //  prepend directory if needed
      if (pp) {
         zfree(initial_file);
         initial_file = zstrdup(pp);
         free(pp);                                                               //  not zfree(), bugfix                15.05
      }
      else {
         printz("invalid file: %s \n",initial_file);                             //  invalid file path
         zfree(initial_file);
         initial_file = 0;
      }
   }

   if (initial_file) {
      ftype = image_file_type(initial_file);
      if (ftype == FNF) {                                                        //  non-existent file
         printz("invalid file: %s \n",initial_file);
         zfree(initial_file);
         initial_file = 0;
      }
      else if (ftype == FDIR) {                                                  //  is a directory
         if (curr_dirk) zfree(curr_dirk);
         curr_dirk = initial_file;
         initial_file = 0;
         Fdirk = 1;
      }
      else {
         if (curr_dirk) zfree(curr_dirk);
         curr_dirk = zstrdup(initial_file);                                      //  set current directory from file
         pp = strrchr(curr_dirk,'/');
         if (pp) *pp = 0;
      }
   }
   
   if (startalbum) {                                                             //  open initial album gallery         16.02
      navi::gallerytype = ALBUM;
      gallery(albumfile,"initF");
      m_viewmode(0,"G"); 
   }

   else if (initial_file)                                                        //  open initial image file
      f_open(initial_file);

   else if (Fdirk) {                                                             //  open initial directory gallery
      gallery(curr_dirk,"init");
      m_viewmode(0,"G");
      gallery(0,"paint",0);
   }

   else if (Fprev) {                                                             //  start with previous file
      if (last_curr_file && *last_curr_file == '/')                              //  15.07
         f_open(last_curr_file);
   }

   else if (Frecent)                                                             //  start with recent files gallery
      m_recentfiles(0,0);

   else if (Fnew)                                                                //  start with newest files gallery
      m_newfiles(0,"file");                                                      //    by file mod date

   else if (Fblank) {                                                            //  blank window, no gallery           15.07
      curr_file = curr_dirk = 0;
      navi::galleryname = 0;
      navi::gallerytype = NONE;
      set_mwin_title();
   }

   //  if no command line option, get startup display from user options

   else if (strmatch(startdisplay,"recent"))                                     //  start with recent files gallery
      m_recentfiles(0,0);

   else if (strmatch(startdisplay,"newest"))                                     //  start with newest files gallery    15.07
      m_newfiles(0,"file");                                                      //    by file mode date
   
   else if (strmatch(startdisplay,"prevG")) {                                    //  start with previous gallery        15.07
      if (last_gallerytype != NONE) {
         navi::gallerytype = last_gallerytype;
         if (last_gallerytype == GDIR)
            gallery(last_galleryname,"init");
         else gallery(last_galleryname,"initF");
         m_viewmode(0,"G");
         gallery(0,"paint",0);
      }
   }

   else if (strmatch(startdisplay,"prevI")) {                                    //  start with previous image file
      if (last_curr_file && *last_curr_file == '/') 
         f_open(last_curr_file);
   }

   else if (strmatch(startdisplay,"dirk")) {                                     //  start with given directory
      if (startdirk && *startdirk == '/') {
         if (curr_dirk) zfree(curr_dirk);
         curr_dirk = zstrdup(startdirk);
         gallery(curr_dirk,"init",0);
         m_viewmode(0,"G");
         gallery(0,"paint",0);
      }
   }

   else if (strmatch(startdisplay,"file"))                                       //  start with given image file
      f_open(startfile);

   if (startmenu) {                                                              //  startup menu on command line
      printz("start menu: %s \n",startmenu);
      for (ii = 0; ii < Nmenus; ii++) {                                          //  convert menu name to menu function
         if (! menutab[ii].menu) continue;                                       //  separator, null menu               15.12
         if (strmatchcase(startmenu,ZTX(menutab[ii].menu))) break;
      }
      if (ii < Nmenus) menutab[ii].func(0,menutab[ii].arg);                      //  call the menu function
   }

   save_params();                                                                //  save parameters now                15.07

   g_timeout_add(20,gtimefunc,0);                                                //  start periodic function (20 ms)    16.02

   return 0;                                                                     //  don't come back
}


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

//  functions for main window event signals

int delete_event()                                                               //  main window closed
{
   int      yn;

   printz("main window delete event \n");
   if (checkpend("mods")) return 1;                                              //  allow user bailout
   if (zfuncs::zdialog_busy) {
      yn = zmessageYN(Mwin,ZTX("Kill active dialog?"));                          //  allow user bailout
      if (! yn) return 1;
   }
   quitxx();
   return 0;
}

int destroy_event()                                                              //  main window destroyed
{
   printz("main window destroy event \n");
   quitxx();
   return 0;
}

int state_event(GtkWidget *, GdkEvent *event)                                    //  main window state changed
{
   int state = ((GdkEventWindowState *) event)->new_window_state;                //  track window fullscreen status
   if (state & GDK_WINDOW_STATE_FULLSCREEN) Ffullscreen = 1;
   else Ffullscreen = 0;
   return 0;
}


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

//  Periodic function (20 milliseconds)

int gtimefunc(void *)
{
   static int  domore = 0;

   if (Fshutdown) return 0;                                                      //  shutdown underway
   
   if (Fpaintlock && gdkwin) {                                                   //  thread request to freeze           16.02
      gdk_window_freeze_updates(gdkwin);                                         //    window updates
      zadd_locked(Fpaintlock,-1);
   }
   
   if (Fpaintunlock && gdkwin) {                                                 //  thread request to thaw             16.02
      gdk_window_thaw_updates(gdkwin);                                           //    window updates
      zadd_locked(Fpaintunlock,-1);
   }

   if (Fpaintrequest && Cdrawin)                                                 //  16.02
      gtk_widget_queue_draw(Cdrawin);

   if (zd_thread && zd_thread_event) {                                           //  send dialog event from thread
      if (! CEF || CEF->thread_status < 2) {
         zdialog_send_event(zd_thread,zd_thread_event);                          //  only if thread done or not busy    16.01
         zd_thread_event = 0;
      }
   }
   
   if (--domore > 0) return 1;                                                   //  do rest every 200 milliseconds     15.05
   domore = 10;

   update_Fpanel();                                                              //  update top panel information
   return 1;
}


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

//  update F window top panel with current status information
//  called from timer function

void update_Fpanel()
{
   static double     time1 = 0, time2, cpu1 = 0, cpu2, cpuload;
   char              text1[300], text2[100];
   static char       ptext1[300] = "";
   int               ww, hh, scale, bpc;
   static int        ftf = 1;
   double            file_MB = 1.0 / MEGA * curr_file_size;
   static cchar      *reduced, *areaactive, *dialogopen;
   static cchar      *blocked, *modified;

   if (! Fpanelshow) return;                                                     //  panel currently hidden             16.02
   
   if (ftf) {
      ftf = 0;
      reduced = ZTX("(reduced)");
      areaactive = ZTX("area active");
      dialogopen = ZTX("dialog open");
      blocked = ZTX("blocked");
      modified = "mod";
   }

   if (Fslideshow) return;                                                       //  16.01
   if (FGWM == 'G') goto update_busy;
   if (FGWM != 'F') return;

   *text1 = *text2 = 0;

   if (! time1) {
      time1 = get_seconds();
      cpu1 = jobtime();
   }

   time2 = get_seconds();                                                        //  compute process cpu load %
   if (time2 - time1 > 1.0) {                                                    //    at 1 second intervals
      cpu2 = jobtime();
      cpuload = 100.0 * (cpu2 - cpu1) / (time2 - time1);
      time1 = time2;
      cpu1 = cpu2;
   }

   parseprocfile("/proc/meminfo","MemFree:",&freemem,0);                         //  get amount of free memory
   freemem /= 1024;                                                              //  megabytes                          15.12

   snprintf(text1,300,"CPU %03.0f%c  MB %.0f",cpuload,'%',freemem);              //  CPU 023%  MB 2345

   if (curr_file && Fpxb)
   {
      if (E3pxm) {
         ww = E3pxm->ww;
         hh = E3pxm->hh;
      }
      else {
         ww = Fpxb->ww;
         hh = Fpxb->hh;
      }

      bpc = curr_file_bpc;

      snprintf(text2,100,"  %dx%dx%d",ww,hh,bpc);                                //  2345x1234x16 (preview) 1.56MB 45%
      strncatv(text1,300,text2,0);
      if (CEF && CEF->Fpreview) strncatv(text1,300," ",reduced,0);
      snprintf(text2,100,"  %.2fM",file_MB);
      strncatv(text1,300,text2,0);
      scale = Mscale * 100 + 0.5;
      snprintf(text2,100,"  %d%c",scale,'%');
      strncatv(text1,300,text2,0);

      if (URS_pos) {                                                             //  edit undo/redo stack depth
         snprintf(text2,100,"  %s: %d",ZTX("edits"),URS_pos);
         strncatv(text1,300,text2,0);
      }

      if (Fmetamod) strncatv(text1,300,"  ","metadata",0);
   }

   else if (Fpxb) {                                                              //  15.03
      snprintf(text2,100,"  %dx%d",Fpxb->ww,Fpxb->hh);
      strncatv(text1,300,text2,0);
   }

   if (sa_stat == 3) strncatv(text1,300,"  ",areaactive,0);
   if (zfuncs::zdialog_busy) strncatv(text1,300,"  ",dialogopen,0);

   if (Fblock) strncatv(text1,300,"  ",blocked,0);                               //  "blocked"
   if (CEF && CEF->Fmods) strncatv(text1,300,"  ",modified,0);                   //  "mod"
   if (*paneltext) strncatv(text1,300,"  ",paneltext,0);                         //  application text

   if (! strmatch(text1,ptext1)) {                                               //  if text changed, update panel bar
      gtk_label_set_label(GTK_LABEL(Fpanlab),text1);
      gtk_widget_show_all(Fpanel);
      strcpy(ptext1,text1);
   }

//  Show BUSY label if Ffuncbusy or Fthreadbusy active.                          //  overhauled  16.01
//  Show progress bar if Fbusy_goal > 0.
//  added to top panel:  BUSY |||||||||||..........| 

update_busy:

   static GtkWidget  *busylabel = 0, *donelabel = 0;
   static cchar      *busytext = "<span font=\"bold\" fgcolor=\"red\" > BUSY</span>";
   static char       donetext[] = "<span font=\"bold\" fgcolor=\"red\" >|::::::::::::::::::::|</span>";
   GtkWidget         *FGpanel;
   int               Nbars = 0, pbcc = 34;
   
   if (FGWM == 'F') FGpanel = Fpanel;                                            //  16.01
   else if (FGWM == 'G') FGpanel = Gpanel;
   else return;
   
   if (Ffuncbusy | Fthreadbusy) {
      if (! busylabel) {
         busylabel = gtk_label_new(null);
         gtk_label_set_markup(GTK_LABEL(busylabel),busytext);
         gtk_box_pack_start(GTK_BOX(FGpanel),busylabel,0,0,5);
      }
   }
   else {
      if (busylabel) gtk_widget_destroy(busylabel);
      busylabel = 0;
   }

   if (Fbusy_done > 0 && Fbusy_done < Fbusy_goal) {
      Nbars = 20.0 * Fbusy_done / Fbusy_goal + 0.5;                              //  N = 0-20
      if (Nbars > 20) Nbars = 20;
      memset(donetext+pbcc,':',Nbars);                                           //  [ N bars + 20-N blanks ]
      memset(donetext+pbcc+Nbars,' ',20-Nbars);
      if (! donelabel) {
         donelabel = gtk_label_new("");
         gtk_box_pack_start(GTK_BOX(FGpanel),donelabel,0,0,0);
      }
      gtk_label_set_markup(GTK_LABEL(donelabel),donetext);
   }
   else {
      if (donelabel) gtk_widget_destroy(donelabel);
      donelabel = 0;
   }
   
   gtk_widget_show_all(FGpanel);

   return;
}


//  block image updated by created threads during painting by main thread
//  block main thread window updates during changes to image data by created threads
//  lock = 1: block updates   lock = 0: unblock

void paintlock(int lock)                                                         //  16.02
{
   static int  freezecount = 0;
   int         nn;

   if (! gdkwin) return;

   if (pthread_equal(pthread_self(),zfuncs::tid_main)) {                         //  main thread caller
      if (lock) {
         gdk_window_freeze_updates(gdkwin);                                      //  freeze window updates
         nn = zadd_locked(freezecount,+1);                                       //  increment freeze counter
         if (nn <= 0) zappcrash("paintlock() freezecount: %d",freezecount);
      }
      else {
         gdk_window_thaw_updates(gdkwin);                                        //  thaw window updates
         nn = zadd_locked(freezecount,-1);                                       //  decrement freeze counter
         if (nn < 0) zappcrash("paintlock() freezecount: %d",freezecount);
      }
   }

   else {                                                                        //  created thread caller
      if (lock) {
         zadd_locked(Fpaintlock,+1);                                             //  set flag for gtimefunc()
         while (zadd_locked(Fpaintlock,0)) zsleep(0.01);                         //  wait for freeze done
         nn = zadd_locked(freezecount,+1);                                       //  increment freeze counter
         if (nn <= 0) zappcrash("paintlock() freezecount: %d",freezecount);
      }
      else {
         zadd_locked(Fpaintunlock,+1);                                           //  set flag for gtimefunc()
         while (zadd_locked(Fpaintunlock,0)) zsleep(0.01);                       //  wait for thaw done
         nn = zadd_locked(freezecount,-1);                                       //  decrement freeze counter
         if (nn < 0) zappcrash("paintlock() freezecount: %d",freezecount);
      }
   }

   return;
}


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

//  GTK3 "draw" function for F and W mode drawing windows.
//  Paint window when created, exposed, resized, or image modified (edited).
//  Update window image if scale change or window size change.
//  Otherwise do only the draw function (triggered from GTK).
//  Draw the image section currently within the visible window.
//  May NOT be called from threads. See Fpaint2() for threads.

int Fpaint(GtkWidget *Cdrawin, cairo_t *cr)
{
   PIXBUF         *pixbuf;
   PXB            *pxb1;
   GdkRGBA        rgba;
   static int     pdww = 0, pdhh = 0;                                            //  prior window size
   float          wscale, hscale;
   int            fww, fhh;                                                      //  current image size at 1x
   int            mww, mhh;                                                      //  scaled image size
   int            morgx, morgy;
   int            dorgx, dorgy;
   int            centerx, centery;
   int            refresh = 0;
   int            mousex, mousey;                                                //  mouse position after zoom
   float          magx, magy;                                                    //  mouse drag, magnification ratios
   uint8          *pixels, *pix, bgpix[3];
   int            rs, px, py;
   
   if (Fshutdown) return 1;                                                      //  shutdown underway

   if (! Cdrawin || ! gdkwin || ! Cstate || ! Cstate->fpxb) {                    //  no image                           15.10
      Fpaintrequest = 0;
      return 1;
   }
   
   Dww = gdk_window_get_width(gdkwin);                                           //  (new) drawing window size
   Dhh = gdk_window_get_height(gdkwin);
   if (Dww < 20 || Dhh < 20) return 1;                                           //  too small
   
   if (Dww != pdww || Dhh != pdhh) {                                             //  window size changed
      refresh = 1;                                                               //  image refresh needed
      pdww = Dww;
      pdhh = Dhh;
   }

   if (Fpaintrequest) {                                                          //  window image changed               15.09
      Fpaintrequest = 0;                                                         //  window updated as of NOW           15.09
      refresh = 1;                                                               //  image refresh needed
      if (FGWM == 'F' && (E0pxm || E3pxm)) {                                     //  insure F-view and E0 or E3
         if (E3pxm) pxb1 = PXM_PXB_copy(E3pxm);                                  //  update fpxb from E0/E3 image
         else pxb1 = PXM_PXB_copy(E0pxm);                                        //  or use already edited image
         PXB_free(Cstate->fpxb);
         Cstate->fpxb = pxb1;
      }
   }

   centerx = (Cstate->morgx + 0.5 * dww) / Cstate->mscale;                       //  center of window, image space
   centery = (Cstate->morgy + 0.5 * dhh) / Cstate->mscale;                       //  (before window or scale change)

   fww = Cstate->fpxb->ww;                                                       //  1x image size
   fhh = Cstate->fpxb->hh;

   if (Cstate->fzoom == 0) {                                                     //  scale to fit window
      wscale = 1.0 * Dww / fww;
      hscale = 1.0 * Dhh / fhh;
      if (wscale < hscale) Cstate->mscale = wscale;                              //  use greatest ww/hh ratio
      else  Cstate->mscale = hscale;
      if (fww <= Dww && fhh <= Dhh && ! Fblowup) Cstate->mscale = 1.0;           //  small image 1x unless Fblowup
      zoomx = zoomy = 0;                                                         //  no zoom target
   }
   else Cstate->mscale = Cstate->fzoom;                                          //  scale to fzoom level

   mww = fww * Cstate->mscale;                                                   //  scaled image size for window
   mhh = fhh * Cstate->mscale;

   dww = Dww;                                                                    //  image fitting inside drawing window
   if (dww > mww) dww = mww;                                                     //  image < window size
   dhh = Dhh;
   if (dhh > mhh) dhh = mhh;
   
   if (Cstate->mscale != Cstate->pscale) {                                       //  scale changed
      Cstate->morgx = Cstate->mscale * centerx - 0.5 * dww;                      //  change origin to keep same center
      Cstate->morgy = Cstate->mscale * centery - 0.5 * dhh;                      //  (subject to later rules)
      Cstate->pscale = Cstate->mscale;                                           //  remember scale
      refresh = 1;                                                               //  image refresh needed
   }

   if (! Cstate->mpxb) refresh++;                                                //  need to make mpxb

   if (refresh) {                                                                //  image refresh needed
      if (Cstate->mpxb) PXB_free(Cstate->mpxb);
      if (Cstate->mscale == 1) Cstate->mpxb = PXB_copy(Cstate->fpxb);            //  fast 1x image
      else Cstate->mpxb = PXB_rescale(Cstate->fpxb,mww,mhh);                     //  rescaled image
   }

   if ((Mxdrag || Mydrag)) {                                                     //  pan/scroll via mouse drag
      zoomx = zoomy = 0;                                                         //  no zoom target
      magx = 1.0 * (mww - dww) / dww;
      magy = 1.0 * (mhh - dhh) / dhh;                                            //  needed magnification of mouse movement
      if (magx < 1) magx = 1;                                                    //  retain a minimum speed
      if (magy < 1) magy = 1;

      if (Fdragopt == 1) {
         Cstate->morgx -= round(Mwdragx);                                        //  same direction
         Cstate->morgy -= round(Mwdragy);
      }
      if (Fdragopt == 2) {
         Cstate->morgx += round(Mwdragx);                                        //  opposite direction
         Cstate->morgy += round(Mwdragy);
      }
      if (Fdragopt == 3) {
         Cstate->morgx -= round(Mwdragx * magx);                                 //  same direction, magnified
         Cstate->morgy -= round(Mwdragy * magy);
      }
      if (Fdragopt == 4) {
         Cstate->morgx += round(Mwdragx * magx);                                 //  opposite direction, magnified
         Cstate->morgy += round(Mwdragy * magy);
      }
   }

   if (dww < Dww) Cstate->dorgx = 0.5 * (Dww - dww);                             //  if scaled image < window,
   else Cstate->dorgx = 0;                                                       //    center image in window
   if (dhh < Dhh) Cstate->dorgy = 0.5 * (Dhh - dhh);
   else Cstate->dorgy = 0;

   if (Fshiftright && dww < Dww-1) Cstate->dorgx = Dww-1 - dww;                  //  shift image to right margin        16.01
   
   if (zoomx || zoomy) {                                                         //  requested zoom center
      Cstate->morgx = Cstate->mscale * zoomx - 0.5 * dww;                        //  corresp. window position within image
      Cstate->morgy = Cstate->mscale * zoomy - 0.5 * dhh;
   }

   if (Cstate->morgx < 0) Cstate->morgx = 0;                                     //  maximize image within window
   if (Cstate->morgy < 0) Cstate->morgy = 0;                                     //  (no unused margins)
   if (Cstate->morgx + dww > mww) Cstate->morgx = mww - dww;
   if (Cstate->morgy + dhh > mhh) Cstate->morgy = mhh - dhh;

   if (zoomx || zoomy) {                                                         //  zoom target
      mousex = zoomx * Cstate->mscale - Cstate->morgx + Cstate->dorgx;
      mousey = zoomy * Cstate->mscale - Cstate->morgy + Cstate->dorgy;           //  mouse pointer follows target
      move_pointer(Cdrawin,mousex,mousey);
      zoomx = zoomy = 0;                                                         //  reset zoom target
   }

   if (zddarkbrite) darkbrite_paint();                                           //  update dark/bright pixels

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

   morgx = Cstate->morgx;                                                        //  window position in (larger) image
   morgy = Cstate->morgy;
   dorgx = Cstate->dorgx;
   dorgy = Cstate->dorgy;

   if (refresh) {                                                                //  renew background image             16.01
      if (BGpixbuf) g_object_unref(BGpixbuf);
      BGpixbuf = gdk_pixbuf_new(GDKRGB,0,8,dww,dhh);
      pixels = gdk_pixbuf_get_pixels(BGpixbuf);
      rs = gdk_pixbuf_get_rowstride(BGpixbuf);

      bgpix[0] = Fbgcolor[0];                                                    //  use background color               16.07
      bgpix[1] = Fbgcolor[1];
      bgpix[2] = Fbgcolor[2];

      for (py = 0; py < dhh; py++)
      for (px = 0; px < dww; px++) {
         pix = pixels + py * rs + px * 3;
         if (py % 10 < 2 && px % 10 < 2)                                         //  with periodic black dots
            memset(pix,0,3);
         else memcpy(pix,bgpix,3);
      }
   }

   gdk_cairo_set_source_pixbuf(cr,BGpixbuf,dorgx,dorgy);                         //  paint background image             16.01
   cairo_paint(cr);

   pixbuf = Cstate->mpxb->pixbuf;                                                //  get image section within window
   pixbuf = gdk_pixbuf_new_subpixbuf(pixbuf,morgx,morgy,dww,dhh);
   gdk_cairo_set_source_pixbuf(cr,pixbuf,dorgx,dorgy);                           //  paint section
   cairo_paint(cr);
   g_object_unref(pixbuf);
   
   mwcr = cr;                                                                    //  cairo context for draw funcs below
   cairo_set_line_width(mwcr,1);

   if (Cstate == &Fstate) {                                                      //  view mode is image
      if (Ntoplines) draw_toplines(1);                                           //  draw line overlays
      if (gridsettings[currgrid][GON]) draw_gridlines();                         //  draw grid lines
      if (Ntoptext) draw_toptext();                                              //  draw text strings
      if (Ntopcircles) draw_topcircles();                                        //  draw circles
      if (Fshowarea) sa_show(1);                                                 //  draw select area outline
      if (refresh && zdbrdist) m_show_brdist(0,0);                               //  update brightness dist.      moved 15.12.1
   }

   if (Cstate == &Wstate)                                                        //  view mode is world maps
      geomap_paint_dots();                                                       //  15.01

   mwcr = 0;                                                                     //  cairo context invalid
   return 1;
}


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

//  Repaint modified image synchrounously.
//  May NOT be called from threads.

void Fpaintnow()
{
   if (! Cdrawin || ! gdkwin || ! Cstate || ! Cstate->fpxb) {                    //  no image                           15.10
      printf("Fpaintnow(), no image \n");
      return;
   }

   Fpaintrequest = 1;                                                            //  request repaint of changed image   15.09
   gtk_widget_queue_draw(Cdrawin);
   while (Fpaintrequest) { 
      zsleep(0.001);
      zmainloop();
   }

   return;
}


//  Cause (modified) output image to get repainted soon.
//  Fpaint() will be called by gtimefunc() next timer cycle.
//  MAY be called from threads.

void Fpaint2()
{
   Fpaintrequest = 1;                                                            //  request repaint of changed image   15.09
   return;
}


//  Update a section of Fpxb and Mpxb from an updated section of E3pxm,
//  then update the corresponding section of the drawing window.
//  This avoids a full image refresh, E3pxm > fpxb > mpxb > drawing window.
//  px3, py3, ww3, hh3: modified section within E3pxm to be propagated.
//  May NOT be called from threads.

void Fpaint3(int px3, int py3, int ww3, int hh3)
{
   PXM_PXB_update(E3pxm,Fpxb,px3,py3,ww3,hh3);                                   //  E3pxm > Fpxb, both 1x scale
   PXB_PXB_update(Fpxb,Mpxb,px3,py3,ww3,hh3);                                    //  Fpxb > Mpxb, scaled up or down
   Fpaint4(px3,py3,ww3,hh3);                                                     //  update drawing window from Mpxb
   return;
}


//  Do the same using E0pxm instead of E3pxm

void Fpaint0(int px3, int py3, int ww3, int hh3)                                 //  16.07
{
   PXM_PXB_update(E0pxm,Fpxb,px3,py3,ww3,hh3);
   PXB_PXB_update(Fpxb,Mpxb,px3,py3,ww3,hh3);
   Fpaint4(px3,py3,ww3,hh3);
   return;
}


//  Repaint a section of the Mpxb image in the visible window.
//  px3, py3, ww3, hh3: area to be repainted (in 1x image space).
//  May NOT be called from threads.
//  Writes directly on the window (cairo pixbuf paint).

void Fpaint4(int px3, int py3, int ww3, int hh3)
{
   PIXBUF      *pixbuf, *bgpixbuf;
   int         px1, py1, px2, py2;
   int         ww1, hh1, ww2, hh2;
   int         crflag = 0;
   
   zthreadcrash();
   
   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   px2 = Mscale * px3 - 2;                                                       //  1x image space to mpxb space
   py2 = Mscale * py3 - 2;                                                       //  (expanded)
   ww2 = Mscale * ww3 + 2 / Mscale + 4;
   hh2 = Mscale * hh3 + 2 / Mscale + 4;

   if (px2 + ww2 > Mpxb->ww) ww2 = Mpxb->ww - px2;                               //  avoid overshoot
   if (py2 + hh2 > Mpxb->hh) hh2 = Mpxb->hh - py2;

   if (px2 < Morgx) {                                                            //  reduce to currently visible window
      ww2 = ww2 - (Morgx - px2);
      px2 = Morgx;
   }

   if (py2 < Morgy) {
      hh2 = hh2 - (Morgy - py2);
      py2 = Morgy;
   }

   if (ww2 <= 0 || hh2 <= 0) return;                                             //  completely outside visible window
   
   paintlock(1);                                                                 //  16.03

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }

   pixbuf = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,px2,py2,ww2,hh2);              //  Mpxb area to paint                 15.08

   px1 = px2 - Morgx;                                                            //  corresp. position in background image
   py1 = py2 - Morgy;

   px2 = px2 - Morgx + Dorgx;                                                    //  corresp. position in drawing window
   py2 = py2 - Morgy + Dorgy;

   if (Mpxb->nc > 3) {                                                           //  alpha channel present              16.01
      ww1 = ww2;                                                                 //  draw background image to area
      hh1 = hh2;
      if (px1 + ww1 > dww) ww1 = dww - px1;
      if (py1 + hh1 > dhh) hh1 = dhh - py1;
      bgpixbuf = gdk_pixbuf_new_subpixbuf(BGpixbuf,px1,py1,ww1,hh1);
      gdk_cairo_set_source_pixbuf(mwcr,bgpixbuf,px2,py2);
      cairo_paint(mwcr);
      g_object_unref(bgpixbuf);
   }

   gdk_cairo_set_source_pixbuf(mwcr,pixbuf,px2,py2);                             //  draw area to window
   cairo_paint(mwcr);

   g_object_unref(pixbuf);

   if (Fshowarea) {
      px3 = (px2 - Dorgx + Morgx) / Mscale;                                      //  back to image scale, expanded
      py3 = (py2 - Dorgy + Morgy) / Mscale;
      ww3 = ww2 / Mscale + 2;
      hh3 = hh2 / Mscale + 2;
      sa_show_rect(px3,py3,ww3,hh3);                                             //  refresh select area outline
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }
   
   paintlock(0);                                                                 //  16.03
   return;
}


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

//  F/W view window mouse event function - capture buttons and drag movements

void mouse_event(GtkWidget *widget, GdkEventButton *event, void *)
{
   void mouse_convert(int xpos1, int ypos1, int &xpos2, int &ypos2);

   int            button, time, type, scroll;
   static int     bdtime = 0, butime = 0;
   static int     mdragx0, mdragy0;
   static int     busy = 0;
   
   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   type = event->type;
   button = event->button;                                                       //  button, 1/2/3 = left/center/right
   time = event->time;
   Mwxposn = event->x;                                                           //  mouse position in window
   Mwyposn = event->y;
   mouse_convert(Mwxposn,Mwyposn,Mxposn,Myposn);                                 //  convert to image space

   scroll = ((GdkEventScroll *) event)->direction;                               //  scroll wheel event

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

   if (type == GDK_SCROLL) {                                                     //  scroll wheel = zoom
      zoomx = Mxposn;                                                            //  zoom center = mouse position
      zoomy = Myposn;
      if (scroll == GDK_SCROLL_UP) m_zoom(0,"in");
      if (scroll == GDK_SCROLL_DOWN) m_zoom(0,"out");
      return;
   }

   if (type == GDK_BUTTON_PRESS) {                                               //  button down
      Mdrag++;                                                                   //  possible drag start
      bdtime = time;                                                             //  time of button down
      Mbutton = button;
      mdragx0 = Mwxposn;                                                         //  window position at button down
      mdragy0 = Mwyposn;
      Mxdown = Mxposn;                                                           //  image position at button down
      Mydown = Myposn;
      Mxdrag = Mydrag = 0;
   }

   if (type == GDK_MOTION_NOTIFY && Mdrag) {                                     //  drag underway
      Mwdragx = Mwxposn - mdragx0;                                               //  drag increment, window space
      Mwdragy = Mwyposn - mdragy0;
      mdragx0 = Mwxposn;                                                         //  new drag origin = current position
      mdragy0 = Mwyposn;
      Mxdrag = Mxposn;                                                           //  drag position, image space
      Mydrag = Myposn;
   }

   if (type == GDK_BUTTON_RELEASE) {                                             //  button up
      Mxclick = Myclick  = 0;                                                    //  reset click status
      butime = time;                                                             //  time of button up
      if (butime - bdtime < 400) {                                               //  less than 0.4 secs down
         if (Mxposn == Mxdown && Myposn == Mydown) {                             //       and not moving
            if (Mbutton == 1) LMclick++;                                         //  left mouse click
            if (Mbutton == 3) RMclick++;                                         //  right mouse click
            Mxclick = Mxdown;                                                    //  click = button down position
            Myclick = Mydown;
            if (button == 2) {                                                   //  center button click
               zoomx = Mxposn;                                                   //  re-center at mouse (Doriano)
               zoomy = Myposn;
               gtk_widget_queue_draw(Cdrawin);
            }
         }
      }
      Mxdown = Mydown = Mxdrag = Mydrag = Mdrag = Mbutton = 0;                   //  forget buttons and drag
   }

   Fmousemain = 1;                                                               //  mouse acts on main window
   if (Mcapture) Fmousemain = 0;                                                 //  curr. function handles mouse
   if (mouseCBfunc) Fmousemain = 0;                                              //  mouse owned by callback function
   if (KBcontrolkey) Fmousemain = 1;                                             //  mouse acts on main window

   if (mouseCBfunc && ! Fmousemain) {                                            //  pass to callback function
      if (! busy) {
         busy = 1;                                                               //  stop re-entrance                   15.12
         (* mouseCBfunc)();
         busy = 0;
         Fmousemain = 1;                                                         //  click/drag params are processed here
      }                                                                          //    unless reset by callback func.
   }                                                                       

   if (FGWM == 'W') geomap_mousefunc();                                          //  geomap mouse function

   if (! Fmousemain) return;                                                     //  curr. function handles mouse

   if (LMclick) {                                                                //  left click = zoom request
      LMclick = 0;
      zoomx = Mxclick;                                                           //  zoom center = mouse
      zoomy = Myclick;
      m_zoom(0,"in");
   }

   if (RMclick) {                                                                //  right click
      RMclick = 0;
      if (Cstate->fzoom) {                                                       //  if zoomed image, reset to fit window
         zoomx = zoomy = 0;
         m_zoom(0,"fit");
      }
      else if (curr_file && FGWM == 'F')
         image_Rclick_popup();                                                   //  image right-click popup menu
   }

   if (Mxdrag || Mydrag)                                                         //  drag = scroll by mouse
      gtk_widget_queue_draw(Cdrawin);

   return;
}


//  convert mouse position from window space to image space

void mouse_convert(int xpos1, int ypos1, int &xpos2, int &ypos2)
{
   xpos2 = (xpos1 - Cstate->dorgx + Cstate->morgx) / Cstate->mscale + 0.5;
   ypos2 = (ypos1 - Cstate->dorgy + Cstate->morgy) / Cstate->mscale + 0.5;

   if (xpos2 < 0) xpos2 = 0;                                                     //  if outside image put at edge
   if (ypos2 < 0) ypos2 = 0;

   if (E3pxm) {
      if (xpos2 >= E3pxm->ww) xpos2 = E3pxm->ww-1;
      if (ypos2 >= E3pxm->hh) ypos2 = E3pxm->hh-1;
   }
   else {
      if (xpos2 >= Cstate->fpxb->ww) xpos2 = Cstate->fpxb->ww-1;
      if (ypos2 >= Cstate->fpxb->hh) ypos2 = Cstate->fpxb->hh-1;
   }

   return;
}


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

//  set new image zoom level or magnification
//  zoom:   "in"        zoom-in in steps
//          "out"       zoom-out in steps
//          "fit"       zoom to fit window
//          "100"       toggle 100% and fit window

void m_zoom(GtkWidget *, cchar *zoom)
{
   int         fww, fhh;
   float       scalew, scaleh, fitscale, fzoom2, zratio = 1;
   float       Rzoom, pixels;

   zthreadcrash();

   if (! Cstate || ! Cstate->fpxb) return;
   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   Rzoom = Cstate->fzoom;                                                        //  current zoom ratio

   if (E3pxm) {
      fww = E3pxm->ww;                                                           //  1x image size
      fhh = E3pxm->hh;
   }
   else  {
      fww = Cstate->fpxb->ww;
      fhh = Cstate->fpxb->hh;
   }

   if (fww > Dww || fhh > Dhh) {                                                 //  get window fit scale
      scalew = 1.0 * Dww / fww;
      scaleh = 1.0 * Dhh / fhh;
      if (scalew < scaleh) fitscale = scalew;
      else fitscale = scaleh;                                                    //  window/image, < 1.0
   }
   else fitscale = 1.0;                                                          //  if image < window use 100%

   if (strmatch(zoom,"Zoom+")) zoom = "in";                                      //  menu button: + = zoom in
   if (strmatch(zoom,"Zoom-")) zoom = "fit";                                     //               - = fit window

   if (strmatch(zoom,"fit")) Cstate->fzoom = 0;                                  //  zoom to fit window

   if (strmatch(zoom,"100")) {
      if (Cstate->fzoom != 0) Cstate->fzoom = 0;                                 //  toggle 100% and fit window
      else  Cstate->fzoom = 1;
   }

   if (strstr("in out",zoom))                                                    //  caller: zoom in or out
   {
      if (! Cstate->fzoom) Cstate->fzoom = fitscale;                             //  current zoom scale
      for (fzoom2 = 0.125; fzoom2 < 4.0; fzoom2 *= zoomratio)                    //  find nearest natural ratio
         if (Cstate->fzoom < fzoom2 * sqrt(zoomratio)) break;
      if (strmatch(zoom,"in")) zratio = zoomratio;                               //  zoom in, make image larger
      if (strmatch(zoom,"out")) zratio = 1.0 / zoomratio;                        //  zoom out, make image smaller
      Cstate->fzoom = fzoom2 * zratio;

      if (Cstate->fzoom > 0.124 && Cstate->fzoom < 0.126)                        //  hit these ratios exactly
         Cstate->fzoom = 0.125;
      if (Cstate->fzoom > 0.24  && Cstate->fzoom < 0.26)
         Cstate->fzoom = 0.25;
      if (Cstate->fzoom > 0.49  && Cstate->fzoom < 0.51)
         Cstate->fzoom = 0.50;
      if (Cstate->fzoom > 0.99  && Cstate->fzoom < 1.01)
         Cstate->fzoom = 1.00;
      if (Cstate->fzoom > 1.99  && Cstate->fzoom < 2.01)
         Cstate->fzoom = 2.00;
      if (Cstate->fzoom > 3.99) Cstate->fzoom = 4.0;                             //  max. allowed zoom
      if (Cstate->fzoom < fitscale) Cstate->fzoom = 0;                           //  image < window
   }

   if (FGWM == 'W') {                                                            //  optimize for geomaps
      if (strmatch(zoom,"in") && Cstate->fzoom < 1.0)
         Cstate->fzoom = 1.0;                                                    //  zoom to 100% directly
      if (strmatch(zoom,"out")) Cstate->fzoom = 0.0;                             //  zoom out = fit window directly
      if (Cstate->fzoom == 1.0 && Rzoom == 1.0) {
         gtk_widget_queue_draw(Cdrawin);                                         //  if already, move mouse pointer only
         return;
      }
   }

   if (Cstate->fzoom > 1.0) {                                                    //  limit image in window to 1 gigabyte
      pixels = Cstate->fzoom * fww * Cstate->fzoom * fhh;                        //  (333 megapixels x 3 bytes/pixel)
      if (pixels > 333 * MEGA) {                                                 //  if size is at maximum,
         Cstate->fzoom = Rzoom;                                                  //    move mouse pointer only
         gtk_widget_queue_draw(Cdrawin);
         return;
      }
   }

   if (! Cstate->fzoom) zoomx = zoomy = 0;                                       //  no requested zoom center
   Fpaint2();                                                                    //  refresh window
   return;
}


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

//  function for dialogs to call to send KB keys for processing by main app

void KBstate(GdkEventKey *event, int state)
{
   if (state) KBpress(0,event,0);
   else KBrelease(0,event,0);
   return;
}


//  keyboard event functions
//  GDK key symbols: /usr/include/gtk-3.0/gdk/gdkkeysyms.h

namespace trimrotate { void KBfunc(int key); }                                   //  keyboard functions called from here
namespace perspective { void KBfunc(int key); }
namespace mashup { void KBfunc(int key); }

int KBpress(GtkWidget *win, GdkEventKey *event, void *)                          //  prevent propagation of key-press
{                                                                                //    events to menu buttons
   KBkey = event->keyval;

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

   return 1;
}

int KBrelease(GtkWidget *win, GdkEventKey *event, void *)                        //  some key was released
{
   int      ii, jj, cc;
   char     matchkey[20];

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

   KBkey = event->keyval;

   if (! KBcontrolkey && ! KBshiftkey && ! KBaltkey) {                           //  f/g/w/m keys set corresp. window mode
      if (KBkey == GDK_KEY_f) { m_viewmode(0,"F"); goto KBend; }                 //  Ctrl/Alt + f/g/w/m usable for shortcuts
      if (KBkey == GDK_KEY_g) { m_viewmode(0,"G"); goto KBend; }
      if (KBkey == GDK_KEY_w) { m_viewmode(0,"W"); goto KBend; }
      if (KBkey == GDK_KEY_m) { m_viewmode(0,"M"); goto KBend; }                 //  OSM                                16.05
   }

   if (KBcapture) return 1;                                                      //  let current function handle it

   if (KBcontrolkey + KBshiftkey + KBaltkey) goto KBcustom;                      //  combinations are always custom     15.06

//  reserved shortcuts (not user configurable)

   if (KBkey == GDK_KEY_F1) {                                                    //  F1  >>  user guide
      showz_userguide(F1_help_topic);                                            //  show topic if there, or page 1
      goto KBend;
   }

   if (KBkey == GDK_KEY_F10) {                                                   //  F10: fullscreen toggle with menu   16.02
      if (Fslideshow) {
         ss_escape = 1;                                                          //  quit slideshow if active
         goto KBend;
      }
      if (! Ffullscreen) win_fullscreen(0);                                      //  toggle full-screen mode and back
      else win_unfullscreen();
      goto KBend;
   }

   if (KBkey == GDK_KEY_F11) {                                                   //  F11: fullscreen toggle no menu     16.02
      if (Fslideshow) {
         ss_escape = 1;                                                          //  quit slideshow if active
         goto KBend;
      }
      if (! Ffullscreen) win_fullscreen(1);                                      //  toggle full-screen mode and back
      else win_unfullscreen();
      goto KBend;
   }

   if (KBkey == GDK_KEY_Escape) {                                                //  ESC key
      if (Fslideshow) ss_escape = 1;                                             //  quit slideshow if active           16.01
      else if (Ffullscreen) win_unfullscreen();                                  //  exit full screen mode
      goto KBend;
   }

   if (KBkey == GDK_KEY_s)                                                       //  's' shortcut for Sync Gallery      15.02
      navi::menufuncx(0,"Sync Gallery");                                         //  15.12

   if (FGWM == 'G') {                                                            //  window mode = G
      navi::KBrelease(win,event,0);                                              //  pass to gallery KB function
      goto KBend;                                                                //  (keys below not for gallery)
   }

   if (KBkey == GDK_KEY_z) {                                                     //  'z' key
      if (! KBcontrolkey) m_zoom(0,"100");                                       //  if no CTRL, toggle zoom 1x
      goto KBend;
   }

   if (FGWM == 'W') goto KBend;                                                  //  window mode = W, no other KB events

   if (KBkey == GDK_KEY_plus) m_zoom(0,"in");                                    //  + key  >>  zoom in
   if (KBkey == GDK_KEY_minus) m_zoom(0,"fit");                                  //  - key  >>  fit to window
   if (KBkey == GDK_KEY_equal) m_zoom(0,"in");                                   //  = key: same as +
   if (KBkey == GDK_KEY_KP_Add) m_zoom(0,"in");                                  //  keypad +
   if (KBkey == GDK_KEY_KP_Subtract) m_zoom(0,"fit");                            //  keypad -

   if (Fslideshow) {                                                             //  slide show mode
      if (KBkey == GDK_KEY_Left) ss_Larrow = 1;                                  //  arrow keys = prev/next image
      if (KBkey == GDK_KEY_Right) ss_Rarrow = 1;
      if (KBkey == GDK_KEY_space) ss_spacebar = 1;                               //  spacebar = pause/resume
      if (KBkey == GDK_KEY_b) ss_Bkey = 1;                                       //  B = blank screen + pause
      if (KBkey == GDK_KEY_x) ss_Xkey = 1;                                       //  X = shortcut for Magnify
      if (KBkey == GDK_KEY_n) ss_Nkey = 1;                                       //  N = do transition + next image     16.01
      goto KBend;
   }

   if (Fmashup) {                                                                //  mashup active, pass KB key
      mashup::KBfunc(KBkey);
      goto KBend;
   }

   if (CEF && CEF->menufunc == m_trimrotate) {                                   //  trim_rotate active, pass KB key
      trimrotate::KBfunc(KBkey);
      goto KBend;
   }

   if (CEF && CEF->menufunc == m_perspective) {                                  //  perspective active, pass KB key
      perspective::KBfunc(KBkey);
      goto KBend;
   }

   if (KBkey == GDK_KEY_Left) m_prev(0,0);                                       //  arrow keys = prev/next image
   if (KBkey == GDK_KEY_Right) m_next(0,0);
   if (KBkey == GDK_KEY_Page_Up) m_prev(0,0);                                    //  page up/down = prev/next image
   if (KBkey == GDK_KEY_Page_Down) m_next(0,0);
   if (KBkey == GDK_KEY_Delete) m_delete_trash(0,0);                             //  delete  >>  trash                  16.06
   if (KBkey == GDK_KEY_x) m_magnify(0,0);                                       //  'x' Magnify                        16.05

//  look for custom shortcut keys in shortcut table

KBcustom:

   if (KBkey >= GDK_KEY_F1 && KBkey <= GDK_KEY_F9) {                             //  input key is F1 to F9
      ii = KBkey - GDK_KEY_F1;
      strcpy(matchkey,"F1");
      matchkey[1] += ii;
   }

   else if (KBkey > 255) goto KBend;                                             //  not a single letter or digit

   else {
      *matchkey = 0;                                                             //  build input key combination
      if (KBcontrolkey) strcat(matchkey,"Ctrl+");                                //  [Ctrl+] [Alt+] [Shift+] key
      if (KBaltkey) strcat(matchkey,"Alt+");
      if (KBshiftkey) strcat(matchkey,"Shift+");
      cc = strlen(matchkey);
      matchkey[cc] = KBkey;
      matchkey[cc+1] = 0;
   }

   for (ii = 0; ii < Nshortcuts; ii++)                                           //  convert key combination to menu name
      if (strmatchcase(matchkey,shortcutkey[ii])) break;
   
   if (ii < Nshortcuts) {                                                        //  convert menu name to menu function
      for (jj = 0; jj < Nmenus; jj++) {
         if (! menutab[jj].menu) continue;                                       //  ignore separator
         if (strmatchcase(shortcutmenu[ii],ZTX(menutab[jj].menu))) break;
      }

      if (jj < Nmenus) menutab[jj].func(0,menutab[jj].arg);                      //  call the menu function
   }

KBend:
   KBkey = 0;
   return 1;
}


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

//  set the main window to fullscreen status
//  (with no menu or panel)

void win_fullscreen(int hidemenu)
{
   zthreadcrash();

   if (FGWM == 'F' && hidemenu) {                                                //  if F window, hide menu and panel
      gtk_widget_hide(Fmenu);
      gtk_widget_hide(Fpanel);
      Fpanelshow = 0;                                                            //  16.02
   }

   gtk_window_fullscreen(MWIN);
   while (! Ffullscreen) zmainloop();                                            //  15.09
   return;
}


//  restore window to former size and restore menu etc.

void win_unfullscreen()
{
   zthreadcrash();

   gtk_window_unfullscreen(MWIN);                                                //  restore old window size
   gtk_widget_show(Fmenu);
   gtk_widget_show(Fpanel);
   Fpanelshow = 1;                                                               //  16.02
   while (Ffullscreen) zmainloop();                                              //  16.01
   return;
}


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

//  update information in main window title bar

namespace meta_names {
   extern char     meta_pdate[16];                                               //  image (photo) date, yyyymmddhhmmss
}


void set_mwin_title()
{
   GTYPE       gtype;
   int         cc, fposn, Nfiles, Nimages;
   char        *pp, titlebar[250];
   char        pdate[12], ptime[12], pdatetime[20];
   char        fname[100], gname[100], fdirk[100];

   zthreadcrash();

   if (! curr_file || *curr_file != '/') {
      gtk_window_set_title(MWIN,Frelease);
      return;
   }

   pp = (char *) strrchr(curr_file,'/');
   strncpy0(fname,pp+1,99);                                                      //  file name
   cc = pp - curr_file;
   if (cc < 99) strncpy0(fdirk,curr_file,cc+2);                                  //  get dirk/path/ if short enough
   else {
      strncpy(fdirk,curr_file,96);                                               //  or use /dirk/path...
      strcpy(fdirk+95,"...");
   }

   Nfiles = navi::Nfiles;                                                        //  total gallery files (incl. directories)
   Nimages = navi::Nimages;                                                      //  total image files

   fposn = gallery_position(curr_file,curr_file_posn);                           //  curr. file in curr. gallery?
   if (fposn >= 0) {
      curr_file_posn = fposn;
      fposn = fposn + 1 - Nfiles + Nimages;                                      //  position among images, 1-based
   }

   if (*meta_names::meta_pdate) {
      metadate_pdate(meta_names::meta_pdate,pdate,ptime);                        //  get formatted date and time        15.03
      strncpy0(pdatetime,pdate,11);                                              //  yyyy-mm-dd hh:mm
      strncpy0(pdatetime+11,ptime,6);
      pdatetime[10] = ' ';
   }
   else strcpy(pdatetime,"(undated)");

   gtype = navi::gallerytype;

   if (gtype == GDIR)                                                            //  gallery name = directory
      snprintf(titlebar,250," %d/%d  %s  %s  %s",                                //  16.04
               fposn,Nimages,fdirk,fname,pdatetime);
   else {
      if (gtype == SEARCH || gtype == METADATA)
         strcpy(gname,"SEARCH RESULTS");
      else if (gtype == ALBUM) {
         pp = strrchr(navi::galleryname,'/');
         if (! pp) pp = navi::galleryname;
         else pp++;
         strcpy(gname,"ALBUM: ");
         strncpy0(gname+7,pp,87);
      }
      else if (gtype == RECENT)
         strcpy(gname,"RECENT FILES");
      else if (gtype == NEWEST)
         strcpy(gname,"NEWEST FILES");
      else
         strcpy(gname,"NO GALLERY");

      if (fposn > 0)
         snprintf(titlebar,250,"%s  %d/%d  %s  %s  %s",                          //  window title bar                   16.04
                  gname,fposn,Nimages,fdirk,fname,pdatetime);
      else
         snprintf(titlebar,250,"%s  (*)/%d  %s  %s  %s",                         //  image not in gallery               16.04
                  gname,Nimages,fdirk,fname,pdatetime);
   }

   gtk_window_set_title(MWIN,titlebar);
   return;
}


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

//  draw a pixel using foreground color.
//  px, py are image space.

void draw_pixel(int px, int py)
{
   int               qx, qy, crflag = 0;
   static int        pqx, pqy;
   static uint8      pixel[12];                                                  //  2x2 block of pixels
   static PIXBUF     *pixbuf1 = 0, *pixbuf4 = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   if (! pixbuf1) {
      pixbuf1 = gdk_pixbuf_new_from_data(pixel,GDKRGB,0,8,1,1,3,0,0);            //  1x1 pixels
      pixbuf4 = gdk_pixbuf_new_from_data(pixel,GDKRGB,0,8,2,2,6,0,0);            //  2x2 pixels
   }
   
   if (Cstate->fpxb->nc > 3)                                                     //  omit transparent pixels            15.09
      if (PXBpix(Cstate->fpxb,px,py)[3] < 128) return;

   qx = Mscale * px - Morgx;                                                     //  image to window space
   qy = Mscale * py - Morgy;

   if (qx == pqx && qy == pqy) return;                                           //  avoid redundant points

   pqx = qx;
   pqy = qy;

   if (qx < 0 || qx > dww-2) return;                                             //  keep off image edges
   if (qy < 0 || qy > dhh-2) return;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }

   if (Mscale <= 1) {                                                            //  write 1x1 pixels                   15.08
      pixel[0] = LINE_COLOR[0];
      pixel[1] = LINE_COLOR[1];
      pixel[2] = LINE_COLOR[2];

      gdk_cairo_set_source_pixbuf(mwcr,pixbuf1,qx+Dorgx,qy+Dorgy);
      cairo_paint(mwcr);
   }

   else {                                                                        //  write 2x2 fat pixels
      pixel[0] = pixel[3] = pixel[6] = pixel[9] = LINE_COLOR[0];
      pixel[1] = pixel[4] = pixel[7] = pixel[10] = LINE_COLOR[1];
      pixel[2] = pixel[5] = pixel[8] = pixel[11] = LINE_COLOR[2];

      gdk_cairo_set_source_pixbuf(mwcr,pixbuf4,qx+Dorgx,qy+Dorgy);
      cairo_paint(mwcr);
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


//  erase one drawn pixel - restore from window image Mpxb.
//  px, py are image space.

void erase_pixel(int px, int py)
{
   GdkPixbuf      *pixbuf;
   static int     pqx, pqy;
   int            qx, qy, crflag = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   qx = Mscale * px;                                                             //  image to window space
   qy = Mscale * py;

   if (qx == pqx && qy == pqy) return;                                           //  avoid same target pixel

   pqx = qx;
   pqy = qy;

   if (qx < 0 || qx > Mpxb->ww-2) return;                                        //  pixel outside scaled image
   if (qy < 0 || qy > Mpxb->hh-2) return;

   if (qx < Morgx || qx > Morgx + dww-2) return;                                 //  pixel outside drawing window
   if (qy < Morgy || qy > Morgy + dhh-2) return;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }
   
   pixbuf = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,qx,qy,2,2);                    //  2x2 Mpxb area to copy              15.08
   qx = qx - Morgx + Dorgx;                                                      //  target pixel in window
   qy = qy - Morgy + Dorgy;
   gdk_cairo_set_source_pixbuf(mwcr,pixbuf,qx,qy);
   cairo_paint(mwcr);

   g_object_unref(pixbuf);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


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

//  draw line.
//  coordinates are image space.
//  type = 1/2 for solid/dotted line

void draw_line(int x1, int y1, int x2, int y2, int type)
{
   float       px1, py1, px2, py2;
   double      dashes[2] = { 4, 4 };
   int         crflag = 0;
   double      R, G, B;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   px1 = Mscale * x1 - Morgx + Dorgx;                                            //  image to window space
   py1 = Mscale * y1 - Morgy + Dorgy;
   px2 = Mscale * x2 - Morgx + Dorgx;
   py2 = Mscale * y2 - Morgy + Dorgy;

   if (px1 > Dww-2) px1 = Dww-2;                                                 //  play nice
   if (py1 > Dhh-2) py1 = Dhh-2;
   if (px2 > Dww-2) px2 = Dww-2;
   if (py2 > Dhh-2) py2 = Dhh-2;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   R = LINE_COLOR[0] / 255.0;                                                    //  use line color
   G = LINE_COLOR[1] / 255.0;
   B = LINE_COLOR[2] / 255.0;

   cairo_set_source_rgb(mwcr,R,G,B);
   if (type == 2) cairo_set_dash(mwcr,dashes,2,0);                               //  dotted line 
   else cairo_set_dash(mwcr,dashes,0,0);

   cairo_move_to(mwcr,px1,py1);                                                  //  draw line
   cairo_line_to(mwcr,px2,py2);
   cairo_stroke(mwcr);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


//  erase line. refresh line path from mpxb window image.
//  double line width is erased.
//  coordinates are image space.

void erase_line(int x1, int y1, int x2, int y2)
{
   float       pxm, pym, slope;
   int         crflag = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }

   if (abs(y2 - y1) > abs(x2 - x1)) {
      slope = 1.0 * (x2 - x1) / (y2 - y1);
      for (pym = y1; pym <= y2; pym++) {
         pxm = x1 + slope * (pym - y1);
         erase_pixel(pxm,pym);
      }
   }

   else {
      slope = 1.0 * (y2 - y1) / (x2 - x1);
      for (pxm = x1; pxm <= x2; pxm++) {
         pym = y1 + slope * (pxm - x1);
         erase_pixel(pxm,pym);
      }
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


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

//  draw pre-set overlay lines on top of image
//  arg = 1:   paint lines only (because window repainted)
//        2:   erase lines and forget them
//        3:   erase old lines, paint new lines, save new in old

void draw_toplines(int arg)
{
   int         ii, crflag = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   if (arg == 2 || arg == 3)                                                     //  erase old lines
      for (ii = 0; ii < Nptoplines; ii++)
         erase_line(ptoplines[ii].x1,ptoplines[ii].y1,
                    ptoplines[ii].x2,ptoplines[ii].y2);

   if (arg == 1 || arg == 3)                                                     //  draw new lines
      for (ii = 0; ii < Ntoplines; ii++)
         draw_line(toplines[ii].x1,toplines[ii].y1,
                   toplines[ii].x2,toplines[ii].y2,toplines[ii].type);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   if (arg == 2) {
      Nptoplines = Ntoplines = 0;                                                //  forget lines
      return;
   }

   for (ii = 0; ii < Ntoplines; ii++)                                            //  save for future erase
      ptoplines[ii] = toplines[ii];

   Nptoplines = Ntoplines;

   return;
}


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

//  draw a grid of horizontal and vertical lines.
//  grid line spacings are in window space.

void draw_gridlines()
{
   int         crflag = 0, G = currgrid;
   int         px, py, gww, ghh;
   int         startx, starty, endx, endy, stepx, stepy;
   int         startx1, starty1;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

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

   gww = dww;                                                                    //  grid box size
   ghh = dhh;
   startx = Dorgx;                                                               //  starting corner (top left)
   starty = Dorgy;

   if (CEF && strmatch(CEF->funcname,"trim_rotate")) {                           //  trim/rotate function is active
      gww = Mscale * (trimrect[2] - trimrect[0]);                                //  fit grid box to trim rectangle
      ghh = Mscale * (trimrect[3] - trimrect[1]);
      startx = Mscale * trimrect[0] - Morgx + Dorgx;
      starty = Mscale * trimrect[1] - Morgy + Dorgy;
   }

   endx = startx + gww;
   endy = starty + ghh;

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

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

   startx1 = startx + stepx * gridsettings[G][GXF] / 100;                        //  variable starting offsets
   starty1 = starty + stepy * gridsettings[G][GYF] / 100;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   cairo_set_source_rgb(mwcr,1,1,1);                                             //  white lines

   if (gridsettings[G][GX] && stepx)
      for (px = startx1; px < endx; px += stepx) {
         cairo_move_to(mwcr,px,starty);
         cairo_line_to(mwcr,px,endy);
      }

   if (gridsettings[G][GY] && stepy)
      for (py = starty1; py < endy; py += stepy) {
         cairo_move_to(mwcr,startx,py);
         cairo_line_to(mwcr,endx,py);
      }

   cairo_stroke(mwcr);

   cairo_set_source_rgb(mwcr,0,0,0);                                             //  adjacent black lines

   if (gridsettings[G][GX] && stepx)
      for (px = startx1+1; px < endx+1; px += stepx) {
         cairo_move_to(mwcr,px,starty);
         cairo_line_to(mwcr,px,endy);
      }

   if (gridsettings[G][GY] && stepy)
      for (py = starty1+1; py < endy+1; py += stepy) {
         cairo_move_to(mwcr,startx,py);
         cairo_line_to(mwcr,endx,py);
      }

   cairo_stroke(mwcr);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


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

//  maintain a set of text strings written over the image in the window.
//  add a new text string to the list.
//  multiple text strings can be added with the same ID.
//  px and py are image space.

void add_toptext(int ID, int px, int py, cchar *text, cchar *font)
{
   zthreadcrash();

   if (Ntoptext == maxtoptext) {
      printz("*** maxtoptext exceeded \n");
      return;
   }

   int ii = Ntoptext++;
   toptext[ii].ID = ID;
   toptext[ii].px = px;
   toptext[ii].py = py;
   toptext[ii].text = text;
   toptext[ii].font = font;

   return;
}


//  draw current text strings over the image in window.
//  called from Fpaint().

void draw_toptext()
{
   int         crflag = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   for (int ii = 0; ii < Ntoptext; ii++)
      draw_text(toptext[ii].px,toptext[ii].py,toptext[ii].text,toptext[ii].font);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


//  delete text strings having the given ID from the list

void erase_toptext(int ID)
{
   int      ii, jj;

   for (ii = jj = 0; ii < Ntoptext; ii++)
   {
      if (toptext[ii].ID == ID) continue;
      else toptext[jj++] = toptext[ii];
   }

   Ntoptext = jj;
   return;
}


//  draw text on window, black on white background
//  coordinates are image space

void draw_text(int px, int py, cchar *text, cchar *font)
{
   static PangoFontDescription   *pangofont = 0;
   static PangoLayout            *pangolayout = 0;
   static char                   priorfont[40] = "";
   int                           ww, hh, crflag = 0;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;

   px = Mscale * px - Morgx + Dorgx;                                             //  image to window space
   py = Mscale * py - Morgy + Dorgy;

   if (! strmatch(font,priorfont)) {                                             //  change font
      strncpy0(priorfont,font,40);
      if (pangofont) pango_font_description_free(pangofont);
      if (pangolayout) g_object_unref(pangolayout);
      pangofont = pango_font_description_from_string(font);                      //  make pango layout for font
      pangolayout = gtk_widget_create_pango_layout(Cdrawin,0);
      pango_layout_set_font_description(pangolayout,pangofont);
   }

   pango_layout_set_text(pangolayout,text,-1);                                   //  add text to layout
   pango_layout_get_pixel_size(pangolayout,&ww,&hh);

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   cairo_set_source_rgb(mwcr,1,1,1);                                             //  draw white background
   cairo_rectangle(mwcr,px,py,ww,hh);
   cairo_fill(mwcr);

   cairo_move_to(mwcr,px,py);                                                    //  draw layout with text
   cairo_set_source_rgb(mwcr,0,0,0);
   pango_cairo_show_layout(mwcr,pangolayout);

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


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

//  maintain a set of circles drawn over the image in the window
//  px, py are image space, radius is window space

void add_topcircle(int px, int py, int radius)
{
   if (Ntopcircles == maxtopcircles) {
      printz("*** maxtopcircles exceeded \n");
      return;
   }

   int ii = Ntopcircles++;
   topcircles[ii].px = px;
   topcircles[ii].py = py;
   topcircles[ii].radius = radius;

   return;
}


//  draw current circles over the image in the window
//  called from window repaint function Fpaint()

void draw_topcircles()
{
   int         ii, px, py, rad;
   int         crflag = 0;
   double      R, G, B;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      cairo_set_line_width(mwcr,1);
      crflag = 1;
   }

   R = LINE_COLOR[0] / 255.0;                                                    //  use LINE_COLOR
   G = LINE_COLOR[1] / 255.0;
   B = LINE_COLOR[2] / 255.0;

   for (ii = 0; ii < Ntopcircles; ii++)
   {
      px = topcircles[ii].px * Mscale - Morgx + Dorgx;                           //  image to window space
      py = topcircles[ii].py * Mscale - Morgy + Dorgy;
      rad = topcircles[ii].radius;                                               //  radius is window space

      cairo_set_source_rgb(mwcr,R,G,B);
      cairo_arc(mwcr,px,py,rad,0,2*PI);                                          //  draw 360 deg. arc
      cairo_stroke(mwcr);
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


//  erase top circles (next window repaint)

void erase_topcircles()
{
   Ntopcircles = 0;
   return;
}


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

//  Draw circle around the mouse pointer.
//  Prior circle will be erased first.
//  Used for mouse/brush radius in select and paint functions.
//  cx, cy, cr: center and radius of circle in image space.
//  if Ferase, then erase previous circle only.

void draw_mousecircle(int cx, int cy, int cr, int Ferase)                        //  15.09
{
   int         px3, py3, ww3, hh3;
   static int  ppx3, ppy3, pww3 = 0, phh3;
   int         crflag = 0;
   int         px, py, pok;
   double      R, G, B;
   double      t, dt, t1, t2;
   
   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->mpxb) return;                                       //  no image

   if (pww3 > 0) {                                                               //  erase prior
      Fpaint4(ppx3,ppy3,pww3,phh3);                                              //  refresh from Mpxb
      pww3 = 0;
   }

   if (Ferase) return;                                                           //  erase only, done

   px3 = cx - cr - 2;                                                            //  convert pointer center + radius
   py3 = cy - cr - 2;                                                            //    to block position, width, length
   ww3 = 2 * cr + 4;
   hh3 = 2 * cr + 4;

   ppx3 = px3;                                                                   //  remember pixel block area
   ppy3 = py3;                                                                   //    to erase in next call
   pww3 = ww3;
   phh3 = hh3;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }
   
   cx = cx * Mscale - Morgx + Dorgx;                                             //  convert to window coordinates
   cy = cy * Mscale - Morgy + Dorgy;
   cr = cr * Mscale;
   
   R = LINE_COLOR[0] / 255.0;                                                    //  use LINE_COLOR
   G = LINE_COLOR[1] / 255.0;
   B = LINE_COLOR[2] / 255.0;

   cairo_set_source_rgba(mwcr,R,G,B,1.0);                                        //  16.08
   
   t1 = t2 = -1;                                                                 //  angle limits of arc to draw
   dt = 1.0 / cr;

   for (t = 0; t < 2*PI; t += dt)                                                //  loop 0-360 degrees
   {
      px = cx + cr * cos(t);                                                     //  pixel on mouse circle
      py = cy + cr * sin(t);
      
      pok = 1;                                                                   //  assume pixel OK to draw
      
      if (px < Dorgx || py < Dorgy) pok = 0;                                     //  outside image limits
      if (px >= Dorgx+dww || py >= Dorgy+dhh) pok = 0;

/***      
      if (pok && nc > 3) {                                                       //  removed 16.08
         qx = px + Morgx - Dorgx;                                                //  check alpha channel
         qy = py + Morgy - Dorgy;
         mpix1 = mpix + qy * rs + qx * nc;
         if (mpix1[3] < 128) pok = 0;                                            //  pixel is transparent
      }
***/
      
      if (pok) {                                                                 //  pixel ok, add to arc
         if (t1 < 0) t1 = t;                                                     //  start of arc to draw
         t2 = t;                                                                 //  end of arc, so far
      }
      
      else if (t1 >= 0) {                                                        //  pixel not ok
         cairo_arc(mwcr,cx,cy,cr,t1,t2);                                         //  draw accumulated arc
         cairo_stroke(mwcr);
         t1 = t2 = -1;                                                           //  start over
      }
   }
   
   if (t1 >= 0) {
      cairo_arc(mwcr,cx,cy,cr,t1,t2);                                            //  draw rest of arc
      cairo_stroke(mwcr);
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


//  duplicate for drawing and tracking a 2nd mouse circle
//  (used by paint/clone to track source pixels being cloned)

void draw_mousecircle2(int cx, int cy, int cr, int Ferase)                       //  15.09
{
   int         px3, py3, ww3, hh3;
   static int  ppx3, ppy3, pww3 = 0, phh3;
   int         crflag = 0;
   int         px, py, pok;
   double      R, G, B;
   double      t, dt, t1, t2;
   
   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->mpxb) return;                                       //  no image

   if (pww3 > 0) {                                                               //  erase prior
      Fpaint4(ppx3,ppy3,pww3,phh3);                                              //  refresh from Mpxb
      pww3 = 0;
   }

   if (Ferase) return;                                                           //  erase only, done

   px3 = cx - cr - 2;                                                            //  convert pointer center + radius
   py3 = cy - cr - 2;                                                            //    to block position, width, length
   ww3 = 2 * cr + 4;
   hh3 = 2 * cr + 4;

   ppx3 = px3;                                                                   //  remember pixel block area
   ppy3 = py3;                                                                   //    to erase in next call
   pww3 = ww3;
   phh3 = hh3;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }
   
   cx = cx * Mscale - Morgx + Dorgx;                                             //  convert to window coordinates
   cy = cy * Mscale - Morgy + Dorgy;
   cr = cr * Mscale;
   
   R = LINE_COLOR[0] / 255.0;                                                    //  use LINE_COLOR
   G = LINE_COLOR[1] / 255.0;
   B = LINE_COLOR[2] / 255.0;

   cairo_set_source_rgba(mwcr,R,G,B,1.0);                                        //  16.08
   
   t1 = t2 = -1;                                                                 //  angle limits of arc to draw
   dt = 1.0 / cr;

   for (t = 0; t < 2*PI; t += dt)                                                //  loop 0-360 degrees
   {
      px = cx + cr * cos(t);                                                     //  pixel on mouse circle
      py = cy + cr * sin(t);
      
      pok = 1;                                                                   //  assume pixel OK to draw
      
      if (px < Dorgx || py < Dorgy) pok = 0;                                     //  outside image limits
      if (px >= Dorgx+dww || py >= Dorgy+dhh) pok = 0;

/***      
      if (pok && nc > 3) {                                                       //  removed 16.08
         qx = px + Morgx - Dorgx;                                                //  check alpha channel
         qy = py + Morgy - Dorgy;
         mpix1 = mpix + qy * rs + qx * nc;
         if (mpix1[3] < 128) pok = 0;                                            //  pixel is transparent
      }
***/
      
      if (pok) {                                                                 //  pixel ok, add to arc
         if (t1 < 0) t1 = t;                                                     //  start of arc to draw
         t2 = t;                                                                 //  end of arc, so far
      }
      
      else if (t1 >= 0) {                                                        //  pixel not ok
         cairo_arc(mwcr,cx,cy,cr,t1,t2);                                         //  draw accumulated arc
         cairo_stroke(mwcr);
         t1 = t2 = -1;                                                           //  start over
      }
   }
   
   if (t1 >= 0) {
      cairo_arc(mwcr,cx,cy,cr,t1,t2);                                            //  draw rest of arc
      cairo_stroke(mwcr);
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   return;
}


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

//  Draw ellipse around the mouse pointer.
//  Prior ellipse will be erased first.
//  cx, cy, cww, chh: center and axes of ellipse
//  if Ferase, then erase previous ellipse only.

void draw_mousearc(int cx, int cy, int cww, int chh, int Ferase)
{
   int         px3, py3, ww3, hh3;
   static int  ppx3, ppy3, pww3 = 0, phh3;
   int         px, py;
   int         crflag = 0;
   float       a, b, a2, b2;
   float       x, y, x2, y2;

   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Cstate || ! Cstate->fpxb) return;                                       //  no image

   paintlock(1);                                                                 //  16.03

   if (pww3 > 0) {                                                               //  erase prior
      Fpaint4(ppx3,ppy3,pww3,phh3);                                              //  refresh from Mpxb
      pww3 = 0;
   }

   if (Ferase) {
      paintlock(0);                                                              //  16.03
      return;
   }

   px3 = cx - (cww + 2) / 2;                                                     //  convert pointer center + radius
   py3 = cy - (chh + 2) / 2;                                                     //    to block position, width, length
   ww3 = cww + 2;
   hh3 = chh + 2;

   ppx3 = px3;                                                                   //  remember pixel block area
   ppy3 = py3;                                                                   //    to erase in next call
   pww3 = ww3;
   phh3 = hh3;

   if (! mwcr) {
      mwcr = gdk_cairo_create(gdkwin);                                           //  create cairo context if not already
      crflag = 1;
   }

   a = cww / 2;                                                                  //  ellipse constants from
   b = chh / 2;                                                                  //    enclosing rectangle
   a2 = a * a;
   b2 = b * b;

   for (y = -b; y < b; y++)                                                      //  step through y values, omitting
   {                                                                             //    curve points covered by x values
      y2 = y * y;
      x2 = a2 * (1 - y2 / b2);
      x = sqrtf(x2);                                                             //  corresp. x values, + and -
      py = y + cy;
      px = cx - x + 0.5;
      draw_pixel(px,py);                                                         //  draw 2 points on ellipse
      px = cx + x + 0.5;
      draw_pixel(px,py);
   }

   for (x = -a; x < a; x++)                                                      //  step through x values, omitting
   {                                                                             //    curve points covered by y values
      x2 = x * x;
      y2 = b2 * (1 - x2 / a2);
      y = sqrtf(y2);                                                             //  corresp. y values, + and -
      px = x + cx;
      py = cy - y + 0.5;
      draw_pixel(px,py);                                                         //  draw 2 points on ellipse
      py = cy + y + 0.5;
      draw_pixel(px,py);
   }

   if (crflag) {
      cairo_destroy(mwcr);
      mwcr = 0;
   }

   paintlock(0);                                                                 //  16.03
   return;
}


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

   spline curve setup and edit functions

   Support multiple frames with curves in parallel
   (edit curve(s) and image mask curve)

   Usage:
   Add frame widget to dialog or zdialog.
   Set up drawing in frame:
      sd = splcurve_init(frame, callback_func)
   Initialize no. of curves in frame (1-10):
      sd->Nspc = n
   Initialize active flag for curve spc:
      sd->fact[spc] = 1                                                    15.10
   Initialize vert/horz flag for curve spc:
      sd->vert[spc] = hv
   Initialize anchor points for curve spc:
      sd->nap[spc], sd->apx[spc][xx], sd->apy[spc][yy]
   Generate data for curve spc:
      splcurve_generate(sd,spc)
   Curves will now be shown and edited inside the frame when window is realized.
   The callback_func(spc) will be called when curve spc is edited.
   Change curve in program:
      set anchor points, call splcurve_generate(sd,spc)
   Get y-value (0-1) for curve spc and given x-value (0-1):
      yval = splcurve_yval(sd,spc,xval)
   If faster access to curve is needed (no interpolation):
      kk = 1000 * xval;
      if (kk > 999) kk = 999;
      yval = sd->yval[spc][kk];

***/

//  initialize for spline curve editing
//  initial anchor points are pre-loaded into spldat before window is realized

spldat * splcurve_init(GtkWidget *frame, void func(int spc))
{
   zthreadcrash();

   int      cc = sizeof(spldat);                                                 //  allocate spc curve data area
   spldat * sd = (spldat *) zmalloc(cc);
   memset(sd,0,cc);

   sd->drawarea = gtk_drawing_area_new();                                        //  drawing area for curves
   gtk_container_add(GTK_CONTAINER(frame),sd->drawarea);
   sd->spcfunc = func;                                                           //  user callback function

   gtk_widget_add_events(sd->drawarea,GDK_BUTTON_PRESS_MASK);                    //  connect mouse events to drawing area
   gtk_widget_add_events(sd->drawarea,GDK_BUTTON_RELEASE_MASK);
   gtk_widget_add_events(sd->drawarea,GDK_BUTTON1_MOTION_MASK);
   G_SIGNAL(sd->drawarea,"motion-notify-event",splcurve_adjust,sd);
   G_SIGNAL(sd->drawarea,"button-press-event",splcurve_adjust,sd);
   G_SIGNAL(sd->drawarea,"draw",splcurve_draw,sd);

   return sd;
}


//  modify anchor points in curve using mouse

int splcurve_adjust(void *, GdkEventButton *event, spldat *sd)
{
   int            ww, hh, kk;
   int            mx, my, button, evtype;
   static int     spc, ap, mbusy = 0, Fdrag = 0;                                 //  drag continuation logic
   int            minspc, minap, apset = 0;
   float          mxval, myval, cxval, cyval;
   float          dist2, mindist2 = 0;
   float          dist, dx, dy;
   float          minx = 0.01 * splcurve_minx;                                   //  % to absolute distance             15.10

   zthreadcrash();

   mx = event->x;                                                                //  mouse position in drawing area
   my = event->y;
   evtype = event->type;
   button = event->button;

   if (evtype == GDK_MOTION_NOTIFY) {
      if (mbusy) return 0;                                                       //  discard excess motion events
      mbusy++;
      zmainloop();
      mbusy = 0;
   }

   if (evtype == GDK_BUTTON_RELEASE) {
      Fdrag = 0;
      return 0;
   }

   ww = gtk_widget_get_allocated_width(sd->drawarea);                            //  drawing area size
   hh = gtk_widget_get_allocated_height(sd->drawarea);

   if (mx < 0) mx = 0;                                                           //  limit edge excursions
   if (mx > ww) mx = ww;
   if (my < 0) my = 0;
   if (my > hh) my = hh;

   if (evtype == GDK_BUTTON_PRESS) Fdrag = 0;                                    //  left or right click

   if (Fdrag)                                                                    //  continuation of drag
   {
      if (sd->vert[spc]) {
         mxval = 1.0 * my / hh;                                                  //  mouse position in curve space
         myval = 1.0 * mx / ww;
      }
      else {
         mxval = 1.0 * mx / ww;
         myval = 1.0 * (hh - my) / hh;
      }

      if (ap < sd->nap[spc] - 1) {                                               //  not the last anchor point
         dx = sd->apx[spc][ap+1] - mxval;                                        //  get distance to next anchor point
         if (dx < 0.01) return 0;                                                //  x-value not increasing, forbid
         dy = sd->apy[spc][ap+1] - myval;
         dist = sqrtf(dx * dx + dy * dy);
         if (dist < minx) return 0;                                              //  too close, forbid
      }
      if (ap > 0) {                                                              //  not the first anchor point
         dx = mxval - sd->apx[spc][ap-1];                                        //  get distance to prior anchor point
         if (dx < 0.01) return 0;                                                //  x-value not increasing, forbid
         dy = myval - sd->apy[spc][ap-1];
         dist = sqrtf(dx * dx + dy * dy);
         if (dist < minx) return 0;                                              //  too close, forbid
      }
      
      apset = 1;                                                                 //  mxval/myval = new node position    15.10
   }

   else                                                                          //  mouse click or new drag begin
   {
      minspc = minap = -1;                                                       //  find closest curve/anchor point
      mindist2 = 999999;

      for (spc = 0; spc < sd->Nspc; spc++)                                       //  loop curves
      {
         if (! sd->fact[spc]) continue;                                          //  not active                         15.10

         if (sd->vert[spc]) {
            mxval = 1.0 * my / hh;                                               //  mouse position in curve space
            myval = 1.0 * mx / ww;
         }
         else {
            mxval = 1.0 * mx / ww;
            myval = 1.0 * (hh - my) / hh;
         }

         for (ap = 0; ap < sd->nap[spc]; ap++)                                   //  loop anchor points
         {
            cxval = sd->apx[spc][ap];
            cyval = sd->apy[spc][ap];
            dist2 = (mxval-cxval)*(mxval-cxval)
                  + (myval-cyval)*(myval-cyval);
            if (dist2 < mindist2) {
               mindist2 = dist2;                                                 //  remember closest anchor point
               minspc = spc;
               minap = ap;
            }
         }
      }

      if (minspc < 0) return 0;                                                  //  impossible
      spc = minspc;                                                              //  nearest curve
      ap = minap;                                                                //  nearest anchor point
   }

   if (evtype == GDK_BUTTON_PRESS && button == 3)                                //  right click, remove anchor point
   {
      if (sqrtf(mindist2) > minx) return 0;                                      //  not close enough
      if (sd->nap[spc] < 3) return 0;                                            //  < 2 anchor points would remain
      sd->nap[spc]--;                                                            //  decr. before loop
      for (kk = ap; kk < sd->nap[spc]; kk++) {
//       if (kk < 0) printz("meaningless reference %d \n",kk);                   //  stop gcc opt. bug - removed        16.01
         sd->apx[spc][kk] = sd->apx[spc][kk+1];
         sd->apy[spc][kk] = sd->apy[spc][kk+1];
      }
      splcurve_generate(sd,spc);                                                 //  regenerate data for modified curve
      gtk_widget_queue_draw(sd->drawarea);
      sd->spcfunc(spc);                                                          //  call user function
      return 0;
   }

   if (! Fdrag)                                                                  //  new drag or left click
   {
      if (sd->vert[spc]) {
         mxval = 1.0 * my / hh;                                                  //  mouse position in curve space
         myval = 1.0 * mx / ww;
      }
      else {
         mxval = 1.0 * mx / ww;
         myval = 1.0 * (hh - my) / hh;
      }

      if (sqrtf(mindist2) < minx)                                                //  anchor point close enough,
      {                                                                          //    move this one to mouse position
         if (ap < sd->nap[spc]-1) {                                              //  not the last anchor point
            dx = sd->apx[spc][ap+1] - mxval;                                     //  get distance to next anchor point
            if (dx < 0.01) return 0;                                             //  x-value not increasing, forbid
            dy = sd->apy[spc][ap+1] - myval;
            dist = sqrtf(dx * dx + dy * dy);
            if (dist < minx) return 0;                                           //  too close, forbid
         }
         if (ap > 0) {                                                           //  not the first anchor point
            dx = mxval - sd->apx[spc][ap-1];                                     //  get distance to prior anchor point
            if (dx < 0.01) return 0;                                             //  x-value not increasing, forbid
            dy = myval - sd->apy[spc][ap-1];
            dist = sqrtf(dx * dx + dy * dy);
            if (dist < minx) return 0;                                           //  too close, forbid
         }

         apset = 1;                                                              //  mxval/myval = new node position    15.10
      }

      else                                                                       //  none close, add new anchor point
      {
         minspc = -1;                                                            //  find closest curve to mouse
         mindist2 = 999999;

         for (spc = 0; spc < sd->Nspc; spc++)                                    //  loop curves
         {
            if (! sd->fact[spc]) continue;                                       //  not active                         15.10

            if (sd->vert[spc]) {
               mxval = 1.0 * my / hh;                                            //  mouse position in curve space
               myval = 1.0 * mx / ww;
            }
            else {
               mxval = 1.0 * mx / ww;
               myval = 1.0 * (hh - my) / hh;
            }

            cyval = splcurve_yval(sd,spc,mxval);
            dist2 = fabsf(myval - cyval);
            if (dist2 < mindist2) {
               mindist2 = dist2;                                                 //  remember closest curve
               minspc = spc;
            }
         }

         if (minspc < 0) return 0;                                               //  impossible
         if (mindist2 > minx) return 0;                                          //  not close enough to any curve
         spc = minspc;

         if (sd->nap[spc] > 49) {
            zmessageACK(Mwin,ZTX("Exceed 50 anchor points"));
            return 0;
         }

         if (sd->vert[spc]) {
            mxval = 1.0 * my / hh;                                               //  mouse position in curve space
            myval = 1.0 * mx / ww;
         }
         else {
            mxval = 1.0 * mx / ww;
            myval = 1.0 * (hh - my) / hh;
         }

         for (ap = 0; ap < sd->nap[spc]; ap++)                                   //  find anchor point with next higher x
            if (mxval <= sd->apx[spc][ap]) break;                                //    (ap may come out 0 or nap)

         if (ap < sd->nap[spc] && sd->apx[spc][ap] - mxval < minx)               //  disallow < minx from next
               return 0;                                                         //    or prior anchor point
         if (ap > 0 && mxval - sd->apx[spc][ap-1] < minx) return 0;

         for (kk = sd->nap[spc]; kk > ap; kk--) {                                //  make hole for new point
            sd->apx[spc][kk] = sd->apx[spc][kk-1];
            sd->apy[spc][kk] = sd->apy[spc][kk-1];
         }

         sd->nap[spc]++;                                                         //  up point count
         apset = 1;                                                              //  mxval/myval = new node position    15.10
      }
   }

   if (evtype == GDK_MOTION_NOTIFY) Fdrag = 1;                                   //  remember drag is underway

   if (apset)                                                                    //  15.10
   {
      sd->apx[spc][ap] = mxval;                                                  //  new or moved anchor point
      sd->apy[spc][ap] = myval;                                                  //    at mouse position

      splcurve_generate(sd,spc);                                                 //  regenerate data for modified curve
      if (sd->drawarea) gtk_widget_queue_draw(sd->drawarea);                     //  redraw graph
      if (sd->spcfunc) sd->spcfunc(spc);                                         //  call user function
   }

   return 0;
}


//  add a new anchor point to a curve
//  spc:     curve number
//  px, py:  node coordinates in the range 0-1

int splcurve_addnode(spldat *sd, int spc, float px, float py)                    //  15.07
{
   int      ap, kk;
   float    minx = 0.01 * splcurve_minx;                                         //  % to absolute distance             15.10

   for (ap = 0; ap < sd->nap[spc]; ap++)                                         //  find anchor point with next higher x
      if (px <= sd->apx[spc][ap]) break;                                         //    (ap may come out 0 or nap)

   if (ap < sd->nap[spc] && sd->apx[spc][ap] - px < minx)                        //  disallow < minx from next
         return 0;                                                               //    or prior anchor point
   if (ap > 0 && px - sd->apx[spc][ap-1] < minx) return 0;

   for (kk = sd->nap[spc]; kk > ap; kk--) {                                      //  make hole for new point
      sd->apx[spc][kk] = sd->apx[spc][kk-1];
      sd->apy[spc][kk] = sd->apy[spc][kk-1];
   }

   sd->apx[spc][ap] = px;                                                        //  add node coordinates
   sd->apy[spc][ap] = py;

   sd->nap[spc]++;                                                               //  up point count
   return 1;
}


//  for expose event or when a curve is changed
//  draw all curves based on current anchor points

int splcurve_draw(GtkWidget *drawarea, cairo_t *cr, spldat *sd)
{
   int         ww, hh, spc, ap;
   float       xval, yval, px, py, qx, qy;

   zthreadcrash();

   ww = gtk_widget_get_allocated_width(sd->drawarea);                            //  drawing area size
   hh = gtk_widget_get_allocated_height(sd->drawarea);
   if (ww < 50 || hh < 50) return 0;

   cairo_set_line_width(cr,1);
   cairo_set_source_rgb(cr,0.7,0.7,0.7);

   for (int ii = 0; ii < sd->Nscale; ii++)                                       //  draw y-scale lines if any
   {
      px = ww * sd->xscale[0][ii];
      py = hh - hh * sd->yscale[0][ii];
      qx = ww * sd->xscale[1][ii];
      qy = hh - hh * sd->yscale[1][ii];
      cairo_move_to(cr,px,py);
      cairo_line_to(cr,qx,qy);
   }
   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,0,0);

   for (spc = 0; spc < sd->Nspc; spc++)                                          //  loop all curves
   {
      if (! sd->fact[spc]) continue;                                             //  not active                         15.10

      if (sd->vert[spc])                                                         //  vert. curve
      {
         for (py = 0; py < hh; py++)                                             //  generate all points for curve
         {
            xval = 1.0 * py / hh;
            yval = splcurve_yval(sd,spc,xval);
            px = ww * yval;
            if (py == 0) cairo_move_to(cr,px,py);
            cairo_line_to(cr,px,py);
         }
         cairo_stroke(cr);

         for (ap = 0; ap < sd->nap[spc]; ap++)                                   //  draw boxes at anchor points
         {
            xval = sd->apx[spc][ap];
            yval = sd->apy[spc][ap];
            px = ww * yval;
            py = hh * xval;
            cairo_rectangle(cr,px-2,py-2,4,4);
         }
         cairo_fill(cr);
      }
      else                                                                       //  horz. curve
      {
         for (px = 0; px < ww; px++)                                             //  generate all points for curve
         {
            xval = 1.0 * px / ww;
            yval = splcurve_yval(sd,spc,xval);
            py = hh - hh * yval;
            if (px == 0) cairo_move_to(cr,px,py);
            cairo_line_to(cr,px,py);
         }
         cairo_stroke(cr);

         for (ap = 0; ap < sd->nap[spc]; ap++)                                   //  draw boxes at anchor points
         {
            xval = sd->apx[spc][ap];
            yval = sd->apy[spc][ap];
            px = ww * xval;
            py = hh - hh * yval;
            cairo_rectangle(cr,px-2,py-2,4,4);
         }
         cairo_fill(cr);
      }
   }

   return 0;
}


//  generate all curve data points when anchor points are modified

int splcurve_generate(spldat *sd, int spc)
{
   int      kk, kklo, kkhi;
   float    xval, yvalx;

   spline1(sd->nap[spc],sd->apx[spc],sd->apy[spc]);                              //  compute curve fitting anchor points

   kklo = 1000 * sd->apx[spc][0] - 30;                                           //  xval range = anchor point range
   if (kklo < 0) kklo = 0;                                                       //    + 0.03 extra below/above
   kkhi = 1000 * sd->apx[spc][sd->nap[spc]-1] + 30;
   if (kkhi > 1000) kkhi = 1000;

   for (kk = 0; kk < 1000; kk++)                                                 //  generate all points for curve
   {
      xval = 0.001 * kk;                                                         //  remove anchor point limits
      yvalx = spline2(xval);
      if (yvalx < 0) yvalx = 0;                                                  //  yval < 0 not allowed, > 1 OK
      sd->yval[spc][kk] = yvalx;
   }

   return 0;
}


//  Retrieve curve data using interpolation of saved table of values

float splcurve_yval(spldat *sd, int spc, float xval)
{
   int      ii;
   float    x1, x2, y1, y2, y3;

   if (xval <= 0) return sd->yval[spc][0];
   if (xval >= 0.999) return sd->yval[spc][999];

   x2 = 1000.0 * xval;
   ii = x2;
   x1 = ii;
   y1 = sd->yval[spc][ii];
   y2 = sd->yval[spc][ii+1];
   y3 = y1 + (y2 - y1) * (x2 - x1);
   return y3;
}


//  load curve data from a file
//  returns 0 if succcess, sd is initialized from file data
//  returns 1 if fail (invalid file data), sd not modified

int splcurve_load(spldat *sd, FILE *fid)                                         //  15.10
{
   char        *pfile, *pp, buff[300];
   int         nn, ii, jj, err, myfid = 0;
   int         Nspc, fact[10], vert[10], nap[10];
   float       apx[10][50], apy[10][50];

   zthreadcrash();

   if (! fid)                                                                    //  request file from user
   {
      pfile = zgetfile(ZTX("load curve from a file"),MWIN,"file",saved_curves_dirk);
      if (! pfile) return 1;
      fid = fopen(pfile,"r");
      zfree(pfile);
      if (! fid) goto fail;
      myfid = 1;
   }

   pp = fgets_trim(buff,300,fid,1);
   if (! pp) goto fail;
   nn = sscanf(pp,"%d",&Nspc);                                                   //  no. of curves
   if (nn != 1) goto fail;
   if (Nspc < 1 || Nspc > 10) goto fail;
   if (Nspc != sd->Nspc) goto fail;

   for (ii = 0; ii < Nspc; ii++)                                                 //  loop each curve
   {
      pp = fgets_trim(buff,300,fid,1);
      if (! pp) goto fail;
      nn = sscanf(pp,"%d %d %d",&fact[ii],&vert[ii],&nap[ii]);                   //  active flag, vert flag, anchors
      if (nn != 3) goto fail;
      if (fact[ii] < 0 || fact[ii] > 1) goto fail;
      if (vert[ii] < 0 || vert[ii] > 1) goto fail;
      if (nap[ii] < 2 || nap[ii] > 50) goto fail;

      pp = fgets_trim(buff,300,fid,1);                                           //  anchor points: nnn/nnn nnn/nnn ...

      for (jj = 0; jj < nap[ii]; jj++)                                           //  anchor point values
      {
         pp = (char *) strField(buff,"/ ",2*jj+1);
         if (! pp) goto fail;
         err = convSF(pp,apx[ii][jj],0,1);
         if (err) goto fail;
         pp = (char *) strField(buff,"/ ",2*jj+2);
         err = convSF(pp,apy[ii][jj],0,1);
         if (err) goto fail;
      }
   }

   if (myfid) fclose(fid);

   sd->Nspc = Nspc;                                                              //  copy curve data to caller's arg

   for (ii = 0; ii < Nspc; ii++)
   {
      sd->fact[ii] = fact[ii];
      sd->vert[ii] = vert[ii];
      sd->nap[ii] = nap[ii];

      for (jj = 0; jj < nap[ii]; jj++)
      {
         sd->apx[ii][jj] = apx[ii][jj];
         sd->apy[ii][jj] = apy[ii][jj];
      }
   }

   for (ii = 0; ii < Nspc; ii++)                                                 //  generate curve data from anchor points
      splcurve_generate(sd,ii);

   if (sd->drawarea)                                                             //  redraw all curves
      gtk_widget_queue_draw(sd->drawarea);

   return 0;                                                                     //  success

fail:
   if (fid && myfid) fclose(fid);
   zmessageACK(Mwin,ZTX("curve file is invalid"));
   return 1;
}


//  save curve data to a file

int splcurve_save(spldat *sd, FILE *fid)
{
   char        *pfile, *pp;
   int         ii, jj, myfid = 0;
   
   if (! fid)
   {
      pp = zgetfile(ZTX("save curve to a file"),MWIN,"save",saved_curves_dirk);
      if (! pp) return 1;
      pfile = zstrdup(pp,8);
      zfree(pp);
      pp = strrchr(pfile,'/');                                                   //  force .curve extension
      if (pp) pp = strrchr(pp,'.');
      if (pp) strcpy(pp,".curve");
      else strcat(pfile,".curve");

      fid = fopen(pfile,"w");
      zfree(pfile);
      if (! fid) return 1;
      myfid = 1;
   }

   fprintf(fid,"%d \n",sd->Nspc);                                                //  no. of curves

   for (ii = 0; ii < sd->Nspc; ii++)                                             //  loop each curve
   {
      fprintf(fid,"%d %d %d \n",sd->fact[ii],sd->vert[ii],sd->nap[ii]);          //  activ flag, vert flag, anchors     15.10
      for (jj = 0; jj < sd->nap[ii]; jj++)                                       //  anchor point values
         fprintf(fid,"%.4f/%.4f  ",sd->apx[ii][jj],sd->apy[ii][jj]);
      fprintf(fid,"\n");
   }

   if (myfid) fclose(fid);
   return 0;
}


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

   edit transaction management

      edit_setup()                  get E0 if none, E0 > E1 > E3
      edit_cancel()                 free (E1 E3 ER)
      edit_done()                   E3 > E0, free (E1 ER) add to undo stack
      edit_undo()                   E3 > ER, E1 > E3
      edit_redo()                   ER > E3
      edit_reset()                  free ER, E1 > E3
      edit_fullsize()               free (E1 E3) E0 > E1 > E3

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

  Setup for a new edit transaction
  Create E1 (edit input) and E3 (edit output) pixmaps from
  previous edit (E0) or image file (new E0).

  FprevReq   0     edit full-size image
             1     edit preview image unless select area exists

  Farea      0     select_area is invalid and will be deleted (e.g. rotate)
             1     select_area not used but remains valid (e.g. red-eye)
             2     select_area can be used and remains valid (e.g. gamma)

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

int edit_setup(editfunc &EF)
{
   int      yn, rww, rhh;
   int      Fpreview;

   if (! curr_file) return 0;                                                    //  no image file
   if (FGWM != 'F') m_viewmode(0,"F");                                           //  insure file view mode              16.06

   if (checkpend("busy block")) return 0;                                        //  blocking function                  15.11

   if (CEF && CEF->zd)                                                           //  if pending edit, complete it
      zdialog_send_event(CEF->zd,"done");
   if (checkpend("edit")) return 0;                                              //  failed (HDR etc.)

   if (URS_pos > maxedits-2) {                                                   //  undo/redo stack capacity reached
      zmessageACK(Mwin,ZTX("Too many edits, please save image"));
      return 0;
   }
   
   if (Fscriptbuild && ! EF.Fscript) {                                           //  this function not scriptable       15.10
      zmessageACK(Mwin,ZTX("this function cannot be scripted"));
      return 0;
   }
   
   free_geomap();                                                                //  free map memory for edit usage

   sa_validate();                                                                //  delete area if not valid

   if (EF.Farea == 0 && sa_stat) {                                               //  select area will be lost, warn user
      yn = zmessageYN(Mwin,ZTX("Select area cannot be kept.\n"
                               "Continue?"));
      if (! yn) return 0;
      sa_unselect();                                                             //  unselect area
      if (zdsela) zdialog_free(zdsela);
   }

   if (EF.Farea == 2 && sa_stat && sa_stat != 3) {                               //  select area exists and can be used,
      yn = zmessageYN(Mwin,ZTX("Select area not active.\n"                       //    but not active, ask user
                               "Continue?"));
      if (! yn) return 0;
   }

   if (! E0pxm) {                                                                //  first edit for this file
      E0pxm = PXM_load(curr_file,1);                                             //  get E0 image (poss. 16-bit color)
      if (! E0pxm) return 0;
      curr_file_bpc = f_load_bpc;
   }
   
   paintlock(1);                                                                 //  block window updates               16.02

   if (URS_pos == 0) save_undo();                                                //  initial image >> undo/redo stack

   Fpreview = 0;                                                                 //  assume no preview

   if (EF.FprevReq && ! Fzoom)                                                   //  preview requested by edit func.
      Fpreview = 1;

   if (EF.Farea == 2 && sa_stat == 3)                                            //  not if select area active
      Fpreview = 0;

   if (E0pxm->ww * E0pxm->hh < 2000000)                                          //  if image is small, don't use preview
      Fpreview = 0;

   if (E0pxm->ww < 1.4 * Dww && E0pxm->hh < 1.4 * Dhh)                           //  if image slightly larger than window,
      Fpreview = 0;                                                              //       don't use preview

   if (Fpreview) {
      if (Fpxb->ww * Dhh > Fpxb->hh * Dww) {                                     //  use preview image 1.4 * window size
         rww = 1.4 * Dww;
         if (rww < 1200) rww = 1200;                                             //  at least 1200 on one side
         rhh = 1.0 * rww * Fpxb->hh / Fpxb->ww + 0.5;
      }
      else {
         rhh = 1.4 * Dhh;
         if (rhh < 1200) rhh = 1200;
         rww = 1.0 * rhh * Fpxb->ww / Fpxb->hh + 0.5;
      }
      if (rww > Fpxb->ww) Fpreview = 0;
   }

   if (Fpreview) {
      E1pxm = PXM_rescale(E0pxm,rww,rhh);                                        //  scale image to preview size
      sa_show(0);                                                                //  hide select area if present
   }
   else E1pxm = PXM_copy(E0pxm);                                                 //  else use full size image

   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3

   CEF = &EF;                                                                    //  set current edit function
   CEF->Fmods = 0;                                                               //  image not modified yet
   CEF->Fpreview = Fpreview;

   CEF->thread_command = CEF->thread_status = 0;                                 //  no thread running
   CEF->thread_pend = CEF->thread_done = CEF->thread_hiwater = 0;                //  no work pending or done
   if (CEF->threadfunc) start_thread(CEF->threadfunc,0);                         //  start thread func if any

   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return 1;
}


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

//  print error message if CEF invalid

int CEF_invalid()
{
   if (CEF) return 0;
   printz("CEF invalid \n");                                                     //  15.05
   return 1;
}


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

//  process edit cancel
//  keep: retain zdialog, mousefunc, curves

void edit_cancel(int keep)                                                       //  15.04
{
   if (CEF_invalid()) return;
   wrapup_thread(9);                                                             //  tell thread to quit, wait          15.05
   if (CEF_invalid()) return;
   paintlock(1);                                                                 //  block window updates               16.02

   PXM_free(E1pxm);                                                              //  free E1, E3, ER
   PXM_free(E3pxm);
   PXM_free(ERpxm);
   PXM_free(E8pxm);
   PXM_free(E9pxm);

   if (! keep)
   {
      if (CEF->zd == zd_thread) zd_thread = 0;                                   //  thread -> zdialog event
      if (CEF->curves) zfree(CEF->curves);                                       //  free curves data
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog             sequence   16.01
   }

   CEF = 0;                                                                      //  no current edit func
   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

//  process edit done
//  keep: retain zdialog, mousefunc, curves

void edit_done(int keep)                                                         //  15.04
{
   if (CEF_invalid()) return;
   wrapup_thread(8);                                                             //  tell thread to finish, wait        15.05
   if (CEF_invalid()) return;
   paintlock(1);                                                                 //  block window updates               16.02

   if (CEF->Fmods) {                                                             //  image was modified
      PXM_free(E0pxm);
      E0pxm = E3pxm;                                                             //  E3 updated image >> E0
      E3pxm = 0;
      PXM_free(E1pxm);                                                           //  free E1, ER
      PXM_free(ERpxm);
      PXM_free(E8pxm);
      PXM_free(E9pxm);
      URS_pos++;                                                                 //  next undo/redo stack position
      URS_max = URS_pos;                                                         //  image modified - higher mods now obsolete
      save_undo();                                                               //  save undo state (for following undo)
      if (Fscriptbuild) addscript(CEF);                                          //  add to script file                 15.10
   }

   else {                                                                        //  not modified
      PXM_free(E1pxm);                                                           //  free E1, E3, ER
      PXM_free(E3pxm);
      PXM_free(ERpxm);
      PXM_free(E8pxm);
      PXM_free(E9pxm);
   }

   if (! keep)
   {
      if (CEF->zd == zd_thread) zd_thread = 0;                                   //  thread -> zdialog event
      if (CEF->curves) zfree(CEF->curves);                                       //  free curves data
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog             sequence   16.02
   }

   CEF = 0;                                                                      //  no current edit func
   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

//  Convert from preview mode (window-size pixmaps) to full-size pixmaps.
//  Called by the edit function prior to edit_done().

void edit_fullsize()                                                             //  15.04
{
   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! CEF->Fpreview) return;                                                  //  FprevReq ignored if small image
   paintlock(1);                                                                 //  block window updates               16.02
   PXM_free(E1pxm);                                                              //  free preview pixmaps
   PXM_free(E3pxm);
   E1pxm = PXM_copy(E0pxm);                                                      //  E0 >> E1, full size image
   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3
   PXB_free(Cstate->fpxb);
   Cstate->fpxb = PXM_PXB_copy(E3pxm);                                           //  update Fpxb from image E3
   Fzoom = 0;
   paintlock(0);                                                                 //  unblock window updates             16.02
   CEF->Fpreview = 0;
   return;
}


//  edit undo, redo, reset functions
//  these apply within an active edit function

void edit_undo()
{
   F1_help_topic = "undo_redo";

   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! CEF->Fmods) return;                                                     //  not modified
   paintlock(1);                                                                 //  block window updates               16.02

   PXM_free(ERpxm);                                                              //  E3 >> redo copy
   ERpxm = E3pxm;
   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3
   CEF->Fmods = 0;                                                               //  reset image modified status
   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


void edit_redo()
{
   F1_help_topic = "undo_redo";

   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! ERpxm) return;                                                          //  no prior undo
   paintlock(1);                                                                 //  block window updates               16.02

   PXM_free(E3pxm);                                                              //  redo copy >> E3
   E3pxm = ERpxm;
   ERpxm = 0;
   CEF->Fmods = 1;                                                               //  image modified
   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


void edit_reset()                                                                //  reset E3 to E1 status
{
   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! CEF->Fmods) return;                                                     //  not modified
   paintlock(1);                                                                 //  block window updates               16.02

   PXM_free(ERpxm);                                                              //  delete redo copy
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3
   CEF->Fmods = 0;                                                               //  reset image modified status
   paintlock(0);                                                                 //  unblock window updates             16.02
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

//  Load/save all edit zdialog widgets data from/to a file.
//  dirname for edit data files: /home/<user>/.fotoxx/funcname 
//    where zdialog data is saved for the respective function.
//  return 0 = OK, +N = error

int edit_load_widgets(editfunc *EF, FILE *fid)                                   //  15.10
{
   using namespace zfuncs;

   cchar    *mess = ZTX("Load settings from file");
   zdialog  *zd;
   spldat   *sd;
   int      myfid = 0;
   char     *filename, dirname[200], buff[1000];
   char     *wname, *wdata, wdata2[1000];
   char     *pp, *pp1, *pp2;
   int      ii, kk, err, cc1, cc2;

   zd = EF->zd;
   sd = EF->curves;

   if (! fid)                                                                    //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zuserdir(),EF->funcname);                 //  directory for data files
      filename = zgetfile(mess,MWIN,"file",dirname,0);                           //  open data file
      if (! filename) return 1;                                                  //  user cancel
      fid = fopen(filename,"r");
      zfree(filename);
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
         return 1;
      }
      myfid = 1;
   }

   for (ii = 0; ii < zdmaxwidgets; ii++)                                         //  read widget data recs
   {
      pp = fgets_trim(buff,1000,fid,1);
      if (! pp) break;
      if (strmatch(pp,"curves")) {
         if (! sd) goto baddata;
         err = splcurve_load(sd,fid);                                            //  load curves data
         if (err) goto baddata;
         continue;
      }
      if (strmatch(pp,"end")) break;
      pp1 = pp;
      pp2 = strstr(pp1," ==");
      if (! pp2) continue;                                                       //  widget has no data
      cc1 = pp2 - pp1;
      if (cc1 > 100) continue;
      pp1[cc1] = 0;
      wname = pp1;                                                               //  widget name
      pp2 += 3;
      if (*pp2 == ' ') pp2++;
      wdata = pp2;                                                               //  widget data
      cc2 = strlen(wdata);
      if (cc2 < 1) wdata = (char *) "";
      if (cc2 > 300) continue;
      repl_1str(wdata,wdata2,"\\n","\n");                                        //  replace "\n" with newline chars.
      kk = zdialog_put_data(zd,wname,wdata2);
      if (! kk) goto baddata;
   }

   if (myfid) fclose(fid);
   return 0;

baddata:
   zmessageACK(Mwin,ZTX("file data does not fit dialog"));
   if (myfid) fclose(fid);
   return 1;
}


int edit_save_widgets(editfunc *EF, FILE *fid)                                   //  15.10
{
   using namespace zfuncs;

   cchar    *mess = ZTX("Save settings to a file");
   zdialog  *zd;
   spldat   *sd;
   int      myfid = 0;
   char     *filename, dirname[200];
   char     *wtype, *wname, *wdata, wdata2[1000];
   int      ii, cc;

   cchar    *editwidgets = "entry edit text togbutt check combo comboE "         //  widget types to save
                           "radio spin hscale vscale colorbutt";
   zd = EF->zd;
   sd = EF->curves;

   if (! fid)                                                                    //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zuserdir(),EF->funcname);                 //  directory for data files
      filename = zgetfile(mess,MWIN,"save",dirname,0);                           //  open data file
      if (! filename) return 1;                                                  //  user cancel
      fid = fopen(filename,"w");
      zfree(filename);
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
         return 1;
      }
      myfid = 1;
   }
   
   for (ii = 0; ii < zdmaxwidgets; ii++)
   {
      wtype = (char *) zd->widget[ii].type;
      if (! wtype) break;
      if (! strstr(editwidgets,wtype)) continue;
      wname = (char *) zd->widget[ii].name;                                      //  write widget data recs:
      wdata = zd->widget[ii].data;                                               //    widgetname == widgetdata
      if (! wdata) continue;
      cc = strlen(wdata);
      if (cc > 900) continue;
      repl_1str(wdata,wdata2,"\n","\\n");
      fprintf(fid,"%s == %s \n",wname,wdata);
   }
   
   if (sd) {
      fprintf(fid,"curves\n");
      splcurve_save(sd,fid);
   }
   
   fprintf(fid,"end\n");

   if (myfid) fclose(fid);
   return 0;
}


//  functions to support [prev] buttons in edit dialogs
//  load or save last-used widgets

int edit_load_prev_widgets(editfunc *EF)                                         //  15.10
{
   using namespace zfuncs;

   char     filename[200];
   FILE     *fid;
   int      err;

   snprintf(filename,200,"%s/%s/%s",get_zuserdir(),EF->funcname,"last-used");
   fid = fopen(filename,"r");
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
      return 1;
   }

   err = edit_load_widgets(EF,fid);
   fclose(fid);
   return err;
}


int edit_save_last_widgets(editfunc *EF)                                         //  15.10
{
   using namespace zfuncs;

   char     filename[200];
   FILE     *fid;
   int      err;

   snprintf(filename,200,"%s/%s/%s",get_zuserdir(),EF->funcname,"last-used");
   fid = fopen(filename,"w");
   if (! fid) {
      zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
      return 1;
   }

   err = edit_save_widgets(EF,fid);
   fclose(fid);
   return err;
}


/********************************************************************************
      undo / redo menu buttons
*********************************************************************************/

//  [undo/redo] menu function
//  Call m_undo() / m_redo() if left / right mouse click on menu.
//  If A key is pressed, undo or redo all edits.
//  If middle mouse button is clicked, pop-up a list of all edit steps
//  and choose a step to go back to.

void m_undo_redo(GtkWidget *, cchar *) 
{
   void undo_redo_choice(GtkWidget *, cchar *menu);

   GtkWidget   *popmenu;
   int         button = zfuncs::vmenuclickbutton;
   char        menuitem[40], flag;
   
   F1_help_topic = "undo_redo";
   
   if (button == 1) {
      if (KBkey == GDK_KEY_a) undo_all();                                        //  undo all steps
      else m_undo(0,0);                                                          //  undo one step
   }
   
   if (button == 2)                                                              //  go back to selected edit step      15.07
   {
      if (URS_max == 0) return;
      popmenu = create_popmenu();
      for (int ii = 0; ii < 30; ii++) {                                          //  insert all edit steps
         if (ii > URS_max) break;
         if (ii == URS_pos) flag = '*';                                          //  flag step matching current status
         else flag = ' ';
         snprintf(menuitem,40,"%d %s %c",ii,URS_funcs[ii],flag);
         add_popmenu_item(popmenu,menuitem,undo_redo_choice,(char *) &Nval[ii],0);
      }
      popup_menu(Mwin,popmenu);
   }

   if (button == 3) {
      if (KBkey == GDK_KEY_a) redo_all();                                        //  redo all steps
      else m_redo(0,0);                                                          //  redo one step
   }

   return;
}


//  popup menu response function

void undo_redo_choice(GtkWidget *, cchar *menu)                                  //  15.07
{
   int nn = *((int *) menu);
   if (nn < 0 || nn > URS_max) return;
   URS_pos = nn;
   load_undo();
   return;
}


//  [undo] menu function - reinstate previous edit in undo/redo stack

void m_undo(GtkWidget *, cchar *)
{
   if (CEF) {                                                                    //  undo active edit
      edit_undo();
      return;
   }
   if (URS_pos == 0) return;                                                     //  undo past edit
   URS_pos--;
   load_undo();
   return;
}


//  [redo] menu function - reinstate next edit in undo/redo stack

void m_redo(GtkWidget *, cchar *)
{
   if (CEF) {                                                                    //  redo active edit
      edit_redo();
      return;
   }
   if (URS_pos == URS_max) return;                                               //  redo past edit
   URS_pos++;
   load_undo();
   return;
}


//  undo all edits of the current image
//  (discard modifications)

void undo_all()
{
   if (CEF) return;                                                              //  not if edit active
   if (URS_pos == 0) return;
   URS_pos = 0;                                                                  //  original image
   load_undo();
   return;
}


//  redo all edits of the current image
//  (reinstate all modifications)

void redo_all()
{
   if (CEF) return;                                                              //  not if edit active
   if (URS_pos == URS_max) return;
   URS_pos = URS_max;;                                                           //  image with last edit applied
   load_undo();
   return;
}


//  Save E0 to undo/redo file stack
//  stack position = URS_pos

void save_undo()                                                                 //  overhauled for 4 GB files          16.03
{
   char     *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   uint     cc1, cc2;
   uint     ccmax = 256 * MEGA;

   ww = E0pxm->ww;
   hh = E0pxm->hh;
   nc = E0pxm->nc;

   pp = strstr(URS_filename,"undo_");                                            //  get undo/redo stack filename to use
   if (! pp) zappcrash("undo/redo stack corrupted");
   snprintf(pp+5,3,"%02d",URS_pos);

   fid = fopen(URS_filename,"w");
   if (! fid) goto writefail;

   snprintf(buff,24,"fotoxx %05d %05d %d",ww,hh,nc);                             //  write header
   nn = fwrite(buff,20,1,fid);
   if (nn != 1) goto writefail;

   cc1 = ww * hh * nc * sizeof(float);                                           //  write ww * hh RGB(A) pixels
   cc2 = 0;
   while (cc1) {
      if (cc1 <= ccmax) {
         pp = (char *) E0pxm->pixels;
         nn = fwrite(pp+cc2,cc1,1,fid);
         if (nn != 1) goto writefail;
         break;
      }
      else {
         pp = (char *) E0pxm->pixels;
         nn = fwrite(pp+cc2,ccmax,1,fid);
         if (nn != 1) goto writefail;
         cc1 -= ccmax;
         cc2 += ccmax;
      }
   }

   fclose(fid);

   if (URS_pos == 0) {                                                           //  stack posn. 0 = original image
      strcpy(URS_funcs[0],"original");                                           //  function name for original image
      URS_saved[0] = 1;                                                          //  original image already on disk
   }
   else {                                                                        //  stack position
      if (CEF_invalid()) return;                                                 //  must have an edit function
      strncpy0(URS_funcs[URS_pos],CEF->funcname,32);                             //  edit function name
      URS_saved[URS_pos] = 0;                                                    //  not yet saved to disk
   }

   return;

writefail:
   zmessageACK(Mwin,"undo/redo stack write failure: %d \n"
                    "(may be out of disk space)",errno);
   shell_quiet("rm -R %s",tempdir);
   exit(errno);
}


//  Load E0 from undo/redo file stack
//  stack position = URS_pos

void load_undo()                                                                 //  overhauled                         16.03
{
   char     *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   uint     cc1, cc2;
   uint     ccmax = 256 * MEGA;
   
   pp = strstr(URS_filename,"undo_");
   if (! pp) goto readfail;
   snprintf(pp+5,3,"%02d",URS_pos);

   fid = fopen(URS_filename,"r");
   if (! fid) goto readfail;

   nn = fread(buff,20,1,fid);
   if (nn != 1) goto readfail;
   nn = sscanf(buff,"fotoxx %d %d %d",&ww,&hh,&nc);
   if (nn != 3) goto readfail;

   PXM_free(E0pxm);
   E0pxm = PXM_make(ww,hh,nc); 

   cc1 = ww * hh * nc * sizeof(float);
   cc2 = 0;
   while (cc1) {
      if (cc1 <= ccmax) {
         pp = (char *) E0pxm->pixels;
         nn = fread(pp+cc2,cc1,1,fid);
         if (nn != 1) goto readfail;
         break;
      }
      else {
         pp = (char *) E0pxm->pixels;
         nn = fread(pp+cc2,ccmax,1,fid);
         if (nn != 1) goto readfail;
         cc1 -= ccmax;
         cc2 += ccmax;
      }
   }

   fclose(fid);

   sa_validate();                                                                //  delete area if invalid
   if (Cdrawin) Fpaintnow();                                                     //  update image synchronous
   return;

readfail:
   zmessageACK(Mwin,"undo/redo stack read failure: %d \n",errno);
   shell_quiet("rm -R %s",tempdir);
   exit(errno);
}


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

//  check for busy or pending conditions and message the user
//  returns  1  if busy or pending condition
//           0  if nothing is pending or user decides to discard mods
//  conditions:
//       edit    edit function is active (CEF not null)
//       mods    current file has unsaved mods (image or metadata edits)
//       busy    function is still running                                       //  15.04
//       block   edit function mutex flag                                        //  15.04
//       all     check all the above
//       quiet   suppress user message

int checkpend(cchar *list)
{
   int      edit, mods, busy, block, all, quiet, pend, choice;
   cchar    *modmess = ZTX("This action will discard changes to current image");
   cchar    *activemess = ZTX("prior function still active");
   cchar    *keep = ZTX("Keep");
   cchar    *discard = ZTX("Discard");
   
   edit = mods = busy = block = all = quiet = pend = 0;

   if (strstr(list,"edit")) edit = 1;
   if (strstr(list,"mods")) mods = 1;
   if (strstr(list,"busy")) busy = 1;
   if (strstr(list,"block")) block = 1;
   if (strstr(list,"all")) all = 1;
   if (strstr(list,"quiet")) quiet = 1;

   if (all) edit = mods = busy = block = 1;

   if ((edit && CEF) || (busy && (Ffuncbusy | Fthreadbusy)) || (block && Fblock)) {
      pend = 1;
      if (! quiet) zmessage_post(Mwin,2,activemess);                             //  16.03
   }

   if (! pend && mods) {
      if (CEF && CEF->Fmods && ! CEF->Fsaved) pend = 1;                          //  active edits unsaved
      if (URS_pos > 0 && URS_saved[URS_pos] == 0) pend = 1;                      //  completed edits unsaved
      if (Fmetamod) pend = 1;                                                    //  metadata edits unsaved             15.03
      if (pend && ! quiet) {
         choice = zdialog_choose(Mwin,modmess,keep,discard,null);                //  ask user
         if (choice == 2) {                                                      //  choice is discard
            if (CEF && CEF->zd)
               zdialog_send_event(CEF->zd,"cancel");
            if (URS_pos > 0) undo_all();                                         //  undo prior edits
            Fmetamod = 0;                                                        //  discard metadata edits
            pend = 0;
         }
         else m_viewmode(0,"F");                                                 //  keep - back to current image
      }
   }

   return pend;                                                                  //  1 if something pending
}


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

//  zdialog mouse capture and release

void takeMouse(mcbFunc func, GdkCursor *cursor)                                  //  capture mouse for dialog
{
   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   freeMouse();
   mouseCBfunc = func;
   Mcapture = 1;
   gdk_window_set_cursor(gdkwin,cursor);
   return;
}

void freeMouse()                                                                 //  free mouse for main window
{
   zthreadcrash();

   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Mcapture) return;
   mouseCBfunc = 0;
   Mcapture = 0;
   draw_mousearc(0,0,0,0,1);                                                     //  remove mouse circle
   gdk_window_set_cursor(gdkwin,0);                                              //  set normal cursor
   return;
}


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

   functions to manage working threads

   main level thread management
      start_thread(func,arg)           start thread running
      signal_thread()                  signal thread that work is pending
      wait_thread_idle()               wait for pending work complete
      wrapup_thread(command)           wait for exit or command thread exit

   thread function
      thread_idle_loop()               wait for pending work, exit if commanded
      thread_exit()                    exit thread unconditionally

   thread_status (thread ownership
      0     no thread is running
      1     thread is running and idle (no work)
      2     thread is working
      0     thread has exited

   thread_command (main program ownership)
      0     idle, initial status
      8     exit when pending work is done
      9     exit now, unconditionally

   thread_pend       work requested counter
   thread_done       work done counter
   thread_hiwater    high water mark

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

//  start thread that does the edit work

void start_thread(threadfunc func, void *arg)
{
   CEF->thread_status = 1;                                                       //  thread is running
   CEF->thread_command = CEF->thread_pend = CEF->thread_done
                       = CEF->thread_hiwater = 0;                                //  nothing pending
   start_detached_thread(func,arg);
   return;
}


//  signal thread that new work is pending

void signal_thread()
{
   if (CEF_invalid()) return;
   if (CEF->thread_status > 0) CEF->thread_pend++;
   return;
}


//  wait for edit thread to complete pending work and become idle

void wait_thread_idle()
{
   if (CEF_invalid()) return;
   if (! CEF->thread_status) return;
   if (CEF->thread_pend == CEF->thread_done) return;
   while (CEF->thread_status && CEF->thread_pend > CEF->thread_done) {
      zmainloop();                                                               //  15.04
      zsleep(0.01);
      if (CEF_invalid()) return;
   }
   return;
}


//  wait for thread exit or command thread exit
//  command = 0    wait for normal completion
//            8    finish pending work and exit
//            9    quit, exit now

void wrapup_thread(int command)
{
   if (CEF_invalid()) return;
   if (! CEF->thread_status) return;
   CEF->thread_command = command;                                                //  tell thread to quit or finish
   while (CEF->thread_status > 0) {                                              //  wait for thread to finish
      zmainloop();                                                               //  15.04
      zsleep(0.01);
      if (CEF_invalid()) return;
   }
   return;
}


//  called only from edit threads
//  idle loop - wait for work request or exit command

void thread_idle_loop()
{
   if (CEF_invalid()) thread_exit();
   if (CEF->thread_status == 2) zadd_locked(Fthreadbusy,-1);
   CEF->thread_status = 1;                                                       //  status = idle
   CEF->thread_done = CEF->thread_hiwater;                                       //  work done = high-water mark

   while (true)
   {
      if (CEF->thread_command == 9) thread_exit();                               //  quit now command
      if (CEF->thread_command == 8)                                              //  finish work and exit
         if (CEF->thread_pend <= CEF->thread_done) thread_exit();
      if (CEF->thread_pend > CEF->thread_done) break;                            //  wait for work request
      zsleep(0.01);
   }

   CEF->thread_hiwater = CEF->thread_pend;                                       //  set high-water mark
   CEF->thread_status = 2;                                                       //  thread is working
   zadd_locked(Fthreadbusy,+1);
   return;                                                                       //  perform edit
}


//  exit thread unconditionally, called only from edit threads

void thread_exit()
{
   wait_wthreads();                                                              //  wait for worker threads if any
   if (CEF_invalid()) pthread_exit(0);
   if (CEF->thread_status == 2) zadd_locked(Fthreadbusy,-1);
   CEF->thread_pend = CEF->thread_done = CEF->thread_hiwater = 0;
   CEF->thread_status = 0;
   pthread_exit(0);                                                              //  "return" cannot be used here
}


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

//  edit support functions for worker threads (per processor core)

void start_wthread(threadfunc func, void *arg)                                   //  start thread and increment busy count
{                                                                                //  may be called from a thread
   zadd_locked(Fthreadbusy,+1);
   zadd_locked(wthreads_busy,+1);
   start_detached_thread(func,arg);
   return;
}


void exit_wthread()                                                              //  decrement busy count and exit thread
{
   zadd_locked(Fthreadbusy,-1);
   zadd_locked(wthreads_busy,-1);
   pthread_exit(0);                                                              //  "return" cannot be used here
}


void wait_wthreads(int block)                                                    //  wait for all worker threads done
{                                                                                //  may be called from thread or non-thread
   if (CEF) {
      while (wthreads_busy) {
         zsleep(0.002);                                                          //  16.05
         if (block) continue;
         zmainloop();
      }
      if (CEF_invalid()) return;
   }
   else {
      while (wthreads_busy) {
         zsleep(0.002);                                                          //  16.05
         if (block) continue;
         zmainloop();
      }
   }
   return;
}


/********************************************************************************
      other support functions
*********************************************************************************/

//  table for loading and saving adjustable parameters between sessions

typedef struct {
   char     name[20];
   char     type[12];
   int      count;
   void     *location;
}  param;

#define Nparms 40
param paramTab[Nparms] = {
//     name                    type        count      location
{     "fotoxx release",       "char",        1,       &Prelease               },
{     "first time",           "int",         1,       &Ffirsttime             },
{     "window geometry",      "int",         4,       &mwgeom                 },
{     "thumbnail size",       "int",         1,       &navi::thumbsize        },
{     "menu style",           "char",        1,       &menu_style             },
{     "F-base-color",         "int",         3,       &Fbgcolor               },
{     "G-base-color",         "int",         3,       &Gbgcolor               },
{     "gallery sort",         "int",         1,       &gallerysort            },
{     "icon size",            "int",         1,       &iconsize               },
{     "dialog font",          "char",        1,       &dialog_font            },
{     "drag option",          "int",         1,       &Fdragopt               },
{     "zoom count",           "int",         1,       &zoomcount              },
{     "map_dotsize",          "int",         1,       &map_dotsize            },
{     "show hidden",          "int",         1,       &Fshowhidden            },
{     "last version",         "int",         1,       &Flastversion           },
{     "curve node dist %",    "float",       1,       &splcurve_minx          },
{     "startup display",      "char",        1,       &startdisplay           },
{     "start image file",     "char",        1,       &startfile              },
{     "start directory",      "char",        1,       &startdirk              },
{     "last curr file",       "char",        1,       &last_curr_file         },
{     "last galleryname",     "char",        1,       &last_galleryname       },
{     "last gallerytype",     "int",         1,       &last_gallerytype       },
{     "copy/move location",   "char",        1,       &copymove_loc           },
{     "grid base",            "int",         10,      &gridsettings[0]        },
{     "grid trim/rotate",     "int",         10,      &gridsettings[1]        },
{     "grid unbend",          "int",         10,      &gridsettings[2]        },
{     "grid warp curved",     "int",         10,      &gridsettings[3]        },
{     "grid warp linear",     "int",         10,      &gridsettings[4]        },
{     "grid warp affine",     "int",         10,      &gridsettings[5]        },
{     "RAW file types",       "char",        1,       &RAWfiletypes           },
{     "trim size",            "int",         2,       &trimsize               },
{     "trim buttons",         "char",        6,       &trimbuttons            },
{     "trim ratios",          "char",        6,       &trimratios             },
{     "edit resize",          "int",         2,       &editresize             },
{     "jpeg def quality",     "int",         1,       &jpeg_def_quality       },
{     "thumb file size",      "int",         1,       &thumbfilesize          },
{     "lens mm",              "float",       1,       &lens_mm                },
{     "cycledesktop_album",   "char",        1,       &cycledesktop_album     },
{     "cycledesktop_index",   "int",         1,       &cycledesktop_index     },
{     "printer color map",    "char",        1,       &colormapfile           }  };


//  save parameters to file /.../.fotoxx/parameters

void save_params()
{
   FILE        *fid;
   char        buff[1050], text[1000];                                           //  limit for character data cc
   char        *name, *type;
   int         count;
   void        *location;
   char        **charloc;
   int         *intloc;
   float       *floatloc;
   
   last_curr_file = last_galleryname = 0;                                        //  save curr_file and gallery poop    15.07
   last_gallerytype = NONE;                                                      //    for next session startup
   if (curr_file && *curr_file == '/') 
      last_curr_file = zstrdup(curr_file);
   if (navi::galleryname) {
      last_galleryname = zstrdup(navi::galleryname);
      last_gallerytype = navi::gallerytype;
   }

   snprintf(buff,199,"%s/parameters",get_zuserdir());                            //  open output file
   fid = fopen(buff,"w");
   if (! fid) return;

   for (int ii = 0; ii < Nparms; ii++)                                           //  write table of state data
   {
      name = paramTab[ii].name;
      type = paramTab[ii].type;
      count = paramTab[ii].count;
      location = paramTab[ii].location;
      charloc = (char **) location;
      intloc = (int *) location;
      floatloc = (float *) location;

      fprintf(fid,"%-20s  %-8s  %02d  ",name,type,count);                        //  write parm name, type, count

      for (int kk = 0; kk < count; kk++)                                         //  write "value" "value" ...
      {
         if (strmatch(type,"char")) {
            if (! *charloc) break;                                               //  missing, discontinue this parameter
            repl_1str(*charloc++,text,"\n","\\n");                               //  replace newlines with "\n"
            fprintf(fid,"  \"%s\"",text);
         }
         if (strmatch(type,"int"))
            fprintf(fid,"  \"%d\"",*intloc++);

         if (strmatch(type,"float"))
            fprintf(fid,"  \"%.2f\"",*floatloc++);
      }

      fprintf(fid,"\n");                                                         //  write EOR
   }

   fprintf(fid,"\n");
   fclose(fid);                                                                  //  close file

   return;
}


//  load parameters from file /.../.fotoxx/parameters

void load_params()
{
   FILE        *fid;
   int         ii, err, pcount;
   int         Idata;
   float       Fdata;
   char        buff[1000], text[1000], *pp;
   char        name[20], type[12], count[8], data[1000];
   void        *location;
   char        **charloc;
   int         *intloc;
   float       *floatloc;

   snprintf(buff,199,"%s/parameters",get_zuserdir());                            //  open parameters file
   fid = fopen(buff,"r");
   if (! fid) return;                                                            //  none, defaults are used

   while (true)                                                                  //  read parameters
   {
      pp = fgets_trim(buff,999,fid,1);
      if (! pp) break;
      if (*pp == '#') continue;                                                  //  comment
      if (strlen(pp) < 40) continue;                                             //  rubbish

      err = 0;

      strncpy0(name,pp,20);                                                      //  parm name
      strTrim2(name);

      strncpy0(type,pp+22,8);                                                    //  parm type
      strTrim2(type);

      strncpy0(count,pp+32,4);                                                   //  parm count
      strTrim2(count);
      err = convSI(count,pcount);

      strncpy0(data,pp+38,1000);                                                 //  parm value(s)
      strTrim2(data);

      for (ii = 0; ii < Nparms; ii++)                                            //  match file record to param table
      {
         if (! strmatch(name,paramTab[ii].name)) continue;                       //  parm name
         if (! strmatch(type,paramTab[ii].type)) continue;                       //  parm type
         if (paramTab[ii].count != pcount) continue;                             //  parm count
         break;
      }

      if (ii == Nparms) continue;                                                //  not found, ignore file param

      location = paramTab[ii].location;                                          //  get parameter memory location
      charloc = (char **) location;
      intloc = (int *) location;
      floatloc = (float *) location;

      for (ii = 1; ii <= pcount; ii++)                                           //  get parameter value(s)
      {
         pp = (char *) strField(data,' ',ii);
         if (! pp) break;

         if (strmatch(type,"char")) {
            repl_1str(pp,text,"\\n","\n");                                       //  replace "\n" with real newlines
            *charloc++ = zstrdup(text);
         }

         if (strmatch(type,"int")) {
            err = convSI(pp,Idata);
            if (err) continue;
            *intloc++ = Idata;
         }

         if (strmatch(type,"float")) {
            err = convSF(pp,Fdata);
            if (err) continue;
            *floatloc++ = Fdata;
         }
      }
   }

   fclose(fid);

   for (ii = 0; ii < Nparms; ii++)                                               //  set any null strings to ""
   {
      if (! strmatch(paramTab[ii].type,"char")) continue;
      charloc = (char **) paramTab[ii].location;
      pcount = paramTab[ii].count;
      for (int jj = 0; jj < pcount; jj++)
         if (! charloc[jj])
            charloc[jj] = zstrdup("",20);
   }

   zoomratio = pow( 2.0, 1.0 / zoomcount);                                       //  set zoom ratio from zoom count
   return;
}


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

//  free all resources associated with the current image file

void free_resources(int fkeepundo)
{
   paintlock(1);                                                                 //  block window updates               16.02

   if (! fkeepundo)
      shell_quiet("rm -f %s/undo_*",tempdir);                                    //  remove undo/redo files

   if (Fshutdown) return;                                                        //  stop here if shutdown mode

   URS_pos = URS_max = 0;                                                        //  reset undo/redo stack
   Fmetamod = 0;                                                                 //  no unsaved metadata changes        15.03
   sa_unselect();                                                                //  unselect select area

   draw_toplines(2);                                                             //  no toplines
   erase_topcircles();                                                           //  no topcircles
   Ntoptext = 0;                                                                 //  no toptext
   Fbusy_goal = 0;

   if (curr_file) {
      if (zdsela) zdialog_free(zdsela);                                          //  kill select area dialog if active
      freeMouse();                                                               //  free mouse
      zfree(curr_file);                                                          //  free image file
      curr_file = 0;
      *paneltext = 0;
   }

   gtk_window_set_title(MWIN,Frelease);                                          //  sparse title                       15.03

   PXB_free(Fpxb);
   PXM_free(E0pxm);
   PXM_free(E1pxm);
   PXM_free(E3pxm);
   PXM_free(ERpxm);
   PXM_free(E8pxm);
   PXM_free(E9pxm);

   paintlock(0);                                                                 //  unblock window updates             16.02
   return;
}


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

//  compare two floats for significant difference
//  signf: e.g. 0.01 for 1% threshold of significance
//  return:  0  difference not significant
//          +1  d1 > d2
//          -1  d1 < d2

int sigdiff(float d1, float d2, float signf)
{
   float diff = fabsf(d1-d2);
   if (diff == 0.0) return 0;
   diff = diff / (fabsf(d1) + fabsf(d2));
   if (diff < signf) return 0;
   if (d1 > d2) return 1;
   else return -1;
}



