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

   Fotocx - edit photos and manage collections

   Copyright 2007-2026 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
   check_outboards         check and install required/optional outboard programs
   first_index             user initial index decision at fotocx install time

   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

   progress_setgoal        set/reset progress counter
   progress_addvalue       increment progress value
   progress_getvalue       get progress, 0 - goal
   progress_percent        get progress, 0 - 100% 

   timerfunc               periodic function for top pamel update
   update_Fpanel           update status parameters on F window top panel
   panelmessage            create top panel message for short time display

   Fblock                  block/unblock functions that can interfere each-other

   paintlock               block window paint during E1/E3 image updates
   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)
   Fpaintmouse             update drawing window within mouse circle

   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

   RGBmatch                calculate match value for two RGB colors
   PIXmatch                calculate match value for two pixels
   RGBfix                  check and repair bad RGB values

   edit_wwhhOK             check file size for edit function
   edit_setup              start an image edit function
   edit_cancel             cancel image edit
   edit_fullsize           convert preview to full size edit
   edit_done               finish image edit
   edit_addhist            format edit parms, add to edit hist
   edit_undo               undo current edit (reset)
   edit_redo               redo current edit
   edit_reset              reset all edit changes

   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
   takeMouse               set mouse event function and special cursor
   freeMouse               remove mouse event function, set normal cursor

   thread_start            start thread in idle state
   thread_signal           signal thread that work is pending
   thread_stop             stop thread and wait for exit
   thread_wait             wait for thread to become idle
   do_wthreads             thread: start worker threads and wait

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

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

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

using namespace zfuncs;

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


//  fotocx main program

int main(int argc, ch *argv[])                                                         //  Fotocx main program
{
   ch       *temp, *pp;
   ch       *pid, *pidlist;
   int      Fclone=0, cloxx=0, cloyy=0, cloww=0, clohh=0;
   int      Fblankwindow = 0;
   int      ii, nn, cc, err, yn;
   int      Pindexlev;
   FTYPE    ftype;
   FILE     *fid;
   ch       filename[200], newname[200], buff[200];
   ch       *prev_file = 0;
   double   startsecs, freememory;
   float    maxmegapix;

   ch       *lost_settings_notify =
            TX("A new Fotocx release is being installed. \n"
               "You may need to restore custom settings.");

   startsecs = get_seconds();                                                          //  start time
   
   if (geteuid() == 0) {                                                               //  debian said this is needed            25.0
      printf("*** fotocx: root user not allowed \n");                                  //  (?? root user can do anything,
      exit(EPERM);                                                                     //   and fotocx adds nothing more)
   }

   #ifdef DEBUG
      printf("*** DEBUG BUILD *** \n");                                                //  note if makefile debug build          26.0
   #endif

   pp = getenv("WSL_DISTRO_NAME");                                                     //  note if Windows WSL Linux
   if (pp && *pp) printf("Windows WSL %s\n",pp);

   zinitapp(Frelease,argc,argv);                                                       //  initz. app and GTK, also zhomedir

   if (gtk_clutter_init(&argc,&argv) != CLUTTER_INIT_SUCCESS)                          //  intiz. clutter                        26.0
      zexit(1,"gtk_clutter_init() failed");                                            //  (quietly resets locale = system)      26.1

   setlocale(LC_NUMERIC,"C");                                                          //  stop comma decimal points             26.1

   //  initialize default parameter values (saved parameters will override these)

   Fdevelop = 0;                                                                       //  no show developer menu
   Findexlev = 2;                                                                      //  fotocx command: index old + new files
   FMindexlev = 0;                                                                     //  file manager click: no index
   Xindexlev = 0;                                                                      //  actual index level (TBD)
   Pindexlev = -1;                                                                     //  no -xN index command parameter
   xxrec_tab = 0;                                                                      //  no image index yet
   Nxxrec = 0;                                                                         //  no image index table xxrec_tab[]
   Prelease = zstrdup("unknown","params");                                             //  prev. release (params override)
   mwgeom[0] = mwgeom[1] = 20;                                                         //  default main window geometry
   mwgeom[2] = 1000; mwgeom[3] = 760;                                                  //  accomodate small screen
   *paneltext = 0;                                                                     //  no top panel text
   menu_style = zstrdup("both","params");                                              //  default menu style
   iconsize = 32;                                                                      //  default menu 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] = 220;                                               //  menu font color
   MBrgb[0] = MBrgb[1] = MBrgb[2] = 60;                                                //  menu background color
   lc = zstrdup("en","params");                                                        //  default language code, english        26.0
   strcpy(UGfile,"userguide-en");                                                      //  default userguide file                26.3
   application_font = zstrdup("FreeSans 10","params");                                 //  default menu + dialog font            26.3
   splcurve_minx = 5;                                                                  //  default curve node separation %
   startdisplay = zstrdup("previous file","params");                                   //  start with previous image file
   Fdragopt = 1;                                                                       //  image drag with mouse
   ImagePosn = zstrdup("center","params");                                             //  F-view image is centered
   zoomcount = 4;                                                                      //  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
   captext_cc[0] = 80;                                                                 //  captions line low limit
   captext_cc[1] = 100;                                                                //  captions line high limit
   curr_file = 0;                                                                      //  no current file
   Ntopfolders = 0;                                                                    //  no top folders list yet
   Flastversion = 0;                                                                   //  gallery shows all versions
   curr_album = 0;                                                                     //  no current album
   copymove_loc = 0;                                                                   //  no current copy/move to location
   RGB_chooser_file = 0;                                                               //  users RGB color chooser file
   thumbfolder = 0;                                                                    //  no thumbnail folder
   navi::thumbsize = 256;                                                              //  gallery default thumbnail size
   initial_menu = 0;                                                                   //  command line menu function
   initial_file = 0;                                                                   //  start with image file or folder
   initial_album = 0;                                                                  //  command line album name
   video_command = zstrdup("vlc --play-and-exit %s","params");                         //  default video play command
   jpeg_def_quality = 90;                                                              //  default .jpeg save quality
   tiff_comp_method = 1;                                                               //  default TIFF compression method
   raw_loader_command = zstrdup("dcraw -w -T -6 -q 0 <input>","params");               //  default raw loader command            25.3
   Fraw_match_embed = 0;                                                               //  RAW loader match embedded image
   colormapfile = zstrdup("undefined","params");                                       //  printer calibration color map
   Fcaps = 0;                                                                          //  show captions = off
   Faskquit = 0;                                                                       //  ask to quit = NO
   Fshowhidden = 0;                                                                    //  show hidden files = NO                26.4
   Fpaintedits = 0;                                                                    //  not paint edits mode
   Fmousevents = 0;                                                                    //  no show mouse events popup text
   Nblacklist = 0;                                                                     //  blacklist folders/files count
   NFbusy = 0;                                                                         //  nothing busy
   Fescape = 0;                                                                        //  no escape key pressed
   editrescale[0] = 1600;                                                              //  default rescale memory
   editrescale[1] = 1200;

   cropsizes[0] = zstrdup("1920x1080","params");                                       //  default crop size buttons
   cropsizes[1] = zstrdup("1600x900", "params");                                       //  (must match fotocx.h 'cropmem')
   cropsizes[2] = zstrdup("1440x900", "params");
   cropsizes[3] = zstrdup("1280x1024","params");
   cropsizes[4] = zstrdup("1366x768", "params");

   cropratios[0]  = zstrdup("5:4", "params");                                          //  default crop ratio buttons
   cropratios[1]  = zstrdup("4:3", "params");
   cropratios[2]  = zstrdup("8:5", "params");
   cropratios[3]  = zstrdup("16:9","params");
   cropratios[4]  = zstrdup("2:1", "params");

   gridsettings[GON] = 0;                                                              //  grid off
   gridsettings[GXC] = gridsettings[GYC] = 2;                                          //  x/y-lines count
   gridsettings[GXF] = gridsettings[GYF] = 0;                                          //  x/y-lines offset

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

//  end of parameter settings 

   tagnamex[0] = meta_fdate_tag;                                                       //  tag names for metadata items
   tagnamex[1] = meta_fsize_tag;                                                       //    in xxrec_tab[]
   tagnamex[2] = meta_pdate_tag;
   tagnamex[3] = meta_psize_tag;
   tagnamex[4] = meta_bpc_tag;
   tagnamex[5] = meta_rating_tag;
   tagnamex[6] = meta_keywords_tag;
   tagnamex[7] = meta_title_tag;
   tagnamex[8] = meta_desc_tag;
   tagnamex[9] = meta_city_tag;
   tagnamex[10] = meta_country_tag;
   tagnamex[11] = meta_gps_tag;
   tagnamex[12] = meta_make_tag;
   tagnamex[13] = meta_model_tag;
   tagnamex[14] = meta_lens_tag;
   tagnamex[15] = meta_exp_tag;
   tagnamex[16] = meta_fnum_tag;
   tagnamex[17] = meta_flen_tag;
   tagnamex[18] = meta_iso_tag;

   tagname_alt_title[0] = "headline";                                                  //  alt. tag names for image title        ATD
   tagname_alt_title[1] = "event";
   tagname_alt_title[2] = "abstract";

   tagname_alt_desc[0] = "imagedescription";                                           //  alt. tag names for description        ATD
   tagname_alt_desc[1] = "caption-abstract";
   tagname_alt_desc[2] = "subject";
   tagname_alt_desc[3] = "comment";
   tagname_alt_desc[4] = "usercomment";
   tagname_alt_desc[5] = "caption";

   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 drawing color

   Nval = (int *) zmalloc(maximages * sizeof(int),"Nval");                             //  static integer values 0-maximages
   for (int ii = 0; ii < maximages; ii++) Nval[ii] = ii;

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

   #define makehomefolder(name1,name2)                                           \
      snprintf(name1,200,"%s/"#name2,get_zhomedir());                            \
      if (! dirfile(name1)) mkdir(name1,0750);

   makehomefolder(albums_folder,albums)
   makehomefolder(custom_kernel_folder,custom_kernel)
   makehomefolder(scripts_folder,custom_scripts)
   makehomefolder(index_folder,image_index)                                            //  image index folder, this release
   makehomefolder(markup_folder,markup)
   makehomefolder(mashup_folder,mashup)
   makehomefolder(palettes_folder,palettes)
   makehomefolder(pattern_folder,patterns)
   makehomefolder(printer_color_folder,printer_color)
   makehomefolder(saved_areas_folder,saved_areas)
   makehomefolder(saved_searches_folder,saved_searches)
   makehomefolder(slideshow_folder,slideshows)
   makehomefolder(slideshow_trans_folder,slideshow_trans)
   makehomefolder(translations_folder,translations)                                    //  26.0

   snprintf(parameters_file,199,"%s/parameters",get_zhomedir());                       //  fotocx parameters file
   snprintf(blacklist_file,199,"%s/blacklist",get_zhomedir());                         //  blacklist folders and files
   snprintf(capstags_file,199,"%s/capstags",get_zhomedir());                           //  metadata tags for image captions
   snprintf(gallerymem_file,199,"%s/gallery_memory",get_zhomedir());                   //  recent gallery memory
   snprintf(image_folders_file,199,"%s/image_folders",index_folder);                   //  top image and thumbnail folders
   snprintf(image_index_file,199,"%s/image_index",index_folder);                       //  image index file
   snprintf(meta_index_file,199,"%s/meta_index",index_folder);                         //  index extra metadata tags
   snprintf(KB_shortcuts_file,199,"%s/KB_shortcuts",get_zhomedir());                   //  KB shortcuts file
   snprintf(meta_picklist_file,199,"%s/meta_picklist",get_zhomedir());                 //  metadata picklist file
   snprintf(meta_report_tags_file,199,"%s/meta_report_tags",get_zhomedir());           //  metadata batch report tags
   snprintf(meta_view_extras_file,199,"%s/meta_view_extra",get_zhomedir());            //  view_meta extra tags picklist
   snprintf(recentfiles_file,199,"%s/recent_files",get_zhomedir());                    //  recent files file
   snprintf(recent_folders_file,199,"%s/recent_folders",get_zhomedir());               //  recent folders file
   snprintf(searchresults_file,199,"%s/search_results",get_zhomedir());                //  output of image search function
   snprintf(keywords_defined_file,199,"%s/keywords_defined",get_zhomedir());           //  defined keywords file
   snprintf(raw_commands_file,199,"%s/raw_commands",get_zhomedir());                   //  RAW file loader commands file
   snprintf(bookmarks_file,199,"%s/bookmarks",get_zhomedir());                         //  gallery bookmarks file
   snprintf(custom_menu_file,199,"%s/custom_menu",get_zhomedir());                     //  custom menu file
   snprintf(map_regions_file,199,"%s/map_regions",get_zhomedir());                     //  internet map regions file
   snprintf(plugins_file,199,"%s/plugins",get_zhomedir());                             //  plugin menu functions file
   snprintf(grid_picklist_file,199,"%s/grid_picklist",get_zhomedir());                 //  grid settings file

   //  delete fotocx temp files if owner process is no longer running

   pidlist = 0;
   snprintf(buff,200,"pidof fotocx fotocx.x");                                         //  get active fotocx PIDs
   fid = popen(buff,"r");
   if (fid) {
      pp = fgets_trim(buff,200,fid);
      if (pp) pidlist = zstrdup(pp,"params");
      pclose(fid);
   }
   if (! pidlist) pidlist = zstrdup("none","params");

   snprintf(temp_folder,200,"%s/temp/temp-*",get_zhomedir());                          //  get existing /.../temp/temp-xxxxx     26.3
   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
      zshell("ack","rm -R -f %s",pp);                                                  //  delete folder and contents
   }
   pclose(fid);
   zfree(pidlist);

   snprintf(temp_folder,200,"%s/temp/temp-%d",get_zhomedir(),getpid());                //  temp folder: /.../temp/temp-xxxx/     26.3
   err = zshell("ack","mkdir -p -m 0750 %s",temp_folder);
   if (err) quitxx();
   printf("temp files: %s \n",temp_folder);

   snprintf(URS_filename,200,"%s/undo_nn",temp_folder);                                //  file name template for undo/redo files

   load_params();                                                                      //  restore parameters from last session

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

      if (strmatch(pp,"-home")) ii++;                                                  //  -home homedir    see zinitapp() above
      else if (strmatchV(pp,"-help","-h",null))                                        //  -h -help         show user guide
         showz_docfile(Mwin,"userguide","COMMAND PARAMETERS");
      else if (strmatchV(pp,"-clone","-c",null) && 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,"-lang","-l",null))                                        //  -l -lang            language code     26.0
         lc = zstrdup(argv[++ii],"params");
      else if (strmatchV(pp,"-recent","-r",null))                                      //  -r -recent          recent files
         Frecent = 1;
      else if (strmatchV(pp,"-new","-n",null))                                         //  -n -new             newest files
         Fnew = 1;
      else if (strmatchV(pp,"-album","-a",null) && argc > ii+1)                        //  -a -album "name"    album to open
         initial_album = zstrdup(argv[++ii],"params");
      else if (strmatchV(pp,"-prev","-p",null))                                        //  -p -prev            previous file
         Fprev = 1;
      else if (strmatchV(pp,"-blank","-b",null))                                       //  -b -blank           blank window
         Fblankwindow = 1;
      else if (strmatchV(pp,"-devel","-d",null))                                       //  -d -devel           show dev. menu    26.0
         Fdevelop = 1;
      else if (strmatchV(pp,"-menu","-m",null) && argc > ii+1)                         //  -m -menu "name"     do menu function
         initial_menu = zstrdup(argv[++ii],"params");
      else if (strmatch(pp,"-cwp"))                                                    //  -cwp secs "name"    cycle wallpaper
         m_cycle_wallpaper(argc,argv);
      else if (strmatch(pp,"-x0"))                                                     //  -x0                 index level 0
         Pindexlev = 0;
      else if (strmatch(pp,"-x1"))                                                     //  -x1                 index level 1
         Pindexlev = 1;
      else if (strmatch(pp,"-x2"))                                                     //  -x2                 index level 2
         Pindexlev = 2;
      else {                                                                           //  assume initial image file
         pp = combine_argvs(argc,argv,ii);                                             //  xxx xxxxxx.ext -> "xxx xxxxxx.ext"
         initial_file = zstrdup(pp,"params");
         if (*pp == '\'' || *pp == '"')                                                //  if quotes, remove them
            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),"params",cc+4);
            if (temp) {
               strncatv(temp,200,"/",initial_file,null);                               //  prepend CWD
               initial_file = temp;
            }
         }
         break;
      }
   }

   TX_init(lc,translations_folder);                                                    //  GUI translation file                  26.0
   
   snprintf(UGfile,20,"userguide-%s",lc);                                              //  userguide file                        26.3
   snprintf(filename,200,"%s/%s",get_zdatadir(),UGfile);
   if (! regfile(filename)) snprintf(UGfile,20,"userguide-en");                        //  fallback to english

   if (initial_menu) printf("initial menu: %s \n",initial_menu);
   if (initial_file) printf("initial file: %s \n",initial_file);
   if (initial_album) printf("initial album: %s \n",initial_album);

   zsetfont(application_font);                                                         //  default font for widgets              26.3

   build_widgets();                                                                    //  build window widgets and menus
   mainwin = Mwin;                                                                     //  zfuncs parent = main window

   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);
   viewmode('F');

   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
   targcursor = gdk_cursor_new_for_display(display,GDK_TARGET);                        //  cursor for precise targeting
   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 free memory, report estimated image size limits

   freememory = realmemory();                                                          //  avail. real memory in MB
   printf("free real memory: %.0f MB \n",freememory);

   maxmegapix = freememory / 32;                                                       //  E1 + E3, 16 bytes/pixel each          26.3
   printf("image size limit: %.0f megapixels \n",maxmegapix);                          //  26.3
   wwhh_limit1 = 80000;                                                                //  max image width or height
   wwhh_limit2 = maxmegapix * MEGA;                                                    //  max image width x height

   if (freememory < 4000) {
      yn = zmessageYN(Mwin,TX("Main memory is too small to run Fotocx. \n"
                              "You can try anyway if you wish. \n Continue?"));
      if (! yn) quitxx();
   }

   //  Report SMP (core) count for parallel threads. Hyperthreads and E-cores 
   //  have marginal value, but the kernel scheduler does not discriminate.

   NSMP = get_nprocs_conf();                                                           //  get actual CPU core count             25.2
   NSMP -= 2;                                                                          //  don't use all of them                 26.0
   if (NSMP < 4) NSMP = 4;                                                             //  minimum required for some functions
   if (NSMP > XSMP) NSMP = XSMP;                                                       //  compile time limit
   printf("fotocx SMP threads: %d \n",NSMP);

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

   strcpy(desktopname,"Desktop");                                                      //  default desktop folder name

   snprintf(filename,200,"%s/.config/user-dirs.dirs",getenv("HOME"));                  //  get locale specific name for Desktop
   fid = fopen(filename,"r");                                                          //    for "copy to desktop" function
   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;
         printf("locale desktop name: %s \n",desktopname);
         break;
      }
      fclose(fid);
   }
   
   if (! strmatch(Prelease,Frelease))                                                  //  new install or upgrade                26.0
   {
      printf("*** Fotocx new release: %s \n",Frelease);                                //  log new release
      
      zinitappfiles(1);                                                                //  copy app data files to ~/.fotocx/*

      snprintf(filename,200,"%s/zdialog_geometry",get_zhomedir());                     //  remove zdialog memory files
      remove(filename);                                                                //  (start new files from now) 
      snprintf(filename,200,"%s/zdialog_inputs",get_zhomedir());
      remove(filename);

      snprintf(filename,200,"%s/tags_defined",get_zhomedir());                         //  rename file 'tags_defined' to
      snprintf(newname,200,"%s/keywords_defined",get_zhomedir());                      //    to 'keywords_defined'
      rename(filename,newname);

      showz_textfile("doc","changelog",Mwin);                                          //  show change log
      zmessageACK(Mwin,lost_settings_notify);                                          //  show list of customizable files

      nn = check_outboards(1);                                                         //  check req./opt. outboard programs,
      if (nn) {                                                                        //    add image file types supported
         printf("*** %d required programs not installed, EXIT \n",nn);                 //  (display table of outboards)
         quitxx();
      }

      first_index();                                                                   //  initial user indexing decision

      Prelease = zstrdup(Frelease,"params");                                           //  update installed release
      initial_file = 0;                                                                //  no initial file
      Pindexlev = -1;                                                                  //  no -x index parameter set
   }
   
   else zinitappfiles(0);                                                              //  copy any missing app data files       26.0

   nn = check_outboards(0);                                                            //  check req./opt. outboard programs,
   if (nn) {                                                                           //    add image file types supported
      printf("*** %d required programs not installed, EXIT \n",nn);                    //  (silent - no table display)
      quitxx();
   }
   
   //  restore data and settings from prior sessions

   zdialog_inputs("load");                                                             //  load saved dialog inputs
   zdialog_geometry("load");                                                           //  load saved dialogs position/size
   gallery_memory("load");                                                             //  load recent gallery positions
   KB_shortcuts_load();                                                                //  load KB shortcuts from file

   //  image file index creation or update

   if (Pindexlev < 0) {                                                                //  no -x0 -x1 -x2 command parameter
      if (initial_file) Pindexlev = FMindexlev;                                        //  use file manager index level (settings)
      else Pindexlev = Findexlev;                                                      //  use normal index level (settings)
   }

   index_rebuild(Pindexlev,0);                                                         //  0/1/2 = none/old-index/old+search new
   if (Xindexlev < Pindexlev) m_index(0,0);                                            //  failed, do manual index

   save_params();                                                                      //  save parameters now

   prev_file = curr_file;                                                              //  prev_file = last session curr. file
   curr_file = 0;

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

   if (initial_file)                                                                   //  command line file or folder
   {
      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
         viewmode('G');
      }
      else if (ftype == IMAGE || ftype == RAW || ftype == VIDEO) {                     //  image file
         gallery(initial_file,"init",0);                                               //  initz. gallery from initial file
         gallery(0,"sort",-2);
         f_open(initial_file);
         viewmode('F');
      }
      else {
         if (Ntopfolders) {
            gallery(topfolders[0],"init",0);
            gallery(0,"sort",-2);
            viewmode('G');
         }
      }
   }

   else if (initial_album) {                                                           //  -album albumname
      printf("initial album: %s \n",initial_album);
      snprintf(filename,200,"%s/albums/%s",get_zhomedir(),initial_album);
      album_show(filename);
   }

   else if (Fprev) {                                                                   //  -prev
      if (prev_file && *prev_file == '/') {
         f_open(prev_file);
         viewmode('F');
      }
   }

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

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

   else if (Fblankwindow) {                                                            //  blank window, no gallery
      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 settings

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

   else if (strmatch(startdisplay,"start file")) {                                     //  start with given image file
      f_open(startfile);
      viewmode('F');
   }

   else if (strmatch(startdisplay,"start album")) {
      printf("initial album: %s \n",startalbum);
      album_show(startalbum);
   }

   else if (strmatch(startdisplay,"previous file")) {                                  //  start with previous image file
      err = 1;
      if (prev_file && *prev_file == '/') {
         err = f_open(prev_file,0,0,0,0);                                              //  no diagnostic if missing
         viewmode('F');
      }
      if (err) {                                                                       //  not available, use topfolder[0]       26.0
         if (Ntopfolders) {
            gallery(topfolders[0],"init",0);
            viewmode('G');
         }
         else {
            gallery(getenv("HOME"),"init",0);
            viewmode('G');
         }
      }
   }

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


   else if (strmatch(startdisplay,"newest files"))                                     //  start with newest files gallery
      m_newest_files(0,"file");                                                        //    by file mode date


   else if (Ntopfolders) {                                                             //  start with first topfolders[]         26.0
      gallery(topfolders[0],"init",0);
      viewmode('G');
   }

   else {
      gallery(getenv("HOME"),"init",0);                                                //  last resort, user $HOME folder        26.0
      viewmode('G');
   }

   printf("start gallery: %s \n",navi::galleryname);
   printf("start file: %s \n",curr_file);

   g_timeout_add(100,timerfunc,0);                                                     //  start timer function, 100ms interval

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

   if (initial_menu) {                                                                 //  startup menu on command line
      for (ii = 0; ii < Nmenus; ii++) {                                                //  convert menu name to menu function
         if (! menutab[ii].menu) continue;                                             //  separator, null menu
         if (strmatchcase(initial_menu,menutab[ii].menu)) break;                       //  26.0
         if (strmatchcase(TX(initial_menu),menutab[ii].menu)) break;
         if (strmatchcase(initial_menu,TX(menutab[ii].menu))) break;
      }
      if (ii < Nmenus) menutab[ii].func(0,menutab[ii].arg);                            //  call the menu function
   }

   zmainsleep(0.01);                                                                   //  insure window update

   gtk_main();                                                                         //  start processing window events
   printf("return from gtk_main() \n");
   return 0;
}


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

//  Check that necessary outboard programs are installed.
//  List optional programs for added capabilities.
//  Return 0/1 if all required programs are/not installed.

int check_outboards(int fshow)
{
   zdialog  *zdpop;
   char     command[100];
   int      ii, cc, err;
   int      Fmissing;
   char     *pp1, *pp2;

   typedef struct {
      char     prog[20];
      char     purp[20];
      char     req[4];
      char     avail[4];
      char     pack[32];
      int      *installed;
   }  outboard_t;

//  table of outboard programs

   #define NOB 9
   outboard_t OB[NOB] =
   {  //  program             purpose            REQ     AVAIL    package (debian)          status flag
      {  "exiftool",         "metadata",        "yes",   "no",   "libimage-exiftool-perl",  &Fexiftool        },
      {  "dcraw",            "RAW files",       "yes",   "no",   "dcraw",                   &Fdcraw           },
      {  "addr2line",        "crash report",    "yes",   "no",   "binutils",                &Faddr2line       },
      {  "ffmpeg",           "video files",     "no",    "no",   "ffmpeg",                  &Ffmpeg           },
      {  "dwebp",            ".webp files",     "no",    "no",   "webp",                    &Fwebp            },
      {  "heif-convert",     ".heic files",     "no",    "no",   "libheif-examples",        &Fheif            },
      {  "heif-convert",     ".avif files",     "no",    "no",   "libheif-examples",        &Fheif            },
      {  "opj_decompress",   ".jp2 files",      "no",    "no",   "libopenjp2-tools",        &Fjp2             },
      {  "vlc",              "video play",      "no",    "no",   "vlc",                     &Fvlc             }
   };

   Fmissing = 0;

   for (ii = 0; ii < NOB; ii++)                                                        //  mark available programs and
   {                                                                                   //    count missing required programs
      snprintf(command,100,"which %s >/dev/null 2>&1",OB[ii].prog);
      err = system(command);
      if (err == 0) {
         strcpy(OB[ii].avail,"yes");
         *OB[ii].installed = 1;
         pp1 = OB[ii].purp;
         if (*pp1 == '.') {                                                            //  add to image file types list
            pp2 = strchr(pp1,' ');
            cc = pp2 - pp1;
            strncat(imagefiletypes,pp1,cc+1);                                          //  append ".ext " + null
         }
      }
      else {
         *OB[ii].installed = 0;
         printf("outboard program %s is not installed \n",OB[ii].prog);
         if (OB[ii].req[0] == 'y') Fmissing++;
      }
   }

   if (Fmissing || fshow)                                                              //  if anything missing or 'fshow' arg
   {                                                                                   //    output a status report
      zdpop = popup_report_open("Outboard Programs",Mwin,700,500,0,1,0,"OK",null);
      popup_report_write(zdpop,1,"%-18s %-16s %-5s %-7s %s \n",
                         "program","purpose","REQ","AVAIL","package (debian)");
      for (ii = 0; ii < NOB; ii++) {
         popup_report_write(zdpop,0,"%-18s %-16s %-5s %-7s %s \n",
            OB[ii].prog, OB[ii].purp, OB[ii].req, OB[ii].avail, OB[ii].pack);
      }

      if (Fmissing) {                                                                  //  must install missing required programs
         popup_report_write(zdpop,0,"\n");
         popup_report_write(zdpop,1,"Install following programs using your package manager: \n");
         for (ii = 0; ii < NOB; ii++) {
            if (OB[ii].req[0] == 'y' && OB[ii].avail[0] == 'n')
               popup_report_write(zdpop,0,"%-18s %-16s %-5s %-7s %s \n",
                  OB[ii].prog, OB[ii].purp, OB[ii].req, OB[ii].avail, OB[ii].pack);
         }
      }

      zdialog_wait(zdpop);
   }

   return Fmissing;
}


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

//   Fotocx first index - initial user decision about indexing

void first_index()
{
   zdialog  *zd;
   int      zstat, nn;

   ch       *defer1 = TX("Defer image file indexing:");
   ch       *defer2 = TX("* Fotocx 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");

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

   ch       *info = TX("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. You can change index options later, \n"
                       "using these menus: Tools > Index, Tools > Settings. \n");

   F1_help_topic = TX("FIRST INDEX");

/***
          ______________________________________________________________
         |               Fotocx First Index                             |
         |                                                              |
         | (o) Defer image file indexing:                               |
         |     * Fotocx 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                   |
         |                                                              |
         | 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. You can change index options later,     |
         | using these menus: Tools > Index, Tools > Settings.          |
         |                                                              |
         |                                      [Help] [Proceed] [Quit] |
         |______________________________________________________________|

***/

   zd = zdialog_new("Fotocx First Index",Mwin,TX("Help"),TX("Proceed"),TX("Quit"),null);
   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] button
   {
      topfolders[0] = zstrdup(getenv("HOME"),"top-folders");                           //  default top folder = /home/<user>
      Ntopfolders = 1;

      zdialog_fetch(zd,"index1",nn);                                                   //  "index now" selection
      zdialog_free(zd);
      if (nn) {                                                                        //  do indexing: use standard index policy
         Findexlev = 2;                                                                //  command start: curr. index + search new
         FMindexlev = 1;                                                               //  file manager start: curr. index only
      }
      else Findexlev = FMindexlev = 0;                                                 //  use no index
   }

   else {                                                                              //  [Quit]
      zdialog_free(zd);
      quitxx();
   }

   return;
}


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

//  functions for main window event signals

int delete_event()                                                                     //  main window [x] button
{
   printf("main window delete event \n");
   m_quit(0,0);                                                                        //  returns if user does not quit
   return 1;
}

int destroy_event()                                                                    //  main window destroyed
{
   printf("*** 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, ch *file)                                      //  file drag-drop event
{
   if (! file) return;
   printf("drag-drop file: %s \n",file);
   f_open(file,0,0,1);
   return;
}


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

   Overview of function busy and progress tracking

   int   NFbusy                        processes/threads currently busy                fotocx.h
   zadd_locked(NFbusy,+1)              increase NFbusy
   zadd_locked(NFbusy,-1)              decrease NFbusy

   int   Fescape                       if NFbusy, set Fescape to kill busy function    fotocx.h
                                       busy function must reset when done

   double   progress_goal;             process goal value
   double   progress_done[XSMP];       progress per thread

   initialize:       progress_setgoal(goal)           end goal value
   add progress:     progress_addvalue(value)         increment function progress
                     progress_addvalue(index,value)   increment thread progress
   get progress:     progress_getvalue()              get progress, 0 - goal
                     pct = progress_percent()         get progress, 0 - 100%
   reset to idle:    progress_setgoal(0)              reset, stop progress tracking

   timerfunc               periodic function for top pamel update
   update_Fpanel           update status parameters on F-view top panel
                           show "working" if Fbusy > 0 and progress_goal = 0
                           show "working nn%" if progress_goal > 0

   panelmessage            show short-time message in top panel

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

namespace progress_tracking
{
   double      progress_goal;                                                          //  goal value
   double      progress_done[XSMP];                                                    //  progress per thread
}


//  reset thread counters and set new goal value (0 = inactive)

void progress_setgoal(double goal)
{
   using namespace progress_tracking;

   for (int ii = 0; ii < XSMP; ii++)
      progress_done[ii] = 0;
   progress_goal = goal;
   return;
}


//  accumulate progress value, 0 - goal

void progress_addvalue(double value)
{
   using namespace progress_tracking;

   progress_done[0] += value;
   return;
}


//  increment progress counter per thread

void progress_addvalue(int index, double value)
{
   using namespace progress_tracking;

   progress_done[index] += value;
   return;
}


//  get progress value, 0 - goal

int progress_getvalue()
{
   using namespace progress_tracking;

   double   progress = 0;

   if (progress_goal == 0) return 0;
   for (int ii = 0; ii < XSMP; ii++)
      progress += progress_done[ii];
   return progress;
}


//  get progress value, percent 0-100

int progress_percent()
{
   using namespace progress_tracking;

   double   progress = 0;
   int      pct;

   if (progress_goal == 0) return 0;
   for (int ii = 0; ii < XSMP; ii++)
      progress += progress_done[ii];
   pct = 100.0 * progress / progress_goal;
   return pct;
}


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

//  Periodic function - started by main() after initializations                        //  25.0

int timerfunc(void *)
{
   if (Fshutdown) return 0;                                                            //  shutdown underway

   if (Fpaintrequest && Cdrawin && ! Fpaintlock)                                       //  paint request pending, not blocked
      gtk_widget_queue_draw(Cdrawin);

   if (zd_thread && zd_thread_event) {                                                 //  send dialog event from thread
      zdialog_send_event(zd_thread,zd_thread_event);
      zd_thread_event = 0;
   }

   Fpaint3_main();                                                                     //  update window area from thread

   update_Fpanel();                                                                    //  update top panel information

   return 1;
}


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

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

void update_Fpanel()
{
   ch          paneltext1[300];                                                        //  note global 'paneltext' in fotocx.h
   static ch   priortext1[300] = "";
   ch          *pp, text[100];
   int         ww, hh, scale, bpc;
   float       file_MB;

   if (! main_thread()) zappcrash("panel update from thread");

   if (! Fpanelshow) {                                                                 //  remove panel
      if (! *priortext1) return;
      *priortext1 = 0;
      gtk_widget_hide(Fpanel);
      return;
   }

   if (FGM == 'G') {                                                                   //  gallery panel only has 'working'
      goto update_progress;
      return;
   }

   if (FGM != 'F') return;                                                             //  file panel has all data

   *paneltext1 = *text = 0;

   if (curr_file && Fpxb)
   {
      if (E3pxm) {                                                                     //  edit function engaged
         ww = E3pxm->ww;                                                               //  pano does not update Eww/Ehh
         hh = E3pxm->hh;
      }
      else {
         ww = Fpxb->ww;
         hh = Fpxb->hh;
      }

      bpc = curr_file_bpc;                                                             //  file bits per channel
      snprintf(text,100,"  %dx%dx%d",ww,hh,bpc);                                       //  width x height x bits
      strncatv(paneltext1,300,text,null);

      if (CEF && CEF->Fpreview) strncatv(paneltext1,300," (reduced)",null);            //  (reduced) 45%
      scale = Mscale * 100 + 0.5;
      snprintf(text,100,"  %d%c",scale,'%');
      strncatv(paneltext1,300,text,null);

      if (URS_pos) {                                                                   //  edit undo/redo stack depth
         snprintf(text,100,"  edits: %d",URS_pos);                                     //  edits N
         strncatv(paneltext1,300,text,null);
      }

      if (! URS_pos && ! CEF) {                                                        //  if not modified, file size
         file_MB = 1.0 / MEGA * curr_file_size;                                        //  N.NN MB
         snprintf(text,100,"  %.2f MB",file_MB);
         strncatv(paneltext1,300,text,null);
      }

      if (Fmetamod) strncatv(paneltext1,300,"  ","meta mod",null);                     //  metadata modified
   }

   if (sa_stat == sa_stat_fini) strncatv(paneltext1,300,"  area active",null);

   if (zdialog_count) strncatv(paneltext1,300,"  dialog open",null);

   if (CEF && CEF->Fmods) strncatv(paneltext1,300,"  mod",null);                       //  image is modified

   if (curr_file) {
      pp = strrchr(curr_file,'/');                                                     //  add filename.jpg
      if (pp && Ffullscreen && ! NFbusy) {
         strncpy0(text,pp+1,100);
         strncatv(paneltext1,300,"   ",text,null);
      }
   }

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

//  show "working xx%" if progress_percent() > 0
//  else show "working" in top panel if NFbusy > 0
//  show application text if paneltext (fotocx.h) has text to display

update_progress:

   static GtkWidget  *busylabel = 0, *donelabel = 0, *textlabel = 0;
   static ch         busytext[] = "<span font=\"bold\" fgcolor=\"red\" > working </span>";
   static ch         donetext[] = "<span font=\"bold\" fgcolor=\"red\" > working xx% </span>";
   static ch         *doneposn = 0;
   GtkWidget         *FGpanel;
   int               pct;
   ch                nn[4];

   if (! main_thread()) return;                                                        //  thread caller
   if (! Fpanelshow) return;                                                           //  no panel

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

   pct = progress_percent();                                                           //  add "working xx%" to top panel
   if (pct > 0) {
      if (! doneposn) doneposn = (ch *) strstr(donetext,"xx%");                        //  position to insert "xx%" in donetext
      if (pct > 99) pct = 99;
      snprintf(nn,4,"%02d",pct);
      memcpy(doneposn,nn,2);
      if (! donelabel) {
         donelabel = gtk_label_new(0);                                                 //  create label if not already
         gtk_box_pack_start(GTK_BOX(FGpanel),donelabel,0,0,5);
      }
      gtk_label_set_markup(GTK_LABEL(donelabel),donetext);
   }
   else {
      if (donelabel) gtk_widget_destroy(donelabel);
      donelabel = 0;
   }

   if (NFbusy && pct == 0) {                                                           //  add "working" to top panel
      if (! busylabel) {
         busylabel = gtk_label_new(0);
         gtk_box_pack_start(GTK_BOX(FGpanel),busylabel,0,0,5);
      }
      gtk_label_set_markup(GTK_LABEL(busylabel),busytext);
   }
   else {
      if (busylabel) gtk_widget_destroy(busylabel);
      busylabel = 0;
   }

   if (*paneltext) {                                                                   //  add application message               25.1
      if (! textlabel) {
         textlabel = gtk_label_new(0);
         gtk_box_pack_start(GTK_BOX(FGpanel),textlabel,0,0,8);
      }
      gtk_label_set_markup(GTK_LABEL(textlabel),paneltext);
   }
   else {
      if (textlabel) gtk_widget_destroy(textlabel);
      textlabel = 0;
   }

   gtk_widget_show_all(FGpanel);
   zmainloop();                                                                        //  25.1
   return;
}


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

//  add an application message to the Fotocx top panel bar
//       panelmessage(red, secs, format, ...)
//  red: 1 if message is bold red, 0 for plain text
//  secs: time to keep message on screen, 0 = forever until cancelled
//  format: variable format with optional args to be inserted into format

void panelmessage(int red, int secs, ch *format, ...)                                  //  25.1
{
   int panelmessage_timeout(void *);

   va_list  arglist;
   ch       message[200], messagebold[200];

   if (! main_thread()) zappcrash("illegal call from thread");

   va_start(arglist,format);
   vsnprintf(message,200,format,arglist);
   va_end(arglist);

   if (red) {
      snprintf(messagebold,200,"<span font=\"bold\" color=\"red\">%s</span>",message);
      strncpy0(message,messagebold,200);
   }

   strncpy0(paneltext,message,200);

   if (secs)
      g_timeout_add_seconds(secs,(GSourceFunc) panelmessage_timeout,0);

   return;
}

//  clear the message when time expires

int panelmessage_timeout(void *)
{
   *paneltext = 0;
   return 0;
}

//  unconditionally clear the message (use secs = 0 for clarity)

void panelmessage(int secs)
{
   *paneltext = 0;
   return;
}


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

//  block/unblock for mutex functions
//  block for calling function if not already blocked
//  block call: if (Fblock("funcname")) return;
//  unblock call:  Fblock(0);

int Fblock(ch *funcname)
{
   static int  blocked = 0;
   static ch   Bfunction[20] = "no function";

   if (funcname) printf("Fblock %s \n",funcname);                                      //  25.1
   else printf("Fblock(0) \n");

   if (funcname)
   {
      if (blocked) {                                                                   //  already blocked
         zmessageACK(Mwin,TX("blocking function: %s"),Bfunction);                      //  caller must stop
         return 1;
      }

      blocked++;
      strncpy0(Bfunction,funcname,20);                                                 //  block for caller
      return 0;
   }

   if (! blocked) printf("Fblock(0): not blocked \n");                                 //  not blocked (OK)
   blocked = 0;                                                                        //  unblock
   return 0;
}


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

//  block main window painting during image updates from threads
//  Fpaintlock: if > 0 painting is blocked
//  d = 1: block painting
//  d = -1: unblock

int paintlock(int d)
{
   zadd_locked(Fpaintlock,d);                                                          //  increment or decrement
   if (Fpaintlock < 0) zappcrash("Fpaintlock: %d %d",d,Fpaintlock);                    //  fatal bug
   return 0;
}


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

//  GTK3 "draw" function for F 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            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 (Fview360) return 1;

   if (! Cdrawin || ! gdkwin || ! Cstate || ! Cstate->fpxb)                            //  no image
      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
      pdww = Dww;
      pdhh = Dhh;
   }

   if (Fpaintrequest && ! Fpaintlock) {
      if (FGM == 'F' && (E0pxm || E3pxm)) {                                            //  edit active, refresh image
         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);                                                       //  0.05 secs. typical
         Cstate->fpxb = pxb1;
      }
      Fpaintrequest = 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
   else Fsmallimage = 0;

   if (Cstate->fzoom == -1) {                                                          //  fit to window A
      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
      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 + 0.5;                                                   //  scaled image size for window
   mhh = fhh * Cstate->mscale + 0.5;

   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
   }

   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    (0.02 secs. typical)

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

      magx = magy = 2;                                                                 //  use 2x always

      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 width,
   else Cstate->dorgx = 0;                                                             //    center image in window
   if (dhh < Dhh) Cstate->dorgy = 0.5 * (Dhh - dhh);
   else Cstate->dorgy = 0;

   if (strmatch(ImagePosn,"left")) Cstate->dorgx = 0;                                  //  shift image to left margin
   if (strmatch(ImagePosn,"right")) Cstate->dorgx = Dww-1 - dww;                       //  shict 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 (BGpixbuf) g_object_unref(BGpixbuf);                                             //  0.005 secs. typical
   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
   if (! pixbuf) {
      zmessageACK(Mwin,TX("pixbuf failure, image too large (?)"));
      exit(0);
   }
   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 secs, 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[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 (zd_RGB_hist) m_RGB_hist(0,0);                                                //  update brightness histogram
   }

   return 1;
}


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

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

void Fpaintnow()
{
   if (! pthread_equal(pthread_self(),tid_main))
      zappcrash("Fpaintnow() called from thread");

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

   Fpaintrequest = 1;                                                                  //  request repaint of changed image
   gtk_widget_queue_draw(Cdrawin);
   while (Fpaintrequest) zmainsleep(0.01);                                             //  necessary
   return;
}


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

void Fpaint2()
{
   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.01);

   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 timerfunc() each timer cycle
//  if pending, paint    else return

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) {
      printf("*** 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 printf("*** 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;
}


//  update Mpxb and Fpxb from E3 image updated via mouse painting
//  then update corresponding portion of drawing window

void Fpaintmouse(int mxposn, int myposn, int radius)
{
   int      px, py, ww, hh;

   px = mxposn - radius;
   py = myposn - radius;
   ww = radius + radius;
   hh = ww;

   if (px < 0) { ww += px; px = 0; }
   if (py < 0) { hh += py; py = 0; }
   if (px + ww > Eww) ww = Eww - px;
   if (py + hh > Ehh) hh = Ehh - py;

   Fpaint3(px,py,ww,hh,0);
   return;
}


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

//  F 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, thresh;
   static int     bdtime = 0, butime = 0;
   static int     dragstart = 0, mdragx0, mdragy0;

   #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 (button == 1 && KBaltkey) button = 3;                                            //  left butt + ALT key >> right butt

   thresh = 50 / Cstate->mscale;                                                       //  25.0
   if (Mxposn > 0 && Mxposn < thresh && Myposn > 0 && Myposn < thresh)                 //  if mouse near top left corner
      meta_popup_title(1);                                                             //    popup title and description
   else meta_popup_title(0);

   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 && time - bdtime > 200)                                            //  drag underway
      {
         if (Fmousevents) {
            if (Mbutton == 1) poptext_mouse("left drag",30,0,0,0.5);
            if (Mbutton == 3) poptext_mouse("right drag",30,0,0,0.5);
         }
         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;                                    //  no drag underway
   }

   if (type == GDK_BUTTON_RELEASE)                                                     //  button up
   {
      dragstart = 0;                                                                   //  reset drag status
      Mxclick = Myclick = 0;                                                           //  reset click and drag status
      butime = time;                                                                   //  time of button up

      if (butime - bdtime < 500)                                                       //  < 0.5 secs down, ignore motion
      {
         if (Mbutton == 1)
         if (Mbutton == 1) LMclick++;                                                  //  left mouse click
         if (Mbutton == 3) RMclick++;                                                  //  right mouse click
         if (Fmousevents) {
            if (KBshiftkey) {
               if (LMclick) poptext_mouse("shift + L-click",20,0,0,0.5);
               if (RMclick) poptext_mouse("shift + R-click",20,0,0,0.5);
            }
            else {
               if (LMclick) poptext_mouse("L-click",20,0,0,0.5);
               if (RMclick) poptext_mouse("R-click",20,0,0,0.5);
            }
         }

         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);
         }
      }
      Mxdrag = Mydrag = Mdrag = 0;                                                     //  forget buttons and drag
      Mxdown = Mydown = Mbutton = 0;
   }

   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 (! Fmousemain) return;                                                           //  curr. function handles mouse

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

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

   if (Mxdrag || Mydrag)                                                               //  drag = scroll window 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 >= Eww) xpos2 = Eww-1;
      if (ypos2 >= Ehh) ypos2 = Ehh-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 *, ch *zoom)
{
   int         fww, fhh;
   float       scalew, scaleh, fitscale, fzoom2;
   float       Czoom, Gzoom = 0, pixels;
   int64       z64 = 1;

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

   if (E3pxm) {                                                                        //  edit image active
      fww = Eww;                                                                       //  1x image size
      fhh = Ehh;
   }
   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 * z64 * fww * fhh;                                        //  new image size in pixels
      if (pixels > wwhh_limit2) Gzoom = Czoom;                                         //  limit size
   }

   if (strmatch(zoom,"out")) {                                                         //  zoom out - make image smaller
      Gzoom = fzoom2 / zoomratio;
      if (fitscale <= 1 && Gzoom < fitscale) Gzoom = -1;                               //  large image: limit = fit window
      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
   }

   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 program

void KBevent(GdkEventKey *event)
{
   KBpress(0,event,0);
   return;
}


//  keyboard event functions
//  GDK key symbols: /usr/include/gtk-3.0/gdk/gdkkeysyms.h
//  zdialog completion button KB shortcuts removed

namespace crop_names { 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 *, GdkEventKey *event, void *)                                   //  keyboard key was pressed
{
   int         ii, jj, cc;
   ch          shortkey[20] = "";
   ch          *action = 0;
   ch          *pp, *file = 0, *qfile;
   static int  Fshowmenu = 1;

   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 (KBcontrolkey && KBkey == GDK_KEY_plus) KBcontrolkey = 0;                        //  treat Ctrl/+ and Ctrl/-
   if (KBcontrolkey && KBkey == GDK_KEY_minus) KBcontrolkey = 0;                       //    same as [+] and [-]

   if (KBkey == GDK_KEY_F1) {                                                          //  F1  >>  user guide, current topic
      showz_docfile(Mwin,UGfile,F1_help_topic);
      return 1;
   }

   if (Fslideshow) {                                                                   //  slide show active
      ss_KBfunc(KBkey);                                                                //  pass key to slide show
      return 1;
   }

   if (Fview360) {                                                                     //  view360 active
      view360::KB_func(KBkey);                                                         //  pass key to view360
      return 1;
   }

   if (KBkey == GDK_KEY_Escape) {                                                      //  [esc] key pressed
      if (NFbusy) Fescape = 1;                                                         //  tell pending function to quit
      else if (CEF && zdialog_valid(CEF->zd)) {
         CEF->zd->zstat = -2;                                                          //  zdialog [x] status                    26.0
         zdialog_send_event(CEF->zd,"zstat");
      }
      else if (Ffullscreen) win_unfullscreen();                                        //  full-screen mode, shrink
      else if (FGM != 'F') viewmode('F');                                              //  file view mode if not already
      else m_quit(0,0);                                                                //  quit fotocx
      return 1;                                                                        //  ESC also terminates dialogs
   }

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

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

   if (KBkey == GDK_KEY_p)                                                             //  P key, play video file
   {                                                                                   //  (OK to also use P in shortcuts)
      if (! curr_file) return 1;

      if (image_file_type(curr_file) == VIDEO)
      {
         file = zescape_quotes(curr_file);
         cc = strlen(file) + 4;
         qfile = (ch *) zmalloc(cc,"KBpress");
         strncatv(qfile,cc,"\"",file,"\"",null);
         zshell("log ack",video_command,qfile);                                        //  video-command "curr_file"
         zfree(file);
         zfree(qfile);
         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_KP_Left)       KBkey = GDK_KEY_Left;                           //  map keypad keys
   if (KBkey == GDK_KEY_KP_Right)      KBkey = GDK_KEY_Right;
   if (KBkey == GDK_KEY_KP_Up)         KBkey = GDK_KEY_Up;
   if (KBkey == GDK_KEY_KP_Down)       KBkey = GDK_KEY_Down;
   if (KBkey == GDK_KEY_KP_Home)       KBkey = GDK_KEY_Home;
   if (KBkey == GDK_KEY_KP_End)        KBkey = GDK_KEY_End;
   if (KBkey == GDK_KEY_KP_Page_Up)    KBkey = GDK_KEY_Page_Up;
   if (KBkey == GDK_KEY_KP_Page_Down)  KBkey = GDK_KEY_Page_Down;

   if (KBkey == GDK_KEY_KP_Add)        KBkey = GDK_KEY_plus;                           //  numeric keypad works the same
   if (KBkey == GDK_KEY_KP_Subtract)   KBkey = GDK_KEY_plus;                           //    with/without num-lock
   if (KBkey == GDK_KEY_KP_7)          KBkey = GDK_KEY_Home;
   if (KBkey == GDK_KEY_KP_8)          KBkey = GDK_KEY_Up;
   if (KBkey == GDK_KEY_KP_9)          KBkey = GDK_KEY_Page_Up;
   if (KBkey == GDK_KEY_KP_4)          KBkey = GDK_KEY_Left;
   if (KBkey == GDK_KEY_KP_6)          KBkey = GDK_KEY_Right;
   if (KBkey == GDK_KEY_KP_1)          KBkey = GDK_KEY_End;
   if (KBkey == GDK_KEY_KP_2)          KBkey = GDK_KEY_Down;
   if (KBkey == GDK_KEY_KP_3)          KBkey = GDK_KEY_Page_Down;

   if (KBkey == GDK_KEY_Delete) action = "Delete";                                     //  reserved shortcuts
   if (KBkey == GDK_KEY_Left) action = "Left";
   if (KBkey == GDK_KEY_Right) action = "Right";
   if (KBkey == GDK_KEY_Up) action = "Up";
   if (KBkey == GDK_KEY_Down) action = "Down";
   if (KBkey == GDK_KEY_Home) action = "Home";
   if (KBkey == GDK_KEY_End) action = "End";
   if (KBkey == GDK_KEY_Page_Up) action = "Page_Up";
   if (KBkey == GDK_KEY_Page_Down) action = "Page_Down";
   if (KBkey == GDK_KEY_plus) action = "Zoom+";
   if (KBkey == GDK_KEY_minus) action = "Zoom-";
   if (KBkey == GDK_KEY_z) action = "toggle-zoom";

   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) {                                                                     //  26.0
      zmessage_post(Mwin,"parent",3,TX("shortcut not found: %s"),shortkey);
      return 1;
   }

   if (FGM == 'G') {                                                                   //  G view mode
      navi::KBaction((ch *) action);                                                   //  pass KB action to gallery
      return 1;
   }

   if (strmatch(action,"Show Hidden")) return 1;                                       //  hidden fles - only in G view

   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_crop) {                                               //  crop image active, pass KB key
      crop_names::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,"Up")) {                                                        //  up arrow - previous image
      m_prev(0,"jump");                                                                //  (last version, jump folder)
      return 1;
   }

   if (strmatch(action,"Down")) {                                                      //  down arrow - next image
      m_next(0,"jump");                                                                //  (last version, jump folder)
      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
      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(TX(action),KBsftab[jj].menu)) break;                            //  26.0

   if (jj == Nkbsf || ! KBsftab[jj].func) {                                            //  26.0
      zmessage_post(Mwin,"parent",3,TX("shortcut function not found: %s"),action);
      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 or without menu and panel)

void win_fullscreen(int hidemenu)
{
   if (FGM == '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

void set_mwin_title()
{
   GTYPE       gtype;
   int         cc, fposn, Gfiles, Gimages;
   ch          *pp, titlebar[250];
   ch          pdate[20];
   ch          fname[100], gname[100], ffolder[100];
   xxrec_t     *xxrec;

   if (FGM != 'F') return;

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

   pp = (ch *) 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,"...");
   }

   Gfiles = navi::Gfiles;                                                              //  total gallery files (incl. folders)
   Gimages = navi::Gimages;                                                            //  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 - Gfiles + Gimages;                                            //  position among images, 1-based
   }

   xxrec = get_xxrec(curr_file);                                                       //  photo date/time
   if (xxrec && *xxrec->pdate)
      strcpy(pdate,xxrec->pdate);
   else strcpy(pdate,"undated");

   gtype = navi::gallerytype;

   if (gtype == FOLDER)                                                                //  gallery name = folder
      snprintf(titlebar,250,"%s   %d/%d  %s  %s  %s",
               Frelease,fposn,Gimages,ffolder,fname,pdate);
   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,"%s   %s  %d/%d  %s  %s  %s",                           //  window title bar
                  Frelease,gname,fposn,Gimages,ffolder,fname,pdate);
      else
         snprintf(titlebar,250,"%s   %s  (*)/%d  %s  %s  %s",                          //  image not in gallery
                  Frelease,gname,Gimages,ffolder,fname,pdate);
   }

   gtk_window_set_title(MWIN,titlebar);
   return;
}


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

//  draw a pixel using foreground color.
//  px, py are image space.
//  mode: 0 = scaled, 1 = thin pixel, 2 = fat pixel
//  scaled: thin pixel if zoom <= 1, fat pixel if zoom > 1

void draw_pixel(int px, int py, cairo_t *cr, int mode)
{
   int               qx, qy;
   static int        pqx, pqy;
   static uint8      pixel[27];                                                        //  3x3 block of RGB pixels
   static PIXBUF     *pixbuf1 = 0, *pixbuf9 = 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
      pixbuf9 = gdk_pixbuf_new_from_data(pixel,GDKRGB,0,8,3,3,9,0,0);                  //  3x3 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 (mode == 1 || (mode == 0 && Mscale <= 1)) {                                      //  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 3x3 fat pixels
      pixel[0] = pixel[3] = pixel[6] = pixel[9] = pixel[12] = pixel[15]
               = pixel[18] = pixel[21] = pixel[24] = LINE_COLOR[0];
      pixel[1] = pixel[4] = pixel[7] = pixel[10] = pixel[13] = pixel[16]
               = pixel[19] = pixel[22] = pixel[25] = LINE_COLOR[1];
      pixel[2] = pixel[5] = pixel[8] = pixel[11] = pixel[14] = pixel[17]
               = pixel[20] = pixel[23] = pixel[26] = LINE_COLOR[2];

      gdk_cairo_set_source_pixbuf(cr,pixbuf9,qx+Dorgx-1,qy+Dorgy-1);
      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;
   int            qx, qy, ww, hh, f;

   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 < 0 || qx > Mpxb->ww-1) return;                                              //  pixel outside scaled image
   if (qy < 0 || qy > Mpxb->hh-1) return;

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

   if (Mscale <= 1) f = 0;
   else f = 1;

   qx = qx - 1 - f;                                                                    //  erase fat pixel if Mscale > 1
   qy = qy - 1 - f;
   ww = 3 + f;
   hh = 3 + f;

   if (qx < Morgx) qx = Morgx;
   if (qy < Morgy) qy = Morgy;
   if (qx + ww > Mpxb->ww) ww = Mpxb->ww - qx;
   if (qy + hh > Mpxb->hh) hh = Mpxb->hh - qy;

   pixbuf = gdk_pixbuf_new_subpixbuf(Mpxb->pixbuf,qx,qy,ww,hh);                        //  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/3/4 = black/white/both/dash

void draw_line(int x1, int y1, int x2, int y2, int type, cairo_t *cr)
{
   float       px1, py1, px2, py2;
   float       R, px3, py3, px4, py4;
   double      dashes[2] = { 8, 8 };
   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;

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

   cairo_set_line_width(cr,2);

   if (type == 1) {
      cairo_set_source_rgb(cr,0,0,0);                                                  //  black line
      cairo_set_dash(cr,dashes,0,0);
      cairo_move_to(cr,px1,py1);
      cairo_line_to(cr,px2,py2);
      cairo_stroke(cr);
   }

   if (type == 2) {
      cairo_set_source_rgb(cr,1,1,1);                                                  //  white line
      cairo_set_dash(cr,dashes,0,0);
      cairo_move_to(cr,px1,py1);
      cairo_line_to(cr,px2,py2);
      cairo_stroke(cr);
   }

   if (type == 3) {
      cairo_set_source_rgb(cr,0,0,0);                                                  //  black-white adjacent lines
      cairo_set_dash(cr,dashes,0,0);
      cairo_move_to(cr,px1,py1);
      cairo_line_to(cr,px2,py2);
      cairo_stroke(cr);

      R = sqrtf((px1-px2)*(px1-px2) + (py1-py2)*(py1-py2));                            //  offset = 2 pixels at right angle
      px3 = px1 - 2 * (py2-py1) / R;
      py3 = py1 + 2 * (px2-px1) / R;
      px4 = px3 + (px2-px1);
      py4 = py3 + (py2-py1);

      cairo_set_source_rgb(cr,1,1,1);
      cairo_set_dash(cr,dashes,0,0);
      cairo_move_to(cr,px3,py3);
      cairo_line_to(cr,px4,py4);
      cairo_stroke(cr);
   }

   if (type == 4) {
      cairo_set_source_rgb(cr,1,1,1);                                                  //  dash line
      cairo_set_dash(cr,dashes,2,0);
      cairo_move_to(cr,px1,py1);
      cairo_line_to(cr,px2,py2);
      cairo_stroke(cr);

      cairo_set_source_rgb(cr,0,0,0);                                                  //  compliment dash line in gaps
      cairo_set_dash(cr,dashes,2,8);                                                   //    of first dash line
      cairo_move_to(cr,px1,py1);
      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
//  line types defined in toplines[*].type

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      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[GON]) return;                                                    //  grid lines off

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

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

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

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

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

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

   if (stepx)
      for (px = startx1; px < endx; px += stepx) {
         cairo_move_to(cr,px,starty);
         cairo_line_to(cr,px,endy);
      }

   if (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 (stepx)
      for (px = startx1+1; px < endx+1; px += stepx) {
         cairo_move_to(cr,px,starty);
         cairo_line_to(cr,px,endy);
      }

   if (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, ch *text, ch *font)
{
   if (Ntoptext == maxtoptext) {
      printf("*** maxtoptext exceeded \n");
      return;
   }

   int ii = Ntoptext++;
   toptext[ii].ID = ID;
   toptext[ii].px = px;
   toptext[ii].py = py;
   toptext[ii].text = zstrdup(text,"toptext");
   toptext[ii].font = zstrdup(font,"toptext");

   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) {
         zfree(toptext[ii].text);
         zfree(toptext[ii].font);
         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, ch *text, ch *font, cairo_t *cr)
{
   static PangoFontDescription   *pangofont = 0;
   static PangoLayout            *pangolayout = 0;
   static ch                     priorfont[60] = "";
   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,60);
      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+10,hh+10);                                              //  padding
   cairo_fill(cr);

   cairo_move_to(cr,px+5,py+5);                                                        //  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) {
      printf("*** 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;
   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);
      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

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

   if (Ferase) {
      gdk_window_thaw_updates(gdkwin);
      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);
   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;
}


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

//  calculate a match value for two sets of RGB values                                 //  replace former macros
//  0.0 = maximum unmatch (black/white pixels)
//  1.0 = perfect match (same RGB values)

float RGBmatch(uint8 r1, uint8 g1, uint8 b1, uint8 r2, uint8 g2, uint8 b2)
{
   int      dr, dg, db;
   float    dist;
   float    F768 = 1.0 / 768;

   dr = abs(r1 - r2);                                                                  //  0-255
   dg = abs(g1 - g2);
   db = abs(b1 - b2);

   dist = F768 * (dr + dg + db);                                                       //  0 - 1.0
   dist = sqrtf(dist);
   return 1.0 - dist;
}


float RGBmatch(float r1, float g1, float b1, float r2, float g2, float b2)
{
   float    dr, dg, db;
   float    dist;
   float    F768 = 1.0 / 768;

   dr = fabsf(r1 - r2);                                                                //  0-255
   dg = fabsf(g1 - g2);
   db = fabsf(b1 - b2);

   dist = F768 * (dr + dg + db);                                                       //  0 - 1.0
   dist = sqrtf(dist);
   return 1.0 - dist;
}


float RGBmatch(float r1, float g1, float b1, uint8 r2, uint8 g2, uint8 b2)
{
   float    dr, dg, db;
   float    dist;
   float    F768 = 1.0 / 768;

   dr = fabsf(r1 - r2);                                                                //  0-255
   dg = fabsf(g1 - g2);
   db = fabsf(b1 - b2);

   dist = F768 * (dr + dg + db);                                                       //  0 - 1.0
   dist = sqrtf(dist);
   return 1.0 - dist;
}


//  calculate a match value for two RGB pixels
//  0.0 = maximum unmatch (black/white pixels)
//  1.0 = perfect match (same RGB values)

float PIXmatch(uint8 *pix1, uint8 *pix2)
{
   return RGBmatch(pix1[0],pix1[1],pix1[2],pix2[0],pix2[1],pix2[2]);
}


float PIXmatch(float *pix1, float *pix2)
{
   return RGBmatch(pix1[0],pix1[1],pix1[2],pix2[0],pix2[1],pix2[2]);
}


float PIXmatch(float *pix1, uint8 *pix2)
{
   return RGBmatch(pix1[0],pix1[1],pix1[2],pix2[0],pix2[1],pix2[2]);
}


//  insure RGB values are within limits 0-255
//  overflows are fixed, preserving RGB ratios
//  return: 0: OK  1: < 0  2: > 255  3: NAN or INF

int RGBfix(float &R, float &G, float &B)
{
   float    max, F;
   int      N1 = 0, N2 = 0, N3 = 0;

   if (isnan(R)) { N3++; R = 0; }                                                      //  26.0
   if (isnan(G)) { N3++; G = 0; }
   if (isnan(B)) { N3++; B = 0; }

   if (isinf(R)) { N3++; R = 0; }                                                      //  26.0
   if (isinf(G)) { N3++; G = 0; }
   if (isinf(B)) { N3++; B = 0; }

   if (R < 0) { N1++; R = 0; }
   if (G < 0) { N1++; G = 0; }
   if (B < 0) { N1++; B = 0; }

   max = R;
   if (G > max) max = G;
   if (B > max) max = B;
   if (max > 255.4) {                                                                  //  must round to <= 255     //  26.0
      F = 255.0 / max;
      R *= F;
      G *= F;
      B *= F;
      N2++;
   }
   
   if (N1) return 1;
   if (N2) return 2;
   if (N3) return 3;

   return 0;
}


int RGBfix(int &R, int &G, int &B)
{
   float    max, F;
   int      N1 = 0, N2 = 0;

   if (R < 0) { N1++; R = 0; }                                                         //  26.0
   if (G < 0) { N1++; G = 0; }
   if (B < 0) { N1++; B = 0; }

   max = R;
   if (G > max) max = G;
   if (B > max) max = B;
   if (max > 255) {
      F = 255 / max;
      R *= F;
      G *= F;
      B *= F;
      N2++;
   }

   if (N1) return 1;
   if (N2) return 2;

   return 0;
}


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

   edit transaction management

      edit_wwhhOK()                 check image size within limits
      edit_setup()                  get E0 if none, E0 > E1 > E3
      edit_cancel()                 free (E1 E3 ER)
      edit_fullsize()               free (E1 E3) E0 > E1 > E3
      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

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


//  check that image size is within limits
//  used by edit_setup, m_rescale, m_mashup

int edit_wwhhOK(int ww, int hh)
{
   if (ww < 10 || hh < 10) {
      zmessageACK(Mwin,TX("image is too small to edit: %d x %d"),ww,hh);
      return 0;
   }

   if (ww < wwhh_limit1 && hh < wwhh_limit1 && ww * hh < wwhh_limit2)
      return 1;

   zmessageACK(Mwin,TX("image is too large to edit: %d x %d"),ww,hh);
   return 0;
}


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

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

   if (! curr_file || ! Fpxb) {
      zmessageACK(Mwin,TX("no current file"));
      return 0;
   }

   viewmode('F');                                                                      //  insure file view mode

   ftype = image_file_type(curr_file);
   if (ftype != IMAGE && ftype != RAW) {                                               //  file type not editable
      zmessageACK(Mwin,TX("File type cannot be edited"));
      return 0;
   }

   if (! edit_wwhhOK(Fpxb->ww,Fpxb->hh)) return 0;                                     //  image too big

   if (CEF) {
      zmessageACK(Mwin,TX("finish or cancel pending edit"));
      return 0;
   }

   if (URS_pos > maxedits-2) {                                                         //  undo/redo stack capacity reached
      zmessageACK(Mwin,TX("Too many edits, please save image"));
      return 0;
   }

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

   if (Fblock(EF.menuname)) return 0;                                                  //  block for edit function

   sa_validate();                                                                      //  delete select area if not valid

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

   if (EF.Farea == 1 && sa_stat) {                                                     //  select area kept, not used
      yn = zmessageYN(Mwin,TX("Select area will be ignored. \n"
                              "Continue?"));
      if (! yn) { Fblock(0); return 0; }
   }

   if (EF.Farea == 2 && sa_stat && sa_stat != sa_stat_fini)                            //  area usable, exists, not finished/enabled
   {
      if (sa_stat == sa_stat_disab)                                                    //  area finished and disabled
         yn = zmessageYN(Mwin,TX("Select area disabled, Continue?"));
      else
         yn = zmessageYN(Mwin,TX("Select area not finished, Continue?"));            //  area not finished

      if (! yn) { Fblock(0); return 0; }
   }

   zmainsleep(0.01);

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

   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 == sa_stat_fini)                                       //  not if select area active
      Fpreview = 0;

   if (E0pxm->ww < 1.5 * Dww && E0pxm->hh < 1.5 * Dhh)                                 //  image < 1.5 * window, no preview
      Fpreview = 0;

   if (Fpreview) {
      if (Fpxb->ww * Dhh > Fpxb->hh * Dww) {                                           //  use preview image 1.5 * window size
         rww = 1.5 * Dww;
         if (rww < 1500) rww = 1500;                                                   //  at least 1500 on one side
         rhh = 1.0 * rww * Fpxb->hh / Fpxb->ww + 0.5;
      }
      else {
         rhh = 1.5 * Dhh;
         if (rhh < 1500) rhh = 1500;
         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

   Eww = E3pxm->ww;                                                                    //  image dimensions
   Ehh = E3pxm->hh;

   CEF = &EF;                                                                          //  set current edit function
   CEF->Fmods = 0;                                                                     //  image not modified yet
   CEF->Fpreview = Fpreview;
   CEF->edit_hist[0] = 0;                                                              //  no edit history

   if (CEF->threadfunc) thread_start(CEF->threadfunc,0);                               //  start thread func if any

   Fpaintnow();                                                                        //  update image synchronous
   Fescape = 0;                                                                        //  reset kill switch
   return 1;
}


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

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

void edit_cancel(int keep)
{
   if (! CEF) return;
   Fescape = 1;                                                                        //  kill worker threads
   if (CEF->threadfunc) thread_stop();                                                 //  stop thread and wait for exit
   Fescape = 0;                                                                        //  reset

   PXM_free(E1pxm);                                                                    //  free E1 E3 ER E8 E9 if present
   PXM_free(E3pxm);
   PXM_free(ERpxm);
   PXM_free(E8pxm);
   PXM_free(E9pxm);

   Fpaintnow();                                                                        //  update image synchronous              25.0

   if (! keep) {
      if (CEF->zd == zd_thread) zd_thread = 0;                                         //  thread -> zdialog event sender
      if (CEF->zd) zdialog_free(CEF->zd);                                              //  kill dialog
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                                  //  if my mouse, free mouse
      if (CEF->sd) zfree(CEF->sd);                                                     //  free spl curve data                   25.1
   }

   CEF = 0;                                                                            //  no current edit func
   Eww = Ehh = 0;

   if (zd_paintedits) zdialog_send_event(zd_paintedits,"cancel");                      //  kill paint_edits() if active

   Fblock(0);                                                                          //  edit_setup() blocks
   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) return;
   if (! CEF->Fpreview) return;                                                        //  FprevReq ignored if small image
   if (CEF->threadfunc) thread_wait();                                                 //  wait for thread idle
   viewmode('F');                                                                      //  bugfix                                25.0
   PXM_free(E1pxm);                                                                    //  free preview pixmaps
   PXM_free(E3pxm);
   E1pxm = PXM_copy(E0pxm);                                                            //  E0 >> E1, full size image
   E3pxm = PXM_copy(E1pxm);                                                            //  E1 >> E3
   Eww = E3pxm->ww;                                                                    //  image dimensions
   Ehh = E3pxm->hh;
   PXB_free(Cstate->fpxb);
   Cstate->fpxb = PXM_PXB_copy(E3pxm);                                                 //  update Fpxb from E3
   Fzoom = 0;
   CEF->Fpreview = 0;
   viewmode('F');                                                                      //  insure F-view
   return;
}


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

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

void edit_done(int keep)
{
   if (! CEF) return;
   if (CEF->threadfunc) {
      thread_wait();                                                                   //  wait thread idle
      thread_stop();                                                                   //  stop thread and wait for exit
   }

   if (CEF->Fmods) {                                                                   //  image was modified
      PXM_audit(E3pxm);                                                                //  sanity check RGB values
      PXM_free(E0pxm);
      E0pxm = E3pxm;                                                                   //  E3 updated image >> E0
      E3pxm = 0;
      PXM_free(E1pxm);                                                                 //  free E1 ER E8 E9 if present
      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);
   }

   Fpaintnow();                                                                        //  update image synchronous              25.0

   if (! keep) {
      if (CEF->zd == zd_thread) zd_thread = 0;                                         //  thread -> zdialog event sender
      if (CEF->zd) zdialog_free(CEF->zd);                                              //  kill dialog
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                                  //  if my mouse, free mouse
      if (CEF->sd) zfree(CEF->sd);                                                     //  free spl curve data                   25.1
   }

   CEF = 0;                                                                            //  no current edit func
   Eww = Ehh = 0;
   Fescape = 0;                                                                        //  reset Fescape kill switch

   if (zd_paintedits) zdialog_send_event(zd_paintedits,"done");                        //  kill paint_edits() if active

   Fblock(0);                                                                          //  edit_setup() blocks
   return;
}


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

//  format edit history data for current edit function
//  example: edit_addhist("parm1: %d parm2: %.3f",parm1,parm2);

void edit_addhist(ch *format, ...)
{
   va_list     arglist;
   ch          edit_hist[edithistXcc];

   if (! CEF) zappcrash("edit_addhist() no CEF");

   va_start(arglist,format);
   vsnprintf(edit_hist,edithistXcc,format,arglist);
   strncpy0(CEF->edit_hist,edit_hist,edithistXcc);
   return;
}


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

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

void edit_undo()                                                                       //  E3 >> ER, E1 >> E3
{
   if (! CEF) return;
   if (CEF->threadfunc) thread_wait();                                                 //  wait for thread to become idle
   if (! CEF->Fmods) return;                                                           //  not modified
   PXM_free(ERpxm);                                                                    //  E3 >> redo copy
   ERpxm = E3pxm;
   E3pxm = PXM_copy(E1pxm);                                                            //  E1 >> E3
   Eww = E3pxm->ww;                                                                    //  image dimensions
   Ehh = E3pxm->hh;
   CEF->Fmods = 0;                                                                     //  image not modified
   if (CEF->zd) zdialog_send_event(CEF->zd,"undo");                                    //  notify edit function
   Fpaintnow();                                                                        //  update image synchronous
   return;
}


void edit_redo()                                                                       //  ER >> E3
{
   if (! CEF) return;
   if (CEF->threadfunc) thread_wait();                                                 //  wait for thread to become idle
   if (! ERpxm) return;                                                                //  no prior undo
   PXM_free(E3pxm);                                                                    //  redo copy >> E3
   E3pxm = ERpxm;
   ERpxm = 0;
   Eww = E3pxm->ww;                                                                    //  image dimensions
   Ehh = E3pxm->hh;
   CEF->Fmods = 1;                                                                     //  image modified
   if (CEF->zd) zdialog_send_event(CEF->zd,"redo");                                    //  notify edit function
   Fpaintnow();                                                                        //  update image synchronous
   return;
}


void edit_reset()                                                                      //  reset E3 to E1 status
{
   if (! CEF) return;
   Fescape = 1;                                                                        //  kill worker threads
   if (CEF->threadfunc) thread_wait();                                                 //  wait for thread to become idle
   Fescape = 0;                                                                        //  reset
   if (! CEF->Fmods) return;                                                           //  not modified
   PXM_free(ERpxm);                                                                    //  delete redo copy
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E1pxm);                                                            //  E1 >> E3
   Eww = E3pxm->ww;                                                                    //  image dimensions
   Ehh = E3pxm->hh;
   CEF->Fmods = 0;                                                                     //  reset image modified status
   Fpaintnow();                                                                        //  update image synchronous
   return;
}


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

//  [undo/redo] menu function
//  Reinstate a prior edit step as long as an edited image remains current.
//  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 *, ch *)
{
   void undo_redo_choice(GtkWidget *, ch *menu);

   GtkWidget   *popmenu;
   int         button = vmenuclickbutton;
   ch          menuitem[40], flag;

   F1_help_topic = TX("UNDO/REDO BUTTON");

   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');

   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_menu[ii],flag);
         add_popmenu(popmenu,menuitem,undo_redo_choice,(ch *) &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 *, ch *menu)
{
   int nn = *((int *) menu);
   if (nn < 0 || nn > URS_max) return;
   URS_pos = nn;
   if (E0pxm) PXM_free(E0pxm);
   E0pxm = load_undo(URS_pos);
   Fpaintnow();
   return;
}


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

void m_undo(GtkWidget *, ch *)
{
   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');

   if (CEF) {                                                                          //  undo active edit
      edit_undo();
      return;
   }

   if (URS_pos == 0) return;                                                           //  undo past edit
   URS_pos--;
   if (E0pxm) PXM_free(E0pxm);
   E0pxm = load_undo(URS_pos);
   Fpaintnow();
   return;
}


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

void m_redo(GtkWidget *, ch *)
{
   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');

   if (CEF) {                                                                          //  redo active edit
      edit_redo();
      return;
   }
   if (URS_pos == URS_max) return;                                                     //  redo past edit
   URS_pos++;
   if (E0pxm) PXM_free(E0pxm);
   E0pxm = load_undo(URS_pos);
   Fpaintnow();
   return;
}


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

void undo_all()
{
   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   if (CEF) return;                                                                    //  not if edit active
   if (URS_pos == 0) return;
   URS_pos = 0;                                                                        //  original image
   if (E0pxm) PXM_free(E0pxm);
   E0pxm = load_undo(URS_pos);
   Fpaintnow();
   return;
}


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

void redo_all()
{
   if (! curr_file) {
      zmessageACK(Mwin,TX("no current file"));
      return;
   }

   viewmode('F');

   if (CEF) return;                                                                    //  not if edit active
   if (URS_pos == URS_max) return;
   URS_pos = URS_max;;                                                                 //  image with last edit applied
   if (E0pxm) PXM_free(E0pxm);
   E0pxm = load_undo(URS_pos);
   Fpaintnow();
   return;
}


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

void save_undo()
{
   ch       *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   int64    cc1, cc2;
   int64    ccmax = 128 * MEGA;                                                        //  max. I/O size
   int64    z64 = 1;
   float    diskmb;

   zadd_locked(NFbusy,+1);

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

   diskmb = diskspace(temp_folder);                                                    //  disk MB
   diskmb -= 0.000001 * ww * hh * nc * sizeof(float);                                  //  - file MB
   if (diskmb < 100) {                                                                 //  remaining MB
      zmessageACK(Mwin,TX("out of space for temp files \n"
                          "save your edits and quit Fotocx"));                         //  allow user recovery
      zadd_locked(NFbusy,-1);
      return;                                                                          //  continuation is crash prone
   }

   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,"fotocx %05d %05d %d",ww,hh,nc);                                   //  write header
   nn = fwrite(buff,20,1,fid);
   if (nn != 1) goto writefail;

   cc1 = z64 * ww * hh * nc * sizeof(float);                                           //  bytes to write
   cc2 = 0;                                                                            //  bytes done
   while (cc1) {
      if (cc1 <= ccmax) {
         pp = (ch *) E0pxm->pixels;
         nn = fwrite(pp+cc2,cc1,1,fid);
         if (nn != 1) goto writefail;
         break;
      }
      else {
         pp = (ch *) 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_menu[0],"original");                                                  //  function name for original image
      URS_saved[0] = 1;                                                                //  original image already on disk
   }
   else {                                                                              //  stack position
      if (! CEF) {
         zadd_locked(NFbusy,-1);                                                                 //  must have an edit function
         return;
      }
      strncpy0(URS_menu[URS_pos],CEF->menuname,32);                                    //  edit function menu name
      strncpy0(URS_parms[URS_pos],CEF->edit_hist,edithistXcc);                         //  edit history text
      URS_saved[URS_pos] = 0;                                                          //  not yet saved to disk
   }

   zadd_locked(NFbusy,-1);
   return;

writefail:
   zmessageACK(Mwin,"undo/redo stack write failure: %s",strerror(errno));
   quitxx();
}


//  Load PXM from undo/redo image file stack
//  stack position = URS_pos

PXM * load_undo(int posn)
{
   ch       *pp, buff[24];
   FILE     *fid;
   int      ww, hh, nc, nn;
   int64    cc1, cc2;
   int64    ccmax = 128 * MEGA;
   int64    z64 = 1;
   PXM      *pxm;

   zadd_locked(NFbusy,+1);

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

   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
   nn = sscanf(buff,"fotocx %d %d %d",&ww,&hh,&nc);
   if (nn != 3) goto err4;

   pxm = PXM_make(ww,hh,nc);

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

   zadd_locked(NFbusy,-1);
   fclose(fid);
   return pxm;

err1:
   printf("*** undo err1: %s \n",URS_filename);                                        //  extended diagnostics
   goto fail;

err2:
   printf("*** open() failure, errno: %d %s \n",errno,strerror(errno));
   goto fail;

err3:
   printf("*** fread() failure, errno: %d %s \n",errno,strerror(errno));
   goto fail;

err4:
   printf("*** undo err4: %s \n",buff);
   goto fail;

fail:
   zmessageACK(Mwin,"undo/redo stack read failure");
   quitxx();
   return 0;
}


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

//  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
{
   mouseCBfunc = 0;
   Mcapture = 0;
   if (gdkwin) gdk_window_set_cursor(gdkwin,0);                                        //  set normal cursor
   return;
}


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

//   functions to manage working threads                                               //  edit thread management

int     Fthread_pend;                                                                  //  work request, run thread function
int     Fthread_busy;                                                                  //  thread status, working or idle
int     Fthread_stop;                                                                  //  command: stop working, exit thread

threadfunc  *userfunc;                                                                 //  caller-supplied thread function


//  start thread function that does the edit work
//  thread waits for work or exit request

void thread_start(threadfunc ufunc, void *arg)
{
   threadfunc  Fthreadfunc;                                                            //  defined below
   if (userfunc) zappcrash("thread_start(): old thread active");
   Fthread_pend = Fthread_busy = Fthread_stop = 0;                                     //  not busy, no work pending
   userfunc = ufunc;
   Fescape = 0;                                                                        //  reset kill switch
   start_detached_thread(Fthreadfunc,0);
   return;
}


//  signal thread that new work is pending

void thread_signal()
{
   if (! userfunc) zappcrash("thread_signal(): no thread active");
   Fthread_pend = 1;
   return;
}


//  stop thread and wait for exit
//  pending edits may not be completed

void thread_stop()
{
   if (! userfunc) zappcrash("thread_stop(): no thread active");
   Fthread_stop = 1;
   Fescape = 1;                                                                        //  kill worker threads
   while (Fthread_busy) zsleep(0.01);
   Fescape = 0;                                                                        //  reset
   userfunc = 0;
   return;
}


//  main thread: wait for working thread to finish current work and become idle

void thread_wait()
{
   if (! main_thread()) zappcrash("thread_wait(): called from thread");
   if (! userfunc) zappcrash("thread_wait(): no thread active");
   while (Fthread_busy || Fthread_pend) zmainsleep(0.002);
   return;
}


//  thread: wait for work or exit request

void * Fthreadfunc(void *)
{
   while (true)
   {
      if (Fthread_stop) {                                                              //  stop thread
         Fthread_busy = 0;
         Fthread_pend = 0;
         userfunc = 0;
         pthread_exit(0);                                                              //  exit thread
      }

      if (Fthread_pend) {                                                              //  new work requested
         Fthread_busy = 1;                                                             //  thread is working
         Fthread_pend = 0;                                                             //  all pending requests will be done
         paintlock(1);                                                                 //  window paint blocked
         zadd_locked(NFbusy,+1);                                                                 //  top panel busy indicator
         userfunc(0);                                                                  //  execute thread function
         zadd_locked(NFbusy,-1);
         paintlock(-1);                                                                //  window paint allowed
         Fthread_busy = 0;
         continue;                                                                     //  loop without delay
      }

      zsleep(0.01);                                                                    //  idle loop if pend = stop = 0
   }
}


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

//  thread: start worker threads (per processor core) and wait for completion
//  threadfunc:  void * func(void *index)
//  start Nt threads and wait for all to exit

void do_wthreads(threadfunc func, int Nt)
{
   pthread_t tid[XSMP];                                                                //  25.5

   zadd_locked(NFbusy,+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(NFbusy,-1);

   return;
}


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

//  table for loading and saving adjustable parameters between sessions

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

#define Nparms 46
param_t paramTab[Nparms] = {
//     name                    type        count      location
{     "fotocx release",       "char",        1,       &Prelease               },
{     "language code",        "char",        1,       &lc                     },       //  26.0
{     "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             },
{     "application font",     "char",        1,       &application_font       },       //  menu + dialog font                    26.3
{     "drag option",          "int",         1,       &Fdragopt               },
{     "zoom count",           "int",         1,       &zoomcount              },
{     "map_dotsize",          "int",         1,       &map_dotsize            },
{     "captions cc",          "int",         2,       &captext_cc             },
{     "last version",         "int",         1,       &Flastversion           },
{     "Image Position",       "char",        1,       &ImagePosn              },
{     "curve node dist %",    "float",       1,       &splcurve_minx          },
{     "start display",        "char",        1,       &startdisplay           },
{     "start album",          "char",        1,       &startalbum             },
{     "start 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             },
{     "copymove loc",         "char",        1,       &copymove_loc           },
{     "RGB chooser file",     "char",        1,       &RGB_chooser_file       },
{     "gridsettings",         "int",         5,       &gridsettings           },       //  5 must match fotocx.h 'gridsettings'
{     "RAW file types",       "char",        1,       &RAWfiletypes           },
{     "video file types",     "char",        1,       &VIDEOfiletypes         },
{     "video command",        "char",        1,       &video_command          },
{     "crop sizes",           "char",        5,       &cropsizes              },       //  5 must match fotocx.h 'cropmem'
{     "crop ratios",          "char",        5,       &cropratios             },
{     "edit rescale",         "int",         2,       &editrescale            },
{     "show captions",        "int",         1,       &Fcaps                  },
{     "jpeg def quality",     "int",         1,       &jpeg_def_quality       },
{     "tiff comp method",     "int",         1,       &tiff_comp_method       },
{     "RAW loader command",   "char",        1,       &raw_loader_command     },
{     "RAW match embed",      "int",         1,       &Fraw_match_embed       },
{     "line color",           "int",         3,       &LINE_COLOR             },
{     "Fshowhidden",          "int",         1,       &Fshowhidden            },       //  26.4
{     "Faskquit",             "int",         1,       &Faskquit               },
{     "printer color map",    "char",        1,       &colormapfile           }  };


//  save parameters to file /.../.fotocx/parameters

void save_params()
{
   FILE        *fid;
   ch          text[1000];                                                             //  limit for character data cc
   ch          *name, *type;
   int         nr, count;
   void        *location;
   ch          **charloc;
   int         *intloc;
   float       *floatloc;

   printf("save_params()\n");

   fid = fopen(parameters_file,"w");                                                   //  open parameters file
   if (! fid) return;

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

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

      for (int kk = 0; kk < count; kk++)                                               //  write "value" "value" ...
      {
         if (strmatch(type,"char")) {
            if (! *charloc) strcpy(text,"null");                                       //  missing, use "null"
            else repl_1str(*charloc++,text,999,"\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,"%03d EOF \n",nr);
   fclose(fid);                                                                        //  close file

   return;
}


//  load parameters from file /.../.fotocx/parameters

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

   printf("load_params()\n");

   fid = fopen(parameters_file,"r");                                                   //  open parameters file
   if (! fid) return;                                                                  //  none, defaults are used

   while (true)                                                                        //  read parameters
   {
      pp = fgets_trim(buff,999,fid,1);
      if (! pp) break;                                                                 //  EOF

      ii = atoi(pp);
      if (ii != seqno) {                                                               //  check sequence number                 25.1
         printf("*** parameter file clobbered - deleted \n");
         fclose(fid);
         remove(parameters_file);
         return;
      }

      seqno++;                                                                         //  next record sequence no.
      pp += 4;

      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 = (ch **) 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,999,"\\n","\n");                                         //  replace "\n" with real newlines
            *charloc++ = zstrdup(text,"params");
            break;
         }

         pp = substring(data,' ',ii);
         if (! pp) break;

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

         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 null strings to "null"
   {
      if (! strmatch(paramTab[ii].type,"char")) continue;
      charloc = (ch **) paramTab[ii].location;
      pcount = paramTab[ii].count;
      for (int jj = 0; jj < pcount; jj++)
         if (! charloc[jj])
            charloc[jj] = zstrdup("null","params",20);
   }

   if (curr_file && ! *curr_file) curr_file = 0;                                       //  no current file >> null

   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)
      zshell(0,"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

   for (int ii = 0; ii < Ntoptext; ii++) {                                             //  no toptext
      zfree(toptext[ii].text);
      zfree(toptext[ii].font);
   }

   Ntoptext = 0;
   Nptoplines = Ntoplines = 0;                                                         //  no toplines
   Ntopcircles = 0;                                                                    //  no topcircles
   progress_setgoal(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;
   }

   if (FGM == 'F') gtk_window_set_title(MWIN,Frelease);                                //  fotocx release

   PXB_free(Fpxb);
   PXM_free(E0pxm);
   PXM_free(E1pxm);
   PXM_free(E3pxm);
   Eww = Ehh = 0;
   PXM_free(ERpxm);
   PXM_free(E8pxm);
   PXM_free(E9pxm);

   return;
}


