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

   Fotocx - edit photos and manage collections

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

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

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

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

   Fotocx image edit - album and slide show functions

   m_album_create                   create a new album
   m_album_rename                   rename an album
   m_album_delete                   delete an album
   m_album_choose                   list albums, pick, show
   album_show                       callable show album
   m_album_current                  show current (last used) album
   m_gallery_to_album               make album from current gallery

   album thumbnail functions:
      album_add_selfiles            add selected files to album at position
      album_add_currfile            add current file at position
      album_add_file                add single file at position (drag/drop from gallery)
      album_remove_file             remove file at position from album
      album_move_file               move album file position (drag/drop within album)

   album_folder_rename              update album file names after a folder rename
   album_purge_replace              purge missing files, replace target files

   m_update_albums                  replace old with new version or append new to old
   m_album_mass_update              album mass update utilities

   m_cycle_wallpaper                cycle desktop wallpaper from album or folder

   m_slideshow                      album --> slide show

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

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

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

//  Albums - create, view, edit named albums of image files

//  albums created by search and report functions - omit from album update functions

int      NXalbums = 7;
cch      *Xalbums[7] = { "/current_set", "/duplicate_images", "/gallery_screen",
                         "/places_dates", "/search_results", "/selected_files",
                         "/timeline" };

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

//  create or replace an album

void m_album_create(GtkWidget *, ch *)                                                 //  25.1
{
   int album_create_dialog_event(zdialog *zd, ch *event);

   ch       **flist;
   int      NF;
   ch       patt[200];

   F1_help_topic = TX("CREATE ALBUM");
   printf("m_album_create \n");

   snprintf(patt,200,"%s/*",albums_folder);                                            //  get current album count
   zfind(patt,flist,NF);

   for (int ii = 0; ii < NF; ii++)
      zfree(flist[ii]);
   zfree(flist);

   if (NF >= maxalbums) {                                                              //  cannot create more
      zmessageACK(Mwin,TX("max. album count exceeded: %d"),maxalbums);
      return;
   }

/***
       _______________________________________________
      |         Create or replace an album            |
      |                                               |
      |  Album Name [____________________] [Browse]   |
      |     (o)  make an initially empty album        |
      |     (o)  fill from selected files             |
      |     (o)  fill from the current gallery        |
      |     (o)  select initial files                 |
      |                                               |
      |                                      [OK] [X] |
      |_______________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Create or replace an album"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labname","hbname",TX("Album Name"),"space=3");
   zdialog_add_widget(zd,"zentry","albumname","hbname",0,"space=3|size=20");
   zdialog_add_widget(zd,"button","browse","hbname",TX("Browse"),"space=3");
   zdialog_add_widget(zd,"hbox","hbopt","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hbopt",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbopt","hbopt");
   zdialog_add_widget(zd,"radio","empty","vbopt",TX("make an initially empty album"));
   zdialog_add_widget(zd,"radio","selected","vbopt",TX("fill from selected files"));
   zdialog_add_widget(zd,"radio","gallery","vbopt",TX("fill from current gallery"));
   zdialog_add_widget(zd,"radio","select","vbopt",TX("select initial files"));

   zdialog_stuff(zd,"empty",1);
   zdialog_stuff(zd,"selected",0);
   zdialog_stuff(zd,"gallery",0);
   zdialog_stuff(zd,"select",0);

   zdialog_run(zd,album_create_dialog_event,"parent");
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  dialog event and completion function

int album_create_dialog_event(zdialog *zd, ch *event)
{
   int         Fempty, Fselected, Fgallery, Fselect;
   int         yn;
   ch          albumname[ANCC], newalbumfile[AFCC];
   ch          *cfile, *pp;
   FILE        *fid;

   if (strmatch(event,"browse"))
   {
      cfile = zgetfile(TX("Album Name"),MWIN,"file",albums_folder);                    //  choose file name
      if (! cfile) return 1;
      pp = strrchr(cfile,'/');
      if (! pp) return 1;
      zdialog_stuff(zd,"albumname",pp+1);
      zfree(cfile);
      return 1;
   }

   if (! zd->zstat) return 1;                                                          //  wait for completion
   if (zd->zstat != 1) return 1;                                                       //  cancel

   zdialog_fetch(zd,"albumname",albumname,ANCC);                                       //  get album name
   if (*albumname <= ' ') {
      zmessageACK(Mwin,TX("enter an album name"));
      zd->zstat = 0;                                                                   //  keep dialog active
      return 1;
   }

   snprintf(newalbumfile,AFCC,"%s/%s",albums_folder,albumname);                        //  make filespec
   if (regfile(newalbumfile)) {                                                        //  already exists
      yn = zmessageYN(Mwin,TX("replace album %s ?"),albumname);
      if (! yn) return 1;
   }

   zstrcopy(curr_album,newalbumfile,"albums");

   fid = fopen(curr_album,"w");                                                        //  open/write empty album file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }

   fclose(fid);

   zdialog_fetch(zd,"empty",Fempty);                                                   //  get option
   zdialog_fetch(zd,"selected",Fselected);
   zdialog_fetch(zd,"gallery",Fgallery);
   zdialog_fetch(zd,"select",Fselect);

   if (Fempty) {
      zmessageACK(Mwin,TX("Use [Select] to add files to an empty album"));
      Fselect = 1;
   }

   if (Fselected) {
      if (zd_select_files) {                                                           //  select files active                   26.0
         zmessageACK(Mwin,TX("complete file selection first"));
         return 1;
      }
      album_add_selfiles(1);                                                           //  fill album from selected files
   }

   if (Fgallery)                                                                       //  fill album from gallery
      album_create_from_gallery(curr_album);

   if (Fselect) {                                                                      //  select initial album files
      m_select_files(0,0);
      album_add_selfiles(1);
   }

   album_show();
   panelmessage(1,3,TX("new album created"));                                          //  25.1
   return 1;
}


//  make an album from the current gallery
//  return 0 = OK, +N = error

int album_create_from_gallery(ch *newalbumfile)
{
   ch          *pp;
   int         Nth;
   FILE        *fid;
   FTYPE       ftype;

   if (navi::Gimages == 0) {
      zmessageACK(Mwin,TX("gallery is empty"));
      return 1;
   }

   if (navi::Gfiles > maxalbumfiles) {
      zmessageACK(Mwin,TX("max. album size exceeded: %d"),maxalbumfiles);
      return 1;
   }

   fid = fopen(newalbumfile,"w");                                                      //  open/write album file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }

   for (Nth = 0; Nth < navi::Gfiles; Nth++)                                            //  add gallery images to album file
   {
      pp = gallery(0,"getR",Nth);
      if (! pp) break;
      ftype = image_file_type(pp);                                                     //  must be image type file
      if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) continue;
      fprintf(fid,"%s\n",pp);
   }

   fclose(fid);
   return 0;
}


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

//  rename an existing album

void m_album_rename(GtkWidget *, ch *)                                                 //  25.1
{
   zlist_t  *zlist;
   ch       *oldname, *newname;
   ch       oldfile[200], newfile[200];
   int      err;

   F1_help_topic = TX("RENAME ALBUM");
   printf("m_album_rename \n");

   zlist = zlist_from_folder(albums_folder);
   if (! zlist) {
      zmessageACK(Mwin,TX("albums folder missing"));
      return;
   }

   oldname = popup_choose(zlist);
   zlist_free(zlist);
   if (! oldname) return;

   newname = zdialog_text(Mwin,TX("new album name"),oldname);
   if (! newname) return;

   snprintf(oldfile,200,"%s/%s",albums_folder,oldname);
   snprintf(newfile,200,"%s/%s",albums_folder,newname);

   if (regfile(newfile)) {
      zmessageACK(Mwin,TX("album already exists: %s"),newname);
      return;
   }

   err = rename(oldfile,newfile);
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   zmessageACK(Mwin,TX("%s renamed: %s"),oldname,newname);
   return;
}


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

//  delete an album

void m_album_delete(GtkWidget *, ch *)                                                 //  25.1
{
   zlist_t  *zlist;
   ch       *delname;
   ch       delfile[200];

   F1_help_topic = TX("DELETE ALBUM");
   printf("m_album_delete \n");

   zlist = zlist_from_folder(albums_folder);
   if (! zlist) {
      zmessageACK(Mwin,TX("albums folder missing"));
      return;
   }

   delname = popup_choose(zlist);
   zlist_free(zlist);
   if (! delname) return;

   snprintf(delfile,200,"%s/%s",albums_folder,delname);
   remove(delfile);

   zmessageACK(Mwin,TX("album %s: deleted"),delname);
   return;
}


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

//  choose an album and show a gallery of album image files

void m_album_choose(GtkWidget *, ch *)                                                 //  25.1
{
   zlist_t  *zlist;
   ch       *album, albumfile[200];

   F1_help_topic = TX("CHOOSE ALBUM");
   printf("m_album_choose \n");

   zlist = zlist_from_folder(albums_folder);
   if (! zlist) {
      zmessageACK(Mwin,TX("albums folder missing"));
      return;
   }

   album = popup_choose(zlist);

   if (! album) {
      zlist_free(zlist);
      return;
   }

   snprintf(albumfile,200,"%s/%s",albums_folder,album);
   album_show(albumfile);
   zlist_free(zlist);

   return;
}


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

//  initz. gallery from current album, show gallery
//  if albumfile is null, show current album
//  if not null, set current album from albumfile

void album_show(ch *albumfile)                                                         //  25.1
{
   zlist_t     *ZLfiles;
   int         ii, Nf;
   ch          *afile, *pp;

   printf("album_show %s\n",albumfile);

   if (! albumfile && ! curr_album) return;
   if (! albumfile) albumfile = curr_album;
   zstrcopy(curr_album,albumfile,"albums");

   ZLfiles = zlist_from_file(albumfile);                                               //  get image files in album
   if (! ZLfiles) {
      pp = strrchr(albumfile,'/');
      if (! pp) pp = albumfile;
      else pp++;
      zmessageACK(Mwin,TX("unknown or empty album: %s"),pp);
      return;
   }

   Nf = zlist_count(ZLfiles);

   if (Nf > maxalbumfiles) {
      zmessageACK(Mwin,TX("max. album size exceeded: %d"),maxalbumfiles);
      Nf = maxalbumfiles;
   }

   for (ii = 0; ii < Nf; ii++)                                                         //  loop files in album
   {
      afile = zlist_get(ZLfiles,ii);                                                   //  album file >> zlist
      if (! regfile(afile)) {                                                          //  test file exists
         printf("missing file removed: %s \n",afile);
         zlist_put(ZLfiles,0,ii);                                                      //  not found, replace with null
      }
   }

   zlist_to_file(ZLfiles,albumfile);                                                   //  zlist >> album file
   zlist_free(ZLfiles);

   navi::gallerytype = ALBUM;
   gallery(albumfile,"initF",0);                                                       //  gallery = album
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  paint
   viewmode('G');

   return;
}


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

//  1-click to show the current (last used) album
//  This is a convenience when navigating back-and-forth between an album
//  and other folders while adding images to an album.

void m_album_current(GtkWidget *, ch *menu)                                            //  25.1
{
   F1_help_topic = TX("CURRENT ALBUM");
   printf("m_album_current \n");

   album_show(null);
   return;
}


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

//  make current gallery into an album with chosen or given name

void m_gallery_to_album(GtkWidget *, ch *)
{
   int  gallery_album_dialog_event(zdialog *zd, ch *event);

   ch       albumname[ANCC], albumfile[AFCC];
   int      yn, zstat;

/***
       _________________________________________
      |         Save Gallery as Album           |
      |                                         |
      | Album Name [_________________] [Browse] |
      |                                         |
      |                                [OK] [X] |
      |_________________________________________|

***/

   F1_help_topic = TX("GALLERY TO ALBUM");
   printf("m_gallery_to_album \n");

   zdialog *zd = zdialog_new(TX("Save Gallery as Album"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbname","dialog");
   zdialog_add_widget(zd,"label","labname","hbname",TX("Album Name"),"space=5");
   zdialog_add_widget(zd,"zentry","albumname","hbname",0,"space=3|size=20");
   zdialog_add_widget(zd,"button","browse","hbname",TX("Browse"),"space=3");

   zdialog_stuff(zd,"albumname","gallery");                                            //  default name "gallery"

   zdialog_run(zd,gallery_album_dialog_event,"save");                                  //  run dialog
   zstat = zdialog_wait(zd);

   zdialog_fetch(zd,"albumname",albumname,ANCC);                                       //  get album name
   zdialog_free(zd);

   if (zstat != 1) return;                                                             //  user cancel
   if (*albumname <= ' ') return;

   snprintf(albumfile,AFCC,"%s/%s",albums_folder,albumname);                           //  make album file name

   if (regfile(albumfile)) {                                                           //  already exists
      yn = zmessageYN(Mwin,TX("replace album %s ?"),albumname);
      if (! yn) return;
   }

   album_create_from_gallery(albumfile);                                               //  create album
   album_show(albumfile);

   return;
}


//  dialog event and completion function

int  gallery_album_dialog_event(zdialog *zd, ch *event)
{
   ch       *cfile, *pp, albumname[ANCC];

   if (strmatch(event,"browse"))
   {
      cfile = zgetfile(TX("Album Name"),MWIN,"file",albums_folder);                    //  choose file name
      if (! cfile) return 1;
      pp = strrchr(cfile,'/');
      if (! pp) return 1;
      zdialog_stuff(zd,"albumname",pp+1);
      zfree(cfile);
      return 1;
   }

   if (zd->zstat != 1) return 1;                                                       //  wait for [OK]

   zdialog_fetch(zd,"albumname",albumname,ANCC);                                       //  get album name
   if (*albumname <= ' ') {
      zmessageACK(Mwin,TX("enter an album name"));
      zd->zstat = 0;                                                                   //  keep dialog active
   }

   return 1;
}


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

//  insert selected files into album at designated position
//  mode: 1 = add at position 0
//        2 = use clicked_posn and clicked_width

void album_add_selfiles(int mode)                                                      //  use zlist funcs
{
   zlist_t  *ZLalbum, *ZLfiles, *ZLtemp;
   ch       sfiles[AFCC];
   int      posn;

   F1_help_topic = TX("ALBUMS");                                                           //  for album popup menu

   if (! curr_album) return;

   if (mode == 1) posn = 0;                                                            //  insert at album start
   else if (mode == 2) {
      posn = clicked_posn;                                                             //  insert at clicked thumb position
      if (clicked_width > 50) posn++;                                                  //  right side of thumbnail clicked
   }
   else return;

   snprintf(sfiles,AFCC,"%s/%s",albums_folder,"selected_files");                       //  selected_files > zlist
   ZLfiles = zlist_from_file(sfiles);
   if (! ZLfiles) return;

   ZLalbum = zlist_from_file(curr_album);                                              //  current album > zlist
   if (! ZLalbum) ZLalbum = zlist_new(0);

   ZLtemp = zlist_insert_zlist(ZLalbum,ZLfiles,posn);                                  //  insert ZLfiles into ZLalbum
   zlist_free(ZLalbum);
   ZLalbum = ZLtemp;

   zlist_to_file(ZLalbum,curr_album);                                                  //  zlist > current album
   album_show();                                                                       //  (will alarm maxalbumfiles)

   zlist_free(ZLfiles);                                                                //  free memory
   zlist_free(ZLalbum);
   return;
}


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

//  insert 'curr_file' into album at designated position
//  mode: 1 = add at position 0
//        2 = use clicked_posn and clicked_width

void album_add_currfile(int mode)                                                      //  use zlist
{
   zlist_t  *ZLalbum;
   int      posn;

   F1_help_topic = TX("ALBUMS");                                                           //  for album popup menu

   if (! curr_album) {
      zmessageACK(Mwin,TX("no current album"));
      return;
   }

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

   if (mode == 1) posn = 0;                                                            //  insert at album start
   else if (mode == 2) {
      posn = clicked_posn;                                                             //  insert at clicked thumb position
      if (clicked_width > 50) posn++;                                                  //  right side of thumbnail clicked
   }
   else return;

   ZLalbum = zlist_from_file(curr_album);
   if (! ZLalbum) return;

   zlist_insert(ZLalbum,curr_file,posn);
   zlist_to_file(ZLalbum,curr_album);
   zlist_free(ZLalbum);

   album_show();                                                                       //  (will alarm maxalbumfiles)
   return;
}


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

//  insert single file into album at position (drag/drop from gallery window)

void album_add_file(ch *file, int posn)                                                //  use zlist
{
   zlist_t  *ZLalbum;

   F1_help_topic = TX("ALBUMS");                                                           //  for album popup menu

   if (! curr_album) return;

   ZLalbum = zlist_from_file(curr_album);
   if (! ZLalbum) return;

   zlist_insert(ZLalbum,file,posn);
   zlist_to_file(ZLalbum,curr_album);
   zlist_free(ZLalbum);

   album_show();
   return;
}


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

//  remove file at position from album

void album_remove_file(int posn)                                                       //  use zlist
{
   zlist_t  *ZLalbum;

   F1_help_topic = TX("ALBUMS");                                                           //  for album popup menu

   if (! curr_album) return;

   ZLalbum = zlist_from_file(curr_album);
   if (! ZLalbum) return;

   zlist_remove(ZLalbum,posn);
   zlist_to_file(ZLalbum,curr_album);
   zlist_free(ZLalbum);

   album_show();
   return;
}


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

//  move an album file from pos1 to pos2 (for drag and drop)

void album_move_file(int pos1, int pos2)                                               //  use zlist
{
   zlist_t  *ZLalbum;
   ch       *file;

   if (! curr_album) return;
   if (pos1 == pos2) return;

   ZLalbum = zlist_from_file(curr_album);
   if (! ZLalbum) return;

   file = zstrdup(zlist_get(ZLalbum,pos1),"albums");
   zlist_remove(ZLalbum,pos1);

   if (pos2 > pos1) pos2--;
   zlist_insert(ZLalbum,file,pos2);
   zfree(file);

   zlist_to_file(ZLalbum,curr_album);
   zlist_free(ZLalbum);

   album_show();
   return;
}


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

//  Replace file names in all albums after a folder rename.
//  Input args are the old and new folder full path names.

void album_folder_rename(ch *oldname, ch *newname)
{
   int      ii, jj, Fchange, Nalbums = 0;
   ch       findcomm[300];
   ch       *pp, **albumfiles = 0;
   int      oldcc, newcc;
   ch       newfile[XFCC];
   zlist_t  *ZL1, *ZL2;

   printf("album folder rename \n");

   oldcc = strlen(oldname);
   newcc = strlen(newname);
   strncpy0(newfile,newname,newcc);

   snprintf(findcomm,300,"%s/*",albums_folder);                                        //  find all albums (files)
   zfind(findcomm,albumfiles,Nalbums);
   if (! Nalbums) return;

   for (ii = 0; ii < Nalbums; ii++)                                                    //  loop all albums
   {
      pp = strrchr(albumfiles[ii],'/');                                                //  /album_name
      if (! pp) continue;
      for (jj = 0; jj < NXalbums; jj++)                                                //  if excluded album, skip it
         if (strmatch(pp,Xalbums[jj])) break;                                          //  (place_dates, search, etc.)
      if (jj < NXalbums) continue;

      ZL1 = zlist_from_file(albumfiles[ii]);                                           //  input files
      ZL2 = zlist_new(zlist_count(ZL1));                                               //  output files

      Fchange = 0;

      for (jj = 0; jj < zlist_count(ZL1); jj++)                                        //  loop input files
      {
         if (strmatchN(zlist_get(ZL1,jj),oldname,oldcc)) {                             //  input file matches?
            strcpy(newfile + newcc, zlist_get(ZL1,jj) + oldcc);
            zlist_put(ZL2,newfile,jj);                                                 //  yes, replace
            Fchange = 1;
         }
         else zlist_put(ZL2,zlist_get(ZL1,jj),jj);                                     //  no, copy input to output
      }

      if (Fchange) zlist_to_file(ZL2,albumfiles[ii]);                                  //  save updated album file

      zlist_free(ZL1);                                                                 //  free memory
      zlist_free(ZL2);
   }

   for (ii = 0; ii < Nalbums; ii++)                                                    //  free album names
      zfree(albumfiles[ii]);
   zfree(albumfiles);

   return;
}


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

//  Purge missing album files, delete and optionally replace target files.
//  'album' is an album name or "ALL" to process all albums.

void album_purge_replace(ch *album, int Nt, ch **targfile, ch **repfile)
{
   ch          findcomm[300], *pp;
   ch          **albfiles, *afile;
   int         ii, jj, kk;
   int         Na, Nf;
   int         Fupdates;
   zlist_t     *ZLfiles;

   printf("update album %s \n",album);

   for (ii = 0; ii < Nt; ii++)
   {
      if (repfile)
         printf("replace: %s \n    with: %s \n",targfile[ii], repfile[ii]);
      else if (targfile)
         printf("delete: %s \n",targfile[ii]);
   }

   if (strmatch(album,"ALL")) {
      snprintf(findcomm,300,"%s/*",albums_folder);                                     //  find all albums (files)
      zfind(findcomm,albfiles,Na);
   }
   else {
      albfiles = (ch **) zmalloc(sizeof(ch *),"albums");                               //  single album
      albfiles[0] = zstrdup(album,"albums");
      Na = 1;
   }

   for (ii = 0; ii < Na; ii++)                                                         //  loop albums
   {
      pp = strrchr(albfiles[ii],'/');                                                  //  /album_name
      if (! pp) continue;
      for (jj = 0; jj < NXalbums; jj++)                                                //  if excluded album, skip it
         if (strmatch(pp,Xalbums[jj])) break;
      if (jj < NXalbums) continue;

      ZLfiles = zlist_from_file(albfiles[ii]);                                         //  get image files in album
      if (! ZLfiles) continue;

      Fupdates = 0;                                                                    //  count updates                         26.4

      Nf = zlist_count(ZLfiles);

      for (jj = 0; jj < Nf; jj++)                                                      //  loop files in album
      {
         afile = zlist_get(ZLfiles,jj);                                                //  album file >> zlist

         for (kk = 0; kk < Nt; kk++)                                                   //  loop target files
         {
            if (strmatch(afile,targfile[kk])) {                                        //  album file = target file
               if (repfile && repfile[kk])
                  zlist_put(ZLfiles,repfile[kk],jj);                                   //  yes, replace if available             25.1
               else zlist_put(ZLfiles,0,jj);                                           //  no, replace with null
               Fupdates++;                                                             //  count updates
               break;
            }
         }

         afile = zlist_get(ZLfiles,jj);                                                //  album file (possibly replaced)
         if (afile && ! regfile(afile)) {
            zlist_put(ZLfiles,0,jj);                                                   //  not found, replace with null
            Fupdates++;                                                                //  count updates                         26.4
         }
      }

      if (! Fupdates) continue;                                                        //  nothing changed                       26.4

      zlist_to_file(ZLfiles,albfiles[ii]);                                             //  zlist >> album file
      zlist_free(ZLfiles);
   }

   for (ii = 0; ii < Na; ii++)                                                         //  free memory
      zfree(albfiles[ii]);
   zfree(albfiles);

   if (curr_album && strmatch(navi::galleryname,curr_album))
      album_show();

   return;
}


//  version for a single file and optional replacement

void album_purge_replace(ch *album, ch *targfile, ch *repfile)
{
   ch       **tfile = 0, **rfile = 0;

   if (targfile) tfile = &targfile;
   if (repfile) rfile = &repfile;
   album_purge_replace(album, 1, tfile, rfile);
   return;
}


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

//  Replace an old file with a designated new file in selected albums,
//  or add the new file after the old file.

namespace update_albums_names
{
   ch       **albumfiles = 0;
   ch       oldfile[XFCC], newfile[XFCC];                                              //  albums and replace option kept
   int      Nalbums = 0;                                                               //    to initialize next run
   int      Freplace = 1;
   ch       *selectformat = TX("%d albums selected");
   ch       selectmess[60];
};


//  menu function

void m_update_albums(GtkWidget *, ch *)
{
   using namespace update_albums_names;

   int update_albums_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      ii, jj, kk, zstat, Fchange;
   ch       *pp;
   zlist_t  *ZL1, *ZL2;

   F1_help_topic = TX("UPDATE ALBUMS");
   printf("m_update_albums \n");

   if (Fblock("update albums")) return;

/***
       __________________________________________________
      |           Replace Album File                     |
      |                                                  |
      |  [Select] N albums selected   [x] select all     |
      |                                                  |
      |  old file [____________________________________] |
      |  new file [____________________________________] |
      |                                                  |
      |  (o) replace old  (o) add after old              |
      |                                                  |
      |                            [Clear] [Proceed] [X] |
      |__________________________________________________|

***/

   zd = zdialog_new("Replace Album File",Mwin,"Clear",TX("Proceed"),"X",null);
   zdialog_add_widget(zd,"hbox","hbselect","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","select","hbselect","Select","space=5");
   zdialog_add_widget(zd,"label","labselect","hbselect",TX("0 albums selected"),"space=5");
   zdialog_add_widget(zd,"check","all","hbselect",TX("select all"),"space=10");
   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbold","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labold","hbold",TX("old file"),"space=5");
   zdialog_add_widget(zd,"zentry","oldfile","hbold",0,"expand|space=5");
   zdialog_add_widget(zd,"hbox","hbnew","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labnew","hbnew",TX("new file"),"space=5");
   zdialog_add_widget(zd,"zentry","newfile","hbnew",0,"expand|space=5");
   zdialog_add_widget(zd,"hbox","hbrep","dialog",0,"space=10");
   zdialog_add_widget(zd,"radio","replace","hbrep",TX("replace old"),"space=5");
   zdialog_add_widget(zd,"radio","addafter","hbrep",TX("add after old"),"space=10");

   snprintf(selectmess,60,selectformat,Nalbums);
   zdialog_stuff(zd,"labselect",selectmess);

   if (Freplace) {
      zdialog_stuff(zd,"replace",1);
      zdialog_stuff(zd,"addafter",0);
   }
   else {
      zdialog_stuff(zd,"replace",0);
      zdialog_stuff(zd,"addafter",1);
   }

   zdialog_resize(zd,500,0);

   viewmode('G');                                                                      //  gallery view

   zd_album_update = zd;                                                               //  set up mouse intercepts

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

   zd_album_update = 0;                                                                //  cancel mouse intercepts

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

   zdialog_fetch(zd,"oldfile",oldfile,XFCC);                                           //  [proceed]
   zdialog_fetch(zd,"newfile",newfile,XFCC);
   zdialog_fetch(zd,"replace",Freplace);
   zdialog_free(zd);

   if (! regfile(oldfile)) {
      zmessageACK(Mwin,TX("file not found: %s"),oldfile);
      Fblock(0);
      return;
   }

   if (! regfile(newfile)) {
      zmessageACK(Mwin,TX("file not found: %s"),newfile);
      Fblock(0);
      return;
   }

   if (! Nalbums) {
      zmessageACK(Mwin,TX("no albums selected"));
      Fblock(0);
      return;
   }

   for (ii = 0; ii < Nalbums; ii++)                                                    //  loop all albums
   {
      pp = strrchr(albumfiles[ii],'/');                                                //  /album_name
      if (! pp) continue;
      for (jj = 0; jj < NXalbums; jj++)                                                //  if excluded album, skip it
         if (strmatch(pp,Xalbums[jj])) break;                                          //  (place_dates, etc.)
      if (jj < NXalbums) continue;

      ZL1 = zlist_from_file(albumfiles[ii]);                                           //  input files
      ZL2 = zlist_new(maxalbumfiles);                                                  //  output files

      Fchange = 0;

      for (jj = kk = 0; jj < zlist_count(ZL1); jj++)                                   //  loop input files
      {
         if (strmatch(zlist_get(ZL1,jj),oldfile)) {                                    //  input file matches?
            Fchange = 1;
            if (Freplace) zlist_put(ZL2,newfile,kk++);                                 //  replace: output new file
            else {
               zlist_put(ZL2,oldfile,kk++);                                            //  add after:
               zlist_put(ZL2,newfile,kk++);                                            //  output both files
            }
         }
         else zlist_put(ZL2,zlist_get(ZL1,jj),kk++);                                   //  no match, copy input to output
         if (kk > maxalbumfiles-2) break;
      }

      if (Fchange) zlist_to_file(ZL2,albumfiles[ii]);                                  //  save new album file

      zlist_free(ZL1);                                                                 //  free memory
      zlist_free(ZL2);

      album_show(albumfiles[ii]);                                                      //  show updated album
   }

   Fblock(0);
   m_update_albums(0,0);                                                               //  repeat until canceled
   return;
}


//  dialog event and completion callback function

int update_albums_dialog_event(zdialog *zd, ch *event)
{
   using namespace update_albums_names;

   ch       **pp;
   int      ii,cc;
   ch       findcomm[300];

   if (zd->zstat < 0) {
      zd_album_update = 0;                                                             //  escape or cancel
      return 1;
   }

   if (zd->zstat == 1) {                                                               //  [clear]
      zd->zstat = 0;                                                                   //  keep dialog active
      zdialog_stuff(zd,"oldfile","");                                                  //  clear file name inputs
      zdialog_stuff(zd,"newfile","");
      return 1;
   }

   if (strmatch(event,"select"))                                                       //  [choose] button
   {
      for (ii = 0; ii < Nalbums; ii++)                                                 //  free prior album names, if any
         zfree(albumfiles[ii]);
      if (albumfiles) zfree(albumfiles);
      albumfiles = 0;
      Nalbums = 0;

      pp = zgetfiles(TX("Choose Albums"),MWIN,"files",albums_folder);                  //  choose album files
      if (pp)
      {
         cc = maxalbums * sizeof(ch *);
         albumfiles = (ch **) zmalloc(cc,"albums");
         for (ii = 0; ii < maxalbums && pp[ii]; ii++)
            albumfiles[ii] = pp[ii];
         Nalbums = ii;                                                                 //  album count selected
         zfree(pp);
      }

      snprintf(selectmess,60,selectformat,Nalbums);                                    //  update dialog album count
      zdialog_stuff(zd,"labselect",selectmess);
   }

   if (strmatch(event,"all"))                                                          //  select all albums
   {
      for (ii = 0; ii < Nalbums; ii++)                                                 //  free prior album names, if any
         zfree(albumfiles[ii]);
      if (albumfiles) zfree(albumfiles);
      albumfiles = 0;
      Nalbums = 0;

      snprintf(findcomm,300,"%s/*",albums_folder);                                     //  find all albums (files)
      zfind(findcomm,albumfiles,Nalbums);

      snprintf(selectmess,60,selectformat,Nalbums);                                    //  update dialog album count
      zdialog_stuff(zd,"labselect",selectmess);
   }

   return 1;
}


//  mouse click function

void update_albums_Lclick_func(int Nth)
{
   using namespace update_albums_names;

   ch       *imagefile = 0;
   ch       filename[100];
   int      ftype;
   zdialog  *zd = zd_album_update;

   if (! zd) return;                                                                   //  should not happen
   if (Nth < 0) return;

   imagefile = gallery(0,"getR",Nth);                                                  //  get file at clicked position
   if (! imagefile) return;

   ftype = image_file_type(imagefile);                                                 //  must be image or RAW file
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) return;

   zdialog_fetch(zd,"oldfile",filename,100);                                           //  stuff oldfile or newfile
   if (filename[0] <= ' ') zdialog_stuff(zd,"oldfile",imagefile);                      //    if currently blank
   else {
      zdialog_fetch(zd,"newfile",filename,100);
      if (filename[0] <= ' ') zdialog_stuff(zd,"newfile",imagefile);
   }

   return;
}


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

//  mass update of album files

namespace album_mass_update_names
{
   ch       *selectformat = TX("%d albums selected");
   ch       selectmess[60];
   ch       massoption = 0;
   ch       *albumfiles[maxalbums];                                                    //  albums to process
   zlist_t  *ZLoldfs, *ZLnewfs;                                                        //  old and new album files list
   int      Nalbums = 0, Nold, Nnew;                                                   //  corresp. counts
   zdialog  *zdpop = 0;
}


//  menu function

void m_album_mass_update(GtkWidget *, ch *)
{
   using namespace album_mass_update_names;

   int   album_mass_update_dialog_event(zdialog *zd, ch *event);
   void  album_mass_update_process(ch *albumname);

   int      ii, jj;
   ch       *albumname;

   F1_help_topic = TX("ALBUM MASS UPDATE");
   printf("m_album_mass_update \n");

   if (Fblock("album mass update")) return;

/***
          ______________________________________________________
         |             Album Mass Update                        |
         |                                                      |
         |  [select] N albums selected                          |
         |                                                      |
         |  Process selected album files:                       |
         |    (o) Replace all with newest version only          |
         |    (o) Replace all versions with newest version      |
         |    (o) Add newest version to existing versions       |
         |    (o) Replace all with original and all versions    |
         |    (o) Replace all with original + newest version    |
         |                                                      |
         |  Process album files matching selected files:        |
         |    (o) Replace all with selected versions            |
         |    (o) Replace all versions with selected versions   |
         |    (o) Add selected versions to existing versions    |
         |    (o) Replace all with original + selected versions |
         |                                                      |
         |                                        [Proceed] [X] |
         |______________________________________________________|

***/

   massoption = 0;
   Nalbums = 0;
   zdpop = 0;

   zdialog *zd = zdialog_new(TX("Album Mass Update"),Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbselect","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","select","hbselect","Select","space=3");
   zdialog_add_widget(zd,"label","labselect","hbselect",TX("0 albums selected"),"space=5");

   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hball","dialog");
   zdialog_add_widget(zd,"label","laball","hball",TX("Process selected album files:"),"space=3");
   zdialog_add_widget(zd,"vbox","vb1","dialog");
   zdialog_add_widget(zd,"hbox","hbA","vb1");
   zdialog_add_widget(zd,"check","optA","hbA",TX("Replace all with newest version only"),"space=12");
   zdialog_add_widget(zd,"hbox","hbB","vb1");
   zdialog_add_widget(zd,"check","optB","hbB",TX("Replace all versions with newest version"),"space=12");
   zdialog_add_widget(zd,"hbox","hbC","vb1");
   zdialog_add_widget(zd,"check","optC","hbC",TX("Add newest version to existing versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbD","vb1");
   zdialog_add_widget(zd,"check","optD","hbD",TX("Replace all with original and all versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbE","vb1");
   zdialog_add_widget(zd,"check","optE","hbE",TX("Replace all with original + newest version"),"space=12");

   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbselc","dialog");
   zdialog_add_widget(zd,"label","labselc","hbselc",TX("Process album files matching selected files:"),"space=3");
   zdialog_add_widget(zd,"vbox","vb2","dialog");
   zdialog_add_widget(zd,"hbox","hbM","vb2");
   zdialog_add_widget(zd,"check","optM","hbM",TX("Replace all with selected versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbN","vb2");
   zdialog_add_widget(zd,"check","optN","hbN",TX("Replace all versions with selected versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbO","vb2");
   zdialog_add_widget(zd,"check","optO","hbO",TX("Add selected versions to existing versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbP","vb2");
   zdialog_add_widget(zd,"check","optP","hbP",TX("Replace all with original + selected versions"),"space=12");

   zdialog_run(zd,album_mass_update_dialog_event,"parent");                            //  run dialog
   zdialog_wait(zd);                                                                   //  wait for completion

   if (! Nalbums || ! massoption) {                                                    //  canceled
      Fblock(0);
      return;
   }

   if (zdpop) zdialog_free(zdpop);                                                     //  open log report
   zdpop = popup_report_open(TX("album mass update"),Mwin,800,600,
                                             0,0,0,"Save","X",null);
   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < Nalbums; ii++)                                                    //  loop selected albums
   {
      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      albumname = strrchr(albumfiles[ii],'/');                                         //  /album_name
      if (! albumname) continue;

      for (jj = 0; jj < NXalbums; jj++)                                                //  if excluded album, skip it
         if (strmatch(albumname,Xalbums[jj])) break;
      if (jj < NXalbums) continue;

      ZLoldfs = zlist_from_file(albumfiles[ii]);                                       //  album file list
      if (! ZLoldfs) continue;
      Nold = zlist_count(ZLoldfs);

      ZLnewfs = zlist_new(maxalbumfiles);                                              //  container, new file list

      album_mass_update_process(albumname);                                            //  ZLoldfs --> ZLnewfs

      zlist_to_file(ZLnewfs,albumfiles[ii]);                                           //  replace album file

      zlist_free(ZLoldfs);                                                             //  free memory
      zlist_free(ZLnewfs);
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
   printf("*** report canceled \n");

   album_show(albumfiles[0]);                                                          //  show first album processed

   for (ii = 0; ii < Nalbums; ii++)                                                    //  free memory
      zfree(albumfiles[ii]);

   Fblock(0);
   return;
}


//  dialog event and completion function

int album_mass_update_dialog_event(zdialog *zd, ch *event)
{
   using namespace album_mass_update_names;

   int      ii;
   ch       **pp;

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                              //  proceed
      {
         if (Nalbums == 0) {
            zd->zstat = 0;
            zmessageACK(Mwin,TX("no albums selected"));
            return 1;
         }

         if (! massoption) {
            zd->zstat = 0;
            zmessageACK(Mwin,TX("no option selected"));
            return 1;
         }

         zdialog_free(zd);
         return 1;
      }

      massoption = 0;                                                                  //  cancel
      zdialog_free(zd);
      return 1;
   }

   if (strmatch(event,"select"))
   {
      for (ii = 0; ii < Nalbums; ii++)
         zfree(albumfiles[ii]);

      pp = zgetfiles(TX("Select Albums"),MWIN,"files",albums_folder);                  //  select album files
      if (pp)
      {
         for (ii = 0; ii < maxalbums && pp[ii]; ii++)
            albumfiles[ii] = pp[ii];
         Nalbums = ii;                                                                 //  album count selected
         zfree(pp);
      }

      snprintf(selectmess,60,selectformat,Nalbums);                                    //  update dialog album count
      zdialog_stuff(zd,"labselect",selectmess);
   }

   if (zstrstr("optA optB optC optD optE optM optN optO optP",event))
   {
      zdialog_stuff(zd,"optA",0);
      zdialog_stuff(zd,"optB",0);
      zdialog_stuff(zd,"optC",0);
      zdialog_stuff(zd,"optD",0);
      zdialog_stuff(zd,"optE",0);
      zdialog_stuff(zd,"optM",0);
      zdialog_stuff(zd,"optN",0);
      zdialog_stuff(zd,"optO",0);
      zdialog_stuff(zd,"optP",0);
      zdialog_stuff(zd,event,1);
      massoption = event[3];
   }

   return 1;
}


//  process the list of Nold album files in ZLoldfs
//  output list of Nnew new album files in ZLnewfs

void album_mass_update_process(ch *albumname)
{
   using namespace album_mass_update_names;

   int      ii, ii1, ii2, jj, kk, kk1, kk2;
   int      ll, nf, nc, Fchanged;
   int      Nself = 0;
   ch       sfiles[AFCC];
   ch       *rootname, *basename, *pp;
   ch       **fileversions, *ppselect[100];
   zlist_t  *ZLselfs = 0;

   popup_report_write(zdpop,0,"\n");                                                   //  report album name
   popup_report_write(zdpop,1,"%s \n",albumname);

   if (massoption >= 'M')                                                              //  if 'selected files' option
   {
      snprintf(sfiles,AFCC,"%s/%s",albums_folder,"selected_files");                    //  get selected_files
      ZLselfs = zlist_from_file(sfiles);
      if (ZLselfs) Nself = zlist_count(ZLselfs);
      if (! Nself) {
         zmessageACK(Mwin,TX("no files selected"));
         return;
      }
   }

   ii = 0;                                                                             //  input oldfiles index
   kk = 0;                                                                             //  output newfiles index

   while (ii < Nold)                                                                   //  loop old files in name/version groups
   {
      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      if (kk > maxalbumfiles-100) {
         zmessageACK(Mwin,TX("max. album size exceeded: %d"),maxalbumfiles);           //  stop with sufficient margin
         break;
      }

      rootname = file_rootname(zlist_get(ZLoldfs,ii));                                 //  get root name for group /.../filename
      if (! rootname) continue;                                                        //  no files found for group
      basename = file_basename(zlist_get(ZLoldfs,ii));                                 //  get base name for group /.../filename.ext
                                                                                       //  (null if only versions are in group)
      ii1 = ii;
      for (ii2 = ii+1; ii2 < Nold; ii2++)                                              //  range of oldfiles group, ii1 to ii2
         if (! strmatch(rootname,file_rootname(zlist_get(ZLoldfs,ii2))))
            break;
      ii = ii2;                                                                        //  leave ii at start of next oldfiles group
      ii2--;

      kk1 = kk;                                                                        //  start of next newfiles group
      kk2 = kk1 - 1;                                                                   //  (empty)

      if (massoption == 'A')                                                           //  Replace all files with newest version only
      {
         pp = file_newest_version(zlist_get(ZLoldfs,ii1));
         if (pp) zlist_put(ZLnewfs,pp,kk++);                                           //  add newest version only to newfiles
         goto report;
      }

      if (massoption == 'B')                                                           //  Replace all versions with newest version
      {
         if (basename) {
            for (jj = ii1; jj <= ii2; jj++)                                            //  oldfiles includes basename file?
               if (strmatch(basename,zlist_get(ZLoldfs,jj))) break;
            if (jj <= ii2) zlist_put(ZLnewfs,basename,kk++);                           //  yes, add basename to newfiles
         }

         pp = file_newest_version(zlist_get(ZLoldfs,ii1));                             //  newest version
         if (pp && basename && ! strmatch(pp,basename))                                //  not same as basename
            zlist_put(ZLnewfs,pp,kk++);                                                //  add to newfiles
         goto report;
      }

      if (massoption == 'C')                                                           //  Add newest version to existing versions
      {
         for (jj = ii1; jj <= ii2; jj++)                                               //  add oldfiles to newfiles
            zlist_put(ZLnewfs,zlist_get(ZLoldfs,jj),kk++);

         pp = file_newest_version(zlist_get(ZLoldfs,ii1));                             //  newest version
         if (pp) {
            for (jj = ii1; jj <= ii2; jj++)                                            //  already included in oldfiles?
               if (strmatch(pp,zlist_get(ZLoldfs,jj))) break;
            if (jj > ii2) zlist_put(ZLnewfs,pp,kk++);                                  //  no, add to newfiles
         }
         goto report;
      }

      if (massoption == 'D')                                                           //  Replace all with original + all versions
      {
         fileversions = file_all_versions(zlist_get(ZLoldfs,ii1),nf);                  //  get basename and all versions that exist
         if (! nf) goto report;
         for (jj = 0; jj < nf; jj++)                                                   //  add all file versions to newfiles
            zlist_put(ZLnewfs,fileversions[jj],kk++);
         zfree(fileversions);                                                          //  free memory
         goto report;
      }

      if (massoption == 'E')                                                           //  Replace all with original + newest version
      {
         if (basename) zlist_put(ZLnewfs,basename,kk++);                               //  add basename to newfiles
         pp = file_newest_version(zlist_get(ZLoldfs,ii1));
         if (pp && (! basename || ! strmatch(pp,basename)))                            //  add newest version to newfiles
            zlist_put(ZLnewfs,pp,kk++);                                                //    if not same as basename
         goto report;
      }

      if (massoption < 'M') continue;                                                  //  not a selected files option, stop here

      for (nc = ll = 0; ll < Nself && nc < 100; ll++)                                  //  find matching select files
         if (strmatch(rootname,file_rootname(zlist_get(ZLselfs,ll))))
            ppselect[nc++] = zlist_get(ZLselfs,ll);

      if (nc == 0) {
         for (jj = ii1; jj <= ii2; jj++)                                               //  none, add all oldfiles to newfiles
            zlist_put(ZLnewfs,zlist_get(ZLoldfs,jj),kk++);
         goto report;                                                                  //  album unchanged
      }

      if (massoption == 'M')                                                           //  Replace all with selected versions
      {
         for (ll = 0; ll < nc; ll++)                                                   //  loop selected files
            zlist_put(ZLnewfs,ppselect[ll],kk++);                                      //  add selected files to newfiles
         goto report;
      }

      if (massoption == 'N')                                                           //  Replace all versions with selected versions
      {
         if (basename) {
            for (jj = ii1; jj <= ii2; jj++)                                            //  oldfiles includes basename?
               if (strmatch(basename,zlist_get(ZLoldfs,jj))) break;
            if (jj <= ii2) zlist_put(ZLnewfs,basename,kk++);                           //  yes, add basename to newfiles
         }

         for (ll = 0; ll < nc; ll++)                                                   //  loop selected files
            if (! basename || ! strmatch(basename,ppselect[ll]))                       //  if selected file not basename
               zlist_put(ZLnewfs,ppselect[ll],kk++);                                   //    add selected file to newfiles

         goto report;
      }

      if (massoption == 'O')                                                           //  Add selected versions to existing versions
      {
         for (jj = ii1; jj <= ii2; jj++)                                               //  add all oldfiles to newfiles
            zlist_put(ZLnewfs,zlist_get(ZLoldfs,jj),kk++);

         for (ll = 0; ll < nc; ll++) {                                                 //  loop selected files
            for (jj = ii1; jj <= ii2; jj++)                                            //  loop oldfiles
               if (strmatch(zlist_get(ZLoldfs,jj),ppselect[ll])) break;                //  is selected file included in oldfiles?
            if (jj > ii2) zlist_put(ZLnewfs,ppselect[ll],kk++);                        //  no, add selected file to newfiles
         }

         goto report;
      }

      if (massoption == 'P')                                                           //  Replace all with original + selected vers.
      {
         if (basename) zlist_put(ZLnewfs,basename,kk++);                               //  add basename to newfiles

         for (ll = 0; ll < nc; ll++)                                                   //  loop selected files
            if (! basename || ! strmatch(basename,ppselect[ll]))                       //  if selected file not basename
               zlist_put(ZLnewfs,ppselect[ll],kk++);                                   //    add selected file to newfiles

         goto report;
      }

   report:                                                                             //  report old and new/replacement files

      kk2 = kk - 1;                                                                    //  newfiles range: kk1-kk2  oldfiles: ii1-ii2

      Fchanged = 0;
      if (kk2-kk1 != ii2-ii1) Fchanged = 1;                                            //  report only changed file groups

      if (! Fchanged) {
         for (jj = ii1; jj <= ii2; jj++)
            if (zlist_find(ZLnewfs,zlist_get(ZLoldfs,jj),0) < 0) break;
         if (jj < ii2) Fchanged = 1;
      }

      if (! Fchanged) {
         for (jj = kk1; jj <= kk2; jj++)
            if (zlist_find(ZLoldfs,zlist_get(ZLnewfs,jj),0) < 0) break;
         if (jj < kk2) Fchanged = 1;
      }

      if (! Fchanged) continue;

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      popup_report_write(zdpop,0,"old files: ");                                       //  report old files group

      for (jj = ii1; jj <= ii2; jj++) {
         pp = zlist_get(ZLoldfs,jj);
         pp = strrchr(pp,'/');
         popup_report_write(zdpop,0,"%s ",pp+1);
      }

      popup_report_write(zdpop,0,"\n");

      popup_report_write(zdpop,0,"new files: ");                                       //  report corresp. new files group

      for (jj = kk1; jj <= kk2; jj++) {
         pp = zlist_get(ZLnewfs,jj);
         pp = strrchr(pp,'/');
         popup_report_write(zdpop,0,"%s ",pp+1);
      }

      popup_report_write(zdpop,0,"\n");
   }

   Nnew = kk;                                                                          //  new file count in ZLnewfs

   if (ZLselfs) zlist_free(ZLselfs);
   return;
}


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

   cycle wallpaper function

   Periodic update of desktop wallpaper image,
   using Fotocx album name or any folder containing image files.

   Called via Fotocx command line: $ fotocx -cwp NNN container
      NNN         update interval in seconds
      container   a Fotocx album name or folder pathname

   if NNN = 0, make one wallpaper update and exit

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

void m_cycle_wallpaper(int argc, ch **argv)
{
   zlist_t  *ZLfiles = 0;
   int      ii, err, Nfiles, secs, nextF;
   ch       *pp, *container, **imagefiles, **wprecs;
   ch       albumfile[500], wpfile[500];
   FILE     *fid;

   printf("m_cycle_wallpaper \n");
   printf("cycle wallpaper: secs: %s  container: %s \n",argv[2],argv[3]);

   if (argc < 4) {
      printf("*** missing command parameters \n");
      zexit(0,"cycle wallpaper exit");
   }

   secs = atoi(argv[2]);

   container = argv[3];

   if (! *container) {
      printf("*** no image container specified \n");
      zexit(0,"cycle wallpaper exit");
   }

   if (*container == '/')                                                              //  container is folder path name
   {
      if (! dirfile(container)) {
         printf("*** image container not a valid folder \n");
         zexit(0,"cycle wallpaper exit");
      }

      err = find_imagefiles(container,1,imagefiles,Nfiles);                            //  get list of image files
      if (err || Nfiles == 0) {
         printf("*** image container is empty \n");
         zexit(0,"cycle wallpaper exit");
      }

      if (Nfiles > 1) HeapSort(imagefiles,Nfiles);                                     //  sort alphabetically
   }

   else                                                                                //  container is a fotocx album name
   {
      snprintf(albumfile,500,"%s/%s",albums_folder,container);                         //  album file name

      ZLfiles = zlist_from_file(albumfile);                                            //  get contained files
      if (! ZLfiles) goto filerr;

      Nfiles = zlist_count(ZLfiles);
      if (! Nfiles) goto filerr;

      imagefiles = (ch **) zmalloc(Nfiles * sizeof(ch *),"wallpaper");                 //  allocate memory for file list

      for (ii = Nfiles = 0; ii < zlist_count(ZLfiles); ii++) {                         //  screen for image files
         imagefiles[Nfiles] = zlist_get(ZLfiles,ii);
         if (image_file_type(imagefiles[Nfiles]) == IMAGE) Nfiles++;
      }

      printf("album contains %d image files \n",Nfiles);                               //  keep ZLfiles in memory
      if (Nfiles == 0) zexit(0,"cycle wallpaper exit");
   }

   snprintf(wpfile,500,"%s/wallpaper",get_zhomedir());                                 //  get last wallpaper file used
   ii = zreadfile(wpfile,wprecs);
   if (ii < 1) pp = "no wallpaper file";
   else pp = wprecs[0];

   for (ii = 0; ii < Nfiles; ii++)                                                     //  find in file list
      if (strmatch(pp,imagefiles[ii])) break;
   if (ii < Nfiles) nextF = ii;                                                        //  found
   else nextF = -1;                                                                    //  not found, set first file - 1

   while (true)                                                                        //  loop forever
   {
      nextF++;                                                                         //  next file
      if (nextF >= Nfiles) nextF = 0;                                                  //  last + 1 --> first
      ii = nextF;
      printf("image file: %s \n",imagefiles[ii]);
      curr_file = imagefiles[ii];
      m_wallpaper(0,0);                                                                //  file --> desktop wallpaper
      fid = fopen(wpfile,"w");
      if (! fid) goto filerr;                                                          //  write file to wallpaper file
      fprintf(fid,"%s",imagefiles[ii]);
      fclose(fid);
      if (secs <= 0) zexit(0,"exit fotocx wallpaper");
      zsleep(secs);
   }

filerr:
   zexit(0,"cycle wallpaper file error: %s",strerror(errno));
}


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

//  slide show function

int   ss_dialog_event(zdialog *zd, ch *event);                                         //  main dialog

int   ss_timerfunc(void *);                                                            //  timer function
int   ss_nextimage(int current);                                                       //  get next image to show
int   ss_nextrans();                                                                   //  select next transition to use
int   ss_showtext(int, int, int);                                                      //  show filename/title/description
PXB  *ss_loadpxb(ch *file);                                                            //  load image as PXB pixmap

void  ss_transprefs_dialog();                                                          //  edit transition preferences
void  ss_imageprefs_dialog();                                                          //  edit image preferences
void  ss_imageprefs_dialog();

void  ss_loadprefs();                                                                  //  load preferences from file
void  ss_saveprefs();                                                                  //  write preferences to file

void  ss_blankwindow();                                                                //  blank the window
void  ss_instant();                                                                    //  transition functions
void  ss_fadein();
void  ss_rollright();
void  ss_slidefadein();
void  ss_rolldown();
void  ss_venetian();
void  ss_grate();
void  ss_rectangle();
void  ss_contract();
void  ss_expand();
void  ss_radar();
void  ss_japfan();
void  ss_spiral();
void  ss_ellipse();
void  ss_raindrops();
void  ss_doubledoor();
void  ss_rotate();
void  ss_fallover();
void  ss_spheroid();
void  ss_turnpage();
void  ss_frenchdoor();
void  ss_windmill();
void  ss_pixelize();
void  ss_twist();
void  ss_Xopen();
void  ss_squishout();
void  ss_disintegrate();
void  ss_fliptiles();
void  ss_interleave();
void  ss_explode();
void  ss_implode();
void  ss_zoom_setup(ch *file);
void  ss_zoom_start(float zoom);
PXB * ss_zoom_wait();
void  ss_zoomin();
void  ss_zoomout();

ch       *ss_albumfile = 0;                                                            //  slide show album file
ch       *ss_albumname = 0;                                                            //  album name (ss_albumfile tail)
int      ss_Nfiles = 0;                                                                //  album file count
int      ss_imagetime = 0;                                                             //  image display default time
int      ss_texttime = 0;                                                              //  show text time
int      ss_cliplimit = 10;                                                            //  image clipping limit from user
ch       ss_musicfile[500] = "none";                                                   //  /folder.../musicfile.ogg
ch       *ss_vlc_command = "cvlc --noloop --play-and-exit";                            //  play audio file once and quit         26.0
int      ss_vlcnotify = 0;                                                             //  flag, missing VLC notified
int      ss_playmusic = 0;                                                             //  flag, play given music file
int      ss_randomimage = 0;                                                           //  use random image order
int      ss_randomtrans = 0;                                                           //  use random transition order
int      ss_fullscreen = 0;                                                            //  flag, full screen mode
int      ss_replay = 0;                                                                //  flag, start over when done
float    ss_trantime = 4.0;                                                            //  default transition time, seconds
float    ss_zoomsize = 1.0;                                                            //  zoom size, 1-3.0 = 3x
int      ss_zoomtype = 0;                                                              //  0/1/2 = none/zoomin/zoomout
int      ss_zoomtime = 2;                                                              //  zoom time, seconds
int      ss_zoomlocx, ss_zoomlocy;                                                     //  zoom target (50/50 = image midpoint)
int      ss_setzloc;                                                                   //  1-shot flag for image prefs dialog
int      ss_ww, ss_hh, ss_rs;                                                          //  slide show image size, rowstride
ch       *ss_oldfile, *ss_newfile;                                                     //  image files for transition
PXB      *ss_pxbold, *ss_pxbnew;                                                       //  PXB pixmap images: old, new
double   ss_timer = 0;                                                                 //  slide show timer
ch       *ss_event;                                                                    //  slide show event
int      ss_escape;                                                                    //  flag, kill running slide show
int      ss_paused;                                                                    //  slide show paused status
int      ss_Fcurrent = 0;                                                              //  current image file
int      ss_isblank = 0;                                                               //  window is blank
ch       ss_KBkeyB = 'B';                                                              //  KB key: blank screen
ch       ss_KBkeyN = 'N';                                                              //  KB key: transition to next image
ch       ss_KBkeyP = 'P';                                                              //  KB key: pause /resume
ch       ss_KBkeyX = 'X';                                                              //  KB key: magnify image
int      ss_NT;                                                                        //  threads for transition funcs

struct ss_trantab_t {                                                                  //  transition table
   ch       tranname[32];                                                              //  transition name
   int      enabled;                                                                   //  enabled or not
   float    trantime;                                                                  //  duration, seconds
   int      preference;                                                                //  relative preference, 0-99
   void     (*func)();                                                                 //  function to perform transition
};

#define     SSNT 30                                                                    //  slide show transition types
#define     SSMAXI 10000                                                               //  max. slide show images

ss_trantab_t  ss_trantab[SSNT];                                                        //  specific slide show transition prefs

int      ss_Tused[SSNT];                                                               //  list of transition types enabled
int      ss_Tlast[SSNT];                                                               //  last transitions used, in order
int      ss_Nused;                                                                     //  count of enabled transitions, 0-SSNT
int      ss_Tnext;                                                                     //  next transition to use >> last one used

ss_trantab_t  ss_trantab_default[SSNT] =                                               //  transition defaults
{    //   name            enab   time    pref   function                               //  (enabled, trantime, preference)
      {  "instant",        1,     0.0,    10,   ss_instant         },
      {  "fade-in",        1,     2.0,    10,   ss_fadein          },
      {  "roll-right",     1,     2.0,    10,   ss_rollright       },                  //  NO BLANKS IN TRANSITION NAMES
      {  "slide-fade-in",  1,     2.0,    10,   ss_slidefadein     },
      {  "roll-down",      1,     2.0,    10,   ss_rolldown        },
      {  "venetian",       1,     2.0,    10,   ss_venetian        },
      {  "grate",          1,     2.0,    10,   ss_grate           },
      {  "rectangle",      1,     2.0,    10,   ss_rectangle       },
      {  "contract",       1,     2.0,    10,   ss_contract        },
      {  "expand",         1,     2.0,    10,   ss_expand          },
      {  "radar",          1,     2.0,    10,   ss_radar           },
      {  "Japan-fan",      1,     2.0,    10,   ss_japfan          },
      {  "Spiral",         1,     2.0,    10,   ss_spiral          },
      {  "ellipse",        1,     2.0,    10,   ss_ellipse         },
      {  "raindrops",      1,     2.0,    10,   ss_raindrops       },
      {  "doubledoor",     1,     2.0,    10,   ss_doubledoor      },
      {  "rotate",         1,     2.0,    10,   ss_rotate          },
      {  "fallover",       1,     2.0,    10,   ss_fallover        },
      {  "spheroid",       1,     2.0,    10,   ss_spheroid        },
      {  "turn-page",      1,     2.0,    10,   ss_turnpage        },
      {  "french-door",    1,     2.0,    10,   ss_frenchdoor      },
      {  "windmill",       1,     2.0,    10,   ss_windmill        },
      {  "pixelize",       1,     2.0,    10,   ss_pixelize        },
      {  "twist",          1,     2.0,    10,   ss_twist           },
      {  "Xopen",          1,     2.0,    10,   ss_Xopen           },
      {  "squish-out",     1,     2.0,    10,   ss_squishout       },
      {  "disintegrate",   1,     2.0,    10,   ss_disintegrate    },
      {  "flip-tiles",     1,     2.0,    10,   ss_fliptiles       },
      {  "explode",        1,     2.0,    10,   ss_explode         },
      {  "implode",        1,     2.0,    10,   ss_implode         }
};

//  note: ss_interleave is omitted (visually unappealing)

struct ss_imagetab_t   {                                                               //  image table
   ch       *imagefile;                                                                //  image file
   int      preference;                                                                //  relative preference, 0-99
   int      tone;                                                                      //  flag, play tone when shown
   int      wait0;                                                                     //  seconds to wait
   int      filesecs;                                                                  //  seconds to show file name
   int      titlesecs;                                                                 //  seconds to show title
   int      descsecs;                                                                  //  seconds to show description
   int      wait1;                                                                     //  seconds to wait
   int      zoomtype;                                                                  //  0/1/2 = none/zoomin/zoomout
   float    zoomsize;                                                                  //  zoom size, 1-3.0 = 3x
   int      zoomtime;                                                                  //  zoom time, seconds
   int      zoomlocx, zoomlocy;                                                        //  zoom target (50/50 = image midpoint)
   int      wait2;                                                                     //  seconds to wait
   ch       tranname[32];                                                              //  transition type to use
};

ss_imagetab_t  ss_imagetab[SSMAXI];                                                    //  specific slide show image table


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

//  menu function - start or stop a slide show

void m_slideshow(GtkWidget *, ch *)
{
   zdialog     *zd;
   int         zstat, ii;
   ch          *pp;

   F1_help_topic = TX("SLIDE SHOW");
   printf("m_slideshow \n");

   if (Fblock("slide show")) return;

   ss_NT = NSMP - 2;                                                                   //  use SMP count from settings           25.0
   if (ss_NT < 4) ss_NT = 4;

/***
       ________________________________________________________
      |               Slide Show                               |
      |                                                        |
      | [Select Album] album-name  123 images                  |
      | text time [___]  Image Time [___]  Clip Limit % [___]  |
      | Music File: [_______________________________] [Browse] |
      | [x] Full Screen  [x] Auto-replay  [x] Random Image     |
      | Customize: [transitions]  [image files]  [KB keys]     |
      |                                                        |
      |                                   [Start] [Resume] [X] |
      |________________________________________________________|

***/

   zd = zdialog_new("Slide Show",Mwin,TX("Start"),TX("Resume"),"X",null);

   zdialog_add_widget(zd,"hbox","hbss","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","selectalbum","hbss",TX("Select Album"),"space=5");
   zdialog_add_widget(zd,"label","albumname","hbss",TX("no selection"),"space=5");
   zdialog_add_widget(zd,"label","nfiles","hbss",TX("no images"),"space=5");

   zdialog_add_widget(zd,"hbox","hbprefs","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labtexttime","hbprefs",TX("text time"),"space=5");
   zdialog_add_widget(zd,"zspin","texttime","hbprefs","0|9999|1|0");
   zdialog_add_widget(zd,"label","space","hbprefs",0,"space=5");
   zdialog_add_widget(zd,"label","labimagetime","hbprefs",TX("Image Time"),"space=5");
   zdialog_add_widget(zd,"zspin","imagetime","hbprefs","1|9999|1|3");
   zdialog_add_widget(zd,"label","space","hbprefs",0,"space=5");
   zdialog_add_widget(zd,"label","labclip","hbprefs",TX("Clip Limit %"),"space=5");
   zdialog_add_widget(zd,"zspin","cliplim","hbprefs","0|50|1|0");

   zdialog_add_widget(zd,"hbox","hbmuf","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labmf","hbmuf",TX("Music File"),"space=3");
   zdialog_add_widget(zd,"zentry","musicfile","hbmuf","none","size=30|space=5");
   zdialog_add_widget(zd,"button","browse","hbmuf",TX("Browse"),"space=2");

   zdialog_add_widget(zd,"hbox","hbscreen","dialog",0,"space=2");
   zdialog_add_widget(zd,"check","fullscreen","hbscreen",TX("Full Screen"),"space=3");
   zdialog_add_widget(zd,"check","replay","hbscreen",TX("Auto-replay"),"space=5");
   zdialog_add_widget(zd,"check","randomimage","hbscreen",TX("Random Image"),"space=5");

   zdialog_add_widget(zd,"hbox","hbcust","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labprefs","hbcust",TX("Customize:"),"space=5");
   zdialog_add_widget(zd,"button","transprefs","hbcust",TX("transitions"),"space=5");
   zdialog_add_widget(zd,"button","imageprefs","hbcust",TX("image files"),"space=5");
   zdialog_add_widget(zd,"button","KBkeys","hbcust",TX("KB keys"),"space=5");

   zdialog_add_ttip(zd,"texttime",TX("seconds to show with text"));
   zdialog_add_ttip(zd,"imagetime",TX("seconds to show without text"));
   zdialog_add_ttip(zd,"cliplim",TX("maximum clip allowed to fit window"));
   zdialog_add_ttip(zd,"replay",TX("start over at end of album"));
   zdialog_add_ttip(zd,"randomimage",TX("select images randomly"));
   zdialog_add_ttip(zd,"Start",TX("start from first image in album"));
   zdialog_add_ttip(zd,"Resume",TX("resume from current image"));

   zdialog_run(zd,ss_dialog_event,"save");                                             //  run dialog
   zdialog_send_event(zd,"initz_album");                                               //  initial album

retry:

   zstat = zdialog_wait(zd);                                                           //  wait for completion

   if (zstat != 1 && zstat != 2) {                                                     //  cancel
      ss_saveprefs();                                                                  //  26.0
      zdialog_free(zd);
      Fpanelshow = 1;
      Fblock(0);
      return;
   }

   zdialog_fetch(zd,"imagetime",ss_imagetime);                                         //  image show time seconds
   if (ss_Nused == 0) ss_imagetime = 9999;                                             //  if only arrow-keys used, huge interval
   zdialog_fetch(zd,"texttime",ss_texttime);                                           //  show text time
   zdialog_fetch(zd,"cliplim",ss_cliplimit);                                           //  image clipping limit
   zdialog_fetch(zd,"musicfile",ss_musicfile,500);                                     //  music file
   zdialog_fetch(zd,"fullscreen",ss_fullscreen);                                       //  full screen option
   zdialog_fetch(zd,"replay",ss_replay);                                               //  replay (last image --> first)
   zdialog_fetch(zd,"randomimage",ss_randomimage);                                     //  use random image order

   if (! ss_Nfiles) {                                                                  //  no selection
      zmessageACK(Mwin,TX("invalid album"));
      zd->zstat = 0;
      goto retry;
   }

   ss_playmusic = 0;

   if (*ss_musicfile > ' ' && ! strmatch(ss_musicfile,"none"))                         //  music file was entered
   {
      if (! Fvlc && ! ss_vlcnotify) {
         zmessageACK(Mwin,TX("VLC program is required to play music"));
         ss_vlcnotify = 1;
      }
      if (Fvlc && ! regfile(ss_musicfile)) {
         zmessageACK(Mwin,TX("music file not found"));
         zd->zstat = 0;
         goto retry;
      }

      if (Fvlc) ss_playmusic = 1;
   }

   zdialog_free(zd);                                                                   //  kill dialog

   ss_saveprefs();                                                                     //  save preference changes

   ss_nextimage(-1);                                                                   //  initialization

   if (zstat == 1) ii = 0;                                                             //  [start] selected, start from first file

   else if (zstat == 2)                                                                //  [resume] selected
   {
      if (curr_file) {                                                                 //  start from curr. file
         for (ii = 0; ii < ss_Nfiles; ii++)                                            //    if member of file list
            if (strmatch(curr_file,ss_imagetab[ii].imagefile)) break;
         if (ii == ss_Nfiles) ii = 0;
      }
      else ii = 0;
   }

   ss_Fcurrent = ii;                                                                   //  starting file

   f_open(ss_imagetab[ii].imagefile);                                                  //  first image

   Fpanelshow = 0;
   viewmode('F');                                                                      //  file view without panel

   Fslideshow = 1;                                                                     //  slideshow active for KB events
   ss_isblank = 0;                                                                     //  not blank window
   ss_event = "first";
   Fzoom = 0;                                                                          //  fit window

   if (ss_playmusic) {                                                                 //  play music file
      pp = zescape_quotes(ss_musicfile);
      zshell("log ack","%s \"%s\" ",ss_vlc_command,pp);                                //  26.0
      zfree(pp);
   }

   ss_newfile = 0;                                                                     //  no new image
   ss_pxbnew = 0;
   ss_oldfile = 0;                                                                     //  no old (prior) image
   ss_pxbold = 0;
   ss_escape = 0;                                                                      //  kill switch off

   if (ss_fullscreen) win_fullscreen(1);                                               //  full screen, hide menu and panel

   zmainsleep(1);                                                                      //  necessary: compensate GDK bug

   ss_ww = gdk_window_get_width(gdkwin);                                               //  get window size
   ss_hh = gdk_window_get_height(gdkwin);                                              //  (height incorrect without above sleep)

   g_timeout_add(100,ss_timerfunc,0);                                                  //  start timer for image changes
                                                                                       //  when done: unblock, restart slide show
   return;
}


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

//  dialog event function - file chooser for images to show and music file

int ss_dialog_event(zdialog *zd, ch *event)
{
   ch       *file, *pp;
   ch       countmess[50];
   int      err;

   ch       *KBkeys_tip = TX("KB controls: \n"
                             "Esc: exit running slide show \n"
                             "arrow keys: prev/next image \n"
                             "B: blank / unblank window \n"
                             "N: transition to next image \n"
                             "P: pause / resume \n"
                             "X: magnify at mouse position");

   if (zd->zstat == 1) {                                                               //  [proceed]
      if (ss_Nfiles) return 1;
      zmessageACK(Mwin,TX("invalid album"));                                           //  diagnose and keep dialog open
      zd->zstat = 0;
      return 1;
   }

   if (zd->zstat) return 1;                                                            //  cancel

   if (strmatch(event,"initz_album"))
   {
      if (! ss_albumfile && curr_album)
         ss_albumfile = zstrdup(curr_album,"ss");
      if (! ss_albumfile) return 1;
      pp = strrchr(ss_albumfile,'/');
      if (! pp) return 1;
      ss_albumname = pp + 1;
      if (strmatch(ss_albumname,"gallery")) {
         err = album_create_from_gallery(ss_albumfile);
         if (err) return 1;
      }
      goto initz_album;
   }

   if (strmatch(event,"selectalbum"))                                                  //  select a slide show album
   {
      ss_Nfiles = 0;                                                                   //  reset album data
      ss_albumfile = ss_albumname = 0;
      zdialog_stuff(zd,"albumname",TX("no selection"));
      zdialog_stuff(zd,"nfiles",TX("no images"));

      file = zgetfile(TX("open album"),MWIN,"file",albums_folder);                     //  user file selection
      if (! file) return 1;

      ss_albumfile = file;
      pp = strrchr(ss_albumfile,'/');                                                  //  get album name
      if (! pp) return 1;
      ss_albumname = pp + 1;
      goto initz_album;
   }

   if (strmatch(event,"browse")) {                                                     //  browse for music file
      pp = ss_musicfile;
      pp = zgetfile(TX("Select music file"),MWIN,"file",pp);
      if (! pp) pp = zstrdup("none","ss");
      zdialog_stuff(zd,"musicfile",pp);
      zfree(pp);
   }

   if (! ss_Nfiles) return 1;

   if (strmatch(event,"transprefs")) {                                                 //  edit transition preferences
      zdialog_show(zd,0);
      ss_transprefs_dialog();
      zdialog_show(zd,1);
   }

   if (strmatch(event,"imageprefs")) {                                                 //  edit image preferences
      zdialog_show(zd,0);
      ss_imageprefs_dialog();
      zdialog_show(zd,1);
   }

   if (strmatch(event,"KBkeys"))
      zmessageACK(zd->dialog,KBkeys_tip);

   return 1;

initz_album:

   pp = strrchr(ss_albumfile,'/');
   if (! pp) return 1;
   ss_albumname = pp + 1;

   ss_loadprefs();                                                                     //  get slide show prefs or defaults
   if (! ss_Nfiles) return 1;

   zdialog_stuff(zd,"albumname",ss_albumname);                                         //  update dialog album data
   snprintf(countmess,50,TX("%d images"),ss_Nfiles);
   zdialog_stuff(zd,"nfiles",countmess);
   zdialog_stuff(zd,"imagetime",ss_imagetime);
   zdialog_stuff(zd,"texttime",ss_texttime);
   zdialog_stuff(zd,"cliplim",ss_cliplimit);
   zdialog_stuff(zd,"musicfile",ss_musicfile);
   zdialog_stuff(zd,"fullscreen",ss_fullscreen);
   zdialog_stuff(zd,"replay",ss_replay);
   zdialog_stuff(zd,"randomimage",ss_randomimage);

   album_show(ss_albumfile);                                                           //  open slide show album
   return 1;
}


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

//  process keyboard input key - directed here from main()

void ss_KBfunc(int kbkey)
{
   if ((kbkey == GDK_KEY_Escape) ||
       (kbkey == GDK_KEY_q) ||
       (kbkey == GDK_KEY_F10) ||
       (kbkey == GDK_KEY_F11))
   {
      ss_escape = 1;
      ss_event = "escape";
      return;
   }

   kbkey = toupper(kbkey);
   if (kbkey == ss_KBkeyB) ss_event = "blank";                                         //  blank window
   if (kbkey == ss_KBkeyN) ss_event = "tran next";                                     //  transition to next image
   if (kbkey == ss_KBkeyX) ss_event = "magnify";                                       //  magnify image tool
   if (kbkey == GDK_KEY_Left) ss_event = "prior";                                      //  prior image
   if (kbkey == GDK_KEY_Right) ss_event = "next";                                      //  next image
   if (kbkey == ss_KBkeyP) {
      ss_paused = 1 - ss_paused;                                                       //  toggle paused / running status
      if (! ss_paused) ss_event = "tran next";
   }

   return;
}


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

//  Show next slide when time is up or user navigates with arrow keys.
//  Cycles every 0.1 seconds when slide show is active.

int ss_timerfunc(void *)
{
   int ss_timerfunc_sleep(int sleepsecs);

   int      img, jj;
   int      filesecs, titlesecs, descsecs, sleepsecs;
   ch       *tagnames[2] = { meta_title_tag, meta_desc_tag };
   ch       *tagvals[2], *pp;
   ch       tonefile[200];

   if (zd_magnify) return 1;                                                           //  exit magnify before event processing

   if (ss_ww != gdk_window_get_width(gdkwin)) goto escape;                             //  window size changed
   if (ss_hh != gdk_window_get_height(gdkwin)) goto escape;

   if (ss_escape) ss_event = "escape";                                                 //  KB event from main()
   if (! Fslideshow) ss_event = "escape";                                              //  from m_quit()
   if (FGM != 'F') ss_event = "escape";                                                //  must be F-view mode
   if (strmatch(ss_event,"")) return 1;                                                //  no event, loop and wait
   if (strmatch(ss_event,"escape")) goto escape;
   if (strmatch(ss_event,"blank")) goto blank;
   if (strmatch(ss_event,"first")) goto first;
   if (strmatch(ss_event,"EOL")) goto EOL;
   if (strmatch(ss_event,"show")) goto show;
   if (strmatch(ss_event,"next")) goto next;
   if (strmatch(ss_event,"tran next")) goto tran_next;
   if (strmatch(ss_event,"magnify")) goto magnify;
   if (strmatch(ss_event,"prior")) goto prior;
   goto escape;                                                                        //  unknown event --> escape

first:                                                                                 //  start slide show
   ss_paused = 0;                                                                      //  at ss_Fcurrent image
   ss_event = "show";
   return 1;

EOL:                                                                                   //  last image reached
   zmessage_post(Mwin,"parent",3,TX("SLIDESHOW END"));
   ss_timerfunc_sleep(3);
   img = ss_Fcurrent = ss_Nfiles - 1;                                                  //  set last image

escape:                                                                                //  terminate slide show
   ss_escape = 0;
   if (ss_pxbold) PXB_free(ss_pxbold);                                                 //  free memory
   if (ss_pxbnew) PXB_free(ss_pxbnew);
   ss_pxbold = ss_pxbnew = 0;
   if (*ss_musicfile == '/') zshell(0,"pkill vlc");                                    //  kill music if any
   if (ss_fullscreen) win_unfullscreen();                                              //  restore old window size, menu etc.
   ss_fullscreen = 0;
   viewmode('G');
   img = ss_Fcurrent;
   f_open(ss_imagetab[img].imagefile);
   Fblock(0);
   if (Fslideshow) {                                                                   //  not killed
      Fslideshow = 0;
      m_slideshow(0,0);                                                                //  restart slide show
   }
   return 0;

blank:                                                                                 //  blank window and wait
   ss_isblank = 1 - ss_isblank;
   if (ss_isblank) {
      ss_event = "";
      ss_blankwindow();
   }
   else ss_event = "show";
   return 1;

magnify:                                                                               //  magnify image
   ss_event = "";
   img = ss_Fcurrent;
   f_open(ss_imagetab[img].imagefile);
   m_magnify(0,0);
   ss_event = "show";
   return 1;

prior:                                                                                 //  back to prior image
   ss_Fcurrent--;
   if (ss_Fcurrent < 0) {
      if (ss_replay) ss_Fcurrent = ss_Nfiles-1;                                        //  wrap to last image
      else ss_Fcurrent = 0;
   }
   ss_event = "show";
   return 1;

show:                                                                                  //  current image instantly
next:                                                                                  //  next image instantly
tran_next:                                                                             //  transition to next image

   if (zstrstr(ss_event,"next"))                                                       //  next or tran next
   {
      ss_Fcurrent = ss_nextimage(ss_Fcurrent);
      if (ss_Fcurrent < 0) {
         ss_event = "EOL";                                                             //  last image file was shown
         return 1;
      }
   }

   img = ss_Fcurrent;                                                                  //  new current image
   ss_oldfile = ss_newfile;                                                            //  old file = new
   if (ss_pxbold) PXB_free(ss_pxbold);
   ss_pxbold = ss_pxbnew;                                                              //  new pixbuf --> old

   ss_newfile = ss_imagetab[img].imagefile;                                            //  new current file
   ss_pxbnew = ss_loadpxb(ss_newfile);                                                 //  new pixbuf
   if (! ss_pxbnew) {
      ss_event = "escape";                                                             //  failure, quit slide show
      return 1;
   }

   ss_zoomsize = ss_imagetab[img].zoomsize;                                            //  zoom size
   ss_zoomtype = ss_imagetab[img].zoomtype;                                            //  0/1/2 = none/zoomin/zoomout
   ss_zoomlocx = ss_imagetab[img].zoomlocx;                                            //  target location for final center
   ss_zoomlocy = ss_imagetab[img].zoomlocy;

   if (ss_zoomsize > 1 && ss_zoomtype == 2) {                                          //  next image will be zoomed out
      PXB_free(ss_pxbnew);
      ss_zoom_setup(ss_newfile);                                                       //  initial image is zoomed image
      ss_zoom_start(ss_zoomsize);
      ss_pxbnew = ss_zoom_wait();
   }

   if (strmatch(ss_event,"tran next")) {                                               //  do transition to new image
      ss_event = "";                                                                   //  erase event
      jj = ss_nextrans();                                                              //  select next transition type
      ss_trantime = ss_trantab[jj].trantime;                                           //  set transition duration
      ss_trantab[jj].func();                                                           //  call transition function
      if (*ss_event) return 1;                                                         //  new event during transition
   }
   else ss_instant();                                                                  //  show new image immediately

   if (! ss_fullscreen)                                                                //  if not full screen mode,
      gtk_window_set_title(MWIN,ss_newfile);                                           //    put filename in title bar

//  process image events:
//    play tone, wait before text, show text,
//    wait before zoom, zoom, wait after zoom

   if (ss_imagetab[img].tone) {
      snprintf(tonefile,200,"%s/slideshow-tone.oga",get_zhomedir());                   //  play tone, quit
      zshell(0,"%s \"%s\" &",ss_vlc_command,tonefile);                                 //  26.0
   }

   sleepsecs = ss_imagetab[img].wait0;                                                 //  show image specified time
   jj = ss_timerfunc_sleep(sleepsecs);                                                 //    before filename/titles/description
   if (jj) return 1;

   filesecs = ss_imagetab[img].filesecs;                                               //  time to show file name
   titlesecs = ss_imagetab[img].titlesecs;                                             //  time to show title
   descsecs = ss_imagetab[img].descsecs;                                               //  time to show description
   if (filesecs + titlesecs + descsecs == 0)                                           //  if not specified, use default
      filesecs = titlesecs = descsecs = ss_texttime;
   meta_get(ss_newfile,tagnames,tagvals,2);                                            //  get image titles and description
   if (! tagvals[0]) titlesecs = 0;                                                    //  zero time if no title
   else zfree(tagvals[0]);
   if (! tagvals[1]) descsecs = 0;                                                     //  zero time if no description
   else zfree(tagvals[1]);

   while (filesecs + titlesecs + descsecs)
   {
      ss_showtext(filesecs,titlesecs,descsecs);                                        //  show items with remaining time
      jj = ss_timerfunc_sleep(1.0);                                                    //  wait 1 second
      if (jj) return 1;                                                                //  new event
      filesecs -= 1;  if (filesecs < 0) filesecs = 0;                                  //  decrement show times
      titlesecs -= 1;   if (titlesecs < 0) titlesecs = 0;
      descsecs -= 1;  if (descsecs < 0) descsecs = 0;
   }

   ss_showtext(0,0,0);                                                                 //  remove all

   if (image_file_type(ss_newfile) == VIDEO) {                                         //  if VIDEO file, play now
      jj = ss_timerfunc_sleep(1.0);                                                    //  show image one second
      if (jj) return 1;                                                                //  new event
      f_open(ss_newfile);                                                              //  reset current image
      pp = zescape_quotes(ss_newfile);
      zshell("ack","ffplay -loglevel -8 -autoexit \"%s\" ",pp);                        //  play video
      zfree(pp);
      jj = ss_timerfunc_sleep(1.0);                                                    //  wait one second after play
      if (jj) return 1;                                                                //  new event
      if (ss_paused) ss_event = "";
      else ss_event = "tran next";
      return 1;
   }

   sleepsecs = ss_imagetab[img].wait1;                                                 //  show image specified time
   jj = ss_timerfunc_sleep(sleepsecs);                                                 //    before zoom
   if (jj) return 1;

   ss_zoomsize = ss_imagetab[img].zoomsize;                                            //  1-3x = no zoom to 3x zoom
   ss_zoomtype = ss_imagetab[img].zoomtype;                                            //  0/1/2 = none/zoomin/zoomout
   ss_zoomtime = ss_imagetab[img].zoomtime;                                            //  zoom time
   ss_zoomlocx = ss_imagetab[img].zoomlocx;                                            //  target location for final center
   ss_zoomlocy = ss_imagetab[img].zoomlocy;                                            //  (0-100% of image, 50/50 = middle)
   if (ss_zoomsize > 1.0) {
      ss_event = "";                                                                   //  erase event
      if (ss_zoomtype == 1) ss_zoomin();                                               //  zoom in or zoom out
      if (ss_zoomtype == 2) ss_zoomout();
      if (*ss_event) return 1;                                                         //  new event during zoom
   }

   sleepsecs = ss_imagetab[img].wait2;                                                 //  show zoomed image specified time
   if (sleepsecs == 0) sleepsecs = ss_imagetime;                                       //  not set, use overall default
   jj = ss_timerfunc_sleep(sleepsecs);
   if (jj) return 1;                                                                   //  new event

   if (ss_paused) ss_event = "";
   else ss_event = "tran next";                                                        //  do next image
   return 1;
}


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

//  sleep designated seconds and return 0 if no ss_event happens
//  if ss_event happens during sleep, immediately return 1

int ss_timerfunc_sleep(int sleepsecs)
{
   float    Fsecs = sleepsecs;

   ss_event = "";

   while (Fsecs > 0) {
      zmainsleep(0.1);
      Fsecs -= 0.1;
      if (*ss_event) return 1;
      if (ss_escape) return 1;                                                         //  from main()
   }

   return 0;
}


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

//  get next image to display
//  if mode = sequential, get next image
//  if mode = random, get random image
//  avoid re-use of most recently used images

int ss_nextimage(int currimage)
{
   int            ii, next;
   static float   sumprefs;
   float          frand, rank;
   int            limit;
   static int     lastimage[10];

   if (currimage < 0)                                                                  //  initialization call
   {
      for (ii = 0; ii < 10; ii++)                                                      //  clear memory of most recent files
         lastimage[ii] = -1;

      sumprefs = 0;
      for (ii = 0; ii < ss_Nfiles; ii++)                                               //  compute sum of all image preferences
         sumprefs += ss_imagetab[ii].preference;
      return -1;
   }

   if (! ss_randomimage)                                                               //  sequential image sequence
   {
      if (currimage < ss_Nfiles-1) return currimage + 1;                               //  return next image file
      if (ss_replay) return 0;                                                         //  if EOL and replay wanted, return 1st file
      return -1;                                                                       //  The End
   }

get_random:                                                                            //  random image sequence

   frand = drandz();                                                                   //  random number 0 - 0.999999
   rank = 0;

   for (ii = 0; ii < ss_Nfiles; ii++)                                                  //  choose a random image
   {                                                                                   //    weighted by image preference
      rank += ss_imagetab[ii].preference / sumprefs;
      if (rank >= frand) break;
   }

   next = ii;                                                                          //  selected image

   limit = 10;                                                                         //  search depth = 10
   if (ss_Nfiles/3 < 10) limit = ss_Nfiles/3;                                          //    or 1/3 of files in slide show

   for (ii = 0; ii < limit; ii++)                                                      //  search most recent image files
      if (next == lastimage[ii]) break;
   if (ii < limit) goto get_random;                                                    //  image in recent list, get another

   for (ii = limit-1; ii > 0; ii--)                                                    //  add image to recent list
      lastimage[ii] = lastimage[ii-1];
   lastimage[0] = next;

   return next;                                                                        //  next image to display
}


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

//  select next transition type to use
//  mode = sequential: use enabled transition type in sequence
//  mode = random: choose random from enabled transition type
//  avoid re-use of most recent transition types

int ss_nextrans()
{
   int         ii, jj, maxjj, next;
   float       frand, sumprefs, tranrank;

   ii = ss_Fcurrent - 1;                                                               //  transition type from prior image
   if (ii < 0) ii = ss_Nfiles - 1;

   if (! strmatch(ss_imagetab[ii].tranname,"next")) {                                  //  image transition not "next"
      for (jj = 0; jj < SSNT; jj++)
         if (strmatch(ss_trantab[jj].tranname,ss_imagetab[ii].tranname)) break;
      if (jj < SSNT) {
         next = jj;                                                                    //  assigned transition type
         for (ii = ss_Nused - 1; ii > 0; ii--)                                         //   --> most recently used
            ss_Tlast[ii] = ss_Tlast[ii-1];
         ss_Tlast[0] = next;
         return next;
      }
   }

   if (ss_Nused < 2 || ss_randomtrans == 0)                                            //  few enabled transition types
   {                                                                                   //    or sequential mode
      ss_Tnext++;
      if (ss_Tnext == ss_Nused) ss_Tnext = 0;                                          //  select transition types sequentially
      next = ss_Tused[ss_Tnext];
   }

   else                                                                                //  select transition types randomly
   {
      maxjj = ss_Nused / 2;                                                            //  most recently used to exclude
      if (maxjj > 4) maxjj = 4;                                                        //  max. 4

      sumprefs = 0;

      for (ii = 0; ii < ss_Nused; ii++)                                                //  search enabled transitions
      {
         for (jj = 0; jj < maxjj; jj++)                                                //  exclude most recently used 50%
            if (ss_Tused[ii] == ss_Tlast[jj]) break;
         if (jj < maxjj) continue;

         jj = ss_Tused[ii];                                                            //  transition to count
         sumprefs += ss_trantab[jj].preference;                                        //  sum transition preferences
      }

      frand = drandz();                                                                //  random 0.0 - 0.99999
      tranrank = 0;

      for (ii = 0; ii < ss_Nused; ii++)                                                //  search enabled transitions
      {
         for (jj = 0; jj < maxjj; jj++)                                                //  exclude most recently used 50%
            if (ss_Tused[ii] == ss_Tlast[jj]) break;
         if (jj < maxjj) continue;

         jj = ss_Tused[ii];                                                            //  transition to rank
         tranrank += ss_trantab[jj].preference / sumprefs;                             //  0 ... 1.0
         if (tranrank >= frand) break;
      }

      next = jj;                                                                       //  transition to use

      for (ii = ss_Nused - 1; ii > 0; ii--)                                            //  make it most recent
         ss_Tlast[ii] = ss_Tlast[ii-1];
      ss_Tlast[0] = next;
   }

   return next;
}


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

//  write file name, title and description at the top of the image
//  show only those items with time > 0

int ss_showtext(int filesecs, int titlesecs, int descsecs)
{
   ch          *tagnames[2] = { meta_title_tag, meta_desc_tag };
   ch          *tagvals[2], *pp;
   ch          filename[200], title[1000], description[1000];
   static ch   text1[1000];
   ch          **text2 = 0;
   int         ii, nn;
   PIXBUF      *pixbuf;

   static PangoFontDescription   *pangofont = null;
   static PangoLayout            *pangolayout = null;
   static int                    plww, plhh, pline;

   if (plww) {                                                                         //  clear previous text
      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,0,0,plww+10,plhh+10);
      cairo_t *cr = draw_context_create(gdkwin,draw_context);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);
      draw_context_destroy(draw_context);
      g_object_unref(pixbuf);
      plww = 0;
   }

   if (filesecs + titlesecs + descsecs == 0) return 0;

   pp = strrchr(ss_newfile,'/');                                                       //  get base file name
   if (pp) pp++;
   else pp = ss_newfile;
   strncpy0(filename,pp,200);

   *title = *description = 0;
   meta_get(ss_newfile,(ch **) tagnames,tagvals,2);                                    //  get title and description metadata

   if (tagvals[0]) {
      strncpy0(title,tagvals[0],1000);
      zfree(tagvals[0]);
   }

   if (tagvals[1]) {
      strncpy0(description,tagvals[1],1000);
      zfree(tagvals[1]);
   }

   *text1 = 0;
   pline = 0;

   if (filesecs) {
      strncpy0(text1,filename,200);                                                    //  get items with time remaining
      pline = 1;
   }

   if (titlesecs && *title) {
      if (pline) strncatv(text1,1000,"\n",null);
      strncatv(text1,1000,title,null);
      pline = 1;
   }

   if (descsecs && *description) {
      if (pline) strncatv(text1,1000,"\n",null);
      strncatv(text1,1000,description,null);
   }

   nn = breakup_text(text1,text2,0,captext_cc[0],captext_cc[1]);                       //  break into lines within user limits

   *text1 = 0;
   for (ii = 0; ii < nn; ii++) {                                                       //  combine lines with \n separators
      if (ii == 0) strncpy0(text1,text2[0],1000);
      else strncatv(text1,1000,"\n",text2[ii],null);
      zfree(text2[ii]);
   }

   pangofont = pango_font_description_from_string(zfuncs::appfont);                    //  make pango layout for font
   pangolayout = gtk_widget_create_pango_layout(Fdrawin,0);                            //  Fdrawin instead of Cdrawin
   pango_layout_set_font_description(pangolayout,pangofont);
   pango_layout_set_text(pangolayout,text1,-1);                                        //  add text to layout
   pango_layout_get_pixel_size(pangolayout,&plww,&plhh);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   cairo_set_source_rgb(cr,1,1,1);                                                     //  draw white background
   cairo_rectangle(cr,0,0,plww+10,plhh+10);
   cairo_fill(cr);
   cairo_move_to(cr,5,5);                                                              //  draw layout with text
   cairo_set_source_rgb(cr,0,0,0);
   pango_cairo_show_layout(cr,pangolayout);
   draw_context_destroy(draw_context);

   return 1;
}


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

//  Load image and rescale to fit in window size.
//  If image aspect ratio is close enough to window ratio,
//  truncate to avoid having margins around around the image.

PXB * ss_loadpxb(ch *file)
{
   PXB      *pxbin, *pxbtemp, *pxbout;
   int      ww1, hh1, ww2, hh2;
   int      Iorgx, Iorgy, Worgx, Worgy;
   float    Rm, Rw, dR;

   Dww = gdk_window_get_width(gdkwin);                                                 //  refresh drawing window size
   Dhh = gdk_window_get_height(gdkwin);

   pxbin = PXB_load(file,1);                                                           //  load image
   if (! pxbin) return 0;

   PXB_subalpha(pxbin);                                                                //  remove alpha channel if any

   ww1 = pxbin->ww;                                                                    //  image dimensions
   hh1 = pxbin->hh;

   ww2 = ss_ww;                                                                        //  window dimensions
   hh2 = ss_hh;

   Rm = 1.0 * ww1 / hh1;                                                               //  image width/height ratio
   Rw = 1.0 * ww2 / hh2;                                                               //  window width/height ratio
   dR = fabsf(Rm - Rw) / Rw;                                                           //  discrepancy ratio

   if (dR <= 0.01 * ss_cliplimit) {                                                    //  discrepancy within user limit
      if (Rw >= Rm) {
         ww1 = ww2;                                                                    //  height will be clipped
         hh1 = ww1 / Rm;
      }
      else {
         hh1 = hh2;                                                                    //  width will be clipped
         ww1 = hh1 * Rm;
      }
   }
   else {                                                                              //  discrepancy too great
      if (Rw >= Rm) {
         hh1 = hh2;                                                                    //  ratio image to fit in window
         ww1 = hh1 * Rm;
      }
      else {
         ww1 = ww2;
         hh1 = ww1 / Rm;
      }
   }

   pxbtemp = PXB_rescale_fast(pxbin,ww1,hh1);
   PXB_free(pxbin);

   Iorgx = (ww1 - ww2) / 2.0;                                                          //  top left corner of image to copy from
   if (Iorgx < 0) Iorgx = 0;
   Iorgy = (hh1 - hh2) / 2.0;
   if (Iorgy < 0) Iorgy = 0;

   Worgx = (ww2 - ww1) / 2.0;                                                          //  top left corner of window to copy to
   if (Worgx < 0) Worgx = 0;
   Worgy = (hh2 - hh1) / 2.0;
   if (Worgy < 0) Worgy = 0;

   if (ww2 < ww1) ww1 = ww2;                                                           //  copy width
   if (hh2 < hh1) hh1 = hh2;                                                           //  copy height

   pxbout = PXB_make(ww2,hh2,3);                                                       //  output PXB
   if (! pxbout) return 0;                                                             //  26.3
   PXB_clear(pxbout);

   PXB_copy_area(pxbtemp,Iorgx,Iorgy,ww1,hh1,pxbout,Worgx,Worgy);
   PXB_free(pxbtemp);

   ss_rs = pxbout->rs;                                                                 //  set image row stride
   return pxbout;
}


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

//  set transitions preferences for specific slide show

void ss_transprefs_dialog()
{
   int  transprefs_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd;
   int         ii, jj, zstat;
   ch          nameii[32], enabii[8], timeii[8], prefii[8];

/***
       __________________________________________________________________________
      |                                                                          |
      |  Transitions File [load] [save]    transition filename                   |
      |                                                                          |
      |  Select [all] [none] [x] random        time [___]  [set all]             |
      |                                                                          |
      |  transition   use   time     pref   |  transition   use   time     pref  |
      |  instant      [ ]  [ 1.0 ]  [ 10 ]  |  raindrops    [x]  [ 1.3 ]  [ 10 ] |
      |  fade-in      [x]  [ 1.5 ]  [ 20 ]  |  doubledoor   [x]  [ 0.8 ]  [ 20 ] |
      |  roll-right   [x]  [ 2.0 ]  [  0 ]  |  rotate       [ ]  [ 1.4 ]  [ 50 ] |
      |  ....         ...   .....    ....   |  ....         ...   .....    ....  |
      |                                                                          |
      |                                                                 [OK] [X] |
      |__________________________________________________________________________|

***/

   if (! ss_Nfiles) {
      zmessageACK(Mwin,TX("invalid album"));
      return;
   }

   zd = zdialog_new(TX("Transition Preferences"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfile","hbfile",TX("Transitions File"),"space=3");
   zdialog_add_widget(zd,"button","load","hbfile","Load","space=3");
   zdialog_add_widget(zd,"button","save","hbfile","Save","space=3");
   zdialog_add_widget(zd,"label","tranfile","hbfile",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbopts","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtran","hbopts",TX("Select"),"space=3");
   zdialog_add_widget(zd,"button","all","hbopts",TX("All"),"space=3");
   zdialog_add_widget(zd,"button","none","hbopts",TX("None"),"space=3");
   zdialog_add_widget(zd,"check","randomtrans","hbopts",TX("random"),"space=3");

   zdialog_add_widget(zd,"label","space","hbopts","","space=10");
   zdialog_add_widget(zd,"label","labtime","hbopts",TX("time"),"space=3");
   zdialog_add_widget(zd,"zspin","alltime","hbopts","1|99|1|3","size=4");
   zdialog_add_widget(zd,"button","setalltime","hbopts",TX("set all"));

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vsep","vs1","hb1",0,"space=5");                              //  two columns of transition poop
   zdialog_add_widget(zd,"vbox","vb5","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb6","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb7","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb8","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"label","labname","vb1",TX("transition"));
   zdialog_add_widget(zd,"label","labenab","vb2",TX("use"));
   zdialog_add_widget(zd,"label","labtime","vb3",TX("time"));
   zdialog_add_widget(zd,"label","labpref","vb4",TX("pref"));
   zdialog_add_widget(zd,"label","labname","vb5",TX("transition"));
   zdialog_add_widget(zd,"label","labenab","vb6",TX("use"));
   zdialog_add_widget(zd,"label","labtime","vb7",TX("time"));
   zdialog_add_widget(zd,"label","labpref","vb8",TX("pref"));

   zdialog_stuff(zd,"randomtrans",ss_randomtrans);                                     //  stuff random checkbox

   for (ii = 0; ii < SSNT; ii++) {                                                     //  build input dialog for transition prefs
      snprintf(nameii,32,"name_%d",ii);
      snprintf(enabii,8,"enab_%d",ii);
      snprintf(timeii,8,"time_%d",ii);
      snprintf(prefii,8,"pref_%d",ii);
      if (ii < SSNT / 2) {
         zdialog_add_widget(zd,"label",nameii,"vb1",TX("transition"));
         zdialog_add_widget(zd,"check",enabii,"vb2");
         zdialog_add_widget(zd,"zspin",timeii,"vb3","1|99|1|2","size=4");
         zdialog_add_widget(zd,"zspin",prefii,"vb4","0|99|1|10","size=3");
      }
      else {
         zdialog_add_widget(zd,"label",nameii,"vb5",TX("transition"));
         zdialog_add_widget(zd,"check",enabii,"vb6");
         zdialog_add_widget(zd,"zspin",timeii,"vb7","1|99|1|2","size=4");
         zdialog_add_widget(zd,"zspin",prefii,"vb8","0|99|1|10","size=3");
      }
      zdialog_stuff(zd,nameii,ss_trantab[ii].tranname);                                //  stuff current transition prefs
      zdialog_stuff(zd,enabii,ss_trantab[ii].enabled);
      zdialog_stuff(zd,timeii,ss_trantab[ii].trantime);
      zdialog_stuff(zd,prefii,ss_trantab[ii].preference);
   }

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

   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"randomtrans",ss_randomtrans);                                     //  get mode, 0/1 = sequential/random

   for (ii = 0; ii < SSNT; ii++) {                                                     //  get all preferences
      snprintf(enabii,8,"enab_%d",ii);
      snprintf(timeii,8,"time_%d",ii);
      snprintf(prefii,8,"pref_%d",ii);
      zdialog_fetch(zd,enabii,ss_trantab[ii].enabled);
      zdialog_fetch(zd,timeii,ss_trantab[ii].trantime);
      zdialog_fetch(zd,prefii,ss_trantab[ii].preference);
   }

   zdialog_free(zd);

   ss_saveprefs();                                                                     //  update preferences file

   for (ii = jj = 0; ii < SSNT; ii++) {                                                //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                                    //    and last used transition types
         ss_Tused[jj] = ii;
         jj++;
      }
      ss_Tlast[ii] = 0;
   }

   ss_Nused = jj;                                                                      //  no. enabled transition types
   ss_Tnext = 0;                                                                       //  next one to use (first)

   return;
}


//  transition prefs dialog event and completion function

int transprefs_dialog_event(zdialog *zd, ch *event)
{
   int transprefs_load(ch *file);
   int transprefs_save(ch *file);

   int      err, ii;
   float    time;
   ch       *file, *pp;
   ch       nameii[32], enabii[8], timeii[8], prefii[8];

   if (strmatch(event,"all"))                                                          //  enable all transitions
   {
      for (ii = 0; ii < SSNT; ii++) {
         snprintf(enabii,8,"enab_%d",ii);
         zdialog_stuff(zd,enabii,1);
      }
   }

   if (strmatch(event,"none"))                                                         //  disable all transitions
   {
      for (ii = 0; ii < SSNT; ii++) {
         snprintf(enabii,8,"enab_%d",ii);
         zdialog_stuff(zd,enabii,0);
      }
   }

   if (strmatch(event,"load"))                                                         //  load trans prefs from a file
   {
      file = zgetfile(TX("load"),MWIN,"file",slideshow_trans_folder,0);
      if (! file) return 1;

      err = transprefs_load(file);
      if (err) return 1;

      pp = strrchr(file,'/');                                                          //  show file name in dialog
      zdialog_stuff(zd,"tranfile",pp+1);

      zdialog_stuff(zd,"randomtrans",ss_randomtrans);                                  //  stuff random/sequential mode

      for (ii = 0; ii < SSNT; ii++) {                                                  //  build input dialog for transition prefs
         snprintf(nameii,32,"name_%d",ii);
         snprintf(enabii,8,"enab_%d",ii);
         snprintf(timeii,8,"time_%d",ii);
         snprintf(prefii,8,"pref_%d",ii);
         zdialog_stuff(zd,nameii,ss_trantab[ii].tranname);                             //  stuff current transition prefs
         zdialog_stuff(zd,enabii,ss_trantab[ii].enabled);
         zdialog_stuff(zd,timeii,ss_trantab[ii].trantime);
         zdialog_stuff(zd,prefii,ss_trantab[ii].preference);
      }
   }

   if (strmatch(event,"save"))                                                         //  save trans prefs to a file
   {
      zdialog_fetch(zd,"randomtrans",ss_randomtrans);                                  //  get random/sequential mode

      for (ii = 0; ii < SSNT; ii++) {                                                  //  get all preferences
         snprintf(enabii,8,"enab_%d",ii);
         snprintf(timeii,8,"time_%d",ii);
         snprintf(prefii,8,"pref_%d",ii);
         zdialog_fetch(zd,enabii,ss_trantab[ii].enabled);
         zdialog_fetch(zd,timeii,ss_trantab[ii].trantime);
         zdialog_fetch(zd,prefii,ss_trantab[ii].preference);
      }

      file = zgetfile(TX("save"),MWIN,"save",slideshow_trans_folder,0);                //  save trans prefs file
      if (! file) return 1;
      transprefs_save(file);                                                           //  save dialog to file

      pp = strrchr(file,'/');                                                          //  show file name in dialog
      zdialog_stuff(zd,"tranfile",pp+1);
   }

   if (strmatch(event,"setalltime"))                                                   //  set all transition times
   {
      zdialog_fetch(zd,"alltime",time);
      for (ii = 0; ii < SSNT; ii++) {
         snprintf(timeii,8,"time_%d",ii);
         zdialog_stuff(zd,timeii,time);
      }
   }

   return 1;
}


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

//  load transition prefs from a file
//  returns 0 = OK, +N = error

int transprefs_load(ch *file)
{
   FILE        *fid;
   ch          *pp, buff[XFCC];
   int         ii, jj, nn;
   ch          tranname[32];
   int         n1, n2;
   float       ff;

   fid = fopen(file,"r");
   if (! fid) {
      zmessageACK(Mwin,TX("invalid file"));
      return 1;
   }

   pp = fgets_trim(buff,XFCC,fid,1);
   if (! pp) goto format_error;

   nn = sscanf(buff,"randomtrans %d ",&ss_randomtrans);
   if (nn != 1) goto format_error;

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid,1);
      if (! pp) break;
      nn = sscanf(buff,"%s %d %f %d ",tranname,&n1,&ff,&n2);                           //  tranname        N  N.N  NN
      if (nn != 4) {                                                                   //  (enabled 0-1  duration N.N  pref. 0-99)
         printf("*** bad record: %s \n",buff);
         continue;
      }
      for (ii = 0; ii < SSNT; ii++)
         if (strmatch(tranname,ss_trantab[ii].tranname)) break;
      if (ii == SSNT) {
         printf("*** bad record: %s \n",buff);
         continue;
      }
      ss_trantab[ii].enabled = n1;
      ss_trantab[ii].trantime = ff;
      ss_trantab[ii].preference = n2;
   }

   for (ii = jj = 0; ii < SSNT; ii++) {                                                //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                                    //    transition types
         ss_Tused[jj] = ii;
         jj++;
      }
   }

   ss_Nused = jj;                                                                      //  no. enabled transition types

   fclose(fid);
   return 0;

format_error:
   zmessageACK(Mwin,TX("file format error: %s"),buff);
   fclose(fid);
   return 1;
}


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

//  save transition prefs to a file

int transprefs_save(ch *file)
{
   FILE     *fid;

   fid = fopen(file,"w");
   if (! fid) {
      zmessageACK(Mwin,TX("invalid file"));
      return 1;
   }

   fprintf(fid,"randomtrans %d \n",ss_randomtrans);

   for (int ii = 0; ii < SSNT; ii++)
      fprintf(fid,"%s %d %.1f %d \n", ss_trantab[ii].tranname,
              ss_trantab[ii].enabled, ss_trantab[ii].trantime,
              ss_trantab[ii].preference);

   fclose(fid);
   return 0;
}


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

//  set image preferences for specific slide show

void ss_imageprefs_dialog()
{
   int ss_imageprefs_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   int      ii, kk;
   ch       *pp, zoomloc[40];
   ch       *pref_ttip = TX("For random image sequence, this value is \n"
                            "the relative frequency to show the image. \n"
                            "Base value is 10. 20 --> show 2x as often.");

/***
       __________________________________________________
      |             Image Preferences                    |
      |                                                  |
      |  Image File: [________________________________]  |
      |  Preference: [___]                               |
      |                                                  |
      |  Action                                 seconds  |
      |  ----------------------------------------------  |
      |  Play tone when image shows [x]                  |
      |  Wait before showing text below          [1.5]   |
      |  Show filename (overlap)                 [2.0]   |
      |  Show title (overlap)                    [2.0]   |
      |  Show description (overlap)              [3.0]   |
      |  Wait before zoom                        [1.0]   |
      |  Zoom [2.5] X  [_] in  [x] out           [2.0]   |
      |  [Zoom Center] position: x=40 y=50               |
      |  Wait after zoom                         [1.5]   |
      |  Transition to next image [_________|v]          |
      |                                                  |
      |                                            [OK]  |
      |__________________________________________________|

***/

   if (! ss_Nfiles) {
      zmessageACK(Mwin,TX("invalid album"));
      return;
   }

   viewmode('G');                                                                      //  gallery view

   zd = zdialog_new(TX("Image Preferences"),Mwin,"OK",null);
   zd_ss_imageprefs = zd;

   zdialog_add_widget(zd,"hbox","hbimf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labimf","hbimf",TX("Image File:"),"space=3");
   zdialog_add_widget(zd,"label","imagefile","hbimf",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbpref","dialog",0);
   zdialog_add_widget(zd,"label","labpref","hbpref",TX("Preference:"),"space=3");
   zdialog_add_widget(zd,"zspin","preference","hbpref","0|99|1|10","space=3");

   zdialog_add_ttip(zd,"preference",pref_ttip);

   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=8");

   zdialog_add_widget(zd,"hbox","hbact","dialog");
   zdialog_add_widget(zd,"label","labact","hbact",TX("Action"),"space=3");
   zdialog_add_widget(zd,"label","space","hbact",0,"expand");
   zdialog_add_widget(zd,"label","labsecs","hbact",TX("Seconds"),"space=3");

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

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

   zdialog_add_widget(zd,"hbox","hbtone","vb1");
   zdialog_add_widget(zd,"label","labtone","hbtone",TX("Play tone when image shows"));
   zdialog_add_widget(zd,"check","tone","hbtone","","space=3");
   zdialog_add_widget(zd,"label","space","vb2",0);

   zdialog_add_widget(zd,"hbox","hbw0","vb1");
   zdialog_add_widget(zd,"label","labwait","hbw0",TX("Wait before filename/title/description"));
   zdialog_add_widget(zd,"zspin","wait0","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbfile","vb1");
   zdialog_add_widget(zd,"label","labfile","hbfile",TX("Show image file name (overlap)"));
   zdialog_add_widget(zd,"zspin","filesecs","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbtitle","vb1");
   zdialog_add_widget(zd,"label","labtitle","hbtitle",TX("Show image title (overlap)"));
   zdialog_add_widget(zd,"zspin","titlesecs","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbdesc","vb1");
   zdialog_add_widget(zd,"label","labdesc","hbdesc",TX("Show image description (overlap)"));
   zdialog_add_widget(zd,"zspin","descsecs","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbw1","vb1");
   zdialog_add_widget(zd,"label","labwait","hbw1",TX("Wait before zoom"));
   zdialog_add_widget(zd,"zspin","wait1","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbzoom","vb1");
   zdialog_add_widget(zd,"label","labzoom","hbzoom","Zoom");
   zdialog_add_widget(zd,"zspin","zoomsize","hbzoom","1.0|3.0|0.1|1.0","space=3|size=3");
   zdialog_add_widget(zd,"label","labX","hbzoom","X","space=3");
   zdialog_add_widget(zd,"label","space","hbzoom",0,"space=3");
   zdialog_add_widget(zd,"check","zoomin","hbzoom",TX("zoom-in"),"space=3");
   zdialog_add_widget(zd,"check","zoomout","hbzoom",TX("zoom-out"),"space=3");
   zdialog_add_widget(zd,"zspin","zoomtime","vb2","1|99|1|2","size=3");

   zdialog_add_widget(zd,"hbox","hbzloc","vb1");
   zdialog_add_widget(zd,"button","zloc","hbzloc",TX("Zoom Center"));
   zdialog_add_widget(zd,"label","labzloc","hbzloc",TX("position: 50 50"),"space=5");
   zdialog_add_widget(zd,"label","space","vb2",0);

   zdialog_add_widget(zd,"hbox","hbw2","vb1");
   zdialog_add_widget(zd,"label","labwait","hbw2",TX("Wait after zoom"));
   zdialog_add_widget(zd,"zspin","wait2","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hsep","septrn","dialog",0,"space=4");

   zdialog_add_widget(zd,"hbox","hbtrn","dialog");
   zdialog_add_widget(zd,"label","labtr","hbtrn",TX("Transition to next image"),"space=3");
   zdialog_add_widget(zd,"combo","tranname","hbtrn");

   zdialog_stuff(zd,"tranname","next");                                                //  default transition

   for (ii = 0; ii < SSNT; ii++)                                                       //  add all transitions to dropdown list
      zdialog_stuff(zd,"tranname",ss_trantab[ii].tranname);

   if (curr_file) {                                                                    //  set to curr. file
      for (ii = 0; ii < ss_Nfiles; ii++)                                               //    if member of album
         if (strmatch(curr_file,ss_imagetab[ii].imagefile)) break;
      if (ii == ss_Nfiles) ii = 0;
   }
   else ii = 0;                                                                        //  else first file in album
   ss_Fcurrent = ii;

   ss_setzloc = 0;                                                                     //  1-shot flag is off

   pp = strrchr(ss_imagetab[ii].imagefile,'/');                                        //  stuff curr. file data into dialog
   if (pp) zdialog_stuff(zd,"imagefile",pp+1);
   zdialog_stuff(zd,"preference",ss_imagetab[ii].preference);
   zdialog_stuff(zd,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"wait0",ss_imagetab[ii].wait0);
   zdialog_stuff(zd,"filesecs",ss_imagetab[ii].filesecs);
   zdialog_stuff(zd,"titlesecs",ss_imagetab[ii].titlesecs);
   zdialog_stuff(zd,"descsecs",ss_imagetab[ii].descsecs);
   zdialog_stuff(zd,"wait1",ss_imagetab[ii].wait1);
   zdialog_stuff(zd,"zoomin",0);                                                       //  both zoom checks off
   zdialog_stuff(zd,"zoomout",0);
   kk = ss_imagetab[ii].zoomtype;                                                      //  zoom type 0/1/2
   if (kk == 1) zdialog_stuff(zd,"zoomin",1);                                          //  set corresp. check on
   if (kk == 2) zdialog_stuff(zd,"zoomout",1);
   zdialog_stuff(zd,"zoomsize",ss_imagetab[ii].zoomsize);
   zdialog_stuff(zd,"zoomtime",ss_imagetab[ii].zoomtime);
   snprintf(zoomloc,40,TX("position: x=%02d y=%02d"),                                  //  stuff zoom location if defined
            ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);                        //    (else  x=00  y=00)
   zdialog_stuff(zd,"labzloc",zoomloc);
   zdialog_stuff(zd,"wait2",ss_imagetab[ii].wait2);
   zdialog_stuff(zd,"tranname",ss_imagetab[ii].tranname);

   zdialog_run(zd,ss_imageprefs_dialog_event,"parent");                                //  run dialog
   zdialog_wait(zd);                                                                   //  wait for completion
   zdialog_free(zd);
   zd_ss_imageprefs = 0;

   ss_saveprefs();                                                                     //  save updated preferences file
   return;
}


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

//  image prefs dialog event and completion function

int  ss_imageprefs_dialog_event(zdialog *zd, ch *event)
{
   int         ii, jj;
   float       ff;
   ch          tranname[32];
   GdkWindow   *gdkwin;

   if (zd->zstat) {                                                                    //  OK or cancel
      zd_ss_imageprefs = 0;
      return 1;
   }

   ii = ss_Fcurrent;                                                                   //  from mouse click on thumbnail
   if (ii >= ss_Nfiles) return 1;

   if (strmatch(event,"preference")) {
      zdialog_fetch(zd,"preference",jj);
      ss_imagetab[ii].preference = jj;
   }

   if (strmatch(event,"tone")) {
      zdialog_fetch(zd,"tone",jj);
      ss_imagetab[ii].tone = jj;
   }

   if (strmatch(event,"wait0")) {
      zdialog_fetch(zd,"wait0",jj);
      ss_imagetab[ii].wait0 = jj;
   }

   if (strmatch(event,"filesecs")) {
      zdialog_fetch(zd,"filesecs",jj);
      ss_imagetab[ii].filesecs = jj;
   }

   if (strmatch(event,"titlesecs")) {
      zdialog_fetch(zd,"titlesecs",jj);
      ss_imagetab[ii].titlesecs = jj;
   }

   if (strmatch(event,"descsecs")) {
      zdialog_fetch(zd,"descsecs",jj);
      ss_imagetab[ii].descsecs = jj;
   }

   if (strmatch(event,"wait1")) {
      zdialog_fetch(zd,"wait1",jj);
      ss_imagetab[ii].wait1 = jj;
   }

   if (strmatch(event,"zoomin")) {                                                     //  zoom-in checkbox
      zdialog_fetch(zd,"zoomin",jj);
      if (jj) {
         zdialog_stuff(zd,"zoomout",0);
         ss_imagetab[ii].zoomtype = 1;
      }
      else ss_imagetab[ii].zoomtype = 0;
   }

   if (strmatch(event,"zoomout")) {                                                    //  zoom-out checkbox
      zdialog_fetch(zd,"zoomout",jj);
      if (jj) {
         zdialog_stuff(zd,"zoomin",0);
         ss_imagetab[ii].zoomtype = 2;
      }
      else ss_imagetab[ii].zoomtype = 0;
   }

   if (strmatch(event,"zoomsize")) {
      zdialog_fetch(zd,"zoomsize",ff);
      ss_imagetab[ii].zoomsize = ff;
   }

   if (strmatch(event,"zoomtime")) {
      zdialog_fetch(zd,"zoomtime",jj);
      ss_imagetab[ii].zoomtime = jj;
   }

   if (strmatch(event,"zloc")) {
      ss_setzloc = 1;                                                                  //  set 1-shot flag
      gdkwin = gtk_widget_get_window(Gdrawin);
      gdk_window_set_cursor(gdkwin,dragcursor);
      poptext_mouse(TX("click on thumbnail to set zoom center"),20,20,0,2);
   }

   if (strmatch(event,"wait2")) {
      zdialog_fetch(zd,"wait2",jj);
      ss_imagetab[ii].wait2 = jj;
   }

   if (strmatch(event,"tranname")) {
      zdialog_fetch(zd,"tranname",tranname,32);
      if (! strmatch(tranname,"next")) {
         for (jj = 0; jj < SSNT; jj++)
            if (strmatch(tranname,ss_trantab[jj].tranname)) break;
         if (jj == SSNT) return 1;
         strncpy0(ss_imagetab[ii].tranname,ss_trantab[jj].tranname,32);
      }
      else strncpy0(ss_imagetab[ii].tranname,"next",32);
   }

   return 1;
}


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

//  response function for gallery thumbnail left-click
//  stuff image prefs dialog with data for clicked image

void ss_imageprefs_Lclick_func(int Nth)
{
   zdialog     *zd;
   int         ii, kk;
   ch          *pp, zoomloc[40];
   GdkWindow   *gdkwin;

   if (! clicked_file) return;

   zd = zd_ss_imageprefs;                                                              //  should not happen
   if (! zd) {
      zfree(clicked_file);
      clicked_file = 0;
      return;
   }

   for (ii = 0; ii < ss_Nfiles; ii++)                                                  //  find clicked file in image prefs
      if (strmatch(clicked_file,ss_imagetab[ii].imagefile)) break;
   zfree(clicked_file);
   clicked_file = 0;
   if (ii == ss_Nfiles) return;                                                        //  not found, album file removed

   ss_Fcurrent = ii;

   f_open(ss_imagetab[ii].imagefile);                                                  //  make current, highlight

   if (ss_setzloc) {                                                                   //  1-shot flag is set
      ss_setzloc = 0;
      ss_imagetab[ii].zoomlocx = clicked_width;                                        //  set zoom-in location from
      ss_imagetab[ii].zoomlocy = clicked_height;                                       //    thumbnail click position
      gdkwin = gtk_widget_get_window(Gdrawin);
      gdk_window_set_cursor(gdkwin,0);
   }

   pp = strrchr(ss_imagetab[ii].imagefile,'/');                                        //  stuff image data into dialog
   if (pp) zdialog_stuff(zd,"imagefile",pp+1);
   zdialog_stuff(zd,"preference",ss_imagetab[ii].preference);
   zdialog_stuff(zd,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"wait0",ss_imagetab[ii].wait0);
   zdialog_stuff(zd,"filesecs",ss_imagetab[ii].filesecs);
   zdialog_stuff(zd,"titlesecs",ss_imagetab[ii].titlesecs);
   zdialog_stuff(zd,"descsecs",ss_imagetab[ii].descsecs);
   zdialog_stuff(zd,"wait1",ss_imagetab[ii].wait1);
   zdialog_stuff(zd,"zoomin",0);                                                       //  set both zoom checks off
   zdialog_stuff(zd,"zoomout",0);
   kk = ss_imagetab[ii].zoomtype;                                                      //  0/1/2 = none/zoomin/zoomout
   if (kk == 1) zdialog_stuff(zd,"zoomin",1);                                          //  set corresp. check on
   if (kk == 2) zdialog_stuff(zd,"zoomout",1);
   zdialog_stuff(zd,"zoomsize",ss_imagetab[ii].zoomsize);
   zdialog_stuff(zd,"zoomtime",ss_imagetab[ii].zoomtime);
   snprintf(zoomloc,40,"position: x=%02d  y=%02d",                                     //  stuff zoom location
            ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);
   zdialog_stuff(zd,"labzloc",zoomloc);
   zdialog_stuff(zd,"wait2",ss_imagetab[ii].wait2);
   zdialog_stuff(zd,"tranname",ss_imagetab[ii].tranname);

   return;
}


/***********************  preferences file format  ******************************

   global data:
   imagetime: NN                    0-99        image show time
   texttime: NN                     0-99        text show time
   cliplimit: NN                    0-50
   musicfile: /path.../file.ogg     music file or "none"
   fullscreen:                      0-1
   replay:                          0-1
   randomimage: N                   0-1         random image
   randomtrans: N                   0-1         random transition type

   transitions data:
   tranname N NN NN                 tranname  enabled  duration  preference
   tranname N NN NN                            0-1      0.1-10     0-99
   ...

   images data:
   imagefile: /path.../file.jpg     show this image
   preference: NN                   0-99  image preference
   tone: N                          0-1  play tone for this image
   wait0: NN                        seconds to wait 0-99
   filename: NN                     seconds to show file name
   title: NN                        seconds to show title
   description: NN                  seconds to show description
   wait1: NN                        seconds to wait 0-99
   zoomtype: N                      0/1/2 = none/zoom-in/zoom-out
   zoomsize: N.N                    1.0 - 3.0                                          //  1.0 = no zoom
   zoomtime: NN                     seconds for zoom 0-99
   zoomloc: NN NN                   20-80 20-80
   wait2: NN                        seconds to wait 0-99
   tranname: aaaaaa                 transition name or "next"
   ...

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


//  Load all data for a specific slide show from a slide show preferences file.
//  Set defaults if no data previously defined.

void ss_loadprefs()
{
   FILE        *fid;
   ch          buff[XFCC];
   ch          prefsfile[200], *pp;
   int         ii, jj, nn, format;
   FTYPE       ftype;
   ch          tranname[32];
   int         n1, n2;
   float       ff;

   for (ii = 0; ii < ss_Nfiles; ii++) {                                                //  free prior image data if any
      pp = ss_imagetab[ii].imagefile;
      if (pp) zfree(pp);
      ss_imagetab[ii].imagefile = 0;
   }

   ss_Nfiles = 0;

   fid = fopen(ss_albumfile,"r");                                                      //  open album file
   if (! fid) {
      zmessageACK(Mwin,TX("invalid album"));
      return;
   }

   for (ii = 0; ii < SSMAXI; ) {                                                       //  read all image file names
      pp = fgets_trim(buff,XFCC,fid);
      if (! pp) break;
      ftype = image_file_type(pp);                                                     //  screen out deleted image files
      if (ftype != IMAGE && ftype != VIDEO) continue;
      ss_imagetab[ii].imagefile = zstrdup(pp,"ss");                                    //  add to image table
      ii++;
   }

   fclose(fid);
   ss_Nfiles = ii;

   if (! ss_Nfiles) {
      zmessageACK(Mwin,TX("invalid album"));
      return;
   }

   album_show(ss_albumfile);                                                           //  open slide show album

   ss_imagetime = 3;                                                                   //  default image display time
   ss_texttime = 0;                                                                    //  default text show time
   ss_cliplimit = 0;                                                                   //  image clip limit = no clipping
   strcpy(ss_musicfile,"none");                                                        //  no music file

   for (ii = 0; ii < SSNT; ii++)                                                       //  initialize transitions table
      ss_trantab[ii] = ss_trantab_default[ii];                                         //    with default preferences

   for (ii = 0; ii < SSNT; ii++) {
      ss_Tused[ii] = ii;                                                               //  all transition types are used
      ss_Tlast[ii] = 0;                                                                //  last used list is empty
   }

   ss_randomimage = 0;                                                                 //  random images = NO
   ss_randomtrans = 0;                                                                 //  random transitions = NO
   ss_Nused = SSNT;                                                                    //  used transitions = all
   ss_Tnext = 0;                                                                       //  next = first

   for (ii = 0; ii < ss_Nfiles; ii++) {                                                //  initialize image table with defaults
      ss_imagetab[ii].preference = 10;
      ss_imagetab[ii].tone = 0;
      ss_imagetab[ii].wait0 = 0;
      ss_imagetab[ii].filesecs = 0;
      ss_imagetab[ii].titlesecs = 0;
      ss_imagetab[ii].descsecs = 0;
      ss_imagetab[ii].wait1 = 0;
      ss_imagetab[ii].zoomtype = 0;                                                    //  no zoom-in
      ss_imagetab[ii].zoomsize = 1.0;
      ss_imagetab[ii].zoomtime = 2;
      ss_imagetab[ii].zoomlocx = 50;
      ss_imagetab[ii].zoomlocy = 50;
      ss_imagetab[ii].wait2 = 0;
      strcpy(ss_imagetab[ii].tranname,"next");
   }

   snprintf(prefsfile,200,"%s/%s",slideshow_folder,ss_albumname);
   fid = fopen(prefsfile,"r");                                                         //  open slide show prefs file
   if (! fid) return;

   format = 0;

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid,1);
      if (! pp) break;

      if (strmatchN(pp,"global data:",12)) {
         format = 1;
         continue;
      }

      if (strmatchN(pp,"transitions data:",17)) {
         format = 2;
         continue;
      }

      if (strmatchN(pp,"images data:",12)) {
         format = 3;
         continue;
      }

      if (format == 1)                                                                 //  overall preferences
      {
         if (strmatchN(pp,"imagetime: ",11)) {                                         //  imagetime: NN   show time, secs.
            ss_imagetime = atoi(pp+11);
            continue;
         }

         if (strmatchN(pp,"texttime: ",10)) {                                          //  texttime: NN   filename/title/desc.
            ss_texttime = atoi(pp+10);
            continue;
         }

         if (strmatchN(pp,"cliplimit: ",11)) {                                         //  cliplimit: NN   margin clip limit  0-50%
            ss_cliplimit = atoi(pp+11);
            continue;
         }

         if (strmatchN(pp,"randomimage: ",13)) {                                       //  randomimage: 0/1 = seq./random image
            ss_randomimage = atoi(pp+13);
            continue;
         }

         if (strmatchN(pp,"randomtrans: ",13)) {                                       //  randomtrans: 0/1 = seq./random transition
            ss_randomtrans = atoi(pp+13);
            continue;
         }

         if (strmatchN(pp,"musicfile: ",11)) {                                         //  musicfile: /folder/.../musicfile.ogg
            strncpy0(ss_musicfile,pp+11,500);
            continue;
         }

         if (strmatchN(pp,"fullscreen: ",12)) {                                        //  fullscreen: N   0/1 = no / full screen
            ss_fullscreen = atoi(pp+12);
            continue;
         }

         if (strmatchN(pp,"replay: ",8)) {                                             //  replay: N   0/1 = no / replay after end
            ss_replay = atoi(pp+8);
            continue;
         }
      }

      if (format == 2)                                                                 //  transition preferences
      {
         nn = sscanf(buff,"%s %d %f %d ",tranname,&n1,&ff,&n2);                        //  tranname        N  N.N  NN
         if (nn != 4) {                                                                //  (enabled 0-1  duration N.N  pref. 0-99)
            printf("*** bad record: %s \n",buff);
            continue;
         }
         for (ii = 0; ii < SSNT; ii++)
            if (strmatch(tranname,ss_trantab[ii].tranname)) break;
         if (ii == SSNT) {
            printf("*** unknown transition: %s \n",tranname);                          //  ignore and continue
            continue;
         }
         ss_trantab[ii].enabled = n1;
         ss_trantab[ii].trantime = ff;
         ss_trantab[ii].preference = n2;
      }

      if (format == 3)                                                                 //  image file preferences
      {
         if (strmatchN(pp,"imagefile: ",11)) {                                         //  set image file for subsequent recs
            pp += 11;
            if (*pp != '/') {
               printf("*** bad image file: %s \n",buff);
               continue;
            }
            for (ii = 0; ii < ss_Nfiles; ii++)                                         //  search album for matching image
               if (strmatch(pp,ss_imagetab[ii].imagefile)) break;
            if (ii == ss_Nfiles) ii = -1;                                              //  if not found, set no curr. image
            continue;
         }

         if (ii < 0) continue;                                                         //  ignore recs following invalid image

         if (strmatchN(pp,"preference: ",12)) {                                        //  preference: NN   0-99
            nn = atoi(pp+12);
            ss_imagetab[ii].preference = nn;
            continue;
         }

         if (strmatchN(pp,"tone: ",6)) {                                               //  tone: N    0 or 1 = play tone
            nn = atoi(pp+6);
            ss_imagetab[ii].tone = nn;
            continue;
         }

         if (strmatchN(pp,"wait0: ",7)) {                                              //  wait0: NN   secs before title/desc
            nn = atoi(pp+7);
            ss_imagetab[ii].wait0 = nn;
            continue;
         }

         if (strmatchN(pp,"filename: ",10)) {                                          //  filename: NN   secs for file name
            nn = atoi(pp+10);
            ss_imagetab[ii].filesecs = nn;
            continue;
         }

         if (strmatchN(pp,"title: ",7)) {                                              //  title: NN   secs to show title
            nn = atoi(pp+7);
            ss_imagetab[ii].titlesecs = nn;
            continue;
         }

         if (strmatchN(pp,"description: ",13)) {                                       //  description: NN   secs to show description
            nn = atoi(pp+13);
            ss_imagetab[ii].descsecs = nn;
            continue;
         }

         if (strmatchN(pp,"wait1: ",7)) {                                              //  wait1: NN   secs before zoom-in
            nn = atoi(pp+7);
            ss_imagetab[ii].wait1 = nn;
            continue;
         }

         if (strmatchN(pp,"zoomtype: ",10)) {                                          //  zoomtype: N    zoom type
            nn = atoi(pp+10);                                                          //  0/1/2 = none/zoom-in/zoom-out
            ss_imagetab[ii].zoomtype = nn;
            continue;
         }

         if (strmatchN(pp,"zoomsize: ",10)) {                                          //  zoomsize: N.N   1.0 - 3.0 = 3x
            ff = atofz(pp+10);                                                         //  ff float
            ss_imagetab[ii].zoomsize = ff;
            continue;
         }

         if (strmatchN(pp,"zoomtime: ",10)) {                                          //  zoomtime: NN secs
            nn = atoi(pp+10);
            if (nn < 1) nn = 1;                                                        //  insure >= 1
            ss_imagetab[ii].zoomtime = nn;
            continue;
         }

         if (strmatchN(pp,"zoomloc: ",9)) {                                            //  zoomloc: NN NN   zoom-in location
            nn = strtol(pp+9,&pp,10);                                                  //  (20-80% of image width and height)
            ss_imagetab[ii].zoomlocx = nn;
            nn = atoi(pp);
            ss_imagetab[ii].zoomlocy = nn;
            continue;
         }

         if (strmatchN(pp,"wait2: ",7)) {                                              //  wait2: NN   secs after zoom-in
            nn = atoi(pp+7);
            ss_imagetab[ii].wait2 = nn;
            continue;
         }

         if (strmatchN(pp,"tranname: ",10)) {                                          //  transaction to next image
            strncpy0(ss_imagetab[ii].tranname,pp+10,32);
            continue;
         }
      }
   }

   fclose(fid);

   for (ii = jj = 0; ii < SSNT; ii++) {                                                //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                                    //    transition types
         ss_Tused[jj] = ii;
         jj++;
      }
   }

   ss_Nused = jj;                                                                      //  no. enabled transition types
   return;
}


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

//  Save all data for a specific slide show to a slide show preferences file.

void ss_saveprefs()
{
   FILE        *fid;
   ch          prefsfile[200];
   int         ii;

   if (! ss_Nfiles) {
      zmessageACK(Mwin,TX("invalid album"));
      return;
   }

   snprintf(prefsfile,200,"%s/%s",slideshow_folder,ss_albumname);
   fid = fopen(prefsfile,"w");                                                         //  open slide show prefs file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fprintf(fid,"global data: \n");
   fprintf(fid,"imagetime: %d \n",ss_imagetime);
   fprintf(fid,"texttime: %d \n",ss_texttime);
   fprintf(fid,"cliplimit: %d \n",ss_cliplimit);
   fprintf(fid,"randomimage: %d \n",ss_randomimage);
   fprintf(fid,"randomtrans: %d \n",ss_randomtrans);
   fprintf(fid,"musicfile: %s \n",ss_musicfile);
   fprintf(fid,"fullscreen: %d \n",ss_fullscreen);
   fprintf(fid,"replay: %d \n",ss_replay);

   fprintf(fid,"transitions data: \n");

   for (ii = 0; ii < SSNT; ii++)
      fprintf(fid,"%s %d %.1f %d \n", ss_trantab[ii].tranname,
              ss_trantab[ii].enabled, ss_trantab[ii].trantime,
              ss_trantab[ii].preference);

   fprintf(fid,"images data: \n");

   for (ii = 0; ii < ss_Nfiles; ii++)
   {
      fprintf(fid,"imagefile: %s \n",ss_imagetab[ii].imagefile);
      fprintf(fid,"preference: %d \n",ss_imagetab[ii].preference);
      fprintf(fid,"tone: %d \n",ss_imagetab[ii].tone);
      fprintf(fid,"wait0: %d \n",ss_imagetab[ii].wait0);
      fprintf(fid,"filename: %d \n",ss_imagetab[ii].filesecs);
      fprintf(fid,"title: %d \n",ss_imagetab[ii].titlesecs);
      fprintf(fid,"description: %d \n",ss_imagetab[ii].descsecs);
      fprintf(fid,"wait1: %d \n",ss_imagetab[ii].wait1);
      fprintf(fid,"zoomtype: %d \n",ss_imagetab[ii].zoomtype);
      fprintf(fid,"zoomsize: %.1f \n",ss_imagetab[ii].zoomsize);
      fprintf(fid,"zoomtime: %d \n",ss_imagetab[ii].zoomtime);
      fprintf(fid,"zoomloc: %d %d \n",ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);
      fprintf(fid,"wait2: %d \n",ss_imagetab[ii].wait2);
      fprintf(fid,"tranname: %s \n",ss_imagetab[ii].tranname);
   }

   fclose(fid);

   ss_loadprefs();                                                                     //  reload to sync poss. album edits
   return;
}


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

      Transitions

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

//  write black to entire window

void ss_blankwindow()
{
   GdkRGBA     GDKdark;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   GDKdark.red = GDKdark.green = GDKdark.blue = 0.2;
   GDKdark.alpha = 1;
   gdk_cairo_set_source_rgba(cr,&GDKdark);
   cairo_paint(cr);
   draw_context_destroy(draw_context);
   zmainloop();
   return;
}


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

//  instant transition (also used for keyboard arrow keys)

void ss_instant()
{
   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);
   cairo_paint(cr);
   draw_context_destroy(draw_context);
   zmainloop();
   return;
}


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

//  fade-out / fade-in transition

void ss_fadein()
{
   PXB         *pxbmix;
   int         jj, kk, px, py;
   float       newpart, oldpart;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3;
   double      T0, Te, Tz;

   pxbmix = PXB_copy(ss_pxbold);

   pixels1 = ss_pxbold->pixels;
   pixels2 = ss_pxbnew->pixels;
   pixels3 = pxbmix->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      newpart = Te / Tz;
      oldpart = 1.0 - newpart;

      for (jj = 0; jj < 2; jj++)                                                       //  four passes, each modifies 25%
      for (kk = 0; kk < 2; kk++)                                                       //    of the pixels (visually smoother)
      {
         for (py = jj; py < ss_hh; py += 2)
         for (px = kk; px < ss_ww; px += 2)
         {
            pix1 = pixels1 + py * ss_rs + px * 3;
            pix2 = pixels2 + py * ss_rs + px * 3;
            pix3 = pixels3 + py * ss_rs + px * 3;
            pix3[0] = newpart * pix2[0] + oldpart * pix1[0];
            pix3[1] = newpart * pix2[1] + oldpart * pix1[1];
            pix3[2] = newpart * pix2[2] + oldpart * pix1[2];
         }

         gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);
         cairo_paint(cr);
         zmainloop();
      }

      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image rolls over prior image from left to right

void ss_rollright()
{
   PIXBUF      *pixbuf;
   int         px, ppx, ww;
   double      T0, Te, Tz;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   px = ppx = 0;

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      px = Te / Tz * ss_ww;                                                            //  ending column to paint
      if (px > ss_ww - 1) break;
      ww = px - ppx;                                                                   //  width to paint
      if (ww < 1) ww = 1;
      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,ppx,0,ww,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,ppx,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      ppx = px;
      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image rolls over prior image from left to right
//    and fades-in, parallel with the roll

void ss_slidefadein()
{
   PXB         *pxbmix;
   PIXBUF      *pixbuf;
   int         ww, px, py;
   float       newpart, oldpart;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3;
   double      T0, Te, Tz;

   pxbmix = PXB_copy(ss_pxbold);

   pixels1 = ss_pxbold->pixels;
   pixels2 = ss_pxbnew->pixels;
   pixels3 = pxbmix->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      ww = Te / Tz * ss_ww + 1;                                                        //  new image width to paint
      if (ww > ss_ww - 1) break;

      newpart = Te / Tz;
      oldpart = 1.0 - newpart;

      for (py = 0; py < ss_hh; py++)
      for (px = 0; px < ww; px++)
      {
         pix1 = pixels1 + py * ss_rs + px * 3;                                         //  old image pixel
         pix2 = pixels2 + py * ss_rs + px * 3;                                         //  new image pixel
         pix3 = pixels3 + py * ss_rs + px * 3;                                         //  combined

         pix3[0] = newpart * pix2[0] + oldpart * pix1[0];
         pix3[1] = newpart * pix2[1] + oldpart * pix1[1];
         pix3[2] = newpart * pix2[2] + oldpart * pix1[2];
      }

      pixbuf = gdk_pixbuf_new_subpixbuf(pxbmix->pixbuf,0,0,ww,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image rolls over prior image from top down

void ss_rolldown()
{
   PIXBUF      *pixbuf;
   int         py, ppy, hh;
   double      T0, Te, Tz;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   py = ppy = 0;

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      py = Te / Tz * ss_hh;                                                            //  last row to paint
      hh = py - ppy;
      if (hh < 1) hh = 1;

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,0,ppy,ss_ww,hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,ppy);
      cairo_paint(cr);
      g_object_unref(pixbuf);
      zmainsleep(0.001);                                                               //  fix jitter                            25.0

      ppy = py;
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image opens up in horizontal rows like venetian blinds

void ss_venetian()
{
   PIXBUF      *pixbuf;
   int         py1, py2;
   uint8       *pixels, *pix3;
   int         louver, Nlouvers = 20;
   int         louversize = ss_hh / Nlouvers;
   double      T0, Te, Tz;

   pixels = ss_pxbnew->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      py1 = Te / Tz * louversize;                                                      //  0 .. louversize

      for (louver = 0; louver < Nlouvers; louver++)                                    //  louver, first to last
      {
         py2 = py1 + louver * louversize;
         if (py2 >= ss_hh) break;
         pix3 = pixels + py2 * ss_rs;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ss_ww,4,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,0,py2);
         cairo_paint(cr);
         g_object_unref(pixbuf);
      }

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  a grate opens up to show new image

void ss_grate()
{
   PIXBUF      *pixbuf;
   int         px1, px2, py1, py2;
   uint8       *pixels, *pix3;
   int         row, col, Nrow, Ncol;                                                   //  rows and columns
   int         boxww, boxhh;
   double      T0, Te, Tz;

   pixels = ss_pxbnew->pixels;

   Ncol = 20;                                                                          //  20 columns
   boxww = boxhh = ss_ww / Ncol;                                                       //  square boxes
   Nrow = ss_hh / boxhh;                                                               //  corresp. rows
   Ncol++;                                                                             //  round up
   Nrow++;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      py1 = Te / Tz * boxhh;                                                           //  0 .. boxhh

      for (row = 0; row < Nrow; row++)
      {
         py2 = py1 + row * boxhh;
         if (py2 >= ss_hh) break;
         pix3 = pixels + py2 * ss_rs;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ss_ww,1,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,0,py2);
         cairo_paint(cr);
         g_object_unref(pixbuf);
      }

      px1 = py1;

      for (col = 0; col < Ncol; col++)
      {
         px2 = px1 + col * boxww;
         if (px2 >= ss_ww) break;
         pix3 = pixels + px2 * 3;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,1,ss_hh,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,px2,0);
         cairo_paint(cr);
         g_object_unref(pixbuf);
      }

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  A rectangular hole opens up from the center and expands outward to reveal new image.

void ss_rectangle()
{
   PIXBUF      *pixbuf;
   int         ww, hh, sww, shh;
   int         px, py, ppx, ppy;
   double      T0, Te, Tz;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   ppx = ss_ww / 2;
   ppy = ss_hh / 2;

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      ww = Te / Tz * ss_ww;                                                            //  0 ... ss_ww   central rectangle
      hh = Te / Tz * ss_hh;                                                            //  0 ... ss_hh    width and height

      px = ss_ww / 2 - ww / 2;                                                         //  NW corner, moving NW
      py = ss_hh / 2 - hh / 2;

      if (px == ppx || py == ppy) {
         zsleep(0.001);
         continue;
      }

      if (px + ww > ss_ww - 2) break;
      if (py + hh > ss_hh - 2) break;

      sww = ppx - px;                                                                  //  left/right stripe width
      shh = ppy - py;                                                                  //  top/bottom stripe height

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px,py,ww+2,shh+2);           //  paint top edge stripe
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px,py+hh-shh,ww+2,shh+2);    //  paint bottom edge stripe
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py+hh-shh);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px,py,sww+2,hh+2);           //  paint left edge stripe
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px+ww-sww,py,sww+2,hh+2);    //  paint right edge stripe
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px+ww-sww,py);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      ppx = px;
      ppy = py;

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  old image shrinks to the center, revealing new image

void ss_contract()
{
   float       size;
   int         ww, hh, px, py;
   PXB         *pxbnew, *pxbold;
   double      T0, Te, Tz, R;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;                                                                     //  0 ... 1
      R = R * R;                                                                       //  slower ... faster

      size = 1.0 - R;
      if (size < 0.005) size = 0.005;

      pxbnew = PXB_copy(ss_pxbnew);                                                    //  new image at full size

      ww = ss_ww * size;
      hh = ss_hh * size;
      pxbold = PXB_rescale_fast(ss_pxbold,ww,hh);                                      //  old image at reduced size

      px = ss_ww * 0.5 * (1.0 - size);                                                 //  new image position, NW corner --> center
      py = ss_hh * 0.5 * (1.0 - size);
      PXB_copy_area(pxbold,0,0,ww,hh,pxbnew,px,py);                                    //  copy shrinking old image into new image

      gdk_cairo_set_source_pixbuf(cr,pxbnew->pixbuf,0,0);                              //  paint new image
      cairo_paint(cr);

      PXB_free(pxbnew);
      PXB_free(pxbold);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image grows from the center, covering old image

void ss_expand()
{
   float       size;
   int         ww, hh, px, py;
   PXB         *pxbnew, *pxbold;
   double      T0, Te, Tz, R;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;                                                                     //  0 ... 1
      R = pow(R,1.6);                                                                  //  slower ... faster

      size = R;
      if (size < 0.005) size = 0.005;
      if (size > 1.0) break;

      pxbold = PXB_copy(ss_pxbold);                                                    //  old image at full size

      ww = ss_ww * size;
      hh = ss_hh * size;
      pxbnew = PXB_rescale_fast(ss_pxbnew,ww,hh);                                      //  new image at reduced size

      px = ss_ww * 0.5 * (1.0 - size);                                                 //  new image position, center --> NW corner
      py = ss_hh * 0.5 * (1.0 - size);
      PXB_copy_area(pxbnew,0,0,ww,hh,pxbold,px,py);                                    //  copy shrinking old image into new image

      gdk_cairo_set_source_pixbuf(cr,pxbold->pixbuf,0,0);                              //  paint new image
      cairo_paint(cr);
      PXB_free(pxbnew);
      PXB_free(pxbold);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  New image sweeps into view like a circular radar image

void ss_radar()
{
   int         px, py, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, dR, T, pT, dTmax;
   float       r, r1, r2;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   PIXBUF      *pixbuf;
   double      T0, Te, Tz;

   px = py = 0;                                                                        //  suppress compiler warnings

   pixels1 = ss_pxbold->pixels;
   pixels3 = ss_pxbnew->pixels;

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                                     //  max. line length, center to corner
   dR = 200;                                                                           //  line segment length
   T = pT = 0.0;
   dTmax = 1.3 / Rmax;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time

      T = 2.0 * PI * Te / Tz;                                                          //  0 ... 2 * PI
      if (T - pT > dTmax) T = pT + dTmax;
      pT = T;                                                                          //  enforce max. dT to avoid gaps
      if (T >= 2 * PI) break;

      cosT = cosf(T);
      sinT = sinf(T);

      for (R = 0; R < Rmax; R += dR)                                                   //  R from center to edge
      {
         r1 = R;                                                                       //  R segment
         r2 = R + dR;

         px1 = ww2 + r1 * cosT;                                                        //  R segment
         px2 = ww2 + r2 * cosT;
         py1 = hh2 - r1 * sinT;
         py2 = hh2 - r2 * sinT;

         for (r = r1; r <= r2; r++)                                                    //  loop R segment pixels
         {
            px = ww2 + r * cosT;
            py = hh2 - r * sinT;

            if (px < 0) px = 0;
            if (px > ss_ww-3) px = ss_ww-3;
            if (py < 0) py = 0;
            if (py > ss_hh-3) py = ss_hh-3;

            pix1 = pixels1 + py * ss_rs + px * 3;                                      //  copy new image pixels to old image
            pix3 = pixels3 + py * ss_rs + px * 3;
            memcpy(pix1,pix3,9);
            memcpy(pix1 + ss_rs, pix3 + ss_rs,9);
            memcpy(pix1 + 2 * ss_rs, pix3 + 2 * ss_rs,9);

            if (px == 0 || px == ss_ww-3) break;                                       //  reached edge of image
            if (py == 0 || py == ss_hh-3) break;
         }

         px2 = px;                                                                     //  actual end of R segment
         py2 = py;

         if (px1 < px2) px = px1;                                                      //  get rectangle enclosing R segment
         else px = px2;
         if (py1 < py2) py = py1;
         else py = py2;
         ww = abs(px2-px1) + 1;
         hh = abs(py2-py1) + 1;

         if (px < 0) px = 0;
         if (px > ss_ww-1) px = ss_ww-1;
         if (px + ww > ss_ww) ww = ss_ww - px;

         if (py < 0) py = 0;
         if (py > ss_hh-1) py = ss_hh-1;
         if (py + hh > ss_hh) hh = ss_hh - py;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold->pixbuf,px,py,ww,hh);             //  paint window rectangle
         gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py);
         cairo_paint(cr);
         g_object_unref(pixbuf);

         zmainloop();
      }

      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image opens up like a Japanese fan

void ss_japfan()
{
   int         px, py, pxL, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   double      R, Rmax, dR, T, pT, dTmax;
   double      r, r1, r2;
   double      cosT, sinT;
   double      T0, Te, Tz;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   PIXBUF      *pixbuf;

   px = py = 0;                                                                        //  suppress compiler warnings

   pixels1 = ss_pxbold->pixels;
   pixels3 = ss_pxbnew->pixels;

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                                     //  max. line length, center to corner
   dR = 200;
   dTmax = 1.3 / Rmax;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   pT = PI / 2;

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time

      T = 0.5 * PI - PI * Te / Tz;                                                     //  0.5 * PI ... -0.5 * PI
      T += 0.01;                                                                       //  avoid tiny gaps
      if (T < -0.51 * PI) break;
      if (pT - T > dTmax) T = pT - dTmax;                                              //  enforce max. dT to avoid gaps
      pT = T;

      cosT = cosf(T);
      sinT = sinf(T);

      for (R = 0; R < Rmax; R += dR)                                                   //  R from center to edge
      {
         r1 = R;                                                                       //  R segment
         r2 = R + dR;

         px1 = ww2 + r1 * cosT;                                                        //  R segment
         px2 = ww2 + r2 * cosT;
         py1 = hh2 - r1 * sinT;
         py2 = hh2 - r2 * sinT;

         for (r = r1; r <= r2; r++)                                                    //  loop R segment pixels
         {
            px = ww2 + r * cosT;
            py = hh2 - r * sinT;

            if (px < 0) px = 0;
            if (px > ss_ww-3) px = ss_ww-3;
            if (py < 0) py = 0;
            if (py > ss_hh-3) py = ss_hh-3;

            pix1 = pixels1 + py * ss_rs + px * 3;                                      //  copy new image pixels to old image
            pix3 = pixels3 + py * ss_rs + px * 3;                                      //  right side
            memcpy(pix1, pix3, 9);
            memcpy(pix1 + ss_rs, pix3 + ss_rs, 9);
            memcpy(pix1 + 2 * ss_rs, pix3 + 2 * ss_rs, 9);

            pxL = ss_ww - px - 3;

            pix1 = pixels1 + py * ss_rs + pxL * 3;                                     //  left side pixels
            pix3 = pixels3 + py * ss_rs + pxL * 3;
            memcpy(pix1, pix3, 9);
            memcpy(pix1 + ss_rs, pix3 + ss_rs, 9);
            memcpy(pix1 + 2 * ss_rs, pix3 + 2 * ss_rs, 9);

            if (px == 0 || px == ss_ww-3) break;                                       //  reached edge of image
            if (py == 0 || py == ss_hh-3) break;
         }

         px2 = px;                                                                     //  actual end of R segment
         py2 = py;

         if (px1 < px2) px = px1;                                                      //  get rectangle enclosing R segment
         else px = px2;
         if (py1 < py2) py = py1;
         else py = py2;
         ww = ABS(px2 - px1) + 1;
         hh = abs(py2 - py1) + 1;

         if (px < 0) px = 0;
         if (px > ss_ww-1) px = ss_ww-1;
         if (px + ww > ss_ww) ww = ss_ww - px;

         if (py < 0) py = 0;
         if (py > ss_hh-1) py = ss_hh-1;
         if (py + hh > ss_hh) hh = ss_hh - py;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold->pixbuf,px,py,ww,hh);             //  paint window rectangle
         gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py);
         cairo_paint(cr);
         g_object_unref(pixbuf);

         pxL = ss_ww-3 - px2;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold->pixbuf,pxL,py,ww,hh);            //  left side rectangle
         gdk_cairo_set_source_pixbuf(cr,pixbuf,pxL,py);
         cairo_paint(cr);
         g_object_unref(pixbuf);

         zmainloop();
      }

      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  New image spirals outwards from the center

void ss_spiral()
{
   int         px, py, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, dR, T, Tp;
   float       r, r1, r2;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   PIXBUF      *pixbuf;
   double      T0, Te, Tz;

   px = py = 0;                                                                        //  suppress compiler warnings

   pixels1 = ss_pxbold->pixels;
   pixels3 = ss_pxbnew->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                                     //  max. line length, center to corner
   dR = Rmax / 8;                                                                      //  radius step

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)
   Tp = 0;

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;

      T = 16 * PI * Te / Tz;                                                           //  0 ... 8 revolutions
      if (T - Tp > 0.002) T = Tp + 0.002;                                              //  compensate scheduling jitter
      Tp = T;

      R = Rmax * T / (16.0 * PI);                                                      //  0 ... Rmax
      r1 = R;                                                                          //  R segment
      if (T < 2 * PI) r1 = 0;
      r2 = R + dR + 4;

      cosT = cosf(T);
      sinT = sinf(T);

      px1 = ww2 + r1 * cosT;                                                           //  R segment
      px2 = ww2 + r2 * cosT;
      py1 = hh2 - r1 * sinT;
      py2 = hh2 - r2 * sinT;

      for (r = r1; r < r2; r++)                                                        //  loop R segment pixels
      {
         px = ww2 + r * cosT;
         py = hh2 - r * sinT;

         if (px < 0) px = 0;
         if (px > ss_ww-6) px = ss_ww-6;
         if (py < 0) py = 0;
         if (py > ss_hh-6) py = ss_hh-6;

         pix1 = pixels1 + py * ss_rs + px * 3;                                         //  copy new image pixels to old image
         pix3 = pixels3 + py * ss_rs + px * 3;
         memcpy(pix1, pix3, 18);
         memcpy(pix1 + ss_rs, pix3 + ss_rs, 18);
         memcpy(pix1 + 2 * ss_rs, pix3 + 2 * ss_rs, 18);
         memcpy(pix1 + 3 * ss_rs, pix3 + 3 * ss_rs, 18);
         memcpy(pix1 + 4 * ss_rs, pix3 + 4 * ss_rs, 18);
         memcpy(pix1 + 5 * ss_rs, pix3 + 5 * ss_rs, 18);

         if (px == 0 || px >= ss_ww-6) break;                                          //  reached edge of image
         if (py == 0 || py >= ss_hh-6) break;
      }

      if (px1 < px2) px = px1;                                                         //  get rectangle enclosing R segment
      else px = px2;
      if (py1 < py2) py = py1;
      else py = py2;
      ww = abs(px2-px1) + 20;
      hh = abs(py2-py1) + 20;

      if (px < 0) px = 0;
      if (px > ss_ww-1) px = ss_ww-1;
      if (px + ww > ss_ww) ww = ss_ww - px;

      if (py < 0) py = 0;
      if (py > ss_hh-1) py = ss_hh-1;
      if (py + hh > ss_hh) hh = ss_hh - py;

      pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold->pixbuf,px,py,ww,hh);                //  paint window rectangle
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,py);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  An ellipse opens up from the center and expands outward

void ss_ellipse()
{
   PIXBUF      *pixbuf;
   uint8       *pixels, *pix3;
   int         Np, px1, py1, ww;
   float       a, b, a2, b2, px, py, px2, py2;
   float       ww2 = ss_ww / 2, hh2 = ss_hh / 2;
   double      T0, Te, Tz;

   Np = 3;

   pixels = ss_pxbnew->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      a = ww2 * 1.4 * Te / Tz;                                                         //  ellipse a and b constants
      b = a * ss_hh / ss_ww;                                                           //    from tiny to >> image size
      a2 = a * a;
      b2 = b * b;

      for (py = -b; py <= +b; py += Np)                                                //  py from top of ellipse to bottom
      {
         while (py < -(hh2-Np+1)) py += Np;
         if (py > hh2-Np+1) break;
         py2 = py * py;
         px2 = a2 * (1.0 - py2 / b2);                                                  //  corresponding px value,
         px = sqrt(px2);                                                               //  (+/- from center of ellipse)
         if (px > ww2) px = ww2;
         ww = 2 * px;                                                                  //  length of line thru ellipse
         if (ww < 2) continue;
         px1 = ww2 - px;                                                               //  relocate origin
         py1 = py + hh2;
         if (px1 + ww > ss_ww) px1 = ss_ww - ww;                                       //  insurance
         if (py1 + Np > ss_hh) py1 = ss_hh - Np;
         pix3 = pixels + py1 * ss_rs + px1 * 3;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww,Np,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,px1,py1);
         cairo_paint(cr);
         g_object_unref(pixbuf);
         zmainloop();
      }

      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image splats onto old image one drop at a time

void ss_raindrops()
{
   PXB         *pxbmix;
   PIXBUF      *pixbuf;
   int         rsmix;
   int         px, py, px1, py1, px2, py2, cx, cy;
   int         Rmin, Rmax, R, R2, dist2;
   int         Ndrops = 3000;
   uint8       *pixels2, *pixels3;
   uint8       *pix2, *pix3 = 0;
   double      T0, Te, Tz;

   pixels2 = ss_pxbnew->pixels;                                                        //  source image

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   pxbmix = PXB_copy(ss_pxbold);                                                       //  destination image
   pixels3 = pxbmix->pixels;
   rsmix = pxbmix->rs;

   Rmin = ss_ww * 0.01;                                                                //  drop size range
   Rmax = ss_ww * 0.02;

   Ndrops = 3000;

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      cx = drandz() * ss_ww;                                                           //  drop location on image
      cy = drandz() * ss_hh;
      R = drandz() * Rmax + Rmin;                                                      //  drop size
      R2 = R * R;
      px1 = cx - R;
      if (px1 < 0) px1 = 0;
      py1 = cy - R;
      if (py1 < 0) py1 = 0;
      px2 = cx + R;
      if (px2 >= ss_ww) px2 = ss_ww;
      py2 = cy + R;
      if (py2 > ss_hh) py2 = ss_hh;

      for (py = py1; py < py2; py++)                                                   //  copy drop area from new image
      for (px = px1; px < px2; px++)                                                   //    to old image
      {
         dist2 = (px-cx) * (px-cx) + (py-cy) * (py-cy);
         if (dist2 > R2) continue;
         pix2 = pixels2 + py * ss_rs + px * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix2,3);
      }

      pixbuf = gdk_pixbuf_new_subpixbuf(pxbmix->pixbuf,px1,py1,px2-px1,py2-py1);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px1,py1);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      zmainloop();
      zloop(ss_trantime / Ndrops);
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image spreads from the middle to left and right edges
//  like a double-door swinging open

void ss_doubledoor()
{
   #define GPNFD(pix,ww,hh) gdk_pixbuf_new_from_data(pix,GDKRGB,0,8,ww,hh,ss_rs,0,0)

   PIXBUF      *pixbuf;
   int         bx, px;
   uint8       *pixels, *pix3;
   double      T0, Te, Tz;

   pixels = ss_pxbnew->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      Te = pow(Te,1.7);                                                                //  slow ... fast

      bx = 1.0 * Te / Tz * ss_ww / 2;                                                  //  bx = 0 ... ww/2

      px = ss_ww / 2 - bx;
      if (px < 0) break;
      pix3 = pixels + 3 * px;                                                          //  line from (ww/2-bx,0) to (ww/2-bx,hh-1)
      pixbuf = GPNFD(pix3,8,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      px = ss_ww / 2 + bx;
      if (px > ss_ww-8) break;
      pix3 = pixels + 3 * px;                                                          //  line from (ww/2+bx,0) to (ww/2+bx,hh-1)
      pixbuf = GPNFD(pix3,8,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      zmainsleep(0.001);                                                               //  avoids jerky start (why?)
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  Rotate from old image to new image. Horizontal rotate image 'page'.

namespace ss_rotate_names
{
   int      cx1, cy1, cx2, cy2, cy3, cy4;
   uint8    *pixels1, *pixels2, *pixels3;
   int      rsmix;
}


void ss_rotate()
{
   using namespace ss_rotate_names;

   void * ss_rotate_thread1(void *arg);
   void * ss_rotate_thread2(void *arg);

   PIXBUF      *pixbuf;
   double      T0, Te, Tz, R;

   pixels1 = ss_pxbold->pixels;
   pixels2 = ss_pxbnew->pixels;

   pixbuf = gdk_pixbuf_new(GDKRGB,0,8,ss_ww,ss_hh);                                    //  destination image
   rsmix = gdk_pixbuf_get_rowstride(pixbuf);
   pixels3 = gdk_pixbuf_get_pixels(pixbuf);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = 0.5 * ss_trantime;                                                             //  transition time per 1/2 rotate (90 deg.)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      R = Te / Tz;
      R = pow(R,1.8);                                                                  //  slow ... fast
      cx1 = R * ss_ww / 2.0;                                                           //  corners of shrinking trapezoid
      cy1 = 0.3 * R * ss_hh;
      cx2 = ss_ww - cx1;
      cy2 = 0;
      cy3 = ss_hh;
      cy4 = ss_hh - cy1;

      memset(pixels3,0,ss_hh * rsmix);

      do_wthreads(ss_rotate_thread1,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   T0 = get_seconds();                                                                 //  transition start time

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;
      R = pow(R,0.8);                                                                  //  fast ... slow
      cx1 = ss_ww * (0.5 + 0.5 * R);                                                   //  corners of expanding trapezoid
      cy1 = ss_hh * (0.3 - 0.3 * R);
      cx2 = ss_ww * (0.5 - 0.5 * R);
      cy2 = 0;
      cy3 = ss_hh;
      cy4 = ss_hh - cy1;

      memset(pixels3,0,ss_hh * rsmix);

      do_wthreads(ss_rotate_thread2,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   g_object_unref(pixbuf);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_rotate_thread1(void *arg)
{
   using namespace ss_rotate_names;

   int      index = *((int *) (arg));
   int      px, py, ylo, yhi, vpx, vpy;
   uint8    *pix1, *pix3;
   float    Rx, Ry;

   for (px = cx1 + 2 * index; px < cx2-1; px += 2 * ss_NT)
   {
      Rx = 1.0 * (px - cx1) / (cx2 - cx1);
      ylo = cy1 + Rx * (cy2 - cy1);
      yhi = cy4 + Rx * (cy3 - cy4);

      for (py = ylo; py < yhi-1; py += 2)
      {
         Ry = 1.0 * (py - ylo) / (yhi - ylo);
         vpx = Rx * (ss_ww - 1);
         vpy = Ry * (ss_hh - 1);

         pix1 = pixels1 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix1,6);
         memcpy(pix3+rsmix,pix1+ss_rs,6);                                              //  speedup 25.0
      }
   }

   return 0;
}


void * ss_rotate_thread2(void *arg)
{
   using namespace ss_rotate_names;

   int      index = *((int *) (arg));
   int      px, py, ylo, yhi, vpx, vpy;
   uint8    *pix2, *pix3;
   float    Rx, Ry;

   for (px = cx2 + 2 * index; px < cx1-1; px += 2 * ss_NT)
   {
      Rx = 1.0 * (px - cx2) / (cx1 - cx2);
      ylo = cy2 + Rx * (cy1 - cy2);
      yhi = cy3 + Rx * (cy4 - cy3);

      for (py = ylo; py < yhi-1; py += 2)
      {
         Ry = 1.0 * (py - ylo) / (yhi - ylo);
         vpx = Rx * (ss_ww - 1);
         vpy = Ry * (ss_hh - 1);

         pix2 = pixels2 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix2,6);
         memcpy(pix3+rsmix,pix2+ss_rs,6);                                              //  speedup  25.0
      }
   }

   return 0;
}


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

//  Old image falls over to reveal new image.

namespace ss_fallover_names
{
   int      cx1, cy1, cx2, cy2;
   uint8    *pixels1, *pixels2, *pixels3;
   int      rsmix;
}


void ss_fallover()
{
   using namespace ss_fallover_names;

   void * ss_fallover_thread(void *arg);

   PIXBUF      *pixbuf;
   float       R;
   double      T0, Te, Tz;

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image

   pixbuf = gdk_pixbuf_new(GDKRGB,0,8,ss_ww,ss_hh);                                    //  output image - mixture
   rsmix = gdk_pixbuf_get_rowstride(pixbuf);
   pixels3 = gdk_pixbuf_get_pixels(pixbuf);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = 1.0 * Te / Tz;                                                               //  0 ... 1
      R = pow(R,3);                                                                    //  slow ... fast

      cx1 = 0.2 * ss_ww * R;                                                           //  top corners of falling old image
      cy1 = ss_hh * R;
      cx2 = ss_ww - cx1;
      cy2 = cy1;

      do_wthreads(ss_fallover_thread,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainsleep(0.001);                                                               //  reduce jitter                         25.0
      if (ss_escape) break;
   }

   g_object_unref(pixbuf);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_fallover_thread(void *arg)                                                   //  construct combined image
{
   using namespace ss_fallover_names;

   int      index = *((int *) (arg));
   int      px, py, npix, vpx, vpy;
   int      cx1a, cx2a;
   float    R;
   uint8    *pix1, *pix3;

   for (py = index; py < ss_hh; py += ss_NT)                                           //  py = 0 ... ss_hh
   {
      px = 0;                                                                          //  px = 0 ... ss_ww, new image
      npix = ss_ww;
      pix1 = pixels2 + py * ss_rs + px * 3;
      pix3 = pixels3 + py * rsmix + px * 3;
      memcpy(pix3,pix1,npix*3);

      if (py < cy1) continue;

      R = 1.0 * (py - cy1) / (ss_hh - cy1);                                            //  0 ... 1
      cx1a = cx1 * (1.0 - R);                                                          //  cx1a = cx1 ... 0
      cx2a = cx2 + R * (ss_ww - cx2);                                                  //  cx2a = cx2 ... ss_ww

      for (px = cx1a; px < cx2a-1; px += 2)                                            //  px = cx1a ... cx2a
      {
         vpx = ss_ww * (px - cx1a) / (cx2a - cx1a);                                    //  vpx = 0 ... ss_ww
         vpy = ss_hh * R;                                                              //  vpy = 0 ... ss_hh
         pix1 = pixels1 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix1,6);                                                          //  speedup  25.0
      }
   }

   return 0;
}


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

//  old image deforms from flat to sphere and then shrinks to reveal new image

namespace ss_spheroid_names
{
   float       Cx, Cy, D;
   PXB         *pxbold, *pxbnew;
   uint8       *pixels1, *pixels3;
   float       *s1mem, *s2mem, Rmax;
}


void ss_spheroid()
{
   using namespace ss_spheroid_names;

   void * ss_spheroid_thread(void *arg);

   int         ii, cc, px, py, dx, dy;
   double      T0, Te, Tz;
   float       F;

   cc = ss_ww * ss_hh * sizeof(float);
   s1mem = (float *) zmalloc(cc,"ss");

   Rmax = 1.0 + 0.5 * sqrtf(ss_ww * ss_ww + ss_hh * ss_hh);
   cc = 10.1 * Rmax * sizeof(float);                                                   //  float rounding
   s2mem = (float *) zmalloc(cc,"ss");

   pxbold = PXB_copy(ss_pxbold);                                                       //  old image at full size
   pixels1 = pxbold->pixels;

   Cx = ss_ww / 2;                                                                     //  center of image
   Cy = ss_hh / 2;

   for (py = 0; py < ss_hh; py++)                                                      //  pre-calculate
   for (px = 0; px < ss_ww; px++)
   {
      dx = px - Cx;
      dy = py - Cy;
      ii = py * ss_ww + px;
      s1mem[ii] = sqrtf(dx*dx + dy*dy);                                                //  dist. from center to pixel
   }

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      F = 1.0 - Te / Tz;                                                               //  1 ... 0
      float D1 = pow(F,10) * 8 * ss_ww;
      float D2 = pow(F,6) * 3 * ss_ww;
      float D3 = F * 2 * ss_ww;
      D = D1 + D2 + D3 + 10;

      pxbnew = PXB_copy(ss_pxbnew);                                                    //  new image at full size
      pixels3 = pxbnew->pixels;

      do_wthreads(ss_spheroid_thread,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,pxbnew->pixbuf,0,0);                              //  paint new image
      cairo_paint(cr);
      PXB_free(pxbnew);

      zmainloop();
      if (ss_escape) break;
   }

   zfree(s1mem);
   zfree(s2mem);
   PXB_free(pxbold);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_spheroid_thread(void *arg)
{
   using namespace ss_spheroid_names;

   int      index = *((int *) arg);
   int      ii, px0, py0, px3, py3, dx, dy;
   float    px1, py1, s1, s2, T;
   uint8    *pix0, *pix1, *pix2, *pix3, *pixx, *pixx2;
   float    f0, f1, f2, f3;

   for (ii = 0; ii < 10 * Rmax; ii++)                                                  //  pre-calculate
   {
      s1 = 0.1 * ii;
      T = s1 * PI / D;
      if (s1 == 0) s2mem[ii] = 0;
      else if (T > 1.0) s2mem[ii] = -1;
      else s2mem[ii] = D / PI * asinf(T) / s1;
   }

   for (py3 = 2 * index; py3 < ss_hh-1; py3 += 2 * ss_NT)                              //  loop all output pixels
   for (px3 = 0; px3 < ss_ww-1; px3 += 2)
   {
/***
      dx = px3 - Cx;                                                                   //  code without pre-calculations
      dy = py3 - Cy;
      s1 = sqrtf(dx*dx + dy*dy);                                                       //  dist. from center to output pixel
      if (s1 == 0) continue;
      T = s1 * PI / D;                                                                 //  sine of subtended angle
      if (T > 1.0) continue;
      s2 = D / PI * asinf(T);                                                          //  corresp. dist. on sphere
      px1 = Cx + dx * s2 / s1;                                                         //  input v.pixel
      py1 = Cy + dy * s2 / s1;
***/
      dx = px3 - Cx;
      dy = py3 - Cy;
      ii = py3 * ss_ww + px3;
      s1 = s1mem[ii];                                                                  //  dist. from center to output pixel
      ii = 10 * s1;
      s2 = s2mem[ii];                                                                  //  corresp. dist. on sphere / s1
      if (s2 < 0) continue;
      px1 = Cx + dx * s2;                                                              //  input v.pixel
      py1 = Cy + dy * s2;

      //  inline vpixel() for speed
      px0 = px1;                                                                       //  px0/py0: integer px1/py1
      py0 = py1;

      if (px0 < 0 || py0 < 0) continue;
      if (px0 > ss_ww-3 || py0 > ss_hh-3) continue;

      f0 = (px0+1 - px1) * (py0+1 - py1);                                              //  overlap of (px,py)
      f1 = (px0+1 - px1) * (py1 - py0);                                                //   in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);

      pix0 = pixels1 + py0 * ss_rs + px0 * 3;                                          //  pixel (px0,py0)
      pix1 = pix0 + ss_rs;                                                             //        (px0,py0+1)
      pix2 = pix0 + 3;                                                                 //        (px0+1,py0)
      pix3 = pix1 + 3;                                                                 //        (px0+1,py0+1)

      pixx = pixels3 + py3 * ss_rs + px3 * 3;                                          //  input v.pixel >> output pixel
      pixx[0] = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];
      pixx[1] = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
      pixx[2] = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
      pixx[3] = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3];
      pixx[4] = f0 * pix0[4] + f1 * pix1[4] + f2 * pix2[4] + f3 * pix3[4];
      pixx[5] = f0 * pix0[5] + f1 * pix1[5] + f2 * pix2[5] + f3 * pix3[5];
      pixx2 = pixx + ss_rs;
      memcpy(pixx2,pixx,6);
   }

   return 0;
}


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

//  new image turns up from lower right corner, like a book page

namespace ss_turnpage_names
{
   uint8       *pixels1, *pixels2, *pixels3;
   int         pxA;
}


void ss_turnpage()
{
   using namespace ss_turnpage_names;

   void * ss_turnpage_thread1(void *arg);
   void * ss_turnpage_thread2(void *arg);

   PXB         *pixbuf3;
   double      T0, Te, Tz, Prange;

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image
   pixbuf3 = PXB_copy(ss_pxbold);                                                      //  output image - mixture
   pixels3 = pixbuf3->pixels;                                                          //  (initially = old image)

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   Prange = 1.31 * (ss_ww + ss_hh);                                                    //  pxA loop range
   pxA = ss_ww;

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      pxA = ss_ww - Prange * Te / Tz;

      do_wthreads(ss_turnpage_thread1,ss_NT);                                          //  use threads to reduce jitter          25.0
      do_wthreads(ss_turnpage_thread2,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,pixbuf3->pixbuf,0,0);                             //  paint image
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pixbuf3);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_turnpage_thread1(void *arg)
{
   using namespace ss_turnpage_names;

   int         index = *((int *) arg);
   uint8       *pix2, *pix3;
   int         px1, py1;
   int         cc;

   for (px1 = pxA+index, py1 = ss_hh-1-index;
        px1 < ss_ww && py1 >= 0;
        px1 += ss_NT, py1 -= ss_NT)
   {
      pix2 = pixels2 + py1 * ss_rs + px1 * 3;                                          //  new image row
      pix3 = pixels3 + py1 * ss_rs + px1 * 3;                                          //  output image row
      cc = 3 * (ss_ww - px1);
      if (px1 < 0) {
         pix2 -= 3 * px1;
         pix3 -= 3* px1;
         cc += 3 * px1;
      }
      if (cc < 1) continue;
      if (cc > 3 * ss_ww) cc = cc / 2;
      memcpy(pix3,pix2,cc);                                                            //  paint new image from px1 to right edge
   }

   return 0;
}


void * ss_turnpage_thread2(void *arg)
{
   using namespace ss_turnpage_names;

   int         index = *((int *) arg);
   uint8       *pix1, *pix3;
   int         pxS;
   int         px1, py1, px3, py3;
   float       C;

   for (pxS = pxA+index; pxS < ss_ww; pxS += ss_NT)                                    //  point S moves from point A to the right
   {
      C = 0.53 * (pxS - pxA) / (ss_ww - pxA);
      C = C * (pxS - pxA);
                                                                                       //  loop old image pixels
      for (px1 = pxS, py1 = ss_hh-1; px1 < ss_ww && py1 >= 0; px1++, py1--)
      {
         px3 = px1 - C;                                                                //  dest pixel = source pixel
         py3 = py1 - C;                                                                //    offset in NW direction
         if (px3 < 0 || px3 > ss_ww-1) continue;
         if (py3 < 0 || py3 > ss_hh-1) continue;
         pix1 = pixels1 + py1 * ss_rs + px1 * 3;                                       //  source pixel --> dest pixel
         pix3 = pixels3 + py3 * ss_rs + px3 * 3;
         memcpy(pix3,pix1,3);
      }
   }

   return 0;
}


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

//  French door: old image swings away left and right to reveal new image.

namespace ss_frenchdoor_names
{
   PXB      *pxbmix;
   PIXBUF   *pixbuf;
   float    R, ww2;
   uint8    *pixels1, *pixels3;
   int      ww3, ww4, sww;
}


void ss_frenchdoor()
{
   using namespace ss_frenchdoor_names;

   #define GPNFD(pix,ww,hh)   \
   gdk_pixbuf_new_from_data(pix,GDKRGB,0,8,ww,hh,ss_rs,0,0)

   void * ss_frenchdoor_thread(void *arg);

   double      T0, Te, Tz;

   ww2 = 0.5 * ss_ww;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te >= Tz) break;                                                             //  done

      R = Te / Tz;                                                                     //  0 ... 1

      R = R * R;                                                                       //  start slower

      pixels1 = ss_pxbold->pixels;                                                     //  old image
      pxbmix = PXB_copy(ss_pxbnew);                                                    //  mixed image <-- background
      pixels3 = pxbmix->pixels;

      do_wthreads(ss_frenchdoor_thread,ss_NT);

      sww = 60;                                                                        //  overpaint stripe

      ww3 = (1 - R) * ww2 + sww;                                                       //  left side
      pixbuf = GPNFD(pixels3,ww3,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      ww4 = (1 + R) * ww2 - sww;                                                       //  right side
      pixbuf = GPNFD(pixels3+ww4*3,ww3+sww,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,ww4,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      PXB_free(pxbmix);
      if (ss_escape) break;

      zmainsleep(0.001);                                                               //  reduce jitter                         25.0
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_frenchdoor_thread(void *arg)
{
   using namespace ss_frenchdoor_names;

   int      index = *((int *) arg);
   uint8    *pix1, *pix3;
   int      px1, py1, px3, py3, px3L = -1;
   float    F;

   for (py1 = 4*index; py1 < ss_hh-3; py1 += 4*ss_NT)                                  //  0 >> ss_hh              smoother
   for (px1 = 0; px1 < ww2-1; px1 += 2)                                                //  0 >> ww2
   {
      px3 = (1 - R) * px1;                                                             //  0 >> ww2 - X
      if (px3 == px3L) continue;
      px3L = px3;

      F = 0.2 * ss_hh * R * px1 / ww2;                                                 //  0 >> max
      py3 = F + (1.0 * py1 / ss_hh) * (ss_hh - 2 * F);                                 //  F >> ss_hh - F
      pix1 = pixels1 + py1 * ss_rs + px1 * 3;
      pix3 = pixels3 + py3 * ss_rs + px3 * 3;
      memcpy(pix3,pix1,6);
      memcpy(pix3 + ss_rs, pix1 + ss_rs, 6);
      memcpy(pix3 + 2 * ss_rs, pix1 + 2 * ss_rs, 6);
      memcpy(pix3 + 3 * ss_rs, pix1 + 3 * ss_rs, 6);
   }

   for (py1 = 4*index; py1 < ss_hh-3; py1 += 4*ss_NT)                                  //  0 >> ss_hh
   for (px1 = ww2; px1 < ss_ww-1; px1 += 2)                                            //  ww2 >> ss_ww
   {
      px3 = R * ss_ww + (1 - R) * px1;                                                 //  ww2 + X >> ss_ww
      if (px3 == px3L) continue;
      px3L = px3;

      F = 0.2 * ss_hh * R * (ss_ww - px1) / ww2;                                       //  max >> 0
      py3 = F + (1.0 * py1 / ss_hh) * (ss_hh - 2 * F);                                 //  F >> ss_hh - F
      pix1 = pixels1 + py1 * ss_rs + px1 * 3;
      pix3 = pixels3 + py3 * ss_rs + px3 * 3;
      memcpy(pix3,pix1,6);
      memcpy(pix3 + ss_rs, pix1 + ss_rs, 6);
      memcpy(pix3 + 2 * ss_rs, pix1 + 2 * ss_rs, 6);
      memcpy(pix3 + 3 * ss_rs, pix1 + 3 * ss_rs, 6);
   }

   return 0;
}


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

//  new image rotates over old image in many radial segments

void ss_windmill()
{
   int         px, py;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, T1, T2, T;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   double      T0, Te, Tz;

   pixels1 = ss_pxbold->pixels;
   pixels3 = ss_pxbnew->pixels;

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                                     //  max. line length, center to corner

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      T1 = Te / Tz * PI / 9;

      for (T2 = 0; T2 < 2*PI; T2 += PI/9)                                              //  segments
      {
         T = T1 + T2;                                                                  //  segment and step
         cosT = cosf(T);
         sinT = sinf(T);

         for (R = 0; R < Rmax; R++)                                                    //  radial line from center to edge
         {
            px = ww2 + R * cosT;
            py = hh2 + R * sinT;
            if (px < 0 || px > ss_ww-3) break;
            if (py < 0 || py > ss_hh-3) break;
            pix1 = pixels1 + py * ss_rs + px * 3;                                      //  copy new image pixel to old image
            pix3 = pixels3 + py * ss_rs + px * 3;
            memcpy(pix1,pix3,9);
            memcpy(pix1 + ss_rs, pix3 + ss_rs, 9);
            memcpy(pix1 + 2 * ss_rs, pix3 + 2 * ss_rs, 9);
         }
      }

      gdk_cairo_set_source_pixbuf(cr,ss_pxbold->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  Old image is increasingly / decreasingly pixelated to reveal new image

void ss_pixelize()
{
   PXB         *pxbmix;
   int         px, py, px1, py1, px2, py2;
   int         ii, cc, Npix, blocksize;
   int         Rsum, Gsum, Bsum, Ravg, Gavg, Bavg;
   uint8       *pixels1, *pixels2, *pixels3, *pix3;
   float       F1, F2;
   double      T0, Te, Tz;

   pxbmix = PXB_copy(ss_pxbnew);
   pixels1 = ss_pxbold->pixels;
   pixels2 = ss_pxbnew->pixels;
   pixels3 = pxbmix->pixels;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      F1 = Te / Tz;                                                                    //  new image part, 0 ... 1
      F2 = 1 - F1;                                                                     //  old image part, 1 ... 0

      cc = gdk_pixbuf_get_byte_length(pxbmix->pixbuf);                                 //  pxbmix = mix of old + new images
      for (ii = 0; ii < cc; ii++)
         pixels3[ii] = F1 * pixels2[ii] + F2 * pixels1[ii];                            //  pxbold + pxbnew >> pxbmix

      blocksize = 0.05 * ss_ww;
      if (F1 < 0.5) blocksize = 2 + blocksize * F1;                                    //  pixel block size, 2 ... max ... 2
      else blocksize = 2 + blocksize * F2;

      for (py1 = 0; py1 < ss_hh; py1 += blocksize)                                     //  loop pixel blocks
      for (px1 = 0; px1 < ss_ww; px1 += blocksize)
      {
         py2 = py1 + blocksize;                                                        //  block region in image
         if (py2 > ss_hh) py2 = ss_hh;
         px2 = px1 + blocksize;
         if (px2 > ss_ww) px2 = ss_ww;

         Rsum = Gsum = Bsum = Npix = 0;

         for (py = py1; py < py2; py++)                                                //  loop pixels in pixel block
         for (px = px1; px < px2; px++)
         {
            pix3 = pixels3 + py * ss_rs + px * 3;
            Rsum += pix3[0];                                                           //  sum pixel RGB values
            Gsum += pix3[1];
            Bsum += pix3[2];
            Npix++;
         }

         Ravg = Rsum / Npix;                                                           //  mean pixel RGB values for block
         Gavg = Gsum / Npix;
         Bavg = Bsum / Npix;

         for (py = py1; py < py2; py++)                                                //  loop pixels in pixel block
         for (px = px1; px < px2; px++)
         {
            pix3 = pixels3 + py * ss_rs + px * 3;
            pix3[0] = Ravg;                                                            //  set all pixel RGB values to mean
            pix3[1] = Gavg;
            pix3[2] = Bavg;
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  Old image twists 0 >> 360 deg. as new image untwists -360 >> 0 deg.
//  The old image fades-out as the new image fades-in.

namespace ss_twist_names
{
   PXB      *PXBmix;
   int      cx, cy;
   float    Told, Tnew;
   float    F1, F2;
   float    *D, *Tp;
   float    Dmax;
}


void ss_twist()
{
   using namespace ss_twist_names;

   void * ss_twist_thread(void *arg);

   int         ii, cc, px, py, Dx, Dy;
   double      T0, Te, Tz, R;

   cx = ss_ww / 2;                                                                     //  image center
   cy = ss_hh / 2;
   Dmax = sqrtf(cx * cx + cy * cy);                                                    //  distance to corner

   cc = ss_ww * ss_hh;
   D = (float *) zmalloc(cc * sizeof(float),"ss");                                     //  precalculated factors
   Tp = (float *) zmalloc(cc * sizeof(float),"ss");                                    //    depending only on px, py

   for (py = 0; py < ss_hh; py++)                                                      //  loop all pixels
   for (px = 0; px < ss_ww; px++)
   {
      ii = py * ss_ww + px;

      Dx = px - cx;                                                                    //  px/py relative to cx/cy
      Dy = py - cy;

      D[ii] = Dx * Dx + Dy * Dy;                                                       //  distance pixel to center
      if (D[ii] == 0) continue;
      D[ii] = sqrtf(D[ii]);

      Tp[ii] = asinf(Dy/D[ii]);                                                        //  angle of pixel line to center
      if (Dx < 0) {
         if (Dy > 0) Tp[ii] = PI - Tp[ii];
         else Tp[ii] = - PI - Tp[ii];
      }
   }

   PXBmix = PXB_copy(ss_pxbold);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;                                                                     //  0 ... 1
      R = pow(R,1.7);                                                                  //  slow ... fast

      F1 = R;                                                                          //  new image part, 0 ... 1
      F2 = 1 - F1;                                                                     //  old image part, 1 ... 0

      Told = 2 * PI * F1;                                                              //  old image twist 0 ... 360 deg.
      Tnew = - 2 * PI + Told;                                                          //  new image twist -360 ...  deg.
/*    Tnew = -Tnew;               */                                                   //  opposite directions

      do_wthreads(ss_twist_thread,ss_NT);

      gdk_cairo_set_source_pixbuf(cr,PXBmix->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(PXBmix);
   zfree(D);
   zfree(Tp);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


void * ss_twist_thread(void *arg)
{
   using namespace ss_twist_names;

   int      index = *((int *) arg);
   int      ii, px, py, qx, qy;
   float    DN, Tq, T;
   uint8    *pixmix, *vpix, pix1[3], pix2[3], pix3[3];

   for (py = 2 * index; py < ss_hh-1; py += 2 * ss_NT)                                 //  loop all pixels
   for (px = 0; px < ss_ww-1; px += 2)
   {
      ii = py * ss_ww + px;

      DN = D[ii]/Dmax;                                                                 //  distance from center, 0.0 ... 1.0

      T = Told * DN;                                                                   //  old image rotation at distance
      Tq = Tp[ii] + T;                                                                 //  rotated pixel angle

      qx = D[ii] * cosf(Tq) + cx;                                                      //  rotated pixel position
      qy = D[ii] * sinf(Tq) + cy;
      if (qx > 0 && qx < ss_ww && qy > 0 && qy < ss_hh) {
         vpix = PXBpix(ss_pxbold,qx,qy);
         memcpy(pix1,vpix,3);
      }
      else memset(pix1,0,3);

      T = Tnew * DN;                                                                   //  same for new image
      Tq = Tp[ii] + T;

      qx = D[ii] * cosf(Tq) + cx;
      qy = D[ii] * sinf(Tq) + cy;
      if (qx > 0 && qx < ss_ww && qy > 0 && qy < ss_hh) {
         vpix = PXBpix(ss_pxbnew,qx,qy);
         memcpy(pix2,vpix,3);
      }
      else memset(pix2,0,3);

      pixmix = PXBpix(PXBmix,px,py);                                                   //  blend the twisted image
      pix3[0] = F2 * pix1[0] + F1 * pix2[0];
      pix3[1] = F2 * pix1[1] + F1 * pix2[1];
      pix3[2] = F2 * pix1[2] + F1 * pix2[2];
      pixmix[0] = pix3[0];
      pixmix[1] = pix3[1];
      pixmix[2] = pix3[2];
      pixmix[3] = pix3[0];
      pixmix[4] = pix3[1];
      pixmix[5] = pix3[2];
      pixmix += ss_rs;
      pixmix[0] = pix3[0];
      pixmix[1] = pix3[1];
      pixmix[2] = pix3[2];
      pixmix[3] = pix3[0];
      pixmix[4] = pix3[1];
      pixmix[5] = pix3[2];
   }

   return 0;
}


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

//  X-shaped region opens up from the center to reveal new image

void ss_Xopen()
{
   PXB         *pxbmix;
   int         xdisp, ydisp;
   int         px1, py1, px2, py2;
   int         pxL, pxH;
   int         ww = ss_ww, hh = ss_hh, rs = ss_rs;
   int         ww2 = ww/2, hh2 = hh/2;
   uint8       *pixels1, *pixels2;
   uint8       *pix1, *pix2;
   float       M = 1.0 * hh / ww;
   double      T0, Te, Tz, R;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;                                                                     //  0 ... 1
      R = pow(R,2);                                                                    //  slow ... fast

      pxbmix = PXB_copy(ss_pxbnew);
      pixels1 = ss_pxbold->pixels;                                                     //  old image
      pixels2 = pxbmix->pixels;                                                        //  new image + old image overlay

      ydisp = R * hh2;
      xdisp = ydisp / M;

      for (py1 = ydisp; py1 < hh2; py1++)                                              //  top triangle
      {
         pxL = py1/M;
         pxH = ww - pxL;
         for (px1 = pxL; px1 < pxH; px1++)
         {
            py2 = py1 - ydisp;
            px2 = px1;
            pix1 = pixels1 + py1 * rs + px1 * 3;
            pix2 = pixels2 + py2 * rs + px2 * 3;
            memcpy(pix2,pix1,3);
         }
      }

      for (py1 = ydisp; py1 < hh - ydisp; py1++)                                       //  right triangle
      {
         pxL = ww2 + abs(py1 - hh2)/M;
         pxH = ww - xdisp;
         for (px1 = pxL; px1 < pxH; px1++)
         {
            py2 = py1;
            px2 = px1 + xdisp;
            pix1 = pixels1 + py1 * rs + px1 * 3;
            pix2 = pixels2 + py2 * rs + px2 * 3;
            memcpy(pix2,pix1,3);
         }
      }

      for (py1 = hh2; py1 < hh - ydisp; py1++)                                         //  bottom triangle
      {
         pxL = ww2 - (py1 - hh2)/M;
         pxH = ww - pxL;
         for (px1 = pxL; px1 < pxH; px1++)
         {
            py2 = py1 + ydisp;
            px2 = px1;
            pix1 = pixels1 + py1 * rs + px1 * 3;
            pix2 = pixels2 + py2 * rs + px2 * 3;
            memcpy(pix2,pix1,3);
         }
      }

      for (py1 = ydisp; py1 < hh - ydisp; py1++)                                       //  left triangle
      {
         pxL = xdisp;
         pxH = ww2 - abs(py1 - hh2)/M;
         for (px1 = pxL; px1 < pxH; px1++)
         {
            py2 = py1;
            px2 = px1 - xdisp;
            pix1 = pixels1 + py1 * rs + px1 * 3;
            pix2 = pixels2 + py2 * rs + px2 * 3;
            memcpy(pix2,pix1,3);
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);
      cairo_paint(cr);
      PXB_free(pxbmix);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image presses out from center and squishes old image to the edges

namespace ss_squishout_names
{
   PXB         *pxbout;
   uint8       *pixold, *pixnew, *pixout;
   int         ww, hh, rs, cx, cy, cmin;
   float       R, Rmax;
   float       *De, *Dp;                                                               //  edge and pixel distance from center
   float       *Ex, *Ey;                                                               //  edge coordinates
}


void ss_squishout()
{
   using namespace ss_squishout_names;

   void * ss_squishout_thread(void *arg);

   int         px, py, qx, qy, ex, ey;
   int         cc, ii, Q;
   float       M, Ra;
   double      T0, Te, Tz;

   ww = ss_ww;
   hh = ss_hh;
   rs = ss_rs;
   cx = ww/2;
   cy = hh/2;
   Ra = 1.0 * hh/ww;
   Rmax = sqrtf(cx*cx + cy*cy);                                                        //  distance (0,0) to corner

   cmin = cy;
   if (cx < cy) cmin = cx;

   pxbout = PXB_copy(ss_pxbold);                                                       //  output image
   pixold = ss_pxbold->pixels;                                                         //  old image
   pixnew = ss_pxbnew->pixels;                                                         //  new image
   pixout = pxbout->pixels;

   cc = ww * hh * sizeof(float);                                                       //  allocate distance arrays
   De = (float *) zmalloc(cc,"ss");                                                    //  distance (0,0) to edge
   Dp = (float *) zmalloc(cc,"ss");                                                    //  distance (0,0) to (px,py)
   Ex = (float *) zmalloc(cc,"ss");                                                    //  line (0,0) to (px,py)
   Ey = (float *) zmalloc(cc,"ss");                                                    //    extended to edge (Ex,Ey)

   for (py = 0; py < hh; py++)                                                         //  compute distances to center
   for (px = 0; px < ww; px++)                                                         //    and to edge for each pixel
   {
      qx = px - cx;                                                                    //  convert to center origin
      qy = py - cy;

      if (qx == 0) {
         M = 1000;                                                                     //  qx = 0 >> slope = huge
         if (qy > 0) Q = 4;
         else Q = 2;
      }
      else M = 1.0 * qy / qx;                                                          //  slope of line (0,0) to (px,py)

      Q = 0;
      ex = ey = 0;

      if (qx > 0) {                                                                    //  extended line (0,0) to (px,py)
         if (qy > 0) {                                                                 //    intersects which edge?
            if (M > Ra) Q = 4;                                                         //  bottom edge
            else Q = 1;                                                                //  right edge
         }
         else if (-M > Ra) Q = 2;                                                      //  top edge
         else Q = 1;                                                                   //  right edge
      }
      else {                                                                           //  bugfix
         if (qy > 0) {
            if (-M > Ra) Q = 4;                                                        //  bottom edge
            else Q = 3;                                                                //  left edge
         }
         else {
            if (M > Ra) Q = 2;                                                         //  top edge
            else Q = 3;                                                                //  left edge
         }
      }

      if (Q == 1) {                                                                    //  get edge intersect coordinates
         ex = ww;
         ey = cy + M * (ex-cx);
      }
      else if (Q == 2) {
         ey = 0;
         ex = cx - cy / M;
      }
      else if (Q == 3) {
         ex = 0;
         ey = cy - cx * M;
      }
      else if (Q == 4) {
         ey = hh;
         ex = cx + (ey-cy) / M;
      }

      ii = ww * py + px;
      De[ii] = sqrtf((ex-cx)*(ex-cx) + (ey-cy)*(ey-cy));                               //  distance (0,0) to edge (ex,ey)
      Dp[ii] = sqrtf(qx*qx + qy*qy);                                                   //  distance (0,0) to (px,py)
      Ex[ii] = ex - cx;                                                                //  line (0.0) to (px,py)
      Ey[ii] = ey - cy;                                                                //    extended to edge (ex,ey)
   }

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      R = Te / Tz;                                                                     //  0 ... 1
      R = pow(R,2);                                                                    //  slow ... fast

      R = R * Rmax;

      do_wthreads(ss_squishout_thread,ss_NT);                                          //  speedup    25.0

      gdk_cairo_set_source_pixbuf(cr,pxbout->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   zfree(De);
   zfree(Dp);
   zfree(Ex);
   zfree(Ey);
   PXB_free(pxbout);
   return;
}


void * ss_squishout_thread(void *arg)                                                  //  speedup    25.0
{
   using namespace ss_squishout_names;

   int      index = *((int *) arg);

   uint8    *pix1, *pix2;
   int      px, py, qx, qy, vx, vy;
   int      ii;
   float    V;

   for (py = index; py < hh; py += ss_NT)                                              //  loop all image pixels
   for (px = 0; px < ww-1; px += 2)
   {
      ii = ww * py + px;

      qx = px - cx;                                                                    //  convert to center origin
      qy = py - cy;

      if (Dp[ii] < R)                                                                  //  new image from center to R
      {
         V = (1.0 - R/Rmax) * (cmin/R) + (R/Rmax);
         vx = cx + V * qx;
         vy = cy + V * qy;
         if (vx < 0 || vx > ww-1) continue;
         if (vy < 0 || vy > hh-1) continue;
         pix1 = pixnew + vy * rs + vx * 3;                                             //  new image pixel >> output image
         pix2 = pixout + py * rs + px * 3;
         memcpy(pix2,pix1,6);
      }

      else if (De[ii] > R)                                                             //  old image from R to edge
      {
         V = (Dp[ii] - R) / (De[ii] - R);                                              //  source pixel position
         vx = cx + V * Ex[ii];
         vy = cy + V * Ey[ii];
         if (vx < 0 || vx > ww-1) continue;
         if (vy < 0 || vy > hh-1) continue;
         pix1 = pixold + vy * rs + vx * 3;                                             //  old image pixel >> output image
         pix2 = pixout + py * rs + px * 3;
         memcpy(pix2,pix1,6);
      }
   }

   return 0;
}


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

//  old image disintegrates to reveal new image
//  pieces detach randomly and fall to the bottom

void ss_disintegrate()
{
   PXB         *pxbmix;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix3;
   double      T0, Te, Tz;
   int         cc, row, col, posn;
   int         tww = ss_ww / 32, thh = ss_hh / 32;                                     //  tile width, height
   float       time0[32][32], time, ftime;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image
   pxbmix = PXB_copy(ss_pxbold);                                                       //  mixed image
   pixels3 = pxbmix->pixels;

   for (row = 0; row < 32; row++)                                                      //  initialize tiles
   for (col = 0; col < 32; col++)
      time0[row][col] = 0.6 * drandz();                                                //  fall start, time 0 .. 0.6

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      time = Te / Tz;                                                                  //  0 .. 1

      cc = gdk_pixbuf_get_byte_length(pxbmix->pixbuf);
      memcpy(pixels3,pixels2,cc);                                                      //  mixed image = new image

      for (row = 0; row < 32; row++)                                                   //  loop all tiles
      for (col = 0; col < 32; col++)
      {
         posn = row * thh;                                                             //  initial vertical position

         if (time >= time0[row][col]) {                                                //  tile is falling
            ftime = time - time0[row][col];                                            //  fall time
            posn += ss_hh * (ftime * ftime / 0.16);                                    //  new fall position
         }

         if (posn >= ss_hh - thh) continue;                                            //  finished falling, no paint

         pix1 = pixels1 + row * thh * ss_rs + col * tww * 3;                           //  tile position in old image
         pix3 = pixels3 + posn * ss_rs + col * tww * 3;                                //  tile position in mixed image

         for (int ii = 0; ii < thh; ii++) {                                            //  copy tile from old image
            memcpy(pix3,pix1,tww*3);                                                   //    to mixed image
            pix1 += ss_rs;
            pix3 += ss_rs;
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);                              //  paint mixed image to window
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  old image is divided into tiles.
//  tiles rotate in place to reveal new image on flip side.

void ss_fliptiles()
{
   int      row, col, Nrows = 10, Ncols = 15;                                          //  rows and nominal columns
   int      px, py, ww, hh, rw, rh;
   int      nt1, nt2;
   ch       hv;

   struct tile_t {                                                                     //  tile data
      int      px, py;                                                                 //  position
      int      ww, hh;                                                                 //  size
      ch       hv;                                                                     //  horz or vert flip mode
   };

   tile_t   tile1[200], tile2[200];                                                    //  Nrows * Ncols + extra

   nt1 = 0;                                                                            //  tile1 count

   for (row = 0; row < Nrows; row++)                                                   //  generate tiles
   for (col = 0; ; col++)
   {
      if (col == 0) px = 0;                                                            //  first tile in row at left edge
      else px = px + ww;                                                               //  else at previous tile + width
      if (px == ss_ww) break;                                                          //  row already full, next row

      ww = ss_ww / Ncols;                                                              //  nominal tile width
      rw = 1.5 * drandz() * ww;                                                        //  random tile width adder
      if (col == 0) ww += 0.5 * rw;
      else {
         px -= 0.5 * rw;
         ww += rw;
      }

      if (px + ww > ss_ww) ww = ss_ww - px;                                            //  limit width to image edge
      if (ss_ww - px - ww < 0.5 * (ss_ww / Ncols)) ww = ss_ww - px;                    //  don't leave narrow remnant

      py = ss_hh * row / Nrows;                                                        //  nominal tile top position
      hh = ss_hh / Nrows;                                                              //  nominal tile height
      rh = 1.5 * drandz() * hh;                                                        //  random tile height adder
      if (row == 0) hh += 0.5 * rh;                                                    //  randomize top row tile height
      else {
         py -= 0.5 * rh;                                                               //  randomize tile position and height
         hh += rh;
      }

      if (row == Nrows - 1) hh = ss_hh - py;                                           //  bottom row tiles at image edge

      tile1[nt1].px = px;                                                              //  add to tile table
      tile1[nt1].py = py;
      tile1[nt1].ww = ww;
      tile1[nt1].hh = hh;
      if (drandz() < 0.5) hv = 'h';                                                    //  half the tiles flip horizontally,
      else hv = 'v';                                                                   //     half vertically
      tile1[nt1].hv = hv;
      nt1++;
   }

   nt2 = 0;                                                                            //  tile2 count

   for (row = 0; row < Nrows; row++)                                                   //  generate tiles
   for (col = 0; ; col++)
   {
      if (col == 0) px = 0;                                                            //  first tile in row at left edge
      else px = px + ww;                                                               //  else at previous tile + width
      if (px == ss_ww) break;                                                          //  row already full, next row

      ww = ss_ww / Ncols;                                                              //  nominal tile width
      rw = 1.5 * drandz() * ww;                                                        //  random tile width adder
      if (col == 0) ww += 0.5 * rw;
      else {
         px -= 0.5 * rw;
         ww += rw;
      }

      if (px + ww > ss_ww) ww = ss_ww - px;                                            //  limit width to image edge
      if (ss_ww - px - ww < 0.5 * (ss_ww / Ncols)) ww = ss_ww - px;                    //  don't leave narrow remnant

      py = ss_hh * row / Nrows;                                                        //  nominal tile top position
      hh = ss_hh / Nrows;                                                              //  nominal tile height
      rh = 1.5 * drandz() * hh;                                                        //  random tile height adder
      if (row == 0) hh += 0.5 * rh;                                                    //  randomize top row tile height
      else {
         py -= 0.5 * rh;                                                               //  randomize tile position and height
         hh += rh;
      }

      if (row == Nrows - 1) hh = ss_hh - py;                                           //  bottom row tiles at image edge

      tile2[nt2].px = px;                                                              //  add to tile table
      tile2[nt2].py = py;
      tile2[nt2].ww = ww;
      tile2[nt2].hh = hh;
      if (drandz() < 0.5) hv = 'h';                                                    //  half the tiles flip horizontally,
      else hv = 'v';                                                                   //     half vertically
      tile2[nt2].hv = hv;
      nt2++;
   }

   PXB         *pxbmix;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3, *p1, *p2, *p3;
   double      T0, Te, Tz;
   int         cc;
   int         px1, py1, px2, py2, px3, py3;
   float       time, shift;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image
   pxbmix = PXB_copy(ss_pxbold);                                                       //  mixed image
   pixels3 = pxbmix->pixels;

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      time = Te / Tz;                                                                  //  0 .. 1

      cc = gdk_pixbuf_get_byte_length(pxbmix->pixbuf);
      memset(pixels3,0,cc);                                                            //  mixed image = black

      if (time <= 0.45)
      {
         shift = 1.0 - 2.22 * time;                                                    //  1.0 .. 0.0

         for (int ii = 0; ii < nt1; ii++)                                              //  loop all tiles
         {
            px = tile1[ii].px;                                                         //  tile position
            py = tile1[ii].py;
            ww = tile1[ii].ww;                                                         //  tile dimensions
            hh = tile1[ii].hh;
            hv = tile1[ii].hv;

            pix1 = pixels1 + py * ss_rs + px * 3;                                      //  tile position in old image - input
            pix2 = pixels2 + py * ss_rs + px * 3;                                      //  tile position in new image - input
            pix3 = pixels3 + py * ss_rs + px * 3;                                      //  tile position in mixed image - output

            if (hv == 'h')                                                             //  horizontal flip
            {
               for (py1 = 0; py1 < hh; py1++)                                          //  loop input tile row 0 .. last
               for (px1 = 0; px1 < ww; px1++)                                          //  loop input tile col 0 .. last
               {
                  py3 = py1;                                                           //  output tile row = input tile row
                  px3 = time * ww + px1 * shift;                                       //  output tile col = input shift, compress
                  p1 = pix1 + py1 * ss_rs + px1 * 3;                                   //  input pixel
                  p3 = pix3 + py3 * ss_rs + px3 * 3;                                   //  output pixel
                  memcpy(p3,p1,3);
               }
            }

            else   /*  hv == 'v'  */                                                   //  vertical flip
            {
               for (py1 = 0; py1 < hh; py1++)                                          //  loop input tile row 0 .. last
               for (px1 = 0; px1 < ww; px1++)                                          //  loop input tile col 0 .. last
               {
                  py3 = time * hh + py1 * shift;                                       //  output tile row = input shift, compress
                  px3 = px1;                                                           //  output tile col = input tile col
                  p1 = pix1 + py1 * ss_rs + px1 * 3;                                   //  input pixel
                  p3 = pix3 + py3 * ss_rs + px3 * 3;                                   //  output pixel
                  memcpy(p3,p1,3);
               }
            }
         }
      }

      else if (time <= 0.55) zmainloop();                                              //  10% black time

      else  /*  time > 0.55  */
      {
         shift = (time - 0.55) * 2.22;                                                 //  0.0 .. 1.0
         if (shift >= 1.0) break;

         for (int ii = 0; ii < nt2; ii++)                                              //  loop all tiles
         {
            px = tile2[ii].px;                                                         //  tile position
            py = tile2[ii].py;
            ww = tile2[ii].ww;                                                         //  tile dimensions
            hh = tile2[ii].hh;
            hv = tile2[ii].hv;

            pix1 = pixels1 + py * ss_rs + px * 3;                                      //  tile position in old image - input
            pix2 = pixels2 + py * ss_rs + px * 3;                                      //  tile position in new image - input
            pix3 = pixels3 + py * ss_rs + px * 3;                                      //  tile position in mixed image - output

            if (hv == 'h')                                                             //  horizontal flip
            {
               for (py2 = 0; py2 < hh; py2++)                                          //  loop input tile row 0 .. last
               for (px2 = 0; px2 < ww; px2++)                                          //  loop input tile col 0 .. last
               {
                  py3 = py2;                                                           //  output tile row = input tile row
                  px3 = (1 - time) * ww + px2 * shift;                                 //  output tile col = input shift, compress
                  p2 = pix2 + py2 * ss_rs + px2 * 3;                                   //  input pixel
                  p3 = pix3 + py3 * ss_rs + px3 * 3;                                   //  output pixel
                  memcpy(p3,p2,3);
               }
            }

            else   /*  hv == 'v'  */                                                   //  vertical flip
            {
               for (py2 = 0; py2 < hh; py2++)                                          //  loop input tile row 0 .. last
               for (px2 = 0; px2 < ww; px2++)                                          //  loop input tile col 0 .. last
               {
                  py3 = (1 - time) * hh + py2 * shift;                                 //  output tile row = input shift, compress
                  px3 = px2;                                                           //  output tile col = input tile col
                  p2 = pix2 + py2 * ss_rs + px2 * 3;                                   //  input pixel
                  p3 = pix3 + py3 * ss_rs + px3 * 3;                                   //  output pixel
                  memcpy(p3,p2,3);
               }
            }
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);                              //  paint mixed image to window
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image moves in as bars from the left and right, interleaved
//  (visually not very appealing)

/***
          ________________________
         |______| -->       ______|
         |______       <-- |______|    new image bars move in from left and right
         |______| -->       ______|    to cover the old image
         |             <-- |______|
         |   etc...               |
         |________________________|

***/

void ss_interleave()
{
   PIXBUF      *pixbuf;
   int         Nbars = 20, bar, barww, barhh;
   int         px, py;
   double      T0, Te, Tz;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)
   {
      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      barww = ss_ww * Te / Tz;                                                         //  bar length, 0 ... ss_ww
      if (barww < 2) barww = 2;
      barhh = ss_hh / Nbars;                                                           //  bar height, fixed

      for (bar = 0; bar < Nbars; bar += 2)                                             //  even bars from left side
      {
         px = ss_ww - barww;
         py = bar * barhh;
         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px,py,barww,barhh);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,0,py);
         cairo_paint(cr);
         g_object_unref(pixbuf);
      }

      for (bar = 1; bar < Nbars; bar += 2)                                             //  odd bars from right side
      {
         px = 0;
         py = bar * barhh;
         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbnew->pixbuf,px,py,barww,barhh);
         gdk_cairo_set_source_pixbuf(cr,pixbuf,ss_ww - barww, py);
         cairo_paint(cr);
         g_object_unref(pixbuf);
      }

      zmainloop();
      if (ss_escape) break;
   }

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  old image explodes away in pieces revealing new image
//  pieces detach randomly and race to the edge

void ss_explode()
{
   #define NT 30                                                                       //  tile rows and cols - even number
   PXB         *pxbmix;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix3;
   double      T0, Te, Tz;
   int         cc, row, col;
   float       posx, posy;
   int         tww = ss_ww / NT, thh = ss_hh / NT;                                     //  tile width, height
   int         offw, offh, lenw, lenh;
   float       rg, cg, F;
   float       rgoal[NT][NT], cgoal[NT][NT];                                           //  tile final position at edge
   float       time;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image
   pxbmix = PXB_copy(ss_pxbold);                                                       //  mixed image
   pixels3 = pxbmix->pixels;

   for (row = 0; row < NT; row++)                                                      //  initialize tile goals
   for (col = 0; col < NT; col++)
   {
      cg = col + 0.5 - NT/2;                                                           //  tile center offset from image center
      rg = row + 0.5 - NT/2;
      F = sqrtf(rg*rg + cg*cg);                                                        //  tile distance from center
      F = 0.8 * NT / F;                                                                //  goal distance from center
      cg = F * cg + NT/2;                                                              //  col goal
      rg = F * rg + NT/2;                                                              //  row goal

      cg *= (1 + drandz() * 0.4);
      rg *= (1 + drandz() * 0.4);

      rgoal[row][col] = rg;                                                            //  tile end positions off image edge
      cgoal[row][col] = cg;
   }

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)                                                                        //  loop transition time 0..Tz
   {
      cc = gdk_pixbuf_get_byte_length(pxbmix->pixbuf);
      memcpy(pixels3,pixels2,cc);                                                      //  mixed image = new image

      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      time = Te / Tz;                                                                  //  relative transition time 0..1
      time = pow(time,1.7);                                                            //  slow .. fast

      for (row = 0; row < NT; row++)                                                   //  loop all tiles
      for (col = 0; col < NT; col++)
      {
         rg = rgoal[row][col];                                                         //  tile end position 0..NT
         cg = cgoal[row][col];

         posx = time * cg + (1-time) * col;                                            //  position at this time 0..NT
         posy = time * rg + (1-time) * row;

         posx = posx * tww;                                                            //  tile x position 0..ss_ww
         posy = posy * thh;                                                            //  tile y position 0..ss_hh

         offw = offh = 0;                                                              //  tile part beyond image edge
         lenw = tww;                                                                   //  tile width to copy
         lenh = thh;                                                                   //  tile height to copy

         if (posx < 0) {                                                               //  tile off left image edge
            offw = -posx;                                                              //  offset
            lenw = lenw - offw;                                                        //  width to copy
         }
         else if (posx + lenw > ss_ww)                                                 //  tile off right image edge
            lenw = ss_ww - posx;                                                       //  width to copy
         if (lenw < 2) continue;

         if (posy < 0) {                                                               //  tile off top image edge
            offh = - posy;                                                             //  offset
            lenh = lenh - offh;                                                        //  height to copy
         }
         else if (posy + lenh > ss_hh)                                                 //  tile off bottom image edge
            lenh = ss_hh - posy;                                                       //  heigh to copy
         if (lenh < 2) continue;

         pix1 = pixels1 + row * thh * ss_rs + col * tww * 3;                           //  tile position in old image
         pix3 = pixels3 + int(posy) * ss_rs + int(posx) * 3;                           //  tile position in mixed image

         pix1 += offh * ss_rs + offw * 3;                                              //  adjust for offsets
         pix3 += offh * ss_rs + offw * 3;

         for (int ii = 0; ii < lenh; ii++) {                                           //  copy tile from old image
            memcpy(pix3, pix1, lenw * 3);                                              //    to mixed image
            pix1 += ss_rs;
            pix3 += ss_rs;
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);                              //  paint mixed image to window
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

//  new image pieces flow inward from the periphery, covering the old image
//  implode code is almost the same as explode, with time going backwards

void ss_implode()
{
   #define NT 30                                                                       //  tile rows and cols - even number
   PXB         *pxbmix;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix2, *pix3;
   double      T0, Te, Tz;
   int         cc, row, col;
   float       posx, posy;
   int         tww = ss_ww / NT, thh = ss_hh / NT;                                     //  tile width, height
   int         offw, offh, lenw, lenh;
   float       rg, cg, F;
   float       rgoal[NT][NT], cgoal[NT][NT];                                           //  tile final position at edge
   float       time;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   pixels1 = ss_pxbold->pixels;                                                        //  old image
   pixels2 = ss_pxbnew->pixels;                                                        //  new image
   pxbmix = PXB_copy(ss_pxbold);                                                       //  mixed image
   pixels3 = pxbmix->pixels;

   for (row = 0; row < NT; row++)                                                      //  initialize tile goals
   for (col = 0; col < NT; col++)
   {
      cg = col + 0.5 - NT/2;                                                           //  tile center offset from image center
      rg = row + 0.5 - NT/2;
      F = sqrtf(rg*rg + cg*cg);                                                        //  tile distance from center
      F = 0.8 * NT / F;                                                                //  goal distance from center
      cg = F * cg + NT/2;                                                              //  col goal
      rg = F * rg + NT/2;                                                              //  row goal

      cg *= (1 + drandz() * 0.4);
      rg *= (1 + drandz() * 0.4);

      rgoal[row][col] = rg;                                                            //  tile end positions off image edge
      cgoal[row][col] = cg;
   }

   T0 = get_seconds();                                                                 //  transition start time
   Tz = ss_trantime;                                                                   //  transition time (goal)

   while (true)                                                                        //  loop transition time 0..Tz
   {
      cc = gdk_pixbuf_get_byte_length(pxbmix->pixbuf);
      memcpy(pixels3,pixels1,cc);                                                      //  mixed image = old image

      Te = get_seconds() - T0;                                                         //  elapsed transition time
      if (Te > Tz) break;                                                              //  done

      Te = Tz -Te;                                                                     //  time goes backwards

      time = Te / Tz;                                                                  //  relative transition time 0..1
      time = pow(time,1.7);                                                            //  slow .. fast

      for (row = 0; row < NT; row++)                                                   //  loop all tiles
      for (col = 0; col < NT; col++)
      {
         rg = rgoal[row][col];                                                         //  tile end position 0..NT
         cg = cgoal[row][col];

         posx = time * cg + (1-time) * col;                                            //  position at this time 0..NT
         posy = time * rg + (1-time) * row;

         posx = posx * tww;                                                            //  tile x position 0..ss_ww
         posy = posy * thh;                                                            //  tile y position 0..ss_hh

         offw = offh = 0;                                                              //  tile part beyond image edge
         lenw = tww;                                                                   //  tile width to copy
         lenh = thh;                                                                   //  tile height to copy

         if (posx < 0) {                                                               //  tile off left image edge
            offw = -posx;                                                              //  offset
            lenw = lenw - offw;                                                        //  width to copy
         }
         else if (posx + lenw > ss_ww)                                                 //  tile off right image edge
            lenw = ss_ww - posx;                                                       //  width to copy
         if (lenw < 2) continue;

         if (posy < 0) {                                                               //  tile off top image edge
            offh = - posy;                                                             //  offset
            lenh = lenh - offh;                                                        //  height to copy
         }
         else if (posy + lenh > ss_hh)                                                 //  tile off bottom image edge
            lenh = ss_hh - posy;                                                       //  heigh to copy
         if (lenh < 2) continue;

         pix2 = pixels2 + row * thh * ss_rs + col * tww * 3;                           //  tile position in new image
         pix3 = pixels3 + int(posy) * ss_rs + int(posx) * 3;                           //  tile position in mixed image

         pix2 += offh * ss_rs + offw * 3;                                              //  adjust for offsets
         pix3 += offh * ss_rs + offw * 3;

         for (int ii = 0; ii < lenh; ii++) {                                           //  copy tile from new image
            memcpy(pix3, pix2, lenw * 3);                                              //    to mixed image
            pix2 += ss_rs;
            pix3 += ss_rs;
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbmix->pixbuf,0,0);                              //  paint mixed image to window
      cairo_paint(cr);

      zmainloop();
      if (ss_escape) break;
   }

   PXB_free(pxbmix);

   gdk_cairo_set_source_pixbuf(cr,ss_pxbnew->pixbuf,0,0);                              //  final image
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   return;
}


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

namespace ss_zoom_names
{
   PXB      *pxb1 = 0, *pxb2 = 0, *pxb3 = 0;
   int      ww1, hh1, ww1a, hh1a, ww2, hh2;
   int      cropw, croph, margw, margh, dm;
   float    R1, R2, zoomnow;
   int      orgx1, orgy1, orgx2, orgy2;
   int      zcx1, zcy1, zcx2, zcy2;
   pthread_t   tid;
}


//  initialize for image zoom out or zoom in
//  prepare 1x image, cropped or with added margins for window width/height ratio

void ss_zoom_setup(ch *file)
{
   using namespace ss_zoom_names;

   if (pxb1) PXB_free(pxb1);

   pxb1 = PXB_load(file,1);                                                            //  load 1x image file
   if (! pxb1) return;

   PXB_subalpha(pxb1);                                                                 //  strip alpha channel if present

   ww1 = pxb1->ww;                                                                     //  image dimensions, 1x
   hh1 = pxb1->hh;

   zcx1 = ww1 * 0.01 * ss_zoomlocx;                                                    //  zoom center location
   zcy1 = hh1 * 0.01 * ss_zoomlocy;

   cropw = croph = 0;                                                                  //  image crop sizes
   margw = margh = 0;                                                                  //  added margin sizes

   R1 = 1.0 * ww1 / hh1;                                                               //  image width/height ratio
   R2 = 1.0 * ss_ww / ss_hh;                                                           //  window width/height ratio

   if (fabsf(R1-R2) / R2 < 0.01 * ss_cliplimit)                                        //  difference < clip limits
   {
      if (R1 > R2) {
         hh1a = hh1;                                                                   //  crop image width
         ww1a = R2 * hh1;
      }
      else {
         ww1a = ww1;                                                                   //  crop image height
         hh1a = ww1 / R2;
      }

      cropw = (ww1-ww1a)/2;                                                            //  width reduction, left and right
      croph = (hh1-hh1a)/2;                                                            //  height reduction, top and bottom

      pxb2 = PXB_subpxb(pxb1,cropw,croph,ww1a,hh1a);                                   //  final 1x image, cropped
      PXB_free(pxb1);                                                                  //    to have ratio ss_ww/ss_hh
      pxb1 = pxb2;
      pxb2 = 0;

      zcx1 -= cropw;                                                                   //  adjust zoom center for crop
      zcy1 -= croph;
   }

   else                                                                                //  difference > clip limits
   {
      if (R1 > R2) {
         ww1a = ww1;                                                                   //  add height margins
         hh1a = ww1 / R2;
      }
      else {
         hh1a = hh1;                                                                   //  add width margins
         ww1a = R2 * hh1;
      }

      margw = (ww1a - ww1) / 2;                                                        //  margin adds, left and right
      margh = (hh1a - hh1) / 2;                                                        //  margin adds, top and bottom

      pxb2 = PXB_make(ww1a,hh1a,3);                                                    //  final 1x image, with margins
      PXB_copy_area(pxb1,0,0,ww1,hh1,pxb2,margw,margh);                                //    to have ratio ss_ww/ss_hh
      PXB_free(pxb1);
      pxb1 = pxb2;
      pxb2 = 0;

      zcx1 += margw;                                                                   //  adjust zoom center for margins
      zcy1 += margh;
   }

   ww1 = ww1a;                                                                         //  new 1x image size
   hh1 = hh1a;                                                                         //  image area to show for zoom = 1x

   ww2 = ww1 / ss_zoomsize;                                                            //  image area to show for zoom = max.
   hh2 = hh1 / ss_zoomsize;

   orgx2 = zcx1 - ww2 / 2;                                                             //  image origin at max. zoom
   orgy2 = zcy1 - hh2 / 2;                                                             //    and zoom center at image center

   if (ww2 <= ww1 - 2 * margw) {                                                       //  adjust to avoid margin overlap
      if (orgx2 < margw) orgx2 = margw;
      if (orgx2 + ww2 > ww1 - margw) orgx2 = ww1 - margw - ww2;
   }
   else {                                                                              //  too big, show even margins
      dm = ww2 - (ww1 - 2 * margw);
      orgx2 = margw - dm / 2;
   }

   if (hh2 <= hh1 - 2 * margh) {                                                       //  adjust to avoid margin overlap
      if (orgy2 < margh) orgy2 = margh;
      if (orgy2 + hh2 > hh1 - margh) orgy2 = hh1 - margh - hh2;
   }
   else {                                                                              //  too big, show even margins
      dm = hh2 - (hh1 - 2 * margh);
      orgy2 = margh - dm / 2;
   }

   zcx1 = orgx2 + ww2 / 2;                                                             //  adjusted zoom center
   zcy1 = orgy2 + hh2 / 2;

   return;
}


//  start thread to perform image resize to target zoom size

void ss_zoom_start(float zoom)
{
   using namespace ss_zoom_names;

   void * ss_zoom_thread(void *);

   zoomnow = zoom;
   tid = start_Jthread(ss_zoom_thread,null);
   return;
}

void * ss_zoom_thread(void *)
{
   using namespace ss_zoom_names;

   float    F;

   ww2 = ww1 / zoomnow;                                                                //  image size to show at zoom size
   hh2 = hh1 / zoomnow;

   F = 0;
   if (ss_zoomsize > 1)                                                                //  F = 0 >> 1
      F = (zoomnow - 1) / (ss_zoomsize - 1);                                           //    for zoom = 1x >> zoomsize

   zcx2 = ww1/2 + F * (zcx1 - ww1/2);                                                  //  image center moves from
   zcy2 = hh1/2 + F * (zcy1 - hh1/2);                                                  //    image midpoint >> zoom center

   orgx2 = zcx2 - ww2/2;                                                               //  compute image area around
   if (orgx2 < 0) orgx2 = 0;                                                           //    image center
   if (orgx2 + ww2 > ww1) orgx2 = ww1 - ww2;

   orgy2 = zcy2 - hh2/2;
   if (orgy2 < 0) orgy2 = 0;
   if (orgy2 + hh2 > hh1) orgy2 = hh1 - hh2;

   pxb2 = PXB_subpxb(pxb1,orgx2,orgy2,ww2,hh2);                                        //  resize to window
   pxb3 = PXB_rescale_fast(pxb2,ss_ww,ss_hh);
   PXB_free(pxb2);
   pxb2 = pxb3;
   pxb3 = 0;

   return 0;
}


//  wait for thread completion and return zoomed image

PXB * ss_zoom_wait()
{
   using namespace ss_zoom_names;

   wait_Jthread(tid);
   return pxb2;
}


//  slowly zoom-in on the image (Ken Burns effect)

void ss_zoomin()
{
   PXB      *Zpxb;
   double   T0, Te, Tz, zoom;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   ss_zoom_setup(ss_newfile);                                                          //  initialize for new image
   ss_zoom_start(1.0);                                                                 //  start making initial zoom image

   T0 = get_seconds();                                                                 //  zoom start time
   Tz = ss_zoomtime;                                                                   //  zoom time (goal)

   while (true)
   {
      Zpxb = ss_zoom_wait();                                                           //  wait for zoom image ready

      Te = get_seconds() - T0;                                                         //  elapsed zoom time
      if (Te > Tz) break;                                                              //  done
      if (ss_escape) break;

      zoom = 1 + (ss_zoomsize - 1) * Te / Tz;                                          //  1.0 ... zoom size
      ss_zoom_start(zoom);                                                             //  start making next zoom image

      gdk_cairo_set_source_pixbuf(cr,Zpxb->pixbuf,0,0);                                //  paint image
      cairo_paint(cr);
      zmainloop();                                                                     //  26.0

      PXB_free(Zpxb);
   }

   PXB_free(Zpxb);                                                                     //  discard last

   ss_zoom_start(ss_zoomsize);                                                         //  last image, full zoom size
   Zpxb = ss_zoom_wait();

   gdk_cairo_set_source_pixbuf(cr,Zpxb->pixbuf,0,0);                                   //  paint
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   PXB_free(ss_pxbnew);                                                                //  retain final zoomed image
   ss_pxbnew = Zpxb;                                                                   //    for next transition
   return;
}


//  slowly zoom-out from initial image center point

void ss_zoomout()
{
   PXB      *Zpxb;
   double   T0, Te, Tz, zoom;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   ss_zoom_setup(ss_newfile);                                                          //  initialize for new image
   ss_zoom_start(ss_zoomsize);                                                         //  start making initial zoom image

   T0 = get_seconds();                                                                 //  zoom start time
   Tz = ss_zoomtime;                                                                   //  zoom time (goal)

   while (true)
   {
      Zpxb = ss_zoom_wait();                                                           //  wait for zoomed image ready

      Te = get_seconds() - T0;                                                         //  elapsed zoom time
      if (Te > Tz) break;                                                              //  done
      if (ss_escape) break;

      zoom = ss_zoomsize + (1 - ss_zoomsize) * Te / Tz;                                //  zoom size ... 1.0
      ss_zoom_start(zoom);                                                             //  start making next zoom image

      gdk_cairo_set_source_pixbuf(cr,Zpxb->pixbuf,0,0);                                //  paint
      cairo_paint(cr);
      zmainloop();                                                                     //  26.0

      PXB_free(Zpxb);
   }

   PXB_free(Zpxb);                                                                     //  discard last

   ss_zoom_start(1.0);                                                                 //  last image, size = 1
   Zpxb = ss_zoom_wait();

   gdk_cairo_set_source_pixbuf(cr,Zpxb->pixbuf,0,0);                                   //  paint
   cairo_paint(cr);
   draw_context_destroy(draw_context);

   PXB_free(ss_pxbnew);                                                                //  retain final zoomed image
   ss_pxbnew = Zpxb;                                                                   //    for next transition
   return;
}
