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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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

   timerfunc               periodic function for top pamel update
   update_Fpanel           update status parameters on F window top panel
   update_progress         update top panel % complete or 'working' indicator
   Funcbusy                update top panel BUSY indicator

   Fblock                  block/unblock mutex functions

   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

   progress_setgoal        set/reset progress counter
   progress_setvalue       set current progress value
   progress_addvalue       increment progress value
   progress_percent        get progress percent completion 0-100

   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], buff[200];
   ch       *prev_file = 0;
   double   freememory;
   double   startsecs = get_seconds();                                           //  start time
   
   ch       *lost_settings_notify =                                              //  24.70
            "A new Fotocx release is being installed. \n"
            "You may need to restore custom settings. \n"
            "The following files in your .fotocx folder may need editing: \n"
            "KB_shortcuts_*  widgets.css  raw_commands_*  plugins_* \n"
            "meta_picklist_*  custom_menu_*  capskeys_*  \n";
   
   if (geteuid() == 0) {                                                         //  debian said this is needed            25.0
      Plog(0,"fotocx: root user not allowed \n");                                //  (root user can hack as desired,
      exit(EPERM);                                                               //     fotocx adds nothing more)
   }

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

   zinitapp(Frelease,argc,argv);                                                 //  initz. app (incl. app home folder)
   
   //  initialize default parameter values (saved parameters will override these)

   Fdevmenu = 0;                                                                 //  no show developer menu
   Ffirsttime = 1;                                                               //  first startup (params override)
   Floglevel = 1;                                                                //  0/1/2 = errs/infos/dialog inputs
   Findexlev = 2;                                                                //  exec: search new/modified files
   FMindexlev = 1;                                                               //  file manager open: no search
   Pindexlev = -1;                                                               //  no -x index command parameter
   xxrec_tab = 0;                                                                //  no image index yet
   Nxxrec = Findexvalid = 0;
   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 status bar text

   cropsizes[0] = zstrdup("1920x1080","params");                                 //  default crop size memory
   cropsizes[1] = zstrdup("1600x900", "params");
   cropsizes[2] = zstrdup("1440x900", "params");
   cropsizes[3] = zstrdup("1280x1024","params");
   cropsizes[4] = zstrdup("1366x768", "params");
   cropsizes[5] = zstrdup("1280x800", "params");
   cropsizes[6] = zstrdup("1024x768", "params");

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

   editrescale[0] = 1600;                                                        //  default initial rescale size
   editrescale[1] = 1200;

   gridsettings[GON] = 0;                                                        //  grid off
   gridsettings[GX]  = gridsettings[GY]  = 1;                                    //  x/y-lines enabled
   gridsettings[GXS] = gridsettings[GYS] = 100;                                  //  x/y-lines spacing
   gridsettings[GXC] = gridsettings[GYC] = 2;                                    //  x/y-lines count
   gridsettings[GXF] = gridsettings[GYF] = 0;                                    //  x/y-lines offset

   menu_style = zstrdup("both","params");                                        //  default menu style
   iconsize = 28;                                                                //  default icon size
   FBrgb[0] = FBrgb[1] = FBrgb[2] = 50;                                          //  F view background color
   GBrgb[0] = GBrgb[1] = GBrgb[2] = 200;                                         //  G view background color
   MFrgb[0] = MFrgb[1] = MFrgb[2] = 220;                                         //  menu font color
   MBrgb[0] = MBrgb[1] = MBrgb[2] = 60;                                          //  menu background color

   dialog_font = zstrdup("Sans 10","params");                                    //  default dialog font
   splcurve_minx = 5;                                                            //  default curve node separation %
   startdisplay = zstrdup("prevF","params");                                     //  start with previous image
   Fdragopt = 1;                                                                 //  image drag with mouse
   ImagePosn = zstrdup("center","params");                                       //  F-view image is centered
   zoomcount = 2;                                                                //  zooms to reach 2x image size
   zoomratio = sqrtf(2);                                                         //  corresp. zoom ratio
   Nkbsu = 0;                                                                    //  KB shortcut list is empty
   map_dotsize = 8;                                                              //  map dot size, mouse capture dist
   captext_cc[0] = 80;                                                           //  captions line low limit
   captext_cc[1] = 100;                                                          //  captions line high limit
   curr_file = 0;                                                                //  no curr. file
   navi::galleryname = zstrdup(getenv("HOME"),"params");                         //  curr. gallery
   navi::gallerytype = FOLDER;                                                   //  gallery type
   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 \"%s\" ","params");         //  default raw loader command
   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
   Fpaintedits = 0;                                                              //  not paint edits mode
   Fmousevents = 0;                                                              //  no show mouse events popup text
   Nblacklist = 0;                                                               //  blacklist folders/files count
   Nfuncbusy = 0;                                                                //  nothing busy
   Fescape = 0;                                                                  //  no escape key pressed
   NSMP = 8;                                                                     //  default SMP (core) count

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

   BLACK[0] = BLACK[1] = BLACK[2] = 0;                                           //  define RGB colors
   WHITE[0] = WHITE[1] = WHITE[2] = 255;
   RED[0] = 255; RED[1] = RED[2] = 0;
   GREEN[1] = 255; GREEN[0] = GREEN[2] = 0;
   BLUE[2] = 255; BLUE[0] = BLUE[1] = 0;
   memcpy(LINE_COLOR,RED,3*sizeof(int));                                         //  initial foreground drawing color

   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_E)                                    //  image index folder, this release
   makehomefolder(image_array_maps_folder,image_array_maps)
   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)

   snprintf(parameters_file,199,"%s/parameters",get_zhomedir());                 //  fotocx parameters file                24.20
   snprintf(blacklist_file,199,"%s/blacklist",get_zhomedir());                   //  blacklist folders and files
   snprintf(capskeys_file,199,"%s/capskeys_H",get_zhomedir());                   //  metadata keys 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 items
   snprintf(KB_shortcuts_file,199,"%s/KB_shortcuts_H",get_zhomedir());           //  KB shortcuts file                     24.70
   snprintf(meta_picklist_file,199,"%s/meta_picklist_H",get_zhomedir());         //  metadata picklist file                24.30
   snprintf(meta_report_items_file,199,"%s/meta_report_items",get_zhomedir());   //  metadata batch report items
   snprintf(meta_view_extras_file,199,"%s/meta_view_extra",get_zhomedir());      //  view_meta extra items 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                   24.60
   snprintf(searchresults_file,199,"%s/search_results",get_zhomedir());          //  output of image search function
   snprintf(tags_defined_file,199,"%s/tags_defined",get_zhomedir());             //  defined tags file
   snprintf(raw_commands_file,199,"%s/raw_commands_I",get_zhomedir());           //  RAW file loader commands file         25.0
   snprintf(bookmarks_file,199,"%s/bookmarks",get_zhomedir());                   //  gallery bookmarks file                24.20
   snprintf(custom_menu_file,199,"%s/custom_menu_H",get_zhomedir());             //  custom menu file                      24.30
   snprintf(map_regions_file,199,"%s/map_regions_H",get_zhomedir());             //  internet map regions file             24.20
   snprintf(plugins_file,199,"%s/plugins_H",get_zhomedir());                     //  plugin menu functions file            24.30
   
   //  delete files copied from zdatadir to zhomedir but are referenced only in zdatadir                                   24.20
   
   zshell(0,"rm -f %s/worldcities.txt",get_zhomedir());
   zshell(0,"rm -f %s/userguide",get_zhomedir());
   zshell(0,"rm -f %s/CIE1931.jpg",get_zhomedir());

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

   //  set up temp folder /.../temp/temp-xxxx/  where xxxx is owner PID

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

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

   //  restore parameters from last session

   load_params();

   //  get command line parameters

   for (ii = 1; ii < argc; ii++)
   {
      ch   *pp = argv[ii];

      if (strmatch(pp,"-home")) ii++;                                            //  -home homedir    skip, see above
      else if (strmatchV(pp,"-help","-h",0))                                     //  -h -help         show user guide
         showz_docfile(Mwin,"userguide","command parameters");
      else if (strmatchV(pp,"-clone","-c",0) && argc > ii+4) {                   //  -c -clone        clone new instance
         Fclone = 1;
         cloxx = atoi(argv[ii+1]);                                               //  window position and size
         cloyy = atoi(argv[ii+2]);                                               //    passed from parent instance
         cloww = atoi(argv[ii+3]);
         clohh = atoi(argv[ii+4]);
         ii += 4;
      }
      else if (strmatchV(pp,"-recent","-r",0))                                   //  -r -recent          recent files
         Frecent = 1;
      else if (strmatchV(pp,"-new","-n",0))                                      //  -n -new             newest files
         Fnew = 1;
      else if (strmatchV(pp,"-album","-a",0) && argc > ii+1)                     //  -a -album "name"    album to open
         initial_album = zstrdup(argv[++ii],"params");
      else if (strmatch(pp,"-cwp"))                                              //  -cwp secs "name"    cycle wallpaper
         m_cycle_wallpaper(argc,argv);
      else if (strmatchV(pp,"-prev","-p",0))                                     //  -p -prev            previous file
         Fprev = 1;
      else if (strmatchV(pp,"-blank","-b",0))                                    //  -b -blank           blank window
         Fblankwindow = 1;
      else if (strmatchV(pp,"-devmenu","-d",0))                                  //  -d -devmenu         show dev. menu
         Fdevmenu = 1;
      else if (strmatchV(pp,"-menu","-m",0) && argc > ii+1)                      //  -m -menu "name"     do menu function
         initial_menu = zstrdup(argv[++ii],"params");
      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 {
         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,0);                            //  prepend CWD
               initial_file = temp;
            }
         }
         break;
      }
   }
   
   if (initial_menu) Plog(1,"initial menu: %s \n",initial_menu);
   if (initial_file) Plog(1,"initial file: %s \n",initial_file);
   if (initial_album) Plog(1,"initial album: %s \n",initial_album);

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

   build_widgets();                                                              //  build window widgets and menus
   zfuncs::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 estimate image size limits

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

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

   //  Report SMP (core) count for parallel threads.
   //  Hyperthreads and E-cores have marginal value.

   Plog(1,"SMP thread count: %d \n",NSMP);

   //  get locale specific name for /home/<user>/Desktop

   strcpy(desktopname,"Desktop");

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

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

   //  check required/optional outboard programs, add image file types supported
   
   nn = check_outboards(Ffirsttime);
   if (nn) {
      Plog(0,"%d required programs not installed, EXIT \n",nn);
      quitxx();
   }

   //  Fotocx first install processing

   if (Ffirsttime)
   {
      Plog(0,"first Fotocx install \n");

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

      Prelease = zstrdup(Frelease,"params");                                     //  not a new release
      Pindexlev = -1;                                                            //  no -x index parameter set
      initial_file = 0;                                                          //  no initial file
      first_index();                                                             //  initial user indexing decision
      Ffirsttime = 0;                                                            //  reset first time flag
   }

   //  new release processing
   
   if (! strmatch(Prelease,Frelease))                                            //  new release install
   {
      Plog(0,"Fotocx new release: %s \n",Frelease);                              //  log new release
      showz_textfile("doc","changelog",Mwin);                                    //  show change log
      zmessageACK(Mwin,lost_settings_notify);                                    //  show list of customizable files
      Prelease = zstrdup(Frelease,"params");                                     //  update installed release
      initial_file = 0;                                                          //  no initial file
      Pindexlev = -1;                                                            //  no -x index parameter set
   }

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

   Findexvalid = 0;                                                              //  no valid index yet

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

   index_rebuild(Pindexlev,0);                                                   //  build index or make empty dummy
   if (Pindexlev > 0 && ! Findexvalid) m_index(0,0);                             //  failed, do manual index

   save_params();                                                                //  save parameters now
   
   prev_file = curr_file;                                                        //  parameters curr_file --> prev_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 (topfolders[0]) {
            gallery(topfolders[0],"init",0);
            gallery(0,"sort",-2);
            viewmode('G');
         }
      }
   }

   else if (initial_album) {                                                     //  -album albumname
      Plog(1,"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,"album")) {
      Plog(1,"initial album: %s \n",startalbum);
      album_show(startalbum);
   }

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


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


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

   else if (strmatch(startdisplay,"prevF")) {                                    //  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 prev. gallery
         if (navi::gallerytype != TNONE) {
            if (navi::gallerytype == FOLDER)
               gallery(navi::galleryname,"init",0);
            else gallery(navi::galleryname,"initF",0);
            gallery(0,"sort",-2);
            viewmode('G');
         }
      }
   }

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

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

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

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

   startsecs = get_seconds() - startsecs;
   Plog(1,"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;
      }
      if (ii < Nmenus) menutab[ii].func(0,menutab[ii].arg);                      //  call the menu function
   }

   zmainsleep(0.1);                                                              //  prevent long time blank window

   gtk_main();                                                                   //  start processing window events
   Plog(1,"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 10
   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           },
      {  "djxl",             ".jxl files",    "no",    "no",   "libjxl-tools",            &Fjxl             },         //  JXL
      {  "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);              //  24.10
      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;
         Plog(1,"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,"X",0);
      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 = " Defer image file indexing:";
   ch       *defer2 = "   • 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 = " Index image files now:";
   ch       *index2 = "   • Initial indexing may need considerable time \n"
                      "   • Subsequent startups will be fast \n"
                      "   • Full functionality will be available \n"
                      "   • Thumbnail galleries will be fast";

   ch       *info = " Indexing time depends on the number of image files and the \n"
                    " speed of your computer. This can be a few hundred to a few \n"
                    " thousand per minute. After indexing is done, startup time \n"
                    " should be quite fast. You can change index options later, \n"
                    " using these menus: Tools > Index, Tools > Settings. \n"
                    " \n"
                    " WARNING: Some image collections include corrupted files \n"
                    " that can cause Fotocx or a library function to crash. \n"
                    " Delete or move the file elsewhere and start Fotocx again.";

   F1_help_topic = "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                   |
         |     • Thumbnail galleries will be fast                       |
         |                                                              |
         | Indexing time depends on the number of image files and the   |
         | speed of your computer. This can be a few hundred to a few   |
         | thousand per minute. After indexing is done, startup time    |
         | should be quite fast. You can change index options later,    |
         | using these menus: Tools > Index, Tools > Settings.          |
         |                                                              |
         | WARNING: Some image collections include corrupted files      |
         | that can cause Fotocx or a library function to crash.        |
         | Delete or move the file elsewhere and start Fotocx again.    |
         |                                                              |
         |                                      [Help] [Proceed] [Quit] |
         |______________________________________________________________|

***/

   zd = zdialog_new("Fotocx First Index",Mwin,"Help","Proceed","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" button
      zdialog_free(zd);
      if (nn) {                                                                  //  do indexing
         Findexlev = 2;                                                          //  fotocx command: full index process
         FMindexlev = 1;                                                         //  file manager: use curr. index
      }
      else Findexlev = FMindexlev = 0;                                           //  use no index

      return;
   }

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

   return;
}


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

//  functions for main window event signals

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

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


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

//  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                *pp, text1[300], text2[200];
   static ch         ptext1[300] = "";
   int               ww, hh, scale, bpc;
   float             file_MB;

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

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

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

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

   *text1 = *text2 = 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
      if (f_load_bpc_raw) bpc = f_load_bpc_raw;                                  //  use raw value (8/10/12/14/16) if raw file
      snprintf(text2,100,"  %dx%dx%d",ww,hh,bpc);                                //  width x height x bits
      strncatv(text1,300,text2,0);

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

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

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

      if (Fmetamod) strncatv(text1,300,"  ","meta mod",0);                       //  metadata modified
   }

   if (sa_stat == sa_stat_fini) strncatv(text1,300,"  area active",0);

   if (zfuncs::zdialog_count) strncatv(text1,300,"  dialog open",0);             //  24.20

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

   if (curr_file) {
      pp = strrchr(curr_file,'/');                                               //  add filename.jpg
      if (pp && Ffullscreen && ! Nfuncbusy) {
         strncpy0(text2,pp+1,100);
         strncatv(text1,300,"   ",text2,0);
      }
   }

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

   update_progress();
   return;
}


//  Show 'working' label if Nfuncbusy active.
//  Show progress counter if progress_percent() > 0
//  added to top panel:  'xx%' or 'working' if no xx%

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

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

   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();                                                     //  show % progress in top panel
   if (pct > 0) {
      if (pct > 99) pct = 99;
      snprintf(nn,4,"%02d",pct);
      memcpy(doneposn,nn,2);
      if (! donelabel) {
         donelabel = gtk_label_new("");
         gtk_box_pack_start(GTK_BOX(FGpanel),donelabel,0,0,0);
      }
      gtk_label_set_markup(GTK_LABEL(donelabel),donetext);
      if (busylabel) gtk_widget_destroy(busylabel);
      busylabel = 0;
      gtk_widget_show_all(FGpanel);
      return;
   }
   else {
      if (donelabel) gtk_widget_destroy(donelabel);
      donelabel = 0;
   }

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

   gtk_widget_show_all(FGpanel);
   return;
}


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

//  For extended computations, show/remove 'working' indicator in top panel

void Funcbusy(int delta, int sleep)
{
   if (delta > 0) {
      zadd_locked(Nfuncbusy,+1);
      if (Nfuncbusy == 1 && sleep) {
         update_progress();                                                      //  if first busy, "working"
         zmainsleep(0.01);
      }
   }

   else if (delta < 0)
      zadd_locked(Nfuncbusy,-1);

   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)                                                         //  24.40
{
   static int  blocked = 0;
   static ch   Bfunction[20] = "no function";
   
   Plog(1,"Fblock %s \n",funcname);
   
   if (funcname)
   {
      if (blocked) {                                                             //  already blocked
         zmessageACK(Mwin,"blocking function: %s ",Bfunction);                   //  caller must stop
         return 1;
      }
   
      blocked++;
      strncpy0(Bfunction,funcname,20);                                           //  block for caller
      return 0;
   }

   if (! blocked)                                                                //  unblock
      zmessageACK(Mwin,"Fblock: no blocking function");
   blocked = 0;
   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;                                                         //  24.20
   }

   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,"pixbuf failure, image too large (?)");                   //  24.60
      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(),zfuncs::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) {
      Plog(0,"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 Plog(0,"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;
   ch             *pp;

   #define GAPR GDK_AXIS_PRESSURE

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

   type = event->type;
   button = event->button;                                                       //  button, 1/2/3 = left/center/right
   time = event->time;
   Mwxposn = event->x;                                                           //  mouse position in window
   Mwyposn = event->y;
   scroll = ((GdkEventScroll *) event)->direction;                               //  scroll wheel event

/***
   if (type == GDK_MOTION_NOTIFY)                                                //  dampen fine mouse motion              25.0
   {
      static int  px1 = 0, py1 = 0;
      int         px2, py2, mx, my;
      gdk_device_get_position(mouse,&screen,&px2,&py2);                          //  mouse position
      mx = px2 - px1;                                                            //  x motion
      if (mx < -1) mx += 1;                                                      //  if motion 2+ pixels, reduce by 1
      if (mx > +1) mx -= 1;
      px2 = px1 + mx;
      my = py2 - py1;                                                            //  same for y motion
      if (my < -1) my += 1;
      if (my > +1) my -= 1;
      py2 = py1 + my;
      px1 = px2;                                                                 //  save new position
      py1 = py2;
      if (mx || my) {
         gdk_device_warp(mouse,screen,px2,py2);                                  //  reset mouse to new position
         return;                                                                 //  will re-enter
      }
   }
***/

   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 (curr_file && LMclick && FGM == 'F') {                                     //  F-view, left click on image
      pp = strrchr(curr_file,'/');
      if (! pp) pp = curr_file;
      if (strstr(pp,"array:")) {                                                 //  click on image image_array file,
         image_array_Lclick_func(Mxclick,Myclick);                               //    popup corresp. image file
         LMclick = 0;
      }
   }

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

   if (RMclick) {                                                                //  right click
      RMclick = 0;
      if (Cstate->fzoom) {                                                       //  if zoomed image, reset to fit window
         zoomx = zoomy = 0;
         m_zoom(0,"fit");
      }
      else if (curr_file && 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

   Plog(2,"m_zoom\n");

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

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          *KBchar = 0, *action = 0;
   ch          *pp, *file = 0, *qfile;
   static int  Fshowmenu = 1;

   KBkey = event->keyval;                                                        //  input key
   KBchar = gdk_keyval_name(KBkey);                                              //  graphic char. for key
   Plog(2,"KB key: %s \n",KBchar);

   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,"userguide",F1_help_topic);
      return 1;
   }

   if (KBkey == GDK_KEY_h) {                                                     //  H  >>  user guide, current topic      24.10
      showz_docfile(Mwin,"userguide",F1_help_topic);                             //  Ctrl+H in zdialogs also comes here
      return 1;
   }

   if (KBkey == GDK_KEY_F2) {                                                    //  F2  >>  file view
      viewmode('F');
      return 1;
   }

   if (KBkey == GDK_KEY_F3) {                                                    //  F3  >>  gallery view
      viewmode('G');
      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 (Nfuncbusy) {
         Fescape = 1;                                                            //  tell pending function to quit
         if (CEF && zdialog_valid(CEF->zd)) {
            CEF->zd->zstat = -2;                                                 //  zdialog [x] status
            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,"\"",0);
         zshell("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 (KBkey == GDK_KEY_k) action = "KB Shortcuts";

   if (! action)                                                                 //  custom shortcut
   {
      if (KBkey >= GDK_KEY_F2 && KBkey <= GDK_KEY_F9) {                          //  input key is F2 to F9
         ii = KBkey - GDK_KEY_F1;
         strcpy(shortkey,"F1");                                                  //  convert to "F2" etc.
         shortkey[1] += ii;
      }

      if (! *shortkey && KBkey < 256)                                            //  single ascii character
      {
         if (KBcontrolkey) strcat(shortkey,"Ctrl+");                             //  build input key combination
         if (KBaltkey) strcat(shortkey,"Alt+");                                  //  [Ctrl+] [Alt+] [Shift+] key
         if (KBshiftkey) strcat(shortkey,"Shift+");
         cc = strlen(shortkey);
         shortkey[cc] = KBkey;
         shortkey[cc+1] = 0;
      }

      if (*shortkey) {                                                           //  find key in shortcut list
         for (ii = 0; ii < Nkbsu; ii++)
            if (strmatchcase(shortkey,kbsutab[ii].key)) break;
         if (ii < Nkbsu) action = kbsutab[ii].menu;                              //  corresp. action or function
      }
   }

   if (! action) {
      if (strlen(shortkey) == 1)
         Plog(0,"shortcut key not found: %c \n",toupper(*shortkey));
      else Plog(0,"shortcut key not found: %s \n",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 (FGM == 'M') {                                                             //  file map view mode
      ii = strmatchV(action,"File View","Gallery",0);                            //  allow only view mode changes
      if (! ii) {
         if (strlen(shortkey) == 1)
            Plog(0,"key ignored: %c %s \n",toupper(*shortkey),action);
         else Plog(0,"key ignored: %s %s \n",shortkey,action);
         return 1;
      }
   }

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

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

   if (CEF && CEF->menufunc == m_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(action,kbsftab[jj].menu)) break;

   if (jj == Nkbsf) {
      Plog(0,"shortcut menu func not found: %s %s \n",shortkey,action);
      return 1;
   }

   if (! kbsftab[jj].func) {
      Plog(0,"shortcut func null - ignored \n");
      return 1;
   }

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


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


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

//  set the main window to fullscreen status
//  (with 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

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


void set_mwin_title()
{
   GTYPE       gtype;
   int         cc, fposn, Gfiles, Gimages;
   ch          *pp, titlebar[250];
   ch          pdate[12], ptime[12], pdatetime[24];
   ch          fname[100], gname[100], ffolder[100];

   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
   }

   if (*meta_names::meta_pdate) {
      metadate_pdate(meta_names::meta_pdate,pdate,ptime);                        //  convert "yyyymmddhhmmss"
      strncpy0(pdatetime,pdate,11);                                              //    to "yyyy-mm-dd" and "hh:mm:ss"
      strncpy0(pdatetime+11,ptime,9);
      pdatetime[10] = ' ';
   }
   else strcpy(pdatetime,"(undated)");

   gtype = navi::gallerytype;

   if (gtype == FOLDER)                                                          //  gallery name = folder
      snprintf(titlebar,250,"%s   %d/%d  %s  %s  %s",
               Frelease,fposn,Gimages,ffolder,fname,pdatetime);
   else {
      if (gtype == SEARCH || gtype == META)
         strcpy(gname,"SEARCH RESULTS");
      else if (gtype == ALBUM) {
         pp = strrchr(navi::galleryname,'/');
         if (! pp) pp = navi::galleryname;
         else pp++;
         strcpy(gname,"ALBUM: ");
         strncpy0(gname+7,pp,87);
      }
      else if (gtype == RECENT)
         strcpy(gname,"RECENT FILES");
      else if (gtype == NEWEST)
         strcpy(gname,"NEWEST FILES");
      else
         strcpy(gname,"NO GALLERY");

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

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

   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                      24.60
      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                  24.40
      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);             //  24.40
      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         24.50
   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;

   if (CEF && strmatch(CEF->menuname,"Crop")) {                                  //  crop function is active
      gww = Mscale * (cropx2 - cropx1);                                          //  fit grid box to crop rectangle
      ghh = Mscale * (cropy2 - cropy1);
      startx = Mscale * cropx1 - Morgx + Dorgx;
      starty = Mscale * cropy1 - Morgy + Dorgy;
   }

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

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

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

   if (stepx < 20) stepx = 20;                                                   //  sanity limits
   if (stepy < 20) stepy = 20;

   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 (gridsettings[GX] && stepx)
      for (px = startx1; px < endx; px += stepx) {
         cairo_move_to(cr,px,starty);
         cairo_line_to(cr,px,endy);
      }

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

   cairo_stroke(cr);

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

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

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

   cairo_stroke(cr);

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


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

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

void add_toptext(int ID, int px, int py, ch *text, ch *font)
{
   if (Ntoptext == maxtoptext) {
      Plog(0,"*** 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) {
      Plog(0,"*** 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

void RGBfix(float &R, float &G, float &B)
{
   float    max, F;

   if (R < 0) R = 0;
   if (G < 0) G = 0;
   if (B < 0) 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;
   }

   return;
}


void RGBfix(int &R, int &G, int &B)
{
   float    max, F;

   if (R < 0) R = 0;
   if (G < 0) G = 0;
   if (B < 0) 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;
   }

   return;
}


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

   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,"image size is too small for edit: %d x %d",ww,hh);
      return 0;
   }

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

   zmessageACK(Mwin,"image size is too large for 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,"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,"File type cannot be edited");
      return 0;
   }

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

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

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

   if (Fscriptbuild && ! EF.Fscript) {                                           //  this function not scriptable
      zmessageACK(Mwin,"this function cannot be scripted");
      return 0;
   }
   
   if (Fblock(EF.menuname)) return 0;                                            //  block for edit function               24.40

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

   if (EF.Farea == 0 && sa_stat) {                                               //  select area will be lost, warn user
      yn = zmessageYN(Mwin,"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,"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            24.60
         yn = zmessageYN(Mwin,"Select area disabled.\n Continue?");
      else 
         yn = zmessageYN(Mwin,"Select area not finished.\n 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->sd) gtk_widget_destroy(CEF->sd->drawarea);                        //  25.0
      if (CEF->sd) zfree(CEF->sd);                                               //  free curves data
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
   }

   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->sd) gtk_widget_destroy(CEF->sd->drawarea);                        //  25.0
      if (CEF->sd) zfree(CEF->sd);                                               //  free curves data
      if (CEF->zd) zdialog_free(CEF->zd);                                        //  kill dialog
      if (CEF->mousefunc == mouseCBfunc) freeMouse();                            //  if my mouse, free mouse
   }

   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 = zfuncs::vmenuclickbutton;
   ch          menuitem[40], flag;

   F1_help_topic = "undo/redo button";

   Plog(1,"m_undo_redo \n");

   if (! curr_file) {
      zmessageACK(Mwin,"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_item(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 *)
{
   Plog(1,"m_undo \n");

   if (! curr_file) {
      zmessageACK(Mwin,"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 *)
{
   Plog(1,"m_redo \n");

   if (! curr_file) {
      zmessageACK(Mwin,"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,"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,"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;

   Funcbusy(+1,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,"out of disk space for temp files \n"
                       "save your edits now and quit Fotocx");                   //  allow user recovery
      Funcbusy(-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) {
         Funcbusy(-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
   }

   Funcbusy(-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;

   Funcbusy(+1,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;
      }
   }

   Funcbusy(-1);
   fclose(fid);
   return pxm;

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

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

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

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

readfail:
   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
         Funcbusy(+1);                                                           //  top panel busy indicator
         userfunc(0);                                                            //  execute thread function
         Funcbusy(-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[40];

   Funcbusy(+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]);

   Funcbusy(-1);

   return;
}


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

//  functions to track progress of long computations in multiple threads
//
//  initialize:      progress_setgoal(goal)           end goal value
//  functions:       progress_setvalue(value)         set current progress, 0 - goal
//                   progress_addvalue(value)         increment progress
//  threads:         progress_setvalue(TID,value)     set curent thread progress
//                   progress_addvalue(TID,value)     increment thread progress
//  get progress:    pct = progress_percent()         return 0-100 percent
//  reset to idle:   progress_setgoal(0)              reset, stop progress tracking

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


//  set current progress value, 0 - goal                                         //  24.10

void progress_setvalue(double value)
{
   using namespace progress_tracking;

   progress_done[0] = value;
   return;
}


//  accumulate progress value, 0 - goal                                          //  24.10

void progress_addvalue(double value)
{
   using namespace progress_tracking;

   progress_done[0] += value;
   return;
}


//  set progress value per thread

void progress_setvalue(int tid, double value)
{
   using namespace progress_tracking;

   progress_done[tid] = value;
   return;
}


//  increment progress counter per thread

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

   progress_done[tid] += value;
   return;
}


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


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

//  table for loading and saving adjustable parameters between sessions

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

#define Nparms 48
param paramTab[Nparms] = {
//     name                    type        count      location
{     "fotocx release",       "char",        1,       &Prelease               },
{     "first time",           "int",         1,       &Ffirsttime             },
{     "log level",            "int",         1,       &Floglevel              },
{     "window geometry",      "int",         4,       &mwgeom                 },
{     "thumbnail size",       "int",         1,       &navi::thumbsize        },
{     "menu style",           "char",        1,       &menu_style             },
{     "icon size",            "int",         1,       &iconsize               },
{     "F-base-color",         "int",         3,       &FBrgb                  },
{     "G-base-color",         "int",         3,       &GBrgb                  },
{     "menu font color",      "int",         3,       &MFrgb                  },
{     "menu background",      "int",         3,       &MBrgb                  },
{     "index level",          "int",         1,       &Findexlev              },
{     "FM index level",       "int",         1,       &FMindexlev             },
{     "dialog font",          "char",        1,       &dialog_font            },
{     "drag option",          "int",         1,       &Fdragopt               },
{     "zoom count",           "int",         1,       &zoomcount              },
{     "map_dotsize",          "int",         1,       &map_dotsize            },
{     "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 image file",     "char",        1,       &startfile              },
{     "start folder",         "char",        1,       &startfolder            },
{     "curr file",            "char",        1,       &curr_file              },
{     "galleryname",          "char",        1,       &navi::galleryname      },
{     "gallerytype",          "int",         1,       &navi::gallerytype      },
{     "current album",        "char",        1,       &curr_album             },
{     "copymove loc",         "char",        1,       &copymove_loc           },
{     "RGB chooser file",     "char",        1,       &RGB_chooser_file       },
{     "gridsettings",         "int",         9,       &gridsettings           },
{     "RAW file types",       "char",        1,       &RAWfiletypes           },
{     "video file types",     "char",        1,       &VIDEOfiletypes         },
{     "video command",        "char",        1,       &video_command          },
{     "crop sizes",           "char",        10,      &cropsizes              },
{     "crop buttons",         "char",        5,       &cropbuttons            },
{     "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             },
{     "Faskquit",             "int",         1,       &Faskquit               },
{     "SMP thread count",     "int",         1,       &NSMP                   },
{     "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         count;
   void        *location;
   ch          **charloc;
   int         *intloc;
   float       *floatloc;

   Plog(1,"save_params()\n");

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

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

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

      for (int kk = 0; kk < count; kk++)                                         //  write "value" "value" ...
      {
         if (strmatch(type,"char")) {
            if (! *charloc) 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,"\n");
   fclose(fid);                                                                  //  close file

   return;
}


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

void load_params()
{
   FILE        *fid;
   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;

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