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

   Fotoxx      edit photos and manage collections

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

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

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

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

   main                    main program - defaults, initializations, command line options

   first_startup           first fotoxx startup, user initial index decision
   release_startup         new release startup, merge config files with user changes

   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
   drop_event              response function for main window file drag-drop event

   paintlock               block window paint during E1/E3 image updates

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

   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
   Fpaint3_thread          Fpaint3, callable from threads
   Fpaint0                 same as Fpaint3 but uses E0 instead of E3
   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
   KBevent                 send KB key from dialog to main window
   KBpress                 KB key press 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 overlay 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_resize         resize drawing area if too small
   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
   func_load_widgets       load zdialog widgets and curves from a file
   func_save_widgets       save zdialog widgets and curves to a file
   func_load_prev_widgets  load last-used widgets (for [prev] buttons)
   func_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
   do_wthreads             start N working threads, wait for completion

   save_params             save parameters when fotoxx exits
   load_params             load parameters at fotoxx startup
   free_resources          free resources for the current image file

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

#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[])                                                 //  Fotoxx main program
{
   char     *homedir, *temp, *pp;
   char     *pid, *pidlist;
   int      Fclone=0, cloxx=0, cloyy=0, cloww=0, clohh=0;
   int      ii, jj, cc, err;
   char     cssfile[200];
   STATB    statb;
   int      Fexiftool = 0, Fdcraw = 0;                                           //  mandatory programs                 20.0
   FTYPE    ftype;
   FILE     *fid;
   char     filename[200], buff[200];
   double   freememory, cachememory;
   double   startsecs = get_seconds();                                           //  start time

   err = appimage_install("fotoxx");                                             //  if appimage, menu integration      19.1
   if (err == 4) {
      printz("failed to create ~/.local/share/applications/fotoxx.desktop \n"    //  19.1
             "desktop menu integration did not work \n");
      exit(0);
   }

   printz("%s \n",Frelease);                                                     //  print fotoxx version               20.0
   if (argc > 1 && strmatchV(argv[1],"-ver","-v",0))                             //  exit if nothing else wanted
      exit(0);

   if (argc > 1 && strmatch(argv[1],"-uninstall")) {                             //  uninstall appimage                 19.0
      appimage_unstall();                                                        //  (does not return)
      exit(0);
   }

   setpgid(0,0);                                                                 //  make a new process group           19.0
   
   setenv("GTK_THEME","default",0);                                              //  set theme if missing (KDE etc.)
   setenv("GDK_BACKEND","x11",0);                                                //  wayland                            20.0

   if (gtk_clutter_init(&argc,&argv) != CLUTTER_INIT_SUCCESS)                    //  intiz. clutter and GTK
      zexit("initz. clutter and GTK failure");

   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);

   homedir = 0;
   if (argc > 2 && strmatch(argv[1],"-home")) homedir = argv[2];                 //  relocate user folder

   zinitapp(Frelease,homedir);                                                   //  initz. app folders                 20.0

   //  modify GTK widgets to take less screen space

   snprintf(cssfile,200,"%s/widgets.css",get_zhomedir());
   GtkStyleProvider *provider = (GtkStyleProvider *) gtk_css_provider_new();
   gtk_style_context_add_provider_for_screen(zfuncs::screen,provider,999);
   gtk_css_provider_load_from_path(GTK_CSS_PROVIDER(provider),cssfile,0);

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

   strcpy(zfuncs::zappname,Frelease);                                            //  app name and version
   Ffirsttime = 1;                                                               //  first startup (params override)
   Findexlev = 2;                                                                //  direct exec: old index + search new
   FMindexlev = 1;                                                               //  file manager: old index only       20.0
   Pindexlev = -1;                                                               //  no -index command parameter
   xxrec_tab = 0;                                                                //  no image index yet
   Nxxrec = Findexvalid = 0;
   Prelease = zstrdup("unknown");                                                //  prev. release (params override)
   mwgeom[0] = mwgeom[1] = 100;                                                  //  default main window geometry
   mwgeom[2] = 1200; mwgeom[3] = 800;
   *paneltext = 0;                                                               //  no status bar text

   trimsizes[0] = zstrdup("1920x1080");                                          //  default trim size memory
   trimsizes[1] = zstrdup("1600x900");
   trimsizes[2] = zstrdup("1440x900");
   trimsizes[3] = zstrdup("1280x1024");
   trimsizes[4] = zstrdup("1366x768");
   trimsizes[5] = zstrdup("1280x800");
   trimsizes[6] = zstrdup("1024x768");
   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");
   editresize[0] = 1600;                                                         //  default initial resize size
   editresize[1] = 1200;
   currgrid = 0;                                                                 //  default initial grid settings      20.0
   for (ii = 0; ii < 6; ii++) {                                                  //  6 grids
      gridsettings[ii][GON] = 0;                                                 //  grid disabled
      gridsettings[ii][GX]  = gridsettings[ii][GY]  = 1;                         //  x/y-lines enabled
      gridsettings[ii][GXS] = gridsettings[ii][GYS] = 100;                       //  x/y-lines spacing
      gridsettings[ii][GXC] = gridsettings[ii][GYC] = 2;                         //  x/y-lines count
      gridsettings[ii][GXF] = gridsettings[ii][GYF] = 0;                         //  x/y-lines offset
   }
   menu_style = zstrdup("both");                                                 //  default menu style                 20.0
   iconsize = 32;                                                                //  default icon size
   FBrgb[0] = FBrgb[1] = FBrgb[2] = 50;                                          //  F view background color
   GBrgb[0] = GBrgb[1] = GBrgb[2] = 200;                                         //  G view background color
   MFrgb[0] = MFrgb[1] = MFrgb[2] = 250;                                         //  menu font color 
   MBrgb[0] = MBrgb[1] = MBrgb[2] = 80;                                          //  menu background color
   dialog_font = zstrdup("Sans 10");                                             //  default dialog font
   splcurve_minx = 5;                                                            //  default curve node separation %
   startdisplay = zstrdup("prevF");                                              //  start with previous image
   Fdragopt = 1;                                                                 //  image drag with mouse
   Fshiftright = 0;                                                              //  shift right for max. left margin
   Fmenublock = 1;                                                               //  menus blocked                      20.0
   zoomcount = 2;                                                                //  zooms to reach 2x image size
   zoomratio = sqrtf(2);                                                         //  corresp. zoom ratio
   Nkbsu = 0;                                                                    //  KB shortcut list is empty
   map_dotsize = 8;                                                              //  map dot size, mouse capture dist
   curr_file = 0;                                                                //  no curr. file
   navi::galleryname = 0;                                                        //  no curr. gallery
   navi::gallerytype = TNONE;                                  
   curr_album = 0;                                                               //  no current album 
   copymove_loc = 0;                                                             //  copy/move target folder
   RGB_chooser_file = 0;                                                         //  users RGB color chooser file
   thumbfolder = 0;                                                              //  no thumbnail folder
   navi::thumbsize = 256;                                                        //  gallery default thumbnail size
   commandmenu = 0;                                                              //  command line menu function
   commandparam = 0;                                                             //  command menu parameter             20.0
   commandalbum = 0;                                                             //  command line album gallery
   video_command = zstrdup("vlc");                                               //  default video play command         20.0
   initial_file = 0;                                                             //  start with image file or folder
   jpeg_def_quality = 90;                                                        //  default .jpeg save quality
   tiff_comp_method = 1;                                                         //  default TIFF compression method    20.0
   png_comp_level = 1;                                                           //  default PNG compression level      20.0
   Frawloader = 1;                                                               //  default RAW file loader, dcraw     20.0
   Fautobright = 1;                                                              //  RAW loader auto brighten, ON       20.0
   Fmatchthumb = 0;                                                              //  RAW loader match thumb color OFF   20.0
   ownertran = E2X("owner");                                                     //  for permissions dialogs            20.0
   grouptran = E2X("group");
   othertran = E2X("other");
   RWtran = E2X("read+write");
   ROtran = E2X("read only");
   NOtran = E2X("no access");
   lens_mm = 35;                                                                 //  pano lens parameter
   netmap_source = zstrdup("mapnik");                                            //  default net map source
   mapbox_access_key = zstrdup("undefined");                                     //  mapbox map source access key
   colormapfile = zstrdup("undefined");                                          //  printer calibration color map
   ss_KBkeys = zstrdup("BNPX");                                                  //  default slide show control keys
   Fcaptions = 0;                                                                //  no show captions/comments          20.0

   imagefiletypes = zstrdup(".jpg .jpeg .png .tif .tiff .bmp .ico .ppm .gif .svg .xpm .tga ",20);                   //  20.0
   RAWfiletypes = zstrdup(".arw .srf .sr2 .crw .cr2 .cr3 .dng .mdc .mrw .nef .nrw .raw .rw2 .srw ",20);             //  20.0
   VIDEOfiletypes = zstrdup(".mp4 .flv .mov .avi .wmv .mpeg .mpg .h264 .webm ",20); 

   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;
   memcpy(LINE_COLOR,RED,3*sizeof(int));                                         //  initial foreground drawing color

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

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

   snprintf(index_folder,199,"%s/image_index",get_zhomedir());                   //  image index folder
   snprintf(index_file,199,"%s/index20",index_folder);                           //  image index file       index20     20.0
   snprintf(tags_defined_file,199,"%s/tags_defined",get_zhomedir());             //  defined tags file
   snprintf(recentfiles_file,199,"%s/recent_files",get_zhomedir());              //  recent files file
   snprintf(saved_areas_folder,199,"%s/saved_areas",get_zhomedir());             //  saved areas folder
   snprintf(albums_folder,199,"%s/albums",get_zhomedir());                       //  albums folder
   snprintf(gallerymem_file,199,"%s/gallery_memory",get_zhomedir());             //  recent gallery memory
   snprintf(saved_curves_folder,199,"%s/saved_curves",get_zhomedir());           //  saved curves folder
   snprintf(drawtext_folder,199,"%s/draw_text",get_zhomedir());                  //  add_text folder
   snprintf(favorites_folder,199,"%s/favorites",get_zhomedir());                 //  favorites folder
   snprintf(mashup_folder,199,"%s/mashup",get_zhomedir());                       //  mashup projects folder
   snprintf(slideshow_folder,199,"%s/slideshows",get_zhomedir());                //  slide show folder
   snprintf(slideshow_trans_folder,199,"%s/slideshow_trans",get_zhomedir());     //  slide show transitions
   snprintf(pattern_folder,199,"%s/patterns",get_zhomedir());                    //  pattern files folder
   snprintf(retouch_folder,199,"%s/retouch",get_zhomedir());                     //  retouch settings folder
   snprintf(custom_kernel_folder,199,"%s/custom_kernel",get_zhomedir());         //  custom kernel files folder
   snprintf(printer_color_folder,199,"%s/printer_color",get_zhomedir());         //  printer calibration folder
   snprintf(scripts_folder,199,"%s/custom_scripts",get_zhomedir());              //  custom script files folder
   snprintf(search_images_folder,199,"%s/search_images",get_zhomedir());         //  search_images settings folder      20.0
   snprintf(searchresults_file,199,"%s/search_results",get_zhomedir());          //  output of image search function
   snprintf(maps_folder,199,"/usr/share/fotoxx-maps/data");                      //  map files in fotoxx-maps package
   snprintf(user_maps_folder,199,"%s/user_maps",get_zhomedir());                 //  map files made by user
   snprintf(montage_maps_folder,199,"%s/montage_maps",get_zhomedir());           //  montage map files made by user
   snprintf(palettes_folder,199,"%s/palettes",get_zhomedir());                   //  color palettes folder              19.0
   snprintf(pixel_maps_folder,199,"%s/pixel_maps",get_zhomedir());               //  pixel map files folder             20.0

   err = stat(index_folder,&statb);                                              //  create missing folders
   if (err) mkdir(index_folder,0750);
   err = stat(saved_areas_folder,&statb);
   if (err) mkdir(saved_areas_folder,0750);
   err = stat(albums_folder,&statb);
   if (err) mkdir(albums_folder,0750);
   err = stat(saved_curves_folder,&statb);
   if (err) mkdir(saved_curves_folder,0750);
   err = stat(drawtext_folder,&statb);
   if (err) mkdir(drawtext_folder,0750);
   err = stat(mashup_folder,&statb);
   if (err) mkdir(mashup_folder,0750);
   err = stat(slideshow_folder,&statb);
   if (err) mkdir(slideshow_folder,0750);
   err = stat(slideshow_trans_folder,&statb);
   if (err) mkdir(slideshow_trans_folder,0750);
   err = stat(printer_color_folder,&statb);
   if (err) mkdir(printer_color_folder,0750);
   err = stat(scripts_folder,&statb);
   if (err) mkdir(scripts_folder,0750);
   err = stat(palettes_folder,&statb);
   if (err) mkdir(palettes_folder,0750);
   err = stat(search_images_folder,&statb);
   if (err) mkdir(search_images_folder,0750);

   err = stat(maps_folder,&statb);                                               //  alternative locs for map files     19.0
   if (err) {
      snprintf(maps_folder,199,"%s/.local/share/fotoxx-maps/data",getenv("HOME"));
      err = stat(maps_folder,&statb);
   }
   if (err) snprintf(maps_folder,199,"missing");
   printz("maps folder: %s \n",maps_folder);

   err = stat(user_maps_folder,&statb);
   if (err) mkdir(user_maps_folder,0750);
   err = stat(montage_maps_folder,&statb);
   if (err) mkdir(montage_maps_folder,0750);
   err = stat(palettes_folder,&statb);
   if (err) mkdir(palettes_folder,0750);
   err = stat(pixel_maps_folder,&statb);
   if (err) mkdir(pixel_maps_folder,0750);

   load_params();                                                                //  restore parameters from last session
   
   for (ii = 1; ii < argc; ii++)                                                 //  command line parameters
   {
      char *pp = argv[ii];

      if (strmatch(pp,"-home")) ii++;                                            //  -home homedir    skip, see above
      else if (strmatchV(pp,"-home","-h",0))                                     //  -h -help         list parameters
         showz_docfile(Mwin,"userguide","command parameters");                   //                                     20.0
      else 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(locale,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
         Frecent = 1;
      else if (strmatchV(pp,"-new","-n",0))                                      //  -n -new             newest files
         Fnew = 1;
      else if (strmatchV(pp,"-album","-a",0) && argc > ii+1)                     //  -a -album "name"    album
         commandalbum = zstrdup(argv[++ii]);
      else if (strmatchV(pp,"-prev","-p",0))                                     //  -p -prev            previous file
         Fprev = 1;
      else if (strmatchV(pp,"-blank","-b",0))                                    //  -b -blank           blank window
         Fblank = 1;
      else if (strmatchV(pp,"-menu","-m",0) && argc > ii+1)                      //  -m -menu "name"     menu function
         commandmenu = zstrdup(argv[++ii]);
      else if (strmatchV(pp,"-index","-x",0) && argc > ii+1) {                   //  -x -index N         set index level
         jj = atoi(argv[++ii]);                                                  //   (0/1/2 = none/old/old+new files)
         if (jj >= 0 && jj <= 2) Pindexlev = jj;
      }
      else {                                                                     //  assume command param or initial file
         if (commandmenu) {
            commandparam = zstrdup(pp);
            continue;
         }
         initial_file = zstrdup(pp);
         if (*pp == '\'' || *pp == '"')                                          //  if quotes, remove them             20.0
            strncpy0(initial_file,pp+1,strlen(pp)-1);
         if (*initial_file != '/') {                                             //  if no initial '/'
            cc = strlen(initial_file);                                           //    assume relative to CWD
            temp = zstrdup(getcwd(0,0),cc+4);
            strncatv(temp,200,"/",initial_file,0);                               //  prepend CWD/
            initial_file = temp;
         }
         break;
      }
   }
   
   E2Xinit(locale,0);                                                            //  setup locale, translations
   setlocale(LC_NUMERIC,"en_US.UTF-8");                                          //  stop comma decimal points

   zsetfont(dialog_font);                                                        //  set default font for widgets

   build_widgets();                                                              //  build window widgets and menus
   
   if (Fclone) {                                                                 //  clone: open new window
      gtk_window_move(MWIN,cloxx,cloyy);                                         //    at passed position
      gtk_window_resize(MWIN,cloww,clohh);                                       //       with passed size
   }
   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
   }

   gtk_widget_show_all(Mwin);

   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
   dotcursor = gdk_cursor_new_for_display(display,GDK_DOT);                      //  cursor for show_RGB()

// ------------------------------------------------------------------------------

   //  check that necessary programs are installed

   err = shell_quiet("which heif-convert >/dev/null 2>&1");                      //  file.heic to .jpg converter        20.0
   if (! err) Fheif = 1;

   err = shell_quiet("which opj_decompress >/dev/null 2>&1");                    //  file.jp2 to .tif converter         20.0
   if (! err) Fjp2 = 1;

   err = shell_quiet("which dcraw >/dev/null 2>&1");                             //  check for Dcraw
   if (! err) Fdcraw = 1;

   err = shell_quiet("which exiftool >/dev/null 2>&1");                          //  check for exiftool
   if (! err) Fexiftool = 1;

   err = shell_quiet("which rawtherapee >/dev/null 2>&1");                       //  check for Raw Therapee
   if (! err) Frawtherapee = 1;

   err = shell_quiet("which rawtherapee-cli >/dev/null 2>&1");                   //  Raw Therapee command line          20.0
   if (! err) Frawtherapeecli = 1;
   
   err = shell_quiet("which growisofs >/dev/null 2>&1");                         //  check for growisofs
   if (! err) Fgrowisofs = 1;

   err = shell_quiet("which addr2line >/dev/null 2>&1");                         //  check for addr2line
   if (! err) Faddr2line = 1;
   
   err = shell_quiet("which ffmpeg >/dev/null 2>&1");                            //  check for ffmpeg
   if (! err) Ffmpeg = 1;

   err = shell_quiet("which hugin >/dev/null 2>&1");                             //  need all of it
   if (! err) PTtools = 1;
   
   if (Fexiftool + Fdcraw < 2) {                                                 //  check mandatory dependencies
      strcpy(buff,E2X("Please install missing programs:"));
      if (! Fdcraw) strcat(buff,"\n dcraw (RAW files and thumbnails)");
      if (! Fexiftool) strcat(buff,"\n exiftool (view/edit image metadata)");
      zmessageACK(Mwin,buff);
      quitxx();                                                                  //  unconditional exit                 20.05
   }
   
   if (! Fheif) printz("HEIF files not supported \n");                           //  iPhone photos                      20.0
   else if (! strstr(imagefiletypes,".heic"))                                    //  add .heic file type                20.0
      strcat(imagefiletypes,".heic ");

   if (! Fjp2) printz(".jp2 files not supported \n");                            //  jpeg2000 files                     20.0
   else if (! strstr(imagefiletypes,".jp2"))                                     //  add .jp2 file type                 20.0
      strcat(imagefiletypes,".jp2 ");

   if (! Frawtherapee) printz("RawTherapee not installed \n");
   if (! Frawtherapeecli) printz("rawtherapee-cli not installed \n");
   if (! Frawtherapeecli) Frawloader = 1;                                        //  force dcraw loader                 20.0
   if (! Fgrowisofs) printz("growisofs not installed \n");
   if (! Ffmpeg) printz("ffmpeg not installed \n");
   if (! PTtools) printz("Panorama Tools (Hugin) not installed \n");
   if (! Faddr2line) printz("addr2line not installed \n");

   //  delete fotoxx temp files if owner process is no longer running
   
   fid = popen("pidof fotoxx","r");                                              //  get active fotoxx PIDs             20.0
   pidlist = fgets_trim(buff,200,fid);                                           
   pclose(fid);
   pidlist = zstrdup(pidlist);

   snprintf(temp_folder,200,"%s/tempfiles-*",get_zhomedir());                    //  get existing /.../tempfiles-xxxxx
   snprintf(buff,200,"find %s -type d 2>/dev/null",temp_folder);
   fid = popen(buff,"r");
   while ((pp = fgets_trim(buff,200,fid))) {                                     //  loop temp folders
      pid = strrchr(pp,'-');                                                     //  -xxxxx (pid)
      if (! pid) continue;
      if (strstr(pidlist,pid+1)) continue;                                       //  PID still active, do not delete
      shell_quiet("rm -R -f -v %s",pp);                                          //  delete folder and contents
   }
   pclose(fid);
      
   //  set up temp folder /.../tempfiles-xxxx/  where xxxx is owner PID

   fotoxxPID = getpid();                                                         //  19.0
   snprintf(temp_folder,200,"%s/tempfiles-%d",get_zhomedir(),fotoxxPID);
   err = mkdir(temp_folder,0750);
   if (err) {
      zmessageACK(Mwin,"%s: %s",temp_folder,strerror(errno));
      zexit("cannot create temp folder");
   }
   printz("temp files: %s \n",temp_folder);

   //  file name template for undo/redo files

   snprintf(URS_filename,100,"%s/undo_nn",temp_folder);                          //  /.../tempfiles/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);

   Ftinycomputer = 0;                                                            //  flag, inadequate memory            19.20
   if (freememory < 4000) {
      Ftinycomputer = 1;
      printz("computer has inadequate memory \n");
   }

   printz("image size limits for good performance: \n");
   printz("  view: %.0f megapixels \n",(freememory-1000)/6);                     //  F + preview, 3 bytes/pixel each
   printz("  edit: %.0f megapixels \n",(freememory-1000)/64);                    //  + E0/E1/E3/ER, 16 bytes/pixel each

   //  get locale specific name for /home/<user>/Desktop
   
   strcpy(desktopname,"Desktop");

   snprintf(filename,200,"%s/.config/user-dirs.dirs",getenv("HOME"));
   fid = fopen(filename,"r");
   if (fid) {
      while (true) {
         pp = fgets_trim(buff,200,fid);
         if (! pp) break;
         if (! strmatchN(pp,"XDG_DESKTOP_DIR=",16)) continue;
         pp = strchr(pp+16,'/');
         if (! pp) continue;
         strncpy0(desktopname,pp+1,100);
         cc = strlen(desktopname);
         if (desktopname[cc-1] == '"') desktopname[cc-1] = 0;
         printz("locale desktop name: %s \n",desktopname);
         break;
      }
      fclose(fid);
   }

   //  miscellaneous

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

   printz("screen width: %d  height: %d \n",                                     //  monitor pixel size
            zfuncs::monitor_ww,zfuncs::monitor_hh);

   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_geometry("load");                                                     //  load saved dialogs position/size
   gallery_memory("load");                                                       //  load recent gallery positions
   KBshortcuts_load();                                                           //  load KB shortcuts from file

   Fmenublock = 0;                                                               //  set menus free from here           20.0

   //  check for first time Fotoxx install
   //  perform image index function at each startup

   if (Ffirsttime) {
      first_startup();                                                           //  initial user indexing decision     20.0
      Ffirsttime = 0;                                                            //  reset first time flag
      Prelease = zstrdup(Frelease);                                              //  set installed release
   }
   else {                                                                        //  not first startup
      if (Pindexlev >= 0) index_rebuild(Pindexlev,0);                            //  use -index command parameter
      else if (initial_file) index_rebuild(FMindexlev,0);                        //  use file manager index level
      else index_rebuild(Findexlev,0);                                           //  user standard index level
   }
   
   if (! thumbfolder) {                                                          //  check index was done               20.0
      zmessageACK(Mwin,E2X("Index aborted"));
      zexit("index aborted");
   }

   if (! strmatch(Prelease,Frelease)) {                                          //  release change?
      Prelease = zstrdup(Frelease);                                              //  update installed release
      showz_textfile("doc","changelog",Mwin);                                    //  show change log
      release_housekeeping();                                                    //  do release housekeeping            20.03
   }

   //  set Linux current working directory

   if (! navi::galleryname) {
      if (topfolders[0]) gallery(topfolders[0],"init",0);                        //  default 1st top image folder
      else {
         pp = getcwd(0,0);                                                       //  else use curr. directory           20.0
         if (pp) gallery(pp,"init",0);
         if (pp) free(pp);
      }
   }

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

   if (initial_file) {                                                           //  file parameter (or folder)
      printz("initial file: %s \n",initial_file);
      ftype = image_file_type(initial_file);

      if (ftype == FDIR) {                                                       //  folder
         gallery(initial_file,"init",0);                                         //  initz. gallery
         gallery(0,"sort",-2);                                                   //  recall sort and position
         m_viewmode(0,"G");
      }
      else if (ftype == IMAGE || ftype == RAW || ftype == VIDEO) {               //  image file
         f_open(initial_file);
         gallery(initial_file,"init",0);                                         //  initz. gallery from initial file   20.0
         gallery(0,"sort",-2);
      }
      else {
         printz(" -invalid file \n");
         if (curr_file) zfree(curr_file);                                        //  20.0
         curr_file = 0;
         if (navi::gallerytype == GDIR) 
            gallery(navi::galleryname,"init",0);
         else gallery(navi::galleryname,"initF",0);
         gallery(0,"sort",-2);
         m_viewmode(0,"G");
      }

      zfree(initial_file);
      initial_file = 0;
   }

   else if (commandalbum) {                                                      //  -album parameter
      printz("initial album: %s \n",commandalbum);
      snprintf(filename,200,"%s/albums/%s",get_zhomedir(),commandalbum);
      err = stat(filename,&statb);
      if (err) {
         printz("invalid album file: %s \n",commandalbum);
         commandalbum = 0;
      }
      else album_show(filename);
   }

   else if (Fprev) {                                                             //  start with previous file
      if (curr_file && *curr_file == '/')
         f_open(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
      if (curr_file) zfree(curr_file);
      curr_file = 0;
      if (navi::galleryname) zfree(navi::galleryname);
      navi::galleryname = 0;
      navi::gallerytype = TNONE;
      set_mwin_title();
   }

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

   else if (strmatch(startdisplay,"album")) {
      printz("initial album: %s \n",startalbum);
      err = stat(startalbum,&statb);
      if (err) {
         printz("invalid album file: %s \n",startalbum);
         commandalbum = 0;
      }
      else album_show(startalbum);                                               //                              bugfix 19.0
      if (curr_file) zfree(curr_file);
      curr_file = 0;
   }

   else if (strmatch(startdisplay,"recent")) {                                   //  start with recent files gallery
      m_recentfiles(0,0);
      if (curr_file) zfree(curr_file);
      curr_file = 0;
   }

   else if (strmatch(startdisplay,"newest")) {                                   //  start with newest files gallery
      m_newfiles(0,"file");                                                      //    by file mode date
      if (curr_file) zfree(curr_file);
      curr_file = 0;
   }

   else if (strmatch(startdisplay,"prevG")) {                                    //  start with previous gallery
      if (navi::gallerytype != TNONE) {
         if (navi::gallerytype == GDIR)
            gallery(navi::galleryname,"init",0);
         else gallery(navi::galleryname,"initF",0);
         gallery(0,"sort",-2);                                                   //  recall sort and position
         m_viewmode(0,"G");
      }
      if (curr_file) zfree(curr_file);
      curr_file = 0;
   }

   else if (strmatch(startdisplay,"prevF")) {                                    //  start with previous image file
      err = 1;
      if (curr_file && *curr_file == '/')
         err = f_open(curr_file);
      if (err) {                                                                 //  not available, use prev. gallery   20.0
         if (navi::gallerytype != TNONE) {
            if (navi::gallerytype == GDIR)
               gallery(navi::galleryname,"init",0);
            else gallery(navi::galleryname,"initF",0);
            gallery(0,"sort",-2);
            m_viewmode(0,"G");
         }
      }
   }

   else if (strmatch(startdisplay,"specG")) {                                    //  start with specified gallery (folder)
      if (startfolder && *startfolder == '/') {
         gallery(startfolder,"init",0);
         gallery(0,"sort",-2);                                                   //  recall sort and position
         m_viewmode(0,"G");
      }
      if (curr_file) zfree(curr_file);
      curr_file = 0;
   }

   else if (strmatch(startdisplay,"specF"))                                      //  start with given image file
      f_open(startfile);
   
   save_params();                                                                //  save parameters now

   g_timeout_add(10,gtimefunc,0);                                                //  start periodic function (10 ms)    19.0

   startsecs = get_seconds() - startsecs;                                        //  19.0
   printz("startup time: %.1f secs.\n",startsecs);

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


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

//   Fotoxx first startup - initial user decision about indexing

void first_startup()                                                             //  20.0
{
   zdialog  *zd;
   int      yn, zstat, nn;

   cchar    *defer1 = E2X(" Defer image file indexing:");
   cchar    *defer2 = E2X("   • Fotoxx will start immediately \n"
                          "   • View and edit image files will work normally \n"
                          "   • Image search, batch and map functions will not work \n"
                          "   • Thumbnail galleries will be slow");

   cchar    *index1 = E2X(" Index image files now:");
   cchar    *index2 = E2X("   • Initial indexing may need considerable time \n"
                          "   • Subsequent startups will be fast \n"
                          "   • Full functionality will be available \n"
                          "   • Thumbnail galleries will be fast");

   cchar    *info = E2X(" Indexing time depends on the number of image files and the \n"
                        " speed of your computer. This can be a few hundred to a few \n"
                        " thousand per minute. After indexing is done, startup time \n"
                        " should be quite fast. You can change index options later, \n"
                        " using these menus: Tools > Index and Tools > Preferences. ");
   
   if (Ftinycomputer) {
      yn = zmessageYN(Mwin,E2X("Main memory is too small to run Fotoxx. \n"
                               "You can try anyway if you wish. \n"
                               "   Continue?"));
      if (! yn) quitxx();
   }

   F1_help_topic = "first startup";

/***
          ______________________________________________________________
         |               Fotoxx First Startup                           |
         |                                                              |
         | (o) Defer image file indexing:                               |
         |     • Fotoxx will start immediately                          |
         |     • View and edit image files will work normally           |
         |     • Image search, batch and map functions will not work    |
         |     • Thumbnail galleries will be slow                       |
         |                                                              |
         | (o) Index image files now:                                   |
         |     • Initial indexing may need considerable time            |
         |     • Subsequent startups will be fast                       |
         |     • Full functionality will be available                   |
         |     • Thumbnail galleries will be fast                       |
         |                                                              |
         | Indexing time depends on the number of image files and the   |
         | speed of your computer. This can be a few hundred to a few   |
         | thousand per minute. After indexing is done, startup time    |
         | should be quite fast. You can change index options later,    |
         | using these menus: Tools > Index and Tools > Preferences.    |
         |                                                              |
         |                                      [Help] [Proceed] [Quit] |
         |______________________________________________________________|

***/

   zd = zdialog_new(E2X("Fotoxx First Startup"),Mwin,Bhelp,Bproceed,Bquit,0);
   zdialog_add_widget(zd,"radio","defer1","dialog",defer1);
   zdialog_add_widget(zd,"text","defer2","dialog",defer2);
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","index1","dialog",index1);
   zdialog_add_widget(zd,"text","index2","dialog",index2);
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"text","info","dialog",info,"space=10");

   zdialog_stuff(zd,"defer1",0);
   zdialog_stuff(zd,"index1",1);

   zdialog_run(zd,0,"parent");

   zstat = zdialog_wait(zd);
   
   while (zstat == 1) {                                                          //  [Help]
      zd->zstat = 0;
      m_help(0,"Help");
      zstat = zdialog_wait(zd);
   }

   if (zstat == 2) {                                                             //  [Proceed]
      zdialog_fetch(zd,"index1",nn);
      zdialog_free(zd);
      if (nn) {                                                                  //  do indexing
         Findexlev = 2;                                                          //  fotoxx command: full index process
         FMindexlev = 1;                                                         //  file manager: use curr. index
         m_index(0,0);
      }
      else {
         Findexlev = FMindexlev = 0;                                             //  use no index
         index_noindex();
      }
   }
   
   else {                                                                        //  [Quit]
      zdialog_free(zd);
      quitxx();
   }
}


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

//  Fotoxx new release first startup - merge default config files with
//  possible user additions, save in user's Fotoxx home folder.
//  /usr/share/fotoxx/data/metadata_short_list: merge user additions
//  /usr/share/fotoxx/data/userguide: copy unconditionally

void release_housekeeping()                                                      //  20.03
{
   FILE     *fid;
   char     buff[100];
   int      maxMSL = 200;                                                        //  metadata_short_list max. items
   char     mslfileB[200], mslfileU[200];
   char     *pp, *mslistB[200], *mslistU[200];
   int      ii, jj, NB = 0, NU = 0, NN = 0;
   char     maxexceeded[100];
   
   snprintf(maxexceeded,100,"exceed %d metadata items",maxMSL);

   snprintf(mslfileB,200,"%s/metadata_short_list",get_zdatadir());               //  base file (release file)
   snprintf(mslfileU,200,"%s/metadata_short_list",get_zhomedir());               //  user file with poss. additions
   
   fid = fopen(mslfileB,"r");
   if (! fid) zexit("cannot find file %s",mslfileB);
   while (true) {                                                                //  read base file recs
      pp = fgets_trim(buff,100,fid,1);
      if (! pp) break;
      if (NB == maxMSL) zexit(maxexceeded);
      mslistB[NB] = zstrdup(pp);                                                 //  save list of base metadata keys
      NB++;
   }
   fclose(fid);

   fid = fopen(mslfileU,"r");
   if (fid) {
      while (true) {                                                             //  read user file recs
         pp = fgets_trim(buff,100,fid,1);
         if (! pp) break;
         if (NU == maxMSL) zexit(maxexceeded);
         mslistU[NU] = zstrdup(pp);                                              //  save list of user metadata keys
         NU++;
      }
      fclose(fid);
   }
   
   for (ii = 0; ii < NU; ii++)                                                   //  compare lists
   for (jj = 0; jj < NB; jj++)
      if (strmatch(mslistU[ii],mslistB[jj])) *mslistU[ii] = 0;                   //  mark user items found in base list
   
   fid = fopen(mslfileU,"w");
   if (! fid) zexit("cannot write file %s",mslfileU);                            //  write new user file
   for (ii = 0; ii < NB; ii++)
      fprintf(fid,"%s\n",mslistB[ii]);                                           //  write base metadata keys
   for (ii = 0; ii < NU; ii++) {
      if (*mslistU[ii]) {                                                        //  add non-matching user keys
         fprintf(fid,"%s\n",mslistU[ii]);
         NN++;                                                                   //  count additions
         if (NB + NN == maxMSL) zexit(maxexceeded);
      }
   }
   fclose(fid);
   
   for (ii = 0; ii < NB; ii++)                                                   //  free memory
      zfree(mslistB[ii]);
   for (ii = 0; ii < NU; ii++)
      zfree(mslistU[ii]);
   
   shell_quiet("cp -f %s/userguide %s/userguide",get_zdatadir(),get_zhomedir()); //  refresh user guide
   
   return;
}


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

//  functions for main window event signals

int delete_event()                                                               //  main window [x] button
{
   printz("main window delete event \n");
   m_quit(0,0);                                                                  //  returns if user bails out          20.0
   return 1;
}

int destroy_event()                                                              //  main window destroyed
{
   printz("main window destroy event \n");                                       //  no user bailout possible
   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 if (state & GDK_WINDOW_STATE_MAXIMIZED) Ffullscreen = 1;
   else Ffullscreen = 0;
   return 0;
}

void drop_event(int mousex, int mousey, char *file)                              //  file drag-drop event
{
   if (! file) return;
   printz("drag-drop file: %s \n",file);
   f_open(file,0,0,1);
   return;
}


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

//  block image updates from threads during painting by main thread
//  block main thread painting during image updates from threads
//  lock = 1: block updates   lock = 0: unblock
//  Fpaintlock: 1 = locked status, 0 = not locked

void paintlock(int lock)                                                         //  19.0
{
   if (lock) {
      for (int ii = 0; ii < 5000; ii++) {                                        //  try up to 5 seconds
         if (resource_lock(Fpaintlock)) return;                                  //  OK, locked for caller
         zsleep(0.001);
         if (ii == 1000) printz("waiting for Fpaintlock \n");
      }
      zexit("Fpaintlock, give up");                                              //  failed, no way out
   }
   
   else {
      resource_unlock(Fpaintlock);
      return;
   }
}


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

//  Periodic function (10 milliseconds)

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

   if (Fshutdown) return 0;                                                      //  shutdown underway

   if (Fpaintrequest && ! Fpaintlock && Cdrawin)                                 //  paint request pending              19.0
      gtk_widget_queue_draw(Cdrawin);
   
   if (zd_thread && zd_thread_event) {                                           //  send dialog event from thread
      if (! CEF || CEF->thread_status < 2) {                                     //  only if thread done or not busy
         zdialog_send_event(zd_thread,zd_thread_event);
         zd_thread_event = 0;
      }
   }

   Fpaint3_main();                                                               //  update window area from thread

   if (--domore > 0) return 1;                                                   //  do rest every 200 milliseconds
   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              *pp, text1[300], text2[200];
   char              digitblank[4] = " ";                                        //  digit blank: 0x2007 0x00
   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

   if (ftf) {
      ftf = 0;
      reduced = E2X("(reduced)");
      areaactive = E2X("area active");
      dialogopen = E2X("dialog open");
      blocked = E2X("blocked");
      modified = "mod";
   }

   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;
   }

   snprintf(text1,300,"CPU: %.0f%c",cpuload,'%');                                //  CPU: 123%
   if (cpuload <= 99) strcat(text1,digitblank);                                  //  keep width constant 3 digits       20.0
   if (cpuload <= 9) strcat(text1,digitblank);
   

   if (Fslideshow) {
      gtk_label_set_label(GTK_LABEL(Fpanlab),text1);                             //  done if slide show                 19.0
      gtk_widget_show_all(Fpanel);
      return;
   }

   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,"  %.2fMB",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",E2X("edits"),URS_pos);
         strncatv(text1,300,text2,0);
      }

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

   else if (Fpxb) {
      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 (curr_file) {
      pp = strrchr(curr_file,'/');                                               //  "filename.jpg"
      if (pp && Ffullscreen && ! Ffuncbusy && ! Fthreadbusy) {
         strncpy0(text2,pp+1,100);
         strncatv(text1,300,"   ",text2,0);
      }
   }

   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.
//  Show progress counter if Fbusy_goal > 0
//  added to top panel:  xx%  or  BUSY if no xx%

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\" > xx% </span>";
   static char       *doneposn = 0;
   GtkWidget         *FGpanel;
   int               pct;
   char              nn[4];

   if (! doneposn) doneposn = strstr(donetext,"xx%");                            //  get position to insert % done      19.13

   if (FGWM == 'F') FGpanel = Fpanel;
   else if (FGWM == 'G') FGpanel = Gpanel;
   else return;

   if (Fbusy_done > 0 && Fbusy_done < Fbusy_goal) {                              //  add " xx%" completion to top panel
      pct = 100 * Fbusy_done / Fbusy_goal;
      if (pct > 99) pct = 99;
      snprintf(nn,4,"%02d",pct);
      memcpy(doneposn,nn,2);                                                     //  19.13
      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);
      if (busylabel) gtk_widget_destroy(busylabel);                              //  20.0
      busylabel = 0;
      gtk_widget_show_all(FGpanel);
      return;
   }
   else {
      if (donelabel) gtk_widget_destroy(donelabel);
      donelabel = 0;
   }

   if (Ffuncbusy || Fthreadbusy) {                                               //  add " BUSY" to top panel
      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;
   }

   gtk_widget_show_all(FGpanel);
   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, mscale;
   int            fww, fhh;                                                      //  current image size at 1x
   int            mww, mhh;                                                      //  scaled image size
   int            morgx, morgy;
   int            dorgx, dorgy;
   int            centerx, centery;
   int            Fsmallimage;
   int            Frefresh = 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
      Fpaintrequest = 0;
      return 1;
   }
   
   if (Fview360) 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
      Frefresh = 1;                                                              //  image refresh needed
      pdww = Dww;
      pdhh = Dhh;
   }
   
   if (Fpaintrequest && ! Fpaintlock) {                                          //  image changed, need to paint       19.0
      Frefresh = 1;                                                              //  image refresh needed
      paintlock(1);
      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;
      }
      paintlock(0);
      Fpaintrequest = 0;                                                         //  must be after E3 capture    bugfix 19.0
   }

   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;

   wscale = 1.0 * Dww / fww;                                                     //  calc. image scale for fit window
   hscale = 1.0 * Dhh / fhh;
   if (wscale < hscale) mscale = wscale;                                         //  use greatest ww/hh ratio
   else  mscale = hscale;
   
   if (mscale > 1) Fsmallimage = 1;                                              //  flag, image < window size          20.0
   else Fsmallimage = 0;
   
   if (Cstate->fzoom == -1) {                                                    //  fit to window A                    20.0
      Cstate->mscale = mscale;                                                   //  small image: expand to fit window
      Cstate->fzoom = 0;
      zoomx = zoomy = 0;                                                         //  no zoom target
   }
   else if (Cstate->fzoom == 0) {                                                //  fit to window B                    20.0
      Cstate->mscale = mscale;
      if (Fsmallimage) Cstate->mscale = 1;                                       //  small image: use 1x size
      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 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
      Frefresh = 1;                                                              //  image refresh needed
   }

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

   if (Frefresh) {                                                               //  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 (drag)
         Cstate->morgy -= round(Mwdragy);
      }
      if (Fdragopt == 2) {
         Cstate->morgx += round(Mwdragx);                                        //  opposite direction (scroll)
         Cstate->morgy += round(Mwdragy);
      }
      if (Fdragopt == 3) {
         Cstate->morgx -= round(Mwdragx * magx);                                 //  same direction, fast
         Cstate->morgy -= round(Mwdragy * magy);
      }
      if (Fdragopt == 4) {
         Cstate->morgx += round(Mwdragx * magx);                                 //  opposite direction, fast
         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

   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 (zd_darkbrite) darkbrite_paint();                                          //  update dark/bright pixels

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

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

   if (Frefresh) {                                                               //  renew background within image area
      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] = FBrgb[0];                                                       //  background color 
      bgpix[1] = FBrgb[1];
      bgpix[2] = FBrgb[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)                                         //  add periodic black dots
            memset(pix,0,3);
         else memcpy(pix,bgpix,3);
      }
   }

   gdk_cairo_set_source_pixbuf(cr,BGpixbuf,dorgx,dorgy);                         //  paint background image
   cairo_paint(cr);                                                              //    < 0.01 seconds, 4K x 2K window

   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 image section
   cairo_paint(cr);                                                              //    < 0.01 seconds, 4K x 2K window
   g_object_unref(pixbuf);

   if (Cstate == &Fstate) {                                                      //  view mode is image
      if (Ntoplines) draw_toplines(1,cr);                                        //  draw line overlays
      if (gridsettings[currgrid][GON]) draw_gridlines(cr);                       //  draw grid lines
      if (Ntoptext) draw_toptext(cr);                                            //  draw text strings
      if (Ntopcircles) draw_topcircles(cr);                                      //  draw circles
      if (Fshowarea) sa_show(1,cr);                                              //  draw select area outline
      if (Frefresh && zd_RGB_dist) m_RGB_dist(0,0);                              //  update brightness distribution
   }

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

   return 1;
}


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

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

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

   Fpaintrequest = 1;                                                            //  request repaint of changed image
   gtk_widget_queue_draw(Cdrawin);
   while (Fpaintrequest) zmainloop();                                            //  bugfix 19.0
   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()
{
   if (Fpaintrequest) return;                                                    //  do not overlap              bugfix 19.0
   Fpaintrequest = 1;                                                            //  request repaint of changed image
   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, cairo_t *cr)
{
   int      crflag = 0;

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }
   
   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,cr);                                                  //  update drawing window from Mpxb

   if (crflag) draw_context_destroy(draw_context);
   return;
}


//  Fpaint3 callable ONLY from threads.
//  Prepare data about region to update.
//  Main thread (below) does the window update.

namespace Fpaint3_thread_names                                                   //  window area to update from thread
{
   int      Fpaint3_lock = 0;
   int      pending = 0;
   int      px3a, py3a, ww3a, hh3a;
}


void Fpaint3_thread(int px3, int py3, int ww3, int hh3) 
{
   using namespace Fpaint3_thread_names;

   while (pending) zsleep(0.001);

   resource_lock(Fpaint3_lock);

   if (px3 < px3a) {
      ww3a += (px3a - px3);
      px3a = px3;
   }
   if (py3 < py3a) {
      hh3a += (py3a - py3);
      py3a = py3;
   }
   if (px3 + ww3 > px3a + ww3a)
      ww3a += px3 + ww3 - (px3a + ww3a);
   if (py3 + hh3 > py3a + hh3a)
      hh3a += py3 + hh3 - (py3a + hh3a);

   pending = 1;
   resource_unlock(Fpaint3_lock);
   return;
}


//  called by gtimefunc() each timer cycle

void Fpaint3_main() 
{
   using namespace Fpaint3_thread_names;

   if (! pending) return;
   resource_lock(Fpaint3_lock);
   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   Fpaint3(px3a,py3a,ww3a,hh3a,cr);
   draw_context_destroy(draw_context);
   pending = 0;
   resource_unlock(Fpaint3_lock);
   return;
}


//  same as Fpaint3 but uses E0pxm instead of E3pxm

void Fpaint0(int px3, int py3, int ww3, int hh3, cairo_t *cr)
{
   int      crflag = 0;

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

   PXM_PXB_update(E0pxm,Fpxb,px3,py3,ww3,hh3);
   PXB_PXB_update(Fpxb,Mpxb,px3,py3,ww3,hh3);
   Fpaint4(px3,py3,ww3,hh3,cr);

   if (crflag) draw_context_destroy(draw_context);
   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, cairo_t *cr)
{
   PIXBUF   *pixbuf, *bgpixbuf;
   int      px1, py1, ww1, hh1;
   int      px2, py2, ww2, hh2;
   int      crflag = 0;

   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 a few pixels)
   ww2 = Mscale * ww3 + 2 / Mscale + 4;
   hh2 = Mscale * hh3 + 2 / Mscale + 4;

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

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

   if (px2 + ww2 >= Mpxb->ww) ww2 = Mpxb->ww - px2 - 1;                          //  stay within image
   if (py2 + hh2 >= Mpxb->hh) hh2 = Mpxb->hh - py2 - 1;
   if (ww2 <= 0 || hh2 <= 0) return;

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

   if (px1 + ww2 >= Dww) ww2 = Dww - px1 - 1;                                    //  stay within window
   if (py1 + hh2 >= Dhh) hh2 = Dhh - py1 - 1;
   if (ww2 <= 0 || hh2 <= 0) return;

   pixbuf = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,px2,py2,ww2,hh2);              //  Mpxb area to paint
   if (! pixbuf) {
      printz("Fpaint4() pixbuf failure \n");
      return;
   }

   px2 = px1;                                                                    //  corresp. position in drawing window
   py2 = py1;

   px1 = px2 - Dorgx;                                                            //  corresp. position in background image
   py1 = py2 - Dorgy;

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }
   
   if (Mpxb->nc > 3) {                                                           //  alpha channel present
      ww1 = ww2;                                                                 //  draw background image to area
      hh1 = hh2;
      if (px1 + ww1 > dww) ww1 = dww - px1;
      if (py1 + hh1 > dhh) hh1 = dhh - py1;
      if (ww1 > 0 && hh1 > 0) {
         bgpixbuf = gdk_pixbuf_new_subpixbuf(BGpixbuf,px1,py1,ww1,hh1);
         if (bgpixbuf) {
            gdk_cairo_set_source_pixbuf(cr,bgpixbuf,px2,py2);
            cairo_paint(cr);
            g_object_unref(bgpixbuf);
         }
         else printz("Fpaint4() bgpixbuf failure \n");
      }
   }
   
   gdk_cairo_set_source_pixbuf(cr,pixbuf,px2,py2);                               //  draw area to window
   cairo_paint(cr);

   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,cr);                                          //  refresh select area outline
   }

   if (crflag) draw_context_destroy(draw_context);

   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     dragstart = 0, mdragx0, mdragy0;
   char           *pp;

   #define GAPR GDK_AXIS_PRESSURE

   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;
   scroll = ((GdkEventScroll *) event)->direction;                               //  scroll wheel event

   mouse_convert(Mwxposn,Mwyposn,Mxposn,Myposn);                                 //  convert to image space

   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
      dragstart = 1;                                                             //  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) {
      if (dragstart) {                                                           //  drag underway
         Mdrag = 1;
         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;
         mouse_dragtime = time - bdtime;                                         //  track drag duration
         gdk_event_get_axis((GdkEvent *) event, GAPR, &wacom_pressure);          //  wacom tablet stylus pressure 
      }
      else Mwdragx = Mwdragy = Mxdrag = Mydrag = 0;
   }

   if (type == GDK_BUTTON_RELEASE) {                                             //  button up
      Mxclick = Myclick = Mdrag = dragstart = 0;                                 //  reset click and drag status
      butime = time;                                                             //  time of button up
      if (butime - bdtime < 500) {                                               //  < 0.5 secs down, call it a click   19.0
         if (Mbutton == 1) LMclick++;                                            //  left mouse click
         if (Mbutton == 3) RMclick++;                                            //  right mouse click
         Mxclick = Mxposn = Mxdown;                                              //  click = button down position
         Myclick = Myposn = Mydown;
         if (button == 2) {                                                      //  center button click
            zoomx = Mxclick;                                                     //  re-center at mouse (Doriano)
            zoomy = Myclick;
            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
      (* mouseCBfunc)();                                                         //  remove busy test
      Fmousemain = 1;                                                            //  click/drag params are processed here
   }                                                                             //    unless reset by callback func.

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

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

   if (curr_file && LMclick && FGWM == 'F') {                                    //  F-view, left click on image 
      pp = strrchr(curr_file,'/');
      if (! pp) pp = curr_file;
      if (strstr(pp,"(fotoxx montage)")) {                                       //  click on image montage file,
         montage_Lclick_func(Mxclick,Myclick);                                   //    popup corresp. image file
         LMclick = 0;
      }
   }

   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:       meaning:
//    in          zoom-in in steps
//    out         zoom-out in steps
//    fit         zoom to fit window (image < window >> 1x)
//    fit+        zoom to fit window (image < window >> fit window)
//    toggle      toggle 1x and fit window (Z-key) (image < window >> fit window)

void m_zoom(GtkWidget *, cchar *zoom)                                            //  overhauled  20.0
{
   int         fww, fhh;
   float       scalew, scaleh, fitscale, fzoom2;
   float       Czoom, Gzoom = 0, pixels;

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

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

   scalew = 1.0 * Dww / fww;                                                     //  calc. scale to fit window
   scaleh = 1.0 * Dhh / fhh;
   if (scalew < scaleh) fitscale = scalew;
   else fitscale = scaleh;

   Czoom = Cstate->mscale;                                                       //  current image scale

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

   if (strmatch(zoom,"fit")) Gzoom = 0;                                          //  fit window (image < window >> 1x)
   if (strmatch(zoom,"fit+")) Gzoom = -1;                                        //  fit window (image < window >> fit window)

   if (strmatch(zoom,"toggle")) {                                                //  alternate 1x and fit window (Z-key) 
      if (fabsf(Czoom - fitscale) < 0.01) Gzoom = 1;                             //  at or near fit window: zoom to 1x
      else Gzoom = -1;                                                           //  else fit window (image < win >> fit win)
   }
   
   for (fzoom2 = 0.0625; fzoom2 < 4.0; fzoom2 *= zoomratio)                      //  find nearest natural ratio
      if (Czoom < fzoom2 * sqrt(zoomratio)) break;                               //  fzoom2 = 0.0625 ... 4.0

   if (strmatch(zoom,"in")) {                                                    //  zoom in - make image larger
      Gzoom = fzoom2 * zoomratio;                                                //  next size step larger
      pixels = Gzoom * Gzoom * fww * fhh;                                        //  new image size in pixels
      if (pixels > 500 * MEGA) Gzoom = Czoom;                                    //  limit size to 500 megapixels (8 GB)
   }

   if (strmatch(zoom,"out")) {                                                   //  zoom out - make image smaller
      Gzoom = fzoom2 / zoomratio;
      if (fitscale <= 1 && Gzoom < fitscale) Gzoom = -1;                         //  large image: limit = fit window    20.06
      if (fitscale > 1 && Gzoom < 1) Gzoom = 1;                                  //  small image: limit = 1x
   }

   if (Gzoom > 0 && Gzoom != 1 && Gzoom != fitscale) {
      if (Gzoom > 0.124 && Gzoom < 0.126) Gzoom = 0.125;                         //  hit these ratios exactly
      else if (Gzoom > 0.24  && Gzoom < 0.26)  Gzoom = 0.25;
      else if (Gzoom > 0.49  && Gzoom < 0.51)  Gzoom = 0.50;
      else if (Gzoom > 0.99  && Gzoom < 1.01)  Gzoom = 1.00;
      else if (Gzoom > 1.99  && Gzoom < 2.01)  Gzoom = 2.00;
      else if (Gzoom > 3.99) Gzoom = 4.0;                                        //  max. allowed zoom
   }

   if (FGWM == 'W') {                                                            //  optimize for file maps
      if (strmatch(zoom,"in") && Gzoom < 1.0) Gzoom = 1.0;                       //  zoom from small to 1x directly
      if (strmatch(zoom,"out")) Gzoom = 0.0;                                     //  zoom from large to fit window directly
   }

   Cstate->fzoom = Gzoom;                                                        //  set new zoom size
   if (! Gzoom) 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 KBevent(GdkEventKey *event)
{
   KBpress(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); }
namespace view360 { void KB_func(int key); }

int KBpress(GtkWidget *win, GdkEventKey *event, void *)                          //  keyboard key was pressed
{
   int      ii, jj, cc;
   char     shortkey[20] = "";
   cchar    *action = 0;
   char     *pp, *file = 0;

   KBkey = event->keyval;                                                        //  input key
   
   if ((KBkey & 0xfff0) == 0xffe0) return 1;                                     //  Ctrl/Shift/Alt key

   KBcontrolkey = KBshiftkey = KBaltkey = 0;                                     //  look for combination keys
   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 (KBshiftkey && KBkey == GDK_KEY_plus) KBshiftkey = 0;                      //  treat Shift [+] same as [+]
   if (KBkey == GDK_KEY_equal) KBkey = GDK_KEY_plus;                             //  treat [=] same as [+]
   if (KBkey == GDK_KEY_KP_Add) KBkey = GDK_KEY_plus;                            //  treat keypad [+] same as [+]
   if (KBkey == GDK_KEY_KP_Subtract) KBkey = GDK_KEY_minus;                      //  treat keypad [-] same as [-]
   
   if (KBkey == GDK_KEY_F1) {                                                    //  F1  >>  user guide
      showz_docfile(Mwin,"userguide",F1_help_topic);                             //  text docfile                       20.0
      return 1;
   }

   if (Fslideshow) {                                                             //  slide show active                  19.0
      if (KBkey == GDK_KEY_F10) Fescape = 1;                                     //  tell slide show to quit
      else if (KBkey == GDK_KEY_F11) Fescape = 1;
      else if (KBkey == GDK_KEY_Escape) Fescape = 1;
      else ss_KBfunc(KBkey);                                                     //  pass other keys to slide show
      return 1;
   }
   
   if (Fview360) {                                                               //  view360 active
      view360::KB_func(KBkey);                                                   //  pass KB keys to view360
      return 1;
   }

   if (KBkey == GDK_KEY_F10) {                                                   //  F10: fullscreen toggle with menu
      if (! Ffullscreen) win_fullscreen(0);                                      //  toggle full-screen mode and back
      else win_unfullscreen();
      return 1;
   }

   if (KBkey == GDK_KEY_F11) {                                                   //  F11: fullscreen toggle no menu
      if (! Ffullscreen) win_fullscreen(1);                                      //  toggle full-screen mode and back
      else win_unfullscreen();
      return 1;
   }

   if (KBkey == GDK_KEY_Escape) {                                                //  ESC key                            19.0
      if (Fescape) quitxx();                                                     //  2nd try, quit now
      else if (Ffuncbusy || Fthreadbusy) Fescape = 1;                            //  ask busy function/thread to quit
      else if (Ffullscreen) win_unfullscreen();                                  //  exit full screen mode
      else delete_event();                                                       //  allow bailout if unsaved changes   20.0
      return 1;
   }
   
   if (KBkey == GDK_KEY_p)                                                       //  P key, play video file             20.0
   {                                                                             //  (OK to also use P in shortcuts)
      if (! curr_file) return 1;

      if (image_file_type(curr_file) == VIDEO)
      {
         file = zescape_quotes(curr_file);
         shell_quiet("%s \"%s\"",video_command,file);                            //  command from user preferences      20.0
         zfree(file);
         return 1;
      }

      pp = strrchr(curr_file,'.');                                               //  play animated GIF file
      if (pp && strstr(".gif .GIF",pp)) {
         play_gif(curr_file);
         return 1;
      }
   }

   if (KBkey == GDK_KEY_Delete) action = (char *) "Delete";                      //  reserved shortcuts
   if (KBkey == GDK_KEY_Left) action = (char *) "Left";
   if (KBkey == GDK_KEY_Right) action = (char *) "Right";
   if (KBkey == GDK_KEY_Up) action = (char *) "Up";
   if (KBkey == GDK_KEY_Down) action = (char *) "Down";
   if (KBkey == GDK_KEY_Home) action = (char *) "First";
   if (KBkey == GDK_KEY_End) action = (char *) "Last";
   if (KBkey == GDK_KEY_Page_Up) action = (char *) "Page_Up";
   if (KBkey == GDK_KEY_Page_Down) action = (char *) "Page_Down";
   if (KBkey == GDK_KEY_z) action = (char *) "toggle-zoom";                      //  Z-key                              20.0
   
   if (! action)                                                                 //  custom shortcut
   {
      if (KBkey >= GDK_KEY_F2 && KBkey <= GDK_KEY_F9) {                          //  input key is F2 to F9
         ii = KBkey - GDK_KEY_F1;
         strcpy(shortkey,"F1");                                                  //  convert to "F2" etc.
         shortkey[1] += ii;
      }
      
      if (! *shortkey && KBkey < 256)                                            //  single ascii character
      {
         if (KBcontrolkey) strcat(shortkey,"Ctrl+");                             //  build input key combination
         if (KBaltkey) strcat(shortkey,"Alt+");                                  //  [Ctrl+] [Alt+] [Shift+] key
         if (KBshiftkey) strcat(shortkey,"Shift+");
         cc = strlen(shortkey);
         shortkey[cc] = KBkey;
         shortkey[cc+1] = 0;
      }

      if (*shortkey) {                                                           //  find key in shortcut list
         for (ii = 0; ii < Nkbsu; ii++)
            if (strmatchcase(shortkey,kbsutab[ii].key)) break;
         if (ii < Nkbsu) action = kbsutab[ii].menu;                              //  corresp. action or function
      }
   }
   
   if (! action) {
      if (strlen(shortkey) == 1)                                                 //  19.0
         printz("shortcut key not found: %c \n",toupper(*shortkey));
      else printz("shortcut key not found: %s \n",shortkey);
      return 1;
   }

   if (zstrstr(zdialog_button_shortcuts,action)) {
      printz("dialog button shortcut, ignored \n");
      return 1;
   }

   if (FGWM == 'G') {                                                            //  G view mode
      navi::KBaction(action);                                                    //  pass KB action to gallery
      return 1;
   }
   
   if (strmatch(action,E2X("Show Hidden"))) return 1;                            //  only meaningful in G view
   
   if (FGWM == 'M') {                                                            //  map view mode
      ii = strmatchV(action,"File View","Gallery",0);                            //  allow only view mode changes
      if (! ii) {
         if (strlen(shortkey) == 1)                                              //  19.0
            printz("shortcut key ignored: %c %s \n",toupper(*shortkey),action);
         else printz("shortcut key ignored: %s %s \n",shortkey,action);
         return 1;
      }
   }

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

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

   if (CEF && CEF->menufunc == m_trim_rotate) {                                  //  trim_rotate active, pass KB key
      trimrotate::KBfunc(KBkey);
      return 1;
   }

   if (CEF && CEF->menufunc == m_perspective) {                                  //  perspective active, pass KB key
      perspective::KBfunc(KBkey);
      return 1;
   }
   
   if (strmatch(action,"Left")) {                                                //  left arrow - previous image
      m_prev(0,0);                          
      return 1;
   }

   if (strmatch(action,"Right")) {                                               //  right arrow - next image
      m_next(0,0);
      return 1;
   }

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

   if (strmatch(action,"Zoom+")) {                                               //  zoom center = mouse position
      zoomx = Mxposn;             
      zoomy = Myposn;
      m_zoom(0,"in");                                                            //  zoom-in 
      return 1;
   }

   if (strmatch(action,"Zoom-")) {                                               //  zoom to fit window
      m_zoom(0,"fit");                                                           //  small image >> 1x
      return 1; 
   }

   if (strmatch(action,"toggle-zoom")) {                                         //  toggle zoom 1x / fit window        20.0
      m_zoom(0,"toggle");                                                        //  (Z-key) 
      return 1; 
   }

//  look up action in KB shortcut table, get corresp. function and arg.
   
   for (jj = 0; jj < Nkbsf; jj++)
      if (strmatchcase(action,kbsftab[jj].menu)) break;
   
   if (jj == Nkbsf) {
      printz("shortcut menu func not found: %s %s \n",shortkey,action);
      return 1;
   }
   
   if (! kbsftab[jj].func) {
      printz("shortcut func null - ignored \n");
      return 1;
   }

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


int KBrelease(GtkWidget *win, GdkEventKey *event, void *)                        //  KB key released
{
   KBkey = 0;                                                                    //  reset current active key
   return 1;
}


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

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

void win_fullscreen(int hidemenu)
{
   if (FGWM == 'F' && hidemenu) {                                                //  if F window, hide panel
      gtk_widget_hide(MWmenu);
      gtk_widget_hide(Fpanel);
      Fpanelshow = 0;
   }

   if (hidemenu) gtk_window_fullscreen(MWIN);
   else gtk_window_maximize(MWIN);
   while (! Ffullscreen) zmainloop();
   return;
}


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

void win_unfullscreen()
{
   gtk_window_unfullscreen(MWIN);                                                //  restore old window size
   gtk_window_unmaximize(MWIN);
   gtk_widget_show(MWmenu);
   gtk_widget_show(Fpanel);
   Fpanelshow = 1;
   while (Ffullscreen) zmainloop();
   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[24];
   char        fname[100], gname[100], ffolder[100];

   if (FGWM != 'F') return;

   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(ffolder,curr_file,cc+2);                                //  get folder/path/ if short enough
   else {
      strncpy(ffolder,curr_file,96);                                             //  or use /folder/path...
      strcpy(ffolder+95,"...");
   }

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

   fposn = file_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
      strncpy0(pdatetime,pdate,11);                                              //  yyyy-mm-dd hh:mm:ss
      strncpy0(pdatetime+11,ptime,9);
      pdatetime[10] = ' ';
   }
   else strcpy(pdatetime,"(undated)");

   gtype = navi::gallerytype;

   if (gtype == GDIR)                                                            //  gallery name = folder
      snprintf(titlebar,250,"Fotoxx   %d/%d  %s  %s  %s",
               fposn,Nimages,ffolder,fname,pdatetime);
   else {
      if (gtype == SEARCH || gtype == META)
         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,"Fotoxx   %s  %d/%d  %s  %s  %s",                 //  window title bar
                  gname,fposn,Nimages,ffolder,fname,pdatetime);
      else
         snprintf(titlebar,250,"Fotoxx   %s  (*)/%d  %s  %s  %s",                //  image not in gallery
                  gname,Nimages,ffolder,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, cairo_t *cr, int fat)
{
   int               qx, qy;
   static int        pqx, pqy;
   static uint8      pixel[12];                                                  //  2x2 block of pixels
   static PIXBUF     *pixbuf1 = 0, *pixbuf4 = 0;

   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
      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 (Mscale <= 1 && ! fat) {                                                   //  write 1x1 pixels
      pixel[0] = LINE_COLOR[0];
      pixel[1] = LINE_COLOR[1];
      pixel[2] = LINE_COLOR[2];

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

   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(cr,pixbuf4,qx+Dorgx,qy+Dorgy);
      cairo_paint(cr);
   }

   return;
}


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

void erase_pixel(int px, int py, cairo_t *cr)
{
   GdkPixbuf      *pixbuf;
   static int     pqx, pqy;
   int            qx, qy;

   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;

   pixbuf = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,qx,qy,2,2);                    //  2x2 Mpxb area to copy
   qx = qx - Morgx + Dorgx;                                                      //  target pixel in window
   qy = qy - Morgy + Dorgy;
   gdk_cairo_set_source_pixbuf(cr,pixbuf,qx,qy);
   cairo_paint(cr);

   g_object_unref(pixbuf);

   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, cairo_t *cr)
{
   float       px1, py1, px2, py2;
   double      dashes[2] = { 4, 4 };
   double      R, G, B;
   int         crflag = 0;
   
   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;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

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

   if (crflag) draw_context_destroy(draw_context);
   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, cairo_t *cr)
{
   float    pxm, pym, slope;
   int      crflag = 0;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      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,cr);
      }
   }

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

   if (crflag) draw_context_destroy(draw_context);
   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, cairo_t *cr)
{
   int      ii;
   int      crflag = 0;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      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,cr);

   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,cr);

   if (crflag) draw_context_destroy(draw_context);

   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(cairo_t *cr)
{
   int      G = currgrid;
   int      px, py, gww, ghh;
   int      startx, starty, endx, endy, stepx, stepy;
   int      startx1, starty1;
   int      crflag = 0;

   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 * (trimx2 - trimx1);                                          //  fit grid box to trim rectangle
      ghh = Mscale * (trimy2 - trimy1);
      startx = Mscale * trimx1 - Morgx + Dorgx;
      starty = Mscale * trimy1 - 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]);
   
   if (stepx < 20) stepx = 20;                                                   //  sanity limits                      20.0
   if (stepy < 20) stepy = 20;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

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

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

   cairo_stroke(cr);

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

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

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

   cairo_stroke(cr);

   if (crflag) draw_context_destroy(draw_context);
   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)
{
   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(cairo_t *cr)
{
   int      crflag = 0;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

   if (crflag) draw_context_destroy(draw_context);
   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 overlay text on window, black on white background
//  coordinates are image space

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

   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 (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

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

   if (crflag) draw_context_destroy(draw_context);
   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(cairo_t *cr)
{
   double      R, G, B;
   double      px, py, rad;                                                      //  20.0
   int         ii, crflag = 0;

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

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

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

   if (crflag) draw_context_destroy(draw_context);
   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, rad: center and radius of circle in image space.
//  if Ferase, then erase previous circle only.

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

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

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

   if (Ferase) {
      gdk_window_thaw_updates(gdkwin);                                           //  20.0
      return;                                                                    //  erase only, done
   }

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

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

   cx = cx * Mscale - Morgx + Dorgx;                                             //  convert to window coordinates
   cy = cy * Mscale - Morgy + Dorgy;
   rad = rad * Mscale;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

   cairo_set_source_rgba(cr,R,G,B,1.0);

   t1 = t2 = -1;                                                                 //  angle limits of arc to draw
   dt = 1.0 / rad;

   for (t = 0; t < 2*PI; t += dt)                                                //  loop 0-360 degrees
   {
      px = cx + rad * cos(t);                                                    //  pixel on mouse circle
      py = cy + rad * 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) {                                                                 //  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(cr,cx,cy,rad,t1,t2);                                          //  draw accumulated arc
         cairo_stroke(cr);
         t1 = t2 = -1;                                                           //  start over
      }
   }

   if (t1 >= 0) {
      cairo_arc(cr,cx,cy,rad,t1,t2);                                             //  draw rest of arc
      cairo_stroke(cr);
   }

   if (crflag) draw_context_destroy(draw_context);

   gdk_window_thaw_updates(gdkwin);                                              //  20.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 rad, int Ferase, cairo_t *cr)
{
   int         px3, py3, ww3, hh3;
   static int  ppx3, ppy3, pww3 = 0, phh3;
   int         px, py, pok;
   double      R, G, B;
   double      t, dt, t1, t2;
   int         crflag = 0;

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

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

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

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

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

   cx = cx * Mscale - Morgx + Dorgx;                                             //  convert to window coordinates
   cy = cy * Mscale - Morgy + Dorgy;
   rad = rad * Mscale;

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

   cairo_set_source_rgba(cr,R,G,B,1.0);

   t1 = t2 = -1;                                                                 //  angle limits of arc to draw
   dt = 1.0 / rad;

   for (t = 0; t < 2*PI; t += dt)                                                //  loop 0-360 degrees
   {
      px = cx + rad * cos(t);                                                    //  pixel on mouse circle
      py = cy + rad * 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) {                                                                 //  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(cr,cx,cy,rad,t1,t2);                                          //  draw accumulated arc
         cairo_stroke(cr);
         t1 = t2 = -1;                                                           //  start over
      }
   }

   if (t1 >= 0) {
      cairo_arc(cr,cx,cy,rad,t1,t2);                                             //  draw rest of arc
      cairo_stroke(cr);
   }

   if (crflag) draw_context_destroy(draw_context);
   return;
}


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

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

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

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

   if (! cr) {
      cr = draw_context_create(gdkwin,draw_context);
      crflag = 1;
   }

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

   if (Ferase) {
      if (crflag) draw_context_destroy(draw_context);
      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;

   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,cr);                                                      //  draw 2 points on ellipse
      px = cx + x + 0.5;
      draw_pixel(px,py,cr);
   }

   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,cr);                                                      //  draw 2 points on ellipse
      py = cy + y + 0.5;
      draw_pixel(px,py,cr);
   }

   if (crflag) draw_context_destroy(draw_context);

   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
   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))
{
   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,"realize",splcurve_resize,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

   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
   }

   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

         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++) {
         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
      }

      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

            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,E2X("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
      }
   }

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

   if (apset)
   {
      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)
{
   int      ap, kk;
   float    minx = 0.01 * splcurve_minx;                                         //  % to absolute distance

   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;
}


//  if height/width too small, make bigger

int splcurve_resize(GtkWidget *drawarea)
{
   int ww = gtk_widget_get_allocated_width(drawarea);
   int hh = gtk_widget_get_allocated_height(drawarea);
   if (hh < ww/2) gtk_widget_set_size_request(drawarea,ww,ww/2);
   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;

   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

      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;
   }
   
   sd->mod[spc] = 1;                                                             //  mark curve modified                20.0

   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 success, sd is initialized from file data
//  returns 1 if fail (invalid file data), sd not modified

int splcurve_load(spldat *sd, FILE *fid)
{
   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];

   if (! fid)                                                                    //  request file from user
   {
      pfile = zgetfile(E2X("load curve from a file"),MWIN,"file",saved_curves_folder);
      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);
         if (! pp) goto fail;                                                    //  19.13
         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,E2X("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(E2X("save curve to a file"),MWIN,"save",saved_curves_folder);
      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]);          //  active flag, vert flag, anchors
      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, ftype;
   int      Fpreview;
   char     *pp;

   if (! curr_file) return 0;                                                    //  no image file
   if (! Fpxb) return 0;                                                         //  not loaded yet                     20.0
   m_viewmode(0,"F");                                                            //  19.0

   ftype = image_file_type(curr_file);
   if (ftype != IMAGE && ftype != RAW) {                                         //  not editable
      pp = strrchr(curr_file,'/');
      if (pp) pp++;
      zmessageACK(Mwin,E2X("File cannot be edited \n %s"),pp);
      return 0;
   }

   if (FGWM != 'F') m_viewmode(0,"F");                                           //  insure file view mode

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

   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,E2X("Too many edits, please save image"));
      return 0;
   }

   if (Fscriptbuild && ! EF.Fscript) {                                           //  this function not scriptable
      zmessageACK(Mwin,E2X("this function cannot be scripted"));
      return 0;
   }

   free_filemap();                                                               //  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,E2X("Select area cannot be kept.\n"
                               "Continue?"));
      if (! yn) return 0;
      sa_clear();                                                                //  clear area
      if (zd_sela) zdialog_free(zd_sela);
   }
   
   if (EF.Farea == 1 && sa_stat) {                                               //  select area kept, not used         20.0
      yn = zmessageYN(Mwin,E2X("Select area will be ignored. \n"
                               "Continue?"));
      if (! yn) return 0;
   }

   if (EF.Farea == 2 && sa_stat && sa_stat != 3) {                               //  select area exists and can be used,
      yn = zmessageYN(Mwin,E2X("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;
   }

   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,0);                                                              //  hide select area if present
   }
   else E1pxm = PXM_copy(E0pxm);                                                 //  else use full size imagez

   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

   Fpaintnow();                                                                  //  update image synchronous
   return 1;
}


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

//  print error message if CEF invalid

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


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

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

void edit_cancel(int keep)
{
   if (CEF_invalid()) return;
   wrapup_thread(9);                                                             //  tell thread to quit, wait
   if (CEF_invalid()) return;

   PXM_free(E1pxm);                                                              //  free E1 E3 ER E8 E9
   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->sd) zfree(CEF->sd);                                               //  free curves data
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog
   }

   CEF = 0;                                                                      //  no current edit func
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

//  process edit apply
//  save current edits (E3 image) in E1 image.
//  accumulated 'apply's are in one edit step.

void edit_apply()                                                                //  20.0
{
   if (CEF_invalid()) return;
   wrapup_thread(8);                                                             //  tell thread to finish, wait
   if (CEF_invalid()) return;
   PXM_free(E1pxm);                                                              //  copy edits in E3 to source image E1
   E1pxm = PXM_copy(E3pxm);
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

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

void edit_done(int keep)
{
   if (CEF_invalid()) return;
   wrapup_thread(8);                                                             //  tell thread to finish, wait
   if (CEF_invalid()) return;

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

   else {                                                                        //  not modified
      PXM_free(E1pxm);                                                           //  free E1 E3 ER E8 E9
      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->sd) zfree(CEF->sd);                                               //  free curves data
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog
   }

   CEF = 0;                                                                      //  no current edit func
   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()
{
   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! CEF->Fpreview) return;                                                  //  FprevReq ignored if small image
   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 E3
   Fzoom = 0;
   CEF->Fpreview = 0;
   return;
}


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

void edit_undo()
{
   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! CEF->Fmods) return;                                                     //  not modified
   PXM_free(ERpxm);                                                              //  E3 >> redo copy
   ERpxm = E3pxm;
   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3
   CEF->Fmods = 0;                                                               //  reset image modified status
   if (CEF->zd) zdialog_send_event(CEF->zd,"undo");                              //  notify edit function               19.5
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


void edit_redo()
{
   if (CEF_invalid()) return;
   wait_thread_idle();                                                           //  wait for thread idle
   if (CEF_invalid()) return;
   if (! ERpxm) return;                                                          //  no prior undo
   PXM_free(E3pxm);                                                              //  redo copy >> E3
   E3pxm = ERpxm;
   ERpxm = 0;
   CEF->Fmods = 1;                                                               //  image modified
   if (CEF->zd) zdialog_send_event(CEF->zd,"redo");                              //  notify edit function               19.5
   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
   PXM_free(ERpxm);                                                              //  delete redo copy
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E1pxm);                                                      //  E1 >> E3
   CEF->Fmods = 0;                                                               //  reset image modified status
   Fpaintnow();                                                                  //  update image synchronous
   return;
}


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

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

int func_load_widgets(zdialog *zd, spldat *sd, cchar *funcname, FILE *fid)       //  20.0
{
   using namespace zfuncs;

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

   if (! fid)                                                                    //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                     //  folder 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
      if (strstr("defcats deftags",wname)) continue;                             //  20.0
      pp2 += 3;
      if (*pp2 == ' ') pp2++;
      wdata = pp2;                                                               //  widget data
      cc2 = strlen(wdata);
      if (cc2 < 1) wdata = (char *) "";
      if (cc2 > 1000) 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,E2X("file data does not fit dialog"));
   if (myfid) fclose(fid);
   return 1;
}


int func_save_widgets(zdialog *zd, spldat *sd, cchar *funcname, FILE *fid)       //  20.0
{
   using namespace zfuncs;

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

   cchar    *editwidgets = "entry zentry edit text togbutt check combo comboE "  //  widget types to save
                           "radio spin zspin hscale vscale colorbutt";
   if (! fid)                                                                    //  fid from script
   {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                     //  folder 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:
      if (strstr("defcats deftags",wname)) continue;                             //  20.0
      wdata = zd->widget[ii].data;                                               //    widgetname == widgetdata
      if (! wdata) continue;
      cc = strlen(wdata);
      if (cc > 900) continue;
      repl_1str(wdata,wdata2,"\n","\\n");                                        //  replace newline with "\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 function dialogs
//  load or save last-used widgets

int func_load_prev_widgets(zdialog *zd, spldat *sd, cchar *funcname)             //  20.0
{
   using namespace zfuncs;

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

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

   err = func_load_widgets(zd,sd,funcname,fid);
   fclose(fid);
   return err;
}


int func_save_last_widgets(zdialog *zd, spldat *sd, cchar *funcname)             //  20.0
{
   using namespace zfuncs;

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

   snprintf(filename,200,"%s/%s/%s",get_zhomedir(),funcname,"last-used");
   fid = fopen(filename,"w");
   if (! fid) {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname);                     //  create missing folder
      err = mkdir(dirname,0760);
      if (err) {
         printz("%s \n %s \n",dirname,strerror(errno));
         return 1;
      }
      fid = fopen(filename,"w");                                                 //  open again
   }

   if (! fid) {
      printz("%s \n %s \n",filename,strerror(errno));
      return 1;
   }

   err = func_save_widgets(zd,sd,funcname,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, call undo_all() or redo_all().
//  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 button";
   
   if (! curr_file) return;                                                      //  19.0
   if (FGWM != 'F') return;

   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
   {
      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)
{
   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 (! curr_file) return;                                                      //  19.0
   if (FGWM != 'F') return;

   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 (! curr_file) return;                                                      //  19.0
   if (FGWM != 'F') return;

   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 (! curr_file) return;                                                      //  19.0
   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 (! curr_file) return;                                                      //  19.0
   if (FGWM != 'F') return;

   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
{
   char     *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   uint     cc1, cc2;
   uint     ccmax = 128 * 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);
   zexit("undo/redo stack write failure: %d",errno);
}


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

void load_undo()                                                                 //  overhauled
{
   char     *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   uint     cc1, cc2;
   uint     ccmax = 128 * MEGA;                                                  //  reduced from 256

   pp = strstr(URS_filename,"undo_");
   if (! pp) goto err1;
   snprintf(pp+5,3,"%02d",URS_pos);

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

   nn = fread(buff,20,1,fid);
   if (nn != 1) goto err3;
   buff[20] = 0;                                                                 //  null at end of data         bugfix 19.0
   nn = sscanf(buff,"fotoxx %d %d %d",&ww,&hh,&nc);
   if (nn != 3) goto err4;

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

   cc1 = ww * hh * nc * sizeof(float);                                           //  bytes to read
   cc2 = 0;                                                                      //  bytes done
   while (cc1) {
      if (cc1 <= ccmax) {
         pp = (char *) E0pxm->pixels;                                            //  read entire (remaining) file
         nn = fread(pp+cc2,cc1,1,fid);
         if (nn != 1) goto err3;
         break;
      }
      else {
         pp = (char *) E0pxm->pixels;                                            //  read max. part of file
         nn = fread(pp+cc2,ccmax,1,fid);
         if (nn != 1) goto err3;
         cc1 -= ccmax;
         cc2 += ccmax;
      }
   }

   fclose(fid);

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

err1:
   printz("err1: %s \n",URS_filename);                                           //  extended diagnostics 
   goto readfail;

err2:
   printz("err2: open() failure, errno: %d %s \n",errno,strerror(errno));
   goto readfail;

err3:
   printz("err3: fread() failure, errno: %d %s \n",errno,strerror(errno));
   goto readfail;

err4:
   printz("err4: %s \n",buff);
   goto readfail;

readfail:
   zmessageACK(Mwin,"undo/redo stack read failure");
   zexit("undo/redo stack read failure");
}


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

//  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
//       block   edit function mutex flag
//       all     check all the above
//       quiet   suppress user message

int checkpend(cchar *list)
{
   int      edit, mods, busy, block, all, quiet, pend, choice;
   cchar    *modmess = E2X("This action will discard changes to current image");
   cchar    *activemess = E2X("prior function still active");
   cchar    *keep = E2X("Keep");
   cchar    *discard = E2X("Discard");
   
   if (Fmenublock) return 1;                                                     //  menus blocked                      20.0

   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_bold(Mwin,"20/20",2,activemess);
   }

   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
      if (pend && ! quiet) {
         choice = zdialog_choose(Mwin,"mouse",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
{
   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
{
   if (! Cdrawin) return;
   if (! gdkwin) return;
   if (! Mcapture) return;
   mouseCBfunc = 0;
   Mcapture = 0;
   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) {
      zmainsleep(0.01);                                                          //  19.0
      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) {                                                  //  wait for thread to finish
      zmainsleep(0.01);                                                          //  19.0
      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 in thread
}


//  exit thread unconditionally, called only from edit threads

void thread_exit()
{
   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)
//  threadfunc:  void * func(void *index)

//  start Nt threads and wait for all to exit

void do_wthreads(threadfunc func, int Nt)                                        //  19.0
{
   pthread_t tid[max_threads];
   
   zadd_locked(Fthreadbusy,+1);

   for (int ii = 0; ii < Nt; ii++)
      tid[ii] = start_Jthread(func, &Nval[ii]);

   for (int ii = 0; ii < Nt; ii++)
      wait_Jthread(tid[ii]);
   
   zadd_locked(Fthreadbusy,-1);

   return;
}


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

//  table for loading and saving adjustable parameters between sessions

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

#define Nparms 56
param paramTab[Nparms] = {
//     name                    type        count      location
{     "fotoxx release",       "char",        1,       &Prelease               },
{     "locale",               "char",        1,       &locale                 },
{     "first time",           "int",         1,       &Ffirsttime             },
{     "window geometry",      "int",         4,       &mwgeom                 },
{     "thumbnail size",       "int",         1,       &navi::thumbsize        },
{     "menu style",           "char",        1,       &menu_style             },
{     "icon size",            "int",         1,       &iconsize               },
{     "F-base-color",         "int",         3,       &FBrgb                  },
{     "G-base-color",         "int",         3,       &GBrgb                  },
{     "menu font color",      "int",         3,       &MFrgb                  },
{     "menu background",      "int",         3,       &MBrgb                  },
{     "index level",          "int",         1,       &Findexlev              },
{     "FM index level",       "int",         1,       &FMindexlev             },
{     "dialog font",          "char",        1,       &dialog_font            },
{     "drag option",          "int",         1,       &Fdragopt               },
{     "zoom count",           "int",         1,       &zoomcount              },
{     "map_dotsize",          "int",         1,       &map_dotsize            },
{     "last version",         "int",         1,       &Flastversion           },
{     "shift right",          "int",         1,       &Fshiftright            },
{     "curve node dist %",    "float",       1,       &splcurve_minx          },
{     "start display",        "char",        1,       &startdisplay           },
{     "start album",          "char",        1,       &startalbum             },
{     "start image file",     "char",        1,       &startfile              },
{     "start folder",         "char",        1,       &startfolder            },
{     "curr file",            "char",        1,       &curr_file              },
{     "galleryname",          "char",        1,       &navi::galleryname      },
{     "gallerytype",          "int",         1,       &navi::gallerytype      },
{     "current album",        "char",        1,       &curr_album             },
{     "copy/move loc",        "char",        1,       &copymove_loc           },
{     "RGB chooser file",     "char",        1,       &RGB_chooser_file       },
{     "netmap source",        "char",        1,       &netmap_source          },
{     "mapbox access key",    "char",        1,       &mapbox_access_key      },
{     "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           },
{     "video file types",     "char",        1,       &VIDEOfiletypes         },
{     "trim sizes",           "char",        10,      &trimsizes              },                                    //  20.0
{     "video command",        "char",        1,       &video_command          },                                    //  20.0
{     "trim buttons",         "char",        5,       &trimbuttons            },                                    //  19.0
{     "trim ratios",          "char",        5,       &trimratios             },
{     "edit resize",          "int",         2,       &editresize             }, 
{     "jpeg def quality",     "int",         1,       &jpeg_def_quality       },
{     "tiff comp method",     "int",         1,       &tiff_comp_method       },                                    //  20.0
{     "png comp level",       "int",         1,       &png_comp_level         },                                    //  20.0
{     "RAW file loader",      "int",         1,       &Frawloader             },                                    //  20.0
{     "RAW auto brighten",    "int",         1,       &Fautobright            },                                    //  20.0
{     "RAW match thumb",      "int",         1,       &Fmatchthumb            },                                    //  20.0 
{     "lens mm",              "float",       1,       &lens_mm                },
{     "SS KB keys",           "char",        1,       &ss_KBkeys              },
{     "line color",           "int",         3,       &LINE_COLOR             },                                    //  19.0
{     "Fcaptions",            "int",         1,       &Fcaptions              },                                    //  20.0
{     "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;
   
   snprintf(buff,199,"%s/parameters",get_zhomedir());                            //  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) *text = 0;                                           //  missing, use ""                    20.0
            else 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_zhomedir());                            //  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)
      {
         if (strmatch(type,"char") && pcount == 1) {                             //  one quoted string with possible
            pp = strrchr(data,'"');                                              //    embedded blanks and quotes
            if (pp) *pp = 0;
            pp = data + 1;
            if (strlen(pp) == 0) break;
            repl_1str(pp,text,"\\n","\n");                                       //  replace "\n" with real newlines
            *charloc++ = zstrdup(text);
            break;
         }
            
         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);
   }
   
   if (curr_file && ! *curr_file) curr_file = 0;                                 //  no current file >> null            20.0
   
   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)
{
   if (! fkeepundo)
      shell_quiet("rm -f %s/undo_*",temp_folder);                                //  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
   sa_clear();                                                                   //  clear select area

   Nptoplines = Ntoplines = 0;                                                   //  no toplines
   Ntopcircles = 0;                                                              //  no topcircles
   Ntoptext = 0;                                                                 //  no toptext
   Fbusy_goal = 0;                                                               //  not busy

   if (curr_file) {
      if (zd_sela) zdialog_free(zd_sela);                                        //  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

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

   return;
}


