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

   Fotoxx      edit photos and manage collections

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

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

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

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

   Fotoxx image edit - album and slide show functions

   m_manage_albums               manage albums
      album_create               create a new album
      album_rename               rename an album
      album_delete               delete an album

   album_show                    [TOP] button selection of album to view/edit
   album_purge_replace           purge missing files, replace with later versions

   album thumbnail functions:
      album_addselfiles          add 'selected files' at position
      album_addfile              add single file at position (drag/drop from gallery)
      album_removefile           remove file at position from album
      album_movefile             move album file position (drag/drop within album)

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

   album_batch_rename            mass album file rename (for m_batch_convert)

   m_gallery2album               save current gallery as album

   m_slideshow                   display album with zoom and arty transitions

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

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

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

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

#define ANCC   100                                                               //  max. album name
#define AFCC   300                                                               //  max. album file name (path)
#define maxalbums 200                                                            //  max. total albums
#define maxalbumfiles maxgallery                                                 //  max. files in one album            20.0
#define Balbumtoobig E2X("max. album size exceeded: %d")


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

//  menu function

void m_manage_albums(GtkWidget *, cchar *)                                       //  overhauled                         20.0
{
   int      manage_albums_dialog_event(zdialog *zd, cchar *event);               //  manage albumd dialog event func

   cchar    *helptext1 = E2X("Right-click album thumbnail for edits: \n"
                             "  - add selected files at this position \n"
                             "  - remove this file from the album");
   cchar    *helptext2 = E2X("Arrange files with thumbnail drag and drop");

   F1_help_topic = "manage albums";

   if (FGWM != 'F' && FGWM != 'G') return;
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/***
       __________________________________________________
      |                                                  |
      |               Manage Albums                      |
      |                                                  |
      |  [Create]   Create or replace an album           |
      |  [Rename]   Rename an album                      |
      |  [Delete]   Delete an album                      |
      |  [Select]   Files to add to an album             |
      |                                                  |
      |  Right-click album thumbnail for edits:          |
      |    - add selected files at this position         |
      |    - remove this file from album                 |
      |                                                  |
      |  Arrange files with thumbnail drag and drop      |
      |                                                  |
      |                                          [done]  |
      |__________________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Manage Albums"),Mwin,Bdone,null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"button","create","vb1",Bcreate);
   zdialog_add_widget(zd,"button","rename","vb1",Brename);
   zdialog_add_widget(zd,"button","delete","vb1",Bdelete);
   zdialog_add_widget(zd,"button","select","vb1",Bselect);

   zdialog_add_widget(zd,"hbox","hbcreate","vb2");
   zdialog_add_widget(zd,"label","labcreate","hbcreate",E2X("Create or replace an album"));
   zdialog_add_widget(zd,"hbox","hbrename","vb2");
   zdialog_add_widget(zd,"label","labrename","hbrename",E2X("Rename an album"));
   zdialog_add_widget(zd,"hbox","hbdelete","vb2");
   zdialog_add_widget(zd,"label","labdelete","hbdelete",E2X("Delete an album"));
   zdialog_add_widget(zd,"hbox","hbselect","vb2");
   zdialog_add_widget(zd,"label","labselect","hbselect",E2X("Files to add to an album"));

   zdialog_add_widget(zd,"hbox","hbhelp1","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labhelp1","hbhelp1",helptext1,"space=5");
   zdialog_add_widget(zd,"hbox","hbhelp2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labhelp2","hbhelp2",helptext2,"space=5");

   zdialog_run(zd,manage_albums_dialog_event,"save");                            //  run dialog
   return;
}


//  manage albums dialog event and completion function

int manage_albums_dialog_event(zdialog *zd, cchar *event)
{
   void album_create();
   void album_rename();
   void album_delete();

   char     mess[200], albumname[ANCC], *pp;

   if (zd->zstat)                                                                //  [done] or [x]
   {
      zdialog_free(zd);
      Fblock = 0;
      return 1;
   }

   if (strmatch(event,"create"))                                                 //  start a new album for editing
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog
      album_create();                                                            //  create new album
      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }

   if (strmatch(event,"rename"))                                                 //  rename an album
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog
      album_rename();                                                            //  rename album
      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }

   if (strmatch(event,"delete"))                                                 //  delete an album
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog
      album_delete();                                                            //  delete album
      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }
      
   if (strmatch(event,"select"))                                                 //  select files to add
   {
      *albumname = 0;
      if (curr_album) strncpy0(albumname,curr_album,ANCC);
      zdialog_show(zd,0);                                                        //  hide manage albums dialog
      m_select_files(0,0);                                                       //  creates album "selected files"
      zdialog_show(zd,1);                                                        //  restore manage albums dialog
      if (*albumname) {
         album_show(albumname);                                                  //  restore org. album
         if (navi::Nfiles == 0) {
            album_addselfiles(1);                                                //  if empty album, auto add           20.0
            pp = strrchr(albumname,'/');                                         //     selected files
            if (pp) pp++;
            else pp = albumname;
            snprintf(mess,200,E2X("%d files added to album %s"),GScount,pp);
            poptext_window(MWIN,mess,100,100,0,5);
         }
      }
   }

   return 1;
}


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

//  create a new album

void album_create()
{
   int album_create_dialog_event(zdialog *zd, cchar *event);

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

   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,E2X("max. album count exceeded: %d"),maxalbums);
      return;
   }
   
/***
       _______________________________________________
      |         Create or replace an album            |
      |                                               |
      |  Album Name [____________________] [Browse]   |
      |     (o)  make an initially empty album        |
      |     (o)  fill from pre-selected files         |
      |     (o)  fill from the current gallery        |
      |     (o)  select initial files                 |
      |                                               |
      |                              [done] [cancel]  |
      |_______________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Create or replace an album"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labname","hbname",E2X("Album Name"),"space=3");
   zdialog_add_widget(zd,"zentry","albumname","hbname",0,"space=3|size=20");
   zdialog_add_widget(zd,"button","browse","hbname",Bbrowse,"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",E2X("make an initially empty album"));
   zdialog_add_widget(zd,"radio","selected","vbopt",E2X("fill from pre-selected files"));
   zdialog_add_widget(zd,"radio","gallery","vbopt",E2X("fill from current gallery"));
   zdialog_add_widget(zd,"radio","select","vbopt",E2X("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, cchar *event)
{
   int         Fempty, Fselected, Fgallery, Fselect;
   int         yn, err;
   char        albumname[ANCC], newalbumfile[AFCC];
   char        *cfile, *pp;
   FILE        *fid;
   STATB       statb;

   if (strmatch(event,"browse"))
   {
      cfile = zgetfile(E2X("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 or [x]

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

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

   if (curr_album) zfree(curr_album);                                            //  set current album for editing
   curr_album = zstrdup(newalbumfile);

   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,E2X("Use [Select] to add files to an empty album"));
   
   if (Fselected) 
      album_addselfiles(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_addselfiles(1);
   }

   album_show();
   zmessage_post(Mwin,"20/20",3,E2X("new album created"));
   return 1;
}


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

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

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

   if (navi::Nfiles > maxalbumfiles) {
      zmessageACK(Mwin,Balbumtoobig,maxalbumfiles); 
      return 1;
   }

   fid = fopen(newalbumfile,"w");                                                //  open/write album file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }
   
   for (Nth = 0; Nth < navi::Nfiles; Nth++)                                      //  add gallery images to album file
   {
      pp = gallery(0,"get",Nth);
      if (! pp) break;
      ftype = image_file_type(pp);                                               //  must be image type file
      if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) {
         zfree(pp);
         continue;
      }
      fprintf(fid,"%s\n",pp);
      zfree(pp);
   }

   fclose(fid);
   return 0;
}


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

//  rename an album

void album_rename()
{
   cchar       *renalbum = E2X("Rename an album");
   char        *cfile, *dfile, *cpp, *dpp;
   int         err;
   STATB       statb;

   cfile = zgetfile(renalbum,MWIN,"file",albums_folder);                         //  choose album file
   if (! cfile) return;
   
   cpp = strrchr(cfile,'/');
   if (! cpp) {                                                                  //  should not happen
      printz("invalid file: %s \n",cfile);
      zfree(cfile);
      return;
   }

   dpp = zdialog_text(Mwin,E2X("enter new album name"),cpp+1);
   if (! dpp) {
      zfree(cfile);
      return;
   }

   if (strchr(dpp,'/')) {
      zmessageACK(Mwin,E2X("invalid file name: %s"),dpp);
      zfree(cfile);
      zfree(dpp);
      return;
   }

   if (curr_album && strmatch(cfile,curr_album)) {                               //  current album will be renamed
      zfree(navi::galleryname);
      navi::galleryname = 0;
      navi::gallerytype = TNONE;
      m_viewmode(0,"F");
      zfree(curr_album);
      curr_album = 0;
   }
   
   dfile = zstrdup(cfile,strlen(dpp));                                           //  construct pathname
   cpp = strrchr(dfile,'/');
   strcpy(cpp+1,dpp);

   err = stat(dfile,&statb);
   if (! err) {
      zmessageACK(Mwin,E2X("album already exists: %s"),dpp);
      zfree(cfile);
      zfree(dfile);
      zfree(dpp);
      return;
   }

   err = rename(cfile,dfile);
   if (err) zmessageACK(Mwin,strerror(errno));

   cpp = strrchr(cfile,'/');
   zmessageACK(Mwin,E2X("%s \n renamed: %s"),cpp+1,dpp);

   zfree(cfile);
   zfree(dfile);
   zfree(dpp);

   return;
}


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

//  delete an album

void album_delete()
{
   cchar    *delalbum = E2X("Delete an album");
   char     *cfile, *cpp;
   
   cfile = zgetfile(delalbum,MWIN,"file",albums_folder);                         //  choose album file
   if (! cfile) return;
   
   cpp = strrchr(cfile,'/');
   if (! cpp) {                                                                  //  should not happen
      printz("invalid file: %s \n",cfile);
      zfree(cfile);
      return;
   }

   int yn = zmessageYN(Mwin,E2X("delete %s ?"),cpp+1);
   if (! yn) {
      zfree(cfile);
      return;
   }

   if (curr_album && strmatch(cfile,curr_album)) {                               //  current album will be deleted
      zfree(navi::galleryname);
      navi::galleryname = 0;
      navi::gallerytype = TNONE;
      m_viewmode(0,"F");
      zfree(curr_album);
      curr_album = 0;
   }
   
   remove(cfile);
   zfree(cfile);
   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(cchar *albumfile) 
{
   if (albumfile && albumfile != curr_album) {
      if (curr_album) zfree(curr_album);
      curr_album = zstrdup(albumfile);
   }
   
   if (! curr_album) return;

   album_purge_replace(curr_album);                                              //  purge/replace missing, notify

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

   if (navi::Nfiles > maxalbumfiles)
      zmessageACK(Mwin,Balbumtoobig,maxalbumfiles); 

   return;
}


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

//  Purge missing files from an album, replace with later versions if available.
//  Show popup list of files deleted or replaced.

void album_purge_replace(cchar *albumfile)
{
   FTYPE       ftype;
   char        albumbuff[XFCC];
   cchar       *albumname;
   int         ii, Nmissing = 0, Nfiles = 0;
   int         jj, jj1, jj2;
   char        *pp, *pp2, *fnewest, poptitle[100];
   char        *files[maxalbumfiles];
   zdialog     *zd;
   FILE        *fidr;

   fidr = fopen(albumfile,"r");                                                  //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   while (true)                                                                  //  read album member files
   {
      pp = fgets_trim(albumbuff,XFCC,fidr);
      if (! pp) break;
      ftype = image_file_type(pp);                                               //  count missing files
      if (ftype == FNF) Nmissing++;
      files[Nfiles] = zstrdup(pp);                                               //  save all files
      Nfiles++;
      if (Nfiles == maxalbumfiles) break;
   }

   fclose(fidr);

   if (Nmissing == 0) goto cleanup;                                              //  no missing files

   albumname = strrchr(albumfile,'/');
   if (albumname) albumname++;
   snprintf(poptitle,100,"album %s - missing files",albumname);
   zd = popup_report_open(poptitle,Mwin,600,300,0,0,0);

   linedit_open(albumfile);                                                      //  open album file for editing
   
   for (ii = 0; ii < Nfiles; ii++)                                               //  get all files
   {
      pp = linedit_get();
      if (! pp) break;

      ftype = image_file_type(pp);

      if (ftype == FNF)                                                          //  remove deleted files
      {
         fnewest = file_newest_version(pp);
         if (! fnewest) {                                                        //  no newer version found
            popup_report_write(zd,0,E2X("%s removed with no replacement \n"),pp);
            continue;
         }
         
         jj1 = ii-5;                                                             //  album neighborhood of deleted file
         jj2 = ii+5;
         if (jj1 < 0) jj1 = 0;
         if (jj2 > Nfiles) jj2 = Nfiles;

         for (jj = jj1; jj < jj2; jj++)                                          //  newer version already in neighborhood?
            if (strmatch(fnewest,files[jj])) break;

         if (jj < jj2) {                                                         //  yes, remove file
            popup_report_write(zd,0,E2X("%s removed with no replacement \n"),pp);
            continue;
         }

         pp2 = strrchr(fnewest,'/');
         popup_report_write(zd,0,E2X("%s replaced by ...%s \n"),pp,pp2);         //  replace with newest version
         linedit_put(fnewest);
         zfree(fnewest);
         continue;
      }
      
      else linedit_put(pp);
   }
   
   linedit_close();

cleanup:

   for (int ii = 0; ii < Nfiles; ii++)
      zfree(files[ii]);

   return;
}


//  Purge missing files from ALL albums, replace with later versions if available.
//  Called by m_batch_deltrash(). 

void album_purge_replace_all()
{
   char     **flist, *pp;
   int      ii, NF;
   char     findcomm[200];

   snprintf(findcomm,200,"%s/*",albums_folder);                                  //  find all album files               20.0
   zfind(findcomm,flist,NF);
   
   for (ii = 0; ii < NF; ii++)
   {
      pp = strrchr(flist[ii],'/');
      if (! pp) continue;
      pp++;
      if (*pp == '.') continue;
      album_purge_replace(flist[ii]);
   }
   
   for (ii = 0; ii < NF; ii++)
      zfree(flist[ii]);
   zfree(flist);

   return;
}


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

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

void album_addselfiles(int mode)
{
   char     selfiles[AFCC];
   char     albumbuff[XFCC];
   FILE     *fid;
   int      ii, jj, posn;
   char     *pp;

   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(selfiles,AFCC,"%s/%s",albums_folder,"selected files");
   fid = fopen(selfiles,"r");
   if (! fid) {
      zmessageACK(Mwin,E2X("no selected files"));
      return;
   }
   
   linedit_open(curr_album);

   for (ii = jj = 0; ; ii++)                                                     //  loop album member files
   {
      if (ii == posn) {
         while ((pp = fgets_trim(albumbuff,XFCC,fid))) {                         //  insert selected files
            if (++jj == maxalbumfiles) break;
            linedit_put(pp);
         }
      }
      pp = linedit_get();                                                        //  copy rest
      if (! pp) break;                                                           //  EOF
      if (++jj == maxalbumfiles) break;
      linedit_put(pp);
   }
   
   fclose(fid);
   linedit_close();
   album_show();                                                                 //  (will alarm maxalbumfiles)
   return;
}


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

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

void album_addcurrfile(int mode)                                                 //  20.0
{
   int      ii, jj, posn;
   char     *pp;

   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;

   linedit_open(curr_album);                                                     //  open album file

   for (ii = jj = 0; ; ii++)                                                     //  loop album member files
   {
      if (ii == posn) linedit_put(curr_file);                                    //  insert curr_file at position
      pp = linedit_get();                                                        //  copy rest
      if (! pp) break;                                                           //  EOF
      if (++jj == maxalbumfiles) break;
      linedit_put(pp);
   }
   
   linedit_close();
   album_show();                                                                 //  (will alarm maxalbumfiles)
   return;
}


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

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

void album_addfile(cchar *file, int posn)
{
   int      ii, jj;
   char     *pp;
   
   if (! curr_album) return;

   linedit_open(curr_album);

   for (ii = jj = 0; ; ii++)                                                     //  loop album member files
   {
      if (ii == posn) { 
         if (++jj == maxalbumfiles) break;
         linedit_put(file);                                                      //  insert current file here
      }
      pp = linedit_get();                                                        //  copy album member file
      if (! pp) break;                                                           //  EOF
      if (++jj == maxalbumfiles) break;
      linedit_put(pp);
   }
   
   linedit_close();
   album_show();
   return;
}


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

//  remove file at position from album

void album_removefile(int posn)
{
   int      ii;
   char     *pp;

   if (! curr_album) return;
   
   linedit_open(curr_album);

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      pp = linedit_get();
      if (! pp) break;
      if (ii == posn) continue;                                                  //  position to remove
      linedit_put(pp);
   }

   linedit_close();
   album_show();
   return;
}


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

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

void album_movefile(int pos1, int pos2)
{
   char     *filez;
   int      ii;
   char     *pp;
   
   if (! curr_album) return;
   if (pos1 == pos2) return;

   filez = gallery(0,"get",pos1);                                                //  file to move
   if (! filez) return;
   
   linedit_open(curr_album);

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      pp = linedit_get();                                                        //  album member file
      if (! pp) break;

      if (ii == pos1) continue;                                                  //  skip file position to move from

      if (ii == pos2) {                                                          //  file position to move to
         linedit_put(filez);                                                     //  insert the moved file here
         pos2 = -1;
      }

      linedit_put(pp);                                                           //  copy to output file
   }

   if (pos2 >= 0) linedit_put(filez);                                            //  add at the end

   linedit_close();
   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
{
   char     *albumfiles[maxalbums];
   char     oldfile[XFCC], newfile[XFCC];                                        //  albums and replace option kept
   int      Nalbums = 0;                                                         //    to initialize next run
   int      Freplace = 1;
   char     countmess[40];
   cchar    *countformat = "%d albums chosen";
};


//  menu function

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

   int update_albums_dialog_event(zdialog *zd, cchar *event);
   
   zdialog  *zd;
   STATB    statb;
   char     *pp;
   int      ii, jj, zstat;
   
   F1_help_topic = "update albums";

   if (FGWM != 'F' && FGWM != 'G') return;
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;
   
/***
       __________________________________________________
      |           Replace Album File                     |
      |                                                  |
      |  [Choose Albums] N albums chosen                 |
      |                                                  |
      |  old file [____________________________________] |
      |  new file [____________________________________] |
      |                                                  |
      |  (o) replace old  (o) add after old              |
      |                                                  |
      |                       [Clear] [Proceed] [Cancel] |
      |__________________________________________________|

***/

   zd = zdialog_new(E2X("Replace Album File"),Mwin,Bclear,Bproceed,Bcancel,0);
   zdialog_add_widget(zd,"hbox","hbchoose","dialog",0,"space=5");   
   zdialog_add_widget(zd,"button","choose","hbchoose",E2X("Choose Albums"),"space=5");
   zdialog_add_widget(zd,"label","labcount","hbchoose","0 albums chosen");
   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbold","dialog");
   zdialog_add_widget(zd,"label","labold","hbold",E2X("old file"),"space=5");
   zdialog_add_widget(zd,"zentry","oldfile","hbold",0,"expand");
   zdialog_add_widget(zd,"hbox","hbnew","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labnew","hbnew",E2X("new file"),"space=5");
   zdialog_add_widget(zd,"zentry","newfile","hbnew",0,"expand");
   zdialog_add_widget(zd,"hbox","hbrep","dialog",0,"space=10");
   zdialog_add_widget(zd,"radio","replace","hbrep",E2X("replace old"),"space=5");
   zdialog_add_widget(zd,"radio","addafter","hbrep",E2X("add after old"),"space=10");

   snprintf(countmess,40,countformat,Nalbums);
   zdialog_stuff(zd,"labcount",countmess);

   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);
   
   m_viewmode(0,"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] or [x]
      goto returnx;
   }
   
   zdialog_fetch(zd,"oldfile",oldfile,XFCC);                                     //  [proceed]
   zdialog_fetch(zd,"newfile",newfile,XFCC);
   zdialog_fetch(zd,"replace",Freplace);
   zdialog_free(zd);
   
   if (stat(oldfile,&statb) || ! S_ISREG(statb.st_mode)) {
      zmessageACK(Mwin,Bfilenotfound2,oldfile);
      goto returnx;
   }
   
   if (stat(newfile,&statb) || ! S_ISREG(statb.st_mode)) {
      zmessageACK(Mwin,Bfilenotfound2,newfile);
      goto returnx;
   }

   if (! Nalbums) {
      zmessageACK(Mwin,E2X("no albums chosen"));
      goto returnx;
   }
   
   for (ii = jj = 0; ii < Nalbums; ii++)
   {
      pp = strrchr(albumfiles[ii],'/');                                          //  album: album-name
      if (pp) pp++;
      else pp = albumfiles[ii];

      linedit_open(albumfiles[ii]);

      while ((pp = linedit_get()))                                               //  read all album recs = image files
      {
         if (strmatch(pp,oldfile))                                               //  found old file
         {
            if (Freplace) {
               if (++jj == maxalbumfiles) break;
               linedit_put(newfile);                                             //  replace: write new file only
            }
            else {
               if (++jj == maxalbumfiles) break;
               linedit_put(oldfile);                                             //  add after: write both files
               if (++jj == maxalbumfiles) break;
               linedit_put(newfile);
            }
         }
         else {
            if (++jj == maxalbumfiles) break;
            linedit_put(pp);
         }
      }
      
      linedit_close();

      album_show(albumfiles[ii]);                                                //  show updated album
   }
   
   m_update_albums(0,0);                                                         //  repeat until canceled

returnx:
   Fblock = 0;
   return;
}


//  dialog event and completion callback function

int update_albums_dialog_event(zdialog *zd, cchar *event)
{
   using namespace update_albums_names;
   
   char     **pp;
   int      ii;
   
   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,"choose"))                                                 //  [choose] button
   {
      for (ii = 0; ii < Nalbums; ii++)                                           //  free prior album names, if any
         zfree(albumfiles[ii]);
      Nalbums = 0;

      pp = zgetfiles(E2X("Choose Albums"),MWIN,"files",albums_folder);           //  choose album files
      if (pp) 
      {
         for (ii = 0; ii < 1000 && pp[ii]; ii++) 
            albumfiles[ii] = pp[ii];
         Nalbums = ii;                                                           //  album count selected
         zfree(pp);
      }
      
      snprintf(countmess,40,countformat,Nalbums);                                //  update dialog album count
      zdialog_stuff(zd,"labcount",countmess);
   }
   
   return 1;
}


//  mouse click function

void update_albums_Lclick_func(int Nth)
{
   using namespace update_albums_names;

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

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

   imagefile = gallery(0,"get",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) {
      zfree(imagefile);
      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);
   }

   zfree(imagefile);
   return;
}


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

//  mass update of album files

namespace album_mass_update_names
{
   char     massoption = 0;
   char     *albumfiles[maxalbums];                                              //  albums to process
   char     *oldfiles[maxalbumfiles];                                            //  initial album files
   char     *newfiles[maxalbumfiles];                                            //  album files after processing
   char     albumbuff[XFCC]; 
   int      Nalbums, Nold, Nnew;                                                 //  corresp. counts
}


//  menu function

void m_album_mass_update(GtkWidget *, cchar *)
{
   using namespace album_mass_update_names;
   
   int   album_mass_update_dialog_event(zdialog *zd, cchar *event);
   void  album_mass_update_process();

   int      ii, jj;
   char     *pp, *albumname;
   FILE     *fid;
   
   F1_help_topic = "album mass update";
   
   if (FGWM != 'F' && FGWM != 'G') return;
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/***
          ______________________________________________________
         |             Album Mass Update                        |
         |                                                      |
         |  [select] N albums selected                          |
         |                                                      |
         |  Process all 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    |                //  20.0
         |                                                      |
         |  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] [cancel] |
         |______________________________________________________|

***/

   massoption = 0;
   Nalbums = 0;

   zdialog *zd = zdialog_new(E2X("Album Mass Update"),Mwin,Bproceed,Bcancel,0);

   zdialog_add_widget(zd,"hbox","hbsel","dialog");   
   zdialog_add_widget(zd,"button","select","hbsel",Bselect,"space=3");
   zdialog_add_widget(zd,"label","labcount","hbsel","0 albums selected","space=10");

   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hball","dialog");
   zdialog_add_widget(zd,"label","laball","hball",E2X("Process all 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",E2X("Replace all with newest version only"),"space=12");
   zdialog_add_widget(zd,"hbox","hbB","vb1");
   zdialog_add_widget(zd,"check","optB","hbB",E2X("Replace all versions with newest version"),"space=12");
   zdialog_add_widget(zd,"hbox","hbC","vb1");
   zdialog_add_widget(zd,"check","optC","hbC",E2X("Add newest version to existing versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbD","vb1");
   zdialog_add_widget(zd,"check","optD","hbD",E2X("Replace all with original and all versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbE","vb1");
   zdialog_add_widget(zd,"check","optE","hbE",E2X("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",E2X("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",E2X("Replace all with selected versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbN","vb2");
   zdialog_add_widget(zd,"check","optN","hbN",E2X("Replace all versions with selected versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbO","vb2");
   zdialog_add_widget(zd,"check","optO","hbO",E2X("Add selected versions to existing versions"),"space=12");
   zdialog_add_widget(zd,"hbox","hbP","vb2");
   zdialog_add_widget(zd,"check","optP","hbP",E2X("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 (massoption == 0) goto returnx;                                            //  canceled
   
   for (ii = 0; ii < Nalbums; ii++)
   {
      pp = strrchr(albumfiles[ii],'/');                                          //  album file
      if (pp) pp++;
      else pp = albumfiles[ii];                                                  //  album name
      albumname = pp;

      fid = fopen(albumfiles[ii],"r");                                           //  open/read album file
      if (! fid) {
         zmessageACK(Mwin,"%s %s",albumname,strerror(errno));
         continue;
      }
      
      for (jj = 0; jj < maxalbumfiles; jj++)                                     //  read all contained image files
      {
         pp = fgets_trim(albumbuff,XFCC,fid); 
         if (! pp) break;
         oldfiles[jj] = zstrdup(pp);                                             //  save list of files
      }
      
      fclose(fid);
      
      if (jj == maxalbumfiles) {
         zmessageACK(Mwin,Balbumtoobig,maxalbumfiles); 
         continue;
      }
      
      Nold = jj;                                                                 //  initial file count
      
      album_mass_update_process();                                               //  oldfiles[Nold] --> newfiles[Nnew]
      
      fid = fopen(albumfiles[ii],"w");                                           //  open/write album file
      if (! fid) {
         zmessageACK(Mwin,"%s %s",albumname,strerror(errno));
         continue;
      }
      
      for (jj = 0; jj < Nnew; jj++)                                              //  write all contained image files
         fprintf(fid,"%s\n",newfiles[jj]); 
      
      fclose(fid);   

      for (jj = 0; jj < Nold; jj++)                                              //  free memory
         zfree(oldfiles[jj]);

      for (jj = 0; jj < Nnew; jj++)
         zfree(newfiles[jj]);
   }
   
   album_show(albumfiles[0]);                                                    //  show first album processed
   
   for (ii = 0; ii < Nalbums; ii++)                                              //  free memory
      zfree(albumfiles[ii]);

returnx:
   Fblock = 0;
   return;
}


//  dialog event and completion function

int album_mass_update_dialog_event(zdialog *zd, cchar *event)
{
   using namespace album_mass_update_names;
   
   int      ii;
   char     **pp;
   char     countmess[40];

   if (zd->zstat)
   {
      if (zd->zstat != 1) massoption = 0;
      zdialog_destroy(zd);
      return 1;                                                                  //  19.0
   }
      
   if strmatch(event,"select")
   {
      for (ii = 0; ii < Nalbums; ii++)
         zfree(albumfiles[ii]);

      pp = zgetfiles(E2X("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(countmess,40,"%d albums selected",Nalbums);                       //  update dialog album count
      zdialog_stuff(zd,"labcount",countmess);
   }

   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 album files in oldfiles[Nold]
//  output list of new album files in newfiles[Nnew]

void album_mass_update_process()
{
   using namespace album_mass_update_names;
   
   int      ii, ii1, ii2, jj, kk, ll, nf, nc;
   int      Nselect = 0;
   FILE     *fid;
   char     selcomm[AFCC], albumbuff[XFCC];
   char     *rootname, *basename, *pp;
   char     *selectfiles[maxalbumfiles]; 
   char     **fileversions, *ppselect[100];

   if (massoption >= 'M')                                                        //  if 'selected files' option
   {
      snprintf(selcomm,AFCC,"%s/%s",albums_folder,"selected files");             //  get selected files in memory       20.0
      fid = fopen(selcomm,"r");
      if (fid) {
         while ((pp = fgets_trim(albumbuff,XFCC,fid))) {
            ii = Nselect++;
            selectfiles[ii] = zstrdup(pp);
            if (Nselect == maxalbumfiles) break;
         }
         fclose(fid);
         if (Nselect == maxalbumfiles) {
            zmessageACK(Mwin,E2X("too many selected files: %d"),Nselect);
            for (ii = 0; ii < Nselect; ii++)
               zfree(selectfiles[ii]);
            return;
         }
      }
   }

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

   while (ii < Nold)                                                             //  loop all old files
   {
      if (kk > maxalbumfiles-100) {
         zmessageACK(Mwin,Balbumtoobig,maxalbumfiles);                           //  stop with sufficient margin 
         break;
      }
   
      rootname = file_rootname(oldfiles[ii]);                                    //  get root name for group /.../filename
      if (! rootname) continue;                                                  //  no files found for group
      basename = file_basename(oldfiles[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(oldfiles[ii2]))) break;
      ii = ii2--;                                                                //  leave ii at start of next group

      if (massoption == 'A')                                                     //  Replace all files with newest version only
      {
         pp = file_newest_version(oldfiles[ii1]);
         if (pp) newfiles[kk++] = zstrdup(pp);                                   //  add newest version only to newfiles
         continue;
      }

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

         pp = file_newest_version(oldfiles[ii1]);                                //  newest version
         if (! pp) continue;                                                     //  none
         if (basename && strmatch(pp,basename)) continue;                        //  if same as basename, skip
         newfiles[kk++] = zstrdup(pp);                                           //  add to newfiles
         continue;
      }

      if (massoption == 'C')                                                     //  Add newest version to existing versions
      {
         for (jj = ii1; jj <= ii2; jj++)                                         //  add oldfiles to newfiles
            newfiles[kk++] = zstrdup(oldfiles[jj]);

         pp = file_newest_version(oldfiles[ii1]);                                //  newest version
         if (! pp) continue;                                                     //  none
         for (jj = ii1; jj <= ii2; jj++)                                         //  already included in oldfiles?
            if (strmatch(pp,oldfiles[jj])) break;
         if (jj > ii2) newfiles[kk++] = zstrdup(pp);                             //  no, add to newfiles
         continue;
      }

      if (massoption == 'D')                                                     //  Replace all with original + all versions
      {
         fileversions = file_all_versions(oldfiles[ii1],nf);                     //  get basename and all versions that exist
         if (! nf) continue;
         for (jj = 0; jj < nf; jj++)                                             //  add all file versions to newfiles
            newfiles[kk++] = fileversions[jj];
         zfree(fileversions);                                                    //  free memory
         continue;
      }
      
      if (massoption == 'E')                                                     //  Replace all with original + newest version
      {                                                                          //                                     20.0
         if (basename) newfiles[kk++] = zstrdup(basename);                       //  add basename to newfiles
         pp = file_newest_version(oldfiles[ii1]);
         if (pp && basename && strmatch(pp,basename)) continue;                  //  add newest version to newfiles
         newfiles[kk++] = zstrdup(pp);                                           //    if not same as basename
         continue;
      }
      
      if (massoption < 'M') continue;                                            //  not a selected files option, stop here
      
      for (nc = ll = 0; ll < Nselect && nc < 100; ll++)                          //  find matching select files
         if (strmatch(rootname,file_rootname(selectfiles[ll])))
            ppselect[nc++] = selectfiles[ll];

      if (nc == 0) {
         for (jj = ii1; jj <= ii2; jj++)                                         //  none, add all oldfiles to newfiles
            newfiles[kk++] = zstrdup(oldfiles[jj]);
         continue;
      }
      
      if (massoption == 'M')                                                     //  Replace all with selected versions
      {
         for (ll = 0; ll < nc; ll++)                                             //  loop selected files
            newfiles[kk++] = zstrdup(ppselect[ll]);                              //  add selected files to newfiles
         continue;
      }

      if (massoption == 'N')                                                     //  Replace all versions with selected versions
      {
         if (basename) {
            for (jj = ii1; jj <= ii2; jj++)                                      //  oldfiles includes basename?
               if (strmatch(basename,oldfiles[jj])) break;
            if (jj <= ii2) newfiles[kk++] = zstrdup(basename);                   //  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
               newfiles[kk++] = zstrdup(ppselect[ll]);                           //    add selected file to newfiles
         }
         continue;
      }

      if (massoption == 'O')                                                     //  Add selected versions to existing versions
      {
         for (jj = ii1; jj <= ii2; jj++)                                         //  add all oldfiles to newfiles
            newfiles[kk++] = zstrdup(oldfiles[jj]);

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

      if (massoption == 'P')                                                     //  Replace all with original + selected vers.
      {
         if (basename) newfiles[kk++] = zstrdup(basename);                       //  add basename to newfiles

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

   for (ii = 0; ii < Nselect; ii++)
      zfree(selectfiles[ii]);

   return;
}


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

//  Fix albums when image files have been renamed or moved.
//  inputs: a list of old filenames and corresponding new filenames.
//  used by m_batch_convert().

void album_batch_rename(char **oldfiles, char **newfiles, int nfiles) 
{
   char        **flist, *pp;
   char        *albumfiles[maxalbums];
   char        findcomm[200];
   int         ii, jj, err;
   int         NF;

   snprintf(findcomm,200,"%s/*",albums_folder);                                  //  find all album files
   zfind(findcomm,flist,NF);
   
   for (ii = jj = 0; ii < NF && jj < maxalbums; ii++)
   {
      pp = strrchr(flist[ii],'/');
      if (! pp) continue;
      pp++;
      if (*pp == '.') continue;
      albumfiles[jj] = zstrdup(flist[ii]);
      jj++;
   }

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

   if (! NF) return;
   if (NF == maxalbums) 
      zmessageACK(Mwin,"%d albums limit reached",maxalbums);

   for (ii = 0; ii < NF; ii++)                                                   //  loop all albums
   {
      linedit_open(albumfiles[ii]);

      while ((pp = linedit_get()))                                               //  read all album recs = image files
      {
         for (jj = 0; jj < nfiles; jj++)                                         //  list of renamed/moved files
            if (strmatch(pp,oldfiles[jj])) break;                                //    includes this file?

         if (jj == nfiles) linedit_put(pp);                                      //  no, copy old filespec
         else linedit_put(newfiles[jj]);                                         //  yes, copy corresp. new filespec
      }

      err = linedit_close();
      if (err) goto error;
   }

   goto cleanup;

error:
   zmessageACK(Mwin,"%s \n %s",albumfiles[ii],strerror(errno));

cleanup:
   for (ii = 0; ii < NF; ii++)
      zfree(albumfiles[ii]);
   return;
}


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

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

void m_gallery2album(GtkWidget *, cchar *)                                       //  20.0
{
   char     **flist, *pp;
   int      yn, ii, NF, zstat, err;
   char     findcomm[200], albumname[100], albumfile[200];
   STATB    statb;
   
/***
       ________________________________
      |    Save Gallery as Album       |
      |                                |
      |  Album Name [______________|v| |
      |                                |
      |                         [Done] |
      |________________________________|

***/

   F1_help_topic = "gallery to album";

   zdialog *zd = zdialog_new(E2X("Save Gallery as Album"),Mwin,Bdone,null);
   zdialog_add_widget(zd,"hbox","hbname","dialog");
   zdialog_add_widget(zd,"label","labname","hbname",E2X("Album Name"),"space=5");
   zdialog_add_widget(zd,"comboE","albumname","hbname",0,"space=5");    
   
   snprintf(findcomm,200,"%s/*",albums_folder);                                  //  find all album files               20.0
   zfind(findcomm,flist,NF);
   
   for (ii = 0; ii < NF && ii < 100; ii++)                                       //  add to combo box
   {
      pp = strrchr(flist[ii],'/');
      if (! pp) continue;
      pp++;
      if (*pp == '.') continue;
      zdialog_cb_app(zd,"albumname",pp);
   }
   
   for (int ii = 0; ii < NF; ii++)
      zfree(flist[ii]);
   zfree(flist);
   
   zdialog_stuff(zd,"albumname","gallery");                                      //  default name "gallery"             20.0

   zdialog_run(zd,0,"save");                                                     //  run dialog
   zstat = zdialog_wait(zd);
   zdialog_fetch(zd,"albumname",albumname,100);                                  //  get album name 
   zdialog_free(zd);
   if (zstat != 1) return;                                                       //  user cancel
   if (*albumname <= ' ') return;

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

   err = stat(albumfile,&statb);
   if (! err) {                                                                  //  already exists
      yn = zmessageYN(Mwin,E2X("replace album %s ?"),albumname);
      if (! yn) return;
   }

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

   return;
}


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

//  slide show function

int   ss_dialog_event(zdialog *zd, cchar *event);                                //  main dialog
void  ss_KBprefs_dialog();                                                       //  edit KB control key preferences
void  ss_transprefs_dialog();                                                    //  edit transition preferences
void  ss_imageprefs_dialog();                                                    //  edit image preferences
void  ss_loadprefs();                                                            //  load preferences from file
void  ss_saveprefs();                                                            //  write preferences to file
int   ss_timerfunc(void *);                                                      //  timer function
int   ss_nextrans();                                                             //  select next transition to use
void  ss_blankwindow();                                                          //  blank the window
int   ss_showcapcom(int, int, int);                                              //  show filename/captions/comments    20.0

PXB  *ss_loadpxb(char *file);                                                    //  load image as PXB pixmap
PXB  *ss_zoom_posn(char *file, int mode, float zoom);                            //  position zoomed image in PXB

void  ss_instant();                                                              //  transition functions
void  ss_fadein();
void  ss_rollright();
void  ss_rolldown();
void  ss_venetian();
void  ss_grate();
void  ss_rectangle();
void  ss_implode();
void  ss_explode();
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_turncube();
void  ss_windmill();
void  ss_pixelize();
void  ss_twist();
void  ss_Xopen();
void  ss_squish();
void  ss_disintegrate();
void  ss_interleave();
void  ss_zoom_setup(char *file);
void  ss_zoom_start(float zoom);
PXB * ss_zoom_wait();
void  ss_zoomin();
void  ss_zoomout();

char     *ss_albumfile = 0;                                                      //  slide show album file
char     *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_cctime = 0;                                                          //  filename/caption/comments show time
int      ss_cliplimit = 10;                                                      //  image clipping limit from user
char     ss_musicfile[500] = "none";                                             //  /folder.../musicfile.ogg
int      ss_random = 0;                                                          //  use random transitions option from user
int      ss_fullscreen = 0;                                                      //  flag, full screen mode
int      ss_replay = 0;                                                          //  flag, start over when done
float    ss_trantime = 2.0;                                                      //  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
char     *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
cchar    *ss_event;                                                              //  slide show event 
int      ss_paused;                                                              //  slide show paused status
int      ss_Fcurrent = 0;                                                        //  current image file
int      ss_isblank = 0;                                                         //  window is blank
int      ss_nwt;                                                                 //  threads for transition funcs
char     ss_KBkeyB;                                                              //  KB key: blank screen
char     ss_KBkeyN;                                                              //  KB key: transition to next image
char     ss_KBkeyP;                                                              //  KB key: pause /resume
char     ss_KBkeyX;                                                              //  KB key: magnify image

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

struct ss_trantab_t {                                                            //  transition table
   char     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
};

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
      {  "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       },
      {  "implode",       1,     2.0,    10,   ss_implode         },
      {  "explode",       1,     2.0,    10,   ss_explode         },
      {  "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      },
      {  "turn-cube",     1,     2.0,    10,   ss_turncube        },
      {  "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",        1,     2.0,    10,   ss_squish          },
      {  "disintegrate",  1,     2.0,    10,   ss_disintegrate    },
      {  "interleave",    1,     2.0,    10,   ss_interleave      }
};

struct ss_imagetab_t   {                                                         //  image table
   char     *imagefile;                                                          //  image file
   int      tone;                                                                //  flag, play tone when shown
   int      wait0;                                                               //  seconds to wait
   int      filesecs;                                                            //  seconds to show file name          20.0
   int      capsecs;                                                             //  seconds to show caption
   int      commsecs;                                                            //  seconds to show comments
   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
   char     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 *, cchar *)                                           //  overhauled
{
   zdialog     *zd;
   int         zstat, ii;
   char        *pp;

   E2X("instant");                                                               //  add translations to .po file
   E2X("fade-in");
   E2X("roll-right");
   E2X("roll-down");
   E2X("venetian");
   E2X("grate");
   E2X("rectangle");
   E2X("implode");
   E2X("explode");
   E2X("radar");
   E2X("Japan-fan");
   E2X("spiral");
   E2X("ellipse");
   E2X("raindrops");
   E2X("doubledoor");
   E2X("rotate");
   E2X("fallover");
   E2X("spheroid");
   E2X("turn-page");
   E2X("french-door");
   E2X("turn-cube");
   E2X("windmill");
   E2X("pixelize");
   E2X("twist");
   E2X("Xopen");
   E2X("squish");
   E2X("disintegrate"); 
   E2X("interleave");

   F1_help_topic = "slide show";

   if (checkpend("all")) return;                                                 //  check nothing pending
   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   
   ss_nwt = NWT - 2;                                                             //  leave 2 SMPs free                  20.0
   if (ss_nwt < 1) ss_nwt = 1;
   if (ss_nwt > 4) ss_nwt = 4;                                                   //  more than 4 not helpful 

   ss_KBkeyB = ss_KBkeys[0];                                                     //  unpack KB control keys
   ss_KBkeyN = ss_KBkeys[1];
   ss_KBkeyP = ss_KBkeys[2];
   ss_KBkeyX = ss_KBkeys[3];
   
/***
       ___________________________________________________________
      |               Slide Show                                  |
      |                                                           |
      | [Select Album] album-name  123 images                     |
      | Caption Time [___]  Image Time [___]  Clip Limit % [___]  |
      | Music File: [__________________________________] [Browse] |
      | [x] Full Screen  [x] Auto-replay                          |
      | Customize: [transitions]  [image files] [KB controls]     |              //  KB functions
      |                                                           |
      |                                       [Proceed] [Cancel]  |
      |___________________________________________________________|

***/

   zd = zdialog_new(E2X("Slide Show"),Mwin,Bproceed,Bcancel,null);               //  user dialog

   zdialog_add_widget(zd,"hbox","hbss","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","selectalbum","hbss",E2X("Select Album"),"space=5");
   zdialog_add_widget(zd,"label","albumname","hbss",Bnoselection,"space=5");
   zdialog_add_widget(zd,"label","nfiles","hbss",Bnoimages,"space=5");

   zdialog_add_widget(zd,"hbox","hbprefs","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcctime","hbprefs",E2X("Caption Time"),"space=5");
   zdialog_add_widget(zd,"zspin","cctime","hbprefs","0|9999|1|0");
   zdialog_add_widget(zd,"label","space","hbprefs",0,"space=5");
   zdialog_add_widget(zd,"label","labimagetime","hbprefs",E2X("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",E2X("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",E2X("Music File"),"space=3");
   zdialog_add_widget(zd,"zentry","musicfile","hbmuf","none","size=30|space=5");
   zdialog_add_widget(zd,"button","browse","hbmuf",Bbrowse,"space=2");

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

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

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

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

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

   if (! ss_Nfiles) {                                                            //  no selection
      zmessageACK(Mwin,E2X("invalid album"));
      zdialog_free(zd);
      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,"cctime",ss_cctime);                                         //  filename/caption/comments show 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 option (last --> first image)

   zdialog_free(zd);                                                             //  kill dialog

   ss_saveprefs();                                                               //  save preference changes

   if (curr_file) {                                                              //  start at 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;                                                                  //  else first image
   ss_Fcurrent = ii;                                                             //  next file in list to show

   f_open(ss_imagetab[ii].imagefile);

   m_viewmode(0,"F");                                                            //  insure tab F

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

   if (*ss_musicfile == '/') {
      pp = zescape_quotes(ss_musicfile);
      shell_ack("paplay \"%s\" &",pp);
      zfree(pp);
   }

   ss_newfile = 0;                                                               //  no new image
   ss_pxbnew = 0;
   ss_oldfile = 0;                                                               //  no old (prior) image
   ss_pxbold = 0;

   if (ss_fullscreen) win_fullscreen(1);                                         //  full screen, hide menu and panel
   zmainsleep(0.5);
   ss_ww = gdk_window_get_width(gdkwin);                                         //  window size 
   ss_hh = gdk_window_get_height(gdkwin);

   g_timeout_add(100,ss_timerfunc,0);                                            //  start timer for image changes
   return;
}


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

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

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

   if (zd->zstat) return 1;                                                      //  cancel or [x]
   
   if (strmatch(event,"initz_album"))
   {
      if (! ss_albumfile && curr_album) ss_albumfile = zstrdup(curr_album);
      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",Bnoselection);
      zdialog_stuff(zd,"nfiles",Bnoimages);

      file = zgetfile(E2X("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(E2X("Select music file"),MWIN,"file",pp);
      if (! pp) pp = zstrdup("none");
      zdialog_stuff(zd,"musicfile",pp);
      zfree(pp);
   }
   
   if (strmatch(event,"KBprefs"))
      ss_KBprefs_dialog();

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

   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,E2X("%d images"),ss_Nfiles);
   zdialog_stuff(zd,"nfiles",countmess);
   zdialog_stuff(zd,"imagetime",ss_imagetime);
   zdialog_stuff(zd,"cctime",ss_cctime);
   zdialog_stuff(zd,"cliplim",ss_cliplimit);
   zdialog_stuff(zd,"musicfile",ss_musicfile);
   zdialog_stuff(zd,"fullscreen",ss_fullscreen);
   zdialog_stuff(zd,"replay",ss_replay);

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


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

//  set preferences for keyboard control keys (blank screen, next image, pause/resume, magnify)

void ss_KBprefs_dialog()
{
   int  KBprefs_dialog_event(zdialog *zd, cchar *event);
   
   zdialog  *zd;
   int      zstat;
   char     keyx[4] = "X";
   cchar    *tip = E2X("arrow keys show previous or next image instantly \n"
                       "space bar (blank) is allowed and shows as '-'");
   
/***
          ___________________________________________________
         |           Keyboard Preferences                    |
         |                                                   |
         |  [ B ]    blank or unblank window                 |
         |  [ N ]    show next image, with transition        |
         |  [ P ]    pause or resume slide show              |
         |  [ X ]    magnify image (loupe tool)              |
         |                                                   |
         |  arrow keys show previous or next image instantly |
         |                                                   |
         |                                   [done] [camcel] |
         |___________________________________________________|

***/

   zd = zdialog_new(E2X("Keyboard Preferences"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbB","dialog");
   zdialog_add_widget(zd,"zentry","B","hbB","B","space=5|size=2");
   zdialog_add_widget(zd,"label","labB","hbB",E2X("blank or unblank window"));
   zdialog_add_widget(zd,"hbox","hbN","dialog");
   zdialog_add_widget(zd,"zentry","N","hbN","N","space=5|size=2");
   zdialog_add_widget(zd,"label","labN","hbN",E2X("show next image, with transition"));
   zdialog_add_widget(zd,"hbox","hbP","dialog");
   zdialog_add_widget(zd,"zentry","P","hbP","P","space=5|size=2");
   zdialog_add_widget(zd,"label","labP","hbP",E2X("pause or resume slide show"));
   zdialog_add_widget(zd,"hbox","hbX","dialog");
   zdialog_add_widget(zd,"zentry","X","hbX","X","space=5|size=2");
   zdialog_add_widget(zd,"label","labX","hbX",E2X("magnify image (loupe tool)"));
   zdialog_add_widget(zd,"label","labak","dialog",tip,"space=5");

   keyx[0] = ss_KBkeyB;
   if (*keyx == ' ') *keyx = '-';                                                //  space bar >> '-'
   zdialog_stuff(zd,"B",keyx);

   keyx[0] = ss_KBkeyN;
   if (*keyx == ' ') *keyx = '-';
   zdialog_stuff(zd,"N",keyx);

   keyx[0] = ss_KBkeyP;
   if (*keyx == ' ') *keyx = '-';
   zdialog_stuff(zd,"P",keyx);

   keyx[0] = ss_KBkeyX;
   if (*keyx == ' ') *keyx = '-';
   zdialog_stuff(zd,"X",keyx);

   zdialog_run(zd,KBprefs_dialog_event,"parent");                                //  run, wait for completion
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }
   
   zdialog_fetch(zd,"B",keyx,4);
   if (*keyx == '-') *keyx = ' ';                                                //  '-' >> space
   ss_KBkeyB = ss_KBkeys[0] = keyx[0];

   zdialog_fetch(zd,"N",keyx,4);
   if (*keyx == '-') *keyx = ' ';
   ss_KBkeyN = ss_KBkeys[1] = keyx[0];

   zdialog_fetch(zd,"P",keyx,4);
   if (*keyx == '-') *keyx = ' ';
   ss_KBkeyP = ss_KBkeys[2] = keyx[0];

   zdialog_fetch(zd,"X",keyx,4);
   if (*keyx == '-') *keyx = ' ';
   ss_KBkeyX = ss_KBkeys[3] = keyx[0];
   
   save_params();

   return;
}   


//  dialog event and completion function

int  KBprefs_dialog_event(zdialog *zd, cchar *event)
{
   char     keyx[4];
   
   if (zd->zstat) {
      zdialog_destroy(zd);
      return 1;
   }
   
   if (zstrstr("B N P X",event)) {
      zdialog_fetch(zd,event,keyx,2);
      if (*keyx == ' ') *keyx = '-';                                             //  convert space to '-'
      else keyx[0] = toupper(keyx[0]);
      keyx[1] = 0;
      zdialog_stuff(zd,event,keyx);
   }
      
   return 1;
}
      

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

//  set transitions preferences for specific slide show

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

   zdialog     *zd;
   int         ii, jj, zstat;
   char        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 ] |
      |  ....         ...   .....    ....   |  ....         ...   .....    ....  |
      |                                                                          |
      |                                                         [done] [cancel]  |
      |__________________________________________________________________________|

***/

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

   zd = zdialog_new(E2X("Transition Preferences"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfile","hbfile",E2X("Transitions File"),"space=3");
   zdialog_add_widget(zd,"button","load","hbfile",Bload,"space=3");
   zdialog_add_widget(zd,"button","save","hbfile",Bsave,"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",Bselect,"space=3");
   zdialog_add_widget(zd,"button","all","hbopts",Ball,"space=3");
   zdialog_add_widget(zd,"button","none","hbopts",Bnone,"space=3");
   zdialog_add_widget(zd,"check","rand","hbopts",E2X("random"),"space=3");

   zdialog_add_widget(zd,"label","space","hbopts","","space=10");
   zdialog_add_widget(zd,"label","labtime","hbopts",E2X("time"),"space=3");
   zdialog_add_widget(zd,"zspin","alltime","hbopts","0.1|10|0.1|2","size=4");
   zdialog_add_widget(zd,"button","setalltime","hbopts",E2X("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",E2X("transition"));
   zdialog_add_widget(zd,"label","labenab","vb2",E2X("use"));
   zdialog_add_widget(zd,"label","labtime","vb3",E2X("time"));
   zdialog_add_widget(zd,"label","labpref","vb4",E2X("pref"));
   zdialog_add_widget(zd,"label","labname","vb5",E2X("transition"));
   zdialog_add_widget(zd,"label","labenab","vb6",E2X("use"));
   zdialog_add_widget(zd,"label","labtime","vb7",E2X("time"));
   zdialog_add_widget(zd,"label","labpref","vb8",E2X("pref"));
   
   zdialog_stuff(zd,"rand",ss_random);                                           //  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","transition");
         zdialog_add_widget(zd,"check",enabii,"vb2");
         zdialog_add_widget(zd,"zspin",timeii,"vb3","0.1|10|0.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","transition");
         zdialog_add_widget(zd,"check",enabii,"vb6");
         zdialog_add_widget(zd,"zspin",timeii,"vb7","0.1|10|0.1|2","size=4");
         zdialog_add_widget(zd,"zspin",prefii,"vb8","0|99|1|10","size=3");
      }
      zdialog_stuff(zd,nameii,E2X(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,"rand",ss_random);                                           //  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, cchar *event)
{
   int transprefs_load(FILE *fid);
   int transprefs_save(FILE *fid);
   
   FILE     *fid;
   int      err, ii;
   float    time;
   char     *file, *pp;
   char     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("load",MWIN,"file",slideshow_trans_folder,0);              //  open trans prefs file
      if (! file) return 1;
      fid = fopen(file,"r");
      if (! fid) {
         zmessageACK(Mwin,E2X("invalid file"));
         return 1;
      }

      err = transprefs_load(fid);                                                //  load file into dialog
      fclose(fid);
      if (err) return 1;
      
      pp = strrchr(file,'/');                                                    //  show file name in dialog
      zdialog_stuff(zd,"tranfile",pp+1);

      zdialog_stuff(zd,"rand",ss_random);                                        //  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,E2X(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,"rand",ss_random);                                        //  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("save",MWIN,"save",slideshow_trans_folder,0);              //  save trans prefs file
      if (! file) return 1;
      fid = fopen(file,"w");
      if (! fid) {
         zmessageACK(Mwin,E2X("invalid file"));
         return 1;
      }

      transprefs_save(fid);                                                      //  save dialog to file
      fclose(fid);

      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(FILE *fid)
{
   char        *pp, buff[XFCC];
   int         ii, jj, nn;
   char        tranname[32];
   int         n1, n2;
   float       ff;
   
   pp = fgets_trim(buff,XFCC,fid,1);
   if (! pp) goto format_error;

   nn = sscanf(buff,"random %d ",&ss_random);
   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)
         printz("bad record: %s \n",buff);
         continue;
      }
      for (ii = 0; ii < SSNT; ii++)
         if (strmatch(tranname,ss_trantab[ii].tranname)) break;
      if (ii == SSNT) {
         printz("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
   return 0;

format_error:
   zmessageACK(Mwin,E2X("file format error: \n %s"),buff);
   return 1;
}


//  save transition prefs to a file

int transprefs_save(FILE *fid)
{
   fprintf(fid,"random %d \n",ss_random);

   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);
   return 0;
}


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

//  set image preferences for specific slide show

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

   zdialog  *zd;
   int      ii, kk;
   char     *pp, zoomloc[40];

/***
       __________________________________________________
      |             Image Preferences                    |
      |                                                  |
      |  Image File: [________________________________]  |
      |                                                  |
      |  Action                                 seconds  |
      |  ---------------------------------------------   |
      |  Play tone when image shows [x]                  |
      |  Wait before filename/caption/comments   [1.5]   |
      |  Show image filename (overlap)           [2.0]   |                       //  20.0
      |  Show image caption (overlap)            [2.0]   |
      |  Show image comments (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]          |
      |                                                  |
      |                                          [Done]  |
      |__________________________________________________|

***/

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

   m_viewmode(0,"G");                                                            //  gallery view

   zd = zdialog_new(E2X("Image Preferences"),Mwin,Bdone,null);
   zd_ss_imageprefs = zd;
   
   zdialog_add_widget(zd,"hbox","hbimf","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labimf","hbimf",E2X("Image File:"),"space=3");
   zdialog_add_widget(zd,"label","imagefile","hbimf",0,"space=3");
   
   zdialog_add_widget(zd,"hbox","hbact","dialog");
   zdialog_add_widget(zd,"label","labact","hbact",E2X("Action"),"space=3");
   zdialog_add_widget(zd,"label","space","hbact",0,"expand");
   zdialog_add_widget(zd,"label","labsecs","hbact",Bseconds,"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=3");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=3");
   
   zdialog_add_widget(zd,"hbox","hbtone","vb1");
   zdialog_add_widget(zd,"label","labtone","hbtone",E2X("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",E2X("Wait before filename/caption/comments"));
   zdialog_add_widget(zd,"zspin","wait0","vb2","0|99|1|0","size=3");

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

   zdialog_add_widget(zd,"hbox","hbcap","vb1");
   zdialog_add_widget(zd,"label","labcap","hbcap",E2X("Show image caption (overlap)"));
   zdialog_add_widget(zd,"zspin","capsecs","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbcom","vb1");
   zdialog_add_widget(zd,"label","labcom","hbcom",E2X("Show image comments (overlap)"));
   zdialog_add_widget(zd,"zspin","commsecs","vb2","0|99|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbw1","vb1");
   zdialog_add_widget(zd,"label","labwait","hbw1",E2X("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",E2X("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",E2X("zoom-in"),"space=3");
   zdialog_add_widget(zd,"check","zoomout","hbzoom",E2X("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",E2X("Zoom Center"));
   zdialog_add_widget(zd,"label","labzloc","hbzloc","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",E2X("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",E2X("Transition to next image"),"space=3");
   zdialog_add_widget(zd,"combo","tranname","hbtrn");

   zdialog_cb_app(zd,"tranname",E2X("next"));                                    //  default transition

   for (ii = 0; ii < SSNT; ii++)                                                 //  add all transitions to dropdown list
      zdialog_cb_app(zd,"tranname",E2X(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. image file data
   if (pp) zdialog_stuff(zd,"imagefile",pp+1);                                   //    into dialog
   zdialog_stuff(zd,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"wait0",ss_imagetab[ii].wait0);
   zdialog_stuff(zd,"filesecs",ss_imagetab[ii].filesecs);                        //  20.0
   zdialog_stuff(zd,"capsecs",ss_imagetab[ii].capsecs);
   zdialog_stuff(zd,"commsecs",ss_imagetab[ii].commsecs);
   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,"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",E2X(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, cchar *event)
{
   int         ii, jj;
   float       ff;
   char        tranname[32];
   GdkWindow   *gdkwin;

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

   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")) {                                             //  20.0
      zdialog_fetch(zd,"filesecs",jj);
      ss_imagetab[ii].filesecs = jj;
   }

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

   if (strmatch(event,"commsecs")) {
      zdialog_fetch(zd,"commsecs",jj);
      ss_imagetab[ii].commsecs = 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(E2X("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,E2X("next"))) { 
         for (jj = 0; jj < SSNT; jj++)
            if (strmatch(tranname,E2X(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;
   char        *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;

   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,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"wait0",ss_imagetab[ii].wait0);
   zdialog_stuff(zd,"filesecs",ss_imagetab[ii].filesecs);                        //  20.0
   zdialog_stuff(zd,"capsecs",ss_imagetab[ii].capsecs);
   zdialog_stuff(zd,"commsecs",ss_imagetab[ii].commsecs);
   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",E2X(ss_imagetab[ii].tranname));

   return;
}


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

   global data:
   imagetime: NN                    0-99        image show time
   cctime: NN                       0-99        filename/caption/comments show time      //  20.0
   cliplimit: NN                    0-50
   random: N                        0-1
   musicfile: /path.../file.ogg     music file or "none"
   fullscreen:                      0-1
   replay:                          0-1

   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
   tone: N                          0-1  play tone for this image
   wait0: NN                        seconds to wait 0-99 
   filename: NN                     seconds to show file name                    //  20.0
   caption: NN                      seconds to show caption
   comments: NN                     seconds to show comments
   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;
   char        buff[XFCC];
   char        prefsfile[200], *pp;
   int         ii, jj, nn, format;
   FTYPE       ftype;
   char        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,E2X("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);                                   //  add to image table
      ii++;
   }

   fclose(fid);
   ss_Nfiles = ii;

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

   album_show(ss_albumfile);                                                     //  open slide show album              19.0

   ss_imagetime = 3;                                                             //  default image display time
   ss_cctime = 0;                                                                //  default filename/caption/comment 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_random = 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].tone = 0;
      ss_imagetab[ii].wait0 = 0;
      ss_imagetab[ii].filesecs = 0;                                              //  20.0
      ss_imagetab[ii].capsecs = 0;
      ss_imagetab[ii].commsecs = 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   image show time, secs.
            ss_imagetime = atoi(pp+11);
            continue;
         }

         if (strmatchN(pp,"cctime: ",8)) {                                       //  cctime: NN   filename/caption/comments
            ss_cctime = atoi(pp+8);
            continue;
         }

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

         if (strmatchN(pp,"random: ",8)) {                                       //  random: N  0-1 = seq./random transactions
            ss_random = atoi(pp+8);
            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)) {                                       //  random: 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)
            printz("bad record: %s \n",buff);
            continue;
         }
         for (ii = 0; ii < SSNT; ii++)
            if (strmatch(tranname,ss_trantab[ii].tranname)) break;
         if (ii == SSNT) {
            printz("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 != '/') {
               printz("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,"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 cap/comms
            nn = atoi(pp+7);
            ss_imagetab[ii].wait0 = nn;
            continue;
         }
         
         if (strmatchN(pp,"filename: ",10)) {                                    //  filename: NN   secs for file name  20.0
            nn = atoi(pp+10);
            ss_imagetab[ii].filesecs = nn;
            continue;
         }

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

         if (strmatchN(pp,"comments: ",10)) {                                    //  comments: NN   secs to show comments
            nn = atoi(pp+10);
            ss_imagetab[ii].commsecs = 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 = atof(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;
   char        prefsfile[200];
   int         ii;
   
   if (! ss_Nfiles) {
      zmessageACK(Mwin,E2X("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,"cctime: %d \n",ss_cctime);
   fprintf(fid,"cliplimit: %d \n",ss_cliplimit);
   fprintf(fid,"random: %d \n",ss_random);
   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,"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);                   //  20.0
      fprintf(fid,"caption: %d \n",ss_imagetab[ii].capsecs);
      fprintf(fid,"comments: %d \n",ss_imagetab[ii].commsecs);
      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;
}


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

//  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 *)                                                         //  rewritten
{
   int ss_timerfunc_sleep(int sleepsecs);

   int            img, jj;
   int            filesecs, capsecs, commsecs, sleepsecs;
   cchar          *keynames[2] = { iptc_caption_key, exif_comment_key };
   char           *keyvals[2], *pp;

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

   if (Fescape) ss_event = "escape";                                             //  from main()
   if (FGWM != 'F') ss_event = "escape";                                         //  must be F-view mode                19.0
   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           19.0

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

EOL:                                                                             //  last image reached
   img = ss_Nfiles - 1;
   f_open(ss_imagetab[img].imagefile);                                           //  last file --> current file
   zmessage_post_bold(Mwin,"20/20",5,"END (Escape to exit)");
   ss_event = "";                                                                //  wait for escape
   return 1;
      
escape:                                                                          //  terminate slide show
   Fescape = 0;                                                                  //  reset escape key
   if (ss_pxbold) PXB_free(ss_pxbold);                                           //  free memory
   if (ss_pxbnew) PXB_free(ss_pxbnew);
   ss_pxbold = ss_pxbnew = 0;
   if (*ss_musicfile == '/') shell_quiet("pulseaudio --kill");                   //  kill music if any
   if (ss_fullscreen) win_unfullscreen();                                        //  restore old window size, menu etc.
   ss_fullscreen = 0;
   Fslideshow = 0;                                                               //  reset flags
   Fblock = 0;
   img = ss_Fcurrent;
   f_open(ss_imagetab[img].imagefile);                                           //  open last image shown
   m_slideshow(0,0);                                                             //  return to slide show dialog
   return 0;                                                                     //  stop the timer

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
      if (ss_Fcurrent < ss_Nfiles-1) ss_Fcurrent++;                              //  next image file
      else if (! ss_replay) {
         ss_event = "EOL";                                                       //  last image file was shown
         return 1;
      }
      else ss_Fcurrent = 0;                                                      //  start over with first file
   }

   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      19.0
      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 file/cap/com, show file/cap/com, 
   //    wait before zoom, zoom, wait after zoom

   if (ss_imagetab[img].tone)
      shell_quiet("paplay %s/slideshow-tone.oga &",get_zhomedir());              //  play tone if specified 

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

   filesecs = ss_imagetab[img].filesecs;                                         //  time to show file name             20.0
   capsecs = ss_imagetab[img].capsecs;                                           //  time to show caption
   commsecs = ss_imagetab[img].commsecs;                                         //  time to show comments
   if (filesecs + capsecs + commsecs == 0)                                       //  if not specified, use default      20.0
      filesecs = capsecs = commsecs = ss_cctime;     
   exif_get(ss_newfile,keynames,keyvals,2);                                      //  get image captions and comments
   if (! keyvals[0]) capsecs = 0;                                                //  zero time if no caption
   else zfree(keyvals[0]);
   if (! keyvals[1]) commsecs = 0;                                               //  zero time if no comments
   else zfree(keyvals[1]);

   while (filesecs + capsecs + commsecs)
   {
      ss_showcapcom(filesecs,capsecs,commsecs);                                  //  show items with remaining time     20.0
      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
      capsecs -= 1;   if (capsecs < 0) capsecs = 0;
      commsecs -= 1;  if (commsecs < 0) commsecs = 0;
   }

   ss_showcapcom(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);
      shell_ack("ffplay -loglevel -8 -autoexit \"%s\" ",pp);                     //  play video, wait for user quit
      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 (Fescape) return 1;                                                     //  from main()                        19.0
   }

   return 0;
}


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

//  process keyboard input key

void ss_KBfunc(int kbkey)
{
   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;
}


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

//  select next transition type to use
//  mode = sequential: use each enabled transition type in sequence
//  mode = random: exclude recently used, choose random from remaining

int ss_nextrans()
{
   int      ii, jj, maxii, maxjj, next;
   float    maxrank, rank;

   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_random == 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
   {
      maxrank = 0;
      maxii = 0;
      maxjj = ss_Nused / 2;                                                      //  most recently used to exclude
      if (maxjj > 4) maxjj = 4;                                                  //  max. 4

      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];
         rank = ss_trantab[jj].preference * drandz();                            //  rank = preference * random value
         if (rank > maxrank) {
            maxrank = rank;                                                      //  remember highest rank
            maxii = ii;
         }
      }

      next = ss_Tused[maxii];                                                    //  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, caption and comments at the top of the image
//  show only those items with time > 0

int ss_showcapcom(int filesecs, int capsecs, int commsecs)                       //  20.0
{
   cchar        *keynames[2] = { iptc_caption_key, exif_comment_key };
   char         *keyvals[2], *pp;
   char         filename[200], caption[200], comments[200];
   static char  text[600];
   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 + capsecs + commsecs == 0) return 0;                             //  20.0

   *text = 0;

   pp = strrchr(ss_newfile,'/');                                                 //  get base file name
   if (pp) pp++;
   else pp = ss_newfile;
   strncpy0(filename,pp,200);
   
   *caption = *comments = 0;
   exif_get(ss_newfile,keynames,keyvals,2);                                      //  get captions and comments metadata

   if (keyvals[0]) {
      strncpy0(caption,keyvals[0],200);
      zfree(keyvals[0]);
   }

   if (keyvals[1]) {
      strncpy0(comments,keyvals[1],200);
      zfree(keyvals[1]);
   }
   
   *text = 0;
   pline = 0;

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

   if (capsecs && *caption) {
      if (pline) strncatv(text,600,"\n",0);
      strncatv(text,600,caption,0);
      pline = 1;
   }

   if (commsecs && *comments) {
      if (pline) strncatv(text,600,"\n",0);
      strncatv(text,600,comments,0);
   }
   
   for (int ii = 0; text[ii]; ii++)                                              //  replace "\\n" with real newline char.
      if (text[ii] == '\\' && text[ii+1] == 'n')
         memcpy(text+ii,"\n ",2);
   
   pangofont = pango_font_description_from_string("Sans 12");                    //  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,text,-1);                                   //  add text to layout
   pango_layout_get_pixel_size(pangolayout,&plww,&plhh);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);
   cairo_set_line_width(cr,1);
   cairo_set_source_rgb(cr,1,1,1);                                               //  draw white background
   cairo_rectangle(cr,10,10,plww,plhh);
   cairo_fill(cr);
   cairo_move_to(cr,10,10);                                                      //  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(char *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

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

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


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

//  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, rs;
   float       newpart, oldpart;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3;
   double      T0, Te, Tz;

   pxbmix = PXB_copy(ss_pxbold);
   rs = pxbmix->rs;

   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            19.0
      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 * 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 (Fescape) 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            19.0
      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 (Fescape) 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 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            19.0
      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);
      
      ppy = py;
      zmainloop();
      if (Fescape) 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            19.0
      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 (Fescape) 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            19.0
      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 (Fescape) 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            19.0
      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 (Fescape) 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_implode()
{
   float       size;
   int         ww, hh, px, py;
   PXB         *pxbnew, *pxbold;
   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            19.0
      if (Te > Tz) break;                                                        //  done
      
      size = 1.0 - Te / Tz;
      if (size < 0.01) size = 0.01;

      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 (Fescape) 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_explode()
{
   float       size;
   int         ww, hh, px, py;
   PXB         *pxbnew, *pxbold;
   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            19.0
      if (Te > Tz) break;                                                        //  done
      
      size = Te / Tz;
      if (size < 0.01) size = 0.01;
      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 (Fescape) 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            19.0
 
      T = 2.0 * PI * Te / Tz;                                                    //  0 ... 2 * PI
      if (T - pT > dTmax) T = pT + dTmax;
      pT = T;                                                                    //  enforce max. dT to avoid gaps      19.0
      if (T > 1.99 * 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 (Fescape) 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;
   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;                       
   pT = PI / 2;
   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            19.0
      
      T = 0.51 * PI - PI * Te / Tz;                                              //  0.5 * PI ... -0.5 * PI
      if (pT - T > dTmax) T = pT - dTmax;                                        //  enforce max. dT to avoid gaps      19.0
      pT = T;
      if (T < -0.499 * 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;                                //  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 (Fescape) 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;
   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)
   
   while (true)
   {
      Te = get_seconds() - T0;                                                   //  elapsed transition time            19.0
      if (Te >= Tz) break;

      T = 16 * PI * Te / Tz;                                                     //  0 ... 8 revolutions
            
      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;                                                    //  19.0
      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 (Fescape) 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            19.0
      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 (Fescape) 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            19.0
      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 (Fescape) 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            19.0
      if (Te > Tz) break;                                                        //  done
      
      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,4,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-4) break;
      pix3 = pixels + 3 * px;                                                    //  line from (ww/2+bx,0) to (ww/2+bx,hh-1)
      pixbuf = GPNFD(pix3,4,ss_hh);
      gdk_cairo_set_source_pixbuf(cr,pixbuf,px,0);
      cairo_paint(cr);
      g_object_unref(pixbuf);

      zmainloop();
      if (Fescape) 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;
   float       R;
   double      T0, Te, Tz;

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

   ss_trantime = 0.5 * ss_trantime;                                              //  2 loops, each 1/2 time

   T0 = get_seconds();                                                           //  transition start time
   Tz = ss_trantime;                                                             //  transition time (goal)
   
   while (true)
   {
      Te = get_seconds() - T0;                                                   //  elapsed transition time            19.0
      if (Te >= Tz) break;                                                       //  done
      
      R = Te / Tz;
      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_nwt);

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (Fescape) break;                                                        //  20.0
   }

   T0 = get_seconds();                                                           //  transition start time

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

      R = Te / Tz;
      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_nwt);

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (Fescape) 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;

   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   for (px = cx1 + 2 * index; px < cx2-1; px += 2 * ss_nwt)                      //  speedup
   {
      Rx = 1.0 * (px - cx1) / (cx2 - cx1);
      ylo = cy1 + Rx * (cy2 - cy1);
      yhi = cy4 + Rx * (cy3 - cy4);

      for (py = ylo; py < yhi; py++)
      {
         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);
      }
   }

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

   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   for (px = cx2 + 2 * index; px < cx1-1; px += 2 * ss_nwt)
   {
      Rx = 1.0 * (px - cx2) / (cx1 - cx2);
      ylo = cy2 + Rx * (cy1 - cy2);
      yhi = cy3 + Rx * (cy4 - cy3);

      for (py = ylo; py < yhi; py++)
      {
         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);
      }
   }

   pthread_exit(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            19.0
      if (Te > Tz) break;                                                        //  done
      
      R = 1.0 * Te / Tz;                                                         //  0 ... 1

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

      gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (Fescape) 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;
   
   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   for (py = index; py < ss_hh; py += ss_nwt)                                    //  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; px++)                                           //  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,3);
      }
   }
   
   pthread_exit(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);

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

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

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

      zmainloop();
      if (Fescape) 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;
   
   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

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

   pthread_exit(0);
   return 0;
}


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

//  new image turns up from lower right corner, like a book page

void ss_turnpage()
{
   PXB         *pixbuf3;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3;
   int         pxA, ppxA, pxS;
   int         px1, py1, px3, py3;
   int         cc, np1, np2;
   float       C;
   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
   np1 = Prange / 300;                                                           //  step size
   np2 = 5;
   pxA = ppxA = ss_ww;

   T0 = get_seconds();                                                           //  transition start time
   Tz = ss_trantime;                                                             //  transition time (goal)
   
   while (true)
   {
      Te = get_seconds() - T0;                                                   //  elapsed transition time            19.0
      if (Te > Tz) break;                                                        //  done
      
      pxA = ss_ww - Prange * Te / Tz;
      np1 = ppxA - pxA;
      if (np1 < 1) np1 = 1;

      for (px1 = pxA, py1 = ss_hh-1; px1 < ss_ww && py1 >= 0; px1++, py1--)
      {
         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
      }
      
      for (pxS = pxA; pxS < ss_ww-np2; pxS += np2)                               //  point S moves from point A to the right
      {
         C = 0.53 * (pxS - pxA) / (ss_ww - pxA);
         C = C * (pxS - pxA);

         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*np2);
         }
      }
      
      gdk_cairo_set_source_pixbuf(cr,pixbuf3->pixbuf,0,0);                       //  paint image
      cairo_paint(cr);

      zmainloop();
      if (Fescape) 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;
}


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

//  French door: old image swings away left and right to reveal new image.

namespace ss_frenchdoor_names
{
   PXB      *pxbmix;
   PIXBUF   *pixbuf;                                                             //  output image
   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            19.0
      if (Te >= Tz) break;                                                       //  done
      
      R = Te / Tz;                                                               //  0 ... 1

      pixels1 = ss_pxbold->pixels;                                               //  old image
      pxbmix = PXB_copy(ss_pxbnew);                                              //  mixed image
      pixels3 = pxbmix->pixels;

      do_wthreads(ss_frenchdoor_thread,ss_nwt);

      sww = ww2 / 30 + 6;                                                        //  left side
      ww3 = (1 - R) * ww2 + sww;
      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);

      zmainloop();
      if (Fescape) break;
   }

   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;

   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   for (px1 = index; px1 < ww2; px1 += ss_nwt)                                   //  0 >> ww2
   {
      px3 = (1 - R) * px1;                                                       //  0 >> ww2 - X
      if (px3 == px3L) continue;
      px3L = px3;

      for (py1 = 0; py1 < ss_hh-1; py1 += 2)                                     //  0 >> ss_hh
      {
         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,3);
         memcpy(pix3 + ss_rs, pix1 + ss_rs, 3);
      }
   }
   
   for (px1 = ww2 + index; px1 < ss_ww; px1 += ss_nwt)                           //  ww2 >> ss_ww
   {
      px3 = R * ss_ww + (1 - R) * px1;                                           //  ww2 + X >> ss_ww
      if (px3 == px3L) continue;
      px3L = px3;

      for (py1 = 0; py1 < ss_hh-1; py1 += 2)                                     //  0 >> ss_hh
      {
         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,3);
         memcpy(pix3 + ss_rs, pix1 + ss_rs, 3);
      }
   }

   pthread_exit(0);
   return 0;
}


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

//  turn a cube to a new face with new image

namespace ss_turncube_names
{
   float       To, Te, Tz;                                                       //  start, elapsed, goal times
   PXB         *pxbmix1, *pxbmix2, *pxbtemp;
   float       F, edge;
   uint8       *pixels1, *pixels2, *pixels3;
   pthread_t   tid;
}


void ss_turncube()                                                               //  speedup                            19.0
{
   using namespace ss_turncube_names;

   void ss_turncube_start_step();
   void ss_turncube_wait_step();

   pixels1 = ss_pxbold->pixels;
   pixels2 = ss_pxbnew->pixels;
   pxbmix1 = PXB_copy(ss_pxbold);
   pxbmix2 = PXB_copy(ss_pxbold);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   Tz = ss_trantime;                                                             //  transition goal time
   To = get_seconds();                                                           //  transition start time
   zsleep(0.001);

   pixels3 = pxbmix1->pixels;
   ss_turncube_start_step();                                                     //  make first image

   while (true)
   {
      ss_turncube_wait_step();                                                   //  wait for image done
      if (F >= 1) break;
      if (Fescape) break;                                                        //  20.0

      pxbtemp = pxbmix1;
      pxbmix1 = pxbmix2;
      pxbmix2 = pxbtemp;
      pixels3 = pxbmix1->pixels;
      
      ss_turncube_start_step();                                                  //  start next image

      gdk_cairo_set_source_pixbuf(cr,pxbmix2->pixbuf,0,0);                       //  output image
      cairo_paint(cr);
      zmainsleep(0.002);                                                         //  mysteriously smoother              20.0
   }

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

   draw_context_destroy(draw_context);
   PXB_free(pxbmix1);
   PXB_free(pxbmix2);
   return;
}


void ss_turncube_start_step()
{
   using namespace ss_turncube_names;

   void * ss_turncube_thread(void *);

   tid = start_Jthread(ss_turncube_thread,null);
   return;
}


void ss_turncube_wait_step()
{
   using namespace ss_turncube_names;
   wait_Jthread(tid);
   return;
}


void * ss_turncube_thread(void *)
{
   using namespace ss_turncube_names;
   
   void * ss_turncube_wthread(void *);
   
   Te = get_seconds() - To;                                                      //  elapsed time
   F = Te / Tz;                                                                  //  transition status 0 >> 1
   if (F >= 1) pthread_exit(0);

   edge = F * ss_ww;                                                             //  0 ... ss_ww
   int cc = gdk_pixbuf_get_byte_length(pxbmix1->pixbuf);
   memset(pixels3,0,cc);
   do_wthreads(ss_turncube_wthread,ss_nwt);                                      //  20.0
   pthread_exit(0);
}


void * ss_turncube_wthread(void *arg)
{
   using namespace ss_turncube_names;
   
   int      index = *((int *) arg);
   int      px1, py1, px2, py2;
   uint8    *pix1, *pix2;
   float    c1, c2, c3, c4, c33, c44;
   
   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0
   
   c1 = edge / ss_ww;                                                            //  0 ... 1
   c2 = 1.0 - c1;                                                                //  1 ... 0
   c3 = 0.2 * c2;                                                                //  0.2 ... 0
   c4 = 0.2 * c1;                                                                //  0 ... 0.2
   
   for (py1 = index * 3; py1 < ss_hh-3; py1 += ss_nwt * 3)                       //  loop old image pixels              20.0
   for (px1 = 0; px1 < ss_ww-3; px1 += 3)
   {
      c44 = c4 * px1 / ss_ww;                                                    //  0 ... c4
      px2 = edge + 1.0 * px1 * (ss_ww - edge) / ss_ww;                           //  image on right cube face
      if (px2 > ss_ww - 2) break;
      py2 = c44 * ss_hh + (1.0 - c44 - c44) * py1;                               //  c44 * ss_hh ... (1 - c44) * ss_hh
      
      pix1 = pixels1 + py1 * ss_rs + px1 * 3;                                    //  move pixel image to cube face
      pix2 = pixels3 + py2 * ss_rs + px2 * 3;
      memcpy(pix2,pix1,9);
      pix1 += ss_rs;
      pix2 += ss_rs;
      memcpy(pix2,pix1,9);
      pix1 += ss_rs;
      pix2 += ss_rs;
      memcpy(pix2,pix1,9);
   }

   for (py1 = index * 3; py1 < ss_hh-3; py1 += ss_nwt * 3)                       //  loop new image pixels              20.0
   for (px1 = 0; px1 < ss_ww-3; px1 += 3)
   {
      c33 = c3 * (1.0 - 1.0 * px1 / ss_ww);                                      //  c3 ... 0
      px2 = c1 * px1;                                                            //  image on left cube face
      py2 = c33 * ss_hh + (1.0 - c33 - c33) * py1;                               //  c33 * ss_hh ... (1 - c33) * ss_hh
      
      pix1 = pixels2 + py1 * ss_rs + px1 * 3;                                    //  move pixel image to cube face
      pix2 = pixels3 + py2 * ss_rs + px2 * 3;
      memcpy(pix2,pix1,9);
      pix1 += ss_rs;
      pix2 += ss_rs;
      memcpy(pix2,pix1,9);
      pix1 += ss_rs;
      pix2 += ss_rs;
      memcpy(pix2,pix1,9);
   }

   pthread_exit(0);
   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            19.0
      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 (Fescape) 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            19.0
      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 (Fescape) 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 twist2_names
{
   PXB      *PXBmix;
   int      cx, cy;
   float    Told, Tnew;
   float    F1, F2;
   float    *D, *Tp;
   float    Dmax;
}


void ss_twist()
{
   using namespace twist2_names;

   void * ss_twist_thread(void *arg);
   
   int         ii, px, py, Dx, Dy;
   double      T0, Te, Tz;

   cx = ss_ww / 2;                                                               //  image center
   cy = ss_hh / 2;
   Dmax = sqrtf(cx * cx + cy * cy);                                              //  distance to corner

   ii = ss_ww * ss_hh;
   D = (float *) zmalloc(ii * sizeof(float));                                    //  precalculated factors
   Tp = (float *) zmalloc(ii * sizeof(float));                                   //    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            19.0
      if (Te > Tz) break;                                                        //  done
      
      F1 = Te / Tz;                                                              //  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_nwt);

      gdk_cairo_set_source_pixbuf(cr,PXBmix->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (Fescape) 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 twist2_names;
   
   int      index = *((int *) arg);
   int      ii, px, py, qx, qy;
   float    DN, Tq, T;
   uint8    *pixmix, *vpix, pix1[3], pix2[3], pix3[3];

   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   for (py = 2 * index; py < ss_hh-1; py += 2 * ss_nwt)                          //  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];
   }

   pthread_exit(0);
   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;

   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            19.0
      if (Te > Tz) break;                                                        //  done
      
      pxbmix = PXB_copy(ss_pxbnew);
      pixels1 = ss_pxbold->pixels;                                               //  old image
      pixels2 = pxbmix->pixels;                                                  //  new image + old image overlay
      
      ydisp = Te / Tz * 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 (Fescape) 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

void ss_squish()
{
   PXB         *pxbout;
   uint8       *pixold, *pixnew, *pixout;
   uint8       *pix1, *pix2;
   int         ww, hh, rs, cx, cy, cmin;
   int         cc, ii, Q;
   int         px, py, qx, qy, vx, vy, ex, ey;
   float       R, Rmax;
   float       *De, *Dp;                                                         //  edge and pixel distance from center
   float       *Ex, *Ey;                                                         //  edge coordinates
   float       M, V, 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);                                                   //  distance (0,0) to edge
   Dp = (float *) zmalloc(cc);                                                   //  distance (0,0) to (px,py)
   Ex = (float *) zmalloc(cc);                                                   //  line (0,0) to (px,py)
   Ey = (float *) zmalloc(cc);                                                   //    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 if (qx < 0) {
         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            19.0
      if (Te > Tz) break;                                                        //  done

      R = Te / Tz * Rmax;

      for (py = 0; py < hh-1; py += 2)                                           //  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);
            memcpy(pix2+rs,pix1+rs,6);
         }

         else if (R < De[ii])                                                    //  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);
            memcpy(pix2+rs,pix1+rs,6);
         }
      }

      gdk_cairo_set_source_pixbuf(cr,pxbout->pixbuf,0,0);
      cairo_paint(cr);

      zmainloop();
      if (Fescape) 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;
}


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

//  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            19.0
      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 (Fescape) 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

/***
          ________________________
         |______| -->       ______|
         |______       <-- |______|    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            19.0
      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 (Fescape) break;
   }

   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(char *file)                                                   //  overhauled for speed               19.0
{
   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;
   
   pthread_exit(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 (Fescape) 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);
      zmainsleep(0.002);

      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 (Fescape) break;

      zoom = ss_zoomsize + (1 - ss_zoomsize) * Te / Tz;                          //  zoom size ... 1.0
      ss_zoom_start(zoom);                                                       //  start meking next zoom image

      gdk_cairo_set_source_pixbuf(cr,Zpxb->pixbuf,0,0);                          //  paint
      cairo_paint(cr);
      zmainsleep(0.002);

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


