dkopp/0000775000175000017500000000000015002404054010627 5ustar micomicodkopp/dkopp.cc0000664000175000017500000047335015002404054012267 0ustar micomico/************************************************************************** dkopp copy files to / restore files from BRD/DVD media Copyright 2007-2024 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ***************************************************************************/ #include #include #include #include #include #if defined __linux__ #include #endif #include "zfuncs.h" // order important // parameters and limits #define release "dkopp-8.2" // release 8.2 #define debug 0 #define vrcc 512*1024 // verify read I/O size #define maxnx 1000 // max no. include/exclude recs. #define maxfs 200000 // max no. disk or DVD/BRD files #define maxhist 200 // max no. history files #define giga 1000000000.0 // gigabyte (not 1024**3) #define modtimetolr 1.0 // tolerance, equal mod times #define nano 0.000000001 // nanosecond #define gforce "-use-the-force-luke=tty -use-the-force-luke=notray" // growisofs hidden options // special control files on DVD/BRD #define V_DKOPPDIRK "/dkopp-data/" // dkopp special files on DVD/BRD #define V_FILEPOOP V_DKOPPDIRK "filepoop" // directory data file #define V_JOBFILE V_DKOPPDIRK "jobfile" // backup job data file #define V_DATETIME V_DKOPPDIRK "datetime" // date-time file // GTK GUI widgets GtkWidget *mWin, *mVbox, *mScroll, *mLog; // main window GtkTextBuffer *logBuff; GtkWidget *fc_dialogbox, *fc_widget; // file-chooser dialog GtkWidget *editwidget; // edit box in file selection dialogs namespace zfuncs { // externals from zfuncs module extern GdkDisplay *display; // X11 workstation (KB, mouse, screen) extern GdkDeviceManager *manager; // knows screen / mouse associations extern GdkScreen *screen; // monitor (screen) extern GdkDevice *mouse; // pointer device extern GtkSettings *settings; // screen settings extern GtkWidget *mainwin; // main window for zfuncs parent 7.5 } #define MWIN GTK_WINDOW(mWin) // file scope variables int killFlag; // tell function to quit int pauseFlag; // tell function to pause/resume int menuLock; // menu lock flag int commFail; // command failure flag int Fdialog; // dialog in progress int clrun; // flag, command line 'run' command int Fgui; // flag, GUI or command line ch subprocName[20]; // name of created subprocess ch scriptParam[200]; // parameter from script file ch mbmode[20], mvmode[20]; // actual backup, verify modes double pctdone; // % done from growisofs ch scrFile[XFCC]; // command line script file ch backupDT[16]; // nominal backup date: yyyymmdd-hhmm ch homedir[200]; // /home/user/.dkopp ch TFdiskfiles[200], TFdvdfiles[200]; // scratch files in homedir ch TFjobfile[200], TFfilepoop[200], TFdatetime[200]; ch TFrestorefiles[200], TFrestoredirks[200]; // available DVD/BRD devices int ndvds, maxdvds = 8; ch dvddevs[8][20]; // DVD/BRD devices, /dev/sr0 etc. ch dvddesc[8][40]; // DVD/BRD device descriptions ch dvddevdesc[8][60]; // combined device and description // backup job data ch BJfile[XFCC]; // backup job file ch BJdvd[20]; // DVD/BRD device: /dev/hdb ch BJbmode[20]; // backup: full/incremental/accumulate ch BJvmode[20]; // verify: full/incremental/thorough ch BJdatefrom[12]; // mod date selection, yyyy.mm.dd time_t BJtdate; // binary mod date selection int BJval; // backup job data validated int BJmod; // backup job data modified ch *BJinex[maxnx]; // backup include/exclude records int BJfiles[maxnx]; // corresp. file count per rec double BJbytes[maxnx]; // corresp. byte count per rec int BJdvdno[maxnx]; // corresp. DVD/BRD seqnc. no. (1,2...) int BJnx; // actual record count < maxnx // DVD/BRD medium data ch dvdmp[100]; // mount point, /media/xxxxx int dvdmpcc; // mount point cc int dvdmtd; // DVD/BRD mounted ch mediumDT[16]; // DVD/BRD medium last use date-time time_t dvdtime; // DVD/BRD device mod time ch dvdlabel[32]; // DVD/BRD label // current files for backup struct dfrec { // disk file record ch *file; // directory/filename double size; // byte count double mtime; // mod time int stat; // fstat() status int inclx; // include rec for this file ch disp; // status: new modified unchanged ch ivf; // flag for incr. verify }; dfrec Drec[maxfs]; // disk file data records int Dnf; // actual file count < maxfs double Dbytes; // disk files, total bytes double Dbytes2; // bytes for DVD/BRD medium // DVD/BRD file data struct vfrec { // DVD/BRD file record ch *file; // directory/file (- /media/xxx) double size; // byte count double mtime; // mod time int stat; // fstat() status ch disp; // status: deleted modified unchanged }; vfrec Vrec[maxfs]; // DVD/BRD file data records int Vnf; // actual file count < maxfs double Vbytes; // DVD/BRD files, total bytes // disk:DVD/BRD comparison data int nnew, ndel, nmod, nunc; // counts: new del mod unch int Mfiles; // new + mod + del file count double Mbytes; // new + mod files, total bytes // restore job data ch *RJinex[maxnx]; // file restore include/exclude recs. int RJnx; // actual RJinex count < maxnx int RJval; // restore job data validated ch RJfrom[XFCC]; // restore copy-from: /home/.../ ch RJto[XFCC]; // restore copy-to: /home/.../ struct rfrec { // restore file record ch *file; // restore filespec: /home/.../file.ext double size; // byte count }; rfrec Rrec[maxfs]; // restore file data records int Rnf; // actual file count < maxfs double Rbytes; // total bytes // dkopp local functions int initfunc(void *data); // GTK init function void buttonfunc(GtkWidget *item, ch *menu); // process toolbar button event void menufunc(GtkWidget *item, ch *menu); // process menu select event void script_func(void *); // execute script file int quit_dkopp(ch *); // exit application int clearScreen(ch *); // clear logging window int signalFunc(ch *); // kill/pause/resume curr. function int checkKillPause(); // test flags: killFlag and pauseFlag int fileOpen(ch *); // file open dialog int fileSave(ch *); // file save dialog int BJload(ch *fspec); // backup job data <<< file int BJstore(ch *fspec); // backup job data >>> file int BJvload(ch *); // load job file from DVD/BRD int BJedit(ch *); // backup job edit dialog int BJedit_event(zdialog *zd, ch *event); // dialog event function int BJedit_stuff(zdialog * zd); // stuff dialog widgets with job data int BJedit_fetch(zdialog * zd); // set job data from dialog widgets int Backup(ch *); // backup menu function int FullBackup(ch *vmode); // full backup + verify int IncrBackup(ch *bmode, ch *vmode); // incremental / accumulate + verify int Verify(ch *); // verify functions int Report(ch *); // report functions int get_current_files(ch *); // file stats. per include/exclude int report_summary_diffs(ch *); // disk:DVD/BRD differences summary int report_directory_diffs(ch *); // disk:DVD/BRD differences by directory int report_file_diffs(ch *); // disk:DVD/BRD differences by file int list_current_files(ch *); // list all disk files for backup int list_DVD_files(ch *); // list all files on DVD/BRD int find_files(ch *); // find files on disk, DVD/BRD, hist int view_backup_hist(ch *); // view backup history files int RJedit(ch *); // restore job edit dialog int RJedit_event(zdialog*, ch *event); // RJedit response int RJlist(ch *); // list DVD/BRD files to be restored int Restore(ch *); // file restore function int getDVDs(void *); // get avail. DVD/BRD's, mount points int setDVDdevice(ch *); // set DVD/BRD device and mount point int setDVDlabel(ch *); // set new DVD/BRD label int mountDVD(ch *); // mount DVD/BRD, echo outputs, status int unmountDVD(ch *); // unmount DVD/BRD + echo outputs int ejectDVD(ch *); // eject DVD/BRD + echo outputs int resetDVD(ch *); // hardware reset int eraseDVD(ch *); // fill DVD/BRD with zeros (long time) int formatDVD(ch *); // quick format DVD/BRD int helpFunc(ch *); // help function int fc_dialog(ch *dirk); // file chooser dialog int fc_response(GtkDialog *, int, void *); // fc_dialog response int writeDT(); // write date-time to temp file int save_filepoop(); // save file owner & permissions data int restore_filepoop(); // restore file owner & perm. data int createBackupHist(); // create backup history file int inexParse(ch *rec, ch *&rtype, ch *&fspec); // parse include/exclude record int BJvalidate(ch *); // validate backup job data int RJvalidate(); // validate restore job data int nxValidate(ch **recs, int nr); // validate include/exclude recs int dGetFiles(); // generate file list from job int vGetFiles(); // find all DVD/BRD files int rGetFiles(); // generate restore job file list int setFileDisps(); // set file disps: new del mod unch int SortFileList(ch *recs, int RL, int NR, ch sort); // sort file list in memory int filecomp(ch *file1, ch *file2); // compare directories before files int BJreset(); // reset backup job file data int RJreset(); // reset restore job data int dFilesReset(); // reset disk file data and free memory int vFilesReset(); // reset DVD/BRD file data, free memory int rFilesReset(); // reset restore file data, free memory ch *checkFile(ch *dfile, int compf, double &tcc); // validate file on BRD/DVD medium ch *copyfile(ch *vfile, ch *dfile); // copy file from DVD/BRD to disk int track_filespec(ch *filespec); // track filespec on screen, no scroll int track_filespec_err(ch *filespec, ch *errmess); // error logger for track_filespec() ch *kleenex(ch *name); // clean exotic file names for output int do_shell(ch *pname, ch *command); // shell command + output to window int track_growisofs_files(ch *buff); // convert %done to filespec, output // dkopp menu table struct menuent { ch menu1[20], menu2[40]; // top-menu, sub-menu int lock; // lock funcs: no run parallel int (*mfunc)(ch *); // processing function }; #define nmenu 43 struct menuent menus[nmenu] = { // top-menu sub-menu lock menu-function { "button", "edit job", 1, BJedit }, { "button", "clear", 0, clearScreen }, { "button", "run job", 1, Backup }, { "button", "run DVD/BRD", 1, Backup }, { "button", "pause", 0, signalFunc }, { "button", "resume", 0, signalFunc }, { "button", "kill job", 0, signalFunc }, { "button", "quit", 0, quit_dkopp }, { "File", "open job", 1, fileOpen }, { "File", "open DVD/BRD", 1, BJvload }, { "File", "edit job", 1, BJedit }, { "File", "show job", 0, BJvalidate }, { "File", "save job", 0, fileSave }, { "File", "run job", 1, Backup }, { "File", "run DVD/BRD", 1, Backup }, { "File", "quit", 0, quit_dkopp }, { "Backup", "full", 1, Backup }, { "Backup", "incremental", 1, Backup }, { "Backup", "accumulate", 1, Backup }, { "Verify", "full", 1, Verify }, { "Verify", "incremental", 1, Verify }, { "Verify", "thorough", 1, Verify }, { "Report", "get files for backup", 1, Report }, { "Report", "diffs summary", 1, Report }, { "Report", "diffs by directory", 1, Report }, { "Report", "diffs by file", 1, Report }, { "Report", "list files for backup", 1, Report }, { "Report", "list DVD/BRD files", 1, Report }, { "Report", "find files", 1, Report }, { "Report", "view backup hist", 1, Report }, { "Restore", "setup DVD/BRD restore", 1, RJedit }, { "Restore", "list restore files", 1, RJlist }, { "Restore", "restore files", 1, Restore }, { "DVD/BRD", "set DVD/BRD device", 1, setDVDdevice }, { "DVD/BRD", "set DVD/BRD label", 1, setDVDlabel }, { "DVD/BRD", "erase DVD/BRD", 1, eraseDVD }, { "DVD/BRD", "format DVD/BRD", 1, formatDVD }, { "DVD/BRD", "mount DVD/BRD", 1, mountDVD }, { "DVD/BRD", "unmount DVD/BRD", 1, unmountDVD }, { "DVD/BRD", "eject DVD/BRD", 1, ejectDVD }, { "DVD/BRD", "reset DVD/BRD", 0, resetDVD }, { "Help", "about", 0, helpFunc }, { "Help", "user guide", 0, helpFunc } }; // dkopp main program int main(int argc, ch *argv[]) { GtkWidget *mbar; // menubar GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; GtkWidget *mDVD, *mHelp; int ii; zinitapp(release,argc,argv); // get install directories Fgui = 1; // assume GUI clrun = 0; // no command line run command *scrFile = 0; // no script file *BJfile = 0; // no backup job file for (ii = 1; ii < argc; ii++) // get command line options { if (strmatch(argv[ii],"-nogui")) Fgui = 0; // command line operation else if (strmatch(argv[ii],"-job") && argc > ii+1) // -job jobfile (load job) strcpy(BJfile,argv[++ii]); else if (strmatch(argv[ii],"-run") && argc > ii+1) // -run jobfile (load and run job) { strcpy(BJfile,argv[++ii]); clrun++; } else if (strmatch(argv[ii],"-script") && argc > ii+1) // -script scriptfile (execute script) strcpy(scrFile,argv[++ii]); else strcpy(BJfile,argv[ii]); // assume a job file and load it } if (! Fgui) // no GUI { mLog = mWin = 0; // output goes to STDOUT initfunc(0); // start job or script unmountDVD(0); // unmount DVD/BRD ejectDVD(0); // eject DVD/BRD (may not work) return 0; // exit } mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window zfuncs::mainwin = mWin; gtk_window_set_title(GTK_WINDOW(mWin),release); gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(mWin),800,500); mVbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window mScroll = gtk_scrolled_window_new(0,0); // scrolled window gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox mLog = gtk_text_view_new(); // text edit window gtk_text_view_set_left_margin(GTK_TEXT_VIEW(mLog),2); gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window logBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); // get related text buffer gtk_text_buffer_set_text(logBuff,"", -1); mbar = create_menubar(mVbox); // create menu bar and menus mFile = add_menubar_item(mbar,"File",menufunc); add_submenu_item(mFile,"open job",menufunc); add_submenu_item(mFile,"open DVD/BRD",menufunc); add_submenu_item(mFile,"edit job",menufunc); add_submenu_item(mFile,"show job",menufunc); add_submenu_item(mFile,"save job",menufunc); add_submenu_item(mFile,"run job",menufunc); add_submenu_item(mFile,"run DVD/BRD",menufunc); add_submenu_item(mFile,"quit",menufunc); mBackup = add_menubar_item(mbar,"Backup",menufunc); add_submenu_item(mBackup,"full",menufunc); add_submenu_item(mBackup,"incremental",menufunc); add_submenu_item(mBackup,"accumulate",menufunc); mVerify = add_menubar_item(mbar,"Verify",menufunc); add_submenu_item(mVerify,"full",menufunc); add_submenu_item(mVerify,"incremental",menufunc); add_submenu_item(mVerify,"thorough",menufunc); mReport = add_menubar_item(mbar,"Report",menufunc); add_submenu_item(mReport,"get files for backup",menufunc); add_submenu_item(mReport,"diffs summary",menufunc); add_submenu_item(mReport,"diffs by directory",menufunc); add_submenu_item(mReport,"diffs by file",menufunc); add_submenu_item(mReport,"list files for backup",menufunc); add_submenu_item(mReport,"list DVD/BRD files",menufunc); add_submenu_item(mReport,"find files",menufunc); add_submenu_item(mReport,"view backup hist",menufunc); add_submenu_item(mReport,"save screen",menufunc); mRestore = add_menubar_item(mbar,"Restore",menufunc); add_submenu_item(mRestore,"setup DVD/BRD restore",menufunc); add_submenu_item(mRestore,"list restore files",menufunc); add_submenu_item(mRestore,"restore files",menufunc); mDVD = add_menubar_item(mbar,"DVD/BRD",menufunc); add_submenu_item(mDVD,"set DVD/BRD device",menufunc); add_submenu_item(mDVD,"set DVD/BRD label",menufunc); add_submenu_item(mDVD,"mount DVD/BRD",menufunc); add_submenu_item(mDVD,"unmount DVD/BRD",menufunc); add_submenu_item(mDVD,"eject DVD/BRD",menufunc); add_submenu_item(mDVD,"reset DVD/BRD",menufunc); add_submenu_item(mDVD,"erase DVD/BRD",menufunc); add_submenu_item(mDVD,"format DVD/BRD",menufunc); mHelp = add_menubar_item(mbar,"Help",menufunc); add_submenu_item(mHelp,"about",menufunc); add_submenu_item(mHelp,"user guide",menufunc); /*** buttons removed 8.2 GtkWidget *tbar = create_toolbar(mVbox,32); // create toolbar and buttons add_toolbar_button(tbar,"edit job","edit backup job","editjob.png",buttonfunc); add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc); add_toolbar_button(tbar,"run DVD/BRD","run job on DVD/BRD","run.png",buttonfunc); add_toolbar_button(tbar,"pause","pause running job","media-pause.png",buttonfunc); add_toolbar_button(tbar,"resume","resume running job","media-play.png",buttonfunc); add_toolbar_button(tbar,"kill job","kill running job","kill.png",buttonfunc); add_toolbar_button(tbar,"clear","clear screen","clear.png",buttonfunc); add_toolbar_button(tbar,"quit","quit dkopp","quit.png",buttonfunc); ***/ gtk_widget_show_all(mWin); // show all widgets G_SIGNAL(mWin,"destroy",quit_dkopp,0); // connect window destroy event g_timeout_add(0,initfunc,0); // setup initial call from gtk_main() gtk_main(); // process window events return 0; } // initial function called from gtk_main() at startup int initfunc(void *) { int ii; ch *home; time_t datetime; strcpy(homedir,get_zhomedir()); // get temp file names snprintf(TFdiskfiles,200,"%s/diskfiles",homedir); snprintf(TFdvdfiles,200,"%s/dvdfiles",homedir); snprintf(TFfilepoop,200,"%s/filepoop",homedir); snprintf(TFjobfile,200,"%s/jobfile",homedir); snprintf(TFdatetime,200,"%s/datetime",homedir); snprintf(TFrestorefiles,200,"%s/restorefiles.sh",homedir); snprintf(TFrestoredirks,200,"%s/restoredirks.sh",homedir); datetime = time(0); printf("dkopp errlog %s \n",ctime(&datetime)); menuLock = Fdialog = 0; // initialize controls killFlag = pauseFlag = commFail = 0; strcpy(subprocName,""); strcpy(scriptParam,""); strcpy(BJdvd,"/dev/sr0"); // default DVD/BRD device strcpy(dvdmp,"/media/dkopp"); // default mount point dvdmpcc = strlen(dvdmp); // mount point cc strcpy(dvdlabel,"dkopp"); // default DVD/BRD label strcpy(BJbmode,"full"); // backup mode strcpy(BJvmode,"full"); // verify mode BJval = 0; // not validated BJmod = 0; // not modified strcpy(BJdatefrom,"1970.01.01"); // file age exclusion default BJtdate = 0; BJnx = 4; // backup job include/exclude recs for (ii = 0; ii < BJnx; ii++) BJinex[ii] = (ch *) zmalloc(50,"BJinex"); home = getenv("HOME"); // get "/home/username" if (! home) home = (ch *) "/home/xxx"; strcpy(BJinex[0],"# dkopp default backup job"); // initz. default backup specs 8.1 sprintf(BJinex[1],"include %s/*",home); // include /home/username/* sprintf(BJinex[2],"exclude *thumbnails*"); // exclude thumbnails sprintf(BJinex[2],"exclude *.cache*"); // exclude cache Dnf = Vnf = Rnf = Mfiles = 0; // file counts = 0 Dbytes = Dbytes2 = Vbytes = Mbytes = 0.0; // byte counts = 0 strcpy(RJfrom,"/home/"); // file restore copy-from location strcpy(RJto,"/home/"); // file restore copy-to location RJnx = 0; // no. restore include/exclude recs RJval = 0; // restore job not validated strcpy(mediumDT,"unknown"); // DVD/BRD medium last backup date-time dvdtime = -1; // DVD/BRD device mod time dvdmtd = 0; // DVD/BRD not mounted if (*BJfile) { // command line job file BJload(BJfile); if (commFail) return 0; } if (clrun) { // command line run command menufunc(null,"File"); menufunc(null,"run job"); } if (*scrFile) script_func(0); // command line script file txwidget_append2(mLog,0,"\n Searching for DVD/BRD devices ... \n"); g_timeout_add(1000,getDVDs,0); // blocks GTK until done return 0; } // process toolbar button events (simulate menu selection) void buttonfunc(GtkWidget *item, ch *button) { ch button2[20], *pp; strncpy0(button2,button,19); pp = strchr(button2,'\n'); // replace \n with blank if (pp) *pp = ' '; menufunc(item,"button"); // use menu function for button menufunc(item,button2); return; } // process menu selection event void menufunc(GtkWidget *, ch *menu) { static int ii; static ch menu1[20] = "", menu2[40] = ""; int kk; for (ii = 0; ii < nmenu; ii++) if (strmatch(menu,menus[ii].menu1)) break; // mark top-menu selection if (ii < nmenu) { strcpy(menu1,menu); return; } for (ii = 0; ii < nmenu; ii++) if (strmatch(menu1,menus[ii].menu1) && strmatch(menu,menus[ii].menu2)) break; // mark sub-menu selection if (ii < nmenu) strcpy(menu2,menu); else { // no match to menus txwidget_append2(mLog,0," *** bad command: %s \n",menu); commFail++; return; } if (menuLock && menus[ii].lock) { // no lock funcs can run parallel if (Fgui) zmessageACK(mWin,"wait for current function to complete"); return; } if (! menuLock) { killFlag = pauseFlag = 0; // reset controls *subprocName = 0; commFail = 0; // start with no errors } if (! *scrFile) // if not a script file, txwidget_append2(mLog,1,"\n""command: %s > %s \n",menu1,menu2); // echo command to window kk = ii; // move to non-static memory if (menus[kk].lock) ++menuLock; // call menu function menus[kk].mfunc(menu2); if (menus[kk].lock) --menuLock; return; } // function to execute menu commands from a script file void script_func(void *) { FILE *fid; int cc, Nth; ch buff[200], menu1[20], menu2[40]; ch *pp; ch *bb; fid = fopen(scrFile,"r"); // open file if (! fid) { txwidget_append2(mLog,0," *** can't open script file: %s \n",scrFile); commFail++; *scrFile = 0; return; } while (true) { if (checkKillPause()) break; // exit script if (commFail) break; pp = fgets_trim(buff,199,fid,1); // read next record if (! pp) break; // EOF txwidget_append2(mLog,0,"\n""Script: %s \n",buff); // write to log bb = strchr(buff,'#'); // get rid of comments if (bb) *bb = 0; cc = strTrim(buff); // and trailing blanks if (cc < 2) continue; *menu1 = *menu2 = 0; *scriptParam = 0; Nth = 1; // parse menu1 > menu2 > parameter pp = substring(buff,'>',Nth++); if (pp) strncpy0(menu1,pp,20); pp = substring(buff,'>',Nth++); if (pp) strncpy0(menu2,pp,40); pp = substring(buff,'>',Nth++); if (pp) strncpy0(scriptParam,pp,200); strTrim(menu1); // get rid of trailing blanks strTrim(menu2); if (strmatch(menu1,"exit")) break; menufunc(null,menu1); // simulate menu entries menufunc(null,menu2); while (Fdialog) sleep(1); // if dialog, wait for compl. } txwidget_append2(mLog,0,"script exiting \n"); fclose(fid); *scrFile = 0; return; } // quit dkopp, with last chance to save edits to backup job data int quit_dkopp(ch *menu) { int yn; if (! Fgui) return 0; signalFunc("kill job"); if (BJmod) { // job data was modified yn = zmessageYN(mWin,"SAVE changes to dkopp job?"); // give user a chance to save mods if (yn) fileSave(null); } if (dvdmtd) { unmountDVD(0); // unmount DVD/BRD ejectDVD(0); // eject DVD/BRD (may not work) } gtk_main_quit(); // tell gtk_main() to quit return 0; } // clear logging window int clearScreen(ch *menu) { txwidget_clear(mLog); return 0; } // kill/pause/resume current function - called from menu function int signalFunc(ch *menu) { if (strmatch(menu,"kill job")) { if (! menuLock) { txwidget_append2(mLog,0,"\n""ready \n"); // already dead return 0; } if (killFlag) { // redundant kill if (*subprocName) { txwidget_append2(mLog,0," *** kill again: %s \n",subprocName); signalProc(subprocName,"kill"); // kill subprocess } else txwidget_append2(mLog,0," *** waiting for function exit \n"); // or wait for function to die return 0; } txwidget_append2(mLog,0," *** KILL current function \n"); // initial kill pauseFlag = 0; killFlag = 1; if (*subprocName) { signalProc(subprocName,"resume"); signalProc(subprocName,"kill"); } return 0; } if (strmatch(menu,"pause")) { pauseFlag = 1; if (*subprocName) signalProc(subprocName,"pause"); return 0; } if (strmatch(menu,"resume")) { pauseFlag = 0; if (*subprocName) signalProc(subprocName,"resume"); return 0; } else zappcrash("signalFunc: %s",menu); return 0; } // check kill and pause flags // called periodically from long-running functions int checkKillPause() { while (pauseFlag) { // idle loop while paused zsleep(0.1); zmainloop(); // process menus } if (killFlag) return 1; // return true = stop now return 0; // return false = continue } // file open dialog - get backup job data from a file int fileOpen(ch *menu) { ch *file; int err = 0; if (*scriptParam) { // get file from script strcpy(BJfile,scriptParam); *scriptParam = 0; err = BJload(BJfile); return err; } ++Fdialog; file = zgetfile("open backup job",MWIN,"file",homedir,1); // get file from user if (file) { if (strlen(file) > XFCC-2) zappcrash("pathname too big"); strcpy(BJfile,file); zfree(file); err = BJload(BJfile); // get job data from file } else err = 1; --Fdialog; return err; } // file save dialog - save backup job data to a file int fileSave(ch *menu) { ch *file; int nstat, err = 0; if (*scriptParam) { // get file from script strcpy(BJfile,scriptParam); *scriptParam = 0; BJstore(BJfile); return 0; } if (! BJval) { nstat = zmessageYN(mWin,"Job data not valid, save anyway?"); if (! nstat) return 0; } ++Fdialog; if (! *BJfile) strcpy(BJfile,"dkopp.job"); // if no job file, use default file = zgetfile("save backup job",MWIN,"save",BJfile,1); if (file) { if (strlen(file) > XFCC-2) zappcrash("pathname too big"); strcpy(BJfile,file); zfree(file); err = BJstore(BJfile); if (! err) BJmod = 0; // job not modified } --Fdialog; return 0; } // backup job data <<< file // errors not checked here are checked in BJvalidate() int BJload(ch *fspec) { FILE *fid; ch buff[1000]; ch *fgs, *rtype, *rdata; ch rtype2[20]; int cc, Nth, nerrs; BJreset(); // clear old job from memory nerrs = 0; txwidget_append2(mLog,1,"\n""loading job file: %s \n",fspec); fid = fopen(fspec,"r"); // open file if (! fid) { txwidget_append2(mLog,0," *** cannot open job file: %s \n",fspec); commFail++; return 1; } while (true) // read file { fgs = fgets_trim(buff,998,fid,1); if (! fgs) break; // EOF cc = strlen(buff); if (cc > 996) { txwidget_append2(mLog,0," *** input record too big \n"); nerrs++; continue; } Nth = 1; rtype = substring(buff,' ',Nth++); // parse 1st field, record type if (! rtype) rtype = "#"; // blank record is comment strncpy0(rtype2,rtype,19); strToLower(rtype2); if (strmatch(rtype2,"device")) { rdata = substring(buff,' ',Nth++); // DVD/BRD device: /dev/dvd if (rdata) strncpy0(BJdvd,rdata,19); continue; } if (strmatch(rtype2,"backup")) { rdata = substring(buff,' ',Nth++); // backup mode if (rdata) { strncpy0(BJbmode,rdata,19); strToLower(BJbmode); } continue; } if (strmatch(rtype2,"verify")) { rdata = substring(buff,' ',Nth++); // verify mode if (rdata) { strncpy0(BJvmode,rdata,19); strToLower(BJvmode); } continue; } if (strmatch(rtype2,"datefrom")) { rdata = substring(buff,' ',Nth++); // file mod date selection if (rdata) strncpy0(BJdatefrom,rdata,11); continue; } if (strmatchV(rtype2,"include","exclude","#",null)) { BJinex[BJnx] = zstrdup(buff,"BJinex"); // include/exclude or comment rec. if (++BJnx >= maxnx) { txwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx); nerrs++; break; } continue; } txwidget_append2(mLog,0," *** unrecognized record: %s \n",buff); continue; } fclose(fid); // close file BJmod = 0; // new job, not modified BJvalidate(0); // validation checks, set BJval if (! nerrs && BJval) return 0; BJval = 0; commFail++; return 1; } // backup job data >>> file int BJstore(ch *fspec) { FILE *fid; int ii; fid = fopen(fspec,"w"); // open file if (! fid) { txwidget_append2(mLog,0," *** cannot open file: %s \n",fspec); commFail++; return 1; } fprintf(fid,"device %s \n",BJdvd); // device /dev/dvd fprintf(fid,"backup %s \n",BJbmode); // backup full/incremental/accumulate fprintf(fid,"verify %s \n",BJvmode); // verify full/incremental/thorough fprintf(fid,"datefrom %s \n",BJdatefrom); // file mod date selection for (ii = 0; ii < BJnx; ii++) // output all include/exclude recs fprintf(fid,"%s \n",BJinex[ii]); fclose(fid); return 0; } // backup job data <<< DVD/BRD job file // get job file from prior backup to this same medium int BJvload(ch *menu) { ch vjfile[100]; BJreset(); // reset job data mountDVD(0); // (re) mount DVD/BRD if (! dvdmtd) { commFail++; return 1; } strcpy(vjfile,dvdmp); // dvd mount point strcat(vjfile,V_JOBFILE); // + dvd job file BJload(vjfile); // load job file (BJval set) if (BJval) return 0; commFail++; return 1; } // edit dialog for backup job data int BJedit(ch *menu) { zdialog *zd; ++Fdialog; zd = zdialog_new("edit backup job",mWin,"browse","done","clear","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); zdialog_add_widget(zd,"label","labdev","hb1","DVD/BRD device","space=3"); // DVD/BRD device [______________][v] zdialog_add_widget(zd,"combo","entdvd","hb1",BJdvd); zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=8"); zdialog_add_widget(zd,"button","bopen","hb2","open job file"); // [open job] [DVD/BRD job] [save as] zdialog_add_widget(zd,"button","bdvd","hb2","open DVD/BRD job"); zdialog_add_widget(zd,"button","bsave","hb2"," save as "); zdialog_add_widget(zd,"hbox","hb3","dialog"); zdialog_add_widget(zd,"vbox","vb3","hb3",0,"homog"); zdialog_add_widget(zd,"label","space","hb3",0,"space=20"); zdialog_add_widget(zd,"vbox","vb4","hb3",0,"homog"); zdialog_add_widget(zd,"label","labbmode","vb3","Backup Mode"); zdialog_add_widget(zd,"label","labvmode","vb4","Verify Mode"); zdialog_add_widget(zd,"radio","bmrb1","vb3","full"); // Backup Mode Verify Mode zdialog_add_widget(zd,"radio","bmrb2","vb3","incremental"); // (o) full (o) full zdialog_add_widget(zd,"radio","bmrb3","vb3","accumulate"); // (o) incremental (o) incremental zdialog_add_widget(zd,"radio","vmrb1","vb4","full"); // (o) accumulate (o) thorough zdialog_add_widget(zd,"radio","vmrb2","vb4","incremental"); // file date from: [ yyyy.mm.dd ] zdialog_add_widget(zd,"radio","vmrb3","vb4","thorough"); zdialog_add_widget(zd,"label","labdate","vb3","file date from:"); zdialog_add_widget(zd,"zentry","entdate","vb4","yyyy.mm.dd","size=10"); zdialog_add_widget(zd,"hsep","sep2","dialog",0,"space=8"); // edit box for include/exclude recs zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude Files"); zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand"); zdialog_add_widget(zd,"scrwin","scrwinex","frminex"); zdialog_add_widget(zd,"zedit","edinex","scrwinex"); BJedit_stuff(zd); // stuff dialog widgets with job data zdialog_resize(zd,0,600); zdialog_run(zd,BJedit_event,"parent"); // run dialog return 0; } // edit dialog event function int BJedit_event(zdialog *zd, ch *event) { int zstat, err = 0; zstat = zd->zstat; zd->zstat = 0; // dialog may continue if (zstat) { if (zstat == 1) { // browse, do file-chooser dialog fc_dialog("/home"); return 0; } if (zstat == 2) { // done BJedit_fetch(zd); // get all job data from dialog widgets if (! BJval) commFail++; zdialog_free(zd); // destroy dialog --Fdialog; return 0; } if (zstat == 3) { txwidget_clear(editwidget); // clear include/exclude recs return 0; } zdialog_free(zd); // cancel --Fdialog; return 0; } if (strmatch(event,"bopen")) { err = fileOpen(""); // get job file from user if (! err) BJedit_stuff(zd); // stuff dialog widgets } if (strmatch(event,"bdvd")) { err = BJvload(""); // get job file on DVD/BRD if (! err) BJedit_stuff(zd); // stuff dialog widgets } if (strmatch(event,"bsave")) { BJedit_fetch(zd); // get job data from dialog widgets fileSave(""); // save to file } return 0; } // backup job data in memory >>> job edit dialog widgets int BJedit_stuff(zdialog * zd) { int ii; for (ii = 0; ii < ndvds; ii++) // DVD/BRD drives available zdialog_stuff(zd,"entdvd",dvddevdesc[ii]); // remove mount point get/stuff if (strmatch(BJbmode,"full")) zdialog_stuff(zd,"bmrb1",1); if (strmatch(BJbmode,"incremental")) zdialog_stuff(zd,"bmrb2",1); if (strmatch(BJbmode,"accumulate")) zdialog_stuff(zd,"bmrb3",1); if (strmatch(BJvmode,"full")) zdialog_stuff(zd,"vmrb1",1); if (strmatch(BJvmode,"incremental")) zdialog_stuff(zd,"vmrb2",1); if (strmatch(BJvmode,"thorough")) zdialog_stuff(zd,"vmrb3",1); zdialog_stuff(zd,"entdate",BJdatefrom); // file mod date selection editwidget = zdialog_gtkwidget(zd,"edinex"); txwidget_clear(editwidget); for (int ii = 0; ii < BJnx; ii++) txwidget_append2(editwidget,0,"%s""\n",BJinex[ii]); return 0; } // job edit dialog widgets >>> backup job data in memory int BJedit_fetch(zdialog * zd) { int ii, line; ch text[40], *pp; BJreset(); // reset job data zdialog_fetch(zd,"entdvd",text,19); // get DVD/BRD device strncpy0(BJdvd,text,19); pp = strchr(BJdvd,' '); if (pp) *pp = 0; // remove mount point fetch/save zdialog_fetch(zd,"bmrb1",ii); if (ii) strcpy(BJbmode,"full"); // backup mode zdialog_fetch(zd,"bmrb2",ii); if (ii) strcpy(BJbmode,"incremental"); zdialog_fetch(zd,"bmrb3",ii); if (ii) strcpy(BJbmode,"accumulate"); zdialog_fetch(zd,"vmrb1",ii); if (ii) strcpy(BJvmode,"full"); // verify mode zdialog_fetch(zd,"vmrb2",ii); if (ii) strcpy(BJvmode,"incremental"); zdialog_fetch(zd,"vmrb3",ii); if (ii) strcpy(BJvmode,"thorough"); zdialog_fetch(zd,"entdate",BJdatefrom,11); // file mod date selection for (line = 0; ; line++) { pp = txwidget_line(editwidget,line,1); // include/exclude recs. if (! pp || ! *pp) break; strTrim(pp); // remove trailing blanks BJinex[BJnx] = zstrdup(pp,"BJinex"); // copy new record if (++BJnx >= maxnx) { txwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx); break; } } BJmod++; // job modified BJvalidate(0); // check for errors, set BJval return 0; } // perform DVD/BRD backup using growisofs utility int Backup(ch *menu) { strcpy(mbmode,""); strcpy(mvmode,""); if (strmatchV(menu,"full","incremental","accumulate",null)) // backup only strcpy(mbmode,menu); if (strmatch(menu,"run DVD/BRD")) BJvload(null); // load job file from DVD/BRD if req. if (strmatchV(menu,"run job","run DVD/BRD",null)) { // if run job or job on DVD/BRD, if (BJval) { // and valid job file, strcpy(mbmode,BJbmode); // use job file backup & verify modes strcpy(mvmode,BJvmode); } } if (! BJval) { // check for errors txwidget_append2(mLog,0," *** no valid backup job \n"); goto backup_done; } if (strmatch(mbmode,"full")) FullBackup(mvmode); // full backup (+ verify) else IncrBackup(mbmode,mvmode); // incremental / accumulate (+ verify) backup_done: if (Fgui) txwidget_append2(mLog,0,"ready \n"); return 0; } // full backup using multiple DVD/BRD media if required int FullBackup(ch *BJvmode) { FILE *fid = 0; int gerr, ii, zstat; ch command[200], Nspeed[20] = ""; ch *dfile, vfile[XFCC]; double secs, bspeed, time0; dGetFiles(); // get files for backup if (Dnf == 0) { txwidget_append2(mLog,0," *** nothing to back-up \n"); goto backup_fail; } vFilesReset(); // reset DVD/BRD files data txwidget_append2(mLog,1,"\n""begin full backup \n"); txwidget_append2(mLog,0," files: %d bytes: %.0f \n",Dnf, // files and bytes to copy formatKBMB(Dbytes,3)); if (! *dvdlabel) strcpy(dvdlabel,"dkopp"); // if no label, default "dkopp" BJstore(TFjobfile); // copy job file (DVD/BRD) to temp file save_filepoop(); // + owner and permissions to temp file writeDT(); // create date-time & usage temp file fid = fopen(TFdiskfiles,"w"); // temp file for growisofs path-list if (! fid) { txwidget_append2(mLog,0," *** cannot open /tmp scratch file \n"); goto backup_fail; } fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile); // add job file to growisofs list fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop); // add directory poop file fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime); // add date-time file Dbytes2 = 0.0; for (ii = 0; ii < Dnf; ii++) // process all files for backup { dfile = Drec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,XFCC,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,dfile); // directories/file=/directories/file Dbytes2 += Drec[ii].size; } fclose(fid); txwidget_append2(mLog,0," writing DVD/BRD, %s \n",formatKBMB(Dbytes,3)); start_timer(time0); // start timer for growisofs snprintf(command,200, // build growisofs command line "growisofs -Z %s %s -r -graft-points " "-iso-level 4 -gui -V \"%s\" %s -path-list %s 2>&1", // label in quotes BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles); backup_retry: gerr = do_shell("growisofs", command); // do growisofs, echo outputs if (checkKillPause()) goto backup_fail; // killed by user if (gerr) { if (! Fgui) goto backup_fail; zstat = zdialog_choose(mWin,"parent","growisofs error", // manual compensation for growisofs "abort","retry","ignore (continue)",null); // and/or gnome bugs if (zstat == 1) goto backup_fail; if (zstat == 2) goto backup_retry; } secs = get_timer(time0); // output statistics txwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; txwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); txwidget_append2(mLog,0," backup complete \n"); ejectDVD(0); // DVD may be hung after growisofs verify_retry: if (*BJvmode) // do verify if requested { mountDVD(0); // test if DVD hung if (! dvdmtd) { zstat = zdialog_choose(mWin,"parent","DVD mount failure", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } Verify(BJvmode); // verify this DVD if (commFail) { zstat = zdialog_choose(mWin,"parent","verify error", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; txwidget_append2(mLog,0," backup is being repeated \n"); commFail = 0; } } createBackupHist(); // create backup history file txwidget_append2(mLog,0," backup job complete \n"); ejectDVD(0); return 0; backup_fail: commFail++; secs = get_timer(time0); // output stats even if failed txwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; txwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); txwidget_append2(mLog,1," *** BACKUP FAILED \n"); txwidget_append2(mLog,0," media may be OK: check with Verify \n"); ejectDVD(0); return 0; } // incremental / accumulate backup (one DVD/BRD only) int IncrBackup(ch *BJbmode, ch *BJvmode) { FILE *fid = 0; int gerr, ii, zstat; ch command[200], Nspeed[20] = ""; ch *dfile, vfile[XFCC], disp; double secs, bspeed; double time0; mountDVD(0); // requires successful mount if (! dvdmtd) goto backup_fail; dGetFiles(); // get files for backup vGetFiles(); // get DVD/BRD files setFileDisps(); // file disps: new mod del unch if (! Dnf) { txwidget_append2(mLog,0," *** no files for backup \n"); goto backup_fail; } if (! Vnf) { txwidget_append2(mLog,0," *** no DVD/BRD files \n"); goto backup_fail; } txwidget_append2(mLog,1,"\n""begin %s backup \n",BJbmode); txwidget_append2(mLog,0," files: %d bytes: %s \n",Mfiles, // files and bytes to copy formatKBMB(Mbytes,3)); if (Mfiles == 0) { // nothing to back up txwidget_append2(mLog,0," nothing to back-up \n"); return 0; } if (! *dvdlabel) strcpy(dvdlabel,"dkopp"); // if no label, default "dkopp" fid = fopen(TFdiskfiles,"w"); // temp file for growisofs path-list if (! fid) { txwidget_append2(mLog,0," *** cannot open /tmp scratch file \n"); goto backup_fail; } BJstore(TFjobfile); // copy job file to temp file save_filepoop(); // + file owner & permissions writeDT(); // create date-time & usage temp file fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile); // add job file to growisofs list fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop); // add directory poop file fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime); // add date-time file for (ii = 0; ii < Dnf; ii++) { // process new and modified disk files disp = Drec[ii].disp; if ((disp == 'n') || (disp == 'm')) { // new or modified file dfile = Drec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,XFCC,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,dfile); // directories/file=/directories/file Drec[ii].ivf = 1; // set flag for incr. verify } } if (strmatch(BJbmode,"incremental")) { // incremental backup (not accumulate) for (ii = 0; ii < Vnf; ii++) { // process deleted files still on DVD/BRD if (Vrec[ii].disp == 'd') { dfile = Vrec[ii].file; // add to growisofs path-list repl_1str(dfile,vfile,XFCC,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,"/dev/null"); // directories/file=/dev/null } } } fclose(fid); start_timer(time0); // start timer for growisofs snprintf(command,200,"growisofs -M %s %s -r -graft-points " // build growisofs command line "-iso-level 4 -gui -V %s %s -path-list %s 2>&1", BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles); backup_retry: gerr = do_shell("growisofs", command); // do growisofs, echo outputs if (checkKillPause()) goto backup_fail; // killed by user if (gerr) { zstat = zdialog_choose(mWin,"parent","growisofs error", // manual compensation for growisofs "abort","retry","ignore (continue)",null); // and/or gnome bugs if (zstat == 1) goto backup_fail; if (zstat == 2) goto backup_retry; } secs = get_timer(time0); // output statistics txwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Mbytes/1000000.0/secs; txwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); txwidget_append2(mLog,0," backup complete \n"); vFilesReset(); // reset DVD/BRD files ejectDVD(0); // DVD may be hung after growisofs sleep(5); verify_retry: if (*BJvmode) // do verify if requested { mountDVD(0); // test if DVD/BRD hung if (! dvdmtd) { zstat = zdialog_choose(mWin,"parent","DVD mount failure", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } Verify(BJvmode); // verify new files on DVD/BRD if (commFail) { zstat = zdialog_choose(mWin,"parent","verify error", "abort","retry","ignore (continue)",null); if (zstat == 1) goto backup_fail; if (zstat == 2) goto verify_retry; } } createBackupHist(); // create backup history file ejectDVD(0); return 0; backup_fail: commFail++; txwidget_append2(mLog,1," *** BACKUP FAILED \n"); vFilesReset(); ejectDVD(0); return 0; } // verify DVD/BRD disc data integrity int Verify(ch *menu) { int ii, comp, vfiles; int dfiles1 = 0, dfiles2 = 0; int verrs = 0, cerrs = 0; ch *filespec; ch *errmess = 0; double secs, dcc1, vbytes, vspeed; double mtime, diff; double time0; STATB filestat; vGetFiles(); // get DVD/BRD files txwidget_append2(mLog,0," %d files on DVD/BRD \n",Vnf); if (! Vnf) goto verify_exit; vfiles = verrs = cerrs = 0; vbytes = 0.0; start_timer(time0); if (strmatch(menu,"full")) // verify all files are readable { txwidget_append2(mLog,1,"\n""verify ALL files on DVD/BRD \n"); if (Fgui) txwidget_append2(mLog,0,"\n\n"); for (ii = 0; ii < Vnf; ii++) { if (checkKillPause()) goto verify_exit; filespec = Vrec[ii].file; // /home/.../file.ext track_filespec(filespec); // track progress on screen errmess = checkFile(filespec,0,dcc1); // check file, get length if (errmess) track_filespec_err(filespec,errmess); // log errors if (errmess) verrs++; vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { txwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } if (strmatch(menu,"incremental")) // verify files in prior incr. backup { txwidget_append2(mLog,1,"\n""verify files in prior incremental backup \n"); for (ii = 0; ii < Dnf; ii++) { if (checkKillPause()) goto verify_exit; if (! Drec[ii].ivf) continue; // skip if not in prior incr. backup filespec = Drec[ii].file; txwidget_append2(mLog,0," %s \n",kleenex(filespec)); // output filespec errmess = checkFile(filespec,0,dcc1); // check file on DVD/BRD, get length if (errmess) txwidget_append2(mLog,0," *** %s \n",errmess); if (errmess) verrs++; vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { txwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } if (strmatch(menu,"thorough")) // compare DVD/BRD to disk files { txwidget_append2(mLog,1,"\n Read and verify ALL files on DVD/BRD. \n"); txwidget_append2(mLog,0," Compare to disk files with matching names and mod times.\n"); if (Fgui) txwidget_append2(mLog,0,"\n\n"); for (ii = 0; ii < Vnf; ii++) // process DVD/BRD files { if (checkKillPause()) goto verify_exit; filespec = Vrec[ii].file; // corresp. file name on disk track_filespec(filespec); // track progress on screen comp = 0; if (stat(filespec,&filestat) == 0) { // disk file exists? mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // yes, get file mod time diff = fabs(mtime - Vrec[ii].mtime); // compare to DVD/BRD file mod time if (diff < modtimetolr) comp = 1; // equal dfiles1++; // count matching disk names dfiles2 += comp; // count matching names and mod times } errmess = checkFile(filespec,comp,dcc1); // check DVD/BRD file, compare to disk if (errmess) track_filespec_err(filespec,errmess); // log errors if (errmess) { if (strstr(errmess,"compare")) cerrs++; // file compare error else verrs++; } vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { txwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } txwidget_append2(mLog,0," DVD/BRD files: %d bytes: %s \n",vfiles,formatKBMB(vbytes,3)); txwidget_append2(mLog,0," DVD/BRD read errors: %d \n",verrs); if (strmatch(menu,"thorough")) { txwidget_append2(mLog,0," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); txwidget_append2(mLog,0," compare failures: %d \n",cerrs); } secs = get_timer(time0); txwidget_append2(mLog,0," verify time: %.0f secs \n",secs); vspeed = vbytes/1000000.0/secs; txwidget_append2(mLog,0," verify speed: %.2f MB/sec \n",vspeed); if (verrs + cerrs) txwidget_append2(mLog,1," *** THERE WERE ERRORS *** \n"); else txwidget_append2(mLog,0," NO ERRORS \n"); verify_exit: if (! Vnf) txwidget_append2(mLog,0," *** no files on DVD/BRD \n"); if (! Vnf) commFail++; if (verrs + cerrs) commFail++; if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // Reports menu function int Report(ch *menu) { if (strmatch(menu, "get files for backup")) get_current_files(0); if (strmatch(menu, "diffs summary")) report_summary_diffs(0); if (strmatch(menu, "diffs by directory")) report_directory_diffs(0); if (strmatch(menu, "diffs by file")) report_file_diffs(0); if (strmatch(menu, "list files for backup")) list_current_files(0); if (strmatch(menu, "list DVD/BRD files")) list_DVD_files(0); if (strmatch(menu, "find files")) find_files(0); if (strmatch(menu, "view backup hist")) view_backup_hist(0); return 0; } // refresh files for backup and report summary statistics per include/exclude statement int get_current_files(ch *menu) { ch *bytes; int ii; dFilesReset(); // force refresh dGetFiles(); // get disk files if (! BJval) { txwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } txwidget_append2(mLog,1,"\n files bytes include/exclude filespec \n"); for (ii = 0; ii < BJnx; ii++) { bytes = formatKBMB(BJbytes[ii],3); if (BJfiles[ii] > 0) txwidget_append2(mLog,0," %6d %9s", BJfiles[ii], bytes); if (BJfiles[ii] < 0) txwidget_append2(mLog,0," %6d %9s", BJfiles[ii], bytes); if (BJfiles[ii] == 0) txwidget_append2(mLog,0," "); txwidget_append2(mLog,0," %s \n",BJinex[ii]); } bytes = formatKBMB(Dbytes,3); txwidget_append2(mLog,0," %6d %9s TOTAL \n", Dnf, bytes); report_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences summary int report_summary_diffs(ch *menu) { ch *bytes; if (! BJval) { txwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); txwidget_append2(mLog,0,"\n disk files: %d DVD/BRD files: %d \n",Dnf,Vnf); txwidget_append2(mLog,0,"\n Differences between DVD/BRD and files on disk: \n"); txwidget_append2(mLog,0," %7d disk files not on DVD/BRD - new \n",nnew); txwidget_append2(mLog,0," %7d files on disk and DVD/BRD - unchanged \n",nunc); txwidget_append2(mLog,0," %7d files on disk and DVD/BRD - modified \n",nmod); txwidget_append2(mLog,0," %7d DVD/BRD files not on disk - deleted \n",ndel); bytes = formatKBMB(Mbytes,3); txwidget_append2(mLog,0," Total differences: %d files %s \n",nnew+ndel+nmod,bytes); report_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences by directory, summary statistics int report_directory_diffs(ch *menu) { int kfiles, knew, kdel, kmod; int dii, vii, comp; ch *pp, *pdirk, *bytes, ppdirk[XFCC]; double nbytes; if (! BJval) { txwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); SortFileList((ch *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first SortFileList((ch *) Vrec, sizeof(vfrec), Vnf, 'D'); txwidget_append2(mLog,0,"\n Disk:DVD/BRD differences by directory \n"); txwidget_append2(mLog,0," new mod del bytes directory \n"); nbytes = kfiles = knew = kmod = kdel = 0; dii = vii = 0; while ((dii < Dnf) || (vii < Vnf)) // scan disk and DVD/BRD files parallel { if ((dii < Dnf) && (vii == Vnf)) comp = -1; else if ((dii == Dnf) && (vii < Vnf)) comp = +1; else comp = filecomp(Drec[dii].file, Vrec[vii].file); if (comp > 0) pdirk = Vrec[vii].file; // get file on DVD/BRD or disk else pdirk = Drec[dii].file; pp = (ch *) strrchr(pdirk,'/'); // isolate directory if (pp) *pp = 0; if (! strmatch(pdirk,ppdirk)) { // if directory changed, output bytes = formatKBMB(nbytes,3); // totals from prior directory if (kfiles > 0) txwidget_append2(mLog,0," %5d %5d %5d %8s %s \n", knew,kmod,kdel,bytes,ppdirk); nbytes = kfiles = knew = kmod = kdel = 0; // reset totals strcpy(ppdirk,pdirk); // start new directory } if (pp) *pp = '/'; if (comp < 0) { // unmatched disk file knew++; // count new file nbytes += Drec[dii].size; kfiles++; dii++; } else if (comp > 0) { // unmatched DVD/BRD file: deleted kdel++; // count deleted file kfiles++; vii++; } else if (comp == 0) { // file present on disk and DVD/BRD if (Drec[dii].disp == 'm') { kmod++; // count modified file nbytes += Drec[dii].size; kfiles++; } dii++; // other: u = unchanged vii++; } } if (kfiles > 0) { bytes = formatKBMB(nbytes,3); // totals from last directory txwidget_append2(mLog,0," %5d %5d %5d %8s %s \n",knew,kmod,kdel, bytes,ppdirk); } SortFileList((ch *) Drec, sizeof(dfrec), Dnf, 'A'); // restore ascii sort SortFileList((ch *) Vrec, sizeof(vfrec), Vnf, 'A'); report_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences by file (new, modified, deleted) int report_file_diffs(ch *menu) { int dii, vii; if (! BJval) { txwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } report_summary_diffs(0); // report summary first txwidget_append2(mLog,0,"\n Detailed list of disk:DVD/BRD differences: \n"); txwidget_append2(mLog,0,"\n %d new files (on disk, not on DVD/BRD) \n",nnew); for (dii = 0; dii < Dnf; dii++) { if (Drec[dii].disp != 'n') continue; txwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } txwidget_append2(mLog,0,"\n %d modified files (disk and DVD/BRD files are different) \n",nmod); for (dii = 0; dii < Dnf; dii++) { if (Drec[dii].disp != 'm') continue; txwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } txwidget_append2(mLog,0,"\n %d deleted files (on DVD/BRD, not on disk) \n",ndel); for (vii = 0; vii < Vnf; vii++) { if (Vrec[vii].disp != 'd') continue; txwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); if (checkKillPause()) goto report_exit; } report_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // list all files for backup int list_current_files(ch *menu) { int dii; if (! BJval) { txwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } txwidget_append2(mLog,0,"\n List all files for backup: \n"); dGetFiles(); txwidget_append2(mLog,0," %d files found \n",Dnf); for (dii = 0; dii < Dnf; dii++) { if (checkKillPause()) break; txwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); } report_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // list all files on mounted DVD/BRD int list_DVD_files(ch *menu) { int vii; txwidget_append2(mLog,0,"\n List all files on DVD/BRD: \n"); vGetFiles(); txwidget_append2(mLog,0," %d files found \n",Vnf); for (vii = 0; vii < Vnf; vii++) { if (checkKillPause()) break; txwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); } if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // find desired files on disk, on mounted DVD/BRD, and in history files int find_files(ch *menu) { int dii, vii, hii, ftf, nn; ch *fspec1, *hfile1; static ch fspec2[200] = "/home/*/file*"; ch hfile[200], buff[1000], *pp; FILE *fid; zlist_t *zlist = 0; dGetFiles(); // get disk and DVD/BRD files if (dvdmtd) vGetFiles(); else txwidget_append2(mLog,0," DVD/BRD not mounted \n"); txwidget_append2(mLog,0,"\n find files matching wildcard pattern \n"); // get search pattern fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:",fspec2); if (blank_null(fspec1)) goto report_exit; strncpy0(fspec2,fspec1,199); strTrim(fspec2); txwidget_append2(mLog,0," search pattern: %s \n",fspec2); txwidget_append2(mLog,1,"\n matching files on disk: \n"); for (dii = 0; dii < Dnf; dii++) // search disk files { if (checkKillPause()) goto report_exit; if (MatchWild(fspec2,Drec[dii].file) == 0) txwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); } txwidget_append2(mLog,1,"\n matching files on DVD/BRD: \n"); for (vii = 0; vii < Vnf; vii++) // search DVD/BRD files { if (checkKillPause()) goto report_exit; if (MatchWild(fspec2,Vrec[vii].file) == 0) txwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); } txwidget_append2(mLog,1,"\n matching files in backup history: \n"); zlist = zlist_new(maxhist); snprintf(hfile,199,"%s/dkopp-hist-*",homedir); // find all backup history files ftf = 1; // /home/user/.dkopp/dkopp-hist-* nn = 0; while (true) { hfile1 = SearchWild(hfile,ftf); if (! hfile1) break; if (nn == maxhist) break; zlist_append(zlist,hfile1,0); // add to list nn++; } if (nn == 0) txwidget_append2(mLog,0," no history files found \n"); if (nn == maxhist) txwidget_append2(mLog,0," *** too many history files, please purge"); if (nn == 0 || nn == maxhist) goto report_exit; zlist_purge(zlist); // purge null entries 7.4 zlist_sort(zlist); // sort list ascending for (hii = 0; hii < nn; hii++) // loop all history files { hfile1 = zlist_get(zlist,hii); txwidget_append2(mLog,0," %s \n",hfile1); fid = fopen(hfile1,"r"); // next history file if (! fid) { txwidget_append2(mLog,0," *** file open error \n"); continue; } while (true) // read and search for match { if (checkKillPause()) break; pp = fgets_trim(buff,999,fid,1); if (! pp) break; if (MatchWild(fspec2,buff) == 0) txwidget_append2(mLog,0," %s \n",buff); } fclose(fid); } report_exit: if (zlist) zlist_free(zlist); if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // list available backup history files, select one to view int view_backup_hist(ch *menu) { ch *fspec1; ch fspec2[200], histfile[200]; ch *pp; int ii, jj, nn; int zstat, ftf; zdialog *zd; zlist_t *zlist = 0; txwidget_append2(mLog,0," available history files in %s \n",homedir); snprintf(fspec2,199,"%s/dkopp-hist-*",homedir); zlist = zlist_new(maxhist); ftf = 1; nn = 0; while (true) { fspec1 = SearchWild(fspec2,ftf); // file: dkopp-hist-yyyymmdd-hhmm-label if (! fspec1) break; pp = (ch *) strrchr(fspec1,'/') + 12; // get yyyymmdd-hhmm-label if (nn == maxhist) break; zlist_append(zlist,pp,0); // add to list nn++; } if (nn == 0) txwidget_append2(mLog,0," no history files found \n"); if (nn == maxhist) txwidget_append2(mLog,0," *** too many history files, please purge"); if (nn == 0 || nn == maxhist) goto report_exit; zlist_purge(zlist); // purge null entries 7.4 zlist_sort(zlist); // sort list ascending for (ii = 0; ii < nn; ii++) // report sorted list txwidget_append2(mLog,0," dkopp-hist-%s \n",zlist_get(zlist,ii)); zd = zdialog_new("choose history file",mWin,"OK","cancel",null); zdialog_add_widget(zd,"label","lab1","dialog","history file date and label"); zdialog_add_widget(zd,"combo","hfile","dialog"); jj = nn - 20; if (jj < 0) jj = 0; for (ii = jj; ii < nn; ii++) // stuff combo box list with zdialog_stuff(zd,"hfile",zlist_get(zlist,ii)); // 20 newest hist file IDs zdialog_stuff(zd,"hfile",zlist_get(zlist,nn-1)); // default entry is newest file zdialog_run(zd,0,"parent"); // run dialog zstat = zdialog_wait(zd); zdialog_fetch(zd,"hfile",histfile,199); // get user choice zdialog_free(zd); if (zstat != 1) goto report_exit; // cancelled zshell("log ack","xdg-open %s/%s-%s",homedir,"dkopp-hist",histfile); // view the file report_exit: if (zlist) zlist_free(zlist); if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // file restore dialog - specify DVD/BRD files to be restored int RJedit(ch *menu) { zdialog *zd; txwidget_append2(mLog,0,"\n Restore files from DVD/BRD \n"); vGetFiles(); // get files on DVD/BRD txwidget_append2(mLog,0," %d files on DVD/BRD \n",Vnf); if (! Vnf) return 0; ++Fdialog; zd = zdialog_new("copy files from DVD/BRD",mWin,"browse","done","cancel",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); 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,"label","labdev","vb1","DVD/BRD device"); // DVD/BRD device [___________][v] zdialog_add_widget(zd,"combo","entdvd","vb2",BJdvd); zdialog_add_widget(zd,"label","labfrom","vb1","copy-from DVD/BRD"); // copy-from DVD/BRD [______________] zdialog_add_widget(zd,"label","labto","vb1","copy-to disk"); // copy-to disk [______________] zdialog_add_widget(zd,"zentry","entfrom","vb2",RJfrom); zdialog_add_widget(zd,"zentry","entto","vb2",RJto); zdialog_add_widget(zd,"hsep","hsep1","dialog"); zdialog_add_widget(zd,"label","labfiles","dialog","files to restore"); zdialog_add_widget(zd,"frame","framefiles","dialog",0,"expand"); zdialog_add_widget(zd,"scrwin","scrfiles","framefiles"); zdialog_add_widget(zd,"zedit","editfiles","scrfiles"); for (int ii = 0; ii < ndvds; ii++) // load curr. data into widgets zdialog_stuff(zd,"entdvd",dvddevdesc[ii]); // remove get/stuff mount point editwidget = zdialog_gtkwidget(zd,"editfiles"); for (int ii = 0; ii < RJnx; ii++) // get restore include/exclude recs, txwidget_append2(editwidget,0,"%s""\n",RJinex[ii]); // pack into file selection edit box zdialog_resize(zd,400,400); zdialog_run(zd,RJedit_event,"parent"); // run dialog with response function return 0; } // edit dialog event function int RJedit_event(zdialog *zd, ch *event) { ch text[40], *pp, fcfrom[XFCC]; int zstat, line, cc; zstat = zd->zstat; if (! zstat) return 0; if (zstat != 1 && zstat != 2) goto end_dialog; // cancel or destroy RJreset(); // reset restore job data zdialog_fetch(zd,"entdvd",text,19); // get DVD/BRD device strncpy0(BJdvd,text,19); pp = strchr(BJdvd,' '); if (pp) *pp = 0; // remove fetch/save mount point zdialog_fetch(zd,"entfrom",RJfrom,XFCC); // copy-from location /home/xxx/.../ strTrim(RJfrom); zdialog_fetch(zd,"entto",RJto,XFCC); // copy-to location /home/yyy/.../ strTrim(RJto); for (line = 0; ; line++) { pp = txwidget_line(editwidget,line,1); if (! pp || ! *pp) break; cc = strTrim(pp); // remove trailing blanks if (cc < 3) continue; // ignore absurdities if (cc > XFCC-100) continue; RJinex[RJnx] = zstrdup(pp,"RJinex"); // copy new record if (++RJnx == maxnx) { txwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx); break; } } if (zstat == 1) { // do file-chooser dialog strcpy(fcfrom,dvdmp); // start at /media/xxxx/home/xxxx/ strcat(fcfrom,RJfrom); fc_dialog(fcfrom); zd->zstat = 0; // dialog continues return 0; } RJvalidate(); // validate restore job data if (RJval) rGetFiles(); // get files to restore else txwidget_append2(mLog,0," *** correct errors in restore job \n"); end_dialog: zdialog_free(zd); // destroy dialog --Fdialog; return 0; } // List and validate DVD/BRD files to be restored int RJlist(ch *menu) { int cc1, cc2; ch *file1, file2[XFCC]; if (! RJval) { txwidget_append2(mLog,0," *** restore job has errors \n"); goto list_exit; } txwidget_append2(mLog,0,"\n copy %d files from DVD/BRD: %s \n",Rnf, RJfrom); txwidget_append2(mLog,0," to directory: %s \n",RJto); txwidget_append2(mLog,0,"\n resulting files will be the following: \n"); if (! Rnf) goto list_exit; cc1 = strlen(RJfrom); // from: /home/xxx/.../ cc2 = strlen(RJto); // to: /home/yyy/.../ for (int ii = 0; ii < Rnf; ii++) { if (checkKillPause()) goto list_exit; file1 = Rrec[ii].file; if (! strmatchN(file1,RJfrom,cc1)) { txwidget_append2(mLog,0," *** not within copy-from: %s \n",kleenex(file1)); RJval = 0; continue; } strcpy(file2,RJto); strcpy(file2+cc2,file1+cc1); txwidget_append2(mLog,0," %s \n",kleenex(file2)); } list_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // Restore files based on data from restore dialog int Restore(ch *menu) { int ii, nn, ccf; ch dfile[XFCC]; ch *errmess; if (! RJval || ! Rnf) { txwidget_append2(mLog,0," *** restore job has errors \n"); goto restore_exit; } nn = zmessageYN(mWin,"Restore %d files from: %s%s \n to: %s \n" "Proceed with file restore ?",Rnf,dvdmp,RJfrom,RJto); if (! nn) goto restore_exit; txwidget_append2(mLog,1,"\n""begin restore of %d files to: %s \n",Rnf,RJto); ccf = strlen(RJfrom); // from: /media/xxx/filespec for (ii = 0; ii < Rnf; ii++) { if (checkKillPause()) goto restore_exit; strcpy(dfile,RJto); // to: /destination/filespec strcat(dfile,Rrec[ii].file+ccf); txwidget_append2(mLog,0," %s \n",kleenex(dfile)); errmess = copyfile(Rrec[ii].file,dfile); if (errmess) txwidget_append2(mLog,0," *** %s \n",errmess); } restore_filepoop(); // restore owner/permissions dFilesReset(); // reset disk file data restore_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // get available DVD/BRD devices // the lshw command blocks everything for several seconds int getDVDs(void *) // overhauled { int ii, dvdrw; ch buff[200], *pp; ch command[50] = "lshw -class disk 2>/dev/null"; // better than udevadm FILE *fid; dvdrw = ndvds = 0; fid = popen(command,"r"); if (! fid) { txwidget_append2(mLog,0," No DVD/BRD devices found"); return 0; } while (true) { pp = fgets_trim(buff,200,fid); // read lshw command output if (! pp) break; if (strstr(buff,"*-")) { // start some device if (strstr(buff,"*-cdrom")) dvdrw = 1; // start DVD/BRD device else dvdrw = 0; continue; } if (! dvdrw) continue; // ignore recs for other devices if (strstr(buff,"description:")) { pp = strstr(buff,"description:"); // save DVD/BRD description pp += 12; if (*pp == ' ') pp++; // (assume description comes first) strncpy0(dvddesc[ndvds],pp,40); continue; } if (strstr(buff,"/dev/")) { pp = strstr(buff,"/dev/"); // have /dev/sr0 or similar format if (pp[7] < '0' || pp[7] > '9') continue; pp[8] = 0; strcpy(dvddevs[ndvds],pp); // save DVD/BRD device ndvds++; continue; } } pclose(fid); for (ii = 0; ii < ndvds; ii++) // combine devices and descriptions { // for use in GUI chooser list strcpy(dvddevdesc[ii],dvddevs[ii]); strcat(dvddevdesc[ii]," "); strcat(dvddevdesc[ii],dvddesc[ii]); } txwidget_append2(mLog,0," DVD/BRD devices found: %d \n",ndvds); // output list of DVDs for (ii = 0; ii < ndvds; ii++) txwidget_append2(mLog,0," %s %s \n",dvddevs[ii],dvddesc[ii]); return 0; } // set DVD/BRD device and mount point int setDVDdevice(ch *menu) { ch *pp1; ch *pp2, text[60]; int ii, Nth, zstat; zdialog *zd; if (*scriptParam) { // script Nth = 1; // parse: /dev/dvd /media/xxxx pp1 = substring(scriptParam,' ',Nth++); if (pp1) strncpy0(BJdvd,pp1,19); pp1 = substring(scriptParam,' ',Nth++); if (pp1) { strncpy0(dvdmp,pp1,99); dvdmpcc = strlen(dvdmp); if (dvdmp[dvdmpcc-1] == '/') dvdmp[dvdmpcc--] = 0; // remove trailing / } *scriptParam = 0; return 0; } zd = zdialog_new("select DVD/BRD drive",mWin,"OK","cancel",null); // dialog to select DVD/BRD & mount point zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); 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,"label","labdvd","vb1","DVD/BRD device"); zdialog_add_widget(zd,"label","labmp","vb1","mount point"); zdialog_add_widget(zd,"combo","entdvd","vb2",BJdvd); zdialog_add_widget(zd,"zentry","entmp","vb2",dvdmp); for (ii = 0; ii < ndvds; ii++) // stuff avail. DVDs, mount points zdialog_stuff(zd,"entdvd",dvddevdesc[ii]); zdialog_stuff(zd,"entmp",dvdmp); zdialog_run(zd,0,"parent"); zstat = zdialog_wait(zd); if (zstat != 1) { zdialog_free(zd); return 0; } zstat = zdialog_fetch(zd,"entdvd",text,60); // get selected DVD/BRD strncpy0(BJdvd,text,19); pp2 = strchr(BJdvd,' '); if (pp2) *pp2 = 0; zdialog_fetch(zd,"entmp",text,39); // DVD/BRD mount point strncpy0(dvdmp,text,99); strTrim(dvdmp); dvdmpcc = strlen(dvdmp); if (dvdmpcc && (dvdmp[dvdmpcc-1] == '/')) // remove trailing / dvdmp[dvdmpcc--] = 0; txwidget_append2(mLog,0," DVD/BRD and mount point: %s %s \n",BJdvd,dvdmp); if (Fgui) txwidget_append2(mLog,0," ready \n"); zdialog_free(zd); return 0; } // set label for subsequent DVD/BRD backup via growisofs int setDVDlabel(ch *menu) { ch *pp; if (*dvdlabel) txwidget_append2(mLog,0," old DVD/BRD label: %s \n",dvdlabel); else strcpy(dvdlabel,"dkopp"); pp = zdialog_text(mWin,"set new DVD/BRD label",dvdlabel); if (blank_null(pp)) pp = "dkopp"; strncpy0(dvdlabel,pp,31); txwidget_append2(mLog,0," new DVD/BRD label: %s \n",dvdlabel); return 1; } // Mount DVD/BRD with message feedback to window. int mountDVD(ch *menu) // menu mount function { int err, reset; ch buff[400], mbuff[200]; ch *pp, *pp1; FILE *fid; STATB dstat; if (dvdmtd) { err = stat(dvdmp,&dstat); if ((! err) && (dvdtime == dstat.st_ctime)) return 0; // medium unchanged, do nothing } dvdmtd = 0; // set DVD/BRD not mounted dvdtime = -1; strcpy(mediumDT,"unknown"); *mediumDT = 0; err = reset = 0; vFilesReset(); // reset DVD/BRD files fid = popen("cat /etc/mtab","r"); // get mounted devices if (! fid) goto trymount; while (true) { pp = fgets_trim(buff,400,fid); if (! pp) break; pp1 = substring(pp,' ',1); // get /dev/xxx if (! pp1 || ! strmatch(pp1,BJdvd)) continue; // not my DVD/BRD pp1 = substring(pp,' ',2); // get mount point if (! pp1) continue; repl_1str(pp1,dvdmp,100,"\\040"," "); // replace "\040" with " " dvdmpcc = strlen(dvdmp); txwidget_append2(mLog,0," already mounted: %s %s \n",BJdvd,dvdmp); dvdmtd = 1; } pclose(fid); if (dvdmtd) goto showpoop; trymount: mkdir(dvdmp,0755); // create default mount point snprintf(mbuff,200,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp); // mount the DVD/BRD err = do_shell("mount",mbuff); if (! err) { dvdmtd = 1; goto showpoop; } zmessageACK(mWin,"mount DVD/BRD and wait for completion"); while (true) { fid = popen("cat /etc/mtab","r"); // get mounted disk info if (! fid) goto mount; while (true) { pp = fgets_trim(buff,400,fid); if (! pp) break; pp1 = substring(pp,' ',1); // get /dev/xxx if (! pp1 || ! strmatch(pp1,BJdvd)) continue; // not my DVD/BRD pp1 = substring(pp,' ',2); // get mount point if (! pp1) continue; repl_1str(pp1,dvdmp,100,"\\040"," "); // replace "\040" with " " dvdmpcc = strlen(dvdmp); txwidget_append2(mLog,0," %d %d mounted \n",BJdvd,dvdmp); dvdmtd = 1; } pclose(fid); if (dvdmtd) goto showpoop; // mounted OK mount: snprintf(mbuff,200,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp); // mount the DVD/BRD err = do_shell("mount",mbuff); if (! err) { dvdmtd = 1; goto showpoop; } txwidget_append2(mLog,0," waiting for mount ... \n"); for (int ii = 0; ii < 5; ii++) // 5 secs between "wait" messages { if (checkKillPause()) { // killed by user commFail++; return 1; } zsleep(1); } } showpoop: dvdtime = dstat.st_ctime; // set DVD/BRD ID = mod time snprintf(buff,99,"volname %s",BJdvd); // get DVD/BRD label fid = popen(buff,"r"); if (fid) { pp = fgets_trim(mbuff,99,fid,1); if (pp) strncpy0(dvdlabel,pp,31); pclose(fid); } strcpy(mbuff,dvdmp); strcat(mbuff,V_DATETIME); // get last usage date/time if poss. fid = fopen(mbuff,"r"); if (fid) { pp = fgets_trim(mbuff,99,fid,1); if (pp) strncpy0(mediumDT,pp,15); fclose(fid); } txwidget_append2(mLog,0," DVD/BRD label: %s last dkopp: %s \n",dvdlabel,mediumDT); commFail = 0; return 0; } // unmount DVD/BRD int unmountDVD(ch *menu) { ch command[100]; vFilesReset(); dvdmtd = 0; dvdtime = -1; snprintf(command,100,"umount %s 2>&1",dvdmp); // use mount point do_shell("umount",command); if (Fgui) txwidget_append2(mLog,0," ready \n"); commFail = 0; // ignore unmount error return 0; } // eject DVD/BRD with message feedback to window // not all computers support programmatic eject int ejectDVD(ch *menu) { ch command[60]; vFilesReset(); dvdmtd = 0; dvdtime = -1; sprintf(command,"eject %s 2>&1",BJdvd); do_shell("eject",command); if (Fgui) txwidget_append2(mLog,0," ready \n"); commFail = 0; // ignore eject error return 0; } // wait for DVD/BRD and reset hardware (get over lockups after growisofs) int resetDVD(ch *menu) { if (*subprocName) { // try to kill running job signalProc(subprocName,"resume"); signalProc(subprocName,"kill"); sleep(1); } ejectDVD(0); // the only way I know to reset sleep(1); // a hung-up DVD/BRD drive if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // Erase DVD/BRD medium by filling it with zeros int eraseDVD(ch *menu) { ch command[200]; int nstat; nstat = zmessageYN(mWin,"Erase DVD/BRD. This will take some time. \n Continue?"); if (! nstat) goto erase_exit; vFilesReset(); // reset DVD/BRD file data sprintf(command,"growisofs -Z %s=/dev/zero %s 2>&1",BJdvd,gforce); do_shell("growisofs", command); // do growisofs, echo outputs erase_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // Format DVD/BRD (2-4 minutes) int formatDVD(ch *menu) { ch command[60]; int nstat; nstat = zmessageYN(mWin,"Format DVD/BRD. This will take 2-4 minutes. \n Continue?"); if (! nstat) goto format_exit; vFilesReset(); // reset DVD/BRD file data sprintf(command,"dvd+rw-format -force %s 2>&1",BJdvd); do_shell("dvd+rw-format", command); format_exit: if (Fgui) txwidget_append2(mLog,0," ready \n"); return 0; } // Display help/about or help/user guide int helpFunc(ch *menu) { if (strmatch(menu,"about")) zabout(mWin); // 7.7 if (strmatch(menu,"user guide")) showz_docfile(mWin,"userguide",0); return 0; } // construct file-chooser dialog box // note: Fdialog unnecessary: this dialog called from other dialogs int fc_dialog(ch *dirk) { GtkWidget *vbox; fc_dialogbox = gtk_dialog_new_with_buttons("choose files", GTK_WINDOW(mWin), GTK_DIALOG_MODAL, "hidden",100, "include",101, "exclude",102, "done",103, null); gtk_window_set_default_size(GTK_WINDOW(fc_dialogbox),600,500); G_SIGNAL(fc_dialogbox,"response",fc_response,0); fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); vbox = gtk_dialog_get_content_area(GTK_DIALOG(fc_dialogbox)); gtk_box_pack_end(GTK_BOX(vbox),fc_widget,1,1,0); gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),0); gtk_widget_show_all(fc_dialogbox); return 0; } // file-chooser dialog handler (file selection, OK, Cancel, Kill) int fc_response(GtkDialog *dwin, int arg, void *data) { GSList *flist = 0; ch *file1, *file2, *ppf; int ii, err, hide; STATB filestat; if (arg == 103 || arg == -4) // done, cancel { gtk_widget_destroy(GTK_WIDGET(dwin)); return 0; } if (arg == 100) // hidden { hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(fc_widget)); hide = 1 - hide; gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),hide); } if (arg == 101 || arg == 102) // include, exclude { flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); for (ii = 0; ; ii++) // process selected files { file1 = (ch *) g_slist_nth_data(flist,ii); if (! file1) break; file2 = zstrdup(file1,"file2",2); // extra space for wildcard g_free(file1); err = stat(file2,&filestat); if (err) txwidget_append2(mLog,0," *** error: %s file: %s \n", strerror(errno),kleenex(file2)); if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard ppf = file2; if (strmatchN(ppf,dvdmp,dvdmpcc)) ppf += dvdmpcc; // omit DVD/BRD mount point if (arg == 101) txwidget_append2(editwidget,0,"include %s""\n",ppf); if (arg == 102) txwidget_append2(editwidget,0,"exclude %s""\n",ppf); zfree(file2); } } gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); g_slist_free(flist); return 0; } // backup helper function // set nominal backup date/time // write date/time and updated medium use count to temp file int writeDT() { time_t dt1; struct tm dt2; // year/month/day/hour/min/sec FILE *fid; dt1 = time(0); dt2 = *localtime(&dt1); snprintf(backupDT,15,"%4d%02d%02d-%02d%02d",dt2.tm_year+1900, // yyyymmdd-hhmm dt2.tm_mon+1, dt2.tm_mday, dt2.tm_hour, dt2.tm_min); strcpy(mediumDT,backupDT); fid = fopen(TFdatetime,"w"); if (! fid) { txwidget_append2(mLog,0," *** cannot open /tmp scratch file \n"); commFail++; return 0; } fprintf(fid,"%s \n",mediumDT); // write date/time and medium count fclose(fid); return 0; } // backup helper function // save all file and directory owner and permission data to temp file int save_filepoop() // all files, not just directories { int ii, cc, err; FILE *fid; ch file[XFCC], dirk[XFCC], pdirk[XFCC], *pp; STATB dstat; fid = fopen(TFfilepoop,"w"); if (! fid) { txwidget_append2(mLog,0," *** cannot open /tmp scratch file \n"); commFail++; return 0; } *pdirk = 0; // no prior for (ii = 0; ii < Dnf; ii++) { strcpy(dirk,Drec[ii].file); // next file on disk pp = dirk; while (true) { pp = strchr(pp+1,'/'); // next (last) directory level if (! pp) break; cc = pp - dirk + 1; // cc incl. '/' if (strncmp(dirk,pdirk,cc) == 0) continue; // matches prior, skip *pp = 0; // terminate this directory level err = stat(dirk,&dstat); // get owner and permissions if (err) { txwidget_append2(mLog,0," *** error: %s file: %s \n", strerror(errno),kleenex(dirk)); break; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid permissions directory dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk); // (octal) *pp = '/'; // restore '/' } strcpy(pdirk,dirk); // prior = this directory strcpy(file,Drec[ii].file); // disk file, again err = stat(file,&dstat); // get owner and permissions if (err) { txwidget_append2(mLog,0," *** error: %s file: %s \n", strerror(errno),kleenex(file)); continue; } dstat.st_mode = dstat.st_mode & 0777; fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid permissions file dstat.st_uid, dstat.st_gid, dstat.st_mode, file); // (octal) } fclose(fid); return 0; } // restore helper function // restore original owner and permissions for restored files and directories int restore_filepoop() { FILE *fid; int cc1, cc2, ccf, nn, ii, err; int uid, gid, perms; ch file1[XFCC], file2[XFCC]; ch poopfile[100]; txwidget_append2(mLog,0,"\n restore directory owner and permissions \n"); txwidget_append2(mLog,0," for directories anchored at: %s \n",RJto); cc1 = strlen(RJfrom); // from: /home/xxx/.../ cc2 = strlen(RJto); // to: /home/yyy/.../ strcpy(poopfile,dvdmp); // DVD/BRD file with owner & permissions strcat(poopfile,V_FILEPOOP); fid = fopen(poopfile,"r"); if (! fid) { txwidget_append2(mLog,0," *** cannot open DVD/BRD file: %s \n",poopfile); return 0; } ii = 0; while (true) { nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file1); // uid, gid, permissions, file if (nn == EOF) break; // (nnn:nnn) (octal) if (nn != 4) continue; ccf = strlen(file1); // match directories too if (ccf < cc1) continue; while (ii < Rnf) { nn = strncmp(Rrec[ii].file,file1,ccf); // file in restored file list? if (nn >= 0) break; // (logic depends on sorted lists) ii++; } if (ii == Rnf) break; if (nn > 0) continue; // no strcpy(file2,RJto); // copy-to location strcpy(file2 + cc2, file1 + cc1); // + org. file, less copy-from part txwidget_append2(mLog,0," owner: %4d:%4d permissions: %3o file: %s \n", uid, gid, perms, kleenex(file2)); err = chown(file2,uid,gid); if (err) txwidget_append2(mLog,0," *** error: %s \n",strerror(errno)); err = chmod(file2,perms); if (err) txwidget_append2(mLog,0," *** error: %s \n",strerror(errno)); } fclose(fid); return 0; } // create backup history file after successful backup int createBackupHist() { int ii, err; FILE *fid; ch backupfile[200]; ch disp; snprintf(backupfile,199,"%s/dkopp-hist-%s-%s", // create history file name: homedir,backupDT,dvdlabel); // dkopp-hist-yyyymmdd-hhmm-dvdlabel txwidget_append2(mLog,1,"\n""create history file: %s \n",backupfile); fid = fopen(backupfile,"w"); if (! fid) { txwidget_append2(mLog,0," *** cannot open dkopp-hist file \n"); return 0; } fprintf(fid,"%s (%s backup) \n\n",backupfile,mbmode); for (ii = 0; ii < BJnx; ii++) // output include/exclude recs fprintf(fid," %s \n",BJinex[ii]); fprintf(fid,"\n"); if (strmatch(mbmode,"full")) for (ii = 0; ii < Dnf; ii++) // output all files for backup fprintf(fid,"%s\n",Drec[ii].file); else { for (ii = 0; ii < Dnf; ii++) { // output new and modified files disp = Drec[ii].disp; if ((disp == 'n') || (disp == 'm')) fprintf(fid,"%s\n",Drec[ii].file); } } err = fclose(fid); if (err) txwidget_append2(mLog,0," *** dkopp-hist file error %s \n",strerror(errno)); return 0; } // parse an include/exclude filespec statement // return: 0=comment 1=OK 2=parse-error 3=fspec-error int inexParse(ch * rec, ch *& rtype, ch *& fspec) { ch *pp1, *pp2; int ii; rtype = fspec = 0; if (rec[0] == '#') return 0; // comment recs. if (strlen(rec) < 3) return 0; strTrim(rec); ii = 0; while ((rec[ii] == ' ') && (ii < 30)) ii++; // find 1st non-blank if (rec[ii] == 0) return 0; if (ii == 30) return 0; // blank record rtype = rec + ii; // include/exclude while ((rec[ii] > ' ') && (ii < 30)) ii++; // look for next blank or null if (ii == 30) return 2; if (rec[ii] == ' ') { rec[ii] = 0; ii++; } // end of rtype if (strlen(rtype) > 7) return 2; while ((rec[ii] == ' ') && (ii < 30)) ii++; // find next non-blank if (ii == 30) return 2; fspec = rec + ii; // filespec (wildcards) if (strlen(fspec) < 4) return 3; if (strlen(fspec) > XFCC-100) return 3; if (strmatch(rtype,"exclude")) return 1; // exclude, done if (! strmatch(rtype,"include")) return 2; // must be include if (fspec[0] != '/') return 3; // must have at least /topdirk/ pp1 = strchr(fspec+1,'/'); if (!pp1) return 3; if (pp1-fspec < 2) return 3; pp2 = strchr(fspec+1,'*'); // any wildcards must be later if (pp2 && (pp2 < pp1)) return 3; pp2 = strchr(fspec+1,'%'); if (pp2 && (pp2 < pp1)) return 3; return 1; // include + legit. fspec } // list backup job data and validate as much as practical int BJvalidate(ch *menu) { int ii, err, nerr = 0; int year, mon, day; struct tm tm_date, *tm_date2; STATB dstat; txwidget_append2(mLog,1,"\n""Validate backup job data \n"); BJval = 0; if (! BJnx) { txwidget_append2(mLog,0," *** no job data present \n"); commFail++; return 0; } txwidget_append2(mLog,0," DVD/BRD device: %s \n",BJdvd); err = stat(BJdvd,&dstat); if (err || ! S_ISBLK(dstat.st_mode)) { txwidget_append2(mLog,0," *** DVD/BRD device is apparently invalid \n"); nerr++; } txwidget_append2(mLog,0," backup %s \n",BJbmode); if (! strmatchV(BJbmode,"full","incremental","accumulate",null)) { txwidget_append2(mLog,0," *** backup mode not full/incremental/accumulate \n"); nerr++; } txwidget_append2(mLog,0," verify %s \n",BJvmode); if (! strmatchV(BJvmode,"full","incremental","thorough",null)) { txwidget_append2(mLog,0," *** verify mode not full/incremental/thorough \n"); nerr++; } txwidget_append2(mLog,0," file date from: %s \n",BJdatefrom); // file age limit err = 0; ii = sscanf(BJdatefrom,"%d.%d.%d",&year,&mon,&day); if (ii != 3) err = 1; tm_date.tm_year = year - 1900; tm_date.tm_mon = mon - 1; tm_date.tm_mday = day; tm_date.tm_hour = tm_date.tm_min = tm_date.tm_sec = 0; tm_date.tm_isdst = -1; BJtdate = mktime(&tm_date); tm_date2 = localtime(&BJtdate); if (tm_date2->tm_year - year + 1900 != 0) err = 3; if (tm_date2->tm_year + 1900 < 1970) err = 4; // < 1970 disallowed if (tm_date2->tm_mon - mon + 1 != 0) err = 5; if (tm_date2->tm_mday - day != 0) err = 6; if (err) { txwidget_append2(mLog,0," *** date must be > 1970.01.01 \n"); nerr++; BJtdate = 0; } nerr += nxValidate(BJinex,BJnx); // validate include/exclude recs txwidget_append2(mLog,0," *** %d errors \n",nerr); if (nerr) commFail++; else BJval = 1; return 0; } // validate restore job data int RJvalidate() { int cc, nerr = 0; ch rdirk[XFCC]; DIR *pdirk; if (RJval) return 1; txwidget_append2(mLog,0,"\n Validate restore job data \n"); if (! RJnx) { txwidget_append2(mLog,0," *** no job data present \n"); return 0; } txwidget_append2(mLog,0," copy-from: %s \n",RJfrom); strcpy(rdirk,dvdmp); // validate copy-from location strcat(rdirk,RJfrom); // /media/dvd/home/... pdirk = opendir(rdirk); if (! pdirk) { txwidget_append2(mLog,0," *** invalid copy-from location \n"); nerr++; } else closedir(pdirk); cc = strlen(RJfrom); // insure '/' at end if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); txwidget_append2(mLog,0," copy-to: %s \n",RJto); pdirk = opendir(RJto); // validate copy-to location if (! pdirk) { txwidget_append2(mLog,0," *** invalid copy-to location \n"); nerr++; } else closedir(pdirk); cc = strlen(RJto); // insure '/' at end if (RJto[cc-1] != '/') strcat(RJto,"/"); nerr += nxValidate(RJinex,RJnx); // validate include/exclude recs txwidget_append2(mLog,0," %d errors \n",nerr); if (! nerr) RJval = 1; else RJval = 0; return RJval; } // list and validate a set of include/exclude recs int nxValidate(ch **inexrecs, int nrecs) { ch *rtype, *fspec, nxrec[XFCC]; int ii, nstat, errs = 0; for (ii = 0; ii < nrecs; ii++) // process include/exclude recs { strcpy(nxrec,inexrecs[ii]); txwidget_append2(mLog,0," %s \n",nxrec); // output nstat = inexParse(nxrec,rtype,fspec); // parse if (nstat == 0) continue; // comment if (nstat == 1) continue; // OK if (nstat == 2) { txwidget_append2(mLog,0," *** cannot parse \n"); // cannot parse errs++; continue; } if (nstat == 3) { // bad filespec txwidget_append2(mLog,0," *** invalid filespec \n"); errs++; continue; } } return errs; } // get all files for backup as specified by include/exclude records // save in Drec[] array int dGetFiles() { ch *fsp; ch *rtype, *fspec, bjrec[XFCC], *mbytes; int ftf, cc, nstat, wstat, err; int ii, jj, nfiles, nexc; double nbytes; STATB filestat; if (! BJval) { // validate job data if needed dFilesReset(); BJvalidate(0); if (! BJval) return 0; // job has errors } if (Dnf > 0) return 0; // avoid refresh txwidget_append2(mLog,1,"\n""finding all files for backup \n"); for (ii = 0; ii < BJnx; ii++) // process include/exclude recs { BJfiles[ii] = 0; // initz. include/exclude rec stats BJbytes[ii] = 0.0; BJdvdno[ii] = 0; strcpy(bjrec,BJinex[ii]); // next record nstat = inexParse(bjrec,rtype,fspec); // parse if (nstat == 0) continue; // comment if (strmatch(rtype,"include")) // include filespec { ftf = 1; while (1) { fsp = SearchWild(fspec,ftf); // find matching files if (! fsp) break; cc = strlen(fsp); if (cc > XFCC-100) zappcrash("file cc: %d, %99s...",cc,fsp); Drec[Dnf].file = zstrdup(fsp,"Drec"); err = lstat(fsp,&filestat); // check accessibility if (err == 0) { if (! S_ISREG(filestat.st_mode) && // include files and symlinks ! S_ISLNK(filestat.st_mode)) continue; // omit pipes, devices ... } Drec[Dnf].stat = err; // save file status Drec[Dnf].inclx = ii; // save pointer to include rec Drec[Dnf].size = filestat.st_size; // save file size Drec[Dnf].mtime = filestat.st_mtime // save last mod time + filestat.st_mtim.tv_nsec * nano; // (nanosec resolution) if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0; Drec[Dnf].disp = Drec[Dnf].ivf = 0; // initialize BJfiles[ii]++; // count included files and bytes BJbytes[ii] += Drec[Dnf].size; if (++Dnf == maxfs) { txwidget_append2(mLog,0," *** exceeded %d files \n",maxfs); goto errret; } } } if (strmatch(rtype,"exclude")) // exclude filespec { for (jj = 0; jj < Dnf; jj++) // check included files (SO FAR) { if (! Drec[jj].file) continue; wstat = MatchWild(fspec,Drec[jj].file); if (wstat != 0) continue; BJfiles[ii]--; // un-count excluded file and bytes BJbytes[ii] -= Drec[jj].size; zfree(Drec[jj].file); // clear file data in array Drec[jj].file = 0; Drec[jj].stat = 0; // bugfix } } } // end of include/exclude recs for (ii = 0; ii < Dnf; ii++) // list and remove error files { // (after excluded files removed) if (! Drec[ii].file) continue; if (Drec[ii].stat) { err = stat(Drec[ii].file,&filestat); txwidget_append2(mLog,0," *** %s omit: %s \n",strerror(errno),kleenex(Drec[ii].file)); jj = Drec[ii].inclx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; zfree(Drec[ii].file); Drec[ii].file = 0; } } for (nexc = ii = 0; ii < Dnf; ii++) { if (! Drec[ii].file) continue; if (Drec[ii].mtime < BJtdate) // omit files excluded by date { // or older than 1970 jj = Drec[ii].inclx; BJfiles[jj]--; // un-count file and bytes BJbytes[jj] -= Drec[ii].size; zfree(Drec[ii].file); Drec[ii].file = 0; nexc++; } } if (nexc) txwidget_append2(mLog,0," %d files excluded by selection date \n",nexc); ii = jj = 0; // repack file arrays after deletions while (ii < Dnf) { if (Drec[ii].file == 0) ii++; else { if (ii > jj) { if (Drec[jj].file) zfree(Drec[jj].file); Drec[jj] = Drec[ii]; Drec[ii].file = 0; } ii++; jj++; } } Dnf = jj; // final file count Dbytes = 0.0; for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size; // compute total bytes from files nfiles = 0; nbytes = 0.0; for (ii = 0; ii < BJnx; ii++) // compute total files and bytes { // from include/exclude recs nfiles += BJfiles[ii]; nbytes += BJbytes[ii]; } mbytes = formatKBMB(nbytes,3); txwidget_append2(mLog,0," files for backup: %d %s \n",nfiles,mbytes); if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match txwidget_append2(mLog,0," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); txwidget_append2(mLog,0," *** bug: nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); goto errret; } SortFileList((ch *) Drec,sizeof(dfrec),Dnf,'A'); // sort Drec[Dnf] by Drec[].file for (ii = 1; ii < Dnf; ii++) // look for duplicate files if (strmatch(Drec[ii].file,Drec[ii-1].file)) { txwidget_append2(mLog,0," *** duplicate file: %s \n",kleenex(Drec[ii].file)); BJval = 0; // invalidate backup job } if (! BJval) goto errret; return 0; errret: dFilesReset(); BJval = 0; return 0; } // get existing files on DVD/BRD medium, save in Vrec[] array // (the shell command "find ... -type f" does not find the // files "deleted" via copy from /dev/null in growisofs) int vGetFiles() { int cc, gcc, err; ch command[200], *pp; ch fspec1[XFCC], fspec2[XFCC]; FILE *fid; STATB filestat; if (Vnf) return 0; // avoid refresh mountDVD(0); // mount with retries if (! dvdmtd) return 0; // cannot mount txwidget_append2(mLog,1,"\n""find all DVD/BRD files \n"); snprintf(command,200,"find \"%s\" -type f -or -type l >%s", // get regular files and symlinks dvdmp,TFdvdfiles); // add quotes in case of blanks txwidget_append2(mLog,0," %s \n",command); err = zshell(0,command); // list all DVD/BRD files to temp file if (err) { txwidget_append2(mLog,0," *** find command failed: %s \n",strerror(err)); commFail++; return 0; } fid = fopen(TFdvdfiles,"r"); // read file list if (! fid) { txwidget_append2(mLog,0," *** cannot open /tmp scratch file \n"); commFail++; return 0; } gcc = strlen(V_DKOPPDIRK); while (1) { pp = fgets_trim(fspec1,XFCC-2,fid); // get next file if (! pp) break; // eof cc = strlen(pp); // absurdly long file name if (cc > XFCC-100) { txwidget_append2(mLog,0," *** absurd file skipped: %200s (etc.) \n",kleenex(pp)); continue; } if (strmatchN(fspec1+dvdmpcc,V_DKOPPDIRK,gcc)) continue; // ignore special dkopp files repl_1str(fspec1,fspec2,XFCC,"\\=","="); // replace "\=" with "=" in file name Vrec[Vnf].file = zstrdup(fspec2 + dvdmpcc,"Vrec"); // save without DVD/BRD mount point err = lstat(fspec1,&filestat); // check accessibility Vrec[Vnf].stat = err; // save file status Vrec[Vnf].size = filestat.st_size; // save file size Vrec[Vnf].mtime = filestat.st_mtime // save last mod time + filestat.st_mtim.tv_nsec * nano; if (err) Vrec[Vnf].size = Vrec[Vnf].mtime = 0; Vnf++; if (Vnf == maxfs) zappcrash("exceed %d files",maxfs); } fclose (fid); SortFileList((ch *) Vrec,sizeof(vfrec),Vnf,'A'); // sort Vrec[Vnf] by Vrec[].file txwidget_append2(mLog,0," DVD/BRD files: %d \n",Vnf); return 0; } // get all DVD/BRD restore files specified by include/exclude records int rGetFiles() { ch *rtype, *fspec, fspecx[XFCC], rjrec[XFCC]; int ii, jj, cc, nstat, wstat, ninc, nexc; if (! RJval) return 0; rFilesReset(); // clear restore files vGetFiles(); // get DVD/BRD files if (! Vnf) return 0; txwidget_append2(mLog,0,"\n""find all DVD/BRD files to restore \n"); for (ii = 0; ii < RJnx; ii++) // process include/exclude recs { strcpy(rjrec,RJinex[ii]); // next record txwidget_append2(mLog,0," %s \n",rjrec); // output nstat = inexParse(rjrec,rtype,fspec); // parse if (nstat == 0) continue; // comment repl_1str(fspec,fspecx,XFCC,"\\=","="); // replace "\=" with "=" in file name if (strmatch(rtype,"include")) // include filespec { ninc = 0; // count of included files for (jj = 0; jj < Vnf; jj++) // screen all DVD/BRD files { wstat = MatchWild(fspecx,Vrec[jj].file); if (wstat != 0) continue; Rrec[Rnf].file = zstrdup(Vrec[jj].file,"Rrec"); // add matching files Rnf++; ninc++; if (Rnf == maxfs) zappcrash("exceed %d files",maxfs); } txwidget_append2(mLog,0," %d files added \n",ninc); } if (strmatch(rtype,"exclude")) // exclude filespec { nexc = 0; for (jj = 0; jj < Rnf; jj++) // check included files (SO FAR) { if (! Rrec[jj].file) continue; wstat = MatchWild(fspecx,Rrec[jj].file); if (wstat != 0) continue; zfree(Rrec[jj].file); // remove matching files Rrec[jj].file = 0; nexc++; } txwidget_append2(mLog,0," %d files removed \n",nexc); } } ii = jj = 0; // repack after deletions while (ii < Rnf) { if (Rrec[ii].file == 0) ii++; else { if (ii > jj) { if (Rrec[jj].file) zfree(Rrec[jj].file); Rrec[jj].file = Rrec[ii].file; Rrec[ii].file = 0; } ii++; jj++; } } Rnf = jj; txwidget_append2(mLog,0," total file count: %d \n",Rnf); cc = strlen(RJfrom); // copy from: /home/.../ for (ii = 0; ii < Rnf; ii++) // get selected DVD/BRD files to restore { if (! strmatchN(Rrec[ii].file,RJfrom,cc)) { txwidget_append2(mLog,0," *** not under copy-from; %s \n",Rrec[ii].file); RJval = 0; // mark restore job invalid continue; } } SortFileList((ch *) Rrec,sizeof(rfrec),Rnf,'A'); // sort Rrec[Rnf] by Rrec[].file return 0; } // helper function for backups and reports // // compare disk and DVD/BRD files, set dispositions in Drec[] and Vrec[] arrays // n new on disk, not on DVD/BRD // d deleted on DVD/BRD, not on disk // m modified on both, but not equal // u unchanged on both, and equal int setFileDisps() { int dii, vii, comp; ch disp; double diff; dii = vii = 0; nnew = nmod = nunc = ndel = 0; Mbytes = 0.0; // total bytes, new and modified files while ((dii < Dnf) || (vii < Vnf)) // scan disk and DVD/BRD files parallel { if ((dii < Dnf) && (vii == Vnf)) comp = -1; // disk file after last DVD/BRD file else if ((dii == Dnf) && (vii < Vnf)) comp = +1; // DVD/BRD file after last disk file else comp = strcmp(Drec[dii].file,Vrec[vii].file); // compare disk and DVD/BRD file names if (comp < 0) { // unmatched disk file: new on disk Drec[dii].disp = 'n'; Mbytes += Drec[dii].size; // accumulate Mbytes nnew++; // count new files dii++; } else if (comp > 0) { // unmatched DVD/BRD file: not on disk Vrec[vii].disp = 'd'; ndel++; // count deleted files vii++; } else if (comp == 0) // file present on disk and DVD/BRD { disp = 'u'; // set initially unchanged if (Drec[dii].stat != Vrec[vii].stat) disp = 'm'; // fstat() statuses are different diff = fabs(Drec[dii].size - Vrec[vii].size); if (diff > 0) disp = 'm'; // sizes are different diff = fabs(Drec[dii].mtime - Vrec[vii].mtime); if (diff > modtimetolr) disp = 'm'; // mod times are different Drec[dii].disp = Vrec[vii].disp = disp; if (disp == 'u') nunc++; // count unchanged files if (disp == 'm') nmod++; // count modified files if (disp == 'm') Mbytes += Drec[dii].size; // and accumulate Mbytes dii++; vii++; } } Mfiles = nnew + nmod + ndel; return 0; } // Sort file list in memory (disk files, DVD/BRD files, restore files). // Sort ascii sequence, or sort subdirectories in a directory before files. int SortFileList(ch * recs, int RL, int NR, ch sort) { HeapSortUcomp fcompA, fcompD; // compare filespecs functions if (sort == 'A') HeapSort(recs,RL,NR,fcompA); // normal ascii compare if (sort == 'D') HeapSort(recs,RL,NR,fcompD); // compare directories first return 0; } int fcompA(ch *rec1, ch *rec2) // ascii comparison { dfrec *r1 = (dfrec *) rec1; dfrec *r2 = (dfrec *) rec2; return strcmp(r1->file,r2->file); } int fcompD(ch *rec1, ch *rec2) // special compare filenames { // subdirectories in a directory are dfrec *r1 = (dfrec *) rec1; // less than files in the directory dfrec *r2 = (dfrec *) rec2; return filecomp(r1->file,r2->file); } int filecomp(ch *file1, ch *file2) // special compare filenames { // subdirectories compare before files ch *pp1, *pp10, *pp2, *pp20; ch slash = '/'; int cc1, cc2, comp; pp1 = file1; // first directory level or file pp2 = file2; while (true) { pp10 = strchr(pp1,slash); // find next slash pp20 = strchr(pp2,slash); if (pp10 && pp20) { // both are directories cc1 = pp10 - pp1; cc2 = pp20 - pp2; if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1); // compare the directories else comp = strncmp(pp1,pp2,cc2); if (comp) return comp; else if (cc1 != cc2) return (cc1 - cc2); pp1 = pp10 + 1; // equal, check next level pp2 = pp20 + 1; continue; } if (pp10 && ! pp20) return -1; // only one is a directory, if (pp20 && ! pp10) return 1; // the directory is first comp = strcmp(pp1,pp2); // both are files, compare return comp; } } // reset all backup job data and free allocated memory int BJreset() { for (int ii = 0; ii < BJnx; ii++) zfree(BJinex[ii]); BJnx = 0; *BJbmode = *BJvmode = 0; BJval = BJmod = 0; dFilesReset(); // reset dependent disk file data return 0; } // reset all restore job data and free allocated memory int RJreset() { for (int ii = 0; ii < RJnx; ii++) zfree(RJinex[ii]); RJnx = 0; RJval = 0; rFilesReset(); // reset dependent disk file data return 0; } // reset all file data and free allocated memory int dFilesReset() { // disk files data for (int ii = 0; ii < Dnf; ii++) { zfree(Drec[ii].file); Drec[ii].file = 0; } Dnf = 0; Dbytes = Dbytes2 = Mbytes = 0.0; return 0; } int vFilesReset() { // DVD/BRD files data for (int ii = 0; ii < Vnf; ii++) { zfree(Vrec[ii].file); Vrec[ii].file = 0; } Vnf = 0; Vbytes = Mbytes = 0.0; return 0; } int rFilesReset() { // DVD/BRD restore files data for (int ii = 0; ii < Rnf; ii++) { zfree(Rrec[ii].file); Rrec[ii].file = 0; } Rnf = 0; return 0; } // helper function to copy a file from DVD/BRD to disk ch *copyfile(ch *vfile, ch *dfile) { ch vfile1[XFCC], vfilex[XFCC]; int fid1, fid2, err, rcc; ch *pp, buff[vrcc]; ch *errmess; STATB fstat; struct timeval ftimes[2]; strcpy(vfile1,dvdmp); // prepend DVD/BRD mount point strcat(vfile1,vfile); repl_1str(vfile1,vfilex,XFCC,"=","\\="); // replace "=" with "\=" in DVD/BRD file fid1 = open(vfilex,O_RDONLY+O_NOATIME+O_LARGEFILE); // open input file if (fid1 == -1) return strerror(errno); fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file if (fid2 == -1 && errno == ENOENT) { pp = dfile; while (true) { // create one or more directories, pp = strchr(pp+1,'/'); // one level at a time if (! pp) break; *pp = 0; err = mkdir(dfile,0700); if (! err) chmod(dfile,0700); *pp = '/'; if (err) { if (errno == EEXIST) continue; errmess = strerror(errno); close(fid1); return errmess; } } fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file again } if (fid2 == -1) { errmess = strerror(errno); close(fid1); return errmess; } while (true) { rcc = read(fid1,buff,vrcc); // read huge blocks if (rcc == 0) break; if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } rcc = write(fid2,buff,rcc); // write blocks if (rcc == -1) { errmess = strerror(errno); close(fid1); close(fid2); return errmess; } } close(fid1); close(fid2); stat(vfilex,&fstat); // get input file attributes ftimes[0].tv_sec = fstat.st_atime; // conv. access times to microsecs ftimes[0].tv_usec = fstat.st_atim.tv_nsec / 1000; ftimes[1].tv_sec = fstat.st_mtime; ftimes[1].tv_usec = fstat.st_mtim.tv_nsec / 1000; chmod(dfile,fstat.st_mode); // set output file attributes err = chown(dfile,fstat.st_uid,fstat.st_gid); // (if supported by file system) if (err) printf("error: %s \n",strerror(err)); utimes(dfile,ftimes); return 0; } // Verify helper function // Verify that file on BRD/DVD medium is readable, return its length. // Optionally compare backup file to current file, byte for byte. // return: 0: OK 1: open error 2: read error 3: compare fail ch *checkFile(ch *dfile, int compf, double &tcc) { int vfid = 0, dfid = 0; int err, vcc, dcc, cmperr = 0; int open_flags = O_RDONLY+O_NOATIME+O_LARGEFILE; // O_DIRECT not allowed for DVD/BRD ch vfile[XFCC], *vbuff = 0, *dbuff = 0; ch *errmess = 0; double dtime, vtime; STATB filestat; tcc = 0.0; strcpy(vfile,dvdmp); // prepend mount point vcc = XFCC - dvdmpcc; repl_1str(dfile,vfile+dvdmpcc,vcc,"=","\\="); // replace "=" with "\=" in DVD/BRD file err = lstat(vfile,&filestat); // check symlinks but do not follow if (err) return strerror(errno); if (S_ISLNK(filestat.st_mode)) return 0; if (compf) goto comparefiles; vfid = open(vfile,open_flags); // open DVD/BRD file if (vfid == -1) return strerror(errno); err = posix_memalign((void**) &vbuff,512,vrcc); // get 512-aligned buffer if (err) zappcrash("memory allocation failure"); while (1) // read DVD/BRD file { vcc = read(vfid,vbuff,vrcc); if (vcc == 0) break; if (vcc == -1) { errmess = strerror(errno); break; } tcc += vcc; // accumulate length if (checkKillPause()) break; zmainloop(10); // keep gtk alive } goto cleanup; comparefiles: vfid = open(vfile,open_flags); // open DVD/BRD file if (vfid == -1) return strerror(errno); dfid = open(dfile,open_flags); // open corresp. disk file if (dfid == -1) { errmess = strerror(errno); goto cleanup; } err = posix_memalign((void**) &vbuff,512,vrcc); // get 512-aligned buffers if (err) zappcrash("memory allocation failure"); err = posix_memalign((void**) &dbuff,512,vrcc); if (err) zappcrash("memory allocation failure"); while (1) { vcc = read(vfid,vbuff,vrcc); // read two files if (vcc == -1) { errmess = strerror(errno); goto cleanup; } dcc = read(dfid,dbuff,vrcc); if (dcc == -1) { errmess = strerror(errno); goto cleanup; } if (vcc != dcc) cmperr++; // compare buffers if (memcmp(vbuff,dbuff,vcc)) cmperr++; tcc += vcc; // accumulate length if (vcc == 0) break; if (dcc == 0) break; if (checkKillPause()) break; zmainloop(5); // keep gtk alive } if (vcc != dcc) cmperr++; if (cmperr) { // compare error stat(dfile,&filestat); dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // file modified since snapshot? stat(vfile,&filestat); vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; if (fabs(dtime-vtime) < modtimetolr) errmess = "compare error"; // no, a real compare error } cleanup: if (vfid) close(vfid); // close files if (dfid) close(dfid); if (vbuff) free(vbuff); // free buffers if (dbuff) free(dbuff); return errmess; } // track current /directory/.../filename.ext on logging window // display directory and file names in overlay mode (no scrolling) int track_filespec(ch *filespec) { int cc; ch pdirk[300], pfile[300], *pp; if (! Fgui) { printf(" %s \n",filespec); return 0; } pp = (ch *) strrchr(filespec+1,'/'); // parse directory/filename if (pp) { cc = pp - filespec + 2; strncpy0(pdirk,filespec,cc); strncpy0(pfile,pp+1,299); } else { strcpy(pdirk," "); strncpy0(pfile,filespec,299); } txwidget_replace(mLog,0,-2," %s \n",kleenex(pdirk)); // output /directory txwidget_replace(mLog,0,-1," %s \n",kleenex(pfile)); // filename return 0; } // log error message and scroll down to prevent it from being overlaid int track_filespec_err(ch *filespec, ch *errmess) { if (Fgui) { txwidget_replace(mLog,0,-2," *** %s %s \n",errmess,kleenex(filespec)); txwidget_append2(mLog,0," \n"); } else printf(" %s %s \n",errmess,filespec); return 0; } // remove special characters in exotic file names causing havoc in output formatting ch *kleenex(ch *name) { static ch name2[1000]; strncpy0(name2,name,999); for (int ii = 0; name2[ii]; ii++) if (name2[ii] >= 8 && name2[ii] <= 13) // screen out formatting chars. name2[ii] = '?'; return name2; } // do shell command (subprocess) and echo outputs to log window // returns command status: 0 = OK, +N = error // compensate for growisofs failure not always indicated as bad status // depends on growisofs output being in english int do_shell(ch *pname, ch *command) { int scroll, pscroll; int err = 0, gerr = 0; ch *pp, buff[1000]; ch *errmess; FILE *fid; static pollfd *pfd = 0; // 8.1 if (! pfd) pfd = (pollfd *) zmalloc(sizeof(pollfd),"pollfd"); txwidget_append2(mLog,1,"\n""shell: %s \n",command); strncpy0(subprocName,pname,20); // set subprocess name, active if (strmatch(pname,"growisofs")) track_growisofs_files(0); // initialize progress tracker scroll = pscroll = 1; txwidget_scroll(mLog,-1); // scroll to last line fid = popen(command,"r"); if (! fid) { err = errno; goto checkstat; } while (true) { pfd->fd = fid->_fileno; pfd->events = POLLIN + POLLHUP; pfd->revents = 0; while (! poll(pfd,1,10000)) { // wait for data or 10 seconds 8.1 zmainloop(); txwidget_append2(mLog,0," waiting ...\n"); } pp = fgets_trim(buff,1000,fid); if (! pp) break; pscroll = scroll; scroll = 1; if (strmatch(pname,"growisofs")) { // growisofs output if (track_growisofs_files(buff)) scroll = 0; // conv. % done into file position if (strstr(buff,"genisoimage:")) gerr = 999; // trap errors not reported in if (strstr(buff,"mkisofs:")) gerr = 998; // flakey growisofs status if (strstr(buff,"failed")) gerr = 997; if (strstr(buff,"media is not recognized")) gerr = 996; } if (strstr(buff,"formatting")) scroll = 0; // dvd+rw-format output if (scroll) { // output to next line txwidget_append2(mLog,0," %s: %s \n",pname,kleenex(buff)); zsleep(0.002); // throttle output a little } else if (Fgui) { // supress output in batch mode if (pscroll) txwidget_append2(mLog,0,"\n"); // transition from scroll to overlay txwidget_replace(mLog,0,-1," %s: %s \n",pname,kleenex(buff)); // output, overlay prior output } if (killFlag) { txwidget_append2(mLog,0,"*** %s \n","pkill %s",subprocName); err = zshell(0,buff); } zmainloop(); while (pauseFlag) zmainsleep(0.2); } pclose(fid); checkstat: errmess = 0; if (err) errmess = strerror(err); if (strmatch(pname,"growisofs")) { err = gerr; if (err == 997) err = 0; // growisofs likes 997 if (err) errmess = "growisofs failure"; } if (err) txwidget_append2(mLog,0," %s status: %d %s \n", pname, err, errmess); else txwidget_append2(mLog,0," %s status: OK \n",pname); *subprocName = 0; // no longer active if (err) commFail++; return err; } // Convert "% done" from growisofs into corresponding position in list of files being copied. // Incremental backups start with % done = (initial DVD/BRD space used) / (final DVD/BRD space used). int track_growisofs_files(ch * buff) { static double bbytes, gpct0, gpct; static int dii, dii2, err; static ch *dfile; if (! buff) { // initialize dii = 0; dii2 = -1; bbytes = 0; dfile = (ch *) ""; return 0; } if (! strstr(buff,"% done")) return 0; // not a % done record err = convSD(buff,gpct,0.0,100.0); // get % done, 0-100 if (err > 1) return 0; if (strmatch(mbmode,"full")) { // full backup, possibly > 1 DVD/BRD while (dii < Dnf) { if (bbytes/Dbytes2 > gpct/100) break; // exit if enough bytes bbytes += Drec[dii].size; // sum files dii2 = dii; dii++; } } else { // incremental backup if (bbytes == 0) gpct0 = gpct; // establish base % done while (dii < Dnf) { if (bbytes/Mbytes > (gpct-gpct0)/(100-gpct0)) break; // exit if enough bytes if (Drec[dii].disp == 'n' || Drec[dii].disp == 'm') { bbytes += Drec[dii].size; // sum new and modified files dii2 = dii; } dii++; } } if (dii2 > -1) dfile = Drec[dii2].file; // file corresponding to byte count snprintf(buff,999,"%6.1f %c %s",gpct,'%',dfile); // nn.n % /directory/.../filename return 1; } // supply unused zdialog callback function void KBevent(GdkEventKey *event) { return; } dkopp/dkopp.png0000664000175000017500000003145615002404054012463 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójS8eXIfMM*JR(‡iZHH0232‘’›œ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Markup Text|fotocx-25.1: Color Mode: mode:2 blend:1.00|¦ì¶á IDATxœí{˜\U•·ß.šû‹1bl†|cˆ…Aäfä"WFPD‘Û`D@>ä&0\e‘Û " ¢ƒ F¹b 1BÄ1b1´™Ll›:ß¿*ººººë¬}ö¹Tõ~Ÿ§ž¦È9gïª:kµ×Z{­Žr¹L ¥L©¼6&›TþN¨yÆ]@ 謼ú+¯2°X¬ÖÔ¼Vªü]¬¬¼zSþ|é  % Lf[Ó*¯Í‘pçÉày`yåõ °X†I ÀP<º€ÙÀöÀ»YÀV <­[…~àY`ð4ð `!²0!(€üévvvfÒz—~`1°ø)ð(ГëŒF9AdOø»Ûä;ÜY< ü)„`!dHPÙ0ØØØ)ÀPÖßî^Ìu6£€ Òc pp 0yÞñ)Oww¢ˆCÀ3Aøecà à´žBï‡2òÜܼ’ïtÚ‡ ’SöŽ>‚BvôXÜÜ<ˆ”CÀ‘ Ü™| ˜šïTF-+€ë€€—òJk€9Àç©?&ç¹DZ\<•ó\ZŠ âQBæý©hmßÊô1¡×_ù[Í;Kë+µÀh™nî&02ÀÁHðgä<—áX‹Rq_¬¼þ€Ìáä,{¥ëö"Áïo|™×éDŠ`{<¦ èË'ÈfI¶- nÈ`¬B1ÚÀ à`Ç”ÇYþF´ž¸3 ¥YLú û1àÓŒ¢eÁhQÈÌ;t]®î&”ÃòÍX`?àxÒUà}À×Ðò°Ý±C `6z§eJ®GOûË Oû¬˜œˆ¬‚´6_-F–Ç”®_ÚYtg dž4žúk€+à‡²VùÐÁ ¤Sµ%G›Zíª¦¡½ãsS¸vp!ò¯Máú;ãP4çÒÉ@|ÕxXžÂµs¥ÀÁhî;ë¬8¸–P·®¨tǧã_ô"ÿ힯›+í¤ºàÒóu{ÑÿR¿Uœ„,ß‚›"h‹‡@»(€­P‚Ï »}ÀõÀ|Ö ´³cðëZ‚–žõxÍ\h°òòûÔô¡¢Ë<^3ÓQ±Ý=^³E îöxÍÌi墕%«½¿iö„¿X†~ÓýÑoìƒñèÞ;‡–£VµºPzíÞž®×Öøói“µ]`XºÐ²à$üí¹¥-·Ü½ÓŠ `2ð}” ⃅ÀÑ„$žÑÆ,à›(QÌ‹€ÒbuZÍt™… døþõ(Ih;‚ðF¡ßþLü¤mû¼73£•,€Ý§ßÇz1pXåo 0øü¤‹÷"_Ãî•:­bÌC…3’ ¸iþ ü*‹Ñ=q1ÉË…G÷꼤“Ê‚V°Ž®$¹²Z Žªðñ'ðm’o=.ŸA)ã…¥èÀi(»/é<ï& 9÷£{eAÂë”н{Zâ¥H‘À—Qî}R.v¦Å¼³\yÝ3{¸Öùè^.$E]œü[Âk¬Cá½Û’O'UJ¨Bî\—^Œ V¶åöÓd v%¼ÎWQQšBQDðe”¨‘„}(v1‡i(•ôPT¯–åhûéYO*ÐÙÀ÷PJæ_I>MœFr³ÿ)$ü«’OÇ;¨Çàñ(¬9Ò¬xm°á¤MØ)9 ¯s:*9VФŽCN“$܃žœEKÉv¤}Ž¡Oû‘¸e—ŠA*4³oÂëOA¢EQóЛÄ)y5 »âU˜„šO‹[C?ðfB‚"QBaéã\£ŒT¹û§ŠØ Å]“Ìe>ÒªEþMÑöÓß_Ä=©)‘@q(£{m~‚k”Ð=¿›—% o `ðSܤŒÌê+¼Í(£'Ük ú|oa”v­iN@ŠÞõáՋ¹íEÉSLF›'6u<¿ŒÂ|ßò5¡Œ>‹6–ø¬Nû(ºAÅå&tU«Pr.y*y)€.àg¸ïœêG!´›½ÍÈ}QsÑÍ=_· ÿ/<_7àŸCQU*×ú‹€÷‘ƒó:@ óH"ü‡“¿ðoŽ6}ü'þ… *0„¿5¸=\“·f!™È\óPgã^ɧ |Š|K3w_žAG|²ù¶ ÅkÍBnF÷¦«I½7ÉàÌd½ØÕQsU<'’¯Ão&2õ|U‘™}·¡ˆ§<^7' nQ.”ÉPùg©¶žÄÝãŸge =õÏÆ_yéçÑò-‚—¿ÝH’ΞihV   yü]ëö_b¯y0U‹ñÕ‘vJw¾›°á§¹ ÷d¡%(2ºS0+ÀU¸ ÿ=(Ã/öžÆð?ì…öšßNþvç3èÞua’§ÅÇ" àPôuá) Ë:<Ò \‚ÖsIY‚¶ºÞ Ö¥ %º¹n :Œ”£]i+€iÀ/q[÷¯DfÐK^gÔœM‘£r‡„×éA‰A7Pœå@öLBË_Ë&°*½À¶¤Ø•8Í%@'zò»ÿ:àcd/üs‘£2‰ð÷_G¡¼ë Â?Úy ÝË.Vìx$C¾˜ !Mp°½ã¹G“}1yÈ\sMMx­ñO!ìà Pm>ãÂö¤XW0­%Àlà縅Ì.F[h³ä ’õx[ƒ„þzo3 ´#';œ×¼—Ši(€N´îwi²°9ý²òûØÛý Ê EGÍèDV¦Ës1òx•4–gá&ü=( *+ნ}®Â¿…z>Lþ@<úÑ=ÞãpîLR(*êÛ˜âæVÓ¿ŒJ_eU·¿ mâqí¿ø8Êݬì‰ÜZÀ}ÈÇäí¾óm\ƒÛºÿR²þpþo¡ðdþ€+÷£{ÞÊ$cÞði… #XY‚rŸ}thmFUøwr8w=ðy RÌ1ÐòŒE!g— Ù£Q~Ib|)€ ÀsØû©õ‘]{î±Høwq8·År“¶‹ j©¶·ZÍ«-ñ°‰Ì×àÜš)žC6Â߉~»8œ[ݘ„?à›EH¬Lt,€mãÏš­´ VÚ^ÿʦ:ØáÜŸ '¯×ÙªÇNBYeç<Ÿ€_:‘`­1Ñ‚K’ îø»ð÷£uL!¿ pþÛPˆ//aÛå(ü¸«òßwÀÏ&¥@1p•…N${‰Hªvöp8ïb²1ýO@uù­\‹7ôùN,f &)¿þW÷ïãQ!—Ï(&‹pëD¼îÑ, Ù „LkÒÏ `kÒßâ»еZ­“-ôã¦á¬|"±pª8œÕøï•¸…O«¼ìg:ñyìÁ­LšqQ¨Ò•kI?}ö4ì=îA»ù²`,p.ÊKÚg´1Ð^,E²båL¬'à'ÑSÖB/rX­¶f`´ÕÒò%üØ•ljÎDù.~“FÜŠ"öc"ê,m]ÆÜd9Áj”Pç+’®ðO@­•,Âÿ<Ú˜…ð™ü¾„ÿn²³ZÙ³·ÿ©eÚjìlV#gUš%²nÁ–ìÓ‹*¬¤½$™€¬%×Vhµ”Qà…•¿öfð{ì>¢}€{ãlµ\žþç“®ð„MøËd³—&ªŒ”TøËÀ(Öû!‚ðÖ"Ù±b’Q‹09¯,ô §Zfö$”<±±áœ,ZŒí‡žüõY|VîEU`'žQ éBV@·ñ¼÷³Ï¤Åp á]Hºkì˰ ÿýÀWSšK•ÓÐÎÃ$¿ÕF܇ ü£™u¸ùbËj\ `ð¶d•5Àf¤gþ[ý«€w‘ž3ÒGÑW€Ó ýŒCÀ&Îéþ/1újĵŽÂž©vé ¶¸”QÈ,-áOZ`´ûðŸP 8 ÊZ$KÆ ™mJ  „b’S X4KõÓ8\€ÚuÇåë¨nŒEF]vE‚”ҧɰ'| åèFøXÃ9+Pî͈ÇØû¥[IOø·B¹óq©6çLƒ.´ãÐUøþ™ ü‘éA2ea*1îË8 àHãÀ ç\Z\DüåH?šG«OþÝÎíGký½È¾ÿa 5q‘©¦²Ûl °1ªHc1=Þo8ÞÂŽOËô¯ÖÜ×áÜÕ¨9Ä#>'ü7°£áøõÀÛ‘s¹!Í,€ƒ° ?ÀUÆã-XB"Ï£˜\…›ðWK ?âu6Ñ‚U¶ÆÒ¤V@3`Ýl²šôÖ³`+œ˜Vᑎq8ïaà}È9¸p7öHÖˆ2<’˜‚½‰á­¤³Þ.a+ƒ|?†|hóp+„r+Zï‡j¾$¬Çî ÜÉrCFR4ù÷FÜh<>.ÓcÛG:•}æ ÎGÖïäjà0ò)0h?¬2VB²<ì?ÇÆ“N¥ß¶$×Ë<Ïa"*ÉÝe<ï àxBbOÀ‹°§‡+ËÃ)€ÉÀ\ã ß1—½‰ß?m-n&úH”Ðvãaͨaøw²-3=Xem.’é! §>2¿5¢Œ}mKïRü§ûž†½öúÍá¤Ç­Ø¬Ê’é†ÿЈ'ô°ÒxNæß¹% ùd{ìŃ(#˜ý´XIÌí¾54”éF   %ÜX¸Ãx|\,[¯ÀC·ÔºÐž~K™±Åh½•E˳ÀèÆ*s¤«‘Ø{òO±ÿnâ7¾X‹škøäl¥Ï{–M³úQ PÅ*sciл‘øã…—¢¬;ßCüœÿ!ÝÑQÓŽ¸ôû“Î2(hÄóØËÚ ‘íF Àº¹å~ãñq‰[õ¶¿›Æ×`s‚žŠö@Yb•½!²]“w?äVåÆãã°ñ· ߇_ ädÔ[0.÷ èC 5VÙÛ†ºú‚õ `'ã× ŒçÄÁ²Ùçæ£M±%­B½Ý [ƒÅoQÜÞ|%”?ñ1Tf:ö= e”Rý *|²€ÛPD~ŒM ’ñÚ‚ ]À«ØâÞ±ëø.ñú÷£ž/z÷Äÿ"_މ߼LŽEÔ©ž¯½¸íµ–Aq°öëèÞDe«|íSa.6áïÅÿ柱ÄÂGð'ü;Æ%(Iø»Q$äÔ}xj cL­\û…ÊXÖftX„m›y'5û|j€µ]õSøÏxÛ…økðïz×R4ôQT»”PÔâ9Ô€Ôº[Ñ…®ÊXÏUÆvi1ðG?v+üuY¯ýñÞm¼È/ŒÇÇaŸ˜Çõá/ûp6ñ«ú–Ïy7)[¡ŽÃao#íƒñ•±Ç–1ðU_—õZ`)·úá}WÁŸ÷ÝÒLñfÒ©y`ejýÏÅŸð¯@Jtá£ñ( z:~BˆÇ '’%­:Œ¥Ø¬è­a@X×ÿÏÃ/‰w“?ài«°Üx|Ö .&Ã5yø8~’ö&¾ó/ÍVg#Q­Fì"ü7["l‡ Tw®CUP·Å^}8¶ˆyÜjvNeÌIØãü½Èt?¿íÙ{*×<{w£9ØÚ¹Üx ÛÖàñÀøöz÷i”ÿªe1j£õF`´qáü:á^yܤÓꬱç¬ÞKº ëÎÊÖ(Ì|â;\îXesŠ‹ðÿŽ>¤ÕÒ¨°·†@Z­Îšq&6«l%ÚçEtfie,‹OˆdU6§”Pø'ÍAŠÈS¨”×HÜO:ÕŽšQÝÒ—^àÃd¦\QÓ²8–`¤U67-aßÖùãñEåpkÄ#Ø[£ûâ8l»úŽ$Ÿ¼Œ¥ØJ·u¡ÏH«lv—€·Oòé\Ê“^T&y´Éçälü8°+ùì÷/aËO¸™tz2ÄåîÊâr4!"&VÙ|['v³,­@”ÑMœ§Õ²ñ3ñzI§ º•/ °jŸÅTôC õt°ÊæÄJÀ±Ð E0[•ý Ç~ƒbXc=h.q±|Æ€ «lN(¡ê°iˆOÜ\îõÀåiNÄÈåÄ—Z;-âc•Í«‰@ŠT ¯˜He‹f î§Oÿ*=ÄïR3 H «lŽ/a¯#š_¦Ãâ;ÈÒêÆœ„¸s*QŒJFíˆU6»\À:ãñxXj2 ¾ÌËœ¬õ'ñ°Ê¦Yô‘Nf^ þþ„Ëü¯ÒCüd¤¸Ÿ5`£[§®®¶¸¬ï6`âfd.KuɈ;7köi >-•°Õ OÿôˆŽ]•ê,’wnÖÐs >í´*€Ð.=Æ6?(¶6îÜâ~Ö€‹Œv†´Ì@`SÂf2…‘qiŠÔ ž¸sË£ÆÂhÁ"£ýV`Y.lÄMâ(²-îÜB2Yz˜|z%lk× ±æÄu ÅÍ̃¸s+²#³Õ±Èh¹„-y` Á H‹ßÅ¥ÖG VÙ\]þl<©Ov¥ |Ópü¡À~)Í%ûUæ—oö“¤‰U6ÿ\¾·|3ãñ×b‹ÌÜH>6f`뜴¸:¥¹„U6{Jس²&ØèAJ .㈟„ラ•1-õ$¯%xÿÓÆ*›«JØ=>çb ÏN~J6–ÀŒÊX–ž’½è3ÒÅ*›+]ÀæÆãvVgÏ™ü8Àÿt^ç€ÊÖ†²gžþY`•Í•årÔ.ÛbÎmB{u*"%àqÜ*èÞœ‚¿ÚÝÀ…À'Î} ØŽàüK›IÀŸ Ç÷oªî^nlšñø€2jPêRè“ÀsÈìNµé®\ã9Ü„-ú AøÓÇ*“Ëa x€UyKj;ñ,ð)ÇsÇg Ž±wóˆ§ º+ÇÞU9÷ ìÍcª| }†@úXer9 líµ*€­Çܹ øg$ˆ.ŒAñúj¾À ”¿ŠGãxTÌc:þ¢ 硹²Á*“ƒÀ3Æ“Cc‡l9=™ñp­©¤2¼Í9V™|–ÖýÙ³ŒÇ’ói$XEçz4×@¶Xer) (€eØjvò²¦ŒÖÔçå=‘8Í18ý²e26go?•­çU°»`¶ñø€Î>A±ú¬Es f>Xeq9•Ê̵%„/²ñø€?n¶¥•užBs ¿ü°Êâë²^«ž6^d{ãñ¿<‹~ø/`¯êäƒÞÊØÛB}yc•Å×e½VXŸ&s‚ó¦ \ l |ƒlZ·¯«Œµeeì°ÞÏ—NìÙ¢¯Ëz5TQôUlBýŠa†ÄDàX䈛êùÚ+€ë»úŠÆàIÃñýÀ›¨<,j-€uÀãà;¤Ëjä‰ßظ…{\žÒåʹ—V®µEåÚAø‹…U—Pc)Ö?íÃOÜ™bQ­¼@–Á”,²Ú8²1…<×£â°/¡¦K‘e„½øìj<þ±Ú7µKÐvÏ; [ ¼è3N"$g ðlý©ï$ú(6Æ;Ï ~Ø{3A2^¯z°ûö2ü`•½%ÔÕˆhÔKüaãEãv„ ~±ÊÞÙn¤~d¼è B™°@ k6Ǿpˆl7RPÉ6gmú@`4b•¹õH¶ÑH¬~b¼x»ÕíŒUæ~BƒLÑF àûÆ‹ÏÁ^)6¸1{úoC™.í÷>àJ†Wõ”€ƒ¯'•+yO!ÐDQ”÷ê9˜ø² J »¯Ñ? w‘'Œ“ú„ñø@ à†UÖž@2=„‘´ˆ%#`&¡TX 6³¬YV–GRwbßDr¤ñø@ `Ã*cejRëI¬ÄÞËý`6˜Œv.¢Ê« |¤Á1sÔgµº£‹±HÆ,,`„öÍ ·›HÈ Òb?$cF”áf àvìIAÇñ°ÊÖz$ÃÃÒL¼Â0áƒØ»“"ŒÌL$[îC2<,qb‰7øœÃ9@`x\dª©ìÆQ¢zp&YWÚ@ 0@7vçß $»#G”Q1H cçÆœˆ=ºv1ÂøqÓ oÀ^öëìÕJÀ`Æ!Y²Ð‡d¶)qÀKŒL0 €ãŒçÁ‡dÉÂHf›bép öuÈ)À¿“MÊ,ØõBØC} }Ñ¿ÓM2:€Þ ¼™–ë€? –Q¿ó<ÞTÔ=h3ôýý5¦|øŸ'Kº Y¹$ö‘årÙòúY¹\ŽŒ¯“cdöŠ¢(îkRE—EQôר1å(Š~EщQmX9çòºÿHƒë΢è5ÇÝc.QEÑk5ç½EÑ9•î¼qQ})Š¢ßUæ3ÜçøMEŸ¢è 1æR?ÿ£kþm·(Š~6ÂXk£(º!Š¢Í 4NÃWŽ÷èÉòö3Ë–-…8Ööì üø,Ûbè tª»öÖ”æÒ̾ÂÀò­ |¹ònßêvÀ"ôûm^¹ÎpןŽ>ÇSÀ»æØ \üU­n¬ÿƒòÚ‡xœ¼‡dÇŠIF­ à>Ô4ÂÂDà$ã9Eáh=7ý²uj¹ÿJ¯uæ9‹ßí5àK•ÿ?œðÒÆñf R–²ï%ÔIèóÄ¿·Þ|ø×Ž“''aOû]Š1qϪʸY§`ÿ0y3…R6¬ûÿÏg AŸŽä è˯ ávø­P¾œÊÀoÖ’C.bxáŸ|‡ÁÊ(B‚}šÿ$$ìG #µ×š€RI'Çœç!(]µú4~)¬÷WÆzÚ_Õ³ê0µsÁÆÉ‹‰¸­ý/Àê‡rX—t–ËåçÖ&Wæ½æ7ø6Œ¢èñºµäkQ] ¿6Þ(Š¢ £Ákó*I|¥Hþ„Ú5n_EÇ4Y»–¢(z n¢èØhx_AGE'×Í)Š¢èÖaΩŸíç½%Š¢ñÃŒÓEÑQõ×÷ë(ŠÆæ8NQ|W:È×sÙ4å:Á#&ør¹<#o¡©ˆ r9Š¢‹‡‚zªÖêù. `ƒ(Š®®»Þߣ(:,Æûºÿµ(ŠNˆyÓŸ U8ïŠ1ÿ*÷DÎБ^§FC¿çÃs§ `FEV¬òu„ËxÖ%@•›gçtb OäGp4ƒ—G¿Bf³âppöï¦h r,¦îzdºÿGŒóA&o•G«bŽýÿPX³Ê†Ä/Dñdžÿ#Ʊ_þ«æ}pñ–¦Y“5—` σ]sýúb¼ ŒXIDAT‘7ÚÊîÀ¾ŽcfÅÆh-Y¥ œOümÑÿƒ¼èI*Inˆ6rÁ€ð¯æÑd{g….àC5ïËÈWðZÌñÿÎÐϰCý!¸øSÌq^CÌÚym‡üE'KöE2be>’I3I4àí ~JÄåtƒ•ÙÀjÞÿ¸ßx{¹°Òæ‡08´õsâ—kŸÉàåjì½îþ·æýš—~ïÇ^Õè1W¬ƒ*%aœ,éÂÍB^D¼‡BC’(€2nžÊ©¸YY1ÁßË“Ø3É^~ë0ö×þ@†Æµ?@üÈÂ;ü2X˜ãð ð›š÷(ƒp$zQ΄…>¤ÜªtмåUVãdÉ|$VN%AjÒ5ÐCÄØrØ€“)náMëÞÿ»9ß<ï0öžÈ ¬ í[Bkó·Å¸Îf V Ïbÿ ¯ËëÆoü=öMc ”ÝZ6+È8Y1 É„•‘ :ãà r öõG'Jʰ:;² >çeÇë¬v8§ƒÁ} ­ãk}›¢æ+Í:š¼¹î}OãšSÞ[šÿWÜžFõã4Ûü’Õ8Yà* ý¸Yàƒð¡–W;œ7yÌ‹F½p99WˆçnD„Öë{¡ ¾Küô>ŒÁ¾FŒ©{ïê¨_64Û“×ÉXOýÓ¼~þy“§!Y°r5’½Dø ƒœ…Ûï,Š·¨ß¹èšÒëâèŒY·7Z炼׵kñ €oãG¸N½òÙÈa.0ÔëßL©uÑÜ:î¼Zš)¬¬ÆI›YH¬¬v»å¾:ŠÁ~–5h)T„qÒ`St¯»ÈÞ×°oÇ‘4r¡ÏÁ-C°ewåìG5j—Ç¿ÙÉlä©Oƒ»ÿ¬yßö(¼³î¸µ ÎüÛ)Џ¿÷†h;mí²áAâ9ÍÞOüöp›_¬çû V^yã“Nt»”Ì_ŒdË+i(€~´qÄe)°nõ|s=ðjÍûq(½·ÙR` JË|C“ã\)#+ 6âòFÔˆ´ÖO¡§L­µ°J/ŽÃ)¨B•~«ŽÃÀµ¨vâHL¾ÇàÜ‚>á(Ò8>¹[•*}H¦\CÒÃ’Ön¨…¸;öNF›^òäÀ… ¶ÞRI?ÉPÏíÀ¨ FÕqVíú뛕À¿1X¸?€LÜZ`pêk íé©ÙJŸÍà§åPq͸¼øª¾Sï£èD‘ŽÇìcˆsëéŽãƒy¸eûdi¡Ç¹¼NG¹œZ!ÛNàg¸m¸X‡L¼T>t•ŽŽãcІ˜iðo¯ õcº ·E¡©*}(·ûô=D(¶__®i.úŽªËž;Ñ>€ft¢ÐÔü¿—Ñò£ÖQ8 m®]Ë—ÑRâºÊgø2I߇–:`°ðÿx/=æõóí¦¬ï¯H_FOáÙ4v.¬ŒÝ(Ô˜Õ8 ‰¢Äz|6ð߸å†<~ïOÀ¹ HÜ×´r¹üªCqƒ¨\.¿P.—'åT¤úzsEDÃWœmD_¤ªµ³jŠXø¬ \}͈¢èoucß©PíqF*"âBoE»Œ0‡úùßEÑîÑðÕ“‡ãé(ŠÞ^€qÒ(2©r/»ÈÀ«JMÒ.ˆ°øŒã¹S£&Ï­ÃEkçóˆ×ÛàE´™ç›•ãUójôÄY‡œ;ÕcVæ¶eƒ-ª9 ¯ÝAN§}šàÔŒç‘õóˆñ¼¡uîc4_ýõØ™æ9y“„.t»$“dgyӣ栖o£µ³ ÷ µœ÷‰6YÔ³ r¢íŽrª¥¿_FÂw/ÚÇ¿Öã}òVäÜ;’á“hªÉ>× ‚ ͼäõ¦ùhC„–»¢f2; !ƒÂoË€7¯IVã4$r[”€»p/€s”1ÏJt1ÔcájTþÉ+FÐ.ŒAVÂ,:ý/ê ôKä‹»Ùf$ÁôIVã4ÄQ\…{k¼%(Ë3õŽZYÅÜס§ø“Œ¼‰e8ŽC埾âsR£”>­X÷DÚ˜/ã.ü½HV2i§—eQÄg‘ùéjrœ½Kj 5' {Õ…2’Eec‘uUÔ»‘CÍ•ËP¡Ì@ ˆîQWÎC2’y”Ežf.”PüÚÚ¥8H›Cѽé*S÷’C­Ì<@·t݉Չ¢ ‡z›Q ŒCQ굫Om’‰ÌÛËçÕaðQ`•ãùU%p„¯ ŽîEWá_…d!§_=yvFy…rb§dÖQB 7Á1È‹Ð=è*G½Hê ¸dFÞ­‘¡‡ËÎAÐü/gäòX@|Ý{®2Ô‡îý¼Š’Ù%5cp ÉÒÕ(u2ö¥‰@#u‰@%T‰É5κGnKp /ämT¹ ÷=UŽC©—En;hmºÐ=–DøA÷zîÂÅQ 'øé ¯±/ðSŠQZ,Ð^lŠî­¤ÍmOÇ­F*I€jž%…ÎAû\š-˜î©ú–VæãV335Šâ¨çTõ& 뀣)ˆ©hYæ!OÒ¥åWñÔÌÃ'E³ªœErK  uÚ½ˆü ZNtï|‡äÂ?Ÿ ?רrp¾‡ë,>NŽñÖ@K1ø.n<ë9‚™ýµ]€<®W’ÜZY ,ÜŸxFvfO”ÙçÚN­Jyû ãðkDQ—µ\b¦®ÉBU&¢òLQ¬^„b0Ýß'¹ð÷¡{¶Ð­aTÙ Å`] ŠÔ³5ïpi`h?f¢Fq›¿ŒDµ Çî•:­`TyutÝ@TËLÖ9ƒà Ít¢{àqüÿ*t¶„ðCkYUm–§ë-DáÂ\s²™3 …÷|å‹,B»úZÊÑÜJ@•Q£×¢"õÌFµ /$¤ºÐoý$þ„ÿ^tO¶”ðCk*P’ÏÇPr…¦5|†ø '­Ç~è7þ"~–~et~Œœöó'¥—õ쇪±øpVyøª+h}¦£Z}»{¼f/*à™i ?ß´ƒØ E\û4¢u žÏàn¼Öa"ªÐ{ Cº&a òôgV½7-ÚE€ÖvWáÞh8zÑšñRŠÛõ'0˜qÀI¨’OËÔ±çxZÔ䯧@•ƒ‘"ðýÃ÷ ´äki“¿ éŽEé·Ýž¯Ý‹ÿVÏ×Í•vTÓP…!—ÖäÍèAÁÕ‹ (ŒC)ã§à_ðA-º!åFyЮ ’<ÎÄïú¯Êà T®'…ëšÓ œˆŠsNHáú}À¹¨aG ×ÏvVUf£(L¯F¬Gfáå„d¢¬˜…ÿ`ÒÛ×±yù¦týB0È8 m/Nèòò?ÜCÀcQÈ÷x`ÇÇéCÛwÏ¡MŸúµŒPep éÞ@ °á­ÈòVA2f¡'ñÁ$ߥ׌ǀOKS§0Œ6På(àÒ¿¡@¦äwBX™ÁxíÀ$🠽¥[-«S2«PŒVrƒ¼ÇYì,O¡„¥;ç3³•Ø8%ØÌ!›4õ~Í9 9uG£YT™ Aì‘ñ¸KQu¢ ’eI ž´cPÉ­½Pžÿ ðF‘¹ßˆ Ø- ²09ëY < ü¸òwíç€êDëù€]+Çå0ÅÈÜ0‡± GPƒ)¡¥Á´çÑ‹’Ož@Å*Òz[M'£ìv(!k.þ³3-,G¦þíäІ»¨ИNÔóýLòUµô Ë`)ÚÒº ÝÔ/å9)`úަ[#S~édä¹°%óÜLûYU‰ `d:‘7úT²_£Æe-r(¾Xyý)…à•Êk ²*ÖÓ\:QÌ}‚ZñyÒÇ@’RU0ªQ±¤›(• àßG0õ›€9¨XÈA´¾°´ }hm µb€;“PBѧ€©ùNeÔ²¸%ðäí iI‚HN …DË„Ðt$]Ö#óþFÊ 7p‚ðËÆhipò´jÑÕ¢QFkû[©ÿJ¾Ói‚H)(µõ@ÊÀFåAÜR§Ã>Š &£t×}€]ý†cðð=”&ÝjÉO-GPÙÓ…Ò`?„úú¬dÜŠ,A­´~ˆÒ C½Å º‘BØù fÒ¾ý ûQ.þà§HàC9µ  xt¡úíw£´Ú­h=¥Ðêæ/ž~ö4„'|  5‹ríg |ûi•׿¤S Ó”м¼òz¥á.#”E+ãQÄa °)ªr´Iå×8¤HºPD¢“«¢¿ò*£'ôz´Ç`MÍk5ð§ÊßUÈ+¿í1´(ÿî“/÷ßãHhIEND®B`‚dkopp/dkopp.desktop0000664000175000017500000000030415002404054013334 0ustar micomico[Desktop Entry] Name=dkopp GenericName=Copy files to DVD Comment=Full and incremental backup to DVD with verify Categories=Utility;Archiving; Type=Application Terminal=false Exec=dkopp Icon=dkopp dkopp/data/0000775000175000017500000000000015002404054011540 5ustar micomicodkopp/data/userguide0000664000175000017500000010437315002404054013467 0ustar micomicoTOPIC: Dkopp User Guide TOPIC: Contents {Dkopp Introduction} {Dkopp Concepts} {License and Warranty} {Origin and Contact} {Dkopp First Tryout} {File Menu} {Backup Menu} {Verify Menu} {Report Menu} {Restore Menu} {DVD/BRD Menu} {Help Menu} {Toolbar Buttons} {Editing Backup Jobs} {Script Files} {Technical Notes} TOPIC: Dkopp Introduction Dkopp is a Linux utility program for copying files to recordable DVD or BlueRay (BRD) disk. Dkopp is a free open source Linux program licensed under the GNU General Public License v.3. Three kinds of backup are available: full, incremental, and accumulate. A full backup copies all specified files and leaves no other files on the DVD/BRD. An incremental backup adds only new or modified files to a prior dkopp copy, bringing it up to date. This is normally much faster than a full backup. Unmatched files on the DVD/BRD are deleted, so that the DVD/BRD is left exacty matching the source files. An accumulate backup is like an incremental backup, but unmatched files are not deleted. You select files to be copied using a GUI. You can navigate through the file system and select files or directories to include or exclude at any level in the hierarchy. These choices can be saved in a job file to automate recurring backups. If files are added or deleted within an included or excluded directory, the next dkopp run will include these changes automatically. DVD/BRDs can be verified three ways: full, incremental, and thorough. A full verify reads the entire DVD/BRD and reports any files having read errors. An incremental verify reads only those files that have been newly written by a preceding incremental backup. This is usually much faster while still offering a high level of security. A thorough verify reads every file on the DVD/BRD and makes a bytewise comparison with the corresponding disk files. This is normally unnecessary, but it provides additional assurance that hardware and software are working correctly. You can list all files in a backup job, or all files on a DVD/BRD. You can search for specific files using wildcards. You can compare a DVD/BRD with the corresponding backup job, listing all differences: files that have been created, deleted, or modified since the DVD/BRD copy was made. This comparison can be done at three levels: a detailed list of files, a directory level summary, or a job level summary. For disaster recovery or file transfer, dkopp has a file restore capability. You can select and restore DVD/BRD files to their original directories or anywhere else. An incremental backup updates a DVD/BRD made with a prior full backup. This simplifies both backup and restore: you do not need to track full and incremental backup media, and you do not need to restore files from multiple media in correct sequence. Searchable log files are generated with time/date, DVD/BRD label, and files copied. You can search the log to find all DVD/BRDs with copies of a desired file, using wildcards to simplify the search (e.g. find */joeblow/*/planB* ). A script file can be used to automate backups or run dkopp from a shell script. The GUI is not used in this case. Incremental backup and verify can take less than a minute if the updated files are within 30 megabytes or so. For larger jobs, the DVD/BRD speed determines the time required. With 4x DVD media, backup + verify runs about 150 megabytes per minute. Dkopp is a graphical front end for the command-line programs growisofs and genisoimage. The added functionality is ease of use, especially the easy way to specify the files to be copied. TOPIC: Dkopp Concepts The files in a backup job are specified with include and exclude statements. These have filespecs with optional wildcards placed almost anywhere. Examples: include /home/* # add user files include /shared/*/documents/* # add shared document files exclude */mp3/* # exclude files in mp3 directories exclude */.Trash/* # exclude trash files The first include adds all files owned by users in their home directories. The 2nd include adds all files under the /shared/ top directory that also have a subdirectory named /documents/. The two exclude statements exclude files within /.Trash/ and /mp3/ directories at any level. GUI interface: The above statements are normally generated using a standard Gnome file selection dialog. The process is documented in the section on editing backup jobs. Note that excludes are effective only against prior includes. They have no effect on following includes, which are processed afterwards. Restriction: include statements must include at least the first directory name (top-level) without wildcards. limitations: + max. 200,000 files in a backup job (compile time constant) + supports DVD/BRD media only (not CD media) + not useful for disk imaging (operating system backup) After installing dkopp, please perform the first tryout exercise (below). This may be all you need at first. You can enhance your security and ultimately save time if you read this entire document. TOPIC: License and Warranty Dkopp is a free program licensed under the GNU General Public License, Version 3 (from the Free Software Foundation). Dkopp is not warranted for any purpose whatsoever, but if you find a bug, I will try to fix it. TOPIC: Origin and Contact Dkopp originates from the author's web site at ^https://kornelix.net^. Other web sites may offer it for download. Modifications could have been made. If you have questions, suggestions or a bug to report: mkornelix@gmail.com TOPIC: Dkopp First Tryout The following short exercise will check that dkopp functions correctly on your system and help you become familiar with dkopp usage. 1. Load a recordable DVD/BRD and wait for the desktop icon to show up. 2. Start dkopp (use the system menu or a terminal command: $ dkopp). 3. Select button: [edit job] 4. Set the DVD/BRD device ID 5. Select full backup and full verify 6. Erase the default backup job shown (use the [clear] button) 7. Select the button [file chooser] at the bottom 8. Navigate and select some files or directories to be copied + double-click a directory to open it and select from its contents + select one or more files/directories + press [include] to include all selected items in the backup job + press [exclude] to exclude items previously included at a higher level + press [include] to include items previously excluded at a higher level + use the buttons at the top to go back up the directory hierarchy + press [hidden] to toggle the display of hidden files 9. Press [done] when finished selecting files 10. Inspect the generated include and exclude statements. These may be edited directly if desired (e.g. erase mistakes or redundancies, change the order, or make additions or revisions). Re-enter the file chooser dialog if wanted to append new choices. Cycle between the editor and file chooser as needed. 11. Select button [OK] when done editing the job 12. If errors are shown, select [edit job] and fix. Remember that exclude statements must follow relevant include statements - excludes remove prior includes, and includes may be exceptions to prior excludes. 13. Select menu: Report > get backup files. Inspect the counts. Be sure the total byte count is within the DVD/BRD capacity. Look for zero counts, indicating possible errors. Re-edit the job if needed. 14. Select button: [run job]. The backup should begin. 15. Verification should follow automatically. You will be asked to re-mount the DVD/BRD. Check that the error count is zero when finished. 16. Save the job file if desired: menu: File > save job 17. Select button: [quit] 18. Next steps: play with incremental backups and reports TOPIC: File Menu ^open job Open a previously saved backup job file for re-use (edit, run). The default location for job files is /home/user/.dkopp ^open DVD/BRD Open the backup job file on the currently loaded DVD/BRD. This file was saved on the DVD/BRD when the last backup job was run on that DVD/BRD. ^edit job Opens an edit dialog for the current backup job (from the last job file opened, or from a prior edit). If no file has been opened, internal default data will be used as a starting point. ^show job List the current backup job data and diagnose any errors. ^save job Save the current backup specifications in a job file. Default is the same file that was last opened, but you may select any file. ^run job The current backup job is executed. Backup and verify modes are taken from the job. ^run DVD/BRD The backup job file stored on the DVD/BRD is executed. Backup and verify modes are taken from the DVD/BRD job. Whenever a backup is performed, the current job file (including any edits that were made) is copied to the DVD/BRD. Note: what is copied to the DVD/BRD is the current job, not menu commands given manually. Thus, if you load a job file which specifies incremental backup, and then do a full backup using the menu command, the backup job stored on the DVD/BRD will still specify incremental. To change the job written to the DVD/BRD, edit the job before starting the backup. ^quit Exit program. TOPIC: Backup Menu ^full The current backup file set is copied to the DVD/BRD fully. All files are copied unconditionally. The DVD/BRD is initialized to an empty status. If growisofs aborts the job (declaring the DVD/BRD to be "unknown type" or "not formatted"), the menu command DVD/BRD > format may fix the problem. ^incremental The current backup file set is copied to DVD/BRD incrementally. New and modified files (since the DVD/BRD was created or updated) are copied. Files that already match their corresponding disk files are not copied. Any "extra" DVD/BRD files (not in the backup file set) are deleted. At the end, the DVD/BRD is 100% identical to the backup file set, with the possible exception of files modified during the backup run. See the technical notes for details about how matching files are recognized and skipped over. ^accumulate Same as incremental, but without DVD/BRD file deletions. TOPIC: Verify Menu ^full All files on the DVD/BRD are read and checked for errors. DVD/BRDs need this extra level of protection, since poor media quality has been a problem. If errors are detected, clean off the fingerprints or discard the DVD/BRD. If errors happen on more than 1% of your media, consider getting a new drive or changing media brands. Note that any CD or DVD or BRD can be "full" verified - it does not have to be a dkopp backup disk. ^incremental New files on the DVD/BRD are read and checked for errors. "New" means any files written by an immediately prior incremental or accumulate backup. Other files are not checked. ^thorough All DVD/BRD files are read and verified that there are no read errors. Those DVD/BRD files that have a matching disk file (matching full path name and modification date/time) are bytewise compared to the disk file, and any files not matching are reported. There should be no differences. This verifies that all hardware and software (driver, file system, dkopp) are working correctly. DVD/BRD files that are expected to be different (different mod times) are read and checked for errors, but not compared with the disk. Dkopp considers two files to have the same mod times if they differ by less than one second (the time resolution for files on DVD/BRD media). The following counts are reported at the end of the verify job: + total DVD/BRD files and bytes + DVD/BRD files having read errors (should be zero) + DVD/BRD files having matching disk files (by name) + DVD/BRD files having matching disk files (by name and mod time) + for the last category, the number of compare errors (should be zero) TOPIC: Report Menu ^get files for backup The backup job include and exclude statements are listed, along with the file and byte counts that are added or removed by each statement. Look for zero counts, indicating a possible error. The disk directories are read and the list of files included in the backup job is saved in memory. This data is used to determine what files are different between the disk and DVD/BRD and must be copied for an incremental backup. The file list is static and is not updated by disk activity. The list of "new" files that are checked with an incremental verify is also reset with this command. ^diffs summary Report the total number of files in each category: new on disk, but not on the DVD/BRD modified on both, but not the same content deleted on the DVD/BRD, but not on disk unchanged on both, with the same content Differences between the disk and DVD/BRD may be caused by disk updates (file additions, deletions, updates, or moves), or by changes to the job file itself. ^diffs by directory Each directory having differences between the disk and DVD/BRD is reported, along with counts of new, modified, and deleted files. The total bytes for new and modified files is also given. ^diffs by file All files that are different between the disk and DVD/BRD are listed in alphabetic sequence within groups for new, modified, and deleted files. ^list files for backup All files in the backup file set are listed in alphabetic sequence. Use this to check that the correct files are being backed-up. ^list DVD/BRD files All files on the DVD/BRD are listed in alphabetic sequence. ^find files Enter a search pattern with optional wildcards (e.g. /home/dir*name/file*name). All matching file names on the disk (in the backup job file set) are listed. All matching file names on the DVD/BRD are listed. All backup log files are also searched, and those containing the target file(s) are listed (by date / time and DVD/BRD label). These files correspond to backup jobs, one-to-one. Use this method to locate all backup copies of a given file or group of files, sorted from oldest to newest. A file may be present in multiple log files for multiple incremental backups made to the same baseline full backup, but it actually exists only once on the DVD/BRD, in its latest version. ^view backup hist All backup history log files are listed (up to 200). These correspond to backup jobs, one-to-one, and contain a list of files copied to the corresponding DVD/BRD. The most recent 20 log files are put into a dialog for selection. Select one of these from the dropdown list, or modify the input to select an older file. The text editor gedit is invoked to display the log file. You can page up and down and search for strings using gedit. Backup log file names are formatted as follows: dkopp-hist-yyyymmdd-hhmm-label. Note that one DVD/BRD having a full backup and one or more incremental backups will have a log file for each backup, showing those files copied for each backup. A file may be present in multiple log files for multiple incremental backups made to the same baseline full backup, but it actually exists only once on the DVD/BRD, in its latest version. ^save screen The main window, where messages and reports are written, is saved in a text file. TOPIC: Restore Menu ^setup DVD/BRD restore Specify the copy-from location (on the DVD/BRD), the copy-to location (on disk), and the files to be restored. The copy-from location is the topmost DVD/BRD directory of a tree of files to be restored. example: /home/joeblow/documents # note that mount point is omitted The copy-to location is an existing directory where the tree of files is copied-to. example 1: /home/joeblow/documents example 2: /home/joeblow/documents/restored In example 1, the restored files will go back to the same place they were when backed-up. In example 2, they will go to a new place. Files to be restored are specified the same way as in a backup job (see the section below on using the file selection dialog). Use the button [file chooser] to start the dialog. If you need to restore multiple trees of files, you can do this in multiple runs, or you can simply begin the tree at a higher level and use the file selection dialog to specify multiple sub-trees. ^list restore files After performing the setup, use this function to list all matching files on the DVD/BRD that will be restored, exactly where they will be restored. You should check this list carefully to be sure you are restoring the correct files to the intended locations. ^restore files When you are satisfied with the restore job specification, use this menu to perform the restore. You will see a running log of the activity. Use the kill button to stop the job if desired. TOPIC: DVD/BRD Menu ^set DVD/BRD device The DVD/BRD device and mount point may be set independently of the backup job. The DVD/BRD device and mount point for the current backup job is modified. No mounting is done. ^set DVD/BRD label Set the DVD/BRD label that will be used for a subsequent backup job. The default is to keep the same label that the DVD/BRD already has. The DVD/BRD mount command will show this label. If no label is assigned, "dkopp" is used. ^mount DVD/BRD Mount a DVD/BRD. You are asked to insert a DVD/BRD and wait for the mount to complete. If the mounted DVD/BRD has been used for dkopp before, the date-time of the last backup to this DVD/BRD is displayed. Be sure to read the technical notes about DVD/BRD mounting. ^eject DVD/BRD The DVD/BRD is unmounted and ejected. ^reset DVD/BRD This does a hardware reset to the DVD/BRD drive. This is sometimes useful if a drive gets locked-up and cannot be ejected using either the dkopp eject command or the tray button. This sometimes happens when there is a DVD/BRD error or a backup is killed in mid-process. This may or may not work. If the DVD/BRD drive remains hung after several minutes, the only resort is a reboot. ^erase DVD/BRD Writes zeros to the entire DVD/BRD surface. This takes 10+ minutes, depending on the DVD/BRD drive speed and medium. This works only for rewritable media (DVD+RW or DVD-RW or BRD-RE). DVD-R and BRD-R media are write-once and cannot be erased. See the technical notes below for more about privacy and data protection. ^format DVD/BRD This uses the dvd+rw-format utility to format a disk in a few minutes. The entire DVD/BRD is not erased. See the technical notes below for more about privacy and data protection. If Backup > full refuses to start, this format command may fix the problem. TOPIC: Help Menu ^contents Display the help file (this file). ^about Display the dkopp program version and date. TOPIC: Toolbar Buttons ^edit job Shortcut to the backup job editor (same as menu File > edit job) ^run job and run DVD/BRD The current job, or the job on the DVD/BRD, is executed. Be sure to read the technical notes about DVD/BRD mounting. ^pause and resume The currently running job or menu function may be paused and resumed. Use this to inspect output on the fly. ^kill job The currently running function is killed. You may need to wait a while for the function to die and screen output to cease. If a backup job is killed, growisofs will gracefully exit in a few seconds, leaving the DVD/BRD in an undetermined status. ^clear The main window, where messages and reports are written, is cleared. ^quit Exit the application. If the job file has been edited and not saved, you will be given an opportunity to save the changes. TOPIC: Editing Backup Jobs (see screenshot below) Select menu: File > edit job or button: edit job Fill-in the following items in the dialog box: DVD/BRD device /dev/sr0 backup mode check full / incremental / accumulate verify mode check full / incremental / thorough file date from leave default (1970.01.01) or input a later date Select the DVD/BRD device from the drop-down list of available devices. "file date from" is an additional method for selecting files to copy. Files with a create/modification date older than this will be ignored. ^File selection dialog You may edit the backup file set (the include and exclude statements) directly in the text window. You may also use the [browse] button to get a standard file selection dialog, with additional buttons: hidden, include, exclude. The [hidden] button toggles the display of hidden files (file names with leading dots, like .gnome). Select one or more directories or files, using left-mouse or shift+left-mouse, then press the [include] or [exclude] button. The selected files/directories will be written into the text window as include or exclude statements. If you select a directory, the entry is modified to add a wildcard at the next level: selecting directory /aaa/bbb/ccc → include /aaa/bbb/ccc/* You may alternate between editing the text window and using the file-chooser dialog. When you are done, press [done] to accept. The include/exclude data will be validated to the extent possible. Go back and re-edit to fix any problems. To change the sequence, cut and paste in the text window. When you are done, use the report functions "get backup files" and "list backup files" to verify that you have the correct files. The include and exclude control statements allow precise control of the backup set: include /aaa/bbb/* # include file tree under /aaa/bbb/ exclude /aaa/bbb/ccc/* # exception: exclude /ccc/ subtree include /aaa/bbb/ccc/xxx.yyy # exception: include file /ccc/xxx.yyy The file-chooser dialog may be used to quickly converge on the desired results. Because of wildcards, newly added files within the scope of existing include or exclude filespecs are automatically comprehended. In the above example, if a new file is added somewhere within the /aaa/bbb/ tree, it will be automatically included in the next backup job, unless of course it is in the excluded /aaa/bbb/ccc/ subtree. +image: dkopp-jobedit.jpg The file > edit job menu command (or toolbar button) pops up the middle box. This can be edited directly: click anywhere in the text area and start writing. The right box is the choose files dialog, which is started with the [browse] button. Choose files using the right box, and the middle box records your choices. You can navigate around the directory hierarchy and select any number of files or directories. The [hidden] button toggles the display of hidden files. Click the [include] or [exclude] button to get the selected files added to or removed from the backup list. Selecting a directory is an implied selection of all contained files, thus the selection appears as directory/* in the list of selected files. To make an exception, go down one level, choose files, and select the opposite [include] or [exclude] button. You can refine the file selections manually if desired. It is sometimes handy to use wildcards in the directories to make more general and compact selection criteria, e.g. exclude *thunderbird*/Trash* will omit trashed mail even if the overlying directories change (they do) and even for multiple users (the leading wildcard includes "/home/*"). You can add comments, or disable an include / exclude line, by putting '#' in column 1. TOPIC: Script Files A script is a text file with a series of commands that can be run as a batch job. All dkopp menu commands can be scripted. The format of the records in the script file is as follows: menu1 > menu2 > parameter # comment The menu names must match the interactive menu names exactly, including case. To run a script file: $ dkopp -script /pathname/scriptfile You can also add the option -nogui for deferred execution. Dkopp will not create a window or ask for any inputs in this mode. You must leave a DVD/BRD in the drive for dkopp to use later. Here is a sample script file to get you familiar with the possibilities: File > open job > jobfile1 Report > get files for backup Report > diffs summary # report Backup > incremental # backup changed files Verify > full # verify all files File > quit The toolbar buttons may also be used, e.g. button > pause # press resume to continue At this point you may use the menu interactively and then resume the script by pressing the resume button. The command 'exit' may be used to end the script file and return to interactive mode. Script file EOF does the same thing. TOPIC: Technical Notes ^Uninstall Debian package: use command: sudo apt remove dkopp Tarball: use command: sudo make uninstall Binary package: delete 'dkopp' files in $HOME/.local/* ^Mounting DVD/BRD media Starting with v.6.4, dkopp no longer mounts DVD/BRD disc itself, but outputs a popup message asking the user to do this. Insert the disc, wait for completion, and then press the [OK] button in the popup dialog. The appearance of the desktop icon may or may not mean that the disc is actually mounted (varies in the systems I test with). If dkopp responds with "waiting for mount ...", click the desktop icon to encourage the window manager to mount the disc. This change was made to relieve dkopp of the need to understand what Gnome and other window managers do about DVD/BRD disc mounting. ^DVD mount errors and growisofs errors There are many of these, and they change with each new release of the kernel, Gnome, and growisofs (the command-line program used to write the DVD/BRD). Often they are bogus errors that are not really errors, or they are temporary errors that will go away when the operation is tried again. Dkopp is unable to distinguish the difference. In the case of a full backup, the user is given a popup dialog with the options to abort the job, retry the full backup, or ignore the error and continue. If the backup job did not even get started, use retry. If the backup job seems to have completed, use ignore. Verify will detect later if any error actually happened. ^Blank DVD/BRDs The first time a DVD/BRD is used, do a full backup. A blank DVD/BRD will not mount, but a full backup will still work and make the DVD/BRD mountable thereafter. ^Flakey DVD/BRDs Drive and media combinations sometimes have compatibility problems, resulting in media errors. The newest drives (2006 and later) are much better at adjusting to media variations. I have had a few DVDs (<1%) that passed a dkopp verify and then became unreadable later, so make regular backups and avoid depending on a single DVD/BRD. ^Media errors If the dkopp verify function runs into a read error, the DVD/BRD drive may lock-up for a minute or more while retrying the failed read hundreds of times. Give the "kill" command and wait for the drive to give up. If the DVD/BRD is dirty, clean it and try again. Otherwise throw it out. ^Privacy and data protection To protect your private data on discarded DVD/BRDs, you should destroy them. A few seconds in a microwave oven will completely destroy the metallic recording layer (with spectacular visual effects). Do not inhale the fumes. ^Command line arguments $ dkopp -job jobfile # load job file $ dkopp jobfile # load job file $ dkopp -run jobfile # load job file and run it $ dkopp -script scriptfile # run script file $ dkopp -nogui -run jobfile # run job in non-GUI mode The -run and -script commands are intended for shell scripts. The -job command is more useful for a desktop launcher, leaving the user free to elect the backup mode or make other changes in the job before execution. If the jobfile name contains blanks, quotes are required, e.g. $ dkopp -job "my dkopp job" The -nogui option will prevent dkopp from opening a window or asking for any interactive inputs. Use this for deferred operation (batch job). You must leave a DVD/BRD in the drive for dkopp/growisofs to use. ^Deleted DVD/BRD files Growisofs is used to perform the file copies. It can replace existing DVD/BRD files with new versions, but it does not delete files. For incremental backups, dkopp replaces deleted files with null files (zero length). Full backups do not have this issue, since the DVD/BRD is initialized. If you recover files from a dkopp DVD/BRD using a shell copy command with wildcards, or Nautilus drag-and-drop of an entire directory, you may get unwanted null files. If this happens, it is easy to get rid of them like this: $ rm -i $(find /dir1/.../dirN -empty) (remove all empty files in a directory tree, with confirmation of each) Note that if you use dkopp restore, these null files are invisible and are not restored. ^Incremental backups A DVD/BRD file is considered identical to its corresponding disk file if their lengths and modification times are the same. Incremental backups exclude such files. If the modification times differ by less than 1 second they are considered equal (another way to look at this issue: file backup times may be wrong by up to 1 second). A thorough verify will read and compare the files unconditionally. File mod times on DVD/BRD media have a 1 second resolution. ^File names containing "=" Genisoimage requires that "=" in file names be replaced with "\=". The file-chooser dialog in dkopp file restore shows "\=" instead of "=", but the files will be correctly restored with "=" only. ^Restoring file owner and permissions The file system used for DVD/BRD media does not support file owner and permissions, so dkopp writes a special file to the DVD/BRD with this data. Restored files have original owner and permissions. ^Special dkopp files on DVD/BRD Directory /dkopp-data is written to the DVD/BRD with three files: datetime backup date-time and DVD/BRD usage count filepoop owner and permissions for all backed-up files and directories jobfile a copy of the backup job specs last used on this DVD/BRD These are ordinary text files which you can view with an editor. ^Special file types Pipes, devices, and sockets are not copied. Symlinks are copied as such. Both symlinks and their targets should be included in the backup or restore file set, since it makes no sense to copy one without the other. This normally happens by default, since symlinks typically link to files in the same directory. Symlinks are used commonly in system directories and in the hidden system files within a /home/user directory. For user files, there is no need for them. ^Killing growisofs (killing a backup job in progress) This will sometimes leave the DVD/BRD in a condition that growisofs refuses to deal with. If you decide to abort a backup job (e.g. to revise the job specs and start over), you may get this condition. You should retry a full backup on this DVD/BRD. If growisofs still refuses, format the DVD/BRD (dkopp menu), then try the full backup job again. ^Duplicate files If job file "include" statements overlap, resulting in duplicate files in the backup set, this is reported and the backup is terminated. ^Microsoft Windows DVD/BRDs created with dkopp use the standard ISO-9660 file system, which can be read by Windows. ^DVD/BRD drive and media information Here are two useful commands: $ udevinfo -q all -n /dev/dvd # DVD/BRD drive information $ dvd+rw-mediainfo /dev/dvd # DVD/BRD media information ^Incremental backups New and updated files are written to a new "session" on the DVD/BRD, along with new directory files which may reference data files in both the old and new sessions. Nothing is changed in the old sessions. Thus, incremental backups consume more space on the DVD/BRD even if the corresponding disk files are not any bigger. For DVD+R, DVD-R and BRD-R media (write once), only one full backup may be made, and as many incremental backups as can fit in the remaining space. For DVD+RW, DVD-RW and BRD-RE media (rewritable), a new full backup will initialize the DVD/BRD and recover all space. These DVD/BRDs can be used until they wear out. I have exceeded 100 uses on a test DVD+RW medium and it still works fine. ^Growisofs progress tracking Growisofs (genisoimage) outputs a "% done" value every few megabytes. Dkopp uses this number to compute the current position in the list of files to be copied, and the resulting file is echoed to the main window. The update frequency is typically less than once per file, so some file names will be bypassed. Large files may stay on the screen for several update cycles. For full backups, the math is straightforward. For incremental backups, growisofs starts off with: % done = 100 * (initial DVD/BRD bytes used) / (final DVD/BRD bytes used) Dkopp assigns this value to the first file in the backup list. The last file is assigned 100%, and the rest are interpolated using accumulated bytes. ^Linux error codes Linux error codes can be misleading. If an attempt is made to open a file that is already open and therefore locked, the error code translates to "no such file or directory". The error codes are the same for an attempt to mount an empty tray or a corrupted DVD/BRD. The same is true for an attempt to mount a DVD/BRD that is already mounted, or a blank DVD/BRD. Dkopp outputs messages of its own that mention the multiple possibilities. Hopefully this will improve over time. ^Backup history files A history file is generated for every backup job run. location: /home/username/.dkopp/ file name: dkopp-hist-yyyymmdd-hhmm-label The file name corresponds to the date and time of the backup and the DVD/BRD label. A history file contains a list of all the files copied to that DVD/BRD at that time. Thus, a DVD/BRD used for a full backup and two incremental backups will have three corresponding history files, each one containing those files copied by the respective backup job. A full backup spanning multiple DVD/BRDs will have multiple history files, one per DVD/BRD. History files accumulate and are not automatically deleted. When 200 files are reached, the find files and view backup history reports produce warnings. Delete the oldest files or move them elsewhere. The 200 limit is a compile time constant: maxhist. This could be set much higher if desired (and if you have so many DVD/BRDs before you re-use them). ^DVD/BRD label The menu DVD/BRD > set DVD/BRD label is for an optional DVD/BRD label input, which you can use as part of your media management system. A subsequent backup job will write this label to the DVD/BRD, and the DVD/BRD mount command will show the label. Recommendation: for full backups, set the label to match what is written on the DVD/BRD (with a soft pen). For incremental backups, leave the label unchanged. ^genisoimage errors If a disk file is deleted after growisofs begins, the DVD/BRD will be defective: directory entries for the missing files and all following files will point to garbage (which may even be readable). The error reported by genisoimage is ignored by growisofs. Dkopp scans growisofs output for the ignored errors and un-ignores them. dkopp/zfuncs.cc0000664000175000017500000172146315002404054012464 0ustar micomico/******************************************************************************** zfuncs.cc collection of Linux and GDK/GTK utility functions Copyright 2007-2024 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. *********************************************************************************/ #include "zfuncs.h" #define RELEASE 25.1 // follows Fotocx release versions /******************************************************************************** System Utility Functions ------------------------ zmalloc zfree wrappers to add add checks and statistics zmalloc_test check if planned allocation is available realmemory get real free memory (incl. file cache) in MB units availmemory get available memory (incl. swap file) in MB units zstrdup duplicate string with added space zstrcpy replace string with new string + added cc zmalloc_report memory usage by tag > popup report zmalloc_growth memory growth by tag > popup report Plog add message to stdout log file, fflush() xmessage output a popup message not requiring GTK zexit exit process and kill subprocesses, opt. popup message zbacktrace callable backtrace dump zappcrash abort with traceback dump to desktop file catch_signals trap segfault, crash with zappcrash() trace implements TRACE macro tracedump dump trace data combine_argvs catenate argv[ii] elements from Nth to last get_seconds get time in real seconds with millisecond resolution print_seconds output seconds since last call zstat get file mod time and size start_timer start a named timer get_timer get elapsed time with millisecond resolution CPUtime get CPU process time for current process, seconds memused get real memory used by current process, MB compact_time convert time_t type to yyyymmddhhmmss format pretty_datetime convert time_t type to yyyy:mm:dd hh:mm:ss format secs_datetime seconds since 1970 (double) to yymmddhhmmss (int[6]) datetime_secs yymmddhhmmss (int[6]) to seconds since 1970 (double) parseprocfile read and parse /proc records formatted "parmname value" parseprocrec read and parse /proc records with fixed series of values coretemp get current processor core temperature disktemp get temperature for given disk drive zsleep sleep for any amount of time (e.g. 0.1 seconds) zloop loop for any amount of time spinlock simple method to prevent parallel execution of code block global_lock lock/unlock a global resource (all processes/threads) resource_lock lock/unlock a resource within a process and threads zget_locked, etc. safely get/put/increment parameters from multiple threads start_detached_thread start a detached thread start_Jthread start a joinable thread wait_Jthread wait for thread and join synch_threads make threads pause and resume together main_thread return 1 if main() thread, else 0 zshell run shell command with options log, ack kill_procname kill process matching wildcard name signalProc pause, resume, or kill a child process fgets_trim fgets() with trim of trailing \r \n and optionally blanks fgets_pend fgets() non-blocking wrapper samefolder test if two files/folders have the same folder path parsefile parse filespec into folder, file, extension renamez like rename() but works across file systems check_create_dir check if folder exists, ask to create if not cp_copy same, using shell "cp -f -p" diskspace get available space on disk of given file, MB get_file_extension use 'file' command to find and append correct extension zreaddir return all files in a folder, sorted zreadfile read file, return array of records zwritefile write array of records to file zreadfile_free free zreadfile() memory String Functions ---------------- strmatch true if strings match, null strings match only null strings strmatchN true if leading N characters match, null strings as above strmatchcase true if strings match, except for case, null strings as above strmatchcaseN true if leading N characters match, null strings as above substringR get delimited substrings from input string substring same, not thread-safe, no zfree() needed get_substrings get all substrings from a delimited input string strHash hash string to random number in a range strncpy0 strncpy() with insured null delimiter strnPad add blank padding to specified length strTrim remove trailing blanks strTrim2 remove leading and trailing blanks strCompress remove embedded blanks strncatv catenate multiple strings with length limit strmatchV compare 1 string to N strings strToUpper convert string to upper case strToLower convert string to lower case repl_1str replace a substring within a string repl_Nstrs replace multiple substrings within string breakup_text insert newline chars to limit text line lengths strncpyx convert string to hex format StripZeros remove trailing zeros (1.23000E+8 >> 1.23E+8) blank_null test string for null pointer, zero length, and all blanks clean_escapes replace 2-character escapes ("\n") with the escaped characters UTF8 functions deal with UTF8 multibyte character strings zsed substitute multiple strings in a file zstrstr zstrstr() and zcasestrstr() work like strstr() and strcasestr() zstrcasestr but the string "" does NOT match with any string zstrcpy strcpy with overlap allowed zstrncpy strncpy with overlap allowed zstrcmp like strcmp, but \n as well as null ends the compare zstrcmp2 works like strcmp(), but using ch *args instead of cch * zstrcasecmp works like strcasecmp(), but using ch *args instead of cch * zescape_quotes escape quote marks (",') for strings used in shell commands Number Conversion and Formatting -------------------------------- convSI string to integer with optional limits check convSD string to double with optional limits check convSF string to float with optional limits check convIS integer to string with returned length convDS double to string with specified digits of precision atofz atof() accepting both '.' and ',' decimal points formatKBMB format a byte count with specified precision and B/KB/MB/GB units Wildcard Functions ------------------ MatchWild match string to wildcard string (multiple * and ?) MatchWildCase works like MatchWild() but ignores case SearchWild wildcard file search (multiple * and ? in path or file name) SearchWildCase works like SearchWild() but ignores case in file name zfind find files matching a pattern. uses glob() Search and Sort Functions ------------------------- bsearch binary search of sorted list HeapSort sort list of integer / float / double / records / pointers to records MemSort sort records with multiple keys (data types and sequence) zmember test if a value is a member of a set of values HashTab hash table: add, delete, find, step through Misc Functions -------------- zlist functions list processing functions - array of string pointers lrandz int64 random numbers drandz double random numbers random numbers int and double random numbers with improved distributions spline1/2 cubic spline curve fitting function Qtext FIFO queue for text strings, dual thread access variance compute variance for a list of numbers Application Admin Functions --------------------------- zinitapp initialize application folder and data files get_zprefix /usr or /home/ get_zhomedir /home//.appname or custom location get_zdatadir app data files location get_zdocdir app documentation files location get_zimagedir app image files location zabout popup application 'about' information zsetfont set new application font widget_font_metrics get font width and height for given widget get_zfilespec get filespec for README, changelog, userguide, parameters ... showz_logfile display application log file showz_textfile show application text file (README, changelog, etc.) showz_docfile show a document file topic and associated image audit_docfile audit docfile for missing topics and bad links GTK Utility Functions --------------------- zmainloop do main loop to process menu events, etc. zmainsleep loop zmainloop and zsleep for designated time draw_context_create get cairo drawing context for GDK window txwidget text report, navigation, line editing create_menubar menubar functions create_toolbar toolbar functions create_stbar statusbar functions create_popmenu create popup menu with response functions popup_menu display/execute popup menu popup_choose popup to choose from zlist or text file Vmenu vertical menu/toolbar in vertical packing box splcurve_init set up a spline curve drawing area splcurve_adjust mouse event function to manipulate curve nodes splcurve_addnode add an anchor point to a curve splcurve_resize resize drawing area if too small splcurve_draw draw curve through nodes splcurve_generate generate x/y table of values from curve splcurve_yval get curve y-value for given x-value splcurve_load load curve data from a saved file splcurve_save save curve data to a file zdialog_new create new zdialog zdialog_set_title change a zdialog title zdialog_set_modal set a zdialog to be modal zdialog_set_decorated set a zdialog to be decorated or not zdialog_present present a zdialog (visible and on top) zdialog_can_focus set zdialog can or cannot have focus zdialog_set_focus set focus on zdialog window or window + widget zdialog_add_widget add widget to existing zdialog zdialog_valid return 1/0 if zdialog is valid/invalid zdialog_find_widget return widget from zdialog and widget name zdialog_gtkwidget get GTK widget from zdialog and widget name zdialog_set_image set image widget from GDK pixbuf zdialog_add_ttip add a popup tool tip to a zdialog widget zdialog_resize resize zdialog greater than initial size zdialog_put_data put data into a zdialog widget of any type zdialog_get_data get data from a zsialog widget of any type zdialog_set_limits set new limits for numeric data entry widget zdialog_get_limits get limits for numeric data entry widget zdialog_rescale expand the scale around a neutral value zdialog_run run the zdialog and send events to event function zdialog_widget_event respond to zdialog widget events zdialog_focus_in_event response handler for "focus-in-event" signal zdialog_activate_event response handler for "activate" signal ('enter' key) zdialog_KB_press respond to zdialog keyboard inputs zdialog_zspin_event response function for "zspin" widget zdialog_copyfunc copy widget data to clipboard zdialog_pastefunc copy clipboard to widget with KB focus zdialog_delete_event process zdialog delete event ([x] button) zdialog_send_event send an event to an active zdialog zdialog_send_response complete a zdialog and assign status zdialog_show show or hide a zdialog window zdialog_destroy destroy a zdialog (data remains available) zdialog_free free zdialog memory (data is gone) zdialog_wait wait for zdialog completion, get status zdialog_goto put cursor at named widget zdialog_set_cursor set zdialog cursor (e.g. busy) zdialog_stuff stuff data into zdialog widget zdialog_labelfont set label text with font zdialog_fetch fetch data from zdialog widget zdialog_combo_clear clear combo box entries zdialog_combo_popup open combo box pick list zdialog_load_widgets load zdialog widgets and curves from a file zdialog_save_widgets save zdialog widgets and curves to a file zdialog_load_prev_widgets load last-used widgets (for [prev] buttons) zdialog_save_last_widgets save last-used widgets (for [prev] buttons) zdialog_geometry load/save zdialog positions at app start/exit zdialog_set_position set zdialog position: null mouse desktop parent save nn/nn zdialog_save_position remember zdialog position relative to parent zdialog_save_inputs save zdialog inputs when zdialog completed zdialog_load_inputs retrieve prior zdialog input fields zdialog_text popup zdialog to get N lines of text input from user zdialog_text1 popup zdialog to get one line of text input from user zdialog_password popup zdialog to get a password entry (invisible) zdialog_choose popup zdialog to show a message, select a button, return choice zdialog_edit_textfile popup zdialog to edit a text file popup_report popup window and scrolling text report popup_command run a shell command with output in a popup window monitor_file show a text file in a popup window, with real-time updates zmessageACK popup message, printf format, wait for user ACK zmessageYN popup message, printf format, wait for user Yes / No zmessage_post popup message, printf format, show until killed poptext_screen popup message at given absolute screen position poptext_mouse popup message at current mouse position + offset poptext_widget popup message at given widget position + offset poptext_killnow kill popup message zgetfile simplified file chooser zdialog zgetfolder file chooser for folder, with create option print_image_file zdialog to print an image file using GTK functions drag_drop_source connect window as drag-drop source drag_drop_dest connect window as drag-drop destination get_thumbnail get thumbnail image for given image file zmakecursor make a cursor from an image file (.png .jpg) gdk_pixbuf_stripalpha remove an alpha channel from a pixbuf text_pixbuf create pixbuf containing text move_pointer move the mouse pointer within a widget/window window_to_mouse move a GtkWindow to the mouse position *********************************************************************************/ namespace zfuncs { GdkDisplay *display; // workstation (KB, mouse, screen) GdkScreen *screen; // screen, N monitors GdkDevice *mouse; // pointer device GtkSettings *gtksettings = 0; // screen settings GtkWidget *mainwin = 0; // main window GtkTextView *curr_textview_widget; // curr. GtkTextView widget ch zcontact[] = "mkornelix@gmail.com"; // author contact ch *build_date_time = __DATE__ " " __TIME__; // build date and time ch *progexe = 0; // executable image file int Floglevel = 1; // 0/1/2 = errs/infos/dialog inputs int Fescape = 0; // zdialog, escape key pressed int Fshutdown; // flag, app shutdown underway 25.1 int monitor_ww, monitor_hh; // monitor dimensions int appfontsize = 10; // application font size ch *appfont = "sans 10"; // application font defaults ch *appboldfont = "sans bold 10"; ch *appmonofont = "mono 10"; ch *appmonoboldfont = "mono bold 10"; ch zappname[40] = "undefined"; // appname without version ch zappvers[40] = "undefined"; // appname-N.N ch zprefix[200], zdatadir[200], zdocdir[200]; // app folders ch zimagedir[200], zhomedir[200]; ch logfile[200]; // application log file pthread_t tid_main = 0; // main() thread ID int vmenuclickposn; // Vmenu image click posn. 0-100 int vmenuclickbutton; // button: 1/2/3 = L/M/R mouse int vmenustop; // setupfunc() stop flag zdialog *zdialog_list[zdialog_max]; // active zdialog list int zdialog_count = 0; // total zdialogs (new - free) float splcurve_minx = 5; // min. anchor point dist, % scale ch *zappcrash_context1 = 0, *zappcrash_context2 = 0; } using namespace zfuncs; /******************************************************************************** system-level utility functions *********************************************************************************/ /******************************************************************************** zmalloc() zfree() zstrdup() These are wrappers for malloc() and free() with extra diagnostics. zmalloc() crashes with a message to standard output if the allocation fails, hence the caller need not check. zmalloc() allocates memory for sentinels placed before and after the returned memory space, and zfree() checks the sentinels for validity and crashes with a message if they don't match. The optional tag argument is also copied into the extra allocated space for use by zmalloc_report(). zmalloc() clears the allocated memory to zeros. zmalloc_report() reports total allocated memory by tag to standard output. Allocation counts and bytes are listed for zmalloc() calls not yet matched by zfree() calls. The extra cpu time is about 4% (for the fotocx application). *********************************************************************************/ #define zmalloc_extra 36 int64 zmalloc_tot = 0; int zmalloc_lock = 0; void * zmalloc(int64 cc, ch *tag) // bytes, tag { void zmalloc_tabulate(ch *tag, int64 cc); double memavail; static int ftf = 1, memcheck = 1; double mcc; while (! resource_lock(zmalloc_lock)) zsleep(0.001); ch *OOMmessage = " \n" // big and obvious " --------------- \n" " OUT OF MEMORY \n" " --------------- \n"; if (ftf) { // first call ftf = 0; memavail = availmemory(); if (! memavail) memcheck = 0; // memory checking not possible, disable } if (cc <= 0) zappcrash("zmalloc: %lld bytes",cc); if (memcheck && cc > 1000) { // large block mcc = cc; mcc = mcc / 1024 / 1024; // requested memory, MB memavail = availmemory(); // avail. memory, MB if (memavail - mcc < 500) { Plog(0,"memory request for %.0f MB failed\n",mcc); zexit(1,OOMmessage); exit(-1); } } if (! tag) tag = "zmalloc notag"; void * maddr = malloc(cc + zmalloc_extra); // 0 allocated memory with extra space int64 *pcc = (int64 *) maddr; // 0..8 caller byte count ch *psen1 = (ch *) maddr + 8; // 8..11 sentinel "sen1" ch *ptag = (ch *) maddr + 12; // 12..31 tag, < 20 chars. ch *puser = (ch *) maddr + 32; // 32..B+31 user data, B chars. ch *psen2 = (ch *) puser + cc; // B+32..B+35 sentinel "sen2" if (! maddr) { zexit(1,OOMmessage); exit(-1); } *pcc = cc; strncpy(psen1,"sen1",4); // set leading sentinel strncpy0(ptag,tag,20); // set tag strncpy(psen2,"sen2",4); // set following sentinel memset(puser,0,cc); // clear allocation (force memory commit) zmalloc_tot += cc; zmalloc_tabulate(ptag,cc); // track usage by tag resource_unlock(zmalloc_lock); return puser; } // free memory allocated by zmalloc(). checks for overflow. void zfree(void *puser) { void zmalloc_tabulate(ch *tag, int64 cc); if (! puser) zappcrash("zfree: null address"); void *maddr = (ch *) puser - 32; int64 *pcc = (int64 *) maddr; ch *psen1 = (ch *) maddr + 8; ch *ptag = (ch *) maddr + 12; int64 cc = *pcc; ch *psen2 = (ch *) puser + cc; while (! resource_lock(zmalloc_lock)) zsleep(0.001); if (strncmp("sen1",psen1,4) || strncmp("sen2",psen2,4)) // check sentinels zappcrash("zfree: sentinels clobbered"); *psen1 = *psen2 = 0; // destroy sentinels ch * puser2 = (ch *) puser; // clobber to detect use after free *puser2 = '*'; zmalloc_tot -= cc; zmalloc_tabulate(ptag,-cc); // track usage by tag free(maddr); // free memory (must be last) resource_unlock(zmalloc_lock); return; } // private function. track how much memory is in use, per tag. // real tag capacity is about 80% of nominal 'zmhtcap' #define zmhtcap 500 HashTab *zmalloc_hashtab = 0; int64 zmalloc_count[zmhtcap]; int64 zmalloc_bytes[zmhtcap]; void zmalloc_tabulate(ch *ptag, int64 cc) { int ii; if (! zmalloc_hashtab) { zmalloc_hashtab = new HashTab(20,zmhtcap); memset(zmalloc_count, 0, zmhtcap * sizeof(int64)); memset(zmalloc_bytes, 0, zmhtcap * sizeof(int64)); } ii = zmalloc_hashtab->Find(ptag); if (ii < 0) ii = zmalloc_hashtab->Add(ptag); if (ii < 0) zappcrash("zmalloc hash table full"); zmalloc_bytes[ii] += cc; if (cc > 0) ++zmalloc_count[ii]; else --zmalloc_count[ii]; return; } // test if a given about of free memory is available // return 1 if OK, return 0 if NO. int zmalloc_test(int64 cc) { double memavail, mb; mb = cc / 1024 / 1024; memavail = availmemory(); // avail. memory, MB memavail -= mb; if (memavail > 300) return 1; // > 300 MB remaining, return OK Plog(0,"planned memory allocation of %.0f MB failed \n",mb); return 0; // not OK } // get real memory in MB units // typical < 0.1 milliseconds double realmemory() { FILE *fid; ch buff[100], *pp; double rmem = 0; fid = fopen("/proc/meminfo","r"); if (! fid) return 0; while (true) { pp = fgets(buff,100,fid); if (! pp) break; if (strmatchN(pp,"MemAvailable:",13)) { // free + file cache rmem = atof(pp+13) / 1024; break; } } fclose(fid); return rmem; } // get available memory in MB units (includes swap space) // typical < 0.1 milliseconds double availmemory() { FILE *fid; ch buff[100], *pp; double avmem = 0; int Ngot = 0; fid = fopen("/proc/meminfo","r"); if (! fid) return 0; while (true) { pp = fgets(buff,100,fid); if (! pp) break; if (strmatchN(pp,"MemAvailable:",13)) { // free + file cache avmem += atof(pp+13) / 1024; if (++Ngot == 2) break; } if (strmatchN(pp,"SwapFree:",9)) { // swapfile free avmem += atof(pp+9) / 1024; if (++Ngot == 2) break; } } fclose(fid); return avmem; } // duplicate string in allocated memory, with additional space at end ch * zstrdup(ch *zstring, ch *tag, int addcc) { if (! zstring) zappcrash("zstrdup() null arg"); if (! tag) tag = "zstrdup notag"; ch *pp = (ch *) zmalloc(strlen(zstring) + 2 + addcc, tag); // add additional chars, clear strcpy(pp,zstring); return pp; } // replace zstring with string + added cc int zstrcopy(ch *&zstring, ch *string, ch *tag, int addcc) { if (! tag) tag = "zstrcopy notag"; if (zstring == string) zstring = 0; // if same string, make a duplicate if (zstring) zfree(zstring); int cc = strlen(string) + 2 + addcc; zstring = (ch *) zmalloc(cc,tag); strcpy(zstring,string); return cc; } /********************************************************************************/ // popup report of zmalloc() memory allocation per tag void zmalloc_report(GtkWidget *parent) // 25.1 { int zmalloc_report_callback(GtkWidget *,int line, int posn, ch *input); static zdialog *zdpop = 0; int count, ii, first = 1; int64 cc; ch tag[20]; if (! zdpop || ! zdialog_valid(zdpop,"zmalloc")) // open new popup report zdpop = popup_report_open("zmalloc",parent,500,400,0,0, zmalloc_report_callback,"apply","clear","X",0); popup_report_write(zdpop,0,"zmalloc total memory: %lld \n",zmalloc_tot); while (true) { ii = zmalloc_hashtab->GetNext(first,tag); if (ii < 0) break; ii = zmalloc_hashtab->Find(tag); if (ii < 0) zappcrash("zmalloc hash table bug: %s",tag); cc = zmalloc_bytes[ii]; count = zmalloc_count[ii]; if (cc == 0) continue; popup_report_write(zdpop,0," %-20s %8d %lld \n",tag,count,cc); } popup_report_write(zdpop,0,"\n"); popup_report_bottom(zdpop); return; } // zmalloc_report() callback function for completion buttons int zmalloc_report_callback(GtkWidget *txwidget, int line, int posn, ch *input) { if (strmatch(input,"apply")) zmalloc_report(0); if (strmatch(input,"clear")) txwidget_clear(txwidget); return 1; } /********************************************************************************/ // report total memory allocated per tag - leak detection utility // report only tags with increased memory consumption since prior report void zmalloc_growth(GtkWidget *parent) // 25.1 { int zmalloc_growth_callback(GtkWidget *,int line, int posn, ch *input); static zdialog *zdpop = 0; int count, ii, first = 1; int64 cc; ch tag[20]; static int pne = 0; // table of prior tag and cc values static ch *ptag[1000]; static int64 pcc[1000]; if (! zdpop || ! zdialog_valid(zdpop,"zmalloc growth")) // open new popup report zdpop = popup_report_open("zmalloc growth",parent,500,400,0,0, zmalloc_growth_callback,"apply","clear","X",0); popup_report_write(zdpop,0,"zmalloc total memory: %lld \n",zmalloc_tot); while (true) // loop all tags in table { ii = zmalloc_hashtab->GetNext(first,tag); if (ii < 0) break; ii = zmalloc_hashtab->Find(tag); if (ii < 0) zappcrash("zmalloc hash table bug: %s",tag); cc = zmalloc_bytes[ii]; // memory allocation for tag count = zmalloc_count[ii]; // zmalloc/zfree calls for tag if (cc == 0) continue; // net memory = 0, skip for (ii = 0; ii < pne; ii++) // find prior allocation for tag if (strmatch(tag,ptag[ii])) break; if (ii == pne) { ptag[ii] = strdup(tag); // new tag, add to table pcc[ii] = cc; pne++; } if (cc <= pcc[ii]) continue; // current <= prior allocation, skip popup_report_write(zdpop,0," %-20s %8d %lld \n",tag,count,cc); // report increased allocation pcc[ii] = cc; // new high-water allocation } popup_report_write(zdpop,0,"\n"); popup_report_bottom(zdpop); return; } // zmalloc_growth() callback function for completion buttons int zmalloc_growth_callback(GtkWidget *txwidget, int line, int posn, ch *input) { if (strmatch(input,"apply")) zmalloc_growth(0); if (strmatch(input,"clear")) txwidget_clear(txwidget); return 1; } /********************************************************************************/ // add message to stdout log file if Floglevel >= message level // flush every output immediately even if stdout is a file // use lev = 0 for mandatory error message // lev = 1 for informative message // lev = 2 for everything void Plog(int lev, ch *format, ...) { if (lev > Floglevel) return; va_list arglist; va_start(arglist,format); vprintf(format,arglist); va_end(arglist); fflush(stdout); return; } /********************************************************************************/ // output a popup message not requiring GTK - callable from a thread. // application is stalled - "not responding" until [ok] button pressed. void xmessage(ch *message) { ch command[400]; ch *font = "-*-bookman-*-r-*--0-140-0-0-p-*-*-*"; // big font 25.1 Plog(0,"%s \n",message); snprintf(command,400,"xmessage -fn %s -center \"%s\" ",font,message); int err = system(command); // do not use zshell if (err) return; // avoid gcc warning return; } /********************************************************************************/ // Output a status or error message and kill all processes in the process group. // killpg(0,SIGKILL) kills all processes, including the caller. // if 'popup' true, popup an xmessage window with error message. void zexit(int popup, ch *errmess, ...) { va_list arglist; ch mess[1000]; if (errmess) { // output error message va_start(arglist,errmess); vsnprintf(mess,1000,errmess,arglist); Plog(0,"zexit: %s\n",mess); if (popup) xmessage(mess); // popup message } else Plog(0,"zexit\n"); killpg(0,SIGKILL); // kill all processes in group sleep(1); // wait here to die exit(-1); } /********************************************************************************/ // produce a backtrace dump to stdout void zbacktrace() { int nstack = 100; void *stacklist[100]; nstack = backtrace(stacklist,nstack); // get backtrace data if (nstack > 100) nstack = 100; backtrace_symbols_fd(stacklist,nstack,STDOUT_FILENO); // backtrace records to STDOUT return; } /********************************************************************************/ // Write an error message and backtrace dump to a file and to a popup window. // Error message works like printf(). // Depends on program addr2line() in binutils package. void zappcrash(ch *format, ... ) { static int crash = 0; struct utsname unbuff; va_list arglist; FILE *fid1, *fid2, *fid3; int fd, ii, err, cc, nstack = 100; int Flinenos = 1; void *stacklist[100]; ch OS1[60] = "?", OS2[60] = "?", OS3[60] = "?"; ch message[300], progexe[300]; ch buff1[300], buff2[300], hexaddr[20]; ch *arch, *pp1, *pp2, dlim, *pfunc; if (crash++) return; // re-entry or multiple threads crash va_start(arglist,format); vsnprintf(message,300,format,arglist); va_end(arglist); uname(&unbuff); // get cpu arch. 32/64 bit arch = unbuff.machine; fid1 = popen("lsb_release -d","r"); // get Linux flavor and release if (fid1) { ii = fscanf(fid1,"%s %s %s",OS1,OS2,OS3); pclose(fid1); } xmessage("A fatal error has occurred. \n" // popup message " See zappcrash file in home folder."); Plog(0,"\n*** zappcrash: %s %s %s %s %s %s \n", arch, OS2, OS3, zappvers, build_date_time, message); Plog(0,"*** zappcrash context: %s | %s \n",zappcrash_context1, zappcrash_context2); nstack = backtrace(stacklist,nstack); // get backtrace data if (nstack <= 0) zexit(0,"zappcrash backtrace() failure"); if (nstack > 100) nstack = 100; fid1 = fopen("zbacktrace","w"); // open backtrace data output file if (! fid1) zexit(0,"zappcrash fopen() failure"); fd = fileno(fid1); backtrace_symbols_fd(stacklist,nstack,fd); // write backtrace data fclose(fid1); // (use of malloc() is avoided) fid1 = fopen("zbacktrace","r"); // open backtrace data file if (! fid1) zexit(0,"zappcrash fopen() failure"); fid2 = fopen("zappcrash","w"); // open zappcrash output file if (! fid2) zexit(0,"zappcrash fopen() failure"); fprintf(fid2,"\n*** zappcrash: %s %s %s %s %s %s \n", arch, OS2, OS3, zappvers, build_date_time, message); fprintf(fid2,"*** zappcrash context: %s | %s \n",zappcrash_context1, zappcrash_context2); fprintf(fid2,"*** please send this crash report to mkornelix@gmail.com *** \n" "*** if possible, please explain how to repeat this problem *** \n"); cc = readlink("/proc/self/exe",progexe,300); // get own program path if (cc > 0) progexe[cc] = 0; // readlink() quirk else { fprintf(fid2,"progexe not available \n"); Flinenos = 0; } err = zshell(0,"which addr2line >/dev/null"); // check if addr2line() available if (err) Flinenos = 0; for (ii = 0; ii < nstack; ii++) // loop backtrace records { pp1 = pp2 = 0; fgets_trim(buff1,300,fid1); // read backtrace line if (! Flinenos) goto output; pfunc = 0; pp1 = strstr(buff1,"+0x"); // new format (+0x12345...) if (pp1) pp2 = strchr(pp1,')'); else { pp1 = strstr(buff1,"[0x"); // old format [0x12345...] if (pp1) pp2 = strchr(pp1,']'); } if (! pp1 || ! pp2) goto output; // cannot parse dlim = *pp2; *pp2 = 0; strncpy0(hexaddr,pp1+1,20); *pp2 = dlim; snprintf(buff2,300,"addr2line -i -e %s %s",progexe,hexaddr); // convert to source program fid3 = popen(buff2,"r"); // and line number if (! fid3) goto output; pfunc = fgets(buff2,300,fid3); pclose(fid3); if (! pfunc) goto output; cc = strlen(pfunc); if (cc < 10) goto output; if (pfunc[cc-1] < ' ') pfunc[cc-1] = 0; // remove tailing \n if present strncatv(buff1,300,"\n--- ",pfunc,null); output: fprintf(fid2,"%s \n",buff1); // output } fclose(fid1); fclose(fid2); zshell(0,"rm zbacktrace"); // remove backtrace file zshell(0,"cat zappcrash"); // add zappcrash to log file zshell(0,"rm zappcrash"); // remove zappcrash file if (*logfile == '/') zshell(0,"cp -f %s $HOME/zappcrash",logfile); // copy log file to $HOME/zappcrash zexit(0,"Zexit zappcrash"); // quit } /********************************************************************************/ // application initialization function to catch some bad news signals // the signal handler calls zappcrash() to output a backtrace dump and exit void catch_signals() { void sighandler(int signal); struct sigaction sigact; sigact.sa_handler = sighandler; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGTERM,&sigact,0); sigaction(SIGSEGV,&sigact,0); sigaction(SIGILL,&sigact,0); // man page says cannot be caught sigaction(SIGFPE,&sigact,0); sigaction(SIGBUS,&sigact,0); sigaction(SIGABRT,&sigact,0); // heap or stack corruption return; } // catch fatal signals and produce backtrace dumps on-screen void sighandler(int signal) { cch *signame = "unknown"; if (signal == SIGTERM) zexit(0,"TERMINATED"); if (signal == SIGKILL) zexit(0,"KILLED"); if (signal == SIGSEGV) signame = "segment fault"; if (signal == SIGILL) signame = "illegal operation"; if (signal == SIGFPE) signame = "arithmetic exception"; if (signal == SIGBUS) signame = "bus error (bad memory)"; if (signal == SIGABRT) signame = "abort"; zappcrash("fatal signal: %s",signame); exit(0); } /********************************************************************************/ // Implement the TRACE macro. // Trace program execution by function and source code line number. // tracedump() dumps last 50 uses of TRACE macro, latest first. namespace tracenames { ch filebuff[50][100]; // last 50 TRACE calls ch funcbuff[50][60]; int linebuff[50]; void *addrbuff[50]; int ii, ftf = 1; }; // Args are source file, source function name, source code line number, // caller address. These all come from the GCC compiler and TRACE macro. void trace(ch *file, ch *func, int line, void *addr) { using namespace tracenames; if (ftf) { ftf = 0; for (ii = 0; ii < 50; ii++) { filebuff[ii][99] = 0; funcbuff[ii][39] = 0; linebuff[ii] = 0; addrbuff[ii] = 0; } ii = 0; } if (line == linebuff[ii] && strmatch(func,funcbuff[ii])) return; // same as last call, don't duplicate if (++ii > 49) ii = 0; // add data to list strncpy(&filebuff[ii][0],file,99); strncpy(&funcbuff[ii][0],func,39); linebuff[ii] = line; addrbuff[ii] = addr; return; } // dump trace records to STDOUT void tracedump() { using namespace tracenames; FILE *fid; int kk; Plog(0," *** tracedump *** \n"); kk = ii; while (linebuff[kk]) { Plog(0,"TRACE %s %s %d %p \n",&filebuff[kk][0], &funcbuff[kk][0],linebuff[kk],addrbuff[kk]); if (--kk == ii) break; } fid = fopen("tracedump","w"); if (! fid) { perror("tracedump fopen() failure \n"); return; } fprintf(fid, " *** tracedump *** \n"); kk = ii; while (linebuff[kk]) { fprintf(fid, "TRACE %s %s %d %p \n",&filebuff[kk][0], &funcbuff[kk][0],linebuff[kk],addrbuff[kk]); if (--kk == ii) break; } fclose(fid); return; } /********************************************************************************/ // ch * combine_argvs(int argc, ch *argv[], Nth) // combine argv[ii] elements from Nth to last // returned char string has one blank between each element ch * combine_argvs(int argc, ch *argv[], int Nth) { int ii, ccv, outcc = 0; static ch output[XFCC]; for (ii = Nth; ii < argc; ii++) { ccv = strlen(argv[ii]); if (outcc + ccv > XFCC - 2) return 0; strcpy(output+outcc,argv[ii]); outcc += ccv; output[outcc] = ' '; outcc++; } outcc--; output[outcc] = 0; return output; } /********************************************************************************/ // get elapsed seconds // if init = 0 (default if missing), return seconds since boot. // if init = 1, returns seconds since last call. // theoretically uses a precise system clock but the precision is poor double get_seconds(int init) { timespec time1; static double secs1 = 0, secs2, secs3; if (init == 0) { clock_gettime(CLOCK_MONOTONIC_RAW,&time1); secs1 = time1.tv_sec; secs1 += time1.tv_nsec * 0.000000001; return secs1; } else { clock_gettime(CLOCK_MONOTONIC_RAW,&time1); secs2 = time1.tv_sec; secs2 += time1.tv_nsec * 0.000000001; secs3 = secs2 - secs1; secs1 = secs2; return secs3; } } // print elapsed seconds since last call with label void print_seconds(ch *label) { double secs = get_seconds(1); printf("%s seconds: %.3f \n",label,secs); return; } // get time in real seconds from timespec struct double get_seconds(timespec *ts) { double secs; secs = ts->tv_sec; secs += ts->tv_nsec * 0.000000001; return secs; } /********************************************************************************/ // simplified stat() function // type = zstat(ch *file, uint &mtime, uint &size) // returns: file modification date/time in seconds // file size in bytes // type returned: 0 = FNF or permissions denied, // 1 = reg. file, 2 = folder, 3 = other int zstat(ch *file, uint &mtime, uint &size) { STATB statB; int err, type; err = stat(file,&statB); if (err) return 0; size = statB.st_size; mtime = statB.st_mtim.tv_sec; type = statB.st_mode & S_IFMT; if (type == S_IFREG) return 1; if (type == S_IFDIR) return 2; return 3; } /********************************************************************************/ // start a timer or get elapsed time with millisecond resolution. void start_timer(double &time0) { timeval timev; gettimeofday(&timev,0); time0 = timev.tv_sec + 0.000001 * timev.tv_usec; return; } double get_timer(double &time0) { timeval timev; double time; gettimeofday(&timev,0); time = timev.tv_sec + 0.000001 * timev.tv_usec; return time - time0; } /********************************************************************************/ // get elapsed CPU time used by current process // returns seconds with millisecond resolution double CPUtime() { clock_t ctime = clock(); double dtime = ctime / 1000000.0; return dtime; } /********************************************************************************/ // get real memory used by current process, MB int memused() // 24.20 { ch buff1[100], buff2[1000]; ch *pp = 0; FILE *fid; double mem; int MB, pagesize; int mega = 1024 * 1024; snprintf(buff1,100,"/proc/self/stat"); // read file /proc/self/stat fid = fopen(buff1,"r"); if (fid) { pp = fgets(buff2,1000,fid); fclose(fid); } MB = 0; if (pp) { pp = strchr(pp,')'); // closing ')' after (short) filename if (pp) { parseprocrec(pp+1,22,&mem,null); // get real memory pagesize = sysconf(_SC_PAGESIZE); // system page size MB = mem * pagesize / mega + 0.5; } } return MB; } /********************************************************************************/ // convert a time_t date/time (e.g. st_mtime from stat() call) // into a compact date/time format "yyyymmddhhmmss" void compact_time(time_t DT, ch *compactDT) { struct tm *fdt; int year, mon, day, hour, min, sec; fdt = localtime(&DT); year = fdt->tm_year + 1900; mon = fdt->tm_mon + 1; day = fdt->tm_mday; hour = fdt->tm_hour; min = fdt->tm_min; sec = fdt->tm_sec; compactDT[0] = year / 1000 + '0'; compactDT[1] = (year % 1000) / 100 + '0'; compactDT[2] = (year % 100) / 10 + '0'; compactDT[3] = year % 10 + '0'; compactDT[4] = mon / 10 + '0'; compactDT[5] = mon % 10 + '0'; compactDT[6] = day / 10 + '0'; compactDT[7] = day % 10 + '0'; compactDT[8] = hour / 10 + '0'; compactDT[9] = hour % 10 + '0'; compactDT[10] = min / 10 + '0'; compactDT[11] = min % 10 + '0'; compactDT[12] = sec / 10 + '0'; compactDT[13] = sec % 10 + '0'; compactDT[14] = 0; return; } /********************************************************************************/ // convert a time_t date/time (e.g. st_mtime from stat() call) // into a pretty date/time format "yyyy:mm:dd hh:mm:ss" (19 chars. + null) void pretty_datetime(time_t DT, ch *prettyDT) // 25.1 { struct tm *fdt; int year, mon, day, hour, min, sec; fdt = localtime(&DT); year = fdt->tm_year + 1900; mon = fdt->tm_mon + 1; day = fdt->tm_mday; hour = fdt->tm_hour; min = fdt->tm_min; sec = fdt->tm_sec; prettyDT[0] = year / 1000 + '0'; prettyDT[1] = (year % 1000) / 100 + '0'; prettyDT[2] = (year % 100) / 10 + '0'; prettyDT[3] = year % 10 + '0'; prettyDT[4] = ':'; prettyDT[5] = mon / 10 + '0'; prettyDT[6] = mon % 10 + '0'; prettyDT[7] = ':'; prettyDT[8] = day / 10 + '0'; prettyDT[9] = day % 10 + '0'; prettyDT[10] = ' '; prettyDT[11] = hour / 10 + '0'; prettyDT[12] = hour % 10 + '0'; prettyDT[13] = ':'; prettyDT[14] = min / 10 + '0'; prettyDT[15] = min % 10 + '0'; prettyDT[16] = ':'; prettyDT[17] = sec / 10 + '0'; prettyDT[18] = sec % 10 + '0'; prettyDT[19] = 0; return; } /********************************************************************************/ // seconds since 1970 (double) <--> year/mon/day/hour/min/secs (int[6]) void secs_datetime(double secs, int datetime[6]) { time_t tsecs = secs; tm *tmx; tmx = localtime(&tsecs); datetime[0] = tmx->tm_year + 1900; datetime[1] = tmx->tm_mon + 1; datetime[2] = tmx->tm_mday; datetime[3] = tmx->tm_hour; datetime[4] = tmx->tm_min; datetime[5] = tmx->tm_sec; return; } void datetime_secs(int datetime[6], double *secs) { time_t tsecs; tm tmx; tmx.tm_year = datetime[0] - 1900; tmx.tm_mon = datetime[1] - 1; tmx.tm_mday = datetime[2]; tmx.tm_hour = datetime[3]; tmx.tm_min = datetime[4]; tmx.tm_sec = datetime[5]; tmx.tm_isdst = -1; tsecs = mktime(&tmx); *secs = tsecs; return; } /********************************************************************************/ // Read and parse /proc file with records formatted "parmname xxxxxxx" // Find all requested parameters and return their numeric values int parseprocfile(ch *pfile, ch *pname, double *value, ...) // EOL = 0 { FILE *fid; va_list arglist; ch buff[1000]; ch *pnames[20]; double *values[20]; int ii, fcc, wanted, found; pnames[0] = pname; // 1st parameter values[0] = value; *value = 0; va_start(arglist,value); for (ii = 1; ii < 20; ii++) // get all parameters { pnames[ii] = va_arg(arglist,ch *); if (! pnames[ii] || pnames[ii] == (ch *) 0x100000000) break; // ARM bug values[ii] = va_arg(arglist,double *); *values[ii] = 0; // initialize to zero } va_end(arglist); if (ii == 20) zappcrash("parseProcFile, too many fields"); wanted = ii; found = 0; fid = fopen(pfile,"r"); // open /proc/xxx file if (! fid) return 0; while ((fgets(buff,999,fid))) // read record, "parmname nnnnn" { for (ii = 0; ii < wanted; ii++) { // look for my fields fcc = strlen(pnames[ii]); if (strmatchN(buff,pnames[ii],fcc)) { *values[ii] = atof(buff+fcc); // return value found++; break; } } if (found == wanted) break; // stop when all found } fclose(fid); return found; } // Parse /proc record of the type "xxx xxxxx xxxxx xxxxxxxx xxx" // Return numeric values for requested fields (starting with 1) int parseprocrec(ch *prec, int field, double *value, ...) // EOL = 0 { va_list arglist; int xfield = 1, found = 0; va_start(arglist,value); while (*prec == ' ') prec++; // skip leading blanks while (field > 0) { while (xfield < field) // skip to next wanted field { prec = strchr(prec,' '); // find next blank if (! prec) break; while (*prec == ' ') prec++; // skip multiple blanks xfield++; } if (! prec) break; *value = atof(prec); // convert, return double found++; field = va_arg(arglist,int); // next field number if (! field || field == (int) 0x100000000) break; // ARM bug value = va_arg(arglist,double *); // next output double * } while (field > 0) { *value = 0; // zero values not found field = va_arg(arglist,int); value = va_arg(arglist,double *); } va_end(arglist); return found; } /********************************************************************************/ // get current CPU temperature // returns 0 if cannot find int coretemp() // use package temp { FILE *fid; static int ftf = 1, zone, temp; static ch Tfile[200]; ch buff[200], *pp; if (ftf) // first call, setup { // dump files ".../thermal_zone*/type" ftf = 0; fid = popen("cat /sys/class/thermal/thermal_zone*/type","r"); // find file containing "pkg_temp" if (! fid) return 0; for (zone = 0; ; zone++) { pp = fgets(buff,200,fid); if (! pp) break; pp = strstr(pp,"pkg_temp"); if (pp) break; } pclose(fid); // ignore 'broken pipe' status 24.20 if (! pp) { // failed zone = -1; return 0; } snprintf(Tfile,200,"cat /sys/class/thermal/thermal_zone%d/temp",zone); // corresp. file ".../thermal_zone*/temp" } if (zone < 0) return 0; // setup call failed fid = popen(Tfile,"r"); // read temp file if (! fid) return 0; pp = fgets(buff,200,fid); pclose(fid); if (! pp) return 0; temp = atoi(pp) / 1000; // get temp, deg. C x 1000 return temp; } /********************************************************************************/ // get current temperature for given disk, e.g. "/dev/sda" // depends on "smartctl" command from package smartmontools int disktemp(ch *disk) { int id, temp; ch *pp, *pp2; ch buff[200], command[100]; FILE *ffid; temp = 0; pp2 = 0; snprintf(command,100,"smartctl -A %s",disk); ffid = popen(command,"r"); if (! ffid) return 0; while (true) { pp = fgets(buff,200,ffid); // revised for smartctl report if (! pp) break; // format changes if (strmatchN(pp,"ID#",3)) pp2 = strstr(pp,"RAW_VALUE"); id = atoi(pp); if (id != 190 && id != 194) continue; // Airflow Temp. or Temp. if (! pp2) continue; temp = atoi(pp2); if (temp < 10 || temp > 99) temp = 0; break; } pclose(ffid); return temp; } /********************************************************************************/ // sleep for specified time in seconds (double) // signals can cause early return void zsleep(double dsecs) { unsigned isecs, nsecs; timespec tsecs; if (dsecs <= 0) return; isecs = unsigned(dsecs); nsecs = unsigned(1000000000.0 * (dsecs - isecs)); tsecs.tv_sec = isecs; tsecs.tv_nsec = nsecs; nanosleep(&tsecs,null); return; } /********************************************************************************/ // loop for specified time in seconds (double) void zloop(double dsecs) { double time0, time1; if (dsecs <= 0) return; time0 = get_seconds(); time1 = time0 + dsecs; while (get_seconds() < time1) continue; return; } /********************************************************************************/ // spinlock() is a simply way for a process to protect a code block from // concurrent execution by more than one thread, including the main() thread. // CANNOT BE USED for coroutines within one thread, e.g. GTK main loop. // // spinlock(1); // ... protected code // only one thread at a time can be in here // spinlock(0); // // will deadlock if already locked by same thread pthread_mutex_t spinmutex = PTHREAD_MUTEX_INITIALIZER; void spinlock(int lock) { if (lock) mutex_lock(&spinmutex); else mutex_unlock(&spinmutex); return; } /********************************************************************************/ // Lock or unlock a multi-process multi-thread resource. // Only one process/thread may possess a given lock. // A reboot or process exit or crash releases the lock. // lockfile is typically "/tmp/filename" and does not have to exist // // fd = global_lock(lockfile); // ... protected code // only one process/thread at a time // global_unlock(fd,lockfile); int global_lock(ch *lockfile) { int err, fd; while (true) // loop until success { fd = open(lockfile,O_RDWR|O_CREAT,0666); // open the lock file if (fd < 0) zappcrash("global_lock() %s",strerror(errno)); err = flock(fd,LOCK_EX); // request exclusive lock if (! err) return fd + 1; // return value >= 1 close(fd); // failed zsleep(0.001); // wait a bit and try again } } void global_unlock(int fd, ch *lockfile) { int err = close(fd-1); if (err < 0) zappcrash("global_unlock() %s",strerror(errno)); return; } /********************************************************************************/ // lock or unlock a resource // does not spin or wait for resource. // usable within or across threads in one process. // CANNOT BE USED for coroutines within one thread, e.g. GTK main loop. // return 0 if already locked, otherwise lock and return 1. mutex_t resource_lock_lock = PTHREAD_MUTEX_INITIALIZER; int resource_lock(int &resource) { if (resource) return 0; // locked mutex_lock(&resource_lock_lock); if (resource) { mutex_unlock(&resource_lock_lock); // locked return 0; } resource = 1; mutex_unlock(&resource_lock_lock); return 1; // locked OK } // unlock a locked resource void resource_unlock(int &resource) { mutex_lock(&resource_lock_lock); if (resource != 1) zappcrash("resource not locked"); // not locked resource = 0; // unlock mutex_unlock(&resource_lock_lock); return; } /********************************************************************************/ // Safely access and update parameters from multiple threads. // A mutex lock is used to insure one thread at a time has access to the parameter. // Many parameters can be used but there is only one mutex lock. // CANNOT BE USED for coroutines within one thread, e.g. GTK main loop. mutex_t zget_lock = PTHREAD_MUTEX_INITIALIZER; int zget_locked(int ¶m) // lock and return parameter { // (wait if locked) mutex_lock(&zget_lock); return param; } void zput_locked(int ¶m, int value) // set and unlock parameter { param = value; mutex_unlock(&zget_lock); return; } int zadd_locked(int ¶m, int incr) // lock, increment, unlock, return { int retval; mutex_lock(&zget_lock); retval = param + incr; param = retval; mutex_unlock(&zget_lock); return retval; } /********************************************************************************/ // Start a detached thread using a simplified protocol. // Will not make a zombie if caller exits without checking thread status. pthread_t start_detached_thread(void * threadfunc(void *), void * arg) { pthread_attr_t pthattr; pthread_t pthtid; int ii, err; pthread_attr_init(&pthattr); pthread_attr_setdetachstate(&pthattr,PTHREAD_CREATE_DETACHED); for (ii = 0; ii < 1000; ii++) { err = pthread_create(&pthtid,&pthattr,threadfunc,arg); if (! err) return pthtid; zsleep(0.001); if (err == EAGAIN) continue; // this shit happens break; } zexit(1,"pthread_create() failure: %s",strerror(err)); return 0; // avoid compiler warning } /********************************************************************************/ // Start a thread using a simplified protocol. // Caller must call wait_Jthread() to avoid creating a zombie process. pthread_t start_Jthread(void * threadfunc(void *), void * arg) { pthread_t tid; int err; err = pthread_create(&tid, null, threadfunc, arg); // retry EAGAIN failure removed 24.60 if (! err) return tid; // (does not work) zexit(1,"pthread_create() failure: %s",strerror(err)); // fail totally return 0; } // wait for thread to exit. int wait_Jthread(pthread_t tid) { int err; err = pthread_join(tid, null); if (! err) return 0; zexit(1,"pthread_join() failure: %s",strerror(err)); return 0; } /********************************************************************************/ // Synchronize execution of multiple threads. // Simultaneously resume NT calling threads. // from main(): synch_threads(NT) /* setup to synch NT threads */ // from each thread: synch_threads(0) /* suspend, resume simultaneously */ // // Each calling thread will suspend execution until all threads have suspended, // then they will all resume execution at the same time. If NT is greater than // the number of calling threads, the threads will never resume. void synch_threads(int NT) { static pthread_barrier_t barrier; static int bflag = 0; if (NT) { // main(), initialize if (bflag) pthread_barrier_destroy(&barrier); pthread_barrier_init(&barrier,null,NT); bflag = 1; return; } pthread_barrier_wait(&barrier); // thread(), wait for NT threads return; // unblock } /********************************************************************************/ // Test if caller is from the main() thread or from a created thread. // return 1 if main(), else 0 int main_thread() { if (pthread_equal(pthread_self(),zfuncs::tid_main)) return 1; return 0; } /******************************************************************************** int err = zshell(ch *options, ch *command, ...) Format and perform a shell command, wait for completion, return status. Shell command is performed in a thread with a GTK options: may be null or may contain any of the following substrings: "log" write command to log file, stdout "ack" popup user ACK message if the shell command has an error "noerr" do not log error status "thread" run shell command in a thread with GTK main loop wait // 25.1 command: shell command with optional '%' printf formats ... : optional arguments to stuff into printf formats returns: status of the shell command *********************************************************************************/ int zshell_stat; int zshell(ch *options, ch *command, ...) { void * zshell_thread(void *command); int Flog, Fack, Fnoerr, Fthread; va_list arglist; int err, cc, ccmax = 9999; // 25.1 ch command2[10000]; pthread_t tid; Flog = Fack = Fnoerr = Fthread = 0; if (options) { if (strstr(options,"log")) Flog = 1; // set options if (strstr(options,"ack")) Fack = 1; if (strstr(options,"noerr")) Fnoerr = 1; if (strstr(options,"thread")) Fthread = 1; // 25.1 } va_start(arglist,command); // format command cc = vsnprintf(command2,ccmax,command,arglist); va_end(arglist); if (cc >= ccmax) zappcrash("zshell: buffer overflow: %d",cc); if (Fnoerr && ccmax-cc > 17) strcat(command2," >/dev/null 2>&1"); // suppress error output if not wanted 25.0 if (Flog) Plog(0,"zshell: %s \n",command2); // command > log file if wanted if (Fthread) // do shell command in a thread 25.1 { zshell_stat = -1; tid = start_Jthread(zshell_thread,command2); // start thread while (zshell_stat < 0) zmainsleep(0.01); // wait for command status wait_Jthread(tid); // join thread } else zshell_stat = system(command2); err = WEXITSTATUS(zshell_stat); // get thread status if (err && ! Fnoerr) { // suppress log of command and error 24.20 if (! Flog) Plog(0,"zshell: %s \n",command2); // log command if not already Plog(0,"zshell error: %s \n",strerror(err)); // log error if (Fack) zmessageACK(mainwin,"command: %s \n error: %s", command2, strerror(err)); // popup error to user if wanted } return err; // return completion status } void * zshell_thread(void *command) { zshell_stat = system((ch *) command); return 0; } /********************************************************************************/ // kill processes matching a given process name, which may have '*' wildcards // int NP = kill_procname("process*name*") // status returned: N = processes found (0 if none found) int kill_procname(ch *wildname) { DIR *procdir; struct dirent *procent; int cc, found = 0; ch *pp, *pid, *filename; ch buff1[100], buff2[XFCC]; procdir = opendir("/proc"); if (! procdir) return 1; while (true) { procent = readdir(procdir); if (! procent) break; pid = procent->d_name; snprintf(buff1,100,"/proc/%s/exe",pid); cc = readlink(buff1,buff2,XFCC); if (cc <= 0) continue; buff2[cc] = 0; pp = strrchr(buff2,'/'); if (! pp) continue; filename = pp + 1; if (MatchWild(wildname,filename) != 0) continue; zshell("log","kill %s",pid); found++; } closedir(procdir); return found; } /********************************************************************************/ // Signal a running subprocess by name (name of executable or shell command). // Signal is "pause", "resume" or "kill". If process is paused, kill may not work, // so issue resume first if process is paused. int signalProc(ch *pname, ch *signal) { pid_t pid; FILE *fid; ch buff[100], *pp; int err, nsignal = 0; snprintf(buff,100,"ps -C %s h o pid",pname); fid = popen(buff,"r"); // popen() instead of system() if (! fid) return 2; pp = fgets(buff,100,fid); pclose(fid); if (! pp) return 4; pid = atoi(buff); if (! pid) return 5; if (strmatch(signal,"pause")) nsignal = SIGSTOP; if (strmatch(signal,"resume")) nsignal = SIGCONT; if (strmatch(signal,"kill")) nsignal = SIGKILL; err = kill(pid,nsignal); return err; } /********************************************************************************/ // fgets() with additional feature: trailing \n \r are removed. // optional bf flag: true if trailing blanks are to be removed. // trailing null character is assured. ch * fgets_trim(ch *buff, int maxcc, FILE *fid, int bf) { int cc; ch *pp; pp = fgets(buff,maxcc,fid); if (! pp) return pp; cc = strlen(buff); if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc; else while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc; buff[cc] = 0; return pp; } /********************************************************************************/ // get next record from open file steam // do not block if next record is not yet available // // buff: buffer for reading file // bcc: length of buffer // fid: open file stream // // returned ch * pointer: // null: EOF or error - file can be closed // "fgets_pend\n": next record not yet available, try again later // other: pointer to record in buffer, \n\0 terminated ch * fgets_pend(ch *buff, int bcc, FILE *fid) // 25.1 { int fd, flags; static FILE *pfid = 0; ch *pp; if (fid != pfid) { pfid = fid; fd = fileno(fid); // stop fgets() from blocking flags = fcntl(fd,F_GETFL,0); flags |= O_NONBLOCK; fcntl(fd,F_SETFL,flags); } pp = fgets(buff,500,fid); // read next record in file if (pp) return pp; // success if (feof(fid)) return 0; // EOF, no more data return "fgets_pend\n"; // next record not available yet } /********************************************************************************/ // Return 1 if both filespecs have the same folder, else return 0. // Both folders must be specified, at least one with ending '/' // (true if a file name is present) int samefolder(ch *file1, ch *file2) { ch *p1, *p2; int cc1, cc2, cc; p1 = strrchr(file1,'/'); // /dir1/dir2 p2 = strrchr(file2,'/'); // /dir1/dir2/file cc1 = cc2 = 0; if (p1) cc1 = p1 - file1; // /dir1/dir2/file if (p2) cc2 = p2 - file2; // | | if (cc2 > cc1) cc = cc2; // 0 cc else cc = cc1; if (cc == 0) return 0; if (strmatchN(file1,file2,cc)) return 1; return 0; } /********************************************************************************/ // Parse a pathname (filespec) and return its components. // Returned strings are subject to zfree() unless they are null. // folder is returned without trailing '/' but with two nulls on end, // to allow appending '/' if needed. // file is returned without .ext // ext is returned with leading '.' // returns 0 if OK, 1 if error int parsefile(ch *path, ch *&folder, ch *&file, ch *&ext) // make reentrant 25.1 { ch *pp1, *pp2; int cc; folder = file = ext = 0; if (! path || ! *path) return 1; // null or empty string pp1 = strrchr(path,'/'); if (! pp1) return 1; // /folders/.../file.ext cc = pp1 - path; // | | | if (cc < 2) return 1; // path pp1 pp2 *pp1 = 0; folder = zstrdup(path,"parsefile",2); *pp1 = '/'; pp1++; // /folders/.../file.ext if (! *pp1) return 0; // | | // pp1 pp2 pp2 = strrchr(pp1,'.'); if (! pp2) { file = zstrdup(pp1,"parsefile"); return 0; } *pp2 = 0; file = zstrdup(pp1,"parsefile"); *pp2 = '.'; ext = zstrdup(pp2,"parsefile"); return 0; } // free file name components allocated by parsefile() void parsefile_free(ch *&folder, ch *&file, ch *&ext) // 25.1 { if (folder) zfree(folder); if (file) zfree(file); if (ext) zfree(ext); folder = file = ext = 0; return; } /********************************************************************************/ // Move a source file to a destination file and delete the source file. // Equivalent to rename(), but the two files MAY be on different file systems. // Pathnames must be absolute (start with '/'). // Returns 0 if OK, +N if not. int renamez(ch *file1, ch *file2) { ch *pp1, *pp2; int err, Frename = 0; if (*file1 != '/' || *file2 != '/') return 1; // not absolute pathnames pp1 = strchr((ch *) file1+1,'/'); pp2 = strchr((ch *) file2+1,'/'); if (! pp1 || ! pp2) return 2; *pp1 = *pp2 = 0; if (strmatch(file1,file2)) Frename = 1; *pp1 = *pp2 = '/'; if (Frename) { // same top folder err = rename(file1,file2); if (err) return errno; return 0; } pp1 = zescape_quotes(file1); pp2 = zescape_quotes(file2); err = zshell(0,"mv -f \"%s\" \"%s\" ",pp1,pp2); zfree(pp1); zfree(pp2); return err; } /********************************************************************************/ // Check if a folder exists. If not, ask user if it should be created. // Returns 0 if OK or +N if error or user refused to create. // The user is notified of failure, no other message needed. // path may not have embedded quotes. int check_create_dir(ch *path) { int err, yn; STATB statB; err = stat(path,&statB); // check status if (! err) { if (S_ISDIR(statB.st_mode)) return 0; // exists, folder, OK else { zmessageACK(mainwin,"%s \n %s",path,strerror(ENOTDIR)); // exists, not a folder return ENOTDIR; } } if (errno != ENOENT) { zmessageACK(mainwin,"%s \n %s",path,strerror(errno)); // error other than missing return errno; } yn = zmessageYN(0,"create folder? \n %s",path); // ask to create if (! yn) return ENOENT; err = zshell("ack","mkdir -p -m 0750 \"%s\" ",path); // create if (! err) return 0; zmessageACK(mainwin,"%s \n %s",path,strerror(errno)); // failed to create return errno; } /********************************************************************************/ // copy a file using shell 'cp' command // file owner, permissions, and timestamps are copied // returns 0 if OK, else errno int cp_copy(ch *sfile, ch *dfile) { ch *pps, *ppd; if (strmatch(sfile,dfile)) { Plog(0,"ignore copy file to self: %s \n",sfile); return 0; } pps = zescape_quotes(sfile); ppd = zescape_quotes(dfile); int err = zshell(0,"cp -f -p \"%s\" \"%s\" ",pps,ppd); zfree(pps); zfree(ppd); return err; } /********************************************************************************/ // get the available space on disk of the given file // returns disk space in MB (limit 4 billion MB) uint diskspace(ch *file) { ch command[200], buff[200]; ch *pp; uint avail; FILE *fid; pp = zescape_quotes(file); snprintf(command,200,"df --output=avail \"%s\" ",pp); zfree(pp); fid = popen(command,"r"); if (! fid) return 0; pp = fgets(buff,200,fid); // "Avail" header pp = fgets(buff,200,fid); // decimal number, KB space pclose(fid); if (! pp) return 0; avail = 0.001 * atoll(pp); // MB space return avail; } /********************************************************************************/ // if a file has an incorrect .ext, return the correct .ext // return null if file is OK or cannot be determined // returned .ext is in a static buffer NOT THREAD SAFE ch * get_file_extension(ch *file) { ch *fext1; ch *extlist, *fext2, *pp; ch *buff; int cc; FILE *fid; static ch fext3[20]; errno = 0; cc = strlen(file) + 20; buff = (ch *) zmalloc(cc,0); pp = zescape_quotes(file); snprintf(buff,cc,"file --extension %s",pp); // 'file' command - get correct extensions zfree(pp); fid = popen(buff,"r"); if (! fid) goto ret0; extlist = fgets(buff,cc,fid); // /.../filename.ext: ext1/ext2/... pclose(fid); if (! extlist) goto ret0; extlist = strrchr(extlist,':'); // extlist = : ext1/ext2/... if (! extlist) goto ret0; extlist += 2; // extlist = ext1/ext2/... fext1 = strrchr(file,'.'); // fext1 = file current .ext if (! fext1) fext1 = ".xxx"; if (strcasestr(extlist,fext1+1)) goto ret0; // fext1 found in extlist fext2 = extlist; pp = strchr(fext2,'/'); // fext2 = first in extlist if (pp) *pp = 0; strncpy0(fext3,fext2,20); // return correct .ext in static buffer zfree(buff); return fext3; ret0: if (errno) Plog(0,"%s\n",strerror(errno)); zfree(buff); return 0; } /********************************************************************************/ // Return all the file names in a folder, sorted in alphabetic order. // Subfolders are not included. // The 'files' argument is allocated and filled with pointers to file names. // (the names in the folder, not the full path names) // The number of files found is returned. // -1 is returned if the folder is invalid or other error. // If 'files' is returned non-null, it is subject to zfree() int zreaddir(ch *folder, ch **&files) { struct dirent *dirent1; int Nfiles = 0, maxfiles = 100; DIR *direc; ch **ufiles, **ufiles2; files = 0; // nothing returned yet ufiles = (ch **) zmalloc(maxfiles * sizeof(ch *),"zreaddir"); // starting space direc = opendir(folder); // open caller's folder if (! direc) return -1; while (true) { if (Nfiles == maxfiles) // out of space { ufiles2 = (ch **) zmalloc(2 * maxfiles * sizeof(ch *),"zreaddir"); // allocate new space = 2x old space memcpy(ufiles2,ufiles, maxfiles * sizeof(ch *)); // copy data to new space zfree(ufiles); // free old space ufiles = ufiles2; // set new space maxfiles *= 2; // new capacity } dirent1 = readdir(direc); // get next file in folder if (! dirent1) break; if (dirent1->d_type != DT_REG) continue; // skip subfolders ufiles[Nfiles] = zstrdup(dirent1->d_name,"zreaddir"); // add to file list Nfiles++; continue; } closedir(direc); if (Nfiles > 1) HeapSort(ufiles,Nfiles); // sort file list files = ufiles; // return allocated file list return Nfiles; // return file count } /********************************************************************************/ // int NR = zreadfile(ch *filename, ch &**rrecs) // // Read a text file into a list of ch * strings, 1 record per string. // The strings are allocated as needed. The number of records is returned. // Returned: -1 error (errno is set) // 0 empty file // NR records read, > 0 // Returned record N: rrecs[N] (ch *) // Trailing blanks and '\n' characters are removed. // The maximum record length is 1000 chars, including terminating null. // The maximum record count is 1000 records. // Null records ("" or "\n") are not included in output. // rrecs[NR] (last + 1) is a null pointer. int zreadfile(ch *filename, ch **&rrecs) { FILE *fid; ch *recs[1001]; ch buff[1001], *pp; int cc, NR = 0; rrecs = 0; // initz. no data fid = fopen(filename,"r"); // open file if (! fid) return -1; while (true) { pp = fgets(buff,1001,fid); // read record if (! pp) break; // EOF cc = strlen(pp); if (cc > 999) { zmessageACK(mainwin,"zreadfile() record too long %s",filename); errno = EFBIG; return -1; } while (cc && pp[cc-1] > 0 && pp[cc-1] <= ' ') --cc; // remove trailing \n, \r, blanks, etc. pp[cc] = 0; // terminating null if (cc == 0) continue; // discard null recs recs[NR] = (ch *) zmalloc(cc+1,"zreadfile"); // allocate memory memcpy(recs[NR],pp,cc+1); // copy record NR++; if (NR == 1000) { zmessageACK(mainwin,"zreadfile() too many records %s",filename); errno = EFBIG; return -1; } } fclose(fid); recs[NR] = 0; // last record + 1 = null cc = (NR + 1) * sizeof(ch *); // allocate caller rrecs list rrecs = (ch **) zmalloc(cc,"zreadfile"); memcpy(rrecs,recs,cc); // copy record pointers + null return NR; } // int NR = zwritefile(ch *filename, ch **rrecs) // write array of records to a file, each with trailing \n character. // EOF is signalled with a null pointer: rrecs[last] = null // returns no. records written (>= 0) or -1 if file error. int zwritefile(ch *filename, ch **rrecs) { FILE *fid; int nr, nn; fid = fopen(filename,"w"); // open file if (! fid) return -1; for (nr = 0; nr < 1000; nr++) { if (! rrecs[nr]) break; nn = fprintf(fid,"%s\n",rrecs[nr]); if (nn <= 0) break; } fclose(fid); return nr; } // free allocated records and their pointer list void zreadfile_free(ch **&rrecs) { for (int ii = 0; rrecs[ii]; ii++) // loop until null pointer zfree(rrecs[ii]); zfree(rrecs); rrecs = 0; // set no data return; } /********************************************************************************/ // these functions are a replacement for the strcmp functions // null strings are allowed and compare false to non-null strings // null strings compare true to null strings int strmatch(cch *str1, cch *str2) { if (str1 && str2) return ! strcmp(str1,str2); if (! str1 && ! str2) return 1; else return 0; } int strmatchN(cch *str1, cch *str2, int cc) { if (str1 && str2) return ! strncmp(str1,str2,cc); else return 0; } int strmatchcase(cch *str1, cch *str2) { if (str1 && str2) return ! strcasecmp(str1,str2); if (! str1 && ! str2) return 1; else return 0; } int strmatchcaseN(cch *str1, cch *str2, int cc) { if (str1 && str2) return ! strncasecmp(str1,str2,cc); else return 0; } /******************************************************************************** substringR() ch * substringR(ch *string, ch *delims, int Nth) Get the Nth substring in an input string, which contains at least N substrings delimited by the character(s) in delim (e.g. blank, comma). Nth >= 1. Returns a pointer to the found substring (actually a pointer to a copy of the found substring, with a null terminator appended). The returned pointer is a subject for zfree(). If a delimiter is immediately followed by another delimiter, it is considered a substring with zero length, and the string "" is returned. Leading blanks in a substring are omitted from the returned substring. A substring with only blanks is returned as "". The last substring may be terminated by null or a delimiter. Characters within quotes (") are treated as data within a substring, i.e. blanks and delimiters are not processed as such. The quotes are removed from the returned substring. If there are less than Nth substrings, a null pointer is returned. This function is thread-safe. See below for simpler non-thread-safe version. Example: input string: ,a,bb, cc, ,dd"ee,ff"ggg, (first and last characters are comma) delimiter: comma Nth returned string 1: (null) 2: a 3: bb 4: cc 5: (one blank) 6: ddee,ffggg 7: (null) last+1 substring *********************************************************************************/ ch * substringR(ch *string, ch *delims, int Nth) { ch *pf1, pf2[2000]; // 2000 char. limit ch quote = '"'; int nf, fcc = 0; if (! string || ! *string) return 0; // bad call if (Nth < 1) return 0; pf1 = (ch *) string - 1; // start parse nf = 0; while (nf < Nth) { pf1++; // start substring nf++; fcc = 0; while (*pf1 == ' ') pf1++; // skip leading blanks while (true) { if (*pf1 == quote) { // pass chars between quotes pf1++; // (but without the quotes) while (*pf1 && *pf1 != quote) pf2[fcc++] = *pf1++; if (*pf1 == quote) pf1++; } else if (strchr(delims,*pf1) || *pf1 == 0) break; // found delimiter or null else pf2[fcc++] = *pf1++; // pass normal character if (fcc > 1999) zappcrash("substringR() too long"); } if (*pf1 == 0) break; // end of input string } if (nf < Nth) return 0; // no Nth substring if (fcc == 0 && *pf1 == 0) return 0; // empty substring pf2[fcc] = 0; return zstrdup(pf2,"substringR"); // returned string (needs zfree()) } // alternative with one delimiter ch * substringR(ch *string, ch delim, int Nth) { ch delims[2] = "x"; *delims = delim; return substringR(string,delims,Nth); } // non-thread-safe versions without zfree() requirement // returned substring is in static memory // only the last 1000 results are kept <----- NOTE limitation ch * substring(ch *string, ch *delims, int Nth) // 24.40 { ch *s1; static ch *s2[1000]; static int kk = -1; if (kk == -1) memset(s2,0,1000*sizeof(ch *)); s1 = substringR(string,delims,Nth); if (! s1) return 0; kk++; if (kk == 1000) kk = 0; if (s2[kk]) zfree(s2[kk]); s2[kk] = s1; return s1; } ch * substring(ch *string, ch delim, int Nth) { ch delims[2] = "x"; *delims = delim; return substring(string,delims,Nth); } /******************************************************************************** decode a string with embedded delimiters and return the substrings input: string1^ string2^ ... stringN where '^' is the delimiter output: output[0], output[1] ... output[N] delim: single delimiter character count: max. number of output strings maxcc: max. output string length the output strings must be pre-allocated with at least maxcc chars. returns: no. substrings found output string = single null if two delimiters are separated by nothing or blanks ****/ int get_substrings(ch *input, ch delim, int count, int maxcc, ch **output) // 25.1 { int Nth = 0, ii, cc; ch *pp1 = input, *pp2; for (ii = 0; ii < count; ii++) // clear outputs output[ii][0] = 0; for (Nth = 0; Nth < count; Nth++) // aaaaaaaa^ bbbbbbbbbbbbb^ ... ^ ccccccc { // | | | | | | | while (*pp1 == ' ') pp1++; // pp1 pp2 pp1 pp2 pp2 pp1 pp2 if (! *pp1) break; pp2 = strchr(pp1,delim); if (! pp2) pp2 = pp1 + strlen(pp1); cc = pp2 - pp1; if (cc >= maxcc) cc = maxcc - 1; if (cc) strncpy(output[Nth],pp1,cc); output[Nth][cc] = 0; if (*pp2 == delim) pp1 = pp2 + 1; else pp1 = pp2; } return Nth; } /********************************************************************************/ // Produce random value from hashed input string. // Output range is 0 to max-1. // Benchmark: 0.2 usec for 99 char. string, 3 GHz Core i5 int strHash(ch *string, uint max) { uint hash = 1357; uch byte; while ((byte = *string++)) { hash = hash * (byte + 111); hash = hash ^ (hash >> 9); hash = hash ^ (byte << 9); } hash = hash % max; return hash; } /********************************************************************************/ // Copy string with specified max. length (including null terminator). // truncate if needed. null terminator is always supplied. // Returns 0 if no truncation, 1 if input string was truncated to fit. int strncpy0(ch *dest, ch *source, uint cc) { strncpy(dest,source,cc); dest[cc-1] = 0; if (strlen(source) >= cc) return 1; // truncated else return 0; } /********************************************************************************/ // Copy string with blank pad to specified length. No null is added. void strnPad(ch *dest, ch *source, int cc) { strncpy(dest,source,cc); int ii = strlen(source); for (int jj = ii; jj < cc; jj++) dest[jj] = ' '; } /********************************************************************************/ // Remove trailing blanks from a string. Returns remaining length. int strTrim(ch *dest, ch *source) { if (dest != source) strcpy(dest,source); return strTrim(dest); } int strTrim(ch *dest) { int ii = strlen(dest); while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0; return ii; } /********************************************************************************/ // Remove leading and trailing blanks from a string. // Returns remaining length, possibly zero. int strTrim2(ch *dest, ch *source) { ch *pp1, *pp2; int cc; pp1 = source; pp2 = source + strlen(source) - 1; while (*pp1 == ' ') pp1++; if (*pp1 == 0) { strcpy(dest,""); // null or blank input return 0; } while (*pp2 == ' ' && pp2 > pp1) pp2--; cc = pp2 - pp1 + 1; memmove(dest,pp1,cc); dest[cc] = 0; return cc; } int strTrim2(ch *string) { return strTrim2(string,string); } /********************************************************************************/ // Remove all blanks from a string. Returns remaining length. int strCompress(ch *dest, ch *source) { if (dest != source) strcpy(dest,source); return strCompress(dest); } int strCompress(ch *string) { int ii, jj; for (ii = jj = 0; string[ii]; ii++) { if (string[ii] != ' ') { string[jj] = string[ii]; jj++; } } string[jj] = 0; return jj; } /********************************************************************************/ // Concatenate multiple strings, staying within a specified overall length. // The destination string is also the first source string. // Null marks the end of the source strings (omission --> crash). // Output is truncated to fit within the specified length. // A final null is assured and is included in the length. // Returns 0 if OK, 1 if truncation was needed. int strncatv(ch *dest, int maxcc, ch *source, ...) { ch *ps; va_list arglist; maxcc = maxcc - strlen(dest) - 1; if (maxcc < 0) return 1; va_start(arglist,source); ps = source; while (ps) { strncat(dest,ps,maxcc); maxcc = maxcc - strlen(ps); if (maxcc < 0) break; ps = va_arg(arglist,ch *); if (! ps || ps == (ch *) 0x100000000) break; // ARM bug } va_end(arglist); if (maxcc < 0) return 1; return 0; } /********************************************************************************/ // Match 1st string to N additional strings. // Return matching string number 1 to N or 0 if no match. // Supply a null argument for end of list. int strmatchV(ch *string, ...) { int match = 0; ch *stringN; va_list arglist; va_start(arglist,string); while (true) { stringN = va_arg(arglist, ch *); if (stringN == null || stringN == (ch *) 0x100000000) // ARM bug { va_end(arglist); return 0; } match++; if (strmatch(string,stringN)) { va_end(arglist); return match; } } } /********************************************************************************/ // convert string to upper case void strToUpper(ch *string) { int ii; ch jj; int delta = 'A' - 'a'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta; } void strToUpper(ch *dest, ch *source) { strcpy(dest,source); strToUpper(dest); } /********************************************************************************/ // convert string to lower case void strToLower(ch *string) { int ii; ch jj; int delta = 'a' - 'A'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta; } void strToLower(ch *dest, ch *source) { strcpy(dest,source); strToLower(dest); } /********************************************************************************/ // Copy string strin to strout, replacing every occurrence // of the substring ssin with the substring ssout. // maxcc is the limit for strout. The actual cc is returned. // Replacement strings may be longer or shorter or have zero length. int repl_1str(ch *strin, ch *strout, int maxcc, ch *ssin, ch *ssout) { int cc, cc1, cc2, ccout, nfound; ch *pp, *pp1, *pp2; pp1 = strin; pp2 = strout; cc1 = strlen(ssin); cc2 = strlen(ssout); nfound = 0; ccout = 0; while ((pp = strstr(pp1,ssin))) { nfound++; cc = pp - pp1; if (ccout + cc >= maxcc) break; memcpy(pp2,pp1,cc); pp2 += cc; pp1 += cc; ccout += cc; if (ccout + cc2 >= maxcc) break; memcpy(pp2,ssout,cc2); pp1 += cc1; pp2 += cc2; ccout += cc2; } cc = strlen(pp1); if (ccout + cc >= maxcc) cc = maxcc - ccout - 1; // overflow, keep as much as poss. 25.1 memcpy(pp2,pp1,cc); ccout += cc; strout[ccout] = 0; return ccout; } /********************************************************************************/ // Copy string strin to strout, replacing multiple substrings with replacement strings. // Multiple pairs of string arguments follow maxcc, a substring and a replacement string. // Last pair of string arguments must be followed by a null argument. // maxcc is the limit for strout. The actual cc is returned. // Replacement strings may be longer or shorter or have zero length. int repl_Nstrs(ch *strin, ch *strout, int maxcc, ...) { va_list arglist; ch *ssin, *ssout; ch ftemp[2000]; int ftf, ccout; ftf = 1; ccout = 0; va_start(arglist,maxcc); while (true) { ssin = va_arg(arglist, ch *); if (! ssin || ssin == (ch *) 0x100000000) break; // ARM bug ssout = va_arg(arglist, ch *); if (ftf) { ftf = 0; ccout = repl_1str(strin,strout,maxcc,ssin,ssout); } else { if (ccout >= 2000) goto stop; strcpy(ftemp,strout); ccout = repl_1str(ftemp,strout,maxcc,ssin,ssout); } } stop: va_end(arglist); return ccout; } /********************************************************************************/ // Break up a long text string into lines no longer than cc2 chars. // If fake newlines ("\n") are found, replace them with real newlines. // Break unconditionally where newlines are found and remove them. // Break at last blank ch between cc1 and cc2 if present. // Break at last delimiter ch between cc1 and cc2 if present. // Break unconditionally at cc2 if none of the above. // Returns text lines in txout[*] with count as returned function value. // txout[*] are subjects for zfree(). int breakup_text(ch *txin0, ch **&txout, ch *delims, int cc1, int cc2) { ch *txin; uch ch1; int ccmax; int p1, p2, cc3, Nout; int Np, Bp, Sp; txin = zstrdup(txin0,"breakup_text"); ccmax = strlen(txin); txout = (ch **) zmalloc(100 * sizeof(ch *),"breakup_text"); // 100 line limit if (strstr(txin0,"\\n")) // replace "\n" with real newline chars repl_1str(txin0,txin,ccmax,"\\n","\n"); Nout = p1 = 0; while (true) { p2 = p1; // input line position cc3 = 0; // output line cc Np = Bp = Sp = 0; while (txin[p2]) // scan further up to cc2 chars { ch1 = txin[p2]; if (ch1 == '\n') { Np = p2; break; } // break out if newline found if (cc3 >= cc1) { if (ch1 == ' ') Bp = p2; // remember last ' ' found after cc1 chars if (delims && strchr(delims,ch1)) Sp = p2; // remember last delimiter found after cc1 } if (ch1 < 0) // UTF8 wide character while ((ch1 = txin[p2+1]) < 0xC0) p2++; p2++; cc3++; if (cc3 == cc2) break; } if (! cc3 && ! Np) break; // nothing left if (Np) cc3 = Np - p1; // newline found else { if (cc3 < cc2) Bp = Sp = 0; // line fits cc2 limit if (Bp) cc3 = Bp - p1 + 1; // break at previous ' ' else if (Sp) cc3 = Sp - p1 + 1; // break at previous delimiter else cc3 = p2 - p1; } if (txin[p1] == ' ' && cc3) { p1++; cc3--; } // remove leading blank if (cc3 > 0) { // avoid blank line txout[Nout] = (ch *) zmalloc(cc3+1,"breakup_text"); strncpy0(txout[Nout],txin+p1,cc3+1); if (++Nout == 100) break; } p2 = p1 + cc3; if (Np) p2++; p1 = p2; } zfree(txin); return Nout; } /********************************************************************************/ // Copy and convert string to hex string. // Each input character 'A' >> 3 output characters "41 " void strncpyx(ch *out, ch *in, int ccin) { int ii, jj, c1, c2; ch cx[] = "0123456789ABCDEF"; if (! ccin) ccin = strlen(in); for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3) { c1 = (uch) in[ii] >> 4; c2 = in[ii] & 15; out[jj] = cx[c1]; out[jj+1] = cx[c2]; out[jj+2] = ' '; } out[jj] = 0; return; } /********************************************************************************/ // Strip trailing zeros from ascii floating numbers // (e.g. 1.230000e+02 --> 1.23e+02) void StripZeros(ch *pNum) { int ii, cc; int pp, k1, k2; ch work[20]; cc = strlen(pNum); if (cc >= 20) return; for (ii = 0; ii < cc; ii++) { if (pNum[ii] == '.') { pp = ii; k1 = k2 = 0; for (++ii; ii < cc; ii++) { if (pNum[ii] == '0') { if (! k1) k1 = k2 = ii; else k2 = ii; continue; } if ((pNum[ii] >= '1') && (pNum[ii] <= '9')) { k1 = 0; continue; } break; } if (! k1) return; if (k1 == pp + 1) k1++; if (k2 < k1) return; strcpy(work,pNum); strcpy(work+k1,pNum+k2+1); strcpy(pNum,work); return; } } } /********************************************************************************/ // test for blank/null string // Returns status depending on input string: // 0 not a blank or null string // 1 argument string is NULL // 2 string has zero length (*string == 0) // 3 string is all blanks int blank_null(ch *string) { if (! string) return 1; // null string if (! *string) return 2; // zero length string int cc = strlen(string); for (int ii = 0; ii < cc; ii++) if (string[ii] != ' ') return 0; // non-blank string return 3; // blank string } /********************************************************************************/ // clean \x escape sequences and replace them with the escaped character // \n >> newline \" >> doublequote \\ >> backslash etc. // see $ man ascii for the complete list int clean_escapes(ch *string) { ch *pp1 = string, *pp2 = string, *pp; ch char1; ch escapes[] = "abtnvfr"; int count = 0; while (true) { char1 = *pp1++; if (char1 == 0) { *pp2 = 0; return count; } else if (char1 == '\\') { char1 = *pp1++; pp = strchr(escapes,char1); if (pp) char1 = pp - escapes + 7; count++; } *pp2++ = char1; } } /********************************************************************************/ // Compute the graphic character count for a UTF8 character string. // Depends on UTF8 rules: // - ascii characters are positive (0x00 to 0x7F) // - 1st ch of multich sequence is negative (0xC0 to 0xFD) // - subsequent multichars are in the range 0x80 to 0xBF int utf8len(ch *utf8string) { int ii, cc; ch xlimit = 0xC0; for (ii = cc = 0; utf8string[ii]; ii++) { if (utf8string[ii] < 0) // multibyte character while (utf8string[ii+1] < xlimit) ii++; // skip extra bytes cc++; } return cc; } /********************************************************************************/ // Extract a UTF8 substring with a specified count of graphic characters. // utf8in input UTF8 string // utf8out output UTF8 string, which must be long enough // pos initial graphic character position to get (0 = first) // cc max. count of graphic characters to get // returns number of graphic characters extracted, <= cc // Output string is null terminated after last extracted character. int utf8substring(ch *utf8out, ch *utf8in, int pos, int cc) { int ii, jj, kk, posx, ccx; ch xlimit = 0xC0; for (ii = posx = 0; posx < pos && utf8in[ii]; ii++) { if (utf8in[ii] < 0) while (utf8in[ii+1] < xlimit) ii++; posx++; } jj = ii; for (ccx = 0; ccx < cc && utf8in[jj]; jj++) { if (utf8in[jj] < 0) while (utf8in[jj+1] < xlimit) jj++; ccx++; } kk = jj - ii; strncpy(utf8out,utf8in+ii,kk); utf8out[kk] = 0; return ccx; } /********************************************************************************/ // check a character string for valid utf8 encoding // returns: 0 = OK, 1 = bad string int utf8_check(ch *string) { ch *pp; uch ch1, ch2, nch; for (pp = string; *pp; pp++) { ch1 = *pp; if (ch1 < 0x7F) continue; if (ch1 > 0xBF && ch1 < 0xE0) nch = 1; else if (ch1 < 0xF0) nch = 2; else if (ch1 < 0xF8) nch = 3; else if (ch1 < 0xFC) nch = 4; else if (ch1 < 0xFE) nch = 5; else return 1; while (nch) { pp++; ch2 = *pp; if (ch2 < 0x80 || ch2 > 0xBF) return 1; nch--; } } return 0; } /********************************************************************************/ // check a character string for valid utf8 encoding // invalid characters are replaced with '?' // returns no. of characters replaced (0 if no errors found) int utf8_clean(ch *string) // 24.60 { int badbytes = 0; ch *pp1, *pp2; uch ch1, ch2, nch; for (pp1 = string; *pp1; pp1++) { ch1 = *pp1; if (ch1 < 0x7F) continue; // ascii, single byte if (ch1 > 0xBF && ch1 < 0xE0) nch = 1; // extra bytes else if (ch1 < 0xF0) nch = 2; else if (ch1 < 0xF8) nch = 3; else if (ch1 < 0xFC) nch = 4; else if (ch1 < 0xFE) nch = 5; else goto fix1; // >5 invalid pp2 = pp1; while (nch) { // check extra bytes pp1++; ch2 = *pp1; if (ch2 < 0x80 || ch2 > 0xBF) goto fix2; // invalid nch--; } continue; fix1: // replace bad byte with '?' *pp1 = '?'; badbytes++; continue; fix2: pp1 = pp2; // back to initial byte *pp1 = '?'; // replace with '?' badbytes++; continue; } return badbytes; } /********************************************************************************/ // Find the Nth graphic character position within a UTF8 string // utf8in input UTF8 string // Nth graphic character position, zero based // returns starting character (byte) position of Nth graphic character // returns -1 if Nth is beyond the string length int utf8_position(ch *utf8in, int Nth) { int ii, posx; ch xlimit = 0xC0; for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++) { if (utf8in[ii] < 0) // multi-byte character while (utf8in[ii+1] && utf8in[ii+1] < xlimit) ii++; // traverse member bytes posx++; } if (utf8in[ii]) return ii; return -1; } /********************************************************************************/ // err = zsed(file, string1, string2 ... null) // // replace string1/3/5... with string2/4/6... in designated file // returns N lines changed // -1 file not found // -2 other error (with message) int zsed(ch *infile ...) { int err, ftf, nn; FILE *fid1, *fid2; ch *outfile, *pp; ch buffin[1000], buffout[1000], buffxx[1000]; ch *stringin, *stringout; va_list arglist; fid1 = fopen(infile,"r"); if (! fid1) return -1; outfile = zstrdup(infile,"zsed",8); strcat(outfile,"-temp"); fid2 = fopen(outfile,"w"); if (! fid2) { Plog(0,"%d \n",strerror(errno)); zfree(outfile); return -2; } nn = 0; while (true) { pp = fgets(buffin,500,fid1); if (! pp) break; va_start(arglist,infile); ftf = 1; while (true) { stringin = va_arg(arglist, ch *); if (! stringin || stringin == (ch *) 0x100000000) break; // ARM bug stringout = va_arg(arglist, ch *); if (! stringout || stringout == (ch *) 0x100000000) break; if (ftf) { ftf = 0; nn += repl_1str(buffin,buffout,1000,stringin,stringout); } else { strcpy(buffxx,buffout); nn += repl_1str(buffxx,buffout,1000,stringin,stringout); } } va_end(arglist); fputs(buffout,fid2); } fclose(fid1); err = fclose(fid2); if (err) { Plog(0,"%s \n",strerror(errno)); zfree(outfile); return -2; } rename(outfile,infile); zfree(outfile); return nn; } /********************************************************************************/ // zstrstr() and zstrcasestr() work like strstr() and strcasestr() // but the needle string "" does NOT match any haystack string. ch * zstrstr(ch *haystack, ch *needle) { if (! needle || ! *needle) return 0; return strstr(haystack,needle); } ch * zstrcasestr(ch *haystack, ch *needle) { if (! needle || ! *needle) return 0; return strcasestr(haystack,needle); } /********************************************************************************/ // strcpy() with overlap allowed ch * zstrcpy(ch *dest, ch *source) { int cc = strlen(source); memmove(dest,source,cc); dest[cc] = 0; return dest; } // strncpy() with overlap allowed ch * zstrncpy(ch *dest, ch *source, int cc) { memmove(dest,source,cc); return dest; } /********************************************************************************/ // works like strcmp(), but compare is terminated by \n as well as null int zstrcmp(ch *s1, ch *s2) { ch *p1, *p2; int nn; p1 = (ch *) strchr(s1,'\n'); p2 = (ch *) strchr(s2,'\n'); if (p1) *p1 = 0; if (p2) *p2 = 0; nn = strcmp(s1,s2); if (p1) *p1 = '\n'; if (p2) *p2 = '\n'; return nn; } /********************************************************************************/ // works like strcmp(), but using char *args instead of const char* int zstrcmp2(ch *s1, ch *s2) { return strcmp((cch *) s1, (cch *) s2); } /********************************************************************************/ // works like strcasecmp(), but using char *args instead of const char* int zstrcasecmp(ch *s1, ch *s2) { return strcasecmp((cch *) s1, (cch *) s2); } /********************************************************************************/ // escape quote marks (" and ') in a string, for use in shell commands // returned file is subject for zfree() ch * zescape_quotes(ch *str1) { ch *str2 = 0; int cc; if (strchr(str1,'"') == 0) { str2 = zstrdup(str1,"zescape_quotes"); return str2; } cc = strlen(str1); str2 = (ch *) zmalloc(cc+40,"zescape_quotes"); repl_Nstrs(str1,str2,cc+40,"\"","\\\"","\'","\\\'",0); return str2; } /******************************************************************************** Conversion Utilities convSI(string, inum, delim) string to int convSI(string, inum, low, high, delim) string to int with range check convSD(string, dnum, delim) string to double convSD(string, dnum, low, high, delim) string to double with range check convSF(string, fnum, delim) string to float convSF(string, fnum, low, high, delim) string to float with range check convIS(inum, string, cc) int to string with returned cc convDS(fnum, digits, string, cc) double to string with specified digits of precision and returned cc string input (ch *) or output (ch *) inum input (int) or output (int &) dnum input (double) or output (double &) delim optional returned delimiter (null or ch **) low, high input range check (int or double) cc output string length (int &) digits input digits of precision (int) to be used for output string NOTE: decimal point may be comma or period. 1000's separators must NOT be present. convIS and convDS also return the length cc of the string output. convDS accepts same formats as atof. Decimal point can be comma or period. convDS will use whatever format (f/e) gives the shortest result. Outputs like "e03" or "e+03" will be shortened to "e3". function status returned: 0 normal conversion, no invalid digits, blank/null termination 1 successful conversion, but trailing non-numeric found 2 conversion OK, but outside specified limits 3 null or blank string, converted to zero (obsolete, now status 4) 4 conversion error, invalid data in string overlapping statuses have following precedence: 4 3 2 1 0 *********************************************************************************/ // Convert string to integer int convSI(ch *string, int &inum, ch **delim) // use glib function { ch *ddelim = 0; int err; inum = strtol(string,&ddelim,10); // convert next characters if (delim) *delim = ddelim; if (ddelim == string) err = 4; // no valid digits else if (*ddelim == '\0') err = 0; // null delimiter else if (*ddelim == ' ') err = 0; // blank delimiter else err = 1; // other delimiter return err; } int convSI(ch *string, int &inum, int lolim, int hilim, ch **delim) { int stat = convSI(string,inum,delim); if (stat > 2) return stat; // invalid or null/blank if (inum < lolim) return 2; // return 2 if out of limits if (inum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } // Convert string to double *** status 3 --> status 4 *** int convSD(ch *string, double &dnum, ch **delim) // use glib function { ch *ddelim = 0; int err; dnum = strtod(string,&ddelim); if (delim) *delim = ddelim; if (ddelim == string) err = 4; // no valid digits else if (*ddelim == '\0') err = 0; // OK, null delimiter else if (*ddelim == ' ') err = 0; // OK, blank delimiter else err = 1; // OK, other delimiter return err; } int convSD(ch *string, double &dnum, double lolim, double hilim, ch **delim) { int stat = convSD(string,dnum,delim); if (stat > 2) return stat; // invalid or null/blank if (dnum < lolim) return 2; // return 2 if out of limits if (dnum > hilim) return 2; // (has precedence over status 1) return stat; // limits OK, return 0 or 1 } int convSF(ch *string, float &fnum, ch **delim) { double dnum; int err; err = convSD(string,dnum,delim); fnum = dnum; return err; } int convSF(ch *string, float &fnum, float lolim, float hilim, ch **delim) { double dnum, dlolim = lolim, dhilim = hilim; int err; err = convSD(string,dnum,dlolim,dhilim,delim); fnum = dnum; return err; } // Convert int to string with returned length. // (will never exceed 12 characters) int convIS(int inum, ch *string, int *cc) { int ccc; ccc = snprintf(string,12,"%d",inum); if (cc) *cc = ccc; return 0; } // Convert double to string with specified digits of precision. // Shortest length format (f/e) will be used. // Output length is returned in optional argument cc. // (will never exceed 20 characters) int convDS(double dnum, int digits, ch *string, int *cc) // bugfix: use memmove not strcpy { ch *pstr; snprintf(string,20,"%.*g",digits,dnum); pstr = strstr(string,"e+"); // 1.23e+12 > 1.23e12 if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1); pstr = strstr(string,"e0"); // 1.23e02 > 1.23e2 if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1); pstr = strstr(string,"e0"); if (pstr) memmove(pstr+1,pstr+2,strlen(pstr+2)+1); pstr = strstr(string,"e-0"); // 1.23e-02 > 1.23e-2 if (pstr) memmove(pstr+2,pstr+3,strlen(pstr+3)+1); pstr = strstr(string,"e-0"); if (pstr) memmove(pstr+2,pstr+3,strlen(pstr+3)+1); if (cc) *cc = strlen(string); return 0; } // convert string to double, accepting either '.' or ',' decimal points. // if there is an error, zero is returned. double atofz(ch *string) { ch string2[20], *pp; strncpy(string2,string,20); string2[19] = 0; pp = strchr(string2,','); if (pp) *pp = '.'; return atof(string2); } // format a number as "123 B" or "12.3 KB" or "1.23 MB" etc. // prec is the desired digits of precision to output. // WARNING: only the last 100 conversions remain available in memory. // Example formats for 3 digits of precision: // 123 B, 999 B, 1.23 KB, 98.7 KB, 456 KB, 2.34 MB, 45.6 GB, 1.23 GB ch * formatKBMB(double fnum, int prec) { #define Bkilo 1024 #define Bmega (Bkilo*Bkilo) #define Bgiga (Bkilo*Bkilo*Bkilo) ch *units; static ch *output[100]; static int ftf = 1, ii; double gnum; if (ftf) { // keep last 100 conversions ftf = 0; for (ii = 0; ii < 100; ii++) output[ii] = (ch *) zmalloc(20,"formatKBMB"); } gnum = fabs(fnum); if (gnum > Bgiga) { fnum = fnum / Bgiga; units = "GB"; } else if (gnum > Bmega) { fnum = fnum / Bmega; units = "MB"; } else if (gnum > Bkilo) { fnum = fnum / Bkilo; units = "KB"; } else units = "B "; gnum = fabs(fnum); if (prec == 2 && gnum >= 99.5) prec++; // avoid e+nn formats if (prec == 3 && gnum >= 999.5) prec++; if (prec == 4 && gnum >= 9999.5) prec++; if (prec == 5 && gnum >= 99999.5) prec++; if (prec == 6 && gnum >= 999999.5) prec++; if (++ii > 99) ii = 0; snprintf(output[ii],20,"%.*g %s",prec,fnum,units); return output[ii]; } /******************************************************************************** Wildcard string match Match candidate string to wildcard string containing any number of '*' or '?' wildcard characters. '*' matches any number of characters, including zero characters. '?' matches any one character. Returns 0 if match, 1 if no match. <<---- WATCH OUT ! Benchmark: 0.032 usec. wild = *asdf*qwer?yxc 3.3 GHz Core i5 match = XXXasdfXXXXqwerXyxc *********************************************************************************/ int MatchWild(ch *pWild, ch *pString) { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (pWild[ii] != pString[ii]) { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /******************************************************************************** Wildcard string match - ignoring case Works like MatchWild() above, but case is ignored. ***/ int MatchWildCase(ch *pWild, ch *pString) { int ii, star; new_segment: star = 0; while (pWild[0] == '*') { star = 1; pWild++; } test_match: for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) { if (! strmatchcaseN(pWild+ii,pString+ii,1)) // the only difference { if (! pString[ii]) return 1; if (pWild[ii] == '?') continue; if (! star) return 1; pString++; goto test_match; } } if (pWild[ii] == '*') { pString += ii; pWild += ii; goto new_segment; } if (! pString[ii]) return 0; if (ii && pWild[ii-1] == '*') return 0; if (! star) return 1; pString++; goto test_match; } /******************************************************************************** SearchWild - wildcard file search Find all files with total /pathname/filename matching a pattern, which may have any number of the wildcard characters '*' and '?' in either or both the pathname and filename. ch * SearchWild(ch *wfilespec, int &flag) inputs: flag = 1 to start a new search flag = 2 abort a running search *** do not modify flag within a search *** wfilespec = filespec to search with optional wildcards e.g. "/name1/na*me2/nam??e3/name4*.ext?" return: a pointer to one matching file is returned per call, or null when there are no more matching files. The search may be aborted before completion, but make a final call with flag = 2 to clean up temp file. A new search with flag = 1 will also finish the cleanup. NOT THREAD SAFE - do not use in parallel threads '#' is used in place of '*' in comments below to prevent the compiler from interpreting /# and #/ as comment delimiters GNU find peculiarities: find /path/# omits "." files find /path/ includes "." files find /path/# recurses folders under /path/ find /path/#.txt does not recurse folders find /path/#/ finds all files under /path/ find /path/#/# finds files >= 1 folder level under /path/ find /path/xxx# never finds anything SearchWild uses simpler rules: '/' and '.' are treated like all other characters and match '#' and '?' no files are excluded except pure folders /path/#.txt finds all xxx.txt files under /path/ at all levels (because #.txt matches aaa.txt, /aaa/bbb.txt, etc.) Benchmark: search path: /usr/# file: #.html 2 strings: #per prop# find 97 files from 209K files in /usr/# first time: 4.6 sec. second time: 1.5 sec. computer: 3.6 GHz core i7 with SSD disk Do not use to search files in /proc/# (causes infinite loop). ***/ ch * SearchWild(ch *wpath, int &uflag) { static FILE *fid = 0; static ch buff[XFCC]; static ch wpath2[XFCC]; static ch command[XFCC]; ch *fcomm = "find \"%s\" -type f 2>/dev/null"; int cc, err; ch *pp, *pp1, *pp2; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) pclose(fid); // if file open, close it fid = 0; if (uflag == 2) return 0; // stop flag, done } if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > XFCC-20) zappcrash("SearchWild: wpath > XFCC"); repl_Nstrs(wpath,wpath2,XFCC,"\"","\\\"","$","\\$",0); // escape " and $ chars. in match pattern pp1 = strchr(wpath2,'*'); // find last wildcard in match pattern pp2 = strchr(wpath2,'?'); pp = 0; if (pp1) { pp = pp1; if (pp2 && pp2 < pp1) pp = pp2; } else if (pp2) pp = pp2; if (pp) *pp = 0; // terminate at first wildcard pp = strrchr(wpath2,'/'); // find last '/' in match pattern if (pp) pp[1] = 0; // terminate after last '/' pp = zescape_quotes(wpath2); snprintf(command,XFCC,fcomm,pp); // result is input to find command zfree(pp); fid = popen(command,"r"); // start find command, get matching files if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(buff,XFCC-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(pp); // get rid of trailing \n pp[cc-1] = 0; err = MatchWild(wpath,pp); // wildcard match? if (err) continue; // no return pp; // return file } } /******************************************************************************** SearchWildCase - wildcard file search - ignoring case Works like SearchWild() above, but case of file name is ignored. Actually, the trailing part of the path name is also case-insensitive, meaning that it is possible to get more matches than technically correct if folders like this are present: /AAA/BBB/.../filename /AAA/bbb/.../filename ***/ ch * SearchWildCase(ch *wpath, int &uflag) { static FILE *fid = 0; static ch buff[XFCC]; static ch wpath2[XFCC]; static ch command[XFCC]; ch *fcomm = "find \"%s\" -type f 2>/dev/null"; int cc, err; ch *pp, *pp1, *pp2; if ((uflag == 1) || (uflag == 2)) { // first call or stop flag if (fid) pclose(fid); // if file open, close it fid = 0; if (uflag == 2) return 0; // stop flag, done } if (uflag == 1) // first call flag { cc = strlen(wpath); if (cc == 0) return 0; if (cc > XFCC-20) zappcrash("SearchWild: wpath > XFCC"); repl_Nstrs(wpath,wpath2,XFCC,"\"","\\\"","$","\\$",0); // escape " and $ chars. in match pattern pp1 = strchr(wpath2,'*'); // find last wildcard in match pattern pp2 = strchr(wpath2,'?'); pp = 0; if (pp1) { pp = pp1; if (pp2 && pp2 < pp1) pp = pp2; } else if (pp2) pp = pp2; if (pp) *pp = 0; // terminate at first wildcard pp = strrchr(wpath2,'/'); // find last '/' in match pattern if (pp) pp[1] = 0; // terminate after last '/' pp = zescape_quotes(wpath2); snprintf(command,XFCC,fcomm,pp); // result is input to find command zfree(pp); fid = popen(command,"r"); // start find command, get matching files if (! fid) zappcrash(strerror(errno)); uflag = 763568954; // begin search } if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); while (true) { pp = fgets(buff,XFCC-2,fid); // next matching file if (! pp) { pclose(fid); // no more fid = 0; return 0; } cc = strlen(pp); // get rid of trailing \n pp[cc-1] = 0; err = MatchWildCase(wpath,pp); // wildcard match? (ignore case) if (err) continue; // no return pp; // return file } } /******************************************************************************** Find all files matching a given pattern (using glob() rules) int zfind(ch *pattern, ch **&flist, int &NF) pattern pattern to match, with wildcards flist list of files returned NF count of files returned Returns 0 if OK, +N if error (errno is set). flist and flist[*] are subjects for zfree(). zfind() works for files containing quotes (") dotfiles (/. and /..) are not included *********************************************************************************/ int zfind(ch *pattern, ch **&flist, int &NF) { ch **zfind_filelist = 0; // list of filespecs returned int globflags = GLOB_PERIOD; // include dotfiles int ii, jj, err, cc; glob_t globdata; ch *pp; globdata.gl_pathc = 0; // glob() setup globdata.gl_offs = 0; globdata.gl_pathc = 0; NF = 0; // empty output flist = 0; err = glob(pattern,globflags,null,&globdata); // find all matching files if (err) { if (err == GLOB_NOMATCH) err = 0; else if (err == GLOB_ABORTED) err = 1; else if (err == GLOB_NOSPACE) err = 2; else err = 3; if (err) Plog(0,"zfind() error: %d \n",err); globfree(&globdata); // free glob memory return err; } NF = globdata.gl_pathc; if (! NF) { globfree(&globdata); return 0; } cc = NF * sizeof(ch *); zfind_filelist = (ch **) zmalloc(cc,"zfind"); for (ii = jj = 0; ii < NF; ii++) { // loop found files pp = strrchr(globdata.gl_pathv[ii],'/'); if (! pp) continue; if (strmatch(pp,"/.")) continue; // skip dotfiles if (strmatch(pp,"/..")) continue; zfind_filelist[jj++] = zstrdup(globdata.gl_pathv[ii],"zfind"); // add file to output list } flist = zfind_filelist; // return file list and count NF = jj; globfree(&globdata); // free glob memory return 0; } /********************************************************************************/ // perform a binary search on sorted list of integers // return matching element or -1 if not found // Benchmark: search a list of 10 million sorted integers // 0.35 usecs. 3.3 GHz Core i5 int bsearch(int seekint, int nn, int list[]) { int ii, jj, kk, rkk; ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (true) { kk = list[ii] - seekint; // check element if (kk > 0) { ii -= jj; // too high, go down if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up if (ii > nn) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of records in memory. // Return matching record number (0 based) or -1 if not found. // Benchmark: search 10 million sorted records of 20 chars. // 0.61 usecs. 3.3 GHz Core i5 int bsearch(ch *seekrec, ch *allrecs, int recl, int nrecs) { int ii, jj, kk, rkk; ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strcmp(allrecs+ii*recl,seekrec); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } // Perform a binary search on sorted set of pointers to records in memory. // Return matching record number (0 based) or -1 if not found. // The pointers are sorted in the order of the records starting at ch N. // The records need not be sorted. // The string length of seekrec is compared. int bsearch(ch *seekrec, ch **allrecs, int N, int nrecs) { int ii, jj, kk, rkk; ii = nrecs / 2; // next element to search jj = (ii + 1) / 2; // next increment nrecs--; // last element rkk = 0; while (true) { kk = strcmp(allrecs[ii]+N,seekrec); // compare member rec to seek rec if (kk > 0) { ii -= jj; // too high, go down in set if (ii < 0) return -1; } else if (kk < 0) { ii += jj; // too low, go up in set if (ii > nrecs) return -1; } else if (kk == 0) return ii; // matched jj = jj / 2; // reduce increment if (jj == 0) { jj = 1; // step by 1 element if (! rkk) rkk = kk; // save direction else { if (rkk > 0) { if (kk < 0) return -1; } // if change direction, fail else if (kk > 0) return -1; } } } } /******************************************************************************** heap sort functions void HeapSort(int list[], int nn) void HeapSort(float flist[], int nn) void HeapSort(double dlist[], int nn) Sort list of nn integers, floats, or doubles. Numbers are sorted in ascending order. Uses 4 parallel threads for lists > 1000. void HeapSort(ch *plist[], int nn) Pointers are sorted in order of the strings they point to. The strings are not changed. void HeapSort(ch *plist1[], ch *plist2[], int nn) Sort two lists of pointers to two sets of strings. Both lists are sorted in order of the first set of strings. void HeapSort(ch *plist[], int nn, compfunc) Sort list of pointers to strings in user-defined order. Pointers are sorted, strings are not changed. Uses 4 parallel threads for lists > 1000. void HeapSort(ch *recs, int RL, int NR, compfunc) Sort an array of records in memory using a caller-supplied compare function. recs pointer to 1st record in array RL record length NR no. of records int compfunc(ch *rec1, ch *rec2) compare rec1 to rec2, return -1 0 +1 if rec1 < = > rec2 in sort order. Benchmarks (4 GHz Core i5) 100 million integers: 3.9 secs 100 million floats: 5.2 secs 100 million doubles: 6.1 secs 52 million pointers to 20 character strings: 18.2 secs 20 million records of 100 characters: 20.2 secs *********************************************************************************/ #define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp) // ------------------------------------------------------------------------------ // heapsort for array of integers void adjust(int vv[], int n1, int n2) { int *bb, jj, kk, temp; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void heapsort(int vv[], int nn) // single thread version { int *bb, jj, temp; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } namespace HeapSort_int { int nn1, nn2, nn3, nn4; int *vv1, *vv2, *vv3, *vv4; pthread_t tid1, tid2, tid3, tid4; int tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4; } void HeapSort(int vv[], int nn) // 4-thread version { using namespace HeapSort_int; void * HeapSort_int_thread(void *arg); int *vv9, *next; if (nn < 1000 || get_nprocs() < 2) { // small list or <2 SMPs heapsort(vv,nn); // use one thread return; } nn1 = nn2 = nn3 = nn / 4; // 1st/2nd/3rd sub-list counts nn4 = nn - nn1 - nn2 - nn3; // 4th sub-list count vv1 = vv; // 4 sub-list start positions vv2 = vv1 + nn1; vv3 = vv2 + nn2; vv4 = vv3 + nn3; tid1 = start_Jthread(HeapSort_int_thread,&tt1); // sort the 4 sub-lists, parallel tid2 = start_Jthread(HeapSort_int_thread,&tt2); tid3 = start_Jthread(HeapSort_int_thread,&tt3); tid4 = start_Jthread(HeapSort_int_thread,&tt4); wait_Jthread(tid1); // wait for 4 thread completions wait_Jthread(tid2); wait_Jthread(tid3); wait_Jthread(tid4); vv9 = (int *) malloc(nn * sizeof(int)); // merge list, output list while (true) { next = 0; if (vv1) next = vv1; if (! next && vv2) next = vv2; if (! next && vv3) next = vv3; if (! next && vv4) next = vv4; if (! next) break; if (vv2 && *vv2 < *next) next = vv2; if (vv3 && *vv3 < *next) next = vv3; if (vv4 && *vv4 < *next) next = vv4; if (next == vv1) { vv1++; nn1--; if (! nn1) vv1 = 0; } else if (next == vv2) { vv2++; nn2--; if (! nn2) vv2 = 0; } else if (next == vv3) { vv3++; nn3--; if (! nn3) vv3 = 0; } else { vv4++; nn4--; if (! nn4) vv4 = 0; } *vv9 = *next; vv9++; } vv9 -= nn; memcpy(vv,vv9,nn * sizeof(int)); // copy output list to input list free(vv9); // free output list return; } void * HeapSort_int_thread(void *arg) // thread function { using namespace HeapSort_int; int tt = *((int *) arg); if (tt == 1) heapsort(vv1,nn1); if (tt == 2) heapsort(vv2,nn2); if (tt == 3) heapsort(vv3,nn3); if (tt == 4) heapsort(vv4,nn4); return 0; } // ------------------------------------------------------------------------------ // heapsort for array of floats void adjust(float vv[], int n1, int n2) { float *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void heapsort(float vv[], int nn) // single thread version { float *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } namespace HeapSort_float { int nn1, nn2, nn3, nn4; float *vv1, *vv2, *vv3, *vv4; pthread_t tid1, tid2, tid3, tid4; int tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4; } void HeapSort(float vv[], int nn) // 4-thread version { using namespace HeapSort_float; void * HeapSort_float_thread(void *arg); float *vv9, *next; if (nn < 1000 || get_nprocs() < 2) { // small list or <2 SMPs heapsort(vv,nn); // use one thread return; } nn1 = nn2 = nn3 = nn / 4; // 1st/2nd/3rd sub-list counts nn4 = nn - nn1 - nn2 - nn3; // 4th sub-list count vv1 = vv; // 4 sub-list start positions vv2 = vv1 + nn1; vv3 = vv2 + nn2; vv4 = vv3 + nn3; tid1 = start_Jthread(HeapSort_float_thread,&tt1); // sort the 4 sub-lists, parallel tid2 = start_Jthread(HeapSort_float_thread,&tt2); tid3 = start_Jthread(HeapSort_float_thread,&tt3); tid4 = start_Jthread(HeapSort_float_thread,&tt4); wait_Jthread(tid1); // wait for 4 thread completions wait_Jthread(tid2); wait_Jthread(tid3); wait_Jthread(tid4); vv9 = (float *) malloc(nn * sizeof(float)); // merge list, output list while (true) { next = 0; if (vv1) next = vv1; if (! next && vv2) next = vv2; if (! next && vv3) next = vv3; if (! next && vv4) next = vv4; if (! next) break; if (vv2 && *vv2 < *next) next = vv2; if (vv3 && *vv3 < *next) next = vv3; if (vv4 && *vv4 < *next) next = vv4; if (next == vv1) { vv1++; nn1--; if (! nn1) vv1 = 0; } else if (next == vv2) { vv2++; nn2--; if (! nn2) vv2 = 0; } else if (next == vv3) { vv3++; nn3--; if (! nn3) vv3 = 0; } else { vv4++; nn4--; if (! nn4) vv4 = 0; } *vv9 = *next; vv9++; } vv9 -= nn; memcpy(vv,vv9,nn * sizeof(float)); // copy output list to input list free(vv9); // free output list return; } void * HeapSort_float_thread(void *arg) // thread function { using namespace HeapSort_float; int tt = *((int *) arg); if (tt == 1) heapsort(vv1,nn1); if (tt == 2) heapsort(vv2,nn2); if (tt == 3) heapsort(vv3,nn3); if (tt == 4) heapsort(vv4,nn4); return 0; } // ------------------------------------------------------------------------------ // heapsort for array of doubles void adjust(double vv[], int n1, int n2) { double *bb, temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && bb[kk] < bb[kk+1]) kk++; if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void heapsort(double vv[], int nn) // single thread version { double *bb, temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv - 1; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(vv,1,jj); } } namespace HeapSort_double { int nn1, nn2, nn3, nn4; double *vv1, *vv2, *vv3, *vv4; pthread_t tid1, tid2, tid3, tid4; int tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4; } void HeapSort(double vv[], int nn) // 4-thread version { using namespace HeapSort_double; void * HeapSort_double_thread(void *arg); double *vv9, *next; if (nn < 1000 || get_nprocs() < 2) { // small list or <2 SMPs heapsort(vv,nn); // use one thread return; } nn1 = nn2 = nn3 = nn / 4; // 1st/2nd/3rd sub-list counts nn4 = nn - nn1 - nn2 - nn3; // 4th sub-list count vv1 = vv; // 4 sub-list start positions vv2 = vv1 + nn1; vv3 = vv2 + nn2; vv4 = vv3 + nn3; tid1 = start_Jthread(HeapSort_double_thread,&tt1); // sort the 4 sub-lists, parallel tid2 = start_Jthread(HeapSort_double_thread,&tt2); tid3 = start_Jthread(HeapSort_double_thread,&tt3); tid4 = start_Jthread(HeapSort_double_thread,&tt4); wait_Jthread(tid1); // wait for 4 thread completions wait_Jthread(tid2); wait_Jthread(tid3); wait_Jthread(tid4); vv9 = (double *) malloc(nn * sizeof(double)); // merge list, output list while (true) { next = 0; if (vv1) next = vv1; if (! next && vv2) next = vv2; if (! next && vv3) next = vv3; if (! next && vv4) next = vv4; if (! next) break; if (vv2 && *vv2 < *next) next = vv2; if (vv3 && *vv3 < *next) next = vv3; if (vv4 && *vv4 < *next) next = vv4; if (next == vv1) { vv1++; nn1--; if (! nn1) vv1 = 0; } else if (next == vv2) { vv2++; nn2--; if (! nn2) vv2 = 0; } else if (next == vv3) { vv3++; nn3--; if (! nn3) vv3 = 0; } else { vv4++; nn4--; if (! nn4) vv4 = 0; } *vv9 = *next; vv9++; } vv9 -= nn; memcpy(vv,vv9,nn * sizeof(double)); // copy output list to input list free(vv9); // free output list return; } void * HeapSort_double_thread(void *arg) // thread function { using namespace HeapSort_double; int tt = *((int *) arg); if (tt == 1) heapsort(vv1,nn1); if (tt == 2) heapsort(vv2,nn2); if (tt == 3) heapsort(vv3,nn3); if (tt == 4) heapsort(vv4,nn4); return 0; } // ------------------------------------------------------------------------------ // heapsort array of pointers to strings in ascending order of strings // pointers are sorted, strings are not changed. void HeapSort(ch *vv[], int nn) { HeapSort(vv,nn, (HeapSortUcomp *) strcmp); return; } // ------------------------------------------------------------------------------ // Heapsort 2 lists of pointers to 2 parallel sets of strings // in ascending order of the first set of strings. // Both lists of pointers are sorted together in tandem. // Pointers are sorted, strings are not changed. void adjust(ch *vv1[], ch *vv2[], int n1, int n2) { ch **bb1, **bb2, *temp; int jj, kk; bb1 = vv1 - 1; bb2 = vv2 - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && strcmp(bb1[kk],bb1[kk+1]) < 0) kk++; if (strcmp(bb1[jj],bb1[kk]) < 0) { SWAP(bb1[jj],bb1[kk]); SWAP(bb2[jj],bb2[kk]); } jj = kk; kk *= 2; } } void HeapSort(ch *vv1[], ch *vv2[], int nn) { ch **bb1, **bb2, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv1,vv2,jj,nn); bb1 = vv1; bb2 = vv2; for (jj = nn-1; jj > 0; jj--) { SWAP(bb1[0], bb1[jj]); SWAP(bb2[0], bb2[jj]); adjust(vv1,vv2,1,jj); } } // ------------------------------------------------------------------------------ // heapsort array of pointers to strings in user-defined order. // pointers are sorted, strings are not changed. void adjust(ch *vv[], int n1, int n2, HeapSortUcomp fcomp) { ch **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++; if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void heapsort(ch *vv[], int nn, HeapSortUcomp fcomp) // single thread version { ch **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj,fcomp); } } namespace HeapSort_string { int nn1, nn2, nn3, nn4; ch **vv1, **vv2, **vv3, **vv4; pthread_t tid1, tid2, tid3, tid4; int tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4; HeapSortUcomp *ttfcomp; } void HeapSort(ch *vv[], int nn, HeapSortUcomp fcomp) // 4-thread version { using namespace HeapSort_string; void * HeapSort_string_thread(void *arg); ch **vv9, **next; if (nn < 1000 || get_nprocs() < 2) { // small list or <2 SMPs heapsort(vv,nn,fcomp); // use one thread return; } nn1 = nn2 = nn3 = nn / 4; // 1st/2nd/3rd sub-list counts nn4 = nn - nn1 - nn2 - nn3; // 4th sub-list count vv1 = vv; // 4 sub-list start positions vv2 = vv1 + nn1; vv3 = vv2 + nn2; vv4 = vv3 + nn3; ttfcomp = fcomp; tid1 = start_Jthread(HeapSort_string_thread,&tt1); // sort the 4 sub-lists, parallel tid2 = start_Jthread(HeapSort_string_thread,&tt2); tid3 = start_Jthread(HeapSort_string_thread,&tt3); tid4 = start_Jthread(HeapSort_string_thread,&tt4); wait_Jthread(tid1); // wait for 4 thread completions wait_Jthread(tid2); wait_Jthread(tid3); wait_Jthread(tid4); vv9 = (ch **) malloc(nn * sizeof(ch *)); // merge list, output list while (true) { next = 0; if (vv1) next = vv1; if (! next && vv2) next = vv2; if (! next && vv3) next = vv3; if (! next && vv4) next = vv4; if (! next) break; if (vv2 && ttfcomp(*vv2,*next) < 0) next = vv2; if (vv3 && ttfcomp(*vv3,*next) < 0) next = vv3; if (vv4 && ttfcomp(*vv4,*next) < 0) next = vv4; if (next == vv1) { vv1++; nn1--; if (! nn1) vv1 = 0; } else if (next == vv2) { vv2++; nn2--; if (! nn2) vv2 = 0; } else if (next == vv3) { vv3++; nn3--; if (! nn3) vv3 = 0; } else { vv4++; nn4--; if (! nn4) vv4 = 0; } *vv9 = *next; vv9++; } vv9 -= nn; memcpy(vv,vv9,nn * sizeof(ch *)); // copy output list to input list free(vv9); // free output list return; } void * HeapSort_string_thread(void *arg) // thread function { using namespace HeapSort_string; int tt = *((int *) arg); if (tt == 1) heapsort(vv1,nn1,ttfcomp); if (tt == 2) heapsort(vv2,nn2,ttfcomp); if (tt == 3) heapsort(vv3,nn3,ttfcomp); if (tt == 4) heapsort(vv4,nn4,ttfcomp); return 0; } // ------------------------------------------------------------------------------ // heapsort for array of records, // using caller-supplied record compare function. // HeapSortUcomp returns [ -1 0 +1 ] for rec1 [ < = > ] rec2 // method: build array of pointers and sort these, then // use this sorted array to re-order the records at the end. void adjust(ch *recs, int RL, int n1, int n2, int *vv1, HeapSortUcomp fcomp) { int *bb, jj, kk, temp; ch *rec1, *rec2; bb = vv1 - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { rec1 = recs + RL * bb[kk]; rec2 = recs + RL * bb[kk+1]; if (kk < n2 && fcomp(rec1,rec2) < 0) kk++; rec1 = recs + RL * bb[jj]; rec2 = recs + RL * bb[kk]; if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(ch *recs, int RL, int NR, HeapSortUcomp fcomp) { int *bb, jj, kk, temp, flag; int *vv1, *vv2; // make reentrant ch *vvrec; vv1 = (int *) malloc((NR+1) * sizeof(int)); for (jj = 0; jj < NR; jj++) vv1[jj] = jj; for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,vv1,fcomp); bb = vv1 - 1; for (jj = NR-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(recs,RL,1,jj,vv1,fcomp); } vv2 = (int *) malloc((NR+1) * sizeof(int)); for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj; vvrec = (ch *) malloc(RL); flag = 1; while (flag) { flag = 0; for (jj = 0; jj < NR; jj++) { kk = vv2[jj]; if (kk == jj) continue; memmove(vvrec,recs+jj*RL,RL); memmove(recs+jj*RL,recs+kk*RL,RL); memmove(recs+kk*RL,vvrec,RL); SWAP(vv2[jj],vv2[kk]); flag = 1; } } free(vv1); free(vv2); free(vvrec); } /******************************************************************************** int MemSort (ch *RECS, int RL, int NR, int KEYS[][3], int NK) RECS is an array of records, to be sorted in-place. (record length = RL, record count = NR) KEYS[NK][3] is an integer array defined as follows: [N][0] starting position of Nth key field in RECS [N][1] length of Nth key field in RECS [N][2] type of sort for Nth key: 1 = ch ascending 2 = ch descending 3 = int*4 ascending 4 = int*4 descending 5 = float*4 ascending 6 = float*4 descending 7 = float*8 ascending (double) 8 = float*8 descending Benchmark: 10 million recs of 80 bytes with 4 sort keys: 10.8 secs (4 GHz Core i5). ***/ int MemSortComp(ch *rec1, ch *rec2); int MemSortKeys[10][3], MemSortNK; int MemSort(ch *RECS, int RL, int NR, int KEYS[][3], int NK) { int ii; if (NR < 2) return 1; if (NK > 10) zappcrash("MemSort, bad NK"); if (NK < 1) zappcrash("MemSort, bad NK"); MemSortNK = NK; for (ii = 0; ii < NK; ii++) { MemSortKeys[ii][0] = KEYS[ii][0]; MemSortKeys[ii][1] = KEYS[ii][1]; MemSortKeys[ii][2] = KEYS[ii][2]; } HeapSort(RECS,RL,NR,MemSortComp); return 1; } int MemSortComp(ch *rec1, ch *rec2) { int ii, stat, kpos, ktype, kleng; int inum1, inum2; float rnum1, rnum2; double dnum1, dnum2; ch *p1, *p2; for (ii = 0; ii < MemSortNK; ii++) // loop each key { kpos = MemSortKeys[ii][0]; // relative position kleng = MemSortKeys[ii][1]; // length ktype = MemSortKeys[ii][2]; // type p1 = rec1 + kpos; // absolute position p2 = rec2 + kpos; switch (ktype) { case 1: // ch ascending stat = strncmp(p1,p2,kleng); // compare 2 key values if (stat) return stat; // + if rec1 > rec2, - if < break; // 2 keys are equal, check next key case 2: // ch descending stat = strncmp(p1,p2,kleng); if (stat) return -stat; break; case 3: // int ascending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return 1; if (inum1 < inum2) return -1; break; case 4: // int descending memmove(&inum1,p1,4); memmove(&inum2,p2,4); if (inum1 > inum2) return -1; if (inum1 < inum2) return 1; break; case 5: // float ascending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return 1; if (rnum1 < rnum2) return -1; break; case 6: // float descending memmove(&rnum1,p1,4); memmove(&rnum2,p2,4); if (rnum1 > rnum2) return -1; if (rnum1 < rnum2) return 1; break; case 7: // double ascending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return 1; if (dnum1 < dnum2) return -1; break; case 8: // double descending memmove(&dnum1,p1,8); memmove(&dnum2,p2,8); if (dnum1 > dnum2) return -1; if (dnum1 < dnum2) return 1; break; default: // key type not 1-8 zappcrash("MemSort, bad KEYS sort type"); } } return 0; // records match on all keys } /********************************************************************************/ // test if an integer value matches any in a list of values // returns the matching value or zero if nothing matches // list of values must end with zero // zero cannot be one of the values to match int zmember(int testval, int matchval1, ...) { va_list arglist; int matchval; va_start(arglist,matchval1); matchval = matchval1; while (matchval) { if (testval == matchval) break; matchval = va_arg(arglist,int); if (! matchval || matchval == (int) 0x100000000); // ARM bug } va_end(arglist); return matchval; } /******************************************************************************** Hash Table class HashTab(int cc, int cap); constructor ~HashTab(); destructor int Add(ch *string); add a new string int Del(ch *string); delete a string int Find(ch *string); find a string int GetCount() { return count; } get string count int GetNext(int &first, ch *string); get first/next string int Dump(); dump hash table to std. output constructor: cc = string length of table entries, cap = table capacity cap should be set 30% higher than needed to reduce collisions and improve speed Benchmark: 0.056 usec. to find 19 ch string in a table of 100,000 which is 80% full. 3.3 GHz Core i5 *********************************************************************************/ // static members (robust for tables up to 60% full) int HashTab::tries1 = 100; // Add() tries int HashTab::tries2 = 200; // Find() tries HashTab::HashTab(int _cc, int _cap) // constructor { cc = 4 * (_cc + 4) / 4; // + 1 + mod 4 length cap = _cap; int len = cc * cap; table = new ch [len]; if (! table) zappcrash("HashTab() new %d fail",len,null); memset(table,0,len); } HashTab::~HashTab() // destructor { delete [] table; table = 0; } // Add a new string to table int HashTab::Add(ch *string) { int pos, fpos, tries; pos = strHash(string,cap); // get random position pos = pos * cc; for (tries = 0, fpos = -1; tries < tries1; tries++, pos += cc) // find next free slot at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) // empty slot: string not found { if (fpos != -1) pos = fpos; // use prior deleted slot if there strncpy(table+pos,string,cc); // insert new string table[pos+cc-1] = 0; // insure null terminator return (pos/cc); // return rel. table entry } if (table[pos] == -1) // deleted slot { if (fpos == -1) fpos = pos; // remember 1st one found continue; } if (strmatch(string,table+pos)) return -2; // string already present } return -3; // table full (tries1 exceeded) } // Delete a string from table int HashTab::Del(ch *string) { int pos, tries; pos = strHash(string,cap); // get random position pos = pos * cc; for (tries = 0; tries < tries2; tries++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strmatch(string,table+pos)) // string found { table[pos] = -1; // delete table entry return (pos/cc); // return rel. table entry } } zappcrash("HashTab::Del() fail",null); // exceed tries2, must not happen return 0; // (table too full to function) } // Find a table entry. int HashTab::Find(ch *string) { int pos, tries; pos = strHash(string,cap); // get random position pos = pos * cc; for (tries = 0; tries < tries2; tries++, pos += cc) // search for string at/after position { if (pos >= cap * cc) pos = 0; // last position wraps to 1st if (! table[pos]) return -1; // empty slot, string not found if (strmatch(string,table+pos)) return (pos/cc); // string found, return rel. entry } zappcrash("HashTab::Find() fail",null); // cannot happen return 0; } // return first or next table entry int HashTab::GetNext(int &ftf, ch *string) { static int pos; if (ftf) // initial call { pos = 0; ftf = 0; } while (pos < (cap * cc)) { if ((table[pos] == 0) || (table[pos] == -1)) // empty or deleted slot { pos += cc; continue; } strcpy(string,table+pos); // return string pos += cc; return 1; } return -4; // EOF } int HashTab::Dump() { int ii, pos; for (ii = 0; ii < cap; ii++) { pos = ii * cc; if (table[pos] && table[pos] != -1) Plog(0,"%d, %s \n", ii, table + pos); if (table[pos] == -1) Plog(0,"%d, deleted \n", pos); } return 1; } /******************************************************************************** zlist - list processing functions typedef struct { list data type int count; count of member strings ch **mber; member strings, null == no members } zlist_t; zlist_t *zlist; zlist = zlist_new(N) make new zlist with N null members void zlist_free(zlist) delete zlist, free memory void zlist_dump(zlist) dump zlist to stdout N = zlist_count(zlist) get member count (including null entries) string = zlist_get(zlist,Nth) get Nth member void zlist_put(zlist,string,Nth) put Nth member (replace) void zlist_insert(zlist,string,Nth) insert member (count += 1) void zlist_remove(zlist,Nth) remove member (count -= 1) void zlist_clear(zlist_t *zlist, int Nth); clear zlist from Nth member to end void zlist_purge(zlist); purge zlist of null members err = zlist_add(zlist,string,Funiq) add member at first null or append (if unique) err = zlist_append(zlist,string,Funiq) append new member (if unique) err = zlist_prepend(zlist,string,Funiq) prepend new member (if unique) Nth = zlist_find(zlist,string,posn); find next matching zlist member at/after posn Nth = zlist_findwild(zlist,wstring,posn); same as above, but wildcard string match zlist2 = zlist_copy(zlist1) copy zlist zlist3 = zlist_insert(zlist1,zlist2,Nth) insert zlist2 into zlist1 at Nth posn zlist3 = zlist_remove(zlist1,zlist2) remove all members of zlist2 from zlist1 void zlist_sort(zlist) sort zlist ascending void zlist_sort(zlist,ccfunc) sort zlist using caller compare function err = zlist_to_file(zlist,filename) make file from zlist zlist = zlist_from_file(filename) make zlist from file zlist = zlist_from_folder(foldername) make zlist from file names in folder *********************************************************************************/ // create zlist with 'count' empty members zlist_t * zlist_new(int count) { zlist_t *zlist = (zlist_t *) zmalloc(sizeof(zlist_t),"zlist"); zlist->count = count; if (count > 0) zlist->mber = (ch **) zmalloc(count * sizeof(ch *),"zlist"); for (int ii = 0; ii < count; ii++) zlist->mber[ii] = null; return zlist; } // delete a zlist void zlist_free(zlist_t *zlist) { for (int ii = 0; ii < zlist->count; ii++) if (zlist->mber[ii]) zfree(zlist->mber[ii]); if (zlist->mber) zfree(zlist->mber); zlist->count = 0; zfree(zlist); return; } // dump zlist to stdout void zlist_dump(zlist_t *zlist) { Plog(0,"count: %d \n",zlist->count); for (int ii = 0; ii < zlist->count; ii++) Plog(0,"%5d %s \n",ii,zlist->mber[ii]); Plog(0,"\n"); return; } // get zlist member count int zlist_count(zlist_t *zlist) { return zlist->count; } // get a zlist member ch * zlist_get(zlist_t *zlist, int Nth) { if (Nth < 0 || Nth >= zlist->count) zappcrash("zlist_get() invalid Nth: %d",Nth); return zlist->mber[Nth]; } // put a zlist member (replace existing) (null allowed) void zlist_put(zlist_t *zlist, ch *string, int Nth) { if (Nth < 0 || Nth >= zlist->count) zappcrash("zlist_put() invalid Nth: %d",Nth); if (zlist->mber[Nth]) zfree(zlist->mber[Nth]); if (string) zlist->mber[Nth] = zstrdup(string,"zlist"); else zlist->mber[Nth] = 0; return; } // insert new zlist member (count increases) // new member is Nth member, old Nth member is Nth+1 // if Nth > last + 1, null members are added in-between void zlist_insert(zlist_t *zlist, ch *string, int Nth) { int count, newcount; int ii1, ii2, cc; ch **newmber; if (Nth < 0) zappcrash("zlist_insert() invalid Nth: %d",Nth); count = zlist->count; if (Nth < count) newcount = count + 1; else newcount = Nth + 1; newmber = (ch **) zmalloc(newcount * sizeof(ch *),"zlist"); if (Nth > 0) { // copy 0 - Nth-1 ii1 = 0; ii2 = Nth; if (Nth > count) ii2 = count; cc = (ii2 - ii1) * sizeof(ch *); memcpy(newmber,zlist->mber,cc); } newmber[Nth] = zstrdup(string,"zlist"); // insert Nth if (Nth < count) { // copy Nth - last ii1 = Nth; ii2 = count; cc = (ii2 - ii1) * sizeof(ch *); memcpy(newmber+ii1+1,zlist->mber+ii1,cc); } if (zlist->mber) zfree(zlist->mber); zlist->mber = newmber; zlist->count = newcount; return; } // remove a zlist member (count -= 1) void zlist_remove(zlist_t *zlist, int Nth) { int newcount, cc; ch **newmber; if (Nth < 0 || Nth >= zlist->count) zappcrash("zlist_remove() invalid Nth: %d",Nth); newcount = zlist->count - 1; if (newcount) newmber = (ch **) zmalloc(newcount * sizeof(ch *),"zlist"); else newmber = 0; if (Nth > 0) { // copy 0 - Nth-1 cc = Nth * sizeof(ch *); memcpy(newmber,zlist->mber,cc); } if (zlist->mber[Nth]) zfree(zlist->mber[Nth]); // remove Nth if (Nth < newcount) { // copy Nth+1 - last cc = (newcount - Nth) * sizeof(ch *); memcpy(newmber+Nth,zlist->mber+Nth+1,cc); } zfree(zlist->mber); zlist->mber = newmber; zlist->count = newcount; return; } // clear zlist members from Nth to end void zlist_clear(zlist_t *zlist, int Nth) { int ii; ch **mber = 0; if (Nth >= zlist_count(zlist)) return; if (Nth > 0) mber = (ch **) zmalloc(Nth * sizeof(ch *),"zlist"); // remaining members for (ii = 0; ii < Nth; ii++) // copy remaining members mber[ii] = zlist->mber[ii]; for (ii = Nth; ii < zlist_count(zlist); ii++) // free deleted members zfree(zlist->mber[ii]); zfree(zlist->mber); zlist->mber = mber; // null if empty list zlist->count = Nth; return; } // purge zlist of all null members and reset count void zlist_purge(zlist_t *zlist) { int ii, jj; ch **mber; for (ii = jj = 0; ii < zlist->count; ii++) if (zlist->mber[ii]) jj++; if (jj) mber = (ch **) zmalloc(jj * sizeof(ch *),"zlist"); else mber = 0; for (ii = jj = 0; ii < zlist->count; ii++) { if (zlist->mber[ii]) { mber[jj] = zlist->mber[ii]; jj++; } } zlist->count = jj; zfree(zlist->mber); zlist->mber = mber; return; } // add new member at first null position, or append (if unique) // return 0 if OK, 1 if not unique int zlist_add(zlist_t *zlist, ch *string, int Funiq) { int ii; if (Funiq && zlist_find(zlist,string,0) >= 0) return 1; for (ii = 0; ii < zlist->count; ii++) if (! zlist->mber[ii]) break; if (ii < zlist->count) { zlist->mber[ii] = zstrdup(string,"zlist"); return 0; } return zlist_append(zlist,string,Funiq); } // append new member at end (if unique) // return 0 if OK, 1 if not unique int zlist_append(zlist_t *zlist, ch *string, int Funiq) { if (Funiq && zlist_find(zlist,string,0) >= 0) return 1; zlist_insert(zlist,string,zlist->count); return 0; } // prepend new member at posn 0 (if unique) // return 0 if OK, 1 if not unique int zlist_prepend(zlist_t *zlist, ch *string, int Funiq) { if (Funiq && zlist_find(zlist,string,0) >= 0) return 1; zlist_insert(zlist,string,0); return 0; } // find next matching zlist member at/from given posn int zlist_find(zlist_t *zlist, ch *string, int posn) { if (posn < 0 || posn >= zlist->count) return -1; for (int ii = posn; ii < zlist->count; ii++) { if (zlist->mber[ii]) if (strmatch(string,zlist->mber[ii])) return ii; } return -1; } // find next matching zlist member at/from given posn (wildcard match) int zlist_findwild(zlist_t *zlist, ch *wstring, int posn) { if (posn < 0 || posn >= zlist->count) return -1; for (int ii = posn; ii < zlist->count; ii++) { if (zlist->mber[ii]) if (MatchWild(wstring,zlist->mber[ii]) == 0) return ii; } return -1; } // copy a zlist zlist_t * zlist_copy(zlist_t *zlist1) { zlist_t *zlist2 = zlist_new(zlist1->count); for (int ii = 0; ii < zlist2->count; ii++) if (zlist1->mber[ii]) zlist2->mber[ii] = zstrdup(zlist1->mber[ii],"zlist"); return zlist2; } // insert zlist2 into zlist1 at Nth position // use Nth = -1 to insert at the end (append) zlist_t * zlist_insert_zlist(zlist_t *zlist1, zlist_t *zlist2, int Nth) { int ii; int nn1 = zlist1->count; // zlist to receive int nn2 = zlist2->count; // zlist to insert int nn3 = nn1 + nn2; // output zlist if (Nth < 0) Nth = nn1; // append to end of zlist1 if (Nth > nn1) nn3 = Nth + nn2; // append with missing members in-between zlist_t *zlist3 = zlist_new(nn3); for (ii = 0; ii < Nth; ii++) // 0 to Nth-1 if (ii < nn1 && zlist1->mber[ii]) zlist3->mber[ii] = zstrdup(zlist1->mber[ii],"zlist"); for (ii = Nth; ii < Nth + nn2; ii++) // Nth to Nth + nn2-1 if (zlist2->mber[ii-Nth]) zlist3->mber[ii] = zstrdup(zlist2->mber[ii-Nth],"zlist"); for (ii = Nth + nn2; ii < nn3; ii++) // Nth + nn2 to nn3-1 if (ii-nn2 < nn1 && zlist1->mber[ii-nn2]) zlist3->mber[ii] = zstrdup(zlist1->mber[ii-nn2],"zlist"); return zlist3; } // remove all members of zlist2 from zlist1 zlist_t * zlist_remove(zlist_t *zlist1, zlist_t *zlist2) { int ii, jj; int nn2 = zlist2->count; zlist_t *zlist3 = zlist_copy(zlist1); // copy input zlist for (ii = 0; ii < nn2; ii++) { jj = zlist_find(zlist3,zlist_get(zlist2,ii),0); // find zlist2 member in zlist3 if (jj >= 0) zlist_put(zlist3,null,jj); // if found, replace with null } zlist_purge(zlist3); // purge null entries return zlist3; } // sort zlist ascending void zlist_sort(zlist_t *zlist) { HeapSort(zlist->mber,zlist->count); return; } // sort zlist via caller compare function void zlist_sort(zlist_t *zlist, int ccfunc(ch *, ch *)) { HeapSort(zlist->mber,zlist->count,ccfunc); return; } // make file from zlist int zlist_to_file(zlist_t *zlist, ch *filename) { int ii, err; FILE *fid = fopen(filename,"w"); if (! fid) return errno; for (ii = 0; ii < zlist->count; ii++) if (zlist->mber[ii]) fprintf(fid,"%s\n",zlist->mber[ii]); err = fclose(fid); if (err) return errno; else return 0; } // make zlist from file // performance with SSD: over 100 MB/sec. zlist_t * zlist_from_file(ch *filename) { FILE *fid; zlist_t *zlist; int ii, count = 0; ch *pp, buff[XFCC]; fid = fopen(filename,"r"); // count recs in file if (! fid) return 0; // this adds 40% to elapsed time while (true) { pp = fgets(buff,XFCC,fid); if (! pp) break; count++; } fclose(fid); fid = fopen(filename,"r"); if (! fid) return 0; zlist = zlist_new(count); // create zlist for (ii = 0; ii < count; ii++) { pp = fgets_trim(buff,XFCC,fid); if (! pp) break; zlist->mber[ii] = zstrdup(buff,"zlist"); } fclose(fid); return zlist; } // create a zlist from all file names in a folder // list members are file names without folder zlist_t * zlist_from_folder(ch *folder) // 25.1 { ch findcomm[1000]; ch *pp, **flist; int ii, NF; zlist_t *zlist; snprintf(findcomm,1000,"%s/*",folder); // find all files in folder zfind(findcomm,flist,NF); if (! NF) return 0; zlist = zlist_new(NF); for (ii = 0; ii < NF; ii++) { // make zlist of file root names pp = strrchr(flist[ii],'/'); if (! pp) pp = flist[ii]; else pp++; zlist_put(zlist,pp,ii); zfree(flist[ii]); } zfree(flist); zlist_sort(zlist); // sort zlist return zlist; } /********************************************************************************/ // Random number generators with explicit context // and improved randomness over a small series. // Benchmark: lrandz 0.012 usec drandz 0.014 usec 3.3 GHz Core i5 // (srand() % range) is much slower. int lrandz(int64 *seed) // returns 0 to 0x7fffffff { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return nrand48((unsigned int16 *) seed); } int lrandz() // implicit seed, repeatable sequence { static int64 seed = 12345678; return lrandz(&seed); } double drandz(int64 *seed) // returns 0.0 to 0.99999... { *seed = *seed ^ (*seed << 17); *seed = *seed ^ (*seed << 20); return erand48((unsigned int16 *) seed); } double drandz() // automatic seed, volatile { static int64 seed = get_seconds(); return drandz(&seed); } /******************************************************************************** spline1: define a curve using a set of data points (x and y values) spline2: for a given x-value, return a y-value fitting the curve For spline1, the no. of curve-defining points must be < 100. For spline2, the given x-value must be within the range defined in spline1. The algorithm was taken from the book "Numerical Recipes" (Cambridge University Press) and converted from Fortran to C++. ***/ namespace splinedata { int nn; float px1[100], py1[100], py2[100]; } void spline1(int dnn, float *dx1, float *dy1) { using namespace splinedata; float sig, p, u[100]; int ii; nn = dnn; if (nn > 100) zappcrash("spline1(), > 100 data points"); for (ii = 0; ii < nn; ii++) { px1[ii] = dx1[ii]; py1[ii] = dy1[ii]; if (ii && px1[ii] <= px1[ii-1]) zappcrash("spline1(), x-value not increasing"); } py2[0] = u[0] = 0; for (ii = 1; ii < nn-1; ii++) { sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]); p = sig * py2[ii-1] + 2; py2[ii] = (sig - 1) / p; u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1]) / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p; } py2[nn-1] = 0; for (ii = nn-2; ii >= 0; ii--) py2[ii] = py2[ii] * py2[ii+1] + u[ii]; return; } float spline2(float x) { using namespace splinedata; int kk, klo = 0, khi = nn-1; float h, a, b, y; while (khi - klo > 1) { kk = (khi + klo) / 2; if (px1[kk] > x) khi = kk; else klo = kk; } h = px1[khi] - px1[klo]; a = (px1[khi] - x) / h; b = (x - px1[klo]) / h; y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo] + (b*b*b - b) * py2[khi]) * (h*h) / 6; return y; } /********************************************************************************/ // Add text strings to a FIFO queue, retrieve text strings. // Can be used by one or two threads. // thread 1: open queue, get strings, close queue. // thread 2: put strings into queue. // create and initialize Qtext queue, empty status void Qtext_open(Qtext *qtext, int cap) { int cc; qtext->qcap = cap; qtext->qnewest = -1; qtext->qoldest = -1; qtext->qdone = 0; cc = cap * sizeof(ch *); qtext->qtext = (ch **) zmalloc(cc,"qtext"); memset(qtext->qtext,0,cc); return; } // add new text string to Qtext queue // if queue full, sleep until space is available void Qtext_put(Qtext *qtext, ch *format, ...) { int qnext; va_list arglist; ch message[200]; va_start(arglist,format); vsnprintf(message,199,format,arglist); va_end(arglist); qnext = qtext->qnewest + 1; if (qnext == qtext->qcap) qnext = 0; while (qtext->qtext[qnext]) zsleep(0.01); qtext->qtext[qnext] = zstrdup(message,"Qtext"); qtext->qnewest = qnext; return; } // remove oldest text string from Qtext queue // if queue empty, return a null string // returned string is subject for zfree() ch * Qtext_get(Qtext *qtext) { int qnext; ch *text; if (qtext->qcap == 0) return 0; qnext = qtext->qoldest + 1; if (qnext == qtext->qcap) qnext = 0; text = qtext->qtext[qnext]; if (! text) return 0; qtext->qtext[qnext] = 0; qtext->qoldest = qnext; return text; } // close Qtext, zfree() any leftover strings void Qtext_close(Qtext *qtext) { for (int ii = 0; ii < qtext->qcap; ii++) if (qtext->qtext[ii]) zfree(qtext->qtext[ii]); zfree(qtext->qtext); qtext->qcap = 0; return; } /********************************************************************************/ // compute variance for a list of numbers // std. deviation = sqrtf(variance) float variance(float *vals, int N) // 24.60 { float mean1, mean2, variance; int ii; if (N == 0) return 0; mean1 = mean2 = 0; for (ii = 0; ii < N; ii++) { mean1 += vals[ii]; mean2 += vals[ii] * vals[ii]; } mean1 = mean1 / N; mean2 = mean2 / N; variance = fabsf(mean2 - mean1 * mean1); return variance; } /******************************************************************************** Initialize application files according to following conventions: // new version + binary executable is at: /prefix/bin/appname // = PREFIX/bin/appname + other application folders are derived as follows: /prefix/share/appname/data/ desktop, parameters, userguide ... /prefix/share/doc/appname/ README, changelog, appname.man ... /prefix/share/appname/icons/ application icon files, filename.png /prefix/share/appname/images/ application image files /home/user/.appname/ some installation files are copied here /home/user/.appname/appname-N.N.log log file with error messages zprefix install location /usr or /usr/local zdatadir installed data files /prefix/share/appname/data/ zimagedir installed image files /prefix/share/appname/images/ zdocdir documentation files /prefix/share/doc/appname/ zhomedir local app files /home//.appname/ If it does not already exist, an application folder for the current user is created at /home/username/.appname (following common Linux convention). If this folder was created for the first time, copy specified files (following the 1st argument) from the install folder into the newly created user-specific folder. The assumption is that all initial data files for the application (e.g. parameters) will be in the install data folder, and these are copied to the user folder where the user or application can modify them. If the running program is not connected to a terminal device, stdout and stderr are redirected to the log file at /home/user/.appname/appname-N.N.log ***/ ch * get_zprefix() { return zfuncs::zprefix; } // /usr or /usr/local ch * get_zhomedir() { return zfuncs::zhomedir; } // /home//.appname ch * get_zdatadir() { return zfuncs::zdatadir; } // data files ch * get_zdocdir() { return zfuncs::zdocdir; } // documentation files ch * get_zimagedir() { return zfuncs::zimagedir; } // image files int zinitapp(ch *appvers, int argc, ch *argv[]) // appname-N.N { ch buff[300]; ch LNhomedir[200]; ch username[140]; ch cssfile[200]; ch *pp, *ch_time; int ii, cc, err, size; time_t startime; STATB statB; FILE *fid; pp = getenv("HOME"); // 24.50 if (pp) err = chdir(pp); Plog(0,"working directory: %s \n",pp); startime = time(null); // app start time, secs. since 1970 catch_signals(); // catch signals, do backtrace Plog(1,"command: "); for (ii = 0; ii < argc; ii++) Plog(1,"%s ",argv[ii]); // log command line Plog(1,"\n"); setlocale(LC_NUMERIC,"C"); // stop comma decimal points setpgid(0,0); // make a new process group strncpy0(zappvers,appvers,40); // appname-N.N Plog(1,"%s \n",zappvers); strncpy0(zappname,appvers,40); // appname without version pp = strchr(zappname,'-'); if (pp && pp[1] > '9') pp = strchr(pp+1,'-'); // bypass '-' in appname if (pp) *pp = 0; if (argc > 1 && strmatchV(argv[1],"-ver","-v",0)) exit(0); // exit if nothing else wanted progexe = 0; cc = readlink("/proc/self/exe",buff,300); // get my executable program path if (cc <= 0) zexit(1,"readlink() /proc/self/exe) failed"); buff[cc] = 0; // readlink() quirk progexe = zstrdup(buff,"zinitapp"); Plog(1,"program exe: %s \n",progexe); // executable path strncpy0(zprefix,progexe,200); pp = strstr(zprefix,"/bin/"); // get install prefix (e.g. /usr) if (pp) *pp = 0; else (strcpy(zprefix,"/usr")); // if /xxxxx/bin --> /xxxxx strncatv(zdatadir,199,zprefix,"/share/",zappname,"/data",null); // /prefix/share/appname/data strncatv(zimagedir,199,zprefix,"/share/",zappname,"/images",null); // /prefix/share/appname/images strncatv(zdocdir,199,zprefix,"/share/doc/",zappname,null); // /prefix/share/doc/appname ch_time = zstrdup(build_date_time,"zinitapp"); if (ch_time[4] == ' ') ch_time[4] = '0'; // replace month day ' d' with '0d' Plog(1,"build date/time: %s \n",ch_time); strncpy0(username,getenv("USER"),140); // get user name if (strlen(username) > 138) zexit(1,"username too big"); // insure derived folders <200 ch. *zhomedir = 0; // app home dir if (argc > 2 && strmatch(argv[1],"-home") && *argv[2] == '/') // get appname home folder from caller strncpy0(zhomedir,argv[2],200); // (any user-owned folder) if (! *zhomedir) { snprintf(LNhomedir,200,"%s/.%s-home",getenv("HOME"),zappname); // check $HOME/.appname-home 24.20 fid = fopen(LNhomedir,"r"); if (fid) { pp = fgets_trim(LNhomedir,200,fid); // if found, read pointer to home folder if (pp) strncpy0(zhomedir,pp,200); // (any user-owned folder) fclose(fid); } } if (! *zhomedir) snprintf(zhomedir,200,"%s/.%s",getenv("HOME"),zappname); // use $HOME/.appname/ 24.20 Plog(0,"%s home folder: %s \n",zappname,zhomedir); if (strchr(zhomedir,' ')) zexit(1,"home folder name contains a space"); // forbid space or quote in home folder if (strchr(zhomedir,'"')) zexit(1,"home folder name contains a quote"); if (strchr(zhomedir,'\'')) zexit(1,"home folder name contains a quote"); err = stat(zhomedir,&statB); // home folder exists already? if (err) { err = mkdir(zhomedir,0750); // no, create if (err) zexit(1,"cannot create %s: \n %s",zhomedir,strerror(errno)); } snprintf(logfile,199,"%s/%s.log",zhomedir,zappvers); // /home//appname-N.N.log Plog(0,"log file: %s \n",logfile); err = stat(logfile,&statB); // start new log file if >50 kb 25.1 if (! err) { size = statB.st_size; if (size > 50000) remove(logfile); } if (! isatty(fileno(stdin))) { // not attached to a terminal fid = freopen(logfile,"a",stdout); // redirect stdout/stderr to log file fid = freopen(logfile,"a",stderr); if (! fid) Plog(0,"*** cannot redirect stdout and stderr \n"); } Plog(1,"-------------------------------------------\n"); // log file separator ch_time = ctime(&startime); // start time: Ddd Mmm dd hh:mm:ss.nn ch_time[19] = 0; // eliminate hundredths of seconds if (ch_time[8] == ' ') ch_time[8] = '0'; // replace ' d' with '0d' Plog(1,"start %s %s \n",zappname,ch_time); fflush(0); tid_main = pthread_self(); // thread ID of main() process err = stat(zdatadir,&statB); // files from .../appname/data if (! err) zshell("noerr","cp -R -n %s/* %s >/dev/null 2>&1", // --> user app home (no replace) 24.20 zdatadir,zhomedir); // new: -n no clobber 24.20 // GTK initialization setenv("GDK_BACKEND","x11",0); // needed by Fedora (11/2023) but not Ubuntu setenv("GTK_THEME","default",0); // " " if (gtk_clutter_init(&argc,&argv) != CLUTTER_INIT_SUCCESS) // intiz. clutter and GTK zexit(1,"gtk_clutter_init() failed"); if (! gtk_init_check(0,null)) zexit(1,"gtk_init_check() failed"); setlocale(LC_NUMERIC,"C"); // NECESSARY: GTK changes locale int v1 = gtk_get_major_version(); // get GTK release version int v2 = gtk_get_minor_version(); int v3 = gtk_get_micro_version(); Plog(1,"GTK version: %d.%02d.%02d \n",v1,v2,v3); display = gdk_display_get_default(); // get hardware info screen = gdk_screen_get_default(); GdkRectangle rect; GdkMonitor *monitor; monitor = gdk_display_get_primary_monitor(display); gdk_monitor_get_geometry(monitor,&rect); monitor_ww = rect.width; monitor_hh = rect.height; if (! monitor_ww) zexit(1,"GTK cannot get monitor data"); GdkSeat *gdkseat = 0; // screen / KB / pointer associations if (screen) gdkseat = gdk_display_get_default_seat(display); // Ubuntu 16.10 if (screen) gtksettings = gtk_settings_get_for_screen(screen); if (gdkseat) mouse = gdk_seat_get_pointer(gdkseat); if (! mouse) zexit(1,"GTK cannot get pointer device"); if (gtksettings) { // get default font g_object_get(gtksettings,"gtk_font_name",&appfont,null); zsetfont(appfont); // set mono and bold versions } // widget style changes if widgets.css file is present snprintf(cssfile,200,"%s/widgets.css",get_zhomedir()); err = stat(cssfile,&statB); // 24.10 if (! err) { GtkStyleProvider *provider = (GtkStyleProvider *) gtk_css_provider_new(); gtk_style_context_add_provider_for_screen(zfuncs::screen,provider,999); gtk_css_provider_load_from_path(GTK_CSS_PROVIDER(provider),cssfile,0); } zmessageACK_init(); // initz. for thread usage 25.1 return 1; } /********************************************************************************/ // popup window with application 'about' information void zabout(GtkWidget *parent) { int zabout_dialog_event(zdialog *zd, ch *event); zdialog *zd; int cc; ch installed_release[80]; ch title[40]; ch *execfile; /*** __________________________________________________ | About Appname | | | | installed release: appname-N.N Mon dd yyyy | // 'query release' removed | executable: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx | | contact: mkornelix@gmail.com | |__________________________________________________| ***/ execfile = progexe; snprintf(installed_release,80,"%s %s",zappvers,build_date_time); // appname-N.N Mon dd yyyy hh:mm:ss cc = strlen(installed_release); installed_release[cc-9] = 0; // remove hh:mm:ss if (installed_release[cc-16] == ' ') installed_release[cc-16] = '0'; // replace "Jan 1" with "Jan 01" snprintf(title,40,"About %s",zappname); zd = zdialog_new(title,parent,null); zdialog_add_widget(zd,"hbox","hbirel","dialog"); zdialog_add_widget(zd,"label","labir1","hbirel","installed release:","space=3"); zdialog_add_widget(zd,"label","labir2","hbirel",installed_release); zdialog_add_widget(zd,"hbox","hbexe","dialog"); zdialog_add_widget(zd,"label","labexe1","hbexe","executable:","space=3"); zdialog_add_widget(zd,"label","labexe2","hbexe",execfile); zdialog_add_widget(zd,"hbox","hbcontact","dialog"); zdialog_add_widget(zd,"label","labcontact","hbcontact","contact:","space=3"); zdialog_add_widget(zd,"label","contact","hbcontact",zcontact); zdialog_run(zd,zabout_dialog_event,"parent"); return; } // dialog event and completion function int zabout_dialog_event(zdialog *zd, ch *event) { if (zd->zstat) zdialog_free(zd); return 1; } /********************************************************************************/ // set a new application font via GtkSettings // newfont should be something like "sans 11" // use generic monospace font since app font may not have a mono version void zsetfont(ch *newfont) { ch font[40], bfont[48], mfont[48], mbfont[56]; ch junk[40]; int nn, size; if (! gtksettings) return; nn = sscanf(newfont,"%s %d",font,&size); // "sans 11" if (nn != 2) { nn = sscanf(newfont,"%s %s %d",font,junk,&size); if (nn != 3) goto fail; } if (size < 5 || size > 30) goto fail; g_object_set(gtksettings,"gtk-font-name",newfont,null); // set dialog font snprintf(bfont,48,"%s bold %d",font,size); // "sans bold 11" snprintf(mfont,48,"mono %d",size-1); // "mono 10" snprintf(mbfont,56,"mono bold %d",size-1); // "mono bold 10" appfont = zstrdup(newfont,"zsetfont"); appboldfont = zstrdup(bfont,"zsetfont"); appmonofont = zstrdup(mfont,"zsetfont"); appmonoboldfont = zstrdup(mbfont,"zsetfont"); appfontsize = size; return; fail: Plog(0,"cannot set font: %s \n",newfont); return; } /********************************************************************************/ // get the font character width and height for a given widget // returns 0 if OK, +N if error int widget_font_metrics(GtkWidget *widget, int &fontwidth, int &fontheight) { PangoContext *pangocontext; PangoFontDescription *pangofontdesc; PangoFontMetrics *pangofontmetrics; PangoLanguage *pangolanguage; pangocontext = gtk_widget_get_pango_context(widget); pangofontdesc = pango_context_get_font_description(pangocontext); pangolanguage = pango_language_get_default(); pangofontmetrics = pango_context_get_metrics(pangocontext,pangofontdesc,pangolanguage); if (! pangofontmetrics) { Plog(0,"widget_font_metrics() failed \n"); return 1; } fontwidth = pango_font_metrics_get_approximate_char_width(pangofontmetrics); fontheight = pango_font_metrics_get_ascent(pangofontmetrics) + pango_font_metrics_get_descent(pangofontmetrics); fontwidth /= PANGO_SCALE; fontheight /= PANGO_SCALE; return 0; } /********************************************************************************/ // Find installation file or user file. // file type: doc, data, user // file name: README, changelog, userguide, parameters ... // Returns complete file name, e.g. /usr/share/appname/data/userguide // Output filespec should be 200 bytes (limit for all installation files). // Returns 0 if OK, +N if not found. int get_zfilespec(ch *filetype, ch *filename, ch *filespec) { int cc, err; STATB statB; filespec[0] = '/'; strcat(filespec,filetype); // leave /type as default if (strmatch(filetype,"doc")) strcpy(filespec,zdocdir); // /usr/share/doc/appname if (strmatch(filetype,"data")) strcpy(filespec,zdatadir); // /usr/share/appname/data if (strmatch(filetype,"user")) strcpy(filespec,zhomedir); // /home//.appname cc = strlen(filespec); filespec[cc] = '/'; // /folders.../ strcpy(filespec+cc+1,filename); // /folders.../filename err = stat(filespec,&statB); if (! err) return 0; // found if (! strmatch(filetype,"doc")) return 1; // doc files may be in strcpy(filespec,zdocdir); // /usr/share/doc/appname/extras strcat(filespec,"/extras/"); // due to Linux chaos cc = strlen(filespec); strcpy(filespec+cc,filename); err = stat(filespec,&statB); if (! err) return 0; // found return 1; // not found } /********************************************************************************/ // display application log file in a popup window // The log file is /home//.appname/logfile void showz_logfile(GtkWidget *parent) { ch buff[200]; fflush(0); snprintf(buff,199,"cat %s",logfile); popup_command(buff,800,600,parent); return; } /********************************************************************************/ // find and show a text file in /usr/share/doc/appname/ // or /usr/share/appname/data // the text file may also be a compressed .gz file // type is "doc" or "data" void showz_textfile(ch *type, ch *file, GtkWidget *parent) { ch filex[40], filespec[200], command[200]; int err; if (! main_thread()) zappcrash("illegal call from thread"); strncpy0(filex,file,36); // look for gzip file first strcat(filex,".gz"); err = get_zfilespec(type,filex,filespec); if (! err) { snprintf(command,200,"zcat \"%s\"",filespec); // use quotes around filename popup_command(command,700,500,parent,1); return; } strncpy0(filex,file,35); // look also for bzip2 file strcat(filex,".bz2"); err = get_zfilespec(type,filex,filespec); if (! err) { snprintf(command,200,"bzcat \"%s\"",filespec); popup_command(command,700,500,parent,1); return; } strncpy0(filex,file,36); // look for uncompressed file err = get_zfilespec(type,filex,filespec); if (! err) { snprintf(command,200,"cat \"%s\"",filespec); popup_command(command,700,500,parent,1); return; } zmessageACK(mainwin,"file not found: %s %s",type,file); return; } /******************************************************************************** void showz_docfile(GtkWidget *parent, ch *docfile, ch *topic) Show docfile in popup scrolling text window with 'topic' at the top. docfile is located in data folder: get_zdatadir() images are located in image folder: get_zimagedir() docfile format: TOPIC: TOPIC 1 topic: bold text text text text text text text text text text text text ... topic text TOPIC: TOPIC 2 +image: file1.png +image: file2.jpg embedded images text text text text text text text text {TOPIC 1} text text ... link to topic, bold ^bold subtopic line^ subtopic, bold 24.10 text text text text text ^bold text bold text^ text text ... embedded bold text text text text text text ^http.........^ text text text ... web link, bold 24.10 *********************************************************************************/ namespace showz_docfile_names { #define TMAX 1000 // max. topics and markups zdialog *zd = 0; GtkWidget *txwidget; int cc, ii, jj; int currline; ch *pp; ch *Tname[TMAX]; // all topic names (link targets) int Tline[TMAX]; // topic lines int Bline[TMAX], Bpos[TMAX], Bcc[TMAX]; // bold texts: line, posn, cc int TN, BN, BF; // counts, flags zlist_t *RTopics = 0; // recent topics list ch RTfile[200] = ""; // recent topics file int RTmax = 10; // max. recent topics } void showz_docfile(GtkWidget *parent, ch *docfile, ch *utopic) { using namespace showz_docfile_names; int showz_docfile_clickfunc(GtkWidget *widget, int line, int pos, ch *input); void audit_docfile(ch *docfile); FILE *fid; ch filespec[200], buff1[10000], buff2[10000]; // record cc limit (100 lines x 100 char.) ch topic[50], image[100]; // limits: topic name, image name ch *pp1, *pp2; int Fm, line, pos1, pos2, cc; GdkPixbuf *pixbuf; GError *gerror; if (! main_thread()) zappcrash("illegal call from thread"); if (utopic && strmatch(utopic,"quit")) { // quit - save recent topics list if (RTopics && *RTfile) zlist_to_file(RTopics,RTfile); if (zdialog_valid2(zd)) popup_report_close(zd,0); // close open report or UG return; } if (utopic && strmatch(utopic,"validate")) { // check document for errors audit_docfile(docfile); // (developer tool) return; } if (zd && zdialog_valid2(zd,docfile)) // document active already goto initz_done; snprintf(RTfile,200,"%s/recent_topics",get_zhomedir()); // get recent topics list RTopics = zlist_from_file(RTfile); if (! RTopics) RTopics = zlist_new(0); for (ii = 0; ii < TN; ii++) zfree(Tname[ii]); // free prior docfile data if any TN = BN = 0; snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile); // open docfile fid = fopen(filespec,"r"); if (! fid) zexit(1,"%s %s \n",filespec,strerror(errno)); for (line = 0; ; line++) // loop docfile recs/lines { pp1 = fgets_trim(buff1,10000,fid); // line with null at end if (! pp1) break; // EOF if (strmatchN(pp1,"EOF",3)) break; // end of displayed text if (! strmatchN(buff1,"TOPIC:",6)) continue; // not a topic name pp1 = buff1 + 6; while (*pp1 == ' ') pp1++; pp2 = pp1 + strlen(pp1); while (*pp2 <= ' ') pp2--; cc = pp2 - pp1 + 1; if (cc < 1 || cc > 49) continue; strncpy0(topic,pp1,cc+1); // add topic and line number to topic list Tname[TN] = zstrdup(topic,"docfile"); Tline[TN] = line; if (++TN == TMAX) zexit(1,"docfile exceeds %d topics \n",TMAX); } fclose(fid); Plog(1,"Docfile topics: %d \n",TN); zd = popup_report_open(docfile,parent,900,800,1,0,showz_docfile_clickfunc, // popup window for docfile text display "Recent", "<", ">", "Find", "Save", "X", 0); // + buttons if (! zd) zexit(1,"cannot open docfile window \n"); popup_report_font_attributes(zd); // use high contrast font txwidget = zdialog_gtkwidget(zd,"text"); // text widget in zdialog snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile); // open docfile again fid = fopen(filespec,"r"); if (! fid) zexit(1,"%s %s \n",filespec,strerror(errno)); for (line = 0; ; line++) // loop docfile recs/lines { pp1 = fgets_trim(buff1,10000,fid); // line with null at end if (! pp1) break; // EOF if (strmatchN(pp1,"EOF",3)) break; // end of displayed text if (strmatchN(buff1,"TOPIC:",6)) // TOPIC: TOPIC NAME { pp1 = buff1 + 6; while (*pp1 == ' ') pp1++; popup_report_write(zd,1,"%s \n",pp1); // output TOPIC NAME, bold continue; } pp1 = strstr(buff1,"+image:"); // line has image names if (pp1) { while (pp1) { pp1 += 7; while (*pp1 == ' ') pp1++; // remove blanks strncpy0(image,pp1,100); pp2 = strchr(image,' '); if (pp2) *pp2 = 0; snprintf(filespec,200,"%s/%s",get_zimagedir(),image); // full filespec gerror = 0; pixbuf = gdk_pixbuf_new_from_file(filespec,&gerror); // convert to pixbuf image if (pixbuf) { popup_report_write(zd,0," ",0); // leading spaces popup_report_insert_pixbuf(zd,line,pixbuf); // write image to output line g_object_unref(pixbuf); } else Plog(0,"cannot load image file: %s \n",image); pp1 = strstr(pp1,"+image:"); // next image file } popup_report_write(zd,0,"\n",0); // write image line EOL continue; // next input line } strncpy0(buff2,buff1,10000); // line is text Fm = 0; // buff1: line with bold markups for (pp1 = buff2; (pp1 = strpbrk(pp1,"{}^"));) { // buff2: line without markups 24.10 cc = strlen(pp1+1); memmove(pp1,pp1+1,cc+1); Fm++; // markups found in this line } popup_report_write(zd,0,"%s \n",buff2); // write line to output, no markups if (! Fm) continue; // no markups found, done BF = 0; // intiz. no markups active pos1 = pos2 = 0; // char. posn. with/without markups while (buff1[pos1]) // loop chars. in line { if (! strchr("{}^",buff1[pos1])) { // not a markup char. 24.10 if (BF) ++Bcc[BN]; // count cc for active markups pos1++; pos2++; continue; } if (buff1[pos1] == '{') { // ... {bold text} ... link to topic if (BN == TMAX) zexit(1,"docfile > %d markups \n",TMAX); BF = 1; // start bold text Bline[BN] = line; Bpos[BN] = pos2; Bcc[BN] = 0; } else if (buff1[pos1] == '}') { // end bold text BF = 0; BN++; // bold markup count } else if (buff1[pos1] == '^') { // ... ^bold text^ ... 24.10 if (! BF) { // starting ^ BF = 1; Bline[BN] = line; Bpos[BN] = pos2; Bcc[BN] = 0; } else { // ending ^ BF = 0; BN++; } } pos1++; // skip over markup char. } if (BF) BN++; // EOL without trailing } or ^ } // end loop line fclose(fid); for (ii = 0; ii < BN; ii++) // do all text bolds popup_report_bold_word(zd,Bline[ii],Bpos[ii],Bcc[ii]); zmainloop(); // necessary here for some reason initz_done: currline = 0; // docfile line for topic if (utopic) // initial topic from caller { strncpy0(topic,utopic,50); strToUpper(topic); // 25.0 cc = strlen(topic); for (ii = 0; ii < TN; ii++) { // search docfile topics if (strmatchcase(topic,Tname[ii])) { currline = Tline[ii]; // get line of matching topic break; } } if (ii == TN) Plog(0,"topic not found: %s %s \n",utopic,topic); else { zlist_prepend(RTopics,topic,0); // add to recent topics, 1st position ii = zlist_find(RTopics,topic,1); if (ii > 0) zlist_remove(RTopics,ii); // if topic present later, remove it ii = zlist_count(RTopics); if (ii > RTmax) zlist_remove(RTopics,ii-1); // limit entry count } } popup_report_scroll_top(zd,currline); // scroll to topic line return; } // handle clicks on document window and KB inputs int showz_docfile_clickfunc(GtkWidget *txwidget, int line, int posn, ch *input) { using namespace showz_docfile_names; int ii, jj, cc; int vtop, vbott, page, posn8; ch *text, *pp1, *pp2; ch *topic; ch text2[10000], weblink[200]; static int Ltab[10], maxL = 10, Lpos = 0; // last 10 links clicked gtk_widget_grab_focus(txwidget); // necessary for some reason txwidget_get_visible_lines(txwidget,vtop,vbott); // range of lines on screen if (strmatch(input,"X")) { // [X] kill report 25.0 showz_docfile(0,0,"quit"); return 1; } if (strmatch(input,"Recent")) // [Recent] recent topics { topic = popup_choose(RTopics); // choose a topic 25.1 if (! topic) return 0; for (ii = 0; ii < TN; ii++) // search docfile topics if (strmatchcase(topic,Tname[ii])) break; if (ii == TN) return 0; // not found currline = Tline[ii]; // get line of matching topic popup_report_scroll_top(zd,currline); // scroll to topic line zlist_prepend(RTopics,topic,0); // add to recent topics, 1st position ii = zlist_find(RTopics,topic,1); if (ii > 0) zlist_remove(RTopics,ii); // if topic present later, remove it ii = zlist_count(RTopics); if (ii > RTmax) zlist_remove(RTopics,ii-1); // limit entry count return 1; } if (input) // 25.1 { if (*input == GDK_KEY_Left || *input == '<') { // [<] or left arrow, go back Ltab[Lpos] = currline; if (Lpos > 0) Lpos--; currline = Ltab[Lpos]; txwidget_scroll_top(txwidget,currline); // scroll line to top of window return 1; } if (*input == GDK_KEY_Right || *input == '>') { // [>] or right arrow, go forward Ltab[Lpos] = currline; if (Lpos < maxL-1 && Ltab[Lpos+1] >= 0) currline = Ltab[++Lpos]; txwidget_scroll_top(txwidget,currline); // scroll line to top of window return 1; } if (strmatch(input,"Find")) { // [Find] zdialog_send_event(zd,"Find"); return 1; } if (*input >= 0xfd00) { // navigation key page = vbott - vtop - 2; // page size, lines if (page < 0) page = 0; currline = 0; // default if (*input == GDK_KEY_Up) currline = vtop - 1; // handle navigation keys else if (*input == GDK_KEY_Down) currline = vbott + 1; else if (*input == GDK_KEY_Page_Up) currline = vtop - page; else if (*input == GDK_KEY_Page_Down) currline = vbott + page; else if (*input == GDK_KEY_KP_Page_Up) currline = vtop - page; else if (*input == GDK_KEY_KP_Page_Down) currline = vbott + page; else if (*input == GDK_KEY_KP_9) currline = vtop - page; else if (*input == GDK_KEY_KP_3) currline = vbott + page; else if (*input == GDK_KEY_Home) currline = 0; else if (*input == GDK_KEY_End) currline = 999999; if (currline < 0) currline = 0; txwidget_scroll(txwidget,currline); // put line on screen return 1; } } if (line < 0 || posn < 0) return 0; // clicked line and position text = txwidget_line(txwidget,line,1); // get line text if (! text) return 0; strncpy0(text2,text,posn+1); // compensate utf8 chars. before posn posn8 = posn + strlen(text2) - utf8len(text2); for (ii = 0; ii < BN; ii++) { // is clicked text bolded text if (line == Bline[ii]) { if (posn8 >= Bpos[ii] && posn8 <= Bpos[ii] + Bcc[ii]) { // yes for (jj = 0; jj < TN; jj++) if (strmatchN(Tname[jj],text+Bpos[ii],Bcc[ii])) break; // does it match a topic name if (jj < TN) goto topiclink; // yes } } } goto weblink; topiclink: topic = Tname[jj]; // clicked topic currline = Tline[jj]; txwidget_scroll_top(txwidget,currline); // scroll topic to top of window zlist_prepend(RTopics,topic,0); // add to recent topics, 1st position ii = zlist_find(RTopics,topic,1); if (ii > 0) zlist_remove(RTopics,ii); // if topic present later, remove it ii = zlist_count(RTopics); if (ii > RTmax) zlist_remove(RTopics,ii-1); // limit entry count if (Lpos == maxL-1) { for (jj = 0; jj < maxL-1; jj++) // back tab table full, Ltab[jj] = Ltab[jj+1]; // discard oldest Lpos--; } Ltab[Lpos] = vtop; // curr. top line >> back tab Lpos++; // advance back tab position Ltab[Lpos] = currline; // >> back tab return 1; weblink: for ( ; posn >= 0; posn--) if ( *(text+posn) == ' ') break; // click position, preceding blank if (posn < 0) posn = 0; if (text[posn] == ' ') posn += 1; // eliminate preceding blank pp1 = text + posn; pp2 = strchr(pp1,' '); // following blank or EOL if (pp2) cc = pp2 - pp1; else cc = strlen(pp1); if (pp1[cc-1] == '.') cc--; // remove trailing period if (cc > 199) return 1; strncpy0(weblink,pp1,cc+1); // copy clicked text string if (strmatchN(pp1,"http",4)) // if "http..." assume a web link zshell("log ack","xdg-open %s",weblink); return 1; } // validate the F1_help_topic links and the internal links in a docfile // (developer tool) void audit_docfile(ch *docfile) { #define LMAX 10000 // max. docfile lines/recs ch *textlines[LMAX]; ch *Tname[TMAX]; ch filespec[200], buff[10000], image[100]; // limits: filename, rec. cc, image name ch topic[50]; ch *pp1, *pp2, *pp3; FILE *fid; int Ntext, Ntop, Nerrs; int ii, cc, line; GdkPixbuf *pixbuf; GError *gerror; Plog(0,"\n*** audit docfile %s *** \n",docfile); Ntext = Ntop = Nerrs = 0; snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile); // open docfile fid = fopen(filespec,"r"); if (! fid) { Plog(0,"%s %s",filespec,strerror(errno)); return; } for (line = 0; line < LMAX; line++) // read docfile text lines { pp1 = fgets_trim(buff,10000,fid); // line without \n EOL if (! pp1) break; // EOF textlines[Ntext] = zstrdup(pp1,"docfile"); // copy text line to memory if (++Ntext == LMAX) zexit(1,"exceed LMAX text recs"); if (! strmatchN(pp1,"TOPIC:",6)) continue; // not a topic line pp1 += 6; while (*pp1 == ' ') pp1++; cc = strlen(pp1); if (cc < 2 || cc > 49) continue; strncpy0(topic,pp1,cc+1); // add topic to list Tname[Ntop] = zstrdup(topic,"docfile"); if (++Ntop == TMAX) zexit(1,"exceed TMAX topics"); printf("topic: %s \n",topic); } fclose(fid); printf("text lines: %d topics: %d \n",Ntext,Ntop); for (line = 0; line < Ntext; line++) // process text lines { pp1 = textlines[line]; pp2 = strstr(pp1,"+image:"); if (pp2) { // line contains images while (pp2) { pp2 += 7; while (*pp2 == ' ') pp2++; // remove blanks strncpy0(image,pp2,100); pp3 = strchr(image,' '); if (pp3) *pp3 = 0; snprintf(filespec,200,"%s/%s",get_zimagedir(),image); // full filespec gerror = 0; pixbuf = gdk_pixbuf_new_from_file(filespec,&gerror); // convert to pixbuf image if (pixbuf) g_object_unref(pixbuf); else { Plog(0,"cannot load image file: %s \n",image); Nerrs++; } pp2 = strstr(pp2,"+image:"); } continue; // next line } if (strmatchN(pp1,"TOPIC:",6)) continue; // skip topic line { pp1 = strchr(pp1,'{'); // get topic links in line while (pp1) { pp2 = strchr(pp1+1,'}'); // ... {topic name} ... if (! pp2) break; // | | pp1 += 1; // pp1 pp2 cc = pp2 - pp1; if (cc < 2 || cc > 49) { printf("topic >49 chars, line %d: %s \n",line,pp1); // topic name > 49 char. Nerrs++; break; } strncpy0(topic,pp1,cc+1); for (ii = 0; ii < Ntop; ii++) if (strcmp(topic,Tname[ii]) == 0) break; if (ii == Ntop) { // topic not found printf("topic not found, line %d: %s \n",line,topic); Nerrs++; } pp1 = strchr(pp2+1,'{'); } continue; // next line } } Plog(0," %d errors \n",Nerrs); for (ii = 0; ii < Ntext; ii++) // free memory zfree(textlines[ii]); for (ii = 0; ii < Ntop; ii++) zfree(Tname[ii]); return; } /******************************************************************************** GTK utility functions ********************************************************************************/ // Iterate GTK main loop every "skip" calls. // If called within the main() thread, does a GTK main loop to process menu events, etc. // You must do this periodically within long-running main() thread tasks if you wish to // keep menus, buttons, output windows, etc. alive and working. The skip argument will // cause the function to do nothing for skip calls, then perform the normal function. // This allows it to be embedded in loops with little execution time penalty. // If skip = N, zmainloop() will do nothing for N calls, execute normally, etc. // If called from a thread, zmainloop() does nothing. void zmainloop(int skip) { static int xskip = 0; if (skip && ++xskip < skip) return; xskip = 0; if (! main_thread()) return; // thread caller, do nothing while (gtk_events_pending()) gtk_main_iteration_do(0); return; } // Iterate the main loop and sleep for designated time void zmainsleep(float secs) { while (secs > 0) { zmainloop(); zsleep(0.0001); // 24.40 secs = secs - 0.0001; } return; } /********************************************************************************/ // cairo drawing context for GDK window cairo_t * draw_context_create(GdkWindow *gdkwin, draw_context_t &context) { if (! main_thread()) zappcrash("illegal call from thread"); if (context.dcr) { Plog(0,"draw_context_create(): nested call"); return context.dcr; } context.win = gdkwin; context.rect.x = 0; context.rect.y = 0; context.rect.width = gdk_window_get_width(gdkwin); context.rect.height = gdk_window_get_height(gdkwin); context.reg = cairo_region_create_rectangle(&context.rect); context.ctx = gdk_window_begin_draw_frame(gdkwin,context.reg); context.dcr = gdk_drawing_context_get_cairo_context(context.ctx); return context.dcr; } void draw_context_destroy(draw_context_t &context) { if (! main_thread()) zappcrash("illegal call from thread"); if (! context.dcr) { Plog(0,"draw_context_destroy(): not created \n"); return; } gdk_window_end_draw_frame(context.win,context.ctx); cairo_region_destroy(context.reg); /* cairo_destroy(context.dcr); this is fatal */ context.dcr = 0; return; } /********************************************************************************/ // txwidget functions // -------------------- // // High-level use of GtkTextView widget for text reports, line editing, text selection // In functions below, txwidget = zdialog_gtkwidget(zd,"widgetname"), // where "widgetname" is a zdialog "text" widget type. // All line numbers and line positions are zero based. // clear the text widget to blank void txwidget_clear(GtkWidget *txwidget) { GtkTextBuffer *textBuff; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; gtk_text_buffer_set_text(textBuff,"",-1); return; } // clear the text widget from given line to end void txwidget_clear(GtkWidget *txwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // iter at line start gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete existing line and rest of buffer return; } // get the current line count int txwidget_linecount(GtkWidget *txwidget) { GtkTextBuffer *textBuff; int nlines; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return 0; nlines = gtk_text_buffer_get_line_count(textBuff); return nlines; } // append a new line of text to the end of existing text lines // line should normally include trailing \n // if current last line has no \n, text is appended to this line void txwidget_append(GtkWidget *txwidget, int bold, ch *format, ...) { va_list arglist; ch textline[20000]; // txwidget append cc limit GtkTextBuffer *textBuff; GtkTextIter enditer; GtkTextTag *fontag = 0; ch *normfont = zfuncs::appmonofont; ch *boldfont = zfuncs::appmonoboldfont; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(textline,19999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; gtk_text_buffer_get_end_iter(textBuff,&enditer); // end of text if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0); // prepare bold/norm tag else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0); gtk_text_buffer_insert_with_tags(textBuff,&enditer,textline,-1,fontag,null); // insert line return; } // same as above, with scroll to last line added (slower) void txwidget_append2(GtkWidget *txwidget, int bold, ch *format, ...) { va_list arglist; ch textline[20000]; // txwidget append cc limit GtkTextBuffer *textBuff; GtkTextIter enditer; GtkTextTag *fontag = 0; ch *normfont = zfuncs::appmonofont; ch *boldfont = zfuncs::appmonoboldfont; GtkAdjustment *vadjust; double upperlimit; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(textline,19999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; gtk_text_buffer_get_end_iter(textBuff,&enditer); // end of text if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0); // prepare bold/norm tag else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0); gtk_text_buffer_insert_with_tags(textBuff,&enditer,textline,-1,fontag,null); // insert line vadjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(txwidget)); upperlimit = gtk_adjustment_get_upper(vadjust); gtk_adjustment_set_value(vadjust,upperlimit); zmainloop(); return; } // insert a new line of text after designated line // use line -1 to insert before line 0 // line should normally include trailing \n void txwidget_insert(GtkWidget *txwidget, int bold, int line, ch *format, ...) { va_list arglist; ch textline[20000]; // txwidget insert cc limit GtkTextBuffer *textBuff; GtkTextIter iter; int nlines; GtkTextTag *fontag = 0; ch *normfont = zfuncs::appmonofont; ch *boldfont = zfuncs::appmonoboldfont; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(textline,19999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; if (line < 0) gtk_text_buffer_get_start_iter(textBuff,&iter); // insert before line 0 if (line >= 0) { nlines = gtk_text_buffer_get_line_count(textBuff); // insert after line if (line < nlines - 1) gtk_text_buffer_get_iter_at_line(textBuff,&iter,line+1); // start of next line else gtk_text_buffer_get_end_iter(textBuff,&iter); // or end of text } if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0); // prepare bold/norm tag else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0); gtk_text_buffer_insert_with_tags(textBuff,&iter,textline,-1,fontag,null); // insert line return; } // replace a given line with a new line // line = -1: replace last line. -2: replace last-1 line, etc. // new line should normally include trailing \n void txwidget_replace(GtkWidget *txwidget, int bold, int line, ch *format, ...) { va_list arglist; ch textline[20000]; // txwidget replace cc limit GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; GtkTextTag *fontag = 0; ch *normfont = zfuncs::appmonofont; ch *boldfont = zfuncs::appmonoboldfont; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(textline,19999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line < 0) line = nlines + line - 1; if (line >= nlines) line = nlines - 1; gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // line start iter2 = iter1; gtk_text_iter_forward_line(&iter2); // end gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete line gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); if (bold) fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0); // prepare bold/norm tag else fontag = gtk_text_buffer_create_tag(textBuff,0,"font",normfont,0); gtk_text_buffer_insert_with_tags(textBuff,&iter1,textline,-1,fontag,null); // insert line return; } // delete a given line including the trailing \n void txwidget_delete(GtkWidget *txwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line < 0 || line >= nlines) return; gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // line start iter2 = iter1; gtk_text_iter_forward_line(&iter2); // end gtk_text_buffer_delete(textBuff,&iter1,&iter2); // delete line return; } // find first line of text containing characters matching input string // search is from line1 to end, then from 0 to line1-1 // returns first matching line or -1 if none // comparison is not case sensitive int txwidget_find(GtkWidget *txwidget, ch *matchtext, int line1, int hilite) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int line, nlines, cc; ch *textline = 0, *pp1, *pp2; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return -1; nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (! nlines) return -1; if (line1 < 0) line1 = 0; // starting line to search if (line1 >= nlines) line1 = 0; line = line1; while (true) { gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // line start iter2 = iter1; gtk_text_iter_forward_line(&iter2); // end textline = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); // get text if (textline) { pp1 = strcasestr(textline,matchtext); // look for matching text if (pp1) break; // found free(textline); } line++; if (line == nlines) line = 0; if (line == line1) return -1; // wrapped around, not found } if (hilite) { cc = strlen(matchtext); // highlight matching text pp2 = pp1 + cc - 1; gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-textline); gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-textline+1); gtk_text_buffer_select_range(textBuff,&iter1,&iter2); } free(textline); return line; } // insert a pixbuf image at designated line void txwidget_insert_pixbuf(GtkWidget *txwidget, int line, GdkPixbuf *pixbuf) { int nlines; GtkTextBuffer *textBuff; GtkTextIter iter; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; nlines = gtk_text_buffer_get_line_count(textBuff); // insert after line if (line < nlines - 1) gtk_text_buffer_get_iter_at_line(textBuff,&iter,line+1); // start of next line else gtk_text_buffer_get_end_iter(textBuff,&iter); // or end of text gtk_text_buffer_insert_pixbuf(textBuff,&iter,pixbuf); return; } // scroll a txwidget to put a given line on screen // 1st line = 0. for last line use line = -1. void txwidget_scroll(GtkWidget *txwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter; GtkTextMark *mark; GtkAdjustment *vadjust; double upperlimit; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; vadjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(txwidget)); if (line < 0) { // bottom zmainloop(); // make it work (GTK problem) upperlimit = gtk_adjustment_get_upper(vadjust); gtk_adjustment_set_value(vadjust,upperlimit); } else { gtk_text_buffer_get_iter_at_line(textBuff,&iter,line); mark = gtk_text_buffer_create_mark(textBuff,0,&iter,0); gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(txwidget),mark); } return; } // scroll a txwidget to put a given line at the top of the window void txwidget_scroll_top(GtkWidget *txwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter; GtkTextMark *mark; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; gtk_text_buffer_get_iter_at_line(textBuff,&iter,line); mark = gtk_text_buffer_create_mark(textBuff,0,&iter,0); gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(txwidget),mark,0,1,0,0); return; } // get the range of txwidget lines currently visible in the window void txwidget_get_visible_lines(GtkWidget *txwidget, int &vtop, int &vbott) { GdkRectangle rect; GtkTextIter iter1, iter2; int y1, y2; if (! main_thread()) zappcrash("illegal call from thread"); gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(txwidget),&rect); y1 = rect.y; y2 = y1 + rect.height; gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(txwidget), &iter1, y1, 0); gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(txwidget), &iter2, y2, 0); vtop = gtk_text_iter_get_line(&iter1); vbott = gtk_text_iter_get_line(&iter2) - 1; return; } // dump the entire txwidget contents into a file void txwidget_dump(GtkWidget *widget, ch *filename) { FILE *fid; ch *prec; int line, err; if (! main_thread()) zappcrash("illegal call from thread"); fid = fopen(filename,"w"); // open file if (! fid) { zmessageACK(mainwin,"cannot open file %s",filename); return; } for (line = 0; ; line++) { prec = txwidget_line(widget,line,1); // get text line, strip \n if (! prec) break; fprintf(fid,"%s\n",prec); // output with \n } err = fclose(fid); // close file if (err) zmessageACK(mainwin,"file close error"); return; } // dump the entire txwidget contents into a file, using a save-as dialog void txwidget_save(GtkWidget *widget, GtkWindow *parent) { ch *file; if (! main_thread()) zappcrash("illegal call from thread"); file = zgetfile("save text to file",parent,"save","noname"); if (! file) return; txwidget_dump(widget,file); zfree(file); return; } // Get a line of text. Returned text is subject for zfree(). // trailing \n is included if strip == 0 ch * txwidget_line(GtkWidget *txwidget, int line, int strip) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int cc, nlines; ch *textline, *ztext; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return 0; nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line < 0 || line >= nlines) return 0; gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // line start iter2 = iter1; gtk_text_iter_forward_line(&iter2); // end textline = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); // get text line if (! textline) return 0; ztext = zstrdup(textline,"txwidget"); free(textline); if (strip) { cc = strlen(ztext); if (cc && ztext[cc-1] == '\n') ztext[cc-1] = 0; } return ztext; } // highlight a given line of text void txwidget_highlight_line(GtkWidget *txwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; zmainloop(); if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; nlines = gtk_text_buffer_get_line_count(textBuff); // lines now in buffer if (line < 0 || line >= nlines) return; gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line); // line start iter2 = iter1; gtk_text_iter_forward_line(&iter2); // end gtk_text_buffer_select_range(textBuff,&iter1,&iter2); // highlight return; } // get the word at the given position within the line // words are defined by line starts and ends, and the given delimiters // returns word and delimiter (&end) ch * txwidget_word(GtkWidget *txwidget, int line, int posn, ch *dlims, ch &end) { GtkTextBuffer *textBuff; ch *txline, *pp1, *pp2, *ztext; int cc, nn; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return 0; txline = txwidget_line(txwidget,line,0); if (! txline) return 0; cc = strlen(txline); if (txline[cc-1] == '\n') { txline[cc-1] = 0; cc--; } pp1 = pp2 = txline; // fix for utf8 while (true) { while (*pp1 == ' ' || strchr(dlims,*pp1)) pp1++; // pp1 = start of next word in line if (*pp1 == 0) return 0; pp2 = pp1; while (true) { if (*pp2 == 0) break; nn = utf8_position(pp2,1); // pp2 = next delimiter or EOL if (nn < 0) break; if (strchr(dlims,pp2[nn])) break; pp2 += nn; posn += nn-1; if (pp2 - txline >= cc) break; } if (txline+posn >= pp1 && txline+posn <= pp2) break; // pp1-pp2 is word clicked nn = utf8_position(pp2,1); if (nn < 0) return 0; pp1 = pp2 + nn; posn += nn-1; if (posn >= cc) return 0; if (pp1 >= txline+posn) return 0; } nn = utf8_position(pp2,1); if (nn > 0) cc = pp2 - pp1 + nn; else cc = pp2 - pp1 + strlen(pp2); end = pp1[cc]; ztext = (ch *) zmalloc(cc+1,"txwidget"); strncpy0(ztext,pp1,cc+1); zfree(txline); return ztext; } // highlight text at line and positiion, length cc void txwidget_highlight_word(GtkWidget *txwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; ch *txline, *pp1, *pp2; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; txline = txwidget_line(txwidget,line,0); if (! txline) return; pp1 = txline + posn; pp2 = pp1 + cc - 1; gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline); gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1); gtk_text_buffer_select_range(textBuff,&iter1,&iter2); zfree(txline); return; } // convert text to bold text at line, positiion, cc void txwidget_bold_word(GtkWidget *txwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; ch *boldfont = zfuncs::appmonoboldfont; ch *txline, *pp1, *pp2; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; txline = txwidget_line(txwidget,line,0); if (! txline) return; fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont,0); /*** fontag = gtk_text_buffer_create_tag(textBuff,0,"font",boldfont, // example "foreground","red","background","light blue",0); ***/ pp1 = txline + posn; pp2 = pp1 + cc - 1; gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline); gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1); gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2); zfree(txline); return; } // convert text to underlined text at line, positiion, cc void txwidget_underline_word(GtkWidget *txwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; ch *txline, *pp1, *pp2; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; txline = txwidget_line(txwidget,line,0); if (! txline) return; fontag = gtk_text_buffer_create_tag(textBuff,0,"underline",PANGO_UNDERLINE_SINGLE,0); pp1 = txline + posn; pp2 = pp1 + cc - 1; gtk_text_buffer_get_iter_at_line_index(textBuff,&iter1,line,pp1-txline); gtk_text_buffer_get_iter_at_line_index(textBuff,&iter2,line,pp2-txline+1); gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2); zfree(txline); return; } // set font attributes for the entire txwidget (black on white) // this does not do anything to the text font - why? FIXME void txwidget_font_attributes(GtkWidget *txwidget) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; if (! main_thread()) zappcrash("illegal call from thread"); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txwidget)); if (! textBuff) return; fontag = gtk_text_buffer_create_tag(textBuff,0, // high contrast "font",appmonofont,"foreground","black","background","white",0); gtk_text_buffer_get_start_iter(textBuff,&iter1); gtk_text_buffer_get_end_iter(textBuff,&iter2); gtk_text_buffer_apply_tag(textBuff,fontag,&iter1,&iter2); return; } // set an event function for mouse and KB events in txwidget // + line selection via mouse click or keyboard up/down arrow key // + line and word selection via mouse click // // optional user callback function looks like this: // void userfunc(GtkWidget *txwidget, int line, int posn, ch *input) // receive clicked line and position (or) // dialog completion button (input) (or) // KB navigation key (input[0]) void txwidget_set_eventfunc(GtkWidget *txwidget, txwidget_CBfunc_t userfunc) { int txwidget_eventfunc(GtkWidget *txwidget, GdkEvent *event, txwidget_CBfunc_t userfunc); if (! main_thread()) zappcrash("illegal call from thread"); gtk_widget_add_events(txwidget,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(txwidget,GDK_KEY_PRESS_MASK); gtk_widget_add_events(txwidget,GDK_POINTER_MOTION_MASK); gtk_widget_add_events(txwidget,GDK_FOCUS_CHANGE_MASK); if (userfunc) { // 25.1 G_SIGNAL(txwidget,"key-press-event",txwidget_eventfunc,userfunc); G_SIGNAL(txwidget,"button-press-event",txwidget_eventfunc,userfunc); G_SIGNAL(txwidget,"motion-notify-event",txwidget_eventfunc,userfunc); G_SIGNAL(txwidget,"focus-in-event",txwidget_eventfunc,userfunc); } return; } // txwidget event function: // if no user callback function, process KB navigation keys (arrow, page, home/end) // if user callback func, send all KB keys to user callback function // process mouse clicks, send clicked line and position to user callback function int txwidget_eventfunc(GtkWidget *txwidget, GdkEvent *event, txwidget_CBfunc_t userfunc) { #define TEXT GTK_TEXT_WINDOW_TEXT #define VIEW GTK_TEXT_VIEW static GdkCursor *arrowcursor = 0; GdkWindow *gdkwin; GtkTextIter iter1; int ustat; int button, mpx, mpy, tbx, tby; int line, pos, vtop, vbott, page, KBkey; ch input[20] = ""; if (! arrowcursor) // first call, get arrow cursor arrowcursor = gdk_cursor_new_for_display(display,GDK_TOP_LEFT_ARROW); gdkwin = gtk_text_view_get_window(VIEW(txwidget),TEXT); // set arrow cursor for window if (gdkwin) gdk_window_set_cursor(gdkwin,arrowcursor); // (must reset every event) gtk_widget_grab_focus(txwidget); if (event->type == GDK_KEY_PRESS) // KB key press event { KBkey = ((GdkEventKey *) event)->keyval; if (userfunc) { // send arrow keys to userfunc strcpy(input,"xxx"); if (KBkey == GDK_KEY_Up) strcpy(input,"up"); if (KBkey == GDK_KEY_Down) strcpy(input,"down"); if (KBkey == GDK_KEY_Left) strcpy(input,"left"); if (KBkey == GDK_KEY_Right) strcpy(input,"right"); ustat = userfunc(txwidget,-1,-1,input); if (ustat) return 1; // handled by userfunc } if (KBkey > 0xff00) // navigation key { txwidget_get_visible_lines(txwidget,vtop,vbott); // range of lines on screen page = vbott - vtop - 2; // page size, lines if (page < 0) page = 0; line = 0; // default if (KBkey == GDK_KEY_Up) line = vtop - 1; // handle some navigation keys if (KBkey == GDK_KEY_Down) line = vbott + 1; if (KBkey == GDK_KEY_Page_Up) line = vtop - page; if (KBkey == GDK_KEY_Page_Down) line = vbott + page; if (KBkey == GDK_KEY_Home) line = 0; if (KBkey == GDK_KEY_End) line = 999999; if (line < 0) line = 0; txwidget_scroll(txwidget,line); // put line on screen return 1; } } if (! userfunc) return 1; if (event->type == GDK_BUTTON_PRESS) // mouse button press { button = ((GdkEventButton *) event)->button; // ignore if not left button if (button != 1) return 0; mpx = int(((GdkEventButton *) event)->x); // mouse click position mpy = int(((GdkEventButton *) event)->y); mpx -= appfontsize / 2; // more accurate if (mpx < 0) mpx = 0; gtk_text_view_window_to_buffer_coords(VIEW(txwidget),TEXT,mpx,mpy,&tbx,&tby); if (tbx && tby) { // can happen gtk_text_view_get_iter_at_location(VIEW(txwidget),&iter1,tbx,tby); line = gtk_text_iter_get_line(&iter1); // clicked txwidget line pos = gtk_text_iter_get_line_offset(&iter1); // clicked position } else line = pos = 0; userfunc(txwidget,line,pos,null); // pass line and posn to user func return 1; } return 0; } /******************************************************************************** simplified GTK menu bar, tool bar, status bar functions These functions simplify the creation of GTK menus and toolbars. The functionality is limited but adequate for most purposes. mbar = create_menubar(vbox) create menubar mitem = add_menubar_item(mbar, label, func) add menu item to menubar msub = add_submenu_item(mitem, label, func, tip) add submenu item to menu or submenu tbar = create_toolbar(vbox, iconsize) create toolbar add_toolbar_button(tbar, label, tip, icon, func) add button to toolbar stbar = create_stbar(vbox) create status bar stbar_message(stbar, message) display message in status bar These functions to the following: * create a menu bar and add to existing window vertical packing box * add menu item to menu bar * add submenu item to menu bar item or submenu item * create a toolbar and add to existing window * add button to toolbar, using stock icon or custom icon * create a status bar and add to existing window * display a message in the status bar argument definitions: vbox GtkWidget * a vertical packing box (in a window) mbar GtkWidget * reference for menu bar popup GtkWidget * reference for popup menu mitem GtkWidget * reference for menu item (in a menu bar) msub GtkWidget * reference for submenu item (in a menu) label ch * menu or toolbar name or label tbar GtkWidget * reference for toolbar tip ch * tool button tool tip (popup text via mouse-over) icon ch * stock icon name or custom icon file name (see below) func see below menu or tool button response function arg ch * argument to response function stbar int reference for status bar message ch * message to display in status bar The icon argument for the function add_toolbar_button() has two forms. For a GTK stock item referenced with a macro like GTK_STOCK_OPEN, use the corresponding text name, like "gtk-open". For a custom icon, use the icon's file name like "my-icon.png". The file is expected to be in get_zdatadir()/icons. The icon file may be any size, and is resized for use on the toolbar. If the file is not found, the stock icon "gtk-missing-image" is used (".png" and ".jpg" files both work). For a button with no icon (text label only), use 0 or null for the icon argument. For a menu separator, use the menu name "separator". For a toolbar separator, use the label "separator". For a title menu (no response function), set the response function to null. The response function for both menus and toolbar buttons looks like this: void func(GtkWidget *, ch *) The following macro is also supplied to simplify the coding of response functions: G_SIGNAL(window,event,func,arg) which expands to: g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) *********************************************************************************/ // create menu bar and add to vertical packing box GtkWidget * create_menubar(GtkWidget *vbox) { GtkWidget *wmbar; wmbar = gtk_menu_bar_new(); gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0); return wmbar; } // add menu item to menu bar, with optional response function GtkWidget * add_menubar_item(GtkWidget *wmbar, ch *mname, cbFunc func) { GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem); if (func) G_SIGNAL(wmitem,"activate",func,mname); return wmitem; } // add submenu item to menu item, with optional response function GtkWidget * add_submenu_item(GtkWidget *wmitem, ch *mlab, cbFunc func, ch *mtip) { GtkWidget *wmsub, *wmsubitem; wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem)); // add submenu if not already if (wmsub == null) { wmsub = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub); } if (strmatch(mlab,"separator")) wmsubitem = gtk_separator_menu_item_new(); else wmsubitem = gtk_menu_item_new_with_label(mlab); // add menu item with label only gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem); // append submenu item to submenu if (func) G_SIGNAL(wmsubitem,"activate",func,mlab); // connect optional response function if (mtip) g_object_set(G_OBJECT(wmsubitem),"tooltip-text",mtip,null); // add optional popup menu tip return wmsubitem; } /********************************************************************************/ // create toolbar and add to vertical packing box int tbIconSize = 32; // default if not supplied GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize) { GtkWidget *wtbar; wtbar = gtk_toolbar_new(); gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0); tbIconSize = iconsize; return wtbar; } // add toolbar button with label and icon ("iconfile.png") and tool tip // at least one of label and icon should be present GtkWidget * add_toolbar_button(GtkWidget *wtbar, ch *blab, ch *btip, ch *icon, cbFunc func) { GtkToolItem *tbutton; GError *gerror = 0; PIXBUF *pixbuf; GtkWidget *wicon = 0; ch iconpath[300], *pp; STATB statB; int err, cc; if (blab && strmatch(blab,"separator")) { tbutton = gtk_separator_tool_item_new(); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); return (GtkWidget *) tbutton; } if (icon && *icon) { // get icon pixbuf *iconpath = 0; strncatv(iconpath,199,zimagedir,"/",icon,null); err = stat(iconpath,&statB); if (err) { // alternative path cc = readlink("/proc/self/exe",iconpath,300); // get own program path if (cc > 0) iconpath[cc] = 0; // readlink() quirk pp = strrchr(iconpath,'/'); // folder of program if (pp) *pp = 0; strncatv(iconpath,300,"/icons/",icon,null); // .../icons/iconfile.png } pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,&gerror); if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf); } tbutton = gtk_tool_button_new(wicon,blab); if (! wicon) gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tbutton),"gtk-missing-image"); if (btip) gtk_tool_item_set_tooltip_text(tbutton,btip); gtk_tool_item_set_homogeneous(tbutton,0); gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1); if (func) G_SIGNAL(tbutton,"clicked",func,blab); return (GtkWidget *) tbutton; } /********************************************************************************/ // create a status bar and add to the start of a packing box GtkWidget * create_stbar(GtkWidget *pbox) { GtkWidget *stbar; stbar = gtk_statusbar_new(); gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0); gtk_widget_show(stbar); return stbar; } // display message in status bar int stbar_message(GtkWidget *wstbar, ch *message) { static int ctx = -1; if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all"); gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx); gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message); return 0; } /******************************************************************************** Popup Menu GtkWidget *popup, *mitem ch *label, *arg, *tip void func(GtkWidget *, ch *arg) popup = create_popmenu() create a popup menu mitem = add_popmenu(popup, label, func, arg, tip) add menu item to popup menu popup_menu(GtkWidget *parent, popup) popup the menu at mouse position Call 'create_popmenu' and then 'add_popmenu' for each item in the menu. 'label' is the menu name, 'func' the response function, 'arg' an argument for 'func', and 'tip' is a tool-tip. 'arg' and 'tip' may be null. A call to 'popup_menu' will show all menu entries at the mouse position. Clicking an entry will call the respective response function. Hovering on the entry will show the tool-tip. The response function looks like this: void func(GtkWidget *, ch *menu) ***/ // create a popup menu GtkWidget * create_popmenu() // event processing removed 24.60 { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *popmenu; popmenu = gtk_menu_new(); return popmenu; } // add a menu item to a popup menu void add_popmenu(GtkWidget *popmenu, ch *mname, cbFunc func, ch *arg, ch *mtip) { void popmenu_item_select(GtkWidget *, ch *mtip); GtkWidget *widget; widget = gtk_menu_item_new_with_label(mname); gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),widget); if (func) { if (arg) G_SIGNAL(widget,"activate",func,arg); // call func with arg else G_SIGNAL(widget,"activate",func,mname); // call func with menu name } if (mtip) { G_SIGNAL(widget,"select",popmenu_item_select,mtip); G_SIGNAL(widget,"deselect",popmenu_item_select,0); } return; } // show popup tip for selected menu item void popmenu_item_select(GtkWidget *wmitem, ch *mtip) // convoluted code but it works { GdkWindow *window; int xp, yp, mx, my; window = gtk_widget_get_window(wmitem); gdk_window_get_origin(window,&xp,&yp); // menu screen origin xp += gdk_window_get_width(window); // + width gdk_device_get_position(zfuncs::mouse,0,&mx,&my); // mouse (x,y) screen position poptext_screen(mtip,xp,my,0,5); // popup px = menu + width, py = mouse return; } // Show a popup menu at current mouse position // GtkWidget * argument is not used void popup_menu(GtkWidget *widget, GtkWidget *popmenu) { gtk_widget_show_all(popmenu); // GTK change: show before popup gtk_menu_popup_at_pointer(GTK_MENU(popmenu),null); return; } /********************************************************************************/ // popup picklist for user choice of zlist member ch * popup_choose_choice = 0; ch * popup_choose(zlist_t *zlist) // 24.70 { void popup_choose_choose(GtkWidget *, ch *); static GtkWidget *popmenu; static int busy = 0; int nl; ch *member; if (! main_thread()) zappcrash("illegal call from thread"); if (busy++) zappcrash("popup_choose() re-entry"); // 25.1 nl = zlist_count(zlist); if (! nl) { zmessageACK(mainwin,"popup_choose: empty list"); busy = 0; return 0; } popmenu = create_popmenu(); for (int ii = 0; ii < nl; ii++) { member = zlist_get(zlist,ii); if (member) add_popmenu(popmenu,member,popup_choose_choose,0,0); } add_popmenu(popmenu,"CLOSE",popup_choose_choose,0,0); if (popup_choose_choice) { // 25.1 zfree(popup_choose_choice); popup_choose_choice = 0; } popup_menu(0,popmenu); while (! popup_choose_choice) { if (! gtk_widget_get_mapped(popmenu)) popup_choose_choice = "NONE"; // escape if menu abandoned zmainsleep(0.2); } if (strmatch(popup_choose_choice,"NONE")) popup_choose_choice = 0; busy = 0; return popup_choose_choice; } // popup_choose response function void popup_choose_choose(GtkWidget *, ch *choice) { if (! choice || strmatch(choice,"CLOSE")) popup_choose_choice = zstrdup("NONE","zlist choose"); else popup_choose_choice = zstrdup(choice,"zlist choose"); return; } /********************************************************************************/ // popup picklist from a text file and choose an entry (text line) ch * popup_choose(ch *file) // 25.1 { static zlist_t *picklist = 0; ch *choice = 0; if (! main_thread()) zappcrash("illegal call from thread"); if (picklist) zlist_free(picklist); picklist = zlist_from_file(file); if (! picklist) { zmessageACK(mainwin,"picklist file not found: %s",file); return 0; } choice = popup_choose(picklist); return choice; } /******************************************************************************** Vertical Menu / Toolbar Build a custom vertical menu and/or toolbar in a vertical packing box Vmenu *vbm; ch *name, *icon, *desc, *arg; int iww, ihh; void func(GtkWidget *, ch *name); void RMfunc(GtkWidget *, ch *name); vbm = Vmenu_new(GtkWidget *vbox, float fgRGB[3], float bgRGB[3]); // create base menu Vmenu_add(vbm, name, icon, iww, ihh, desc, func, arg); // add left-mouse menu function Vmenu_add_setupfunc(vbm, me, func); // add opt. setup function Vmenu_add_RMfunc(vbm, me, func); // add right-mouse menu function Vmenu_block(int flag) 1 to block Vmenu, 0 to unblock Create a vertical menu / toolbar in a vertical packing box. fgRGB and bgRGB are font and background colors, RGB scaled 0-1.0 Added items can have a menu name, icon, description, response function, and function argument. 'name' and 'icon' can be null but not both. name menu name icon menu icon, filespec for a .png file iww, ihh size of icon in menu display desc optional tool tip if mouse is hovered over displayed menu When 'name/icon' is clicked, 'func' is called with 'arg'. If 'arg' is null, 'name' is used instead. To create a menu entry that is a popup menu with multiple entries, do as follows: popup = create_popmenu(); add_popup_menu_item(popup ...); see create_popmenu() add_popup_menu_item(popup ...); ... Vmenu_add(vbm, name, icon, ww, hh, desc, create_popmenu, (ch *) popup); i.e. use create_popmenu() as the response function and use the previously created menu 'popup' as the argument (cast to ch *). ***/ namespace Vmenunames { #define margin 5 // margins for menu text PangoFontDescription *pfont1, *pfont2; PangoAttrList *pattrlist; PangoAttribute *pbackground; int fontheight; int Fblock = 0; void wpaint(GtkWidget *, cairo_t *, Vmenu *); // window repaint - draw event void mouse_event(GtkWidget *, GdkEventButton *, Vmenu *); // mouse event function void paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite); // paint menu entry, opt. highlight } // create Vmenu Vmenu *Vmenu_new(GtkWidget *vbox, float fgRGB[3], float bgRGB[3]) { using namespace Vmenunames; int cc, ww, hh; int K64 = 65536; ch *menufont1, *menufont2; PangoLayout *playout; cc = sizeof(Vmenu); Vmenu *vbm = (Vmenu *) zmalloc(cc,"Vmenu"); vbm->fgRGB[0] = fgRGB[0]; // background color, RGB 0-1.0 vbm->fgRGB[1] = fgRGB[1]; vbm->fgRGB[2] = fgRGB[2]; vbm->bgRGB[0] = bgRGB[0]; // background color, RGB 0-1.0 vbm->bgRGB[1] = bgRGB[1]; vbm->bgRGB[2] = bgRGB[2]; vbm->vbox = vbox; vbm->topwin = gtk_widget_get_toplevel(vbox); vbm->layout = gtk_layout_new(0,0); vbm->mcount = 0; gtk_box_pack_start(GTK_BOX(vbox),vbm->layout,1,1,0); vbm->xmax = vbm->ymax = 10; // initial layout size pattrlist = pango_attr_list_new(); pbackground = pango_attr_background_new(K64*bgRGB[0],K64*bgRGB[1],K64*bgRGB[2]); pango_attr_list_change(pattrlist,pbackground); menufont1 = zstrdup(zfuncs::appfont,"Vmenu"); // set menu fonts, normal and bold menufont2 = zstrdup(zfuncs::appboldfont,"Vmenu"); pfont1 = pango_font_description_from_string(menufont1); pfont2 = pango_font_description_from_string(menufont2); playout = gtk_widget_create_pango_layout(vbm->layout,0); pango_layout_set_font_description(playout,pfont1); pango_layout_set_text(playout,"Ayg",-1); pango_layout_get_pixel_size(playout,&ww,&hh); fontheight = hh; gtk_widget_add_events(vbm->layout,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(vbm->layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(vbm->layout,GDK_POINTER_MOTION_MASK); gtk_widget_add_events(vbm->layout,GDK_LEAVE_NOTIFY_MASK); G_SIGNAL(vbm->layout,"button-press-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"button-release-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"motion-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"leave-notify-event",mouse_event,vbm); G_SIGNAL(vbm->layout,"draw",wpaint,vbm); return vbm; } // add Vmenu entry with name, icon, description, menu function (left click) void Vmenu_add(Vmenu *vbm, ch *name, ch *icon, int iconww, int iconhh, ch *desc, cbFunc func, ch *arg) { using namespace Vmenunames; int me, cc, xpos, ww, hh; ch iconpath[200], *mdesc, *name__; ch *blanks = " "; // 20 blanks PIXBUF *pixbuf; GError *gerror = 0; PangoLayout *playout; PangoFontDescription *pfont; if (! name && ! icon) return; me = vbm->mcount++; // track no. menu entries if (name) vbm->menu[me].name = zstrdup(name,"Vmenu"); // create new menu entry from caller data if (icon) { vbm->menu[me].icon = zstrdup(icon,"Vmenu"); vbm->menu[me].iconww = iconww; vbm->menu[me].iconhh = iconhh; } if (desc) { // pad description with blanks for looks cc = strlen(desc); mdesc = (ch *) zmalloc(cc+3,"Vmenu"); mdesc[0] = ' '; strcpy(mdesc+1,desc); strcpy(mdesc+cc+1," "); vbm->menu[me].desc = mdesc; } vbm->menu[me].LMfunc = func; // left-mouse menu function vbm->menu[me].arg = name; // argument is menu name or arg if avail. if (arg) vbm->menu[me].arg = arg; vbm->menu[me].pixbuf = 0; if (icon) { // if icon is named, get pixbuf *iconpath = 0; strncatv(iconpath,199,zfuncs::zimagedir,"/",icon,null); pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,iconww,iconhh,1,&gerror); if (pixbuf) vbm->menu[me].pixbuf = pixbuf; else Plog(1,"Vmenu no icon: %s \n",iconpath); } if (me == 0) vbm->ymax = margin; // first menu, top position vbm->menu[me].iconx = 0; vbm->menu[me].icony = 0; vbm->menu[me].namex = 0; vbm->menu[me].namey = 0; if (icon) { vbm->menu[me].iconx = margin; // ______ vbm->menu[me].icony = vbm->ymax; // | | if (name) { // | icon | menu name vbm->menu[me].namex = margin + iconww + margin; // |______| vbm->menu[me].namey = vbm->ymax + (iconhh - fontheight) / 2; // } vbm->menu[me].ylo = vbm->ymax; vbm->ymax += iconhh + iconhh / 8; // position for next menu entry vbm->menu[me].yhi = vbm->ymax; if (margin + iconww > vbm->xmax) vbm->xmax = margin + iconww; // keep track of max. layout width } else if (name) { vbm->menu[me].namex = margin; // menu name vbm->menu[me].namey = vbm->ymax; vbm->menu[me].ylo = vbm->ymax; vbm->ymax += 1.5 * fontheight; // more space between text lines vbm->menu[me].yhi = vbm->ymax; } vbm->menu[me].playout1 = gtk_widget_create_pango_layout(vbm->layout,0); vbm->menu[me].playout2 = gtk_widget_create_pango_layout(vbm->layout,0); if (name) { xpos = vbm->menu[me].namex; cc = strlen(name); // menu name with trailing blanks name__ = zstrdup(name,"Vmenu",22); // (long enough to overwrite bold name) strncpy0(name__+cc,blanks,20); playout = vbm->menu[me].playout1; // normal font pfont = pfont1; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name__,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout playout = vbm->menu[me].playout2; // bold font pfont = pfont2; pango_layout_set_attributes(playout,pattrlist); pango_layout_set_font_description(playout,pfont); pango_layout_set_text(playout,name,-1); // compute layout pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout if (xpos + ww > vbm->xmax) vbm->xmax = xpos + ww; // keep track of max. layout width } gtk_widget_set_size_request(vbm->layout,vbm->xmax+margin,0); // add right margin to layout width return; } // add opt. setup function to existing menu entry void Vmenu_add_setup(Vmenu *vbm, int me, cbFunc func, ch *arg) { if (me > vbm->mcount-1) zappcrash("Vmenu_add_RMfunc() bad me: %d",me); vbm->menu[me].setupfunc = func; vbm->menu[me].setuparg = arg; return; } // add alternate function for right-mouse click void Vmenu_add_RMfunc(Vmenu *vbm, int me, cbFunc func, ch *arg) { if (me > vbm->mcount-1) zappcrash("Vmenu_add_RMfunc() bad me: %d",me); vbm->menu[me].RMfunc = func; vbm->menu[me].RMarg = arg; return; } // block or unblock menu void Vmenu_block(int flag) { using namespace Vmenunames; Fblock = flag; return; } // paint window when created, exposed, resized void Vmenunames::wpaint(GtkWidget *widget, cairo_t *cr, Vmenu *vbm) { using namespace Vmenunames; cairo_set_source_rgb(cr,vbm->bgRGB[0],vbm->bgRGB[1],vbm->bgRGB[2]); // background cairo_paint(cr); for (int me = 0; me < vbm->mcount; me++) // paint all menu entries paint_menu(cr,vbm,me,0); return; } // draw menu icon and text into layout void Vmenunames::paint_menu(cairo_t *cr, Vmenu *vbm, int me, int hilite) { using namespace Vmenunames; PIXBUF *pixbuf; PangoLayout *playout; int xpos, ypos; int iconww, iconhh; ch *name; pixbuf = vbm->menu[me].pixbuf; // icon if (pixbuf) { // draw menu icon at menu position xpos = vbm->menu[me].iconx; ypos = vbm->menu[me].icony; iconww = vbm->menu[me].iconww; iconhh = vbm->menu[me].iconhh; if (! hilite) { // erase box around icon cairo_set_source_rgb(cr,vbm->bgRGB[0],vbm->bgRGB[1],vbm->bgRGB[2]); // background cairo_rectangle(cr,xpos-1,ypos-1,iconww+2,iconhh+2); cairo_fill(cr); } gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); // draw icon cairo_paint(cr); if (hilite) { cairo_set_source_rgb(cr,vbm->fgRGB[0],vbm->fgRGB[1],vbm->fgRGB[2]); // draw box around icon cairo_set_line_width(cr,1); cairo_set_line_join(cr,CAIRO_LINE_JOIN_ROUND); cairo_rectangle(cr,xpos,ypos,iconww,iconhh); cairo_stroke(cr); } } name = vbm->menu[me].name; // menu text if (name) { // draw menu text at menu position xpos = vbm->menu[me].namex; ypos = vbm->menu[me].namey; cairo_move_to(cr,xpos,ypos); // draw layout with text cairo_set_source_rgb(cr,vbm->fgRGB[0],vbm->fgRGB[1],vbm->fgRGB[2]); if (hilite) playout = vbm->menu[me].playout2; else playout = vbm->menu[me].playout1; pango_cairo_show_layout(cr,playout); } return; } // mouse event function - capture buttons and drag movements void Vmenunames::mouse_event(GtkWidget *widget, GdkEventButton *event, Vmenu *vbm) { using namespace Vmenunames; GdkWindow *gdkwin; ch *desc; int me, mpx, mpy, button, ww, ylo, yhi; static int me0 = -1, Fmyclick = 0, winww = 0; static draw_context_t context; static GtkWidget *pwidget = 0; static cairo_t *cr = 0; if (widget != pwidget) { // widget changed if (pwidget) draw_context_destroy(context); gdkwin = gtk_layout_get_bin_window(GTK_LAYOUT(widget)); cr = draw_context_create(gdkwin,context); gdkwin = gtk_widget_get_window(widget); // get width of menu widget winww = gdk_window_get_width(gdkwin); pwidget = widget; } mpx = int(event->x); // mouse position mpy = int(event->y); button = event->button; if (button == 1 && (event->state & GDK_MOD1_MASK)) button = 3; // left butt + ALT key >> right butt if (event->type == GDK_MOTION_NOTIFY) // mouse inside layout { for (me = 0; me < vbm->mcount; me++) { // find menu where mouse is ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me != me0 && me0 >= 0) { // unhighlight prior paint_menu(cr,vbm,me0,0); me0 = -1; } if (me == me0) return; // same as before if (me == vbm->mcount) return; // no new menu match paint_menu(cr,vbm,me,1); // highlight menu entry at mouse desc = vbm->menu[me].desc; // show tool tip if (desc) poptext_widget(widget,desc,winww,mpy,0,5); // px = menu width, py = mouse 25.0 me0 = me; // remember last match return; } if (me0 >= 0) // mouse left layout { poptext_killnow(); // 25.0 paint_menu(cr,vbm,me0,0); // unhighlight prior me0 = -1; } if (event->type == GDK_BUTTON_PRESS) // menu entry clicked Fmyclick = 1; // button click is mine if (event->type == GDK_BUTTON_RELEASE) // menu entry clicked { if (Fblock) return; // menu is blocked if (! Fmyclick) return; // ignore unmatched button release Fmyclick = 0; // (from vanished popup window) for (me = 0; me < vbm->mcount; me++) { // look for clicked menu entry ylo = vbm->menu[me].ylo; yhi = vbm->menu[me].yhi; if (mpy < ylo) continue; if (mpy < yhi) break; } if (me == vbm->mcount) return; // no menu match zfuncs::vmenuclickbutton = button; // 1/2/3 = left/mid/right button ww = vbm->menu[me].iconww; // get horiz. click posn. on menu icon if (ww) mpx = 100 * (mpx - margin) / ww; // scale 0-100 else mpx = 0; if (mpx < 0) mpx = 0; if (mpx > 100) mpx = 100; zfuncs::vmenuclickposn = mpx; paint_menu(cr,vbm,me,0); // unhighlight menu if (vbm->menu[me].setupfunc) { // call opt. setup function vbm->menu[me].setupfunc(widget,vbm->menu[me].setuparg); if (vmenustop) { vmenustop = 0; return; } } if (button == 3 && vbm->menu[me].RMfunc) // if right mouse button, vbm->menu[me].RMfunc(widget,vbm->menu[me].RMarg); // call right-mouse function else if (vbm->menu[me].LMfunc) // call left-mouse menu function vbm->menu[me].LMfunc(widget,vbm->menu[me].arg); } return; } /******************************************************************************** spline curve setup and edit functions support multiple frames with multiple curves sd = splcurve_init(frame,callback_func) add draw area widget in dialog frame widget sd->Nspc = n Initialize no. of curves in frame sd->fact[spc] = 1 Initialize active flag for curve spc sd->vert[spc] = hv Initialize vert/horz flag for curve spc sd->nap[spc], sd->apx[spc][xx], sd->apy[spc][yy] Initialize anchor points for curve spc splcurve_generate(sd,spc) Generate data for curve spc Curves will now be shown inside the frame when window is realized. The callback_func(spc) will be called when curve spc is edited (mouse drag). Change curve in program: set anchor points, call splcurve_generate(sd,spc). yval = splcurve_yval(sd,spc,xval) Get y-value (0-1) for curve spc and given x-value (0-1) kk = 1000 * xval; If faster access to curve is needed (no interpolation) if (kk > 999) kk = 999; yval = sd->yval[spc][kk]; ***/ // initialize for spline curve editing // initial anchor points are pre-loaded into spldat before window is realized spldat * splcurve_init(GtkWidget *frame, void func(int spc)) { int cc = sizeof(spldat); // allocate spc curve data area spldat * sd = (spldat *) zmalloc(cc,"splcurve"); memset(sd,0,cc); sd->drawarea = gtk_drawing_area_new(); // drawing area for curves gtk_container_add(GTK_CONTAINER(frame),sd->drawarea); sd->spcfunc = func; // user callback function gtk_widget_add_events(sd->drawarea,GDK_BUTTON_PRESS_MASK); // connect mouse events to drawing area gtk_widget_add_events(sd->drawarea,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(sd->drawarea,GDK_BUTTON1_MOTION_MASK); G_SIGNAL(sd->drawarea,"motion-notify-event",splcurve_adjust,sd); G_SIGNAL(sd->drawarea,"button-press-event",splcurve_adjust,sd); G_SIGNAL(sd->drawarea,"realize",splcurve_resize,sd); G_SIGNAL(sd->drawarea,"draw",splcurve_draw,sd); return sd; } // modify anchor points in curve using mouse int splcurve_adjust(void *, GdkEventButton *event, spldat *sd) { int ww, hh, kk; int mx, my, button, evtype; static int spc, ap, mbusy = 0, Fdrag = 0; // drag continuation logic int minspc, minap, apset = 0; float mxval, myval, cxval, cyval; float dist2, mindist2 = 0; float dist, dx, dy; float minx = 0.01 * splcurve_minx; // % to absolute distance mx = event->x; // mouse position in drawing area my = event->y; evtype = event->type; button = event->button; if (evtype == GDK_MOTION_NOTIFY) { if (mbusy) return 0; // discard excess motion events mbusy++; zmainloop(); mbusy = 0; } if (evtype == GDK_BUTTON_RELEASE) { Fdrag = 0; return 0; } ww = gtk_widget_get_allocated_width(sd->drawarea); // drawing area size hh = gtk_widget_get_allocated_height(sd->drawarea); if (mx < 0) mx = 0; // limit edge excursions if (mx > ww) mx = ww; if (my < 0) my = 0; if (my > hh) my = hh; if (evtype == GDK_BUTTON_PRESS) Fdrag = 0; // left or right click if (Fdrag) // continuation of drag { if (sd->vert[spc]) { mxval = 1.0 * my / hh; // mouse position in curve space myval = 1.0 * mx / ww; } else { mxval = 1.0 * mx / ww; myval = 1.0 * (hh - my) / hh; } if (ap < sd->nap[spc] - 1) { // not the last anchor point dx = sd->apx[spc][ap+1] - mxval; // get distance to next anchor point if (dx < 0.01) return 0; // x-value not increasing, forbid dy = sd->apy[spc][ap+1] - myval; dist = sqrtf(dx * dx + dy * dy); if (dist < minx) return 0; // too close, forbid } if (ap > 0) { // not the first anchor point dx = mxval - sd->apx[spc][ap-1]; // get distance to prior anchor point if (dx < 0.01) return 0; // x-value not increasing, forbid dy = myval - sd->apy[spc][ap-1]; dist = sqrtf(dx * dx + dy * dy); if (dist < minx) return 0; // too close, forbid } apset = 1; // mxval/myval = new node position } else // mouse click or new drag begin { minspc = minap = -1; // find closest curve/anchor point mindist2 = 999999; for (spc = 0; spc < sd->Nspc; spc++) // loop curves { if (! sd->fact[spc]) continue; // not active if (sd->vert[spc]) { mxval = 1.0 * my / hh; // mouse position in curve space myval = 1.0 * mx / ww; } else { mxval = 1.0 * mx / ww; myval = 1.0 * (hh - my) / hh; } for (ap = 0; ap < sd->nap[spc]; ap++) // loop anchor points { cxval = sd->apx[spc][ap]; cyval = sd->apy[spc][ap]; dist2 = (mxval-cxval)*(mxval-cxval) + (myval-cyval)*(myval-cyval); if (dist2 < mindist2) { mindist2 = dist2; // remember closest anchor point minspc = spc; minap = ap; } } } if (minspc < 0) return 0; // impossible spc = minspc; // nearest curve ap = minap; // nearest anchor point } if (evtype == GDK_BUTTON_PRESS && button == 3) // right click, remove anchor point { if (sqrtf(mindist2) > minx) return 0; // not close enough if (sd->nap[spc] < 3) return 0; // < 2 anchor points would remain sd->nap[spc]--; // decr. before loop for (kk = ap; kk < sd->nap[spc]; kk++) { sd->apx[spc][kk] = sd->apx[spc][kk+1]; sd->apy[spc][kk] = sd->apy[spc][kk+1]; } splcurve_generate(sd,spc); // regenerate data for modified curve gtk_widget_queue_draw(sd->drawarea); sd->spcfunc(spc); // call user function return 0; } if (! Fdrag) // new drag or left click { if (sd->vert[spc]) { mxval = 1.0 * my / hh; // mouse position in curve space myval = 1.0 * mx / ww; } else { mxval = 1.0 * mx / ww; myval = 1.0 * (hh - my) / hh; } if (sqrtf(mindist2) < minx) // anchor point close enough, { // move this one to mouse position if (ap < sd->nap[spc]-1) { // not the last anchor point dx = sd->apx[spc][ap+1] - mxval; // get distance to next anchor point if (dx < 0.01) return 0; // x-value not increasing, forbid dy = sd->apy[spc][ap+1] - myval; dist = sqrtf(dx * dx + dy * dy); if (dist < minx) return 0; // too close, forbid } if (ap > 0) { // not the first anchor point dx = mxval - sd->apx[spc][ap-1]; // get distance to prior anchor point if (dx < 0.01) return 0; // x-value not increasing, forbid dy = myval - sd->apy[spc][ap-1]; dist = sqrtf(dx * dx + dy * dy); if (dist < minx) return 0; // too close, forbid } apset = 1; // mxval/myval = new node position } else // none close, add new anchor point { minspc = -1; // find closest curve to mouse mindist2 = 999999; for (spc = 0; spc < sd->Nspc; spc++) // loop curves { if (! sd->fact[spc]) continue; // not active if (sd->vert[spc]) { mxval = 1.0 * my / hh; // mouse position in curve space myval = 1.0 * mx / ww; } else { mxval = 1.0 * mx / ww; myval = 1.0 * (hh - my) / hh; } cyval = splcurve_yval(sd,spc,mxval); dist2 = fabsf(myval - cyval); if (dist2 < mindist2) { mindist2 = dist2; // remember closest curve minspc = spc; } } if (minspc < 0) return 0; // impossible if (mindist2 > minx) return 0; // not close enough to any curve spc = minspc; if (sd->nap[spc] > 49) { zmessageACK(mainwin,"Exceed 50 anchor points"); return 0; } if (sd->vert[spc]) { mxval = 1.0 * my / hh; // mouse position in curve space myval = 1.0 * mx / ww; } else { mxval = 1.0 * mx / ww; myval = 1.0 * (hh - my) / hh; } for (ap = 0; ap < sd->nap[spc]; ap++) // find anchor point with next higher x if (mxval <= sd->apx[spc][ap]) break; // (ap may come out 0 or nap) if (ap < sd->nap[spc] && sd->apx[spc][ap] - mxval < minx) // disallow < minx from next return 0; // or prior anchor point if (ap > 0 && mxval - sd->apx[spc][ap-1] < minx) return 0; for (kk = sd->nap[spc]; kk > ap; kk--) { // make hole for new point sd->apx[spc][kk] = sd->apx[spc][kk-1]; sd->apy[spc][kk] = sd->apy[spc][kk-1]; } sd->nap[spc]++; // up point count apset = 1; // mxval/myval = new node position } } if (evtype == GDK_MOTION_NOTIFY) Fdrag = 1; // remember drag is underway if (apset) { sd->apx[spc][ap] = mxval; // new or moved anchor point sd->apy[spc][ap] = myval; // at mouse position splcurve_generate(sd,spc); // regenerate data for modified curve if (sd->drawarea) gtk_widget_queue_draw(sd->drawarea); // redraw graph if (sd->spcfunc) sd->spcfunc(spc); // call user function } return 0; } // add a new anchor point to a curve // spc: curve number // px, py: node coordinates in the range 0-1 int splcurve_addnode(spldat *sd, int spc, float px, float py) { int ap, kk; float minx = 0.01 * splcurve_minx; // % to absolute distance for (ap = 0; ap < sd->nap[spc]; ap++) // find anchor point with next higher x if (px <= sd->apx[spc][ap]) break; // (ap may come out 0 or nap) if (ap < sd->nap[spc] && sd->apx[spc][ap] - px < minx) // disallow < minx from next return 0; // or prior anchor point if (ap > 0 && px - sd->apx[spc][ap-1] < minx) return 0; for (kk = sd->nap[spc]; kk > ap; kk--) { // make hole for new point sd->apx[spc][kk] = sd->apx[spc][kk-1]; sd->apy[spc][kk] = sd->apy[spc][kk-1]; } sd->apx[spc][ap] = px; // add node coordinates sd->apy[spc][ap] = py; sd->nap[spc]++; // up point count return 1; } // if height/width too small, make bigger int splcurve_resize(GtkWidget *drawarea) { int ww, hh; gtk_widget_get_size_request(drawarea,&ww,&hh); if (hh < 50) gtk_widget_set_size_request(drawarea,ww,50); return 1; } // for expose event or when a curve is changed // draw all curves based on current anchor points int splcurve_draw(GtkWidget *drawarea, cairo_t *cr, spldat *sd) { int ww, hh, spc, ap; float xval, yval, px, py, qx, qy; if (! sd || ! sd->drawarea) return 0; // 25.0 ww = gtk_widget_get_allocated_width(sd->drawarea); // drawing area size hh = gtk_widget_get_allocated_height(sd->drawarea); if (ww < 50 || hh < 50) return 0; cairo_set_line_width(cr,1); cairo_set_source_rgb(cr,0.7,0.7,0.7); for (int ii = 0; ii < sd->Nscale; ii++) // draw y-scale lines if any { px = ww * sd->xscale[0][ii]; py = hh - hh * sd->yscale[0][ii]; qx = ww * sd->xscale[1][ii]; qy = hh - hh * sd->yscale[1][ii]; cairo_move_to(cr,px,py); cairo_line_to(cr,qx,qy); } cairo_stroke(cr); cairo_set_source_rgb(cr,0,0,0); for (spc = 0; spc < sd->Nspc; spc++) // loop all curves { if (! sd->fact[spc]) continue; // not active if (sd->vert[spc]) // vert. curve { for (py = 0; py < hh; py++) // generate all points for curve { xval = 1.0 * py / hh; yval = splcurve_yval(sd,spc,xval); px = ww * yval; if (py == 0) cairo_move_to(cr,px,py); cairo_line_to(cr,px,py); } cairo_stroke(cr); for (ap = 0; ap < sd->nap[spc]; ap++) // draw boxes at anchor points { xval = sd->apx[spc][ap]; yval = sd->apy[spc][ap]; px = ww * yval; py = hh * xval; cairo_rectangle(cr,px-2,py-2,4,4); } cairo_fill(cr); } else // horz. curve { for (px = 0; px < ww; px++) // generate all points for curve { xval = 1.0 * px / ww; yval = splcurve_yval(sd,spc,xval); py = hh - hh * yval; if (px == 0) cairo_move_to(cr,px,py); cairo_line_to(cr,px,py); } cairo_stroke(cr); for (ap = 0; ap < sd->nap[spc]; ap++) // draw boxes at anchor points { xval = sd->apx[spc][ap]; yval = sd->apy[spc][ap]; px = ww * xval; py = hh - hh * yval; cairo_rectangle(cr,px-2,py-2,4,4); } cairo_fill(cr); } } return 0; } // generate all curve data points when anchor points are modified int splcurve_generate(spldat *sd, int spc) { int kk, kklo, kkhi; float xval, yvalx; spline1(sd->nap[spc],sd->apx[spc],sd->apy[spc]); // compute curve fitting anchor points kklo = 1000 * sd->apx[spc][0] - 30; // xval range = anchor point range if (kklo < 0) kklo = 0; // + 0.03 extra below/above kkhi = 1000 * sd->apx[spc][sd->nap[spc]-1] + 30; if (kkhi > 1000) kkhi = 1000; for (kk = 0; kk < 1000; kk++) // generate all points for curve { xval = 0.001 * kk; // remove anchor point limits yvalx = spline2(xval); if (yvalx < 0) yvalx = 0; // yval < 0 not allowed, > 1 OK sd->yval[spc][kk] = yvalx; } sd->mod[spc] = 1; // mark curve modified return 0; } // Retrieve curve data using interpolation of saved table of values float splcurve_yval(spldat *sd, int spc, float xval) { int ii; float x1, x2, y1, y2, y3; if (xval <= 0) return sd->yval[spc][0]; if (xval >= 0.999) return sd->yval[spc][999]; x2 = 1000.0 * xval; ii = x2; x1 = ii; y1 = sd->yval[spc][ii]; y2 = sd->yval[spc][ii+1]; y3 = y1 + (y2 - y1) * (x2 - x1); return y3; } // load curve data from a file // returns 0 if success, sd is initialized from file data // returns 1 if fail (invalid file data), sd not modified int splcurve_load(spldat *sd, FILE *fid) { ch *pp, buff[300]; int nn, ii, jj, err, myfid = 0; int Nspc, fact[10], vert[10], nap[10]; float apx[10][50], apy[10][50]; pp = fgets_trim(buff,300,fid,1); if (! pp) goto fail; nn = sscanf(pp,"%d",&Nspc); // no. of curves if (nn != 1) goto fail; if (Nspc < 1 || Nspc > 10) goto fail; if (Nspc != sd->Nspc) goto fail; for (ii = 0; ii < Nspc; ii++) // loop each curve { pp = fgets_trim(buff,300,fid,1); if (! pp) goto fail; nn = sscanf(pp,"%d %d %d",&fact[ii],&vert[ii],&nap[ii]); // active flag, vert flag, anchors if (nn != 3) goto fail; if (fact[ii] < 0 || fact[ii] > 1) goto fail; if (vert[ii] < 0 || vert[ii] > 1) goto fail; if (nap[ii] < 2 || nap[ii] > 50) goto fail; pp = fgets_trim(buff,300,fid,1); // anchor points: nnn/nnn nnn/nnn ... for (jj = 0; jj < nap[ii]; jj++) // anchor point values { pp = (ch *) substring(buff,"/ ",2*jj+1); if (! pp) goto fail; err = convSF(pp,apx[ii][jj],0,1); if (err) goto fail; pp = (ch *) substring(buff,"/ ",2*jj+2); if (! pp) goto fail; err = convSF(pp,apy[ii][jj],0,1); if (err) goto fail; } } if (myfid) fclose(fid); sd->Nspc = Nspc; // copy curve data to caller's arg for (ii = 0; ii < Nspc; ii++) { sd->fact[ii] = fact[ii]; sd->vert[ii] = vert[ii]; sd->nap[ii] = nap[ii]; for (jj = 0; jj < nap[ii]; jj++) { sd->apx[ii][jj] = apx[ii][jj]; sd->apy[ii][jj] = apy[ii][jj]; } } for (ii = 0; ii < Nspc; ii++) // generate curve data from anchor points splcurve_generate(sd,ii); if (sd->drawarea) // redraw all curves gtk_widget_queue_draw(sd->drawarea); return 0; // success fail: if (fid && myfid) fclose(fid); zmessageACK(mainwin,"curve file is invalid"); return 1; } // save curve data to a file int splcurve_save(spldat *sd, FILE *fid) { int ii, jj, myfid = 0; fprintf(fid,"%d \n",sd->Nspc); // no. of curves for (ii = 0; ii < sd->Nspc; ii++) // loop each curve { fprintf(fid,"%d %d %d \n",sd->fact[ii],sd->vert[ii],sd->nap[ii]); // active flag, vert flag, anchors for (jj = 0; jj < sd->nap[ii]; jj++) // anchor point values fprintf(fid,"%.4f/%.4f ",sd->apx[ii][jj],sd->apy[ii][jj]); fprintf(fid,"\n"); } if (myfid) fclose(fid); return 0; } /******************************************************************************** simplified GTK dialog functions create a dialog with response buttons (OK, cancel ...) add widgets to dialog (button, text entry, combo box ...) put data into widgets (text or numeric data) run the dialog, get user inputs (button press, text entry, checkbox selection ...) run caller event function when widgets change from user inputs run caller event function when dialog is completed or canceled get dialog completion status (OK, cancel, destroyed ...) get data from dialog widgets (the user inputs) destroy the dialog and free memory *********************************************************************************/ // private functions for widget events and dialog completion int zdialog_widget_event(GtkWidget *, zdialog *zd); int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd); int zdialog_zspin_event(GtkWidget *, GdkEvent *, zdialog *zd); // "zspin" widget // create a new zdialog dialog // The title and parent arguments may be null. // optional arguments: up to zdmaxbutts button labels followed by null // returned dialog status: +N = button N (1 to zdmaxbutts) // <0 = [x] button or other GTK destroy action // completion buttons are also events like other widgets // all dialogs run parallel, use zdialog_wait() if needed // The status returned by zdialog_wait() corresponds to the button // (1-10) used to end the dialog. Status < 0 indicates the [x] button // was used or the dialog was killed by the program itself. zdialog * zdialog_new(ch *title, GtkWidget *parent, ...) // parent added { zdialog *zd; GtkWidget *dialog, *hbox, *vbox, *butt, *hsep; ch *bulab[zdmaxbutts]; int cc, ii, nbu; va_list arglist; static int uniqueID = 1; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,parent); for (nbu = 0; nbu < zdmaxbutts; nbu++) { // get completion buttons bulab[nbu] = va_arg(arglist, ch *); if (! bulab[nbu] || bulab[nbu] == (ch *) 0x100000000) break; // ARM bug } va_end(arglist); if (! title) title = ""; dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(dialog),title); vbox = gtk_box_new(VERTICAL,0); // vertical packing box gtk_container_add(GTK_CONTAINER(dialog),vbox); // add to main window gtk_window_set_default_size(GTK_WINDOW(dialog),10,10); // stop auto width of 150 pixels if (parent) gtk_window_set_transient_for(GTK_WINDOW(dialog),GTK_WINDOW(parent)); gtk_box_set_spacing(GTK_BOX(vbox),2); gtk_container_set_border_width(GTK_CONTAINER(vbox),5); cc = sizeof(zdialog); // allocate zdialog zd = (zdialog *) zmalloc(cc,"zdialog"); if (zdialog_count == zdialog_max) { // add to active list for (ii = 0; ii < zdialog_count; ii++) Plog(0,"dialog: %s \n",zdialog_list[ii]->widget[0].data); zappcrash("max. zdialogs exceeded"); } zdialog_list[zdialog_count] = zd; zdialog_count++; if (nbu) { // completion buttons hbox = gtk_box_new(HORIZONTAL,2); // add hbox for buttons at bottom gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,2); hsep = gtk_separator_new(HORIZONTAL); // add separator line gtk_box_pack_end(GTK_BOX(vbox),hsep,0,0,5); for (ii = nbu-1; ii >= 0; ii--) { // add buttons to hbox butt = gtk_button_new_with_label(bulab[ii]); // reverse order nbu-1...0 gtk_box_pack_end(GTK_BOX(hbox),butt,0,0,2); G_SIGNAL(butt,"clicked",zdialog_widget_event,zd); // connect to event function zd->compwidget[ii] = butt; // save button widgets zd->compbutton[ii] = bulab[ii]; // and button labels } } zd->compbutton[nbu] = 0; // mark EOL zd->dialog = dialog; // dialog window zd->title = zstrdup(title,"zdialog"); // dialog title zd->parent = parent; // parent window zd->sentinel1 = zdsentinel | (lrandz() & 0x0000FFFF); // validity sentinels zd->sentinel2 = zd->sentinel1; // fixed part + random part zd->uniqueID = uniqueID++; // increment unique ID zd->eventCB = 0; // no user event callback function zd->zstat = 0; // no zdialog status zd->disabled = 1; // widget signals disabled zd->saveposn = 0; // position not saved zd->widget[0].wname = "dialog"; // set up 1st widget = dialog zd->widget[0].type = "dialog"; zd->widget[0].pname = 0; // no parent zd->widget[0].data = zstrdup(title,"zdialog"); zd->widget[0].widget = dialog; zd->widget[1].type = 0; // eof - no contained widgets yet return zd; } // change a zdialog title void zdialog_set_title(zdialog *zd, ch *title) { if (! main_thread()) zappcrash("illegal call from thread"); gtk_window_set_title(GTK_WINDOW(zd->widget[0].widget),title); return; } // set a zdialog to be modal void zdialog_set_modal(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *widget = zdialog_gtkwidget(zd,"dialog"); gtk_window_set_modal(GTK_WINDOW(widget),1); gtk_window_set_keep_above(GTK_WINDOW(widget),1); return; } // set a zdialog to be decorated or not void zdialog_set_decorated(zdialog *zd, int decorated) { void zdialog_drag(GtkWidget *widget, GdkEventButton *event, void *); GtkWidget *widget; if (! main_thread()) zappcrash("illegal call from thread"); widget = zdialog_gtkwidget(zd,"dialog"); gtk_window_set_decorated(GTK_WINDOW(widget),decorated); if (decorated) return; gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(widget,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(widget,GDK_POINTER_MOTION_MASK); G_SIGNAL(widget,"button-press-event",zdialog_drag,0); // connect mouse events to drag G_SIGNAL(widget,"button-release-event",zdialog_drag,0); // undecorated window G_SIGNAL(widget,"motion-notify-event",zdialog_drag,0); return; } void zdialog_drag(GtkWidget *widget, GdkEventButton *event, void *) { static int bdown = 0, type; static int mx0, my0, mx, my; static int wx0, wy0, wx, wy; if (! main_thread()) zappcrash("illegal call from thread"); type = event->type; gdk_device_get_position(zfuncs::mouse,0,&mx,&my); // mouse position in monitor if (type == GDK_BUTTON_PRESS) { bdown = 1; mx0 = mx; // drag start my0 = my; gtk_window_get_position(GTK_WINDOW(widget),&wx0,&wy0); // initial window position } if (type == GDK_BUTTON_RELEASE) bdown = 0; if (type == GDK_MOTION_NOTIFY) { if (! bdown) return; wx = wx0 + mx - mx0; wy = wy0 + my - my0; gtk_window_move(GTK_WINDOW(widget),wx,wy); } return; } // present a zdialog (visible and on top) void zdialog_present(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); zmainsleep(0.1); if (! zdialog_valid2(zd)) return; gtk_window_present(GTK_WINDOW(zd->dialog)); return; } // set zdialog can or can not receive focus (informational or report dialog) void zdialog_can_focus(zdialog *zd, int Fcan) { if (! main_thread()) zappcrash("illegal call from thread"); gtk_window_set_accept_focus(GTK_WINDOW(zd->dialog),Fcan); return; } // set focus on dialog window or window and named widget // (widget name may be null or missing) // see also: gtk_window_activate_focus(GtkWindow *) void zdialog_set_focus(zdialog *zd, ch *wname) { GtkWindow *window; GtkWidget *widget; if (! main_thread()) zappcrash("illegal call from thread"); window = GTK_WINDOW(zd->dialog); if (wname) widget = zdialog_gtkwidget(zd,wname); else widget = 0; if (wname) gtk_window_set_focus(window,widget); else gtk_window_activate_focus(window); return; } // add widget to existing zdialog // // Arguments after parent are optional and default to 0. // zd zdialog *, created with zdialog_new() // type string, one of the widget types listed below // wname string, widget name, used to stuff or fetch widget data // parent string, parent name: "dialog" or a previously added container widget // data string, initial data for widget (label name, entry string, spin value, etc.) // size cc for text entry or pixel size for image widget // homog for hbox or vbox to make even space allocation for contained widgets // expand widget should expand with dialog box expansion // space extra space between this widget and neighbors, pixels // wrap allow text to wrap at right margin // // data can be a string ("initial widget data") or a number in string form ("12.345") // data for togbutt / check / radio: use "0" or "1" for OFF or ON // data for spin / zspin / hscale / vscale: use "min|max|step|value" (default: 0 | 100 | 1 | 50) // data for colorbutt: use "rrr|ggg|bbb" with values 0-255 for each RGB color. // This format is used to initialize the control and read back when user selects a color. // Multiple radio buttons with same parent are a group: pressing one turns the others OFF. int zdialog_add_widget_long ( zdialog *zd, ch *type, ch *wname, ch *pname, // mandatory args ch *data, int size, int homog, int expand, int space, int wrap) // optional args (default = 0) { int textview_focus_in_event(GtkWidget *widget); // for popup_text insertion int zdialog_activate_event(GtkWidget *, zdialog *zd); GtkWidget *widget = 0, *pwidget = 0, *fwidget = 0; GtkWidget *image, *vbox; GtkTextBuffer *editBuff = 0; PIXBUF *pixbuf = 0; GdkRGBA gdkrgba; GError *gerror = 0; ch *pp, *ptype = 0; ch vdata[30], iconpath[200]; double min, max, step, val; double f256 = 1.0 / 256.0; int iiw, iip, kk, err; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) zappcrash("zdialog invalid"); for (iiw = 1; zd->widget[iiw].type; iiw++); // find next avail. slot if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw); zd->widget[iiw].type = zstrdup(type,"zdialog"); // initz. widget struct zd->widget[iiw].wname = zstrdup(wname,"zdialog"); zd->widget[iiw].pname = zstrdup(pname,"zdialog"); zd->widget[iiw].data = 0; zd->widget[iiw].size = size; zd->widget[iiw].homog = homog; zd->widget[iiw].expand = expand; zd->widget[iiw].space = space; zd->widget[iiw].wrap = wrap; zd->widget[iiw].widget = 0; zd->widget[iiw+1].type = 0; // set new EOF marker if (strmatchV(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin", "label","link","entry","zentry","zedit","text","report", "button","zbutton","togbutt","check","radio", "imagebutt","colorbutt","combo","spin","zspin", "hscale","hscale2","vscale","icon","image",null) == 0) { Plog(0,"zdialog, bad widget type: %s \n",type); return 0; } for (iip = iiw-1; iip >= 0; iip--) // find parent (container) widget if (strmatch(pname,zd->widget[iip].wname)) break; if (iip < 0) zappcrash("zdialog, no parent for widget: %s",wname); pwidget = zd->widget[iip].widget; // parent widget, type ptype = zd->widget[iip].type; if (strmatchV(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0) zappcrash("zdialog, bad widget parent type: %s",ptype); if (strmatch(type,"hbox")) widget = gtk_box_new(HORIZONTAL,space); // expandable container boxes if (strmatch(type,"vbox")) widget = gtk_box_new(VERTICAL,space); if (strstr("hbox vbox",type)) gtk_box_set_homogeneous(GTK_BOX(widget),homog); if (strmatch(type,"hsep")) widget = gtk_separator_new(HORIZONTAL); // horiz. & vert. separators if (strmatch(type,"vsep")) widget = gtk_separator_new(VERTICAL); if (strmatch(type,"frame")) { // frame around contained widgets widget = gtk_frame_new(data); gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN); data = 0; } if (strmatch(type,"scrwin")) { // scrolled window container widget = gtk_scrolled_window_new(0,0); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_overlay_scrolling(GTK_SCROLLED_WINDOW(widget),0); data = 0; } if (strmatch(type,"label")) { // label (static text) widget = gtk_label_new(data); if (size) gtk_label_set_width_chars(GTK_LABEL(widget),size); if (data && strstr(data,"> link G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; } if (strmatch(type,"entry")) { // text input, single line widget = gtk_entry_new(); if (data) gtk_entry_set_text(GTK_ENTRY(widget),data); if (size) gtk_entry_set_width_chars(GTK_ENTRY(widget),size); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); G_SIGNAL(widget,"activate",zdialog_activate_event,zd); } if (strmatch(type,"zentry")) { // text input, single line widget = gtk_text_view_new(); gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5); if (! size) size = 10; // scale widget for font size gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE); // this does nothing GTK bug gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd); // buffer signals, not widget G_SIGNAL(widget,"focus-in-event",textview_focus_in_event,widget); // for popup_text inserts } if (strmatch(type,"zedit")) { // text input, opt. multi-line widget = gtk_text_view_new(); gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5); if (! size) size = 10; // scale widget for font size gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd); // buffer signals, not widget G_SIGNAL(widget,"focus-in-event",textview_focus_in_event,widget); // for popup_text inserts } if (strmatch(type,"text") || strmatch(type,"report")) { // text output (not editable) widget = gtk_text_view_new(); gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); if (data) gtk_text_buffer_set_text(editBuff,data,-1); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(widget),0); if (strmatch(type,"text")) gtk_widget_set_can_focus(widget,0); // inactivate KB navi keys type = "text"; } if (strmatch(type,"button")) { // button widget = gtk_button_new_with_label(data); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); data = 0; } if (strmatch(type,"zbutton")) { // checkbox used as small button if (data) widget = gtk_check_button_new_with_label(data); else widget = gtk_check_button_new(); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // default data } if (strmatch(type,"togbutt")) { // toggle button widget = gtk_toggle_button_new_with_label(data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // default data } if (strmatch(type,"imagebutt")) { // button with image snprintf(iconpath,200,"%s/%s",get_zimagedir(),data); data = 0; pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,size,size,1,&gerror); if (pixbuf) { image = gtk_image_new_from_pixbuf(pixbuf); g_object_unref(pixbuf); } else image = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON); widget = gtk_button_new_with_label(data); gtk_button_set_image(GTK_BUTTON(widget),image); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); } if (strmatch(type,"colorbutt")) { // color edit button if (! data) data = "0|0|0"; // data format: "nnn|nnn|nnn" = RGB pp = substring(data,'|',1); gdkrgba.red = f256 * atoi(pp); // RGB values are 0-1 pp = substring(data,'|',2); gdkrgba.green = f256 * atoi(pp); pp = substring(data,'|',3); gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; widget = gtk_color_button_new_with_rgba(&gdkrgba); G_SIGNAL(widget,"color-set",zdialog_widget_event,zd); } if (strmatch(type,"check")) { // checkbox if (data) widget = gtk_check_button_new_with_label(data); else widget = gtk_check_button_new(); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // default data } if (strmatch(type,"combo")) { // combo box widget = gtk_combo_box_text_new(); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); G_SIGNAL(widget,"popdown",zdialog_widget_event,zd); // fails for wayland } if (strmatch(type,"radio")) { // radio button for (kk = iip+1; kk <= iiw; kk++) if (strmatch(zd->widget[kk].pname,pname) && // find first radio button strmatch(zd->widget[kk].type,"radio")) break; // with same container if (kk == iiw) widget = gtk_radio_button_new_with_label(null,data); // this one is first else widget = gtk_radio_button_new_with_label_from_widget // not first, add to group (GTK_RADIO_BUTTON(zd->widget[kk].widget),data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); data = "0"; // default data } if (strmatchV(type,"spin","hscale","hscale2","vscale",null)) { // spin button or sliding scale if (! data) zappcrash("zdialog_add_widget(): data missing"); // "min|max|step|value" pp = substring(data,'|',1); err = convSD(pp,min); pp = substring(data,'|',2); err += convSD(pp,max); pp = substring(data,'|',3); err += convSD(pp,step); pp = substring(data,'|',4); err += convSD(pp,val); if (err) zappcrash("zdialog_add_widget(): bad data: %s",data); zd->widget[iiw].lolim = min; zd->widget[iiw].hilim = max; zd->widget[iiw].step = step; if (strmatch(type,"spin")) { widget = gtk_spin_button_new_with_range(min,max,step); gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (strmatch(type,"hscale")) { widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); gtk_scale_set_has_origin(GTK_SCALE(widget),0); } if (strmatch(type,"hscale2")) { // add digital value on the right widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),1); gtk_scale_set_value_pos(GTK_SCALE(widget),GTK_POS_RIGHT); gtk_scale_set_has_origin(GTK_SCALE(widget),0); } if (strmatch(type,"vscale")) { widget = gtk_scale_new_with_range(VERTICAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd); snprintf(vdata,30,"%g",val); data = vdata; } if (strmatch(type,"zspin")) { // "zspin" widget with range if (! data) zappcrash("zdialog_add_widget(): data missing"); // "min|max|step|value" pp = substring(data,'|',1); err = convSD(pp,min); pp = substring(data,'|',2); err += convSD(pp,max); pp = substring(data,'|',3); err += convSD(pp,step); pp = substring(data,'|',4); err += convSD(pp,val); if (err) zappcrash("zdialog_add_widget(): bad data: %s",data); zd->widget[iiw].lolim = min; zd->widget[iiw].hilim = max; zd->widget[iiw].step = step; err = convDS(val,7,vdata); // initial value >> text data = vdata; widget = gtk_text_view_new(); // GTK widget is text_view gtk_text_view_set_top_margin(GTK_TEXT_VIEW(widget),2); gtk_text_view_set_left_margin(GTK_TEXT_VIEW(widget),5); if (! size) size = 5; // scale widget for font size gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize); gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE); gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(widget),0); gtk_text_view_set_input_purpose(GTK_TEXT_VIEW(widget),GTK_INPUT_PURPOSE_NUMBER); editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(editBuff,data,-1); gtk_widget_add_events(widget,GDK_SCROLL_MASK); gtk_widget_add_events(widget,GDK_FOCUS_CHANGE_MASK); G_SIGNAL(editBuff,"changed",zdialog_zspin_event,zd); // buffer signals, not widget G_SIGNAL(widget,"key-press-event",zdialog_zspin_event,zd); G_SIGNAL(widget,"focus-out-event",zdialog_zspin_event,zd); G_SIGNAL(widget,"scroll-event",zdialog_zspin_event,zd); } if (strmatch(type,"icon")) { // image widget from icon snprintf(iconpath,200,"%s/%s",get_zimagedir(),data); data = 0; // data not further used pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,size,size,1,&gerror); if (pixbuf) { widget = gtk_image_new_from_pixbuf(pixbuf); g_object_unref(pixbuf); } else widget = gtk_image_new_from_icon_name("missing",GTK_ICON_SIZE_BUTTON); } if (strmatch(type,"image")) // image widget from pixbuf widget = gtk_image_new_from_pixbuf((GdkPixbuf *) data); // use (ch *) pixbuf in call // all widget types come here zd->widget[iiw].widget = widget; // set widget in zdialog if (strmatchV(type,"zentry","zspin","zedit","text","scrwin",0)) { // add frame around these widgets 24.30 if (! strmatch(ptype,"scrwin")) { // unless parent is scroll widget fwidget = gtk_frame_new(0); gtk_frame_set_shadow_type(GTK_FRAME(fwidget),GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(fwidget),widget); widget = fwidget; } } if (strmatch(ptype,"hbox") || strmatch(ptype,"vbox")) // add to hbox/vbox gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space); if (strmatch(ptype,"frame")) // add to frame gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strmatch(ptype,"scrwin")) // add to scroll window gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strmatch(ptype,"dialog")) { // add to dialog box vbox = gtk_bin_get_child(GTK_BIN(pwidget)); // dialog is a gtkwindow gtk_box_pack_start(GTK_BOX(vbox),widget,expand,expand,space); } if (data) zd->widget[iiw].data = zstrdup(data,"zdialog"); // widget memory return 0; } // add widget to existing zdialog - alternative form (clearer and easier code) // options: "size=nn | homog | expand | space=nn | wrap" (all optional, any order) int zdialog_add_widget(zdialog *zd, ch *type, ch *wname, ch *parent, ch *data, ch *options) { int size = 0, homog = 0, expand = 0, space = 0, wrap = 0; ch *pp, *param; int stat, Nth, pval = 0; if (! main_thread()) zappcrash("illegal call from thread"); if (! options) options = ""; for (Nth = 1; true; Nth++) { param = substring(options,"|",Nth); // get pname=value if (! param) break; pp = strchr(param,'='); if (pp) pval = atoi(pp+1); if (strstr(param,"size")) size = pval; else if (strstr(param,"homog")) homog = 1; else if (strstr(param,"expand")) expand = 1; else if (strstr(param,"space")) space = pval; else if (strstr(param,"wrap")) wrap = 1; else zappcrash("bad zdialog options: %s",options); } // printf("zdialog_add_widget_long(type %s wname %s data %s size %d homog %d expand %d space %d wrap %d \n", // type, wname, data, size, homog, expand, space, wrap); stat = zdialog_add_widget_long(zd,type,wname,parent,data,size,homog,expand,space,wrap); return stat; } // return 1/0 if zdialog is valid/invalid int zdialog_valid(zdialog *zd, ch *title) // title is optional { int ok, ii; if (! zd) return 0; for (ii = 0; ii < zdialog_count; ii++) // find in valid zdialog list if (zd == zdialog_list[ii]) break; if (ii == zdialog_count) { Plog(0,"*** zdialog invalid %s \n",title); return 0; } ok = 1; if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0; if (zd->sentinel2 != zd->sentinel1) ok = 0; if (! ok) { Plog(0,"*** zdialog sentinel invalid %s \n",title); return 0; } if (title && ! strmatch(title,zd->title)) { Plog(0,"*** zdialog title invalid %s \n",title); return 0; } return 1; } // return 1/0 if zdialog is valid/invalid // silent version to use when zdialog is possibly destroyed int zdialog_valid2(zdialog *zd, ch *title) { int ok, ii; if (! zd) return 0; // 24.20 for (ii = 0; ii < zdialog_count; ii++) if (zd == zdialog_list[ii]) break; if (ii == zdialog_count) return 0; ok = 1; if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0; if (zd->sentinel2 != zd->sentinel1) ok = 0; if (! ok) return 0; if (title && ! strmatch(title,zd->title)) return 0; return 1; } // find zdialog widget from zdialog and widget name int zdialog_find_widget(zdialog *zd, ch *wname) { if (! zdialog_valid(zd)) { Plog(0,"invalid zdialog %p \n",zd); return 0; } for (int ii = 0; zd->widget[ii].type; ii++) if (strmatch(zd->widget[ii].wname,wname)) return ii; Plog(0,"zdialog bad widget name: %s \n",wname); return 0; } // get GTK widget from zdialog and widget name GtkWidget * zdialog_gtkwidget(zdialog *zd, ch *wname) { if (strmatch(wname,"dialog")) return zd->widget[0].widget; int ii = zdialog_find_widget(zd,wname); if (ii) return zd->widget[ii].widget; return 0; } // set an "image" widget type from a GDK pixbuf // returns 0 if OK, else +N int zdialog_set_image(zdialog *zd, ch *wname, GdkPixbuf *pixbuf) { GtkWidget *widget; int ii; if (! main_thread()) zappcrash("illegal call from thread"); ii = zdialog_find_widget(zd,wname); if (! ii) return 2; if (! strmatch(zd->widget[ii].type,"image")) return 3; widget = zd->widget[ii].widget; gtk_image_set_from_pixbuf(GTK_IMAGE(widget),pixbuf); return 0; } // add a popup tool tip to a zdialog widget int zdialog_add_ttip(zdialog *zd, ch *wname, ch *ttip) { GtkWidget *widget; int ii; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; for (ii = 0; zd->compwidget[ii]; ii++) // search completion buttons if (strmatch(zd->compbutton[ii],wname)) { // for matching wname gtk_widget_set_tooltip_text(zd->compwidget[ii],ttip); return 1; } widget = zdialog_gtkwidget(zd,wname); // search zdialog widgets if (! widget) return 0; gtk_widget_set_tooltip_text(widget,ttip); return 1; } // resize dialog to a size greater than initial size // (as determined by the included widgets) int zdialog_resize(zdialog *zd, int width, int height) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; if (! width) width = 10; // stop spurious GTK warnings if (! height) height = 10; GtkWidget *window = zd->widget[0].widget; gtk_window_set_default_size(GTK_WINDOW(window),width,height); return 1; } // put data into a zdialog widget // private function int zdialog_put_data(zdialog *zd, ch *wname, ch *data) { GtkWidget *widget; GtkTextBuffer *textBuff; GdkRGBA gdkrgba; int iiw, nn, kk, err, Nsteps; ch *type, *pp; ch *wdata, sdata[32]; double dval; double f256 = 1.0 / 256.0; double lval, hval, nval, F, F2; double fdata, lolim, hilim, step; // double if (! main_thread()) zappcrash("illegal call from thread"); iiw = zdialog_find_widget(zd,wname); if (! iiw) return 0; type = zd->widget[iiw].type; widget = zd->widget[iiw].widget; wdata = zd->widget[iiw].data; if (wdata) zfree(wdata); // free prior data memory zd->widget[iiw].data = 0; if (data) { if (utf8_check(data)) wdata = zstrdup("bad UTF8 data","zdialog"); // replace bad UTF-8 encoding else wdata = zstrdup(data,"zdialog"); // set new data for widget zd->widget[iiw].data = wdata; } zd->disabled++; // disable for widget stuffing if (strmatch(type,"label")) gtk_label_set_text(GTK_LABEL(widget),data); if (strmatch(type,"link")) gtk_label_set_text(GTK_LABEL(widget),data); if (strmatch(type,"entry")) gtk_entry_set_text(GTK_ENTRY(widget),data); if (strmatch(type,"zentry")) { // text input, single line textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strmatchV(type,"button","zbutton",null)) // change button label gtk_button_set_label(GTK_BUTTON(widget),data); if (strmatch(type,"zedit")) { // text input to editable text textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strmatch(type,"text")) { // text output textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,data,-1); } if (strmatchV(type,"togbutt","check","radio",null)) { if (! data) kk = nn = 0; else kk = convSI(data,nn); if (kk != 0) nn = 0; // data not integer, force zero if (nn <= 0) nn = 0; else nn = 1; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn); // set gtk widget value } if (strmatch(type,"spin")) { kk = convSD(data,dval); if (kk != 0) dval = 0.0; gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),dval); } if (strmatch(type,"zspin")) { // "zspin" widget lolim = zd->widget[iiw].lolim; hilim = zd->widget[iiw].hilim; step = zd->widget[iiw].step; err = convSD(data,fdata); // string --> double if (err) goto retx; Nsteps = (fdata - lolim) / step + 0.5; // nearest exact step fdata = lolim + Nsteps * step; if (fdata < lolim) fdata = lolim; // enforce limits if (fdata > hilim) fdata = hilim; convDS(fdata,7,sdata); // double --> string, precision 7 textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); gtk_text_buffer_set_text(textBuff,sdata,-1); } if (strmatch(type,"colorbutt")) { // color button data is nnn|nnn|nnn pp = substring(data,'|',1); if (pp) gdkrgba.red = f256 * atoi(pp); // RGB range is 0-1 pp = substring(data,'|',2); if (pp) gdkrgba.green = f256 * atoi(pp); pp = substring(data,'|',3); if (pp) gdkrgba.blue = f256 * atoi(pp); gdkrgba.alpha = 1.0; gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); } if (strmatchV(type,"hscale","hscale2","vscale",null)) // slider widget { kk = convSD(data,dval); // zdialog widget value if (kk != 0) dval = 0.0; if (zd->widget[iiw].rescale) // widget value --> slider value { lval = zd->widget[iiw].lval; // rescaled for more sensitivity nval = zd->widget[iiw].nval; // around neutral value hval = zd->widget[iiw].hval; if (dval > lval && dval <= nval) { // if dval == lval or dval == hval F2 = (nval - dval) / (nval - lval); // then dval is not revised F = sqrtf(F2); dval = nval - F * (nval - lval); } else if (dval >= nval && dval < hval) { F2 = (dval - nval) / (hval - nval); F = sqrtf(F2); dval = nval + F * (hval - nval); } } gtk_range_set_value(GTK_RANGE(widget),dval); } if (strmatch(type,"combo")) // combo box { if (blank_null(data)) // if blank, set no active entry gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1); else { if (! zd->widget[iiw].zlist) // add parallel zlist if not already zd->widget[iiw].zlist = zlist_new(0); nn = zlist_find(zd->widget[iiw].zlist,data,0); // find matching zlist entry if (nn < 0) { zlist_append(zd->widget[iiw].zlist,data,0); // not found, append new entry to zlist gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget),data); // append new entry to combo box nn = zlist_count(zd->widget[iiw].zlist) - 1; // entry = count - 1 } gtk_combo_box_set_active(GTK_COMBO_BOX(widget),nn); // set combo box active entry } } retx: zd->disabled--; // re-enable dialog return iiw; } // get data from a dialog widget based on its name // private function ch * zdialog_get_data(zdialog *zd, ch *wname) { if (! main_thread()) zappcrash("illegal call from thread"); int ii = zdialog_find_widget(zd,wname); if (ii) return zd->widget[ii].data; return 0; } // set new limits for a numeric data entry widget (spin, zspin, hscale, vscale) int zdialog_set_limits(zdialog *zd, ch *wname, double min, double max) { GtkWidget *widget; ch *type; int iiw; if (! main_thread()) zappcrash("illegal call from thread"); iiw = zdialog_find_widget(zd,wname); if (! iiw) { Plog(0,"zdialog_set_limits, %s not found \n",wname); return 0; } widget = zd->widget[iiw].widget; type = zd->widget[iiw].type; if (*type == 's') gtk_spin_button_set_range(GTK_SPIN_BUTTON(widget),min,max); if (*type == 'h' || *type == 'v') gtk_range_set_range(GTK_RANGE(widget),min,max); if (*type == 'z') { // zspin zd->widget[iiw].lval = min; zd->widget[iiw].hval = max; } return 1; } // get lower and upper limits for numeric data entry widget // returns 1 if OK, 0 if not a widget with limits int zdialog_get_limits(zdialog *zd, ch *wname, double &min, double &max) { int iiw; min = max = 0; iiw = zdialog_find_widget(zd,wname); if (! iiw) return 0; if (! strstr("spin zspin hscale hscale2 vscale",zd->widget[iiw].type)) return 0; min = zd->widget[iiw].lolim; max = zd->widget[iiw].hilim; return 1; } // Expand a widget scale in the region around the neutral value. // Control small adjustments near the neutral value more precisely. // lval and hval: the range of values to be rescaled. // nval: the neutral value where the scale will be expanded the most. // lval <= nval <= hval int zdialog_rescale(zdialog *zd, ch *wname, float lval, float nval, float hval) { int iiw; iiw = zdialog_find_widget(zd,wname); if (! iiw) return 0; if (lval > nval || nval > hval) { Plog(0,"zdialog_rescale, bad data: %s \n",wname); return 0; } zd->widget[iiw].rescale = 1; zd->widget[iiw].lval = lval; zd->widget[iiw].nval = nval; zd->widget[iiw].hval = hval; return 1; } // run the dialog and send events to the event function // // evfunc: int func(zdialog *zd, ch *event) // If present, eventFunc is called when a dialog widget is changed or the dialog // is completed. If a widget was changed, event is the widget name. // Get the new widget data with zdialog_fetch(). // If a completion button was pressed, event is "zstat" and zd->zstat will be // the button number 1-N. // If the dialog was destroyed, event is "zstat" and zd->zstat is negative. // // posn: optional dialog box position: // "mouse" = position at mouse // "desktop" = center on desktop DEFAULT // "parent" = center on parent window // "nn/nn" = position NW corner at relative x/y position in parent window, // where nn/nn is a percent 0-100 of the parent window dimensions. // "save" = save last user-set position and use this whenever the dialog // is repeated, also across sessions. // // KBevent: extern void KBevent(GdkEventKey *event) // This function must be supplied by the caller of zdialog. // It is called when Ctrl|Shift|Alt|F1 is pressed. int zdialog_run(zdialog *zd, zdialog_event evfunc, ch *posn) { int zdialog_KB_press(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_focus_in_event(GtkWidget *, GdkEvent *event, zdialog *zd); GtkWidget *dialog; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; if (zd->zrunning) { Plog(0,"zdialog is already running \n"); return 0; } if (posn) zdialog_set_position(zd,posn); // put dialog at desired position else zdialog_set_position(zd,"desktop"); // use default if (evfunc) zd->eventCB = (void *) evfunc; // link to dialog event callback Plog(1,"dialog started: %s \n",zd->title); dialog = zd->widget[0].widget; gtk_widget_show_all(dialog); // activate dialog G_SIGNAL(dialog,"focus-in-event",zdialog_focus_in_event,zd); // connect focus event function G_SIGNAL(dialog,"key-press-event",zdialog_KB_press,zd); // connect key press event function G_SIGNAL(dialog,"delete-event",zdialog_delete_event,zd); // connect delete event function zd->zstat = 0; // dialog status incomplete zd->disabled = 0; // enable widget events zd->zrunning = 1; // dialog is running return 0; } // zdialog event handler - called for dialog events. // Updates data in zdialog, calls user callback function (if present). // private function int zdialog_widget_event(GtkWidget *widget, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; GtkTextIter iter1, iter2; GdkRGBA gdkrgba; int ii, nn; ch *wname, *wtype, *wdata; ch *pp, sdata[20]; double dval; float lval, nval, hval, F; if (! zdialog_valid2(zd)) return 1; // zdialog gone if (zd->disabled) return 1; // events disabled zd->disabled = 1; // disable nested events for (ii = 0; ii < zdmaxbutts; ii++) { // check completion buttons if (zd->compwidget[ii] == null) break; // EOL if (zd->compwidget[ii] != widget) continue; zd->zstat = ii+1; // zdialog status = button no. strncpy0(zd->event,"zstat",40); strncpy0(zd->zstat_button,zd->compbutton[ii],40); // button label "Cancel" etc. wtype = "completion button"; Plog(1,"dialog: %s button: %s \n",zd->title,zd->zstat_button); goto call_evfunc; // call zdialog event function } for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } Plog(0,"zdialog event ignored: %s \n",zd->title); // not found, ignore event zd->disabled = 0; return 1; found_widget: wname = zd->widget[ii].wname; wtype = zd->widget[ii].type; wdata = 0; Plog(2,"dialog: %s event: %s \n",zd->title,wname); // Floglevel=2 to log dialog inputs if (strmatch(wtype,"button")) wdata = (ch *) gtk_button_get_label(GTK_BUTTON(widget)); // button label if (strmatch(wtype,"zbutton")) { // checkbox as smaller button wdata = (ch *) gtk_button_get_label(GTK_BUTTON(widget)); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),0); // reset checkmark = off } if (strmatch(wtype,"zedit")) { // multi-line gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); } if (strmatch(wtype,"entry")) wdata = (ch *) gtk_entry_get_text(GTK_ENTRY(widget)); if (strmatch(wtype,"zentry")) { // single line gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); pp = strchr(wdata,GDK_KEY_Linefeed); // prohibit return character if (pp) { *pp = 0; gtk_text_buffer_set_text(textBuff,wdata,-1); // works, generates bogus error message } } if (strmatchV(wtype,"radio","check","togbutt",null)) { nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)); if (nn == 0) wdata = "0"; else wdata = "1"; } if (strmatch(wtype,"combo")) wdata = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget)); if (strmatch(wtype,"spin")) { dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget)); snprintf(sdata,20,"%g",dval); wdata = sdata; } if (strmatch(wtype,"colorbutt")) // color button { gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget),&gdkrgba); snprintf(sdata,20,"%.0f|%.0f|%.0f",gdkrgba.red*255,gdkrgba.green*255,gdkrgba.blue*255); wdata = sdata; } if (strmatchV(wtype,"hscale","hscale2","vscale",null)) { dval = gtk_range_get_value(GTK_RANGE(widget)); if (zd->widget[ii].rescale) // slider value --> widget value { lval = zd->widget[ii].lval; nval = zd->widget[ii].nval; hval = zd->widget[ii].hval; if (dval > lval && dval < nval) { // lval ... nval F = (nval - dval) / (nval - lval); // 1 ... 0 dval = (1.0 - F * F) * (nval - lval) + lval; // lval ... nval } else if (dval > nval && dval < hval) { // nval ... hval F = (dval - nval) / (hval - nval); // 0 ... 1 dval = F * F * (hval - nval) + nval; // nval ... hval } } snprintf(sdata,20,"%g",dval); wdata = sdata; } // all widgets come here if (zd->widget[ii].data) zfree(zd->widget[ii].data); // clear prior data zd->widget[ii].data = 0; if (wdata) zd->widget[ii].data = zstrdup(wdata,"zdialog"); // set new data zd->lastwidget = widget; // remember last widget updated strncpy0(zd->event,wname,40); // event = widget name call_evfunc: // call zdialog event function if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,zd->event); } if (zdialog_valid2(zd)) zd->disabled = 0; // 'event' may cause zdialog_free() return 1; } // special zdialog handler for GtkTextView widgets // track current input widget for popup_text int textview_focus_in_event(GtkWidget *widget) { curr_textview_widget = GTK_TEXT_VIEW(widget); return 1; } // zdialog response handler for "focus-in-event" signal // private function zdialog *zdialog_focus_zd; // current zdialog int zdialog_focus_in_event(GtkWidget *, GdkEvent *event, zdialog *zd) { if (! zdialog_valid2(zd)) return 0; if (zd->zstat) return 0; // already complete zdialog_focus_zd = zd; zdialog_send_event(zd,"focus"); // notify dialog event function return 0; // must be 0 } // zdialog response handler for "activate" signal // private function int zdialog_activate_event(GtkWidget *, zdialog *zd) { if (! zdialog_valid2(zd)) return 0; if (zd->zstat) return 0; // already complete zdialog_send_event(zd,"activate"); // notify dialog event function return 0; // must be 0 } // zdialog response handler for keyboard events // key symbols can be found at /usr/include/gtk-3.0/gdk/gdkkeysyms.h // main app must provide: extern void KBevent(GdkEventKey *event) // private function int zdialog_KB_press(GtkWidget *widget, GdkEventKey *kbevent, zdialog *zd) { void zdialog_copyfunc(GtkWidget *, GtkClipboard *); void zdialog_pastefunc(GtkClipboard *, ch *, void *); GtkWidget *focuswidget; int KB_Ctrl = 0; int KBkey = kbevent->keyval; ch *type; int ii; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; if (KBkey == GDK_KEY_Escape) { // escape key Plog(3,"zdialog escape key \n"); Fescape = 1; zd->zstat = -2; if (zd->eventCB) zdialog_send_event(zd,"zstat"); return 1; } if (KBkey == GDK_KEY_F1) { KBevent(kbevent); return 1; }; // these keys handled by main app window if (KBkey == GDK_KEY_F10) { KBevent(kbevent); return 1; }; if (KBkey == GDK_KEY_F11) { KBevent(kbevent); return 1; }; if (KB_Ctrl && KBkey == GDK_KEY_h) { KBevent(kbevent); return 1; }; // Ctrl+h (help, user guide) 24.10 focuswidget = gtk_window_get_focus(GTK_WINDOW(widget)); // find widget in zdialog for (ii = 1; zd->widget[ii].type; ii++) if (zd->widget[ii].widget == focuswidget) break; type = zd->widget[ii].type; // screening input widget types removed if (! type) return 0; /*** cch *editwidgets = "entry zentry zedit spin zspin hscale hscale2 vscale"; if (! strstr(editwidgets,type)) { // unless widget type edit, if (KBkey == GDK_KEY_Left) { KBevent(kbevent); return 1; }; // arrow keys --> main window 24.50 if (KBkey == GDK_KEY_Right) { KBevent(kbevent); return 1; }; } ***/ strncpy0(zd->event,zd->widget[ii].wname,40); // save event name return 0; // pass KB key to widget } // event function for "zspin" widget int zdialog_zspin_event(GtkWidget *widget, GdkEvent *event, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int KBkey; int ii, err, Nsteps, state, incr = 0; double fdata, lolim, hilim, step; // double ch *wdata, sdata[20]; int time, elaps, Fchanged; static int time0 = 0, time1 = 0; if (event->type < 0) return 1; // GDK bug ? if (! zdialog_valid2(zd)) return 0; // event after dialog destroyed if (zd->disabled) return 0; // zdialog events disabled for (ii = 1; zd->widget[ii].type; ii++) // find "zspin" (text view) widget if (zd->widget[ii].widget == widget) break; if (! zd->widget[ii].type) return 0; // not found textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); // get widget data gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); lolim = zd->widget[ii].lolim; // limits and step size hilim = zd->widget[ii].hilim; step = zd->widget[ii].step; if (event->type == GDK_SCROLL) { // mouse wheel event gtk_widget_grab_focus(widget); incr = - ((GdkEventScroll *) event)->delta_y; if (! incr) return 0; state = ((GdkEventScroll *) event)->state; // if shift key held, use 10x step if (state & GDK_SHIFT_MASK) incr *= 10; goto checklimits; } if (event->type == GDK_KEY_PRESS) { // KB button press KBkey = ((GdkEventKey *) event)->keyval; if (KBkey == GDK_KEY_Return) goto checklimits; // return = entry finished if (KBkey == GDK_KEY_Up) incr = 1; if (KBkey == GDK_KEY_Down) incr = -1; if (! incr) return 0; // must return 0 state = ((GdkEventKey *) event)->state; // if shift key held, use 10x step if (state & GDK_SHIFT_MASK) incr *= 10; time = ((GdkEventKey *) event)->time; // track time key is held down if (time - time1 > 300) time0 = time; time1 = time; elaps = time - time0; if (elaps > 5000) step = 10 * step; // acceleration table for else if (elaps > 4500) step = 9 * step; // hold time 1-5+ seconds else if (elaps > 4000) step = 8 * step; // use integer values only else if (elaps > 3500) step = 7 * step; else if (elaps > 3000) step = 6 * step; else if (elaps > 2500) step = 5 * step; else if (elaps > 2000) step = 4 * step; else if (elaps > 1500) step = 3 * step; else if (elaps > 1000) step = 2 * step; goto checklimits; } if (event->type == GDK_FOCUS_CHANGE) goto checklimits; // focus change = entry finished if (event->type == GDK_LEAVE_NOTIFY) goto checklimits; // pointer out - entry finished return 0; checklimits: convSD(wdata,fdata); // ignore bad char. inputs fdata += incr * step; Nsteps = (fdata - lolim) / step + 0.5; // set nearest exact step fdata = lolim + Nsteps * step; err = 0; if (fdata < lolim) { // force within range err = 1; fdata = lolim; } if (fdata > hilim) { err = 2; fdata = hilim; } if (err) gtk_widget_grab_focus(widget); // if error, restore focus convDS(fdata,7,sdata); // round to 7 digits gtk_text_buffer_set_text(textBuff,sdata,-1); // insert recognized value Fchanged = 0; if (zd->widget[ii].data) { if (! strmatch(zd->widget[ii].data,sdata)) Fchanged = 1; // detect if widget data changed zfree(zd->widget[ii].data); // clear prior widget data } zd->widget[ii].data = zstrdup(sdata,"zdialog"); // set new data zd->lastwidget = widget; // remember last widget updated strncpy0(zd->event,zd->widget[ii].wname,40); // event = widget name if (Fchanged) Plog(2,"dialog: %s event: %s \n",zd->title,zd->widget[ii].wname); // Floglevel=2 to log dialog inputs if (zd->eventCB && Fchanged) { // if widget data changed zd->disabled = 1; evfunc = (zdialog_event *) zd->eventCB; // do event callback function evfunc(zd,zd->event); if (zdialog_valid2(zd)) zd->disabled = 0; // 'event' may cause zdialog_free() } if (event->type == GDK_FOCUS_CHANGE) return 0; // no propagate return 1; // propagate } // process Ctrl+C (copy text from widget to clipboard) // private function void zdialog_copyfunc(GtkWidget *widget, GtkClipboard *clipboard) { GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; zdialog *zd; int ii, cc = 0; ch *wname; ch text[1000]; widget = gtk_window_get_focus(GTK_WINDOW(widget)); if (! widget) return; zd = zdialog_focus_zd; if (! zdialog_valid2(zd)) return; for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } return; // not found found_widget: wname = zd->widget[ii].wname; zdialog_fetch(zd,wname,text,999); // current text in widget cc = strlen(text); gtk_clipboard_set_text(clipboard,text,cc); return; } // process Ctrl+V (paste text from clipboard to widget with KB focus) // private function void zdialog_pastefunc(GtkClipboard *clipboard, ch *cliptext, void *arg) { GtkWindow *window; GtkWidget *widget; GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; zdialog *zd; int ii, cc = 0; ch *wname; ch text[1000]; window = (GtkWindow *) arg; widget = gtk_window_get_focus(window); if (! widget) return; // widget for pasted text if (! cliptext || ! *cliptext) return; // clipboard text pasted zd = zdialog_focus_zd; if (! zdialog_valid2(zd)) return; if (zd->zstat) return; for (ii = 1; zd->widget[ii].type; ii++) // find widget in zdialog if (zd->widget[ii].widget == widget) goto found_widget; for (ii = 1; zd->widget[ii].type; ii++) { // failed, test if buffer if (strmatchV(zd->widget[ii].type,"zedit","zentry",null)) { // of text view widget textView = GTK_TEXT_VIEW(zd->widget[ii].widget); textBuff = gtk_text_view_get_buffer(textView); if (widget == (GtkWidget *) textBuff) goto found_widget; } } return; // not found found_widget: wname = zd->widget[ii].wname; zdialog_fetch(zd,wname,text,999); // current text in widget cc = strlen(text); if (cc > 995) return; strncpy(text+cc,cliptext,999-cc); // add clipboard text text[999] = 0; zdialog_stuff(zd,wname,text); return; } // private function called when zdialog is completed. // called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0). int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function if (! zd) return 0; // 24.20 zd->widget[0].widget = 0; // widget no longer valid if (! zdialog_valid2(zd)) return 1; // already destroyed if (zd->zstat) return 1; // already complete if (zd->disabled) return 1; // in process zd->zstat = -1; // set zdialog cancel status Plog(0,"dialog: %s killed\n",zd->title); if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function zd->disabled = 1; evfunc(zd,"zstat"); if (zdialog_valid2(zd)) zd->disabled = 0; // 'event' may cause zdialog_free() } zdialog_free(zd); return 0; } // Send an event name to an active zdialog. // The response function eventFunc() will be called with this event. // zdialog completion buttons KB shortcuts removed // 24.20 int zdialog_send_event(zdialog *zd, ch *event) { zdialog_event * evfunc = 0; // dialog event callback function if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid2(zd)) return 0; // zdialog canceled if (zd->disabled) return 0; // zdialog busy evfunc = (zdialog_event *) zd->eventCB; if (! evfunc) return 0; zd->disabled = 1; Plog(3,"zdialog send event: %s \n",event); evfunc(zd,event); // call dialog event function if (zdialog_valid2(zd)) zd->disabled = 0; // 'event' may cause zdialog_free() return 1; } // Complete an active dialog and assign a status. // Equivalent to the user pressing a dialog completion button. // The dialog completion function is called if defined, // and zdialog_wait() is unblocked. // returns: 0 = no active dialog or completion function, 1 = OK int zdialog_send_response(zdialog *zd, int zstat) { zdialog_event *evfunc = 0; // dialog event callback function if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid2(zd)) return 0; if (zd->disabled) return 0; zd->zstat = zstat; // set status evfunc = (zdialog_event *) zd->eventCB; if (! evfunc) return 0; zd->disabled = 1; evfunc(zd,"zstat"); if (zdialog_valid2(zd)) zd->disabled = 0; // 'event' may cause zdialog_free() return 1; } // show or hide a zdialog window // returns 1 if successful, 0 if zd does not exist. int zdialog_show(zdialog *zd, int show) { static GtkWidget *widget, *pwidget = 0; static int posx, posy; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; widget = zdialog_gtkwidget(zd,"dialog"); if (show) { // show window if (widget == pwidget) { // restore prior position gtk_window_move(GTK_WINDOW(widget),posx,posy); pwidget = 0; } gtk_widget_show_all(widget); gtk_window_present(GTK_WINDOW(widget)); // set focus on restored window } else { // hide window pwidget = widget; gtk_window_get_position(GTK_WINDOW(widget),&posx,&posy); // save position gtk_widget_hide(widget); } return 1; } // Destroy the zdialog - must be done by zdialog_run() caller // (else dialog continues active even after completion button). // Data in widgets remains valid until zdialog_free() is called. int zdialog_destroy(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid2(zd)) return 0; // destroyed, not freed yet if (zd->saveposn) zdialog_save_position(zd); // save position for next use if (zd->widget[0].widget) { // multiple destroys OK gtk_widget_destroy(zd->widget[0].widget); // destroy GTK dialog zd->widget[0].widget = 0; } if (! zd->zstat) zd->zstat = -1; // status = destroyed zd->zrunning = 0; // not running zd->disabled = 1; // ignore events after destroy return 1; } // free zdialog memory (will destroy first, if not already) // zd is set to null int zdialog_free(zdialog *&zd) // reference { int ii; if (! main_thread()) zappcrash("illegal call from thread"); if (! zd) return 0; // 24.20 if (! zdialog_valid2(zd)) return 0; // validate zd pointer zdialog_save_inputs(zd); // save user inputs for next use zdialog_destroy(zd); // destroy GTK dialog if there zd->sentinel1 = zd->sentinel2 = 0; // mark sentinels invalid zfree(zd->title); // free title memory zfree(zd->widget[0].data); for (ii = 1; zd->widget[ii].type; ii++) // loop through widgets { zfree((ch *) zd->widget[ii].type); // free strings zfree((ch *) zd->widget[ii].wname); if (zd->widget[ii].pname) zfree((ch *) zd->widget[ii].pname); // parent widget name if (zd->widget[ii].data) zfree(zd->widget[ii].data); // free data if (zd->widget[ii].zlist) zlist_free(zd->widget[ii].zlist); // free combo box zlist } for (ii = 0; ii < zdialog_count; ii++) // remove from valid zdialog list if (zd == zdialog_list[ii]) break; if (ii < zdialog_count) { zdialog_count--; for (NOP; ii < zdialog_count; ii++) // pack down list zdialog_list[ii] = zdialog_list[ii+1]; } else Plog(0,"zdialog_free(), not in zdialog_list \n"); zfree(zd); // free zdialog memory zd = 0; // caller pointer = null return 1; } // Wait for a dialog to complete or be destroyed. This is a zmainloop() loop. // The returned status is the button 1-N used to complete the dialog, or negative // if the dialog was destroyed with [x] or otherwise by GTK. If the status was 1-N and // the dialog will be kept active, set zd->zstat = 0 to restore the active state. int zdialog_wait(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); zdialog_present(zd); // initially has focus while (true) { zmainsleep(0.01); if (! zd) return -1; if (! zdialog_valid2(zd)) return -1; if (zd->zstat) return zd->zstat; } } // put cursor at named widget int zdialog_goto(zdialog *zd, ch *wname) { GtkWidget *widget; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; widget = zdialog_gtkwidget(zd, wname); if (! widget) return 0; gtk_widget_grab_focus(widget); return 1; } // set cursor for zdialog (e.g. a busy cursor) void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor) { GtkWidget *dialog; GdkWindow *window; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; dialog = zd->widget[0].widget; if (! dialog) return; window = gtk_widget_get_window(dialog); gdk_window_set_cursor(window,cursor); return; } // insert data into a zdialog widget int zdialog_stuff(zdialog *zd, ch *wname, ch *data) // stuff a string { if (! main_thread()) zappcrash("illegal call from thread"); if (data) zdialog_put_data(zd,wname,data); else zdialog_put_data(zd,wname,""); // null > "" return 1; } int zdialog_stuff(zdialog *zd, ch *wname, int idata) // stuff an integer { ch string[16]; double min, max; if (! main_thread()) zappcrash("illegal call from thread"); if (zdialog_get_limits(zd,wname,min,max)) if (idata < min || idata > max) return 0; // bad data, do nothing snprintf(string,16,"%d",idata); zdialog_put_data(zd,wname,string); return 1; } int zdialog_stuff(zdialog *zd, ch *wname, double ddata) // stuff a double { ch string[32]; double min, max; if (! main_thread()) zappcrash("illegal call from thread"); if (zdialog_get_limits(zd,wname,min,max)) if (ddata < min || ddata > max) return 0; // bad data, do nothing snprintf(string,32,"%.7g",ddata); // increase from 6 to 7 digits zdialog_put_data(zd,wname,string); // 'g' uses decimal or comma return 1; // (per locale) } int zdialog_stuff(zdialog *zd, ch *wname, double ddata, ch *format) // stuff a double, formatted { ch string[32]; double min, max; if (! main_thread()) zappcrash("illegal call from thread"); if (zdialog_get_limits(zd,wname,min,max)) if (ddata < min || ddata > max) return 0; // bad data, do nothing snprintf(string,32,format,ddata); // use "%.2g" etc. for zdialog_put_data(zd,wname,string); // locale dependent point/comma return 1; } int zdialog_labelfont(zdialog *zd, ch *labl, ch *font, ch *txt) // stuff label text using specified font { GtkWidget *widget; ch *format = "%s"; ch txt2[1000]; if (! main_thread()) zappcrash("illegal call from thread"); if (! font) font = zfuncs::appfont; // default font snprintf(txt2,1000,format,font,txt); widget = zdialog_gtkwidget(zd,labl); gtk_label_set_markup(GTK_LABEL(widget),txt2); return 1; } // get data from a zdialog widget int zdialog_fetch(zdialog *zd, ch *wname, ch *data, int maxcc) // fetch string data { ch *zdata; if (! main_thread()) zappcrash("illegal call from thread"); zdata = zdialog_get_data(zd,wname); if (! zdata) { *data = 0; return 0; } return strncpy0(data,zdata,maxcc); // 0 = OK, 1 = truncation } int zdialog_fetch(zdialog *zd, ch *wname, int &idata) // fetch an integer { ch *zdata; if (! main_thread()) zappcrash("illegal call from thread"); zdata = zdialog_get_data(zd,wname); if (! zdata) { idata = 0; return 0; } idata = atoi(zdata); return 1; } int zdialog_fetch(zdialog *zd, ch *wname, double &ddata) // fetch a double { int stat; ch *zdata; if (! main_thread()) zappcrash("illegal call from thread"); zdata = zdialog_get_data(zd,wname); if (! zdata) { ddata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK if (stat < 4) return 1; return 0; } int zdialog_fetch(zdialog *zd, ch *wname, float &fdata) // fetch a float { int stat; ch *zdata; double ddata; if (! main_thread()) zappcrash("illegal call from thread"); zdata = zdialog_get_data(zd,wname); if (! zdata) { fdata = 0; return 0; } stat = convSD(zdata,ddata); // period or comma decimal point OK fdata = ddata; if (stat < 4) return 1; return 0; } // clear combo box entries int zdialog_combo_clear(zdialog *zd, ch *wname) { int ii; if (! main_thread()) zappcrash("illegal call from thread"); ii = zdialog_find_widget(zd,wname); if (! ii) return 0; gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(zd->widget[ii].widget)); // remove all entries if (zd->widget[ii].zlist) zlist_clear(zd->widget[ii].zlist,0); return 1; } // popup (open) combo box pick list int zdialog_combo_popup(zdialog *zd, ch *wname) { int ii; if (! main_thread()) zappcrash("illegal call from thread"); ii = zdialog_find_widget(zd,wname); if (! ii) return 0; gtk_combo_box_popup(GTK_COMBO_BOX(zd->widget[ii].widget)); return 1; } /********************************************************************************/ // Load/save all function widget data from/to a file. // dirname for data files: /home//.appname/funcname // where zdialog data is saved for the respective function. // return 0 = OK, +N = error int zdialog_load_widgets(zdialog *zd, spldat *sd, ch *funcname, FILE *fid) { using namespace zfuncs; ch *mess = "Load settings from file"; int myfid = 0; ch *filename, dirname[200], buff[1000]; ch *wname, *wdata, wdata2[1000]; ch *pp, *pp1, *pp2; int ii, kk, err, cc1, cc2; if (! main_thread()) zappcrash("illegal call from thread"); if (! fid) // fid from script { snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname); // folder for data files filename = zgetfile(mess,GTK_WINDOW(mainwin),"file",dirname,0); // open data file if (! filename) return 1; // user cancel fid = fopen(filename,"r"); if (! fid) { zmessageACK(mainwin,"%s \n %s",filename,strerror(errno)); return 1; } zfree(filename); myfid = 1; } for (ii = 0; ii < zdmaxwidgets; ii++) // read widget data recs { pp = fgets_trim(buff,1000,fid,1); if (! pp) break; if (strmatch(pp,"curves")) { if (! sd) goto baddata; err = splcurve_load(sd,fid); // load curves data if (err) goto baddata; continue; } if (strmatch(pp,"end")) break; pp1 = pp; pp2 = strstr(pp1," =="); if (! pp2) continue; // widget has no data cc1 = pp2 - pp1; if (cc1 > 100) continue; pp1[cc1] = 0; wname = pp1; // widget name if (strstr("defcats deftags",wname)) continue; // fotocx only kludge pp2 += 3; if (*pp2 == ' ') pp2++; wdata = pp2; // widget data cc2 = strlen(wdata); if (cc2 < 1) wdata = ""; if (cc2 > 1000) continue; repl_1str(wdata,wdata2,1000,"\\n","\n"); // replace "\n" with newline chars. kk = zdialog_put_data(zd,wname,wdata2); if (! kk) goto baddata; Plog(2,"put dialog data: %s %s \n",wname,wdata2); // 24.30 } if (myfid) fclose(fid); return 0; baddata: zmessageACK(mainwin,"file data does not fit dialog"); if (myfid) fclose(fid); return 1; } int zdialog_save_widgets(zdialog *zd, spldat *sd, ch *funcname, FILE *fid) { using namespace zfuncs; ch *mess = "Save settings to a file"; int myfid = 0; ch *filename, dirname[200]; ch *wtype, *wname, *wdata, wdata2[1000]; int ii, cc; ch *editwidgets = "entry zentry edit text togbutt check combo" // widget types to save "radio spin zspin hscale hscale2 vscale colorbutt"; ch *editwidgetsX = "defcats deftags"; // exclude list, fotocx kludge if (! main_thread()) zappcrash("illegal call from thread"); if (! fid) // fid from script { snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname); // folder for data files filename = zgetfile(mess,GTK_WINDOW(mainwin),"save",dirname,0); // open data file if (! filename) return 1; // user cancel fid = fopen(filename,"w"); if (! fid) { zmessageACK(mainwin,"%s \n %s",filename,strerror(errno)); return 1; } myfid = 1; zfree(filename); } for (ii = 0; ii < zdmaxwidgets; ii++) { wtype = (ch *) zd->widget[ii].type; if (! wtype) break; if (! strstr(editwidgets,wtype)) continue; wname = (ch *) zd->widget[ii].wname; // write widget data recs: if (strstr(editwidgetsX,wname)) continue; wdata = zd->widget[ii].data; // widgetname == widgetdata if (! wdata) continue; cc = strlen(wdata); if (cc > 900) continue; repl_1str(wdata,wdata2,1000,"\n","\\n"); // replace newline with "\n" fprintf(fid,"%s == %s \n",wname,wdata); } if (sd) { fprintf(fid,"curves\n"); splcurve_save(sd,fid); } fprintf(fid,"end\n"); if (myfid) fclose(fid); return 0; } // functions to support [prev] buttons in function dialogs // load or save last-used widgets int zdialog_load_prev_widgets(zdialog *zd, spldat *sd, ch *funcname) { using namespace zfuncs; ch filename[200]; FILE *fid; int err; if (! main_thread()) zappcrash("illegal call from thread"); snprintf(filename,200,"%s/%s/last-used",get_zhomedir(),funcname); fid = fopen(filename,"r"); if (! fid) { zmessageACK(mainwin,"%s \n %s",filename,strerror(errno)); return 1; } err = zdialog_load_widgets(zd,sd,funcname,fid); fclose(fid); return err; } int zdialog_save_last_widgets(zdialog *zd, spldat *sd, ch *funcname) { using namespace zfuncs; ch filename[200], dirname[200]; FILE *fid; int err; if (! main_thread()) zappcrash("illegal call from thread"); snprintf(filename,200,"%s/%s/last-used",get_zhomedir(),funcname); fid = fopen(filename,"w"); if (! fid) { snprintf(dirname,200,"%s/%s",get_zhomedir(),funcname); // create missing folder err = mkdir(dirname,0750); if (err) { Plog(0,"%s \n %s \n",dirname,strerror(errno)); return 1; } fid = fopen(filename,"w"); // open again } if (! fid) { Plog(0,"%s \n %s \n",filename,strerror(errno)); return 1; } err = zdialog_save_widgets(zd,sd,funcname,fid); fclose(fid); return err; } /********************************************************************************/ // functions to save and recall zdialog window positions namespace zdposn_names { struct zdposn_t { ch wintitle[64]; // window title (ID) float xpos, ypos; // window posn WRT parent or desktop, 0-100 int xsize, ysize; // window size, pixels } zdposn[200]; // space to remember 200 windows int Nzdposn; // no. in use int Nzdpmax = 200; // table size } // Load zdialog positions table from its file (application startup) // or save zdialog positions table to its file (application exit). // Action is "load" or "save". Number of table entries is returned. int zdialog_geometry(ch *action) { using namespace zdposn_names; ch posfile[200], buff[100], wintitle[64], *pp; float xpos, ypos; int xsize, ysize; int ii, nn, cc; FILE *fid; if (! main_thread()) zappcrash("illegal call from thread"); snprintf(posfile,199,"%s/zdialog_geometry",zhomedir); // /home//.appname/zdialog_geometry if (strmatch(action,"load")) // load dialog positions table from file { fid = fopen(posfile,"r"); if (! fid) { Nzdposn = 0; return 0; } for (ii = 0; ii < Nzdpmax; ) { pp = fgets(buff,100,fid); if (! pp) break; pp = strstr(buff,"||"); if (! pp) continue; cc = pp - buff; strncpy0(wintitle,buff,cc); strTrim(wintitle); if (strlen(wintitle) < 3) continue; nn = sscanf(pp+2," %f %f %d %d ",&xpos,&ypos,&xsize,&ysize); if (nn != 4) continue; strcpy(zdposn[ii].wintitle,wintitle); zdposn[ii].xpos = xpos; zdposn[ii].ypos = ypos; zdposn[ii].xsize = xsize; zdposn[ii].ysize = ysize; ii++; } fclose(fid); Nzdposn = ii; return Nzdposn; } if (strmatch(action,"save")) // save dialog positions table to file { fid = fopen(posfile,"w"); if (! fid) { Plog(0,"cannot write zdialog_geometry file \n"); return 0; } for (ii = 0; ii < Nzdposn; ii++) { fprintf(fid,"%s || %0.1f %0.1f %d %d \n", // dialog-title || xpos ypos xsize ysize zdposn[ii].wintitle, zdposn[ii].xpos, zdposn[ii].ypos, zdposn[ii].xsize, zdposn[ii].ysize); } fclose(fid); return Nzdposn; } Plog(0,"zdialog_geometry bad action: %s \n",action); return 0; } // Set the initial or new zdialog window position from "posn". // Called by zdialog_run(). Private function. // null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window // "save" use the same position last set by the user // "nn/nn" put NW corner of dialog in parent window at % size // (e.g. "50/50" puts NW corner at center of parent) void zdialog_set_position(zdialog *zd, ch *posn) { using namespace zdposn_names; int ii, ppx, ppy, zdpx, zdpy, pww, phh; float xpos, ypos; int xsize, ysize; ch wintitle[64], *pp; GtkWidget *parent, *dialog; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; parent = zd->parent; dialog = zd->widget[0].widget; if (strmatch(posn,"mouse")) { window_to_mouse(zd->dialog); return; } if (strmatch(posn,"desktop")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER); return; } if (strmatch(posn,"parent")) { gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT); return; } if (! parent) parent = mainwin; if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = monitor_ww; phh = monitor_hh; } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } if (strmatch(posn,"save")) // use last saved window position { zd->saveposn = 1; // set flag for zdialog_free() pp = (ch *) gtk_window_get_title(GTK_WINDOW(dialog)); // get window title, used as ID if (! pp || strlen(pp) < 2) goto center_parent; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for title if (strmatch(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) goto center_parent; // not found - zdialog_destroy() will add zdpx = ppx + 0.01 * zdposn[ii].xpos * pww; // set position for dialog window zdpy = ppy + 0.01 * zdposn[ii].ypos * phh; xsize = zdposn[ii].xsize; // set size ysize = zdposn[ii].ysize; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); gtk_window_resize(GTK_WINDOW(dialog),xsize,ysize); return; } else // "nn/nn" // position from caller { ii = sscanf(posn,"%f/%f",&xpos,&ypos); // parse "nn/nn" if (ii != 2) goto center_parent; zdpx = ppx + 0.01 * xpos * pww; // position for dialog window zdpy = ppy + 0.01 * ypos * phh; gtk_window_move(GTK_WINDOW(dialog),zdpx,zdpy); return; } center_parent: // center on parent window gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_CENTER_ON_PARENT); return; } // If the dialog window position is "save" then save // its position WRT parent or desktop for next use. // called by zdialog_destroy(). Private function. void zdialog_save_position(zdialog *zd) { using namespace zdposn_names; int ii, ppx, ppy, pww, phh, zdpx, zdpy; float xpos, ypos; int xsize, ysize; ch wintitle[64], *pp; GtkWidget *parent, *dialog; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; dialog = zd->widget[0].widget; if (! dialog) return; if (! gtk_widget_get_window(dialog)) return; gtk_window_get_position(GTK_WINDOW(dialog),&zdpx,&zdpy); // dialog window NW corner if (! zdpx && ! zdpy) return; // (0,0) ignore gtk_window_get_size(GTK_WINDOW(dialog),&xsize,&ysize); // window size parent = zd->parent; // parent window if (! parent) parent = mainwin; if (! parent) { // no parent window ppx = ppy = 0; // use desktop pww = monitor_ww; phh = monitor_hh; } else { gtk_window_get_position(GTK_WINDOW(parent),&ppx,&ppy); // parent window NW corner gtk_window_get_size(GTK_WINDOW(parent),&pww,&phh); // parent window size } xpos = 100.0 * (zdpx - ppx) / pww; // dialog window relative position ypos = 100.0 * (zdpy - ppy) / phh; // (as percent of parent size) pp = (ch *) gtk_window_get_title(GTK_WINDOW(dialog)); if (! pp) return; if (strlen(pp) < 2) return; strncpy0(wintitle,pp,64); // window title, < 64 chars. for (ii = 0; ii < Nzdposn; ii++) // search table for window if (strmatch(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) { // not found if (ii == Nzdpmax) return; // table full Nzdposn++; // new entry } strcpy(zdposn[ii].wintitle,wintitle); // add window to table zdposn[ii].xpos = xpos; // save window position zdposn[ii].ypos = ypos; zdposn[ii].xsize = xsize; // and window size zdposn[ii].ysize = ysize; return; } /********************************************************************************/ // Functions to save and restore zdialog user inputs // within an app session or across app sessions. namespace zdinputs_names { #define Nwmax zdmaxwidgets // max. widgets in a dialog #define Nzdmax 200 // max. zdialogs #define ccmax1 100 // max. widget name length #define ccmax2 400 // max. widget data length struct zdinputs_t { ch *zdtitle = 0; // zdialog title int Nw; // no. of widgets ch **wname; // list of widget names ch **wdata; // list of widget data } zdinputs[Nzdmax]; // space for Nzdmax dialogs int Nzd = 0; // no. zdialogs in use } // Load zdialog input fields from its file (app startup) // or save zdialog input fields to its file (app shutdown). // Action is "load" or "save". // Number of zdialogs is returned. int zdialog_inputs(ch *action) // sequence numbers added 25.1 { using namespace zdinputs_names; ch zdinputsfile[200], buff[ccmax2]; ch zdtitle[ccmax1], wname[Nwmax][ccmax1], wdata[Nwmax][ccmax2]; ch *pp, *pp1, *pp2, wdata2[ccmax2+50]; FILE *fid; int Nw, ii, jj, cc, cc1, cc2; int seq; if (! main_thread()) zappcrash("illegal call from thread"); snprintf(zdinputsfile,200,"%s/zdialog_inputs",zhomedir); // /home//.appname/zdialog_inputs if (strmatch(action,"load")) // load dialog input fields from its file { Nzd = seq = 0; fid = fopen(zdinputsfile,"r"); // no file if (! fid) return 0; while (true) { pp = fgets_trim(buff,ccmax2,fid,1); // read next zdialog title record if (! pp || seq++ != atoi(pp)) goto abend; pp += 4; if (strstr(pp,"EOF")) break; // normal EOF, OK if (! strmatchN(pp,"zdialog == ",11)) continue; // expect zdialog title record strncpy0(zdtitle,pp+11,ccmax1); // save new zdialog title pp = fgets_trim(buff,ccmax2,fid,1); // read next zdialog title record if (! pp || seq++ != atoi(pp)) goto abend; pp += 4; Nw = atoi(pp); // dialog widget count if (Nw < 1 || Nw > Nwmax) goto abend; for (ii = 0; ii < Nw; ii++) // read widget data recs { pp = fgets_trim(buff,ccmax2,fid,1); // read next zdialog title record if (! pp || seq++ != atoi(pp)) goto abend; pp += 4; pp1 = pp; pp2 = strstr(pp1," =="); if (! pp2) break; // widget has no data cc1 = pp2 - pp1; pp1[cc1] = 0; pp2 += 3; if (*pp2 == ' ') pp2++; cc2 = strlen(pp2); if (cc1 < 1 || cc1 >= ccmax1) break; if (cc2 < 1) pp2 = ""; if (cc2 >= ccmax2) break; // do not copy large inputs strcpy(wname[ii],pp1); // save widget name and data strcpy(wdata2,pp2); repl_1str(wdata2,wdata[ii],ccmax2,"\\n","\n"); // replace "\n" with newline chars. } if (ii < Nw) goto abend; if (Nzd == Nzdmax) { Plog(0,"zdialog_inputs() overflow \n"); break; } zdinputs[Nzd].zdtitle = zstrdup(zdtitle,"zdialog_inputs"); // save acculumated zdialog data zdinputs[Nzd].Nw = Nw; cc = Nw * sizeof(ch *); zdinputs[Nzd].wname = (ch **) zmalloc(cc,"zdialog_inputs"); zdinputs[Nzd].wdata = (ch **) zmalloc(cc,"zdialog_inputs"); for (ii = 0; ii < Nw; ii++) { zdinputs[Nzd].wname[ii] = zstrdup(wname[ii],"zdialog_inputs"); zdinputs[Nzd].wdata[ii] = zstrdup(wdata[ii],"zdialog_inputs"); } Nzd++; } fclose(fid); return Nzd; abend: Plog(0,"zdialog_inputs file corrupted - deleted \n"); // 25.1 fclose(fid); remove(zdinputsfile); Nzd = 0; return 0; } if (strmatch(action,"save")) // save dialog input fields to its file { fid = fopen(zdinputsfile,"w"); if (! fid) { Plog(0,"zdialog_inputs() cannot write file \n"); return 0; } seq = 0; for (ii = 0; ii < Nzd; ii++) { fprintf(fid,"%03d zdialog == %s \n",seq++,zdinputs[ii].zdtitle); // zdialog == zdialog title Nw = zdinputs[ii].Nw; fprintf(fid,"%03d %d \n",seq++,Nw); // widget count for (jj = 0; jj < Nw; jj++) { pp1 = zdinputs[ii].wname[jj]; // widget name == widget data pp2 = zdinputs[ii].wdata[jj]; repl_1str(pp2,wdata2,ccmax2+50,"\n","\\n"); // replace newline chars. with "\n" fprintf(fid,"%03d %s == %s \n",seq++,pp1,wdata2); } fprintf(fid,"%03d \n",seq++); } fprintf(fid,"%03d EOF",seq); fclose(fid); return Nzd; } Plog(0,"zdialog_inputs bad action: %s \n",action); return 0; } // Save dialog user input fields when a dialog is finished. // Called automatically by zdialog_free(). Private function. int zdialog_save_inputs(zdialog *zd) { using namespace zdinputs_names; ch zdtitle[ccmax1], wname[ccmax1], wdata[ccmax2]; ch *wnamex, *type; int ii, jj, Nw, cc; ch *skipwidgets = "dialog hbox vbox hsep vsep frame scrwin" // non-input widgets to omit "label link button zbutton"; ch *skipexceptions = "searchtags"; // fotocx kludge if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 0; if (! zd->saveinputs) return 0; // zdialog does not use this service strncpy0(zdtitle,zd->widget[0].data,ccmax1); // zdialog title is widget[0].data for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs table if (strmatch(zdtitle,zdinputs[ii].zdtitle)) break; if (ii < Nzd) { // found zfree(zdinputs[ii].zdtitle); // delete obsolete zdinputs data for (jj = 0; jj < zdinputs[ii].Nw; jj++) { zfree(zdinputs[ii].wname[jj]); zfree(zdinputs[ii].wdata[jj]); } zfree(zdinputs[ii].wname); zfree(zdinputs[ii].wdata); Nzd--; // decr. zdialog count for (NOP; ii < Nzd; ii++) // pack down the rest zdinputs[ii] = zdinputs[ii+1]; } if (Nzd == Nzdmax) { Plog(0,"zdialog_save_inputs, too many zdialogs \n"); return 0; } ii = Nzd; // next zdinputs table entry for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) { // count zdialog widgets wnamex = (ch *) zd->widget[jj].wname; type = (ch *) zd->widget[jj].type; if (strstr(skipwidgets,type)) // skip non-input widgets if (! strstr(skipexceptions,wnamex)) continue; Nw++; } if (! Nw) return 0; // no input widgets if (Nw > Nwmax) { Plog(0,"zdialog_save_inputs() bad data: %s \n",zdtitle); return 0; } zdinputs[ii].zdtitle = zstrdup(zdtitle,"zdialog_save"); // set zdialog title cc = Nw * sizeof(ch *); // allocate pointers for widgets zdinputs[ii].wname = (ch **) zmalloc(cc,"zdialog_save"); zdinputs[ii].wdata = (ch **) zmalloc(cc,"zdialog_save"); for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) { // add widget names and data wnamex = (ch *) zd->widget[jj].wname; type = (ch *) zd->widget[jj].type; if (strstr(skipwidgets,type)) // skip non-input widgets if (! strstr(skipexceptions,wnamex)) continue; strncpy0(wname,zd->widget[jj].wname,ccmax1); if (zd->widget[jj].data) strncpy0(wdata,zd->widget[jj].data,ccmax2); else strcpy(wdata,""); zdinputs[ii].wname[Nw] = zstrdup(wname,"zdialog_save"); zdinputs[ii].wdata[Nw] = zstrdup(wdata,"zdialog_save"); Nw++; } zdinputs[ii].Nw = Nw; // set widget count Nzd++; // add zdialog to end of zdinputs return 1; } // Restore user input fields from prior use of the same dialog. // Call this if wanted after zdialog is built and before it is run. // Override old user inputs with zdialog_stuff() where needed. int zdialog_load_inputs(zdialog *zd) { using namespace zdinputs_names; ch *zdtitle, *wname, *wdata; int ii, jj; zd->saveinputs = 1; // flag, save data at zdialog_free() if (! main_thread()) zappcrash("illegal call from thread"); zdtitle = (ch *) zd->widget[0].data; // zdialog title for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs if (strmatch(zdtitle,zdinputs[ii].zdtitle)) break; if (ii == Nzd) return 0; // not found for (jj = 0; jj < zdinputs[ii].Nw; jj++) { // stuff all saved widget data wname = zdinputs[ii].wname[jj]; wdata = zdinputs[ii].wdata[jj]; zdialog_put_data(zd,wname,wdata); } return 1; } /********************************************************************************/ // get text input from a popup dialog - multiple lines can be entered // returned text is subject for zfree() // null is returned if user presses [cancel] button. ch * zdialog_text(GtkWidget *parent, ch *title, ch *inittext) { zdialog *zd; int zstat; ch *text; if (! main_thread()) zappcrash("illegal call from thread"); zd = zdialog_new(title,parent,"OK","Cancel",null); zdialog_add_widget(zd,"frame","fred","dialog"); zdialog_add_widget(zd,"zedit","text","fred"); if (inittext) zdialog_stuff(zd,"text",inittext); zdialog_resize(zd,300,0); zdialog_set_modal(zd); zdialog_run(zd,0,"mouse"); zstat = zdialog_wait(zd); if (zstat == 1) text = (ch *) zdialog_get_data(zd,"text"); else text = 0; if (text) text = zstrdup(text,"zdialog_text"); zdialog_free(zd); return text; } /********************************************************************************/ // get text input from a popup dialog - one line only // returned text is subject for zfree() // null is returned if user presses [cancel] button. ch * zdialog_text1(GtkWidget *parent, ch *title, ch *inittext) { zdialog *zd; int zstat; ch *text; if (! main_thread()) zappcrash("illegal call from thread"); zd = zdialog_new(title,parent,"OK","Cancel",null); zdialog_add_widget(zd,"zentry","text","dialog",0); if (inittext) zdialog_stuff(zd,"text",inittext); zdialog_resize(zd,300,0); zdialog_set_modal(zd); zdialog_run(zd,0,"mouse"); zstat = zdialog_wait(zd); if (zstat == 1) text = (ch *) zdialog_get_data(zd,"text"); else text = 0; if (text) text = zstrdup(text,"zdialog_text"); zdialog_free(zd); return text; } /********************************************************************************/ // get password input from a popup dialog - one line only // user input is hidden as '*' characters. // returned text is subject for zfree() // null is returned if user presses [cancel] button. ch * zdialog_password(GtkWidget *parent, ch *title, ch *inittext) { int zdialog_password_event(zdialog *zd, ch *event); zdialog *zd; int zstat; ch *text; GtkWidget *widget; if (! main_thread()) zappcrash("illegal call from thread"); zd = zdialog_new(title,parent,"OK","Cancel",null); zdialog_add_widget(zd,"entry","text","dialog",0); widget = zdialog_gtkwidget(zd,"text"); gtk_entry_set_visibility(GTK_ENTRY(widget),0); if (inittext) zdialog_stuff(zd,"text",inittext); zdialog_resize(zd,300,0); zdialog_set_modal(zd); zdialog_run(zd,zdialog_password_event,"mouse"); zstat = zdialog_wait(zd); if (zstat == 1) text = (ch *) zdialog_get_data(zd,"text"); else text = 0; if (text) text = zstrdup(text,"zdialog_password"); zdialog_free(zd); return text; } int zdialog_password_event(zdialog *zd, ch *event) { if (strmatch(event,"activate")) zd->zstat = 1; return 1; } /********************************************************************************/ // Display a dialog with 1-5 radio buttons with text labels . // Button 1 is the default and is pre-selected. // Returns choice 1-5 corresponding to button selected. // Returns -1 if cancel button [x] or completion button [X] is selected. // // nn = zdialog_choose(parent, where, message, text1, text2, ... null) // 'where' is null: window manager decides dialog placement // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window namespace zdialog_choose_names { int button; // button pushed 1-5 } int zdialog_choose(GtkWidget *parent, ch *where, ch *message, ...) // improved 25.0 { using namespace zdialog_choose_names; int zdialog_choose_event(zdialog *zd, ch *event); zdialog *zd; va_list arglist; int ii, Ntext, zstat; ch *text[5]; ch butt[4] = "B0"; if (! main_thread()) zappcrash("illegal call from thread"); button = 1; // default va_start(arglist,message); for (ii = 0; ii < 5; ii++) { text[ii] = va_arg(arglist,ch *); if (! text[ii] || text[ii] == (ch *) 0x100000000) break; // ARM bug, funny null } Ntext = ii; if (! Ntext) zappcrash("zdialog_choose(), no buttons"); zd = zdialog_new("choose",parent,"OK"," X ",null); zdialog_add_widget(zd,"label","labmess","dialog",message,"space=5"); for (ii = 0; ii < Ntext; ii++) { butt[1] = '1' + ii; // button names: "B1" "B2" etc. zdialog_add_widget(zd,"radio",butt,"dialog",text[ii],"space=2"); } zdialog_set_modal(zd); zdialog_run(zd,zdialog_choose_event,where); zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat != 1) button = -1; return button; } // dialog event and completion function int zdialog_choose_event(zdialog *zd, ch *event) { using namespace zdialog_choose_names; if (! main_thread()) zappcrash("illegal call from thread"); if (strstr("B1 B2 B3 B4 B5",event)) button = event[1] - '0'; // set button pushed 1-5 if (strmatch(event,"escape")) zd->zstat = -2; // escape key if (zd->zstat) zdialog_destroy(zd); // [-] [OK] [X] return 1; } /********************************************************************************/ // popup zdialog to edit a text file // returns 0 if OK or user cancel, +N if error int zdialog_edit_textfile(GtkWidget *parent, char *file) // 24.30 { zdialog *zd; int cc, err, zstat, maxcc = 1000000; // max. file size char *pp, *title, buff[1000001]; FILE *fid; if (! main_thread()) zappcrash("illegal call from thread"); snprintf(buff,maxcc,"du -b %s",file); // command to get file size fid = popen(buff,"r"); // run 'du' command if (! fid) goto filerr; pp = fgets(buff,100,fid); // read command output pclose(fid); if (! pp) goto filerr; cc = atoi(pp); // file size if (cc < 0 || cc > maxcc) { // reject file > 1 MB Plog(0,"file size > %d: %s \n",maxcc,file); return 1; } fid = fopen(file,"r"); // open file if (! fid) goto filerr; for (cc = 0; ; ) { pp = fgets(buff+cc,maxcc-cc,fid); // read entire file if (! pp) break; cc += strlen(pp); } fclose(fid); title = strrchr(file,'/'); // get root file name if (title) title++; else title = file; zd = zdialog_new(title,parent,"Apply"," X ",null); // create text edit dialog zdialog_add_widget(zd,"scrwin","scroll","dialog",0,"expand"); zdialog_add_widget(zd,"zedit","text","scroll",0,"expand"); zdialog_stuff(zd,"text",buff); // stuff file text into dialog zdialog_resize(zd,500,400); zdialog_run(zd,0,"parent"); // run dialog zstat = zdialog_wait(zd); // wait for completion if (zstat != 1) { zdialog_free(zd); // user cancel return 0; } zdialog_fetch(zd,"text",buff,maxcc); // get edited text zdialog_free(zd); cc = strlen(buff); if (cc > maxcc) { Plog(0,"edited file size > %d: %s \n",maxcc,file); return 1; } fid = fopen(file,"w"); if (! fid) goto filerr; err = fputs(buff,fid); if (err < 0 || err == EOF) goto filerr; err = fclose(fid); if (err) goto filerr; return 0; filerr: Plog(0,"file error: %s %s \n",strerror(errno),file); return 1; } /******************************************************************************** popup window with scrolling text report line numbers and line positions are zero based open the report window with given title and pixel dimensions Fheader add optional non-scrolling header at top of report window CBfunc optional callback function: void CBfunc(GtkWidget *, int line, int posn, ch *input) ... optional dialog completion buttons terminated with null: [OK] [X] [Find] [Save] [Esc] are processed here others are passed to callback function ( --> input) zdialog->zstat = 1/2 for buttons [OK] / [X] ***/ zdialog * popup_report_open(ch *title, GtkWidget *parent, int ww, int hh, int wrap, int Fheader, txwidget_CBfunc_t CBfunc, ...) { int popup_report_dialog_event(zdialog *zd, ch *event); va_list arglist; ch *butn[6]; // up to 5 buttons + null int ii, NB; zdialog *zd; GtkWidget *mHead, *mText; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,CBfunc); // get button args, if any for (ii = 0; ii < 5; ii++) { // up to 5 buttons 25.1 butn[ii] = va_arg(arglist,ch *); if (! butn[ii] || butn[ii] == (ch *) 0x100000000) break; // ARM bug } NB = ii; // no. buttons zd = zdialog_new(title,parent,null); if (Fheader) { // non-scrolling header zdialog_add_widget(zd,"text","header","dialog"); zdialog_add_widget(zd,"hsep","hsep","dialog"); } zdialog_add_widget(zd,"scrwin","scroll","dialog",0,"expand"); // scrolling text window for report if (wrap) zdialog_add_widget(zd,"report","text","scroll",0,"expand|wrap"); // text > report else zdialog_add_widget(zd,"report","text","scroll",0,"expand"); if (NB) { // optional event buttons zdialog_add_widget(zd,"hbox","hbbutn","dialog"); zdialog_add_widget(zd,"label","space","hbbutn",0,"expand"); for (ii = 0; ii < NB; ii++) zdialog_add_widget(zd,"button",butn[ii],"hbbutn",butn[ii],"space=5"); } zdialog_resize(zd,ww,hh); // show report dialog box zdialog_run(zd,popup_report_dialog_event,"save"); // keep window size and position if (Fheader) { mHead = zdialog_gtkwidget(zd,"header"); // header initially invisible gtk_widget_set_visible(mHead,0); } mText = zdialog_gtkwidget(zd,"text"); // report text not editable gtk_widget_grab_focus(mText); if (CBfunc) { // 25.1 txwidget_set_eventfunc(mText,CBfunc); // set mouse/KB event function zd->popup_report_CB = (void *) CBfunc; } return zd; } // dialog event and completion function [OK] [ X ] [Find] [Save] [Esc] int popup_report_dialog_event(zdialog *zd, ch *event) { txwidget_CBfunc_t *CBfunc; GtkWidget *mText; static ch findtext[40] = ""; int linem, line1, line2; zdialog *zdf; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return 1; // report cancelled if (strmatch(event,"focus")) return 0; if (zd->zstat) { // [x] cancel or escape, kill dialog zdialog_free(zd); return 1; } if (strstr(" OK X escape",event)) { // kill report window 25.0 zdialog_free(zd); return 1; } if (strmatch(event,"Find")) { // [Find] zdf = zdialog_new("find text",zd->dialog,"Find","Cancel",0); // popup dialog to enter text zdialog_add_widget(zdf,"zentry","text","dialog",findtext,"size=20"); zdialog_run(zdf,0,"mouse"); linem = -1; // no match line yet while (true) { zdialog_wait(zdf); if (zdf->zstat != 1) { // [cancel] zdialog_free(zdf); return 1; } zdf->zstat = 0; zdialog_fetch(zdf,"text",findtext,40); // get text popup_report_get_visible_lines(zd,line1,line2); // lines now visible if (linem < 0) linem = line1; // search from 1st visible line linem = popup_report_find(zd,findtext,linem); // search for text if (linem < 0) continue; // not found popup_report_scroll_top(zd,linem); // found, scroll to top linem++; // next search from line } } if (strmatch(event,"Save")) { // [Save] save text to file mText = zdialog_gtkwidget(zd,"text"); txwidget_save(mText,GTK_WINDOW(zd->parent)); return 1; } mText = zdialog_gtkwidget(zd,"text"); CBfunc = (txwidget_CBfunc_t *) zd->popup_report_CB; // other event if (CBfunc) CBfunc(mText,-1,-1,event); // pass to callback function return 1; } // write a non-scrolling header line void popup_report_header(zdialog *zd, int bold, ch *format, ...) { va_list arglist; ch message[1000]; GtkWidget *mHead; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mHead = zdialog_gtkwidget(zd,"header"); txwidget_append(mHead,bold,message); gtk_widget_set_visible(mHead,1); return; } // write a new text line at the end void popup_report_write(zdialog *zd, int bold, ch *format, ...) { va_list arglist; ch message[20000]; GtkWidget *mText; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } va_start(arglist,format); vsnprintf(message,19999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); txwidget_append(mText,bold,"%s",message); return; } // write a new text line at the end, scroll down to end void popup_report_write2(zdialog *zd, int bold, ch *format, ...) { va_list arglist; ch message[20000]; GtkWidget *mText; if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } va_start(arglist,format); vsnprintf(message,19999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); txwidget_append2(mText,bold,"%s",message); return; } // scroll window back to top line void popup_report_top(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_scroll(mText,0); return; } // scroll window back to bottom line void popup_report_bottom(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_scroll(mText,-1); return; } // clear the report window void popup_report_clear(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_clear(mText); return; } // clear the report window from line to end void popup_report_clear(zdialog *zd, int line) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_clear(mText,line); return; } // insert a new line after a given line void popup_report_insert(zdialog *zd, int bold, int line, ch *format, ...) { va_list arglist; ch message[20000]; GtkWidget *mText; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(message,19999,format,arglist); va_end(arglist); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } mText = zdialog_gtkwidget(zd,"text"); txwidget_insert(mText,bold,line,message); return; } // replace a given line void popup_report_replace(zdialog *zd, int bold, int line, ch *format, ...) { va_list arglist; ch message[20000]; GtkWidget *mText; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(message,19999,format,arglist); va_end(arglist); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } mText = zdialog_gtkwidget(zd,"text"); txwidget_replace(mText,bold,line,message); return; } // delete a given line void popup_report_delete(zdialog *zd, int line) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_delete(mText,line); return; } // find first line of text containing characters matching input string // search is from line1 to end, then from 0 to line1-1 // returns first matching line or -1 if none // comparison is not case sensitive int popup_report_find(zdialog *zd, ch *matchtext, int line1) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return 1; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return txwidget_find(mText,matchtext,line1,1); // highlight line } // insert a pixbuf image after a given line void popup_report_insert_pixbuf(zdialog *zd, int line, GdkPixbuf *pixbuf) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_insert_pixbuf(mText,line,pixbuf); return; } // scroll to bring a given line into the report window void popup_report_scroll(zdialog *zd, int line) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_scroll(mText,line); return; } // scroll to bring a given line to the top of the report window void popup_report_scroll_top(zdialog *zd, int line) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_scroll_top(mText,line); return; } // get the range of visible lines in the report window void popup_report_get_visible_lines(zdialog *zd, int &vtop, int &vbott) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_get_visible_lines(mText,vtop,vbott); return; } // retrieve a given line and optionally strip the trailing \n ch * popup_report_line(zdialog *zd, int line, int strip) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return 0; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return txwidget_line(mText,line,strip); } // retrieve the word starting at a given position in a given line ch * popup_report_word(zdialog *zd, int line, int posn, ch *dlims, ch &end) { if (! main_thread()) zappcrash("illegal call from thread"); GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return txwidget_word(mText,line,posn,dlims,end); } // highlight a given line of text void popup_report_highlight_line(zdialog *zd, int line) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_highlight_line(mText,line); return; } // highlight the text at a given position and length in a given line void popup_report_highlight_word(zdialog *zd, int line, int posn, int cc) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_highlight_word(mText,line,posn,cc); return; } // underline the text at a given position and length in a given line void popup_report_underline_word(zdialog *zd, int line, int posn, int cc) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_underline_word(mText,line,posn,cc); return; } // bold the text at a given position and length in a given line void popup_report_bold_word(zdialog *zd, int line, int posn, int cc) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) { Plog(1,"report cancelled \n"); return; } GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_bold_word(mText,line,posn,cc); return; } // set font attributes for entire report // temp. kludge void popup_report_font_attributes(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; GtkWidget *mText = zdialog_gtkwidget(zd,"text"); txwidget_font_attributes(mText); return; } // close report after given seconds (OK to leave it open until user closes) // also connected to window destroy signal (secs = 0) void popup_report_close(zdialog *zd, int secs) { void popup_report_timeout(zdialog *zd); if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; if (secs < 1) { zdialog_free(zd); return; } g_timeout_add_seconds(secs,(GSourceFunc) popup_report_timeout,zd); return; } // private function for report timeout void popup_report_timeout(zdialog *zd) { if (! main_thread()) zappcrash("illegal call from thread"); if (! zdialog_valid(zd)) return; zdialog_free(zd); return; } /********************************************************************************/ // execute a shell command and show the output in a scrolling popup window // returns: 0 = EOF 1 = command failure int popup_command(ch *command, int ww, int hh, GtkWidget *parent, int top) { FILE *fid; ch buff[1000], *pp; zdialog *zd; if (! main_thread()) zappcrash("illegal call from thread"); Plog(1,"run command: %s \n",command); zd = popup_report_open(command,parent,ww,hh,0,0,0,"Find","Save","X",0); fid = popen(command,"r"); if (! fid) return 1; while (true) { pp = fgets_trim(buff,1000,fid); if (! pp) break; popup_report_write2(zd,0,"%s\n",pp); } pclose(fid); if (top) popup_report_top(zd); // back to top of window return 0; } /********************************************************************************/ // show a text file in a popup window // show appended records in real-time until window is closed // return: 0 = OK // 1 = not a text file // 2 = cannot open popup report window // 3 = cannot open file int monitor_file(ch *file) // 25.1 { zdialog *zd; FILE *fid; ch buff[500]; ch *title, *pp; if (! main_thread()) zappcrash("illegal call from thread"); Plog(0,"monitor file: %s \n",file); snprintf(buff,500,"file \"%s\" ",file); // check for text file fid = popen(buff,"r"); pp = fgets(buff,500,fid); pclose(fid); if (! pp || ! strstr(pp,"text")) return 1; title = strrchr(file,'/') + 1; zd = popup_report_open(title,0,600,400,1,0,0,"Find","Save","X",0); if (! zd) return 2; snprintf(buff,500,"tail -f \"%s\" ",file); fid = popen(buff,"r"); if (! fid) return 3; while (true) { zmainsleep(0.01); if (! zdialog_valid(zd)) break; // popup window closed, exit pp = fgets_pend(buff,500,fid); // read next log record if (! pp) break; // EOF or error, exit if (strmatch(pp,"fgets_pend\n")) continue; // next record not available yet popup_report_write2(zd,0,"%s",pp); // output record to window } pclose(fid); exit(0); } /********************************************************************************/ // Display popup message box and wait for user acknowledgement. // Messages are presented sequentially from main() and from threads. namespace zmessageACK_names { int Finit = 0; ch buffer[2000]; GtkWidget *Pwindow; } void zmessageACK(GtkWidget *parent, ch *format, ... ) // rewrite for threads 25.1 { using namespace zmessageACK_names; va_list arglist; ch message[2000]; va_start(arglist,format); // format the message vsnprintf(message,2000,format,arglist); va_end(arglist); Plog(1,"%s \n",message); // output to log file while (*buffer) zsleep(0.1); // wait for parallel caller strncpy0(buffer,message,2000); Pwindow = parent; while (* buffer) zmainsleep(0.1); // wait for this caller response return; } void zmessageACK_init() // 25.1 { using namespace zmessageACK_names; int zmessageACK_timerfunc(void *); if (! Finit) { // initz. message processor Finit = 1; *buffer = 0; g_timeout_add(100,zmessageACK_timerfunc,0); } return; } int zmessageACK_timerfunc(void *) // loop every 0.1 sec. { using namespace zmessageACK_names; zdialog *zd; ch *posn; if (! *buffer) return 1; if (Fshutdown) return 0; if (Pwindow) posn = "parent"; else posn = "desktop"; zd = zdialog_new("ACK",Pwindow,"OK",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"text","lab1","hb1",buffer,"space=5"); zdialog_resize(zd,200,0); zdialog_set_modal(zd); gtk_window_set_urgency_hint(GTK_WINDOW(zd->dialog),1); zdialog_set_decorated(zd,0); zdialog_run(zd,0,posn); zdialog_wait(zd); zdialog_free(zd); *buffer = 0; return 1; } /********************************************************************************/ // display message box and wait for user Yes or No response // returns 1 or 0 int zmessageYN(GtkWidget *parent, ch *format, ... ) { va_list arglist; ch message[500]; ch *posn; zdialog *zd; int zstat; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(message,500,format,arglist); va_end(arglist); Plog(1,"%s \n",message); // output to log file if (parent) posn = "parent"; else posn = "desktop"; zd = zdialog_new("YN",parent,"Yes","No",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); zdialog_set_modal(zd); zdialog_set_decorated(zd,0); // 24.40 zdialog_run(zd,0,posn); zstat = zdialog_wait(zd); zdialog_free(zd); zmainloop(); // 24.10 if (zstat == 1) return 1; return 0; } /********************************************************************************/ // display message until timeout (can be forever) or user cancel // or caller kills it with zdialog_free() // posn - from zdialog_run(): // "mouse" = position at mouse // "desktop" = center on desktop // "parent" = center on parent window // "nn/nn" = position NW corner at relative x/y position in parent window, // where nn/nn is a percent 0-100 of the parent window dimensions. // seconds: time to keep message on screen, 0 = forever until cancelled typedef struct { zdialog *zd; int uniqueID; } zdx_t; zdialog * zmessage_post(GtkWidget *parent, ch *posn, int seconds, ch *format, ... ) { int zmessage_post_timeout(zdx_t *zdx); va_list arglist; ch message[400], messagebold[460]; static zdx_t zdx[100]; static int ii = 0; zdialog *zd; if (! main_thread()) zappcrash("illegal call from thread"); va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); Plog(1,"%s \n",message); // output to log file snprintf(messagebold,460,"%s",message); zd = zdialog_new("post",parent,null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",messagebold,"space=5"); zdialog_can_focus(zd,0); zdialog_run(zd,0,posn); // mouse position zdialog_set_decorated(zd,0); // 24.40 zdialog_present(zd); if (seconds) { if (ii < 99) ii++; // track unique zdialogs else ii = 0; zdx[ii].zd = zd; zdx[ii].uniqueID = zd->uniqueID; g_timeout_add_seconds(seconds,(GSourceFunc) zmessage_post_timeout,&zdx[ii]); } return zd; } int zmessage_post_timeout(zdx_t *zdx) { zdialog *zd = zdx->zd; // check unique zdialog active if (! zdialog_valid2(zd,"post")) return 0; if (zd->uniqueID != zdx->uniqueID) return 0; zdialog_free(zd); return 0; } /********************************************************************************/ // functions to show popup text windows namespace poptext { ch *ptext = 0; GtkWidget *popwin = 0; ch *pcurrent = 0; #define GSFNORMAL GTK_STATE_FLAG_NORMAL } // timer function to show popup window after a given time int poptext_show(ch *current) { using namespace poptext; if (! main_thread()) zappcrash("illegal call from thread"); if (current != pcurrent) return 0; if (popwin) gtk_widget_show_all(popwin); return 0; } // timer function to kill popup window after a given time int poptext_timeout(ch *current) { using namespace poptext; if (! main_thread()) zappcrash("illegal call from thread"); if (current != pcurrent) return 0; poptext_killnow(); // 25.0 return 0; } // Show a popup text message at a given absolute screen position. // Any prior popup will be killed and replaced. // If text == null, kill without replacement. // secs1 is time to delay before showing the popup. // secs2 is time to kill the popup after it is shown (0 = never). // This function returns immediately. void poptext_screen(ch *text, int px, int py, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int millisec1, millisec2; if (! main_thread()) zappcrash("illegal call from thread"); poptext_killnow(); pcurrent++; // make current != pcurrent if (! text) return; popwin = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(text); gtk_container_set_border_width(GTK_CONTAINER(popwin),5); // 25.0 gtk_container_add(GTK_CONTAINER(popwin),label); gtk_window_move(GTK_WINDOW(popwin),px,py); if (secs1 > 0) { // delayed popup display millisec1 = secs1 * 1000; g_timeout_add(millisec1,(GSourceFunc) poptext_show,pcurrent); } else gtk_widget_show_all(popwin); // immediate display if (secs2 > 0) { // popup kill timer millisec2 = (secs1 + secs2) * 1000; g_timeout_add(millisec2,(GSourceFunc) poptext_timeout,pcurrent); } return; } // Show a popup text message at current mouse position + offsets. void poptext_mouse(ch *text, int dx, int dy, float secs1, float secs2) { using namespace poptext; int mx, my; ch *ptext2; GtkWidget *popwin2; if (! main_thread()) zappcrash("illegal call from thread"); if (! text) { poptext_killnow(); return; } popwin2 = popwin; ptext2 = ptext; popwin = 0; ptext = 0; gdk_device_get_position(zfuncs::mouse,0,&mx,&my); // mouse screen position poptext_screen(text,mx+dx,my+dy,secs1,secs2); // add displacements zmainloop(); if (popwin2) gtk_widget_destroy(popwin2); // kill prior after create new 24.60 if (ptext2) zfree(ptext2); // (prevent flicker) return; } // Show a popup text message at the given widget position. void poptext_widget(GtkWidget *widget, ch *text, int dx, int dy, float secs1, float secs2) { GdkWindow *win; int px, py; if (! main_thread()) zappcrash("illegal call from thread"); if (! text) { poptext_killnow(); return; } win = gtk_widget_get_window(widget); gdk_window_get_origin(win,&px,&py); poptext_screen(text,px+dx,py+dy,secs1,secs2); return; } // kill popup window unconditionally int poptext_killnow() { using namespace poptext; if (popwin) gtk_widget_destroy(popwin); if (ptext) zfree(ptext); popwin = 0; ptext = 0; return 0; } /******************************************************************************** File chooser dialog for one or more files Action: "file" select an existing file "files" select multiple existing files "save" select an existing or new file "folder" select existing folder "folders" select multiple existing folders "create folder" select existing or new folder hidden if > 0, add button to toggle display of hidden files optional, default = 0 Returns a list of filespecs terminated with null. Memory for returned list and returned files are subjects for zfree(); *********************************************************************************/ // version for 1 file only: file, save, folder, create folder // returns one filespec or null // returned file is subject for zfree() ch * zgetfile(ch *title, GtkWindow *parent, ch *action, ch *initfile, int hidden) { if (! main_thread()) zappcrash("illegal call from thread"); if (! strmatchV(action,"file","save","folder","create folder",null)) zappcrash("zgetfile() call error: %s",action); ch **flist = zgetfiles(title,parent,action,initfile,hidden); if (! flist) return 0; ch *file = *flist; zfree(flist); return file; } // version for 2 or more files // returns a list of filespecs (ch **) terminated with null // returns null if canceled by user ch ** zgetfiles(ch *title, GtkWindow *parent, ch *action, ch *initfile, int hidden) { void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget); // private functions int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event, int &fcdes); void zgetfile_newfolder(GtkFileChooser *dialog, void *); GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN; GtkWidget *dialog; PIXBUF *thumbnail; GtkWidget *pvwidget = gtk_image_new(); GSList *gslist = 0; ch *button1 = 0, *buttxx = 0; ch *pdir, *pfile; int ii, err, NF, setfname = 0; int fcstat, bcode = 0, hide = 1; int fcdes = 0; ch *file1, *file2, **flist = 0; STATB statB; if (! main_thread()) zappcrash("illegal call from thread"); if (strmatch(action,"file")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = "choose file"; } else if (strmatch(action,"files")) { fcact = GTK_FILE_CHOOSER_ACTION_OPEN; button1 = "choose files"; } else if (strmatch(action,"save")) { fcact = GTK_FILE_CHOOSER_ACTION_SAVE; button1 = "Save"; setfname = 1; } else if (strmatch(action,"folder")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = "choose folder"; } else if (strmatch(action,"folders")) { fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; button1 = "choose folders"; } else if (strmatch(action,"create folder")) { fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER; button1 = "create folder"; setfname = 1; } else zappcrash("zgetfiles() call error: %s",action); if (hidden) { buttxx = "hidden"; bcode = 103; } dialog = gtk_file_chooser_dialog_new(title, parent, fcact, // create file selection dialog button1, GTK_RESPONSE_ACCEPT, // parent added "Cancel", GTK_RESPONSE_CANCEL, buttxx, bcode, null); gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget); G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget); // create preview for selected file G_SIGNAL(dialog,"key-press-event",zgetfile_KBkey,&fcdes); // respond to special KB keys gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE); // put dialog at mouse position gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0); // default: no show hidden if (strmatch(action,"save")) // overwrite confirmation gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1); if (strmatch(action,"files") || strmatch(action,"folders")) gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),1); // select multiple files or folders if (initfile) { // pre-select filespec err = stat(initfile,&statB); if (err) { pdir = zstrdup(initfile,"zgetfiles"); // non-existent file pfile = strrchr(pdir,'/'); if (pfile && pfile > pdir) { *pfile++ = 0; // set folder name gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),pdir); } if (setfname) { // set new file name if (! pfile) pfile = (ch *) initfile; gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile); } zfree(pdir); } else if (S_ISREG(statB.st_mode)) // select given file gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile); else if (S_ISDIR(statB.st_mode)) // select given folder gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile); } if (initfile) { thumbnail = get_thumbnail(initfile,256); // preview for initial file if (thumbnail) { gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail); gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1); g_object_unref(thumbnail); } else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); } gtk_widget_show_all(dialog); while (true) { fcstat = gtk_dialog_run(GTK_DIALOG(dialog)); // run dialog, get status button if (fcstat == 103) { // show/hide hidden files hide = 1 - hide; gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide); continue; } else if (fcstat == GTK_RESPONSE_ACCEPT) { gslist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); if (! gslist) continue; NF = g_slist_length(gslist); // no. selected files flist = (ch **) zmalloc((NF+1)*sizeof(ch *),"zgetfiles"); // allocate returned list for (ii = 0; ii < NF; ii++) { // process selected files file1 = (ch *) g_slist_nth_data(gslist,ii); file2 = zstrdup(file1,"zgetfiles"); // re-allocate memory flist[ii] = file2; g_free(file1); } flist[ii] = 0; // EOL marker break; } else break; // user bailout } if (gslist) g_slist_free(gslist); // return selected file(s) if (! fcdes) gtk_widget_destroy(dialog); // destroy if not already return flist; } // zgetfile private function - get preview images for image files void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget) { PIXBUF *thumbnail; ch *filename; filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog)); if (! filename) { gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } thumbnail = get_thumbnail(filename,256); // 256x256 pixels g_free(filename); if (thumbnail) { gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail); gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1); g_object_unref(thumbnail); } else gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0); return; } // zgetfile private function - KB functions int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event, int &fcdes) { int KBkey = event->keyval; if (KBkey == GDK_KEY_F1) { // F1 = help KBevent(event); return 1; } if (KBkey == GDK_KEY_Escape) { // escape = cancel gtk_widget_destroy(dialog); fcdes = 1; return 1; } return 0; } /********************************************************************************/ // select a folder (or create a new folder) // returns location (pathname) of selected or created folder. // returned location is subject for zfree(). ch * zgetfolder(ch *title, GtkWindow *parent, ch *initfolder) { GtkWidget *dialog; GtkFileChooser *chooser; int nn; ch *pp1, *pp2 = null; if (! main_thread()) zappcrash("illegal call from thread"); dialog = gtk_file_chooser_dialog_new(title, parent, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "Cancel", GTK_RESPONSE_CANCEL, "Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_filename(chooser, initfolder); nn = gtk_dialog_run(GTK_DIALOG(dialog)); if (nn != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dialog); return null; } pp1 = gtk_file_chooser_get_filename(chooser); if (pp1) { pp2 = zstrdup(pp1,"zgetfolder"); g_free(pp1); } gtk_widget_destroy(dialog); return pp2; } /******************************************************************************** print_image_file(GtkWidget *parent, ch *imagefile) Print an image file using the printer, paper, orientation, margins, and scale set by the user. HPLIP problem: Setting paper size was made less flexible. GtkPrintSettings paper size must agree with the one in the current printer setup. This can only be set in the printer setup dialog, not in the application. Also the print size (width, height) comes from the chosen paper size and cannot be changed in the application. Print margins can be changed to effect printing a smaller or shifted image on a larger paper size. *********************************************************************************/ namespace print_image { #define MM GTK_UNIT_MM #define INCH GTK_UNIT_INCH #define PRINTOP GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG #define PORTRAIT GTK_PAGE_ORIENTATION_PORTRAIT #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE #define QUALITY GTK_PRINT_QUALITY_HIGH GtkWidget *parent = 0; GtkPageSetup *priorpagesetup = 0; GtkPageSetup *pagesetup; GtkPrintSettings *printsettings = 0; GtkPrintOperation *printop; GtkPageOrientation orientation = PORTRAIT; PIXBUF *pixbuf; ch *printer = 0; int landscape = 0; // true if landscape double width = 21.0, height = 29.7; // paper size, CM (default A4 portrait) double margins[4] = { 0.5, 0.5, 0.5, 0.5 }; // margins, CM (default 0.5) double imagescale = 100; // image print scale, percent double pwidth, pheight; // printed image size int page_setup(); int margins_setup(); int margins_dialog_event(zdialog *zd, ch *event); void get_printed_image_size(); void print_page(GtkPrintOperation *, GtkPrintContext *, int page); } // user callable function to set paper, margins, scale, and then print void print_image_file(GtkWidget *pwin, ch *imagefile) { using namespace print_image; GtkPrintOperationResult printstat; GError *gerror = 0; int err; if (! main_thread()) zappcrash("illegal call from thread"); parent = pwin; // save parent window pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror); // read image file if (! pixbuf) { zmessageACK(mainwin,gerror->message); return; } err = page_setup(); // select size and orientation if (err) return; err = margins_setup(); // set margins and scale if (err) return; printop = gtk_print_operation_new(); // print operation gtk_print_operation_set_default_page_setup(printop,pagesetup); gtk_print_operation_set_print_settings(printop,printsettings); gtk_print_operation_set_n_pages(printop,1); g_signal_connect(printop,"draw-page",G_CALLBACK(print_page),0); // start print printstat = gtk_print_operation_run(printop,PRINTOP,0,0); if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) { gtk_print_operation_get_error(printop,&gerror); zmessageACK(mainwin,gerror->message); } g_object_unref(printop); return; } // draw the graphics for the print page // rescale with cairo void print_image::print_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) { using namespace print_image; cairo_t *cairocontext; double iww, ihh, pww, phh, scale; pww = gtk_print_context_get_width(printcontext); // print context size, pixels phh = gtk_print_context_get_height(printcontext); iww = gdk_pixbuf_get_width(pixbuf); // original image size ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale to fit page if (phh / ihh < scale) scale = phh / ihh; cairocontext = gtk_print_context_get_cairo_context(printcontext); // use cairo to rescale cairo_translate(cairocontext,0,0); cairo_scale(cairocontext,scale,scale); gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0); cairo_paint(cairocontext); return; } // Do a print paper format selection, after which the page width, height // and orientation are available to the caller. Units are CM. // (paper width and height are reversed for landscape orientation) int print_image::page_setup() { using namespace print_image; ch printsettingsfile[200], pagesetupfile[200]; snprintf(printsettingsfile,200,"%s/printsettings",zhomedir); snprintf(pagesetupfile,200,"%s/pagesetup",zhomedir); if (! printsettings) { // start with prior print settings printsettings = gtk_print_settings_new_from_file(printsettingsfile,0); if (! printsettings) printsettings = gtk_print_settings_new(); } if (! priorpagesetup) { // start with prior page setup priorpagesetup = gtk_page_setup_new_from_file(pagesetupfile,0); if (! priorpagesetup) priorpagesetup = gtk_page_setup_new(); } pagesetup = gtk_print_run_page_setup_dialog // select printer, paper, orientation (GTK_WINDOW(parent),priorpagesetup,printsettings); // user cancel cannot be detected g_object_unref(priorpagesetup); // save for next call priorpagesetup = pagesetup; orientation = gtk_print_settings_get_orientation(printsettings); // save orientation if (orientation == LANDSCAPE) landscape = 1; else landscape = 0; gtk_print_settings_set_quality(printsettings,QUALITY); // set high quality 300 dpi gtk_print_settings_set_resolution(printsettings,300); gtk_print_settings_to_file(printsettings,printsettingsfile,0); // save print settings to file gtk_page_setup_to_file(pagesetup,pagesetupfile,0); // save print settings to file return 0; } // Optionally set the print margins and print scale. // If canceled the margins are zero (or printer-dependent minimum) // and the scale is 100% (fitting the paper and margins). int print_image::margins_setup() { using namespace print_image; zdialog *zd; int zstat; /*** __________________________________________________ | [x] (-) [_] Margins | | | | Margins Top Bottom Left Right | | CM [ 0.50 ] [ 0.50 ] [ 0.50 ] [ 0.50 ] | | Inch [ 0.20 ] [ 0.20 ] [ 0.20 ] [ 0.20 ] | | | | image scale [ 80 ] percent | | | | image width height | | CM xx.x xx.x | | Inch xx.x xx.x | | [ OK ] [cancel] | |__________________________________________________| ***/ zd = zdialog_new("Margins",parent,"OK","Cancel",null); zdialog_add_widget(zd,"hbox","hbmlab","dialog"); zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"homog|space=3"); zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"homog|space=3"); zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"homog|space=3"); zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"homog|space=3"); zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"homog|space=3"); zdialog_add_widget(zd,"label","labmarg","vbmarg","Margins","space=5"); zdialog_add_widget(zd,"label","labcm","vbmarg","CM","space=5"); zdialog_add_widget(zd,"label","labinch","vbmarg","Inch","space=5"); zdialog_add_widget(zd,"label","labtop","vbtop","Top"); zdialog_add_widget(zd,"zspin","mtopcm","vbtop","0|10|0.01|0"); zdialog_add_widget(zd,"zspin","mtopin","vbtop","0|4|0.01|0"); zdialog_add_widget(zd,"label","labbot","vbbottom","Bottom"); zdialog_add_widget(zd,"zspin","mbottcm","vbbottom","0|10|0.01|0"); zdialog_add_widget(zd,"zspin","mbottin","vbbottom","0|4|0.01|0"); zdialog_add_widget(zd,"label","lableft","vbleft","Left"); zdialog_add_widget(zd,"zspin","mleftcm","vbleft","0|10|0.01|0"); zdialog_add_widget(zd,"zspin","mleftin","vbleft","0|4|0.01|0"); zdialog_add_widget(zd,"label","labright","vbright","Right"); zdialog_add_widget(zd,"zspin","mrightcm","vbright","0|10|0.01|0"); zdialog_add_widget(zd,"zspin","mrightin","vbright","0|4|0.01|0"); zdialog_add_widget(zd,"hbox","hbscale","dialog",0,"space=5"); zdialog_add_widget(zd,"label","labscale","hbscale","image scale","space=5"); zdialog_add_widget(zd,"zspin","scale","hbscale","5|100|1|100"); zdialog_add_widget(zd,"label","labpct","hbscale","percent","space=5"); zdialog_add_widget(zd,"hbox","hbsize","dialog",0,"space=3"); zdialog_add_widget(zd,"vbox","vbunit","hbsize",0,"space=5"); zdialog_add_widget(zd,"vbox","vbwidth","hbsize",0,"space=5"); zdialog_add_widget(zd,"vbox","vbheight","hbsize",0,"space=5"); zdialog_add_widget(zd,"label","space","vbunit","Image"); zdialog_add_widget(zd,"label","labcm","vbunit","CM"); zdialog_add_widget(zd,"label","labinch","vbunit","Inch"); zdialog_add_widget(zd,"label","labwidth","vbwidth","Width"); zdialog_add_widget(zd,"label","labwcm","vbwidth","xx.x"); zdialog_add_widget(zd,"label","labwin","vbwidth","xx.x"); zdialog_add_widget(zd,"label","labheight","vbheight","Height"); zdialog_add_widget(zd,"label","labhcm","vbheight","xx.x"); zdialog_add_widget(zd,"label","labhin","vbheight","xx.x"); zdialog_load_inputs(zd); // recall prior settings zdialog_fetch(zd,"mtopcm",margins[0]); zdialog_fetch(zd,"mbottcm",margins[1]); zdialog_fetch(zd,"mleftcm",margins[2]); zdialog_fetch(zd,"mrightcm",margins[3]); zdialog_fetch(zd,"scale",imagescale); get_printed_image_size(); zdialog_stuff(zd,"labwcm",pwidth,"%.2f"); // update image size in dialog zdialog_stuff(zd,"labhcm",pheight,"%.2f"); zdialog_stuff(zd,"labwin",pwidth/2.54,"%.2f"); zdialog_stuff(zd,"labhin",pheight/2.54,"%.2f"); gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % zdialog_run(zd,margins_dialog_event,"parent"); // run dialog zstat = zdialog_wait(zd); // wait for completion zdialog_free(zd); // kill dialog if (zstat == 1) return 0; return 1; } // dialog event function // save user margin and scale changes // recompute print image size int print_image::margins_dialog_event(zdialog *zd, ch *event) { using namespace print_image; double temp; if (strmatch(event,"escape")) zd->zstat = -2; // escape key if (strmatch(event,"mtopcm")) { // get cm inputs and set inch values zdialog_fetch(zd,"mtopcm",margins[0]); zdialog_stuff(zd,"mtopin",margins[0]/2.54); } if (strmatch(event,"mbottcm")) { zdialog_fetch(zd,"mbottcm",margins[1]); zdialog_stuff(zd,"mbottin",margins[1]/2.54); } if (strmatch(event,"mleftcm")) { zdialog_fetch(zd,"mleftcm",margins[2]); zdialog_stuff(zd,"mleftin",margins[2]/2.54); } if (strmatch(event,"mrightcm")) { zdialog_fetch(zd,"mrightcm",margins[3]); zdialog_stuff(zd,"mrightin",margins[3]/2.54); } if (strmatch(event,"mtopin")) { // get inch inputs and set cm values zdialog_fetch(zd,"mtopin",temp); margins[0] = temp * 2.54; zdialog_stuff(zd,"mtopcm",margins[0]); } if (strmatch(event,"mbottin")) { zdialog_fetch(zd,"mbottin",temp); margins[1] = temp * 2.54; zdialog_stuff(zd,"mbottcm",margins[1]); } if (strmatch(event,"mleftin")) { zdialog_fetch(zd,"mleftin",temp); margins[2] = temp * 2.54; zdialog_stuff(zd,"mleftcm",margins[2]); } if (strmatch(event,"mrightin")) { zdialog_fetch(zd,"mrightin",temp); margins[3] = temp * 2.54; zdialog_stuff(zd,"mrightcm",margins[3]); } zdialog_fetch(zd,"scale",imagescale); // get image scale get_printed_image_size(); zdialog_stuff(zd,"labwcm",pwidth,"%.2f"); // update image size in dialog zdialog_stuff(zd,"labhcm",pheight,"%.2f"); zdialog_stuff(zd,"labwin",pwidth/2.54,"%.2f"); zdialog_stuff(zd,"labhin",pheight/2.54,"%.2f"); gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % return 1; } // compute printed image size based on paper size, // orientation, margins, and scale (percent) void print_image::get_printed_image_size() { using namespace print_image; double iww, ihh, pww, phh, scale; pww = 0.1 * gtk_page_setup_get_paper_width(pagesetup,MM); // get paper size phh = 0.1 * gtk_page_setup_get_paper_height(pagesetup,MM); // (mm to cm units) pww = pww - margins[2] - margins[3]; // reduce for margins phh = phh - margins[0] - margins[1]; pww = pww / 2.54 * 300; // convert to dots @ 300 dpi phh = phh / 2.54 * 300; iww = gdk_pixbuf_get_width(pixbuf); // original image size, pixels ihh = gdk_pixbuf_get_height(pixbuf); scale = pww / iww; // rescale image to fit page if (phh / ihh < scale) scale = phh / ihh; scale = scale * 0.01 * imagescale; // adjust for user scale setting pwidth = iww * scale / 300 * 2.54; // dots to cm pheight = ihh * scale / 300 * 2.54; return; } /********************************************************************************/ // connect a user callback function to a drag-drop source widget void drag_drop_source(GtkWidget *widget, drag_drop_source_func ufunc) { void drag_drop_source2(GtkWidget *, GdkDragContext *, void *ufunc); void drag_drop_source3(GtkWidget *, GdkDragContext *, GtkSelectionData *, int, int, void *ufunc); if (! main_thread()) zappcrash("illegal call from thread"); gtk_drag_source_set(widget,GDK_BUTTON1_MASK,null,0,GDK_ACTION_COPY); gtk_drag_source_add_text_targets(widget); gtk_drag_source_add_image_targets(widget); G_SIGNAL(widget, "drag-begin", drag_drop_source2, ufunc); G_SIGNAL(widget, "drag-data-get", drag_drop_source3, ufunc); return; } // private function for "drag-begin" signal void drag_drop_source2(GtkWidget *widget, GdkDragContext *context, void *ufunc) { drag_drop_source_func *ufunc2; GdkPixbuf *pixbuf; GError *gerror = 0; ch *file = 0; if (! main_thread()) zappcrash("illegal call from thread"); ufunc2 = (drag_drop_source_func *) ufunc; file = ufunc2(); if (! file) goto cancel; pixbuf = gdk_pixbuf_new_from_file_at_size(file,128,128,&gerror); if (! pixbuf) { if (gerror) Plog(0,"%s \n",gerror->message); return; } gtk_drag_set_icon_pixbuf(context,pixbuf,64,64); // hot spot is middle of image return; cancel: Plog(2,"drag canceled \n"); return; } // private function for "drag-data-get" signal void drag_drop_source3(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, int, int, void *ufunc) { drag_drop_source_func *ufunc2; ch *file = 0; if (! main_thread()) zappcrash("illegal call from thread"); ufunc2 = (drag_drop_source_func *) ufunc; file = ufunc2(); if (! file) goto cancel; gtk_selection_data_set_text(data,file,-1); // drops text return; cancel: Plog(2,"drag canceled \n"); return; } // connect a user callback function to a drag-drop destination widget void drag_drop_dest(GtkWidget *widget, drag_drop_dest_func *ufunc) { int drag_drop_dest2(GtkWidget *, GdkDragContext *, int, int, void *, int, int time, void *); int drag_drop_dest3(GtkWidget *, void *, int, int, int, void *); int drag_drop_dest4(GtkWidget *, void *, int, void *); if (! main_thread()) zappcrash("illegal call from thread"); gtk_drag_dest_set(widget,GTK_DEST_DEFAULT_ALL,null,0,GDK_ACTION_COPY); gtk_drag_dest_add_text_targets(widget); G_SIGNAL(widget, "drag-data-received", drag_drop_dest2, ufunc); G_SIGNAL(widget, "drag-motion", drag_drop_dest3, ufunc); G_SIGNAL(widget, "drag-leave", drag_drop_dest4, ufunc); return; } // private function for "drag-data-received" signal // get dropped file, clean escapes, pass to user function // passed filespec is subject for zfree() int drag_drop_dest2(GtkWidget *, GdkDragContext *context, int mpx, int mpy, void *sdata, int, int time, void *ufunc) { ch * drag_drop_unescape(ch *escaped_string); drag_drop_dest_func *ufunc2; ch *text, *text2, *file, *file2; int cc; if (! main_thread()) zappcrash("illegal call from thread"); text = (ch *) gtk_selection_data_get_data((GtkSelectionData *) sdata); ufunc2 = (drag_drop_dest_func *) ufunc; if (strstr(text,"file://")) // text is a filespec { file = zstrdup(text+7,"drag_drop"); // get rid of junk added by GTK cc = strlen(file); while (file[cc-1] < ' ') cc--; file[cc] = 0; file2 = drag_drop_unescape(file); // clean %xx escapes from Nautilus zfree(file); ufunc2(mpx,mpy,file2); // pass file to user function } else // text is text { text2 = zstrdup(text,"drag_drop"); ufunc2(mpx,mpy,text2); } gtk_drag_finish(context,1,0,time); return 1; } // private function for "drag-motion" signal // pass mouse position to user function during drag int drag_drop_dest3(GtkWidget *, void *, int mpx, int mpy, int, void *ufunc) { drag_drop_dest_func *ufunc2; ufunc2 = (drag_drop_dest_func *) ufunc; if (! ufunc2) return 0; ufunc2(mpx,mpy,null); return 0; } // private function for "drag-leave" signal // pass mouse position (0,0) to user function int drag_drop_dest4(GtkWidget *, void *, int, void *ufunc) { drag_drop_dest_func *ufunc2; ufunc2 = (drag_drop_dest_func *) ufunc; if (! ufunc2) return 0; ufunc2(0,0,null); return 0; } // private function // Clean %xx escapes from strange Nautilus drag-drop file names ch * drag_drop_unescape(ch *inp) { int drag_drop_convhex(ch ch1); ch inch, *out, *outp; int nib1, nib2; out = (ch *) zmalloc(strlen(inp)+1,"drag_drop"); outp = out; while ((inch = *inp++)) { if (inch == '%') { nib1 = drag_drop_convhex(*inp++); nib2 = drag_drop_convhex(*inp++); *outp++ = nib1 << 4 | nib2; } else *outp++ = inch; } *outp = 0; return out; } // private function - convert character 0-F to number 0-15 int drag_drop_convhex(ch ch1) { if (ch1 >= '0' && ch1 <= '9') return ch1 - '0'; if (ch1 >= 'A' && ch1 <= 'F') return ch1 - 'A' + 10; if (ch1 >= 'a' && ch1 <= 'f') return ch1 - 'a' + 10; return ch1; } /******************************************************************************** Miscellaneous GDK/GTK functions *********************************************************************************/ // Get thumbnail image for given image file. // Returned thumbnail belongs to caller: g_object_unref() is necessary. PIXBUF * get_thumbnail(ch *fpath, int size) { PIXBUF *thumbpxb; GError *gerror = 0; int err; ch *bpath; STATB statB; if (! main_thread()) zappcrash("illegal call from thread"); err = stat(fpath,&statB); // fpath status info if (err) return 0; if (S_ISDIR(statB.st_mode)) { // if folder, return folder image bpath = (ch *) zmalloc(500,"get_thumbnail"); *bpath = 0; strncatv(bpath,499,zimagedir,"/folder.png",null); thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror); zfree(bpath); return thumbpxb; } thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,size,size,&gerror); return thumbpxb; // return pixbuf to caller } // make a cursor from a graphic file in application folder // (see initz_appfiles()). GdkCursor * zmakecursor(ch *imagefile) { GError *gerror = 0; PIXBUF *pixbuf; GdkDisplay *display; GdkCursor *cursor = 0; ch imagepath[200]; if (! main_thread()) zappcrash("illegal call from thread"); display = gdk_display_get_default(); *imagepath = 0; strncatv(imagepath,199,zimagedir,"/",imagefile,null); pixbuf = gdk_pixbuf_new_from_file(imagepath,&gerror); if (pixbuf && display) cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0); else Plog(0,"*** %s \n",gerror->message); return cursor; } /********************************************************************************/ // strip the alpha channel from a pixbuf // returns 0 if no alpha channel or fatal error PIXBUF * gdk_pixbuf_stripalpha(PIXBUF *pixbuf1) { PIXBUF *pixbuf2; GDKCOLOR color; int ww, hh, rs1, rs2; uint8 *ppix1, *ppix2, *pix1, *pix2; int nch, ac; int px, py; if (! main_thread()) zappcrash("illegal call from thread"); ac = gdk_pixbuf_get_has_alpha(pixbuf1); if (! ac) return 0; nch = gdk_pixbuf_get_n_channels(pixbuf1); color = gdk_pixbuf_get_colorspace(pixbuf1); ww = gdk_pixbuf_get_width(pixbuf1); hh = gdk_pixbuf_get_height(pixbuf1); pixbuf2 = gdk_pixbuf_new(color,0,8,ww,hh); // create output pixbuf2 if (! pixbuf2) return 0; ppix1 = gdk_pixbuf_get_pixels(pixbuf1); // input pixel array ppix2 = gdk_pixbuf_get_pixels(pixbuf2); // output pixel array rs1 = gdk_pixbuf_get_rowstride(pixbuf1); rs2 = gdk_pixbuf_get_rowstride(pixbuf2); for (py = 0; py < hh; py++) { pix1 = ppix1 + py * rs1; pix2 = ppix2 + py * rs2; for (px = 0; px < ww; px++) { memcpy(pix2,pix1,nch-1); pix1 += nch; pix2 += nch-1; } } return pixbuf2; } /********************************************************************************/ // Create a pixbuf containing text with designated font and attributes. // Text is white on black. Widget is ultimate display destination. PIXBUF * text_pixbuf(ch *text, ch *font, int fontsize, GtkWidget *widget) { ch font2[60]; PangoFontDescription *pfont; PangoLayout *playout; cairo_surface_t *surface; cairo_t *cr; PIXBUF *pixbuf; uint8 *pixels, *cairo_data, *cpix, *pix2; int ww, hh, rs, px, py; if (! main_thread()) zappcrash("illegal call from thread"); if (! font) font = zfuncs::appfont; // default font snprintf(font2,60,"%s %d",font,fontsize); // combine font and size pfont = pango_font_description_from_string(font2); // make layout with text playout = gtk_widget_create_pango_layout(widget,text); pango_layout_set_font_description(playout,pfont); pango_layout_get_pixel_size(playout,&ww,&hh); ww += 2 + 0.2 * fontsize; // compensate bad font metrics hh += 2 + 0.1 * fontsize; surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,ww,hh); // cairo output image cr = cairo_create(surface); pango_cairo_show_layout(cr,playout); // write text layout to image cairo_data = cairo_image_surface_get_data(surface); // get text image pixels pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB,0,8,ww,hh); rs = gdk_pixbuf_get_rowstride(pixbuf); pixels = gdk_pixbuf_get_pixels(pixbuf); for (py = 0; py < hh; py++) // copy text image to PXB for (px = 0; px < ww; px++) { cpix = cairo_data + 4 * (ww * py + px); pix2 = pixels + py * rs + px * 3; pix2[0] = pix2[1] = pix2[2] = cpix[3]; } pango_font_description_free(pfont); // free resources g_object_unref(playout); cairo_destroy(cr); cairo_surface_destroy(surface); return pixbuf; } /********************************************************************************/ // move the mouse pointer to given position in given window // widget must be realized int move_pointer(GtkWidget *widget, int px, int py) { int rpx, rpy; GdkWindow *window; if (! main_thread()) zappcrash("illegal call from thread"); window = gtk_widget_get_window(widget); gdk_window_get_root_coords(window,px,py,&rpx,&rpy); gdk_device_warp(mouse,screen,rpx,rpy); return 1; } /********************************************************************************/ // move a window to the mouse position // widget is a GtkWindow, which may or may not be realized void window_to_mouse(GtkWidget *window) { using namespace zfuncs; int px, py; if (! main_thread()) zappcrash("illegal call from thread"); gdk_device_get_position(mouse,&screen,&px,&py); // get mouse position gtk_window_move(GTK_WINDOW(window),px,py); return; } dkopp/debian-control0000664000175000017500000000117215002404054013453 0ustar micomicoPackage: dkopp Version: 8.2 Architecture: amd64 Section: utils Installed-Size: 1436 Maintainer: Mike Cornelison Priority: optional Homepage: https://kornelix.net/ Depends: libc6, libgtk-3-0, binutils, growisofs, genisoimage Description: Back-up files to DVD or Blue-Ray disc. Full or incremental backup with full or incremental verification. Choose files and directories to include or exclude at any level. Incremental backup updates the same disc from a prior full backup. Recover files using Dkopp, or drag and drop using a file browser. Dkopp is a graphical front end for growisofs and genisoimage. dkopp/Makefile0000664000175000017500000000356015002404054012273 0ustar micomico# dkopp make file # defaults for parameters that may be pre-defined CXXFLAGS += -Wall -g -rdynamic -O2 -Wno-format-truncation -Wno-stringop-truncation CXXFLAGS += -fpermissive -Wno-write-strings PKG_CONFIG ?= pkg-config CFLAGS = $(CXXFLAGS) $(CPPFLAGS) -c \ `$(PKG_CONFIG) --cflags gtk+-3.0` \ -I/usr/include/clutter-1.0/ \ -I/usr/include/cogl/ \ -I/usr/include/json-glib-1.0/ \ -I/usr/include/clutter-gtk-1.0/ LIBS = `pkg-config --libs gtk+-3.0` -lpthread -lclutter-1.0 -lclutter-gtk-1.0 dkopp: dkopp.o zfuncs.o $(CXX) $(LDFLAGS) -o dkopp dkopp.o zfuncs.o $(LIBS) \ dkopp.o: dkopp.cc $(CXX) $(CFLAGS) -o dkopp.o dkopp.cc \ zfuncs.o: zfuncs.cc zfuncs.h $(CXX) $(CFLAGS) zfuncs.cc \ # install application files in standard locations PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin MENUDIR = $(PREFIX)/share/applications DOCDIR = $(PREFIX)/share/doc/dkopp MANDIR = $(PREFIX)/share/man/man1 DATADIR = $(PREFIX)/share/dkopp ICONDIR = $(PREFIX)/share/icons install: dkopp uninstall mkdir -p $(DESTDIR)$(BINDIR) mkdir -p $(DESTDIR)$(MENUDIR) mkdir -p $(DESTDIR)$(DOCDIR) mkdir -p $(DESTDIR)$(MANDIR) mkdir -p $(DESTDIR)$(DATADIR) mkdir -p $(DESTDIR)$(ICONDIR) cp -f dkopp $(DESTDIR)$(BINDIR) cp -f dkopp.desktop $(DESTDIR)$(MENUDIR) cp -f -R doc/* $(DESTDIR)$(DOCDIR) gzip -fk -9 man/dkopp.man mv -f man/dkopp.man.gz $(DESTDIR)$(MANDIR)/dkopp.1.gz cp -f -R data $(DESTDIR)$(DATADIR) cp -f -R images $(DESTDIR)$(DATADIR) cp -f dkopp.png $(DESTDIR)$(ICONDIR) uninstall: rm -f $(DESTDIR)$(BINDIR)/dkopp* rm -f $(DESTDIR)$(MENUDIR)/dkopp* rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/dkopp* rm -f -R $(DESTDIR)$(DATADIR) rm -f $(DESTDIR)$(ICONDIR)/dkopp* clean: rm -f dkopp rm -f *.o dkopp/man/0000775000175000017500000000000015002404054011402 5ustar micomicodkopp/man/dkopp.man0000664000175000017500000000430415002404054013215 0ustar micomico.TH DKOPP 1 2024-01-01 "Linux" "Dkopp man page" .SH NAME Dkopp - copy files to DVD or BD (Blue-ray) media .SH SYNOPSIS \fBdkopp\fR [ \fB-job\fR | \fB-run\fR ] \fIjobfile\fR \fBdkopp\fR [ \fB-nogui\fR ] \fB-script\fR \fIscriptfile\fR .SH DESCRIPTION Dkopp copies files to backup DVD or BD media. It supports full or incremental backups and full or incremental media verification. .SH OVERVIEW Dkopp is a menu-driven GUI (GTK) program operating in its own window. Dkopp copies files and directories specified in a job file to DVD or BD media. Dkopp can copy all files to empty media (full copy), or only new and modified files to previously used media (incremental). Files and directories to include or exclude can be selected from the file system hierarchy using a GUI navigator. Specifications are saved in a job file which can be re-edited and re-used. Script files can be run in batch mode using the \-nogui option. Dkopp can be used to select and restore files previously copied, and owner and permission data is also restored. The DVD/BD media can also be accessed with file system tools like Nautilus. Dkopp supports the following functionalities: - Three backup modes: full, incremental, accumulate. - Three media verification modes: full, incremental, thorough. - Use write-once or re-writable DVD or BlueRay media (but not CD). - Report disk:backup differences in detail or summary form. - Select and restore files from a backup copy (or use drag and drop). - Search log files to find media where specified files are saved. .SH OPTIONS Command line options: [ \fB-job\fR ] \fIjobfile\fR open job file for editing \fB-run\fR \fIjobfile\fR execute a job file [ \fB-nogui\fR ] \fB-script\fR \fIscriptfile\fR execute a script file .SH SEE ALSO The online user manual is available using the menu Help > contents. This manual explains Dkopp operation in great detail. Dkopp uses the batch programs \fBgrwoisofs\fR and \fBgenisoimage\fR. Dkopp is essentially a GUI front-end for these programs. .SH AUTHORS Written by Mike Cornelison https://kornelix.net dkopp/images/0000775000175000017500000000000015002404054012074 5ustar micomicodkopp/images/media-play.png0000664000175000017500000002170715002404054014633 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'AIDATxœí{\u•Ç?iZj*EM¥RTˆ ˜ “€ †# `Ĉ¼Dä!OA¡\—„‚EeÑU`³<aó"òŒ1ÆC^Y„C@ȦfS©qvl÷ïtÒ™ôÌôãÞóûÝ{ϧª‹ª$Óç0}Ïéóû׈#FàdšQÀ¸þ׎ÀX``{`4ÐÙÿoFÖ¼Êý¯Rÿ{T€¾þׯš×z X¼¼¬Ö«û_ëSþÿsRd„;€LÐL&S€‰ý¯ ȸC²x XÙÿzè^zêå4€;€ø ÌfÓiÈøKCýP„TX<,– È‰wáìì ìì‰Âó<Ò,žžžÞªQÁq`ÏHdðŸæSêœÀ£À/‘CðÁw6ì |8é­éAÎààAtÙ褈;€ô Ì"{gøÐT€EÀ=ÀÝÀª°êäwÉ288˜}RT€…ÀíÀ](-é$€;€ö)sSP˜ïá}ºô ãÁ-ÀäœqÐ:cS3Q¸ïس ˜ÜŒ ”œ&qÐ<3€¯ óý¶uqD/º'øª5pÄ@ãœÎöN¼,®æ‡V$ ¸š2pp*Ãu²Cpp*@rêà >eààbTsïd—•ÀÀOqG° \Ž~ÞX \܉g6á`3sQȸghEœTYŽŽt B+îT‹5rNqX€.uW„V$$E®T\‡ZUÝø‹Ç\ôÙ_Gø™ Áئ À©ÀýÀþÛ š»p:ð.r…¢hG€ÉÀõ¨×qòpJ!‚¢DÛ—¢TЄÀº8ñ²3ðà=¨ èoaÕIŸ"D{¡Æ¿Ýwša9jðZZ‘4ÉsP.nÞX'{윌ž%§µy& ÞñY¡qrÁ"4ãáµÐŠ$MoÀO@·¹nüNRÌBÏÔ ¡Iš<9€‘è¬Z†á8IÒ‰ž­[г– òrØ ÍŽ+ê„Ý ÚÒó:›7ö¼‰†d¼ƒ–w¬6 ©»=¨1¦z®-¡;“ôpo‡ŠcF¡ CcÑ=JuÑ.ýÿÍÓH3¬@³_ ­H»äÁ ü=´y§‚ΡˀçQ¾º«ÿϬ·ðt »–Éý¯=Г Ã1lNî ­H;dÙ”P×Þ%¡I‘n”~]D-îÿ³˜éDÕu³€ *y>’}uf2KU°ºå?,´" Ó‹¶æ<†æã/%û=ìeT‹1øÚ~”·Qj÷£,Á†ÐŠ4KÀxà>næuÀÃÀ/ÑÇþ ß.ÈŒF©çehñK¦ödÍLB›v²L7šYw;ð8Ùÿ–o•2Ú”t<š¹˜õ£Âàä 2A–ÀtÓŸÕ‡¤‚¾áoB!£¯ÎÞ’t¤; }ÖY½HìF‚GC+ÒYqÇ¢›þ,ž×77±ð0 ãQSÎé(™5zQ†àÎЊ GÀÙhhCÖ¾–× ‡ ¨!~»”‘ó?ícÈàËÀC+2±;€ +C+Ñ$ ÐlÁ'ë‘7öG³ü²6½é"à[¡•Œ˜»/E9Ö¬0øð]T‘ç$ËëèÒô>t,˜T›Æ™üx2´"õˆÕ|Wd‡Q˜ú}|?kŸÿìV†8g~Z‘Äè.%Æ¿m ºÕÝ;¶¼‰¶þü˜ìVaÙ#ØÀ…Äö¯FsãÎÞ¬‹£ÏàFàUàCÄ&>ø+ªöŒ‚˜ÀÑv×Xé¾ÂýßÖÅÙšçQªµ„2å°ê Êà-"ÙbKàXtÁkªï)àLà¥ÐŠ8 1 ˜G¼ÓŸ+¨ú1x@ `*ï±È§mùÏЊ8-qÚúã± • ­ í¦¡K‘? GQYªWïe›ñ¨üzNhEêÐ |”€½!Cîqè›?6ãïA|Ç?¬BŸåyÄ×щl`\(BEÛO_KoðY ¾02ÇLE5“C+2€eÀ¾˜'"(¡ ¿ØŒÿVàƒ¸ñç™è3¾5°™F KðiÀ¯£N¯XèAú\Ž7í>TNüê+ˆ%]¸;° ÆÕ‚ÖG€#ŸOºo5šLENÖ1gšÄì >€ ð Z:€Ý€ßÏ¥ß"4ÂéíЊ8Aƒ"‚XÉt{c4rÜê›x$qMó¹•eºñ;o£gáŽÐŠôÓ‰lÅdùˆÕÀ(ßBCFü¼ïTéCawšZš1¨Óq~Ú‚,À è‚-4à\²7`ıã1à]t9ºDöÀ£5å©‘öÀ´T1tèßœHµ×N&8í !覓âVâ4ïÊ(·Úø{ДV7~§QîDÏLèÊÁNdC©9¢4_>ŸÖ›7HJó=X'{¼ŒÚ¾&l$0ø?Ô‘š8i¦¡”_È_\ÕøÔÁÉ>sQ­@G@úPj0ñ¦¡4Že4Ã?¤ñ÷¡š~7~§] g)dÖ(5›JãppLÒoÚ”y¸' N¾x;’pÙúÿûD’ošô`2ºõ9ÜãËÀ”ïä—sÐ’šPô¢¬@WRo˜tp7Jý…â[xžßIÅ„-Ú؃»“t§¢B›P܉¦õ:Nš<†fN $gàO(Òn›¤Ž£Ðiû$Þ¬¡zîÐy[§t ¶ÝP Dï …(ëÛ}£¤²ß œñ¯F]}nüŽ=è™[HþöÈæÚ&‰`* GB¤ýzÐ(%ïçwB0¶ Q#Ї.Ûš`•Dp5árþgáÆï„c áîÊÈöÚ¢]páÖ5ßJ|³Ýœâq+ážÃ¹È[¦#@ …þ{¶£@‹t¡áŽ~îwb õ „˜6¼*­üp;À±„1þTšéÆïÄBÈgrOÚ¨¼mµ ŒŠ~F·*¸ ¾ŠvÃ;NL¼ ü/ðɲ§×ÓBÐjp0±ÅŸm‡GkÈuœF¸–0»þ&"›lšVîÊÀ‹Ø;€nTé뺜˜V•[ÂY ¼Ÿ&»[‰Ž#Ì·ÿù¸ñ;ñ³ =«ÖLD¶Ù­D/`ÛùÚ¢ê8YáI`?c™]À”f~ Ùà0ì¿oòq²ÇYèÙµd2²Ñ†iÖ„mþûŸLj.ôìZÓ”6s˜æüY²]l˜¯MvœØ]˜[ïÜ›K䛉¾Òš.mqnüNvÙ€žak¶ÕF#€±h²å¨¯ÅÀ‡ å9NZü˜i(¯ Y;Ü?l48û9_5–ç8iaý,o‹lvX‰JÀQƒ‡Êsœ´y8ØPÞ*à} SÜHp¶Æp±±<ÇIëgz< ´ 7âNi_—¦˜O P'0Ë0X÷=€amw¸#ÀhàÏØŽ<šN1Àáh§Á `$J.DË!¨—“ÓHhšoƒô;ëûÃEÇ`kü (¦ñÿíŸ;5‘”]Pm÷ChØÄþts’c¶ëê:fVÀpàøätiˆ".õ88{˜³CýsäœìbýŒiÃCÆ£Ûÿ4ˆÖc ª`*eT_±c?ÓƒJL¯Ä‹¤²ÊïÐQÏ‚ ÊÔí¤ʸæï“æCY±0“æŒÖ} -¬<ÛÏÈIËg½„lyпŒÏ$¯Ë ¬î2” íÌTܸU™ÍNFLj»h J/AµåÁÀ8lKoƾu2’ئ4xe ¬›NœÖèEϼ3äÙÌ:Äß%M˜g$+6’ü‡Ž—¢4¢7óhq”w ”M×ý‹z|*=]¶âQ|ÔWRŒ.G-¨ÇÖÅšUØ­kÓõÀH”¶âCY±‘Ö7Àxà¿ÐÑÀò(ç4‡å³ u"Ãz`슺±/,³ß ­Ùlƒ“>ó‘ XÐAb²zàã©«²™ùø†Ÿ´)¡táËÀ…„ÙdëÔ§Û/À­l»žhkÙ`“Ün(+F¬.@ã©®D÷GÊu†ÆÒ¶²í`,vS×Ér6³ p*-žVÙÀz#Y“‘ob ØÇH€ir‹‰“(û£&£€1aU)4}ȬØÂÆ:Ëå÷ÊŠË#@=JÀéè~àŸ±ûæK[ØÂÆCE½À#F²œá\¶>5µXÂI„G°«„4 L5Rb!vé§q&¢o£_b¿ªÈt#›°`*5õµ`jOµàWFrb'ô`0þ\G2ý ÎðXÙD™šVäZ`Y1b‡ºÓeàt?p.v_EÅÒ&6Ùz­˜n$¼Xj$ËiŸÑ¨ýØÖˆ¥Ø‹7Ùz­ØËHøB<ýW%Ö#@=&£»€Ýë’Gú°»ØdëU0»õY#9N: <|eœä°²Ýè¿,ÕüUÿÿ"#9Nzl üð*ðEü~ )¬l£Dÿ~Õè-S> ­-.Y:Ôc{àzTQhÙBžW,mc2lvSŒ„®Ä®î9 dÝTÙx í6˜X—,³ÙˆS`³˜h$´ˆK?ŠÄᨚð*´àÄi+™öày#9N8:€Aõ§âcË›ÅÊF‚8€.#9NxÆ7¡%–]¦YÇÊF69€QØ…kî¶$/wC±ð4ð3ì×Ìg+éF•°›%_^3’åÄÇ1èXð 4È©ÏkØ}1Œ+aç•WãóÿŠNp KvB`]b¥ÙŠãKØM‹}ÝHN–( ã€ÛÐZ³Yu‰‘×äìXÂn”/ÿp2•¿Þ†-¯ÅÊVÆ”€Œ„Y…5N¶(¡ãÀ«èxàcËíle‡vÞ4’“%Šz¨ÇHtAø"º0,2V¶²} õ{[ð¶‘'Ûì‚R†Oc×¢V¶2º„] À;Frœ|°*"º‰³ì €•­tV ,ð& ­ñ#ÀДP9ñ˨¼¸(÷V¶2ª„Ý.ywN«t¢£(ÆZ3+[ié6ÉqòË´Öì1`R`]ÒÄÊVLÀ#9Y­q ðpFhERÂÊVF–°çäeÀN’tóÐÆã¼ae+eKà“€4¸í7ÌV¶R.a7°ÁÃÝ­ñßI2\M¾: ­ž‹’OkqòÀ(`nh%²H Coc$Ç)&y*2‹ÊKž7Œäd ?$Çë¡H³{9KP”*®fp kÉ×ÂY+[é+aWt§K'.Î#_if+[Ùhé¬ ŽœbñoÀ]¡•H³êÜ2vÀI:IR.¾Z‘°²•e ;Œäd ¿hÕÀIÀã¡I ³ÝÐm$Ìjò“_zPÈÿ~òkü`g+Ýe`‘0«á£N>¹8Ÿbì–°²•ue즼×HN–ð#Àð,¾B¾¿ñbe+·Œ„Ym ròÁ;ÀYÀ)–ñƒ­¼UÆn¡ï…s¡ø!pÅ"ee+o—5FÂv1’ãd—GP¸_ô%²»ÉYSÂn É8¼x >#A¬> |7þ쎫JØm!)¡™nŽS¥ÝìOî¬K,LÀ®pu ³¬j&Éqâ¦Üì |è «NTXÙH7ý…@ ÌwÎ3ÀÞÀðmQõ°²‘•°9Ô°r{ÉÉ EªX|ØXX—˜±²‘•°yð€•˜f$lj‡(Ì¿ ß ÑV6²…xÁHèDÔèPÔünѸ]òùjøÆ…lÄ‚`óÀ2õ2ÃP–†¥(ÔÿnüÍ`i]°Ù¼‚Ýyt–‘œ,·:€·ÑåÞÞè²Ïi+Û¨ ›ßä6VÿÀ€ÉqìèEçü]Qz¯H—›Ibe¯ÐS[p`u3;Ÿœ'D7×çcWO’GÊÈ6,Ødëµà9#áÀ^F²œôèB¥»ŸÂ.zÌ3{!Û°`“­×:€ÅFÂæÊr’e=jØùjÞq’ÁÒ&6Ùz­X‚Ý¥ÔÇäÄN–ÎÊ}ÀÐ9ÿûäï34V6чlØÒlV)1»pÇiŸÇÑ`޳°› U$:±;ÿ¯ ¦ k`בUêf[à #YNë¼| ærÒá dlaãÀ“FJ€ú¿‹N¬G€ hæþ4ŒÓIK[ØÂƦã,‹7í—ïgÉx¨?EÆo5)ªè”‘-X1d°»²àQÀF²œáY |-Üpã·ã@ìt!ßD½É#–©ã eÅH G€5À‰À‡°M;ÂÒ¶²ízàWŠT9ŸŠê–ÝQØïØÓlÀŠ­l»žx»UËØþq/ºà»]ø9a8»tx²í-¨ç6b»ˆáCY±a}XŽRzGQŒ[±cùì?N,ƒM} ]]¶`¾4$mмe'VÆc[þ[צsb÷íTÎ4’U4ú€kQùîð”kLœ‰Ýøï ²é­LÕØÞŸŠ]%TL¤éd †óðl±±-zæ­XÌ “™†ò@?OG—ºŒŽ1” i|#¿‚Zt?‰oÙ‰•cÐ3oÅ ¶<”¸ÛKªó eÅÂÚáÿIìGC9ö`pωËg½‚l¹.C9€UÀÂÄÕœÀ~†òb`%íGÕ-;»ã[v²À~Øÿ\Èû?‡»„¸=Y]†å"cy¡é¡¦7»žA7û¾e';X?ãCÚðpà.슂æR¼å!­ÜµÔnÙY–¬:NŠLCϸ=Ȇe8°û­­—Ë ÍÍ4>dc#úýìÎ0¬%ÖÏöýȆ¥‘<ä-ÉèÒ0‡S¬(`=jÆêìÞüþױʜd˜†}Ùû°¶;bĈÃý›ðGl«õ1”3«}Ðï¼¼ÌnÂKw³ÎCÀÁ†òVïc˜L^#àkÀ (Õ ûRÌí2ÛÛ£c7êäƒ}€§e^Œº=‡¤Q0xÛj½Å¨GÝq²ÎoQ„gE/°3 Ô™4Z‹¼–!Š Rb&pœ±LÇIšã°5~­6TdÖh*^ø]«µÈjàýx(ìd“í€qÆr÷¦Áú’fº‘–`&‡Î2Ž“E.ÆÞøŸ¡‰â²f"€Ã€ûšÕ¨Mzéxc‹“-&£|Ö]®Ÿ¦‰ÚfÀ èÎ’§€ËtœvxûÞ–.4ê­aZHpU ?Ó.ûgë8­paÛš¶ÍV"€2ºØ˜Øì¶I7ju´³Éq"`<ð<ö»/W¢ ó¦ºK[‰ú°/ ýBo ×qšá&Â,¾½‚ZË[‰ \¦pm¹Ž3ç×ÛÒ·?À6-:€ ð./mÍ(áýïNLLEš÷mZð%ZÜÞÜj:><ìÙê´A„á]qN t¿Ç>;2üé´8¾¯±Ä4ƒ.“ëÉvœ\OãÙ`˳;ÛKþ?‚“û_Ž’“ ÷. Íe¾íªLEGgŸÔ6ÜÎ\=Çi•¨Í7Ä‚Û>ú¯hçM’ØL²m Að `L ùNqƒž½PÛ­D›ÆÉD£€WÑ ‹,BÙ¿t,è~ Ì $ÿ´î­íOIí&[\Ð{µÂ,ìg:ÅåÂ?ÈÖY÷Öj@=žD“HB0yæÇÉwŠÁ•ÀÊŠ7 %é@c¼¾l“ä›6Á>¨@Ér±©SΡ9{)Ò‹ö>þ%©7LÚüxa[w碙m_8N Ç7‰L“\AÂK{“º¬¥Œª¢BTVéCeÊÖKMœ|rpaRÝU–£ê×D7J'u XKp鬾n”2ð3l×09ùd.z–Bj6•ô ÊZ*Æ›7H8E#+êád—¹„ÍõW¹9¡ÄIãP¥ <‹ýHäô Eš~pšá0dt¡1ðRЍÓt yÏ¡ñÈ!éCû÷î ¬‡“ Žn#l؇?#Ø´ŽUÖŽLSH”úuX‡§¡9Ýö‡Je×rðDšÒv ÛË ÀÒ4 #ÐrF/rãJ”ç™ê«ò´ :UÒ>T‰ö£MµÖw ïê½èKá&âYE·íÅܘ¶ +°Z-b`b=¡% >Z¬ØŒA#æBÖö×ÒV{½b!,:€Áx8…6¦—$Ì,”"œZ'3Ð3‹ñW˜?ØÜÔò"*±4¡Àç?ËëâØr2ªîXZ®~h)ÐòP¥„Š+³< ·gá÷y§Íð;9°¹8ã9„Õ< L !|ºPÑ7å“©¨¸'ÔÏÁX†FÛm°lyPËÔÖ¸&üÁ˜Œ.*Ï ­ˆ“8ç¢Ï66ã_ƒlÁÜø!\PeÚ¢Kf –GQªÐwf›ñ(Å7'´"uèF­óÁîŸBEU–¡¶ÝÞÀzÔcZòè[‰³Ëè3ŒÑø{ѳôò9´}ÓžD<éÁZ:y(J™X§q&¡ÏlqF—ôÌ?Zë4à`¬@9‡†VdvNG‹‰3bqt¹|9ÊèücXU†äl¤cpbq å%Îp Ô¶/ªx …–N<ÌGý¡»ø†â"àû¡•¨“xø;a‰ G':»Œ*¶Þ«NáÙmå=—8ÃýZ.CÅ>Ñ›ÝÊÄS-8;¡²Í™ÀKÀ›aÕ){7¢î½ëÒßþ5´‰Ñ€¶®Ä TÙ8¥4_FãМô˜†*ù¾‡~÷Yà2"4~ˆ×€"˜ï2 -Œ˜…z ^ªMþØþwÈVFæ"" ûk ]Ôg×Gʲ–× 1d!'$g™2ÏuÙëÚ¬_Ƹ¹§Y²à@ÁmC+ÒkÑYõ¼ª°QÆ£ S§cëÒ ½(Ïý ʬ8ÐQàâ¿éŒ *ü¸ u~y×á–t ÑÓÐgµˆ¯J7Ê/òi„,9ÐÐCÀŽ¡i“n”³¾xœâÊh¡ìñÀád×¹WYB†fKdÍ€ÂÃûˆ¯•¸UÖ£ "çg:Ñ7ü¨–"¦í° ˜ËÔ1/‹Tòy;ñ i—^T õrKÉ~tPF9û9ÀÇÐç,Þå Åý(Š ÒÒÛYu 3âåÀ%¡I‘n`!Ú°´õ!Ä!t¢â¨Yh£Íl²ÚÅ7Qž?Æf¶aɲ¨r$Ê„Þ>dAx …›Ï£ F]ýf}©Øö=LîíŽeÈî^3l@7ý÷†V¤òà@#Çï!ž½ÖT€Õ¨øhuÿëM”‚|XßÿÚ€fÍ÷ £Eõ[«„BõÔñ¸0ªÿµ=JŽ×ÿÚ¥ÿ¿E0ôz¬@7ýfÓ{Ó"/ôàþ€ø†=:ùâVàK,í° O|#jÎ9‘øÏÉNöèFÏÖ)äÄø!_@-P– –…N¶Y„nù_ ­HÒÄÜ Ôÿƒ.ÿ††xä)ÒqìèC™¦Swë’ yjÙ ¸Ø3´"N¦XŽÂý¥¡I“¼Fµ¼ ÜŒæ |˜8ö¾;ñÒ‹ÚwODÙ”\S„ –ɨ§<öiCNžBëáºB+bEÑÎÆ]hÃi(?î8 gá4ôlÆø¡G€z<‡úó·CwEs„ŽèC;Ž~X— íP©ÀÕÀÜЊ8¦,Χà‹`ý›OÀ'û_Ëëâ¤Ïr6Þ…6~pPË`:*øXX'yV¢Ïv:ú¬ü0eààb`b`]œöX‰Òz?%û³ÇÀДÑÊ© ˆo¯¼34]ÀUÀ¸áŠ;€Æ9]Í­ˆ3$ Ñ¥îüЊdwÍ3Í©?šü¶Ê*½ÀÝhÃâÀºd w­35‰œ‰•:ö¬æ¡Ro_ÉÖîÚ§„jNESuœôèD ^ Èè,¾Xp,£cPºi6žfMŠ :ÛߎV¯ «N~pãÑ=ÁQh0‰;ƒæ¨ A÷ ó}¦æígw6ìˆv| mÂñcB}zЦ¤Ьý5aÕÉ?î쉜ÀÇвŒ¢N2®²-Ay næíewáìZQg#‡P©PŠô!ƒ_< <¼R¡¢ã >F3P½Át´lcÙ»C¨/¡%&Ï¡üüü>*Üdƒä&SPÂD4ýxT@½@ G^C5÷+PîKø ôèq}F±ycÏŽ¨@i´Ñg4ÚË7 EÕW¹ÿU**(<ïCßÐÕ×z4ššó*¸YÃæ DëSþÿsRäÿ¢Ä‹ çp=^IEND®B`‚dkopp/images/stop.png0000664000175000017500000001433315002404054013573 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSeXIfMM*JR(‡iZHH0232‘’qœ 0100 ÿÿfotocx-25.1: Color Mode: mode:1 blend:1.00|fotocx-25.1: Paint Transp|fotocx-25.1: Color Mode: mode:2 blend:1.00|ð­(S;IDATxœíÝ[L×ðo'4+ ""¡4TÐÅ4A­M5 ö¡¶6郩P›˜¦}Á‡Æ>ù\ykÒ¤)$ÕÚ>h¢‰©´iÓZ¹TMi¥P‚%^¸xAD`WKgþüQÀ]˜33»3sÎ÷{êegç°;ß·;³gf†a€æVWW¿“––¶ØåáPº®CÓ4@uuµË£ñ‡ `n“á'a ÌMs{^Çðûß»¹±fÁ ÈÿøÎŽ7yð½LŒ7ùð=07yñ½} ` n òã{< àÿ¸a¨ƒïõ#,pƒPßó Ê7uñ½W¼¸êÛ€²Sxãu]GCCƒ¡ëº#"Q𦡲²209ÿßU§ +YN…¿¶¶6ÚÕÕt`HdQaaa¬¦¦&İF¹]†_.]]]ÁÚÚÚ¨ßÂTÜPª~9±¬S¦~¹±¬Q¢~5°ÄI_ ¿ZXb¤.†_M,ó¤-†_m,s¤,†Ÿ–€ÒÃOS±f'U0üK 1i¦{%ü𦡴´óæÍ³=𮩩 vBÌiÓ¢¼þªª*”——lˆÓÒÒbÔ×׳äû]†_åå媪*Ø /w¦óu0üêa 8Ë·Àð«‹%à_ÃO,gø®~šÄ°ÏWÀðÓL,{|S ?%°ÎÀðÓ\XÖx¾~2‹% ÎÓÀð“(–€ÏÃOV±Ìód0üdKÀÏÃONa ÌÍSÀð“ÓX³óL0ü”,,Ä× €á§Ta <ÎÕ`ø)ÕXÓ¹V ?¹…%ðˆ+Àð“ÛXR^ ?yK ÅÀð“ר^)+†Ÿ¼JåHI0üäuª–@Ò €á'¿P±’Z ?ùj%´`øÉ¯T*¤ÃO~§J 8^ ?ÉB…p´~’ì%àX0ü$+™KÀ‘`øIv²–€í`øI2–€­`øI5²•€å¿‚á'UÉT–þ†ŸT'K žá'š C œá'šÎï%`zÔ ?Q|~.S#fø‰fçטs´ ?‘9~,YGÊð‰ñ[ $%ÃOdŸJ î~"{üRŽá'r†J@›ë¢~¢G¼^Z¢ÿaÃOô8/—€6ó?XÅð%æÕ|öÙg¶ŸŒá'2§¥¥Å¨¯¯‡ÆjjjBvÊd’íg`ø‰ÌóÚ7ÍΓ0üDâ¼Rãããw4«ƒ`ø‰¬óB ¤¥¥-¶´v†ŸÈ>/”€ðš~"ç¸]Âkmhh0~"ç8U †èr–¾XÅðÅçD ¤ä€U ?Ñìœ(Q)YÃOdNªK ékaø‰Ä¤²’¾†72üD‚ÊËË¥¥¥I_Oêv6ˆHȼy󒾑ÂXD c)Œ@¤0‘ÂXD c)Œ@¤0‘ÂXD c)Œ@¤0‘ÂXD c)Œ@¤0Ù688(|9jò†4·*CCCFgg'zzz000€ŒŽŽ"‹=¼œ²¦iƒHOOG8F$A$AVVrrr°hÑ"ää䘺¼Ù‘#GŒÿþû/©“ˆ;w"‰_š-===¸|ù2®]»†¡¡!ܾ}ÃÃÃ3/Cm¯a8Fff&"‘rssQPP€üü|„B![—†kkk3.\¸`ç)ÊÌÌÄÒ¥K‡«_H_­­­Æ?ü€îîî9«ë:ÆÆÆ066†›7oÆ}L04V¬Xüü|äççcÕªUqƒuîÜ9ŒÛÿò / ‰˜~|kk«qöìYttt]o^×u ahhpþüyÅPTTdTTT ¬¬ÌR¸®_¿Ž_ýÕÊ¢Bœ«_H[ÆÑ£GÑßßïèóÆb1tww?,”·Þz ›7ovtnjkk3Ž?îøë¦ë:ÚÛÛÑÞÞŽS§N»wïÆªU«<®™cÝ·o–/_îɱÚ%å1€“'OµµµŽoIJ;|ø°ññÇ'ýuëïïGmm-Nž<éùcýýý8xð ZZZX÷îÝÃ×_íö0%M|÷Ýw¸wïžÛÃðãÇ»=ÀÑ£GÝ‚)R} ¦Üþó£ÖÖVÃ+Jûûûñûï¿{>Xº®£µµÕía8FŠèíí5&w&ó¾ýö[·‡0_Žß$k2’¤(€«W¯º=ß2®\¹âö0¦éèè@4õü·€þùÇã4CЉ@7nÜ^&;;›7oFnn.–-[–ðqwïÞ01(âßÿÅÈÈb±FFF°téÒ¸ËíÚµ <0=ž'N¿¼¼+V¬0ýøÅ‹Oû÷?þøCh}“‚Á ¶lÙ‚5kÖ`Ù²eÈÈÈx8AfppÐÀ¥K—ðË/¿ ‹ =·®ëøí·ß›X QYY9ëúš››…ç=躎žžÛ¢ë¤(€Û·o =¾´´ûöí35³+##ÃÒ˜^zé%¡™c'NœúDY¿~=Ö¯_oyvZ{{»ð2+W®Ä{w𜂌ŒŒ@FFŠ‹‹±mÛ6ãСCÂåÜÞÞîtÌú½òÊ+øè£Œ®®.¡ç¶56¯bàþýûBõÕW“4ÿ0snÄTš¦Íþ™"‘HàÝwß…¦‰mbnì–ìÚµKx™[·n%a$©§ddggK9¯Û¬h4*|д¢¢BølÂåË—JKK…ÖsóæÍ”ï_‚A±)$‰Nó) @Ôýû÷¥8€cUOOð2%%%–ÖµnÝ:áeîܹci]vˆO‘‰0oÞ<¡Çÿý÷ßI‰?XÙ}úé§-­«°°Px7~Õ …BBýÖéUJÀ±cǤšÍ%Jtÿuþüù–/‘‘‘˜?¾Ð2###VV•R²€¿ˆ¶÷7ðÁàÉ'Ÿ4rsscáp8îàüùó‘““#ÝbD6ó'DQééé3ýx‘Ǻ%»=GHQYYYÂËèº>yaSGòòòŒ’’lß¾Ý÷eF…o·Âá°ÐA3ÑŸu úšÈBŠÈÉÉIú:®\¹‚+W®àûï¿Ç믿nlݺշ% z @tk&Ñohnèëëz¼þ&3¤8ŸŸ/ü{³U±X _~ù¥/.dá» º|ª'Ùôöö¢» ,HÒhRKŠ…B¢¢¢”®³¹¹Ÿ|ò‰2%`‡ÝI6+W²²ÛéERìU¬LoµãâÅ‹8yò¤±sçN_í¨²¿ûàÁ466&,éàÂ…  LL‹–4PVV8uêTÊÏoÿæ›o°uëVÃÊ5÷Ý"Ëþë\ÆÆÆpäÈÇŸWÓ4Ó÷‡ð:)v&½ýöÛ);0I×ué.E³Kõîf2IUyyyªªª”—@ss³4ç‡'ƒ,“f&=÷ÜsnÁ1R”——8ð<ýdˆÅbRO/¶`Ñå½<É& âùçŸw{Ž‘®€‰o~ø!^{í5¡ÛaÙqíÚµ”¬Ç ¢³[2tܶm›ï'‚M%ÍAÀ™B¡P ²²•••øë¯¿ŒK—.¡»»}}}I™jê§=h÷ì<Ñßõ333m­/Y.\ˆíÛ·»= GI[Sf^¾iò–Ö“—üš‹ÅpáÂ466 ÝÓOû¹¢“Xì€èÔ^Ñ“‡RAÓ4¼óÎ;R}úŠ@<“ײ‹wɯââb¬\¹Òøâ‹/R>®TÄ266†ÑÑÑhzzºð½½†H‘Þ›e§iªªª<{3S;¤<à„-[¶RýkBªX9ÈöçŸZºë’è¥ÇoM²ÉÎÎÆP^^.]ø…¿˜¡išÐn€_äçç /sîÜ9lذAx¹óçÏ /c÷ìÃIÁ`èééÁõë×…®Rœ——‡M›6ÁÏ'}™ÁH ãããn#)B¡P ‰]°½½}}}ÆòåËM¢³³Óž½dÉÇö³ƒÁ Þ|ó͇ÏÕÛÛk\½zÃÃÃD4}x€2cÉ’%ÈÊÊÂÊ•+¥™é7@ÇŽz¼×Ox™é©§žþt>tèöïßoªúúúŒO?ýTx\yyy¢•““HÅ©ã~"MüôÓOÆèè(Âá0ÒÓÓ<ú*¹hÑ¢‡ …Bq?aFGG£wïÞ vww£©©Iø‘ ˆÝÃe«W¯.€{÷îáàÁƒ¨¨¨0JJJ°dÉ’iŸ”½½½ÆÍ›7ÑÖÖ†³gÏZÚ}Z½zµð2d4ðóÏ?ãúõëfîø´Ý¥K—úêÖäÏ>û¬¥åt]Gcc#'ÿ“c¯¥¦i–ÇEÖÈy˜Û¹¹¹nAH$ $óë¶EEEÂ÷ {XÐ4ÍÒ‘u·½üòËnaš_|Ñí!(‡à€¢¢"_Î+++ dgg»= ¿·¯]»Öw¯¡ß±àçO®7ÞxÃí!vïÞíö”İ©°°Ðן\%%%M›6¹:†;vH9ÍÖX6,\¸{÷îu{¶íÙ³'`e–Ÿ6lØ¿]SQ&Òü ˜j .Äþýû¥9j]]]xâ‰'Œ3gΤl;vì`ø]ư °°{÷î•&ü“öìÙX·nqüøq$óâªÙÙÙØ½{7¿ö{€4°sçNtttX:ñÃŒ`0ˆ5kÖ`ëÖ­Ro¸%%%’’´¶¶gÏžEGG‡#'Diš†¢¢"TTT ¬¬LÚ×Ïo¤)€µk×Ö®]ûðß{{{»w··nÝÂÀÀîܹƒÑÑQŒŒŒ щ>iiiX°`233‰DPPP€ÜÜÜ¤ß ô™gžz|²/uVVV(++C45zzzpùòe\»v CCC¸}û6†‡‡ãƒ¦i‡Ã_¿ÜÜ\8òúeee ½NS§€S|ÒÀL“'~̼ÐLÑhÔˆF£/â–÷ßß“ŸŠ¡P(P\\<ëë888h¤âõ›,%rŽ´`V( ¨r£Œdq»<É:þ H¤0‘ÂXD c)Œ@¤0‘ÂXD c)Œ@¤0‘ÂXD c)Œ@¤0‘ÂXD c)Œ@¤°¤@SSZZZ¿/‘ÌZZZŒ¦¦¦¤¯'é ë:êëëYD&µ´´õõõŽ\y.)Ù` ™“Êð)<À š]ªÃX(M³Þ,¢øœ¿•l /QYY(,,´|Û–ÑtN„¿°°0VYY)|yvKßjjjB,"ûœ MMM(%ß–‘Ü?hãããw,-È ²Ì á×uZZZÚb«` ‰óBø‰üÚþ%@džWÂ?I«®®¶ÿ$,¢9y-üÕÕÕßXDÉåÅðS~` %‡WÃÌø%@ä,/‡ˆ3€%@ä ¯‡H0ˆ%@dÂÌ2%@d_ÂÌ1˜%@$ÆOáLœ À 2ÇoáLž Ä šÃœ È ŠÏ¯áOf Mççð®À šà÷ð/ ÕÉ~ÀÆUY¤*YÂؼ,8K€T#Søî À UÈ~À¡ƒ°Hv2†pðÎ@,’•¬á¾5K€d#sø$Ü%@²=ü@’nÊ ¿S!ü@ïÌ ¿R%ü@’oÎ ¿Q)ü@’ ` ¨~ °ÈûT ?¢Xä]ª†Ha,ò•世y‡êá\(€%@îcø'¸RK€ÜÃð?âZ,J=†:W ` Pê0üs½–%ß' ` Pò0ü‰y¦–9៧ ` sþ¹y®–ÙÇð›ãÉXdÃožg ` 8†_Œ§ ` y ¿8ÏÀ ¹1üÖø¢–%Æð[ç›Xô8†ß_À G~û|WK€~§ø²–€Ê~çø¶–€Š~gùº–€J~ç CŽíµ®®Îös躎ÚÚÚhWWWÐêshš†7Ú Mwÿþ}\¼x‘áw˜4x§È{þø|¿ 0•WvÈ[þĤ*€%@Ó1ü³“®–M`øç&e,Õ1üæH[K@U ¿yRÀP Ã/FúXª`øÅ)QK@v ¿5ÊÀÃoR°dÃðÛ#ÕT`NMnhh0ìÌO'ë4MCeee€á·NÙœ)ò?UÃ(¸ 0•ÊoŽ7ùð=7yð½LŒ0 n8þÇ÷pv,€9pò/¾wsc˜0uCâ¼ï¿3ùÏ ¿9ÿTcÇ-¡ÜÈIEND®B`‚dkopp/images/dkopp.png0000664000175000017500000003134615002404054013726 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójS eXIfMM*JR(‡iZHH0232‘’pœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Markup Text|$Ýþô IDATxœí{˜\U•·ßS4¡§ÍĈ± 1äË@ÀŒ1"¨ 7‘›\u”›€ˆ ~‚€ €ˆrD È ƒQn!Æ#DŒ´‘‰=1šNõüñ«¢«««»ÎÚgŸKUï÷yêi#眽«{¯uÖ^{]¢(Š´4€i•Ï`°IåçÄšÏx èJ@GåÐ_ù”µÀ:` °ºæ³ x¾òs%°¢òéMùûR$  %èf³€­€•ÏfH¸ód5ð °¬òyX,EŠ$P`‚(]À`{àmÀl`KßÖ­B?ð°xø5°Y‚@þt;;;ÛÐz—~`0xxèÉuFcœ ²§ üû€Ý­óNî,î~ŠB°2$(€l˜ ì ìì‚”@`8kûó€çrÍ (€ô˜l‹<ïø”G€[€[щCÀ3Aøecà à#h?„Þeä7¸ø!ðR¾Ói‚HN Ø8ø:² ¤Ç:à.àZऎàÎdà(à`z¾S³,®®^Èw*­IPvæ'#S\Îs ˆ>´5¸x,ç¹´AÄ£„ÌûSÑÞ¾•éc0B¯¿ò³wÐIë+µùÀWÑ6!lšÀèt‡ ÁŸ•ó\Fb Å}®òy™Ã=ÈYö ×íE‚ßßø1¯ÒÁf¼qåÓ¶=›¢cÍ©(y¼×oã%HÜDóï,B[º{r»p “õ"¤²d wý r\õe<~ÞŒCÕ½P˜tÖ>–{€Ï ¿Ã˜e,+€‰ÈÔ?žl²ïÊèˆê6ÚúLc¶›¡ÐéýÑQkQ”ýÀ•hk°:ƒñ ÇXUG!sRc-¾¼Ñ!ž=ÓÐéˇÉfK¶ m ®É`¬B1ÖÀ,à[ÀŽ)³ üµh?pg6 ³>„ôöCÀqŒ¡mÁXQÈÌ;t]®n'”ÃòM'°pé*ð>à+h{ØnŽØaŒ0½‰Ó2%ס·ý7 oû¬˜ œ„¬‚´’¯!ËcAJÏ/í¬:€3P0OoýÕÀeHðCY«|èFŠàDÒ)ŽÚ‡‚ˆ. M­vU3Pîø¶)<»¸yפðü€ñè4糤øªñ°,…gçJ;*€CÐ>ÜwÔYðeà*Bݺ¢Ò œŽEЋü7y~n®´“èB‚˜ççö¢7þ%„7~«08Y¾_×#EÐ/vQ[¢Ÿvû€oç c½@ë1 88¿~ Å(`é)ÏÌ…vPû!/¿OM/*ú±Ôã3ù1 y¯Çgö¢S‚Û=>3sZ¹he ÕÞ‚?á_Ž4ûûÂßN,EÓýÑߨÐÚ;–£ ZÔèB¿üc_ ¸•ð^äáybò;TCp#ü”jPVãÛ€;W>/sZq 0ø1 ñÁàc„ ž±Ælà;(PÌ ÒbuZÍt™ døþu(Hh;‚ðE¢¿ý™ø Ûö¹63£•,€Ý‘§ßÇ~ðQ‚¹ÛßÃO¸x/ò5ÜçáY©Ó*>€ƒÑž?iO½2ðu”fú礓 ´ /סõµÉüJ¡õº šVPÇ£½ZÒ¢«P½¿ËiÓ¸î@"úÑñïcèÄ ÉËft<ÝCÁû]œ†ÞØI'9m!O<£@»ó4*à²=*îJ„zI¼ŒÒÄ I‘ÀÙÀ—<<çb”È1&K>œèE!¿ãw&|ÖîÀð@ÒI¥AQÀy(7 kQ^ÀÅ» t ØmOv^‹êyÎc2Úü-Ý0Á³Þ¶°¿ð0/¯ñàl¿„ç€}(v1‡(”ôPT¯–eÈjy$ëI2øŠAIÂ9À“OÇES§¡”Û$<†„eòéx§í O@¦áhq½À;hƒ„“6a Rs>çtTr¬IÒy“pzs-UsÊH;™áoûѸ E—ŠA*4³oÂ眀 ÊäNQÀÁè›$2ñJàkï<5Ÿ8·¦~àu„:E¢„Ž’OðŒ2zQÝìeF (B(ðîÀwI6—sV-ŠðOAé§þ ÷èŤDÅ¡ŒÖZ'u ­ùݽÌ(y[³Ññˆ«€”‘Y}™·%ccÔ`âD’G-‚¾ßë G˜EåD¤è]_^½èä'·\”<ÀT”<1Åñþ2Êâ»Îׄ0ø$J,ñYöA´@Åå©êªV¢ðã\²óR]À/qÏœêGGh7x›‘;û¢æ¢›y~nþ_{~nÀ?‡¢ªT®áê w‘ƒó:@ …Z&þÃÉ_ø7C}ÿÿ¿%,áo n@/$דÙH&2—Ç<"¿ˆ*ù¸PFÇißó73ȱw ðÏÏ^| ™•Eb gð,°7n¹+oFID™F f½Ø Ž«¦;‰|~Û SÏWÙw3Š(tæX '¢nQ.”QYºÌ f©¶ÅÝãŸge ø QöU^ú´P®#xùÛ$áì™F€f¥ºÇßµnÿ•èì5¦£-‡¯Ž´ Q¸ó턺í̸ -F'©;³r:\»ðß"üò`?à üÿ#À^¨‚ì Âßî|­]¶&yX|,²°ÅÝi÷: Ëúx¤"9Ñógá¾­K tsM ú()Ÿv¥­f *<.ûþÈ zÁ댚39*wHøœt Å QdÏd´ýµ$UéÞNŠ]‰ÓÜt 7¿‹ð¯þ•ì…[ä¨L"ü}è(osÔ[0ÿØæ´–]¬Ø H†’ÖÑ4ãÎÀ½SïaÀzœKFfúë<ã~t|RÀóèÔç‡{§¢ŽCzQ…´ÀTSm‡{/®|²ä _àZöi5ŠQ8ø‹¯IÚŠÅèîRcpG`R$^IÃÐöý.Mæ#§_Vr¹Ý÷ ÈÆ–j È…ätÙb.Bþ¯²‘†à,Ü„¿EAe%üã³ÏUø×¢£ž÷„?~´Æ{îÝÉ–W|[³Ð¹¹5Z®ŒJ_Íó9™QèBI<®ýâB±û€•=Qƒ[ë ¸Å‘x[w¾-€oá*{ Ù ÿݸ ÿuèx2À•yhÍ[‡dÌ>-€£Pa+‹Q쳭ͨ ÿN÷®>EAŠ9ZžNtäì!û1_’_ `"j©4Éx_ÙµçîD¿‹Ã½=è,w¾Ï Æ<Õ–âV«y°’È|^ˆ›`}ÅŧMj-îbö/v~ëuFÀ` Û®ÆûºPÛ²»“NÀ‡°5rüY£• ·Ú^ÿЦ:ÄáÞŸ£7¯×Ù˜€ªÇNF 澜çðK²¬5&ú‘C0Q rNÀ ± ?ÚÇdqä÷UÜ„ÿftÄ——°m†bþŒ¬—Ë+?ŸÅO’R ¸ÊB’½D$ݼ·Â_#›š~'âÖaø*äÔÌ#ewò_‰,¤úýáFèéïŸD»ð2éße¼oð+Ô‰$[€2ý­A?Ë­H?ÅwtÖjµN.FÝ|²f3Ôù âÍyJ8*bÄ€.àIT€ÆÂB!è”t–d pnŸ"}áŸü€Öþ‰¨öÀïÐV%îœ;QÁ’@{°Ɇ•ÙHpU¸™þ÷’~aŒ‰¨‹«5 ù2²þðqt|z nTÿäuF¼¹Ɉ•sqLvU‡¢"ŸúqÓpV¾‹öF®AÙ|YQ=ÿ½{ìD-!ó°ýøvßÓ–H&͸(€TéÆÊU¤>{ÊÇ·pî} ¬t磰¤}æA™ˆöb ’+gâ`¸8CoY ½ÈaµÊ:˜Pª¥å—ðk`7²©9¸ ŠGpñ›4â&Ôb:Ð~LBž}ë6öpT‡#6V  „ºßZ¹t…"j­dþgPbÂÿIdòûþÛÉÎj dÏ*ÜÎøOÅ(ÓV `oä`³° 9«Öï³p#¶`Ÿ^T™%í-ÉDd-Y·%(£(À +?íÍxàØ}DûwƽØj¸¼ý¿LºÂ6á/“M.ÿ6¨2RRá/·¢³Þ÷„¬°ÉŽ“ŒZ,€¹Èye¡½ýÓ2³'£à‰ ÷dÑbl?ôæŸð9w¢*0‹Ï(Њt!+ Ûxß;ˆÙgÒb¸á]Hº{ìK± ÿ<ÜBƒ-œ†J%þ¨6â>áˬÅÍ[VãZ“?a VY lJzæ¿Õ±x+é9#}} 8ÐO 0Èx”6ÑpOðÿˆÑW#®pöHµËHOø»°Å¥ŒŽÌÒþ¤FAÙ‡oFgÀAøUÖ Y²0ÉlSâX%t&9Ý0uH¹T?ÃWQ»î¸| ølJséDF÷p¼pö„´ÝÈï4ܳÅÞŒú2‰cì=Cé&Òþ-Qì|\ªÍ9Ó  eº ÿ=À?„?0:=H¦,L'ƺŒ£Ž4 rÎ¥ÅEÄߎô£ù§Qp´úæßÝáÞ~´×ß‹ìûZ™j*»Í¶£Š4Óã!à_ ×[Øø™áú´Lÿ´çß×áÞU¨9Äý>'üj—uÀ›s¹!Í,€ƒ° ?ÀÆë-XŽDžAgþipnÂ_-~¿×ÙÆ VÙê¤I­€f Àšl²Šôö³`+œ˜Vá‘ÏG;Üw*ù´Üëlc‰Û±Ÿd*ã)€iØ›ÞD:ûí*——yâ¡ Œ[!”›Ð~?Tó $avgàH–2š8 ÉoĵÆëãr03æµ}¤SÙg.ê|dý\ |Í+HŠUÆJH–Gü#q q E¤Óá§„­ÉUÀRÏs˜„Jrwï» 8ØðÇBìáá#ÊòH `*°­qï¯ËÞÄ7}4J(ÝxD3jþlËŒÆVYÛÉô0FRå¿5¢Œ}oË1Þ%ø÷= {K±ÂH›°Y•%$Ó ÿC#>hœÐcÀ ã=qØ–øŽÈÕ(HÈ'Ûc·(îAÁì¤Å b¦ûÖÐP¦)€.ìÍ o1^K òexè–ZCÊé·”[„ö[yt Œ-¬2·+ |XÀ.؃Ò8ûï&~ã‹5¨¹†O¾Š­ôyÒ²iV? ªXe®“¼)€÷¼EÝùæhâÇü_Ã(áŽ숚vĥ؟t¶A@#žÁ^Ön˜l7RÖä–yÆëã·êm?~“ÆßÂæ=å@Yb•½a²]¿È»‰äVå'Æëã°ñSï¯òiÔ[0.w Ó‡@ k¬²·5uõëÀNÆ®!Õ–dŸÉGS°­D½Ý<˜Ýç4DÆë=Ü;ö þC\-]o—á·Lö—‰_̳Œ„ß§ï!-&¡Dª­Q•˜É(Õ»êì]‡¾Ç ¨úÓbT˜4Íf.äô!ÜÓpÏΨÌ<0\XrÁ–›—=‰ßéZü·ÏÆÖ`ñ:ŠÛ›¯„â'þU…™‰=‡¡ŒBªïA…OæbŠÈϰ)€!2^[¤ ø¶sïØõÇ ü€xýÎûQÏç<ûâÿ"_ނ߸LŽEÔ鞟½¸åZË 8Xûuô¯¥’*_ûVØ›ð÷â?ù§“øBx?þ„ø ¥" 7: ùê><=…1¦Wžý§ÊXÖftXˆ-ͼƒš<ŸZ`mWýþ#Þv!þüǵ }•ð.%tjñ4j@jÍVt¡«2ÖÓ•±]ZÌüÑÝ UÖkÿxo3>äׯëã°OÌëúð}8‡øU}ËÀÉžÆMÊ–¨ãðEØÛHû`Be쇱ELüc•ÅWe½VXÊmþð¾‰+ˆ÷ãÏûni¦xéÔ<°r0jo‘ßíiŽíà_ðŸþ:-öV^Šši&?èF-Ïâìÿ磆y°%úÛ¸8üzQ•¤KqoÔÚŽ=OÄí¨q ðvà)ÇññÙµ‹ËàíU€ÕXf¼>«Q“‘‹¼|?ÁG{ßù—f«³Ñ¨V#vþë- ž¤KsՌߢòL+ãÑwNÁô±Êä Ðf6í¾†ô:ÚÞ l‡ T³ ×¢*¨oÇ^}$6yÝjj2§2æìçü½Èt?¿íÙ{*Ï<{w£¹ØÚ¹Üx[jð`B {½û4ÊÕ²µÑúG`”¸ðü:áþóº[I§ÕY3&a9X¼“tÖ­•1¬§0çßápÇ*›Ó\€ïóÿ‘èCZ- »qk¤Õê¬gb³ÊV <ï,Ng–TƲ( „,°Êæ´:þIs"ò*å5óH§ÚQ3ª)½qéÞO¶Ç”Ë+cZ¶Ç¬€´±Êæ”ö´ÎgוÃѱX#îÇÞÝÇcËê;’|â2–`+ÝÖ…¾[ =¬²Ù]Þh¼É§s)OzQ™äýQ’ÏýÈÙø!`7òÉ÷/a‹O¸tz2ÄåöÊâò1‰@šXeóØÍ²´Nò ŒqžBTËÄÄë%6èV>ƒŽUãø,¦£ïJ¨§ƒU6'•PŽ…V(‚Ùªìo¸öÃëAs‰‹å;lXesb U‡Ms@|âÆr¯¾™æDŒ|“øÇ¥ÖNËøXesãj …"ÕÂk'&QIÑŒÁ<Šñö¯ÒCü.53 §ia•Í %ìuäBóËt˜K|YZݘ“wN%ŠQɨ±Êf—‹Xk¼>KM†‘Ž/óÄ2'ký‰@<¬²iV}¤™ˆŸŸ°œb™ÿUzˆŒ÷»lôcëÔÕUÂv.ë» X`¸™KSE2âÎÍ}ˆEFK%lµÃÛ?=âÇ®LuɈ;7ëÑs >í°*€Ð.=:›_Û wnq¿kÀŽEF;BXf 0†)a3‚ÂH¸4EêPOܹåQca¬`‘Ñ~«°l6âqÙwn!˜,=L>½¶=ƒk…Ø@sâ:ÐâF æAܹÙ‘ÙêXd´\Â<0Ž`¤Åb^7b¶æî&~&cÜï°ÑM¬µ*ȦõXÄRÔc×ÔfáŽeNy6–igÌQ½.  ÈN¨Væ1âoÇLs"ŽÄS{?û@<¬²¹¶„½ÌsâH‡UĤۓbmºÑœâ°}×€¬²Ù[Â!‡Øx} >qûv'¥9#'?¸ÇWoÇÀp̵=JØd‚HÛ ×~’bXÝh.q±|Ç€ «l®.a7Ç&¯Äg>ñ3ê&oÓÒ*¿¨Ìrò)µ>V°Êæªð¢ñ¦"¼uÚ•2ðÃõ‡û¥4—8ìW™C\¾CÈ'I«l¾Xž[¾©ñú€«°Ì\K>6fa뜴¸2¥¹„U6{JØ£²¦¯ØèAJ .€»‰„ãƒé•1-õ$¯"xÿÓÆ*›+KØ=>çc;ž<@6–À¬ÊX–ž’½è;ÒÅ*›+\ÀfÆëvVçï™ü 8Àÿt^å€ÊÖ†²çÞþY`•ÍQE vÙsnÚ«CP)ãVA÷zà³ø«Ø \æpïcÀvç_ÚLž7\ß ¼¶š;¼Ì8Ø ãõ;eÔ Ô¥ÐaÀÓÈìNrjÓ]yÆÓ¸ ÿô‚ð§U&—Á`ñ«(rJj;ñpŒã½€3PÇØÛ€ƒ‰§ º+×ÞV¹÷ ìÍcªƒ¾C }¬2¹ S{­ `+ãõwnþ ¢ ãÐy}5^`9ŠÇ_É £q*æ1§  ¹²Á*“CÀ“Æ›Cc‡l9½™öð¬é¤døm4ç@vXeòIÜXó³g¯$ç8$XEçÛh®l±ÊäTK±Õì&ÄdMí©/È{"£pšcpúeËTlÎÞ~*©çU°»`Žñú€Î>L±ú¬As f>Xeq•Ê̵%„²ñú€?nÞN1*ë<†æ~ùa•ÅWe½V¼ˆÝjvÆ*s?§A¤h#ðcãÃçb¯ܘ†=ü·¡LoPPåà ál@„¼‹5»°H ä=…@ 0‚ŒäÉ À{ ×—Ó il$ à9àã¤>l¼>¸a•µGLc$¶ˆ@€m¥Â´™d͈²<š¸{É‘Æë€ «Œ•© ý­g4°{/÷CL0ë|¨|ÊÀ\³-ðJÍuV«+0¶èD2fa>£´ÿMÜhl!& H‹ýŒYU†›)€b :Áx} ˆ‡U¶Ö!‘f à%à.ã ;bwRÑÙÉ–…» H3p­qP€“î #ã"SMe7Ž¸Õƒ³pɺÒAº±;ÿ–#Ù•8  ŒŠAZèN2ÞsöÓµ«‰qŒG\ƒ½ì׉ث•¡ŒG²d¡ÉlSâ*€%˜`&Çï C9É’…[‘Ì6ÅÒàëØ÷!ŸþlVdÁF¨ÂV¨êzô‹þ °ˆÖh’oÞ¼ ™–kgQ˨?xo:ê´)úýý7jLù0ð÷'Kº Yùzì+£(²|~EÑ€ñóiã™} L.E‹j Á§ üíÕ6¬Ü“V$`„šp®¯¹o=p£goŽ>‡¼<Ê÷øðIàbÌ¥~þ«ùo»¿e¬5ÈLSM*«q’ãý´ƒ¼ýÒ$Æ íí0¡¿DQ4>oaO öþ2Ââj$@o _`¸ðŸÉè¿êç;T?Oom2ŸF‚Ù\T7ÇÑ>½Àá§!9­ÏñÙ±ÊÛÞi*€REO:Lêóy »£ø0r¨Xgø:ƒõ©"àË ]ðý¨Açh_fwà¾Ã²xF+û^?ÿc€Ëùm<Ò§å«ç=NCrZŸŸw³'+2šš Š¢Ã&ö·(Š&å-ðF0™õ‹è)àtà_Ð^úÀ§Ñ³º Ë( Ó—(¡·]í‚øD“ï0ƒáÖKøpheþoÞ‚ÞŽLé瀩#<¿~þ÷3TA->"ØÞŒ”É)ÀÂã¬C-Èò§!9¬ÍI™±ÊÙafp˜\GEO;Lîò¼Þ 6DΣڅ³9WFÚo\Hc“4‰(¡íDíBîŽí Tî»»n/£æ¡#}ù)³Wêî»i„{êç_û}o&Œ0Npz#×Þ÷[Ÿwg5NCrX›—;È×ÓÙL]EÑ|%Š¢Yy }LpC¹Ú†»™Ù1\X“(€ €+ëž÷2ðÑ&óx7CþzâŸ'ŸÅp…ÓÈ0’`ÞÁ 3t4NeøïùðÇiHÆërVEV¬òu„“ 8N²#Š¢ß;Lò§y } 1üÍùñ߯AÇOI@ò^× âÿÅœÇusøR(qؙϵ÷_cþ¨¡è&1ÇÙ5«¨½ÿ†Ç§d5NC2^—?u«ßGoÿ$ €(Šv˜è@Eûæ-øMqXp“Z6-È8Y1É„•{ :“T€¬€~ã=Àw°å"dE½¹ÿÇç¬r¸'bPpÞÇÐ’lS€¯Ðü4âuuÿîixUsêï{}“ëÿ··Qý8Í’_²' \e¡7 |>ÀbtTeepš‡ñ}S/\VåVåÇûP·½Ð¾ÿë }{)†ÑW÷ï—çR¿mhv²Þqœú·yýüó' NC²`åJ${‰ð¡@çÆ.o¼³(ÞV >sѵ¦Õéô{PþAµÓ Ý‹o|ƒ‘ƒ_`¸òÙÈa.0ÜëßL©u¿\ý}µ4SXY“6³‘ XYåxß0|)€ÕÈae:î*R/ëþíÒô4"æSû3Ôé¸eÖZ"3€³GyÎêº[KItß°ÞrulBüXƒZêÇ-È8iÒ‰Ö¾‹r*ÃÿÆNøR  —æ [ç{œGRþÀP“û­ØÛ†¸y˜WÐØ[ÿ ôû­Î+BQ}Ûðœú>p[`c–zì7ü¹É=S±+›Õ&¨å'MÎGkßÊCĬöŸ à8ܼ³§ xXÄPSw+àŸŒÏx+ª!à‹”ò[+Ø—ÓØzZÊÐ}òìÛ€ ] ý4?ÚÜÜXxŠ[¨RFQˆE'-öDkÞJ’1oøVK—Úe×âf6ûf:6«2û/ýhüŸp¬BGEµÞï94^H¿A)ÀUºQö¢…÷ÿXóï 8úî–uuCý,«ÑV¨ã¤Á´Ö]dï+HÆü‘BDS‡c„à@¤j&N1Í#AAµ‘xÿCüf's#ÑG2P=%Tï­öÙ½(ð§–¸­îºŸÑmˆZJ×Þÿíó@–Â1ÇÙŒ¡!ËÀu9ŽÓϲáRUk "SÞe#-ášEÑËŽ_ô¢(€71¼ü×3@³­À4WÞñYlÃsüïc¸Ÿâ ¯'‹”B[_{à1æ_ý¼„j'ŽÆdÍW{ßË4>Ëjœ†x\s9ÊÄË™ò/) ØÙŽ_6Qrƒ'Ã…`=ŒážÛ ÐÛ蹚kûjî÷Ýø8† ÷1 æôPÝü×0zóÖÕ¬¶;hl=Œ$˜ÈÄþ8Ã}è¤ãOu×—+°Õð=NC<­7×乊,¥#) XGE;~á¿G)i<ƒ‡Â,-º¿¢óúë+ëþûË(À£ºh}+€,T;fÃCug£-Bíuë+ã¼E v }éÀÏh¬ôF²|ê翼Áx/¡ôêëÑ6äyÿNg䨆¬Æiˆ‡µ6§²¦]dáá(ÅmqªEьȭ´Ñ@EŠ¢hrŽ $ ÷c«=ׇŠVÎ&=:f¬¯õ÷C†¿©D )îük?½À.£Ì¡~þób©zòHŸ'ж+ïq’pM®¬eø[E†Ò“”Qêøå¢(z4J1u8&À—š;ÒçY3·CGMO;5xöÖè´°ò±æ¸ŸRyvõþÇiœ&û~ô&·Ë9ΠJ#ÁŒSò¿h®8ûÐQf³7rVã4$Á몬a×õhÚò™… Š¢ï&ø%üG” ßÙƒ¨² ðoh[ðçÊ¢ê«üïy¨ƒK‘[¡½eŽV⼌”Øçz8# &•Ÿ»£ •¥èd¤yãÎex}¼Çiˆãú*UÖ®ëºÿnF²éRm¦ Ùt‰|%>œào:b``À÷#[q¨âñl;ý/üÇ‘E7Ùf[TI¸óp7Jbòý‹Íjœ†8Êȸ·Æ[Œ¬¯Ô;je•Ž»ydÅÍ ;9u¾èsRc”>`~åH‡³qþ^$+™´Óó 8O1XgÎ…s±wI ²æD´V](#yªÙ…¾ÈRÜŽÒ[]¹8ÂÏTïÖ¨+ ÉŒ¬(ÌöNÇ{KÀÕØ»is(Z›®2u'9ÔÊÌC”QqK×L¬à»èCQ‚«Om!’‰ÌÛËç¡@Ž¢:ªJà_ 9­EWá_‰d!§_=y)PÜü^4¯23%TL18yq"Zƒ®rÔ‹d ¾€Kfä©@¦Ïþ¸ÁÆ™£•Ç Òàl´ö\e¨­ý¼Š’ˆŒ¢š}Ž¢h}‚¨©(Š®ˆ¬½Ñ4ˆð»"áZ]e”ñÚì³AAa1Êf³ö¯«e.*ÅõcÜKrè‘‘.Ô¶-©úã ’¤IQ(qæe×íÊLT3ÿ' -‰8S‘‘)ÀO±×"¬çtwZÎ…"),`ñîϘ|µ~ÞǤc›(Šæ Z 3>êŠU;³d +硌´$¬Eyù7'ŸN` s0òô»4z©åKxjæá“¢YU~ArK`CT¦kªž“yE ¥é@õ.bx‡$+ç_H:¡4(ª™ðI} b–»£<~טƒÀØb*ò#äáY§S0³¿–"+OàETe'ÉD7EÅ<Ÿ¤ymûÀØfOTïñÍ ŸSFÞþÂ8üQt:x 5ÌtéW¥ Å[¿x÷®¿ö¤ø*j¼úš„ÏêC%دKøœÔ)ª°»£fNuÝêX„þ@‹<<+ÐúlƒuÆmþ2Õ‚÷yxVêä lá>`g܈jÙ•(;ƒìª"ŠGZãGøW¢5Ú­eT™Š¢ýf{zÞt\˜oLv kf£ã½Ø‚š°eõå–ØãB+YUžÞ…{Q‘zæ Z…’ü¬7P|ºÐßúQü ÿhM¶”ðCk8ñ j‚±°#ÉN@Špà#¨:îï>/PLö~„<ý>^~etÄw<î­¹ÒŠ[€zöCÕX|8«Ü œŒêÌZŸ™¨Vß{=>³ðÌ´†ŸoÚA€ºð܆{ßFô¡–Øç &ÖcªÐ{4ú&a1òôgV½7-Zu PÏ_QY¦MQJ°6@­§GÞâ´¨™7|¥îîH²ø‘z®öE­ÖZžv±j9ueñ¹%Õ+ø2p9Õo 4¥ 8…ßv{~v/êNu“ççæJ;*€À4n”™”äE¾X“ÂóvÆ#Kí³ø|€GƒxY ÏΕvÙÔó Ã\Žg|š€¯AΤ£…÷$êÈžnà4”ò½7ÉCxëéCí莤Mý@íjÔ2øˆôjÄ:d~“L”³“Ðv¯3¥1!Á_Òó ÁXP 'ÞYèmáÓ\ÏCÈÿp;R t¢#ßc/-ú€¯ ¢4mŸ06V@•YÀ·HwÌś嬂dÌFoâCб^š<,IyœÂ0Ö@•£PêgÚ dJ~)„Œ×LCÿaÒÛºÕ² 8¸&ƒ± ÅXU‘™W=çO›2ªmpp+ðLc¶›¡nû£ïYä©ô£Óœ³€ÕŒW8Ʋ¨2 Õ}Û#ãq—óPé©ùŒ½ £q(ÿb/›?+ãñï>Ã2÷À { mA&g=kP•¢ŸU~.¤ýPh?¿°[åçøæ±™û÷ä0vá `(%Tò<L”½(øäT¬b­—j:Án‡²¶Åt¦…eÈÔÿ!¡Bô«ИÔþéLòUµô Ë` >ZŠuÞ1é“Ñïh&°2åg“NDž ËPÊî ´ŸU•˜ F§y£O%û=j\Ö ‡âs•ϳH)ô ˆÈ—ƒ«Å&4‚tæ>9J7®|º‘°oŠÞîS‘ã.3>KЖî&‚àHPñ(¡Æ¥§"ÇU+ÓÇ`RU0ª§ ¤(•ó‘àßE0õ›€¹¨XÈA´¾°´ }ho):j Ä$(w&£€¢c€éùNe̲¸ðäí iI‚HN !‰¶ i%§Ä:dÞ_‹Žò‚™Ÿ€ ü²1Ú|ù Z±êr)£½ýÈÔ)ßé´A¤Ç4Úz :ÊÀFÅAÜ‚B§CE  SQ¸ë>À.„þ#±¸•îžGë?µAdO ƒ}êwè³’q+²µÒú) ƒõ3$(€üéF agä7؆öíWØbñç ïÉuFcœ ŠGŠ¡ßx «Ý’ÖS ý¨nþBà à×(§!¼á DP­A'еŸ…âígT>›¡pÝO¢0Ü¥„²h…'(€Ög:q˜LAUŽ6©üœXóI:‘è`Ъè¯|Êè ½å¬®ù¬ž¯ü\‰¼ò+PŽA Eù?u±±§:„TIEND®B`‚dkopp/images/dkopp-jobedit.jpg0000664000175000017500000034741415002404054015346 0ustar micomicoÿØÿàJFIFÿá(ExifMM*bj(1r‡i„ˆ%ögnome-screenshot0231‘’/Æ 0100 ÿÿFotocxx:trim/rotate|sharpen|trim/rotate|resize|NEÿí0Photoshop 3.08BIMfotocxx, ÿá ´http://ns.adobe.com/xap/1.0/ 2160 3840 0 ÿÛC   %# , #&')*)-0-(0%()(ÿÛC   (((((((((((((((((((((((((((((((((((((((((((((((((((ÿÀÖ'"ÿÄ ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ $4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?úBGÀ¯8×>.øcK¾–Õ®'¹’&*ÆÞ=Êê3žk©ñÃÁá^H˜«¥¤¬¬!5ñ…}&~6øgûš‡ýøãIÿ ·Ã_ÜÔ?ïÀÿù¶ŠC>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm«Qéײ$–³2NÛ"`‡}­}ÿ ·Ã_ÜÔ?ïÀÿ?ávøkûš‡ýøã_8ÜC%´òC:4rÆÅ]`©A¨è¸XúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãGü.ß sPÿ¿ükæÚ(é/ø]¾þæ¡ÿ~øÑÿ ·Ã_ÜÔ?ïÀÿù¶ŠúKþo†¿¹¨ßþ4Âíð×÷5ûð?ƾm¢€>’ÿ…Ûá¯îj÷àð»|5ýÍCþüñ¯›h ¤¿ávøkûš‡ýøãJ>6øgûš‡ýøã_6Q@M[|kð¼’v¾ˆÏò5éZmý¾¡i ÕœÉ5¼ª$C¾¯¦?gÉÝü ؑ̊ žƒƒÔÓëJiÕg"¥ Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( GÇ'þ)]gþ¼æÿÐ |s_bxëþEmgþ¼æÿÐ |wIŒ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠÚðÞ—m~o'¿–Hí-!2Éå¸ã°ÍmGs Yø#û>k³iöé ‚7Ž[®8®AÕÛIšb`Žæ ÐÇ,2te>õþ m-´q ¶›9™"$îÎr3Ž:ûÓ¾ÂkrïˆtÛhg¼“RóßVÔ/_ì‰Æ_†rAÉöRK¡h­{¤[ÝÝVÖ33(òØ’£¿ëT¥ñSÙêóÙHâk¦ºµsqóZ±là|¿0ö⦛ÅQ³\ÜŤÛEª\CäËv’F1¾¿.ƒê-®›¡.Ÿ¡=ô—é6ª#¡]ˆÀÈÆ{ŠlzF™eg®\jÍxë¦Ü¬ìåFàqÏ úÕ©57OÑ|2ÆÖÓR–Ö7!Lä5»eq9÷ô¨ìµ Iü=â E-ng»ºŽV³’S“‘Óñý)¾¢] ¿iPi²ZËc3Ëiu–"ã ¡®§Cñ&¯&›s©ßÜÅö;Ö8!HKÉÑr@ÏÈëúÃjÓBVÞ;kx#E …QïÞ¥—Y¼1“©GYŒ¯?™þƒn8üétcêŠ1\Ç&¨.oâó¢y|ÉcS·p'$ t®›Æ2‰ü9 Íº[C ‘’ä ÝÏ~bjz­½æ‘§ÙŦÁo-°!îüÓg×ñ©5m5 J°[SX£)“ÌÝædç¦?3CÚÁÖæ-Q@Q@Q@Q@Q@Q@Q@ƒ‘WäÖuch休V¸ Ð (¢€.iñ]j1E>óÜX!Á8Rx8>•gËÓî,/%¶·¹†XXœ89`¸ÆÁëëT-.d´¹Ià H2¡‡Lt<±>«q-¼l¶Ž90Ê·D'#ê(Q@^µµ¾Ô’ÒðΦqåÄðÛ$$`²ã%zç^ÕoÅ4ZØLï&¤£tî‡0àôqó{ž•™§j:t’Ég'—$‘˜‹€ ã8=G4Ms=…½œ²o‚Ü“ esÔg®=¨­}û>ø¢›þ¾äþK_8WÑß³ïü‰mÿ_R% _„ð*z‚‚¦Ä-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@—Œâ’êÂ$²ÛI"Œ–b¤=É5òÿü -ÿ¡wSÿÀv¯¬¯ÖEÿ]SÿBºì¨ŒîBªŒ’zIÂâßúu?üj?áñoý ºŸþµ}ioâïÜÜÇoo­éÒÏ!ÂF— Y ¤>*ÒÍ­½Ä/4ÑOwö%) KœAÇu “?áñoý ºŸþµð€x·þ…ÝOÿھѢ€>.ÿ„Å¿ô.êøÔÂâßúu?üjûF±fñNƒãZM¬iér­°ÄÓ¨`Þ˜ÏZù#þÿл©ÿà;Qÿ‹è]Ôÿð«íA‚<‚)hâïø@<[ÿBî§ÿ€íGü -ÿ¡wSÿÀv¯°5mZßK{$¸YXÝÌ Ë\üÇ×ÐqZ4ñwü -ÿ¡wSÿÀv£þÿл©ÿà;WÚ4PÅßð€x·þ…ÝOÿÚø@<[ÿBî§ÿ€í_hÔsËÉ,αÅ–wc€ u$ÐÆ_ð€x·þ…ÝOÿÚø@<[ÿBî§ÿ€í_dÛÞ[\Y­ÜÅ%«.õ•+ëŸJ[+»{ûHŽ{yä–6 ¬=Añ¯ü -ÿ¡wSÿÀv£þÿл©ÿà;WÚ5“ÿ – Ôfk ±iÒ®Y‘€F¹õíšùþÿл©ÿà;Qÿ‹è]Ôÿð«ëý Z²×-žãNy^$}„ÉÄs€z8õëZTñwü -ÿ¡wSÿÀv£þÿл©ÿà;WÚ5•©ø‹FÒ®–©ek1¶M2«`÷Á4òü -ÿ¡wSÿÀv£þÿл©ÿà;WÙ–·ÝÀ“[J“Bã*èÁÄT´ñwü -ÿ¡wSÿÀv£þÿл©ÿà;WÚ4PÅßð€x·þ…ÝOÿÚø@<[ÿBî§ÿ€í_hÔwv÷°™m'Žxƒßdw€>5ÿ„Å¿ô.êøÔÂâßúu?üjûFªjZž—lnu¨mmÁ æLáW' É Žáñoý ºŸþµð€x·þ…ÝOÿÚ¾ÁÒµ3Vt»û[°Ÿ{É”>>¸­ ø»þÿл©ÿà;Qÿ‹è]Ôÿð«í‚ÊîÞúÜOg.ÿ„Å¿ô.êøÔÂâßúu?üjûFŠø»þÿл©ÿà;W¹üÒï´ ½¦«i=¥ÏÚü¹«m `àý zõb^È^O÷úЄL*: ˜S´QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEb^ÿ¬‹þº§þ„+jäo(# ¡ãð¬[ßõ‘×TÿÐ…nK–'²Ô©ÁÁæ“Øhà<oª6“¡¹Ò´°ˆ†n “>9Áf3øÕ%¹¹»Ñ´Y/'yäÿ„”3c!D®ãØW èºd>•m§Ù™ ½ºìO1·63ÜÖ[xCL:OövnD"äÝ«‰Ht¶ì†íÉ4ï­Åm,rZ¶¥¯j:ž´úoöÆl§k{xì„&"T™70cœúcèºd“ͧ[IwŠå£S"álr+RðvŸ{¨5è¸Ô-gp<ÓkrщH€êqÞº8Ô$j€’’ritêóµ½»¶ñMµ¦…c{—R¨–YT2’£¢•íþð¯N®RëÀÚ}ÅÍÄ¿mÕ¢[‰ ²E Û"3¼ @a B ŸxrËH½×ZWµ ¢Æ4È£i28^AïL]Õo«²óÜÝÝDûáû)‚2<ç5ÒÛxVÎ-bk›ùþÏp.Qç¸.ÛñŒ{{SÇ…tÁ«É>Ï©?™<;ÎÀøå” =xïHe»ËÅñúÙ­Ì‹iý$þW2 õÁ®].õ7Âúw‰ŽµwrÒKÍg>Ó«¸R Œæ» 7–ºeÌ—p\ß\Ýý™­£k©Ë…CÎâføwÁAg§>®óËsmóùåšùÈ!N3ŽÔí)$E’6G«=Å:ŠyWq¦xKUð¬9¿lþÏ´ÿ®S†ú-ùT—Òêo¿Ó4Q¬Cm¤Çµ¼zx‡im€‚ûØøWu?‡ì'ñ Ôˆÿm… k†ù~¤zòy÷ªzÏ„4ýRý¯L×Ö—.¡d{;†‹Ì¦ìu FcYÖ´û+½B÷J—û2;™ã·*”½QÖnnn4OÇqs+¬z¼pGœf5ýßOÄæ»˜4KX5XõiÊ[ PZBA@sÏ©÷ªsøSNšßP…ÍÆËë¡y.% ùƒÇ ùG_ˆ_—r/<;®hòM©ÞÝi—$3‰ØŒT=8Sùր澻ÐÚ÷QšIêy&Œ?ðF[£ÛÆ¢ñŽ›ªë.Ö ?ìÉ•D×ÈÞb`óµ@äãæº;XÚÚ("cB(€PµÁëS5¿Žo]4fÕû62è¾_Ê·PÇŸÃ=+¼¬ôÒm“\—V_µK·`_äÚG¼Ð3Ïíî®´E>–U¯¨ªÄ¶…[ìé!ä.ì.îçN–ûÄ:n‡â9$þÖKxlĶ÷â?1dè@(H#½ußðˆé_`¿²1Êmo&ûCGæåÉœîOîœóÇzÎÔ|"ÐøcW±Òîï.no!òÓí÷Lê×µŠÊÚ‡¼G¢C&­u¨ÚjŒñ¼W;I‚n ¤ÇlVjzÔ¾  buq#H4ü/’JSgLç®z×m¤øVÊÃPŽý¤»¸»Hö!¸¤X¸çh=>µ]¼ £µù¸ÅØ„Ëç›A;}œ¾sŸ/§^})õاßjþ3Ô#µí¥µœvÒ¤• Û$6Gµ7áÍ“Keý¢—wqÂnn—ì{‡•Ÿ5†qŽ½ë§µÒm­µ‹ÝJ/3í7h‰ -•ÂnµGNð½–«=õ¤×©½ÞCoçŸ$3}â§½ 7«”ø“‘ @ËÚo­ˆ‹¸ù«Ç<~uÕÖ~·¤Ûk6ÑAve É:ùo´îC‘øfœ†’>×ãyoM‹J“OµušËæÍ¿I ÁQƒÎO5•¢ê~$¾m3U5™¾Ór¦XŠÃöU„¶mÀÏLñ^ƒu¢Z\k–š³y‰yoDѺ«ã½fZx3N³ÔÖòÒãQVO4[%Ówg?s¦=ºP„Ì}2æþßÅÞ^¿{«[I-Ì‚Ú0ŠlçNv¨`9ÁÁȬm[¯ü5¸×­®®LÇ2¥£aVóŠîÆ;kµ·ð•”Zª_Éu¨\s–ÂtÑÐf*ÿjxw\ÐR}fçS‡TÃ,wp‡amÉÀã¥TÓï5+ ê^$›T½¸–¹Eµb¾WË#"œc¶zétoéºUò]Ç%åÄÑ)X~ÕpÒˆAì ô«ºvƒca¤M¦Æ-¤Ï#ºJÛ³½‹0údšŽKÃSx‚=sO2&µqe:7ÚžôC塯U“c9ãèuÏh~³Ñoö·z“€¥V®™ãP}×CCsßojá€#È=kŸø“gkcá(µ;+xaÕ­æ·kYc@®\È ®G' ž+´ÖtØ5}.âÂìÈ v¿–Û[Ʊ´ÿé–—ðÞK-õôÐÑ}²á¥XϪ¯@}è2Þáߘ sê9¬«ïé×Zœ—ÑϨZK+•mnZ4‘½JŽ3Bþ¿g?yusk©xÞêÐ]GclàŽªÛ¤I5=Qð쯫Ýjú¬¢ `¹Úv…ƒ)tÅnë¾K…Õ.ì7 FîÓìÄ<¤Fø)aê=iº/„¬ìnm/g{©ï ˆ*,× "Dq‚Pç@ŽšŠ( aEPX—Ÿò“ýÅþµ·X—Ÿò“ýÅþ´¡AS †‚¦Ä-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@wÃ/ÿ¦‰ÿ¡ ×û?ô×þÿ?øÖMïúÈ¿ëªèB·é1•¾Åý5ÿ¿Ïþ4}ŠúkÿŸüjÍ6GXãg‘‚¢‚ÌO@zƒìPÿÓ_ûüÿãGØ¡ÿ¦¿÷ùÿƸoƺ²i‡ZÓ᳃HYLp,áäžüÿ‰S¦ì:ñÏ­wzEÅÅÖ—i=í¿Ùne^H7gËb9 ýŠúkÿŸühû?ô×þÿ?øÕš(·Ø¡ÿ¦¿÷ùÿƱCÿMïóÿ-õÊÚZÉ3ã 2©í\EçÄx">]½Œ³HŒRFÎÕÜ:ãÔ{×-|m ;Q«+6mJ…JÎÐW;o±CÿMïóÿb‡þšÿßçÿç|?ã[Vaѽ¬íÐIП­uu­ôëÇš›º&­)Ò|³Veo±CÿMïóÿb‡þšÿßçÿ³EjfVû?ô×þÿ?øÑö(é¯ýþñª~ ÖaÑ­–i°W%˜w :Ÿ­r2|J‹Î6s ÁË6µqÖÇáèÏ’¤¬ÍéaªÖÖ¹Ý}ŠúkÿŸühû?ô×þÿ?øÖw‡¼Ge®F~ÎJL>ôOÃñ­ªé§R5#Íte8J–JÌ­ö(é¯ýþñ£ìPÿÓ_ûüÿãVi²H‘FÒJʈ£,Ìpõ&¬’±CÿMïóÿb‡þšÿßçÿò câ~³â é­<koŒ/±õ[ï¸ÄuØûóT~ÑñصÄ>4²º”ò-ç²E>™Öº#†©%tŽyb©EÙ³Û~Åý5ÿ¿Ïþ4}ŠúkÿŸükμ ñ2[ýf?ø²ÉtÍi×÷2#n‚ëÎÖì}«Ó«AÁÚHÚŒ×4^…o±CÿMïóÿb‡þšÿßçÿ³Y^'×ôï è·¦¯8†ÒÉêÌOEQÜŸJ’‹Ÿb‡þšÿßçÿ>Åý5ÿ¿Ïþ5â¯ãOx©¼í+ìžÒÜ|p‚[†¸<Êšº·Ä]lÐ뺈QN^ ›u˜z+(~µÐ°•Z½Žg‹¤®{gØ¡ÿ¦¿÷ùÿƱCÿMïóÿrÿüwcã+YÕ!’ËT´;n¬¦ûñžpGªŸZìkšvgBjJè­ö(é¯ýþñ£ìPÿÓ_ûüÿãVkÇ|qñXÓ<~tý ô»»Kj°’QÄÌÄðq‘Àô4†zÏØ¡ÿ¦¿÷ùÿƱCÿMïóÿqÞø—¥j÷«§ê÷Z6¦Ww‘~žX>¡Xðß…vðËÈD="€!û?ô×þÿ?øÑö(é¯ýþñ«4Po±CÿMïóÿb‡þšÿßçÿ³EVû?ô×þÿ?øÑö(é¯ýþñ£R¼ŽÂÆk©¾äk¸_jä|+â}GVñÚÎ}ŸËw*ˆCG‚1Éëœú ÊU¡(7«-S”¢ä¶G]ö(é¯ýþñ£ìPÿÓ_ûüÿãVh­H+}ŠúkÿŸühû?ô×þÿ?øÓ¯®à°³žîòT†Ú2I#œP2I5óÿ‰>2kzÔÒ/„!OÓ‡ yué$÷xëYV¯NŒyª;#¯‚¯Ÿ³¡³ß¾Åý5ÿ¿Ïþ4}ŠúkÿŸükåµñÿŽ­¤'ŠZá‡Xæ¶„¡ÿ¾TÖ½?áÅä×µ´_Û¥–«&D2¡ýÔøôÏFö¬hch×vƒÔìÇdxÌ yêÇNëSÕ~Åý5ÿ¿Ïþ4}ŠúkÿŸüjÍÖyo±CÿMïóÿb‡þšÿßçÿ³EVû?ô×þÿ?øÑö(é¯ýþñ«4Po±CÿMïóÿb‡þšÿßçÿ³EVû?ô×þÿ?øÑö(é¯ýþñ«4Po±CÿMïóÿb‡þšÿßçÿ³EVû?ô×þÿ?øÑö(é¯ýþñ«4Po±CÿMïóÿeÏǪ:®pz±'¿s[µ‰yÿ!y?Ü_ë@t0¨`è*aLBÑEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE‰{þ²/úêŸú­úÀ½ÿYýuOýVý&0¬Ojz~—áëÇÕg0Á,mÚ ;R0 rMMâ-rÏA°77¬I'lQ'/+vUÍyF¿¬ËÊëòùú¼¿»±Ó¢ùÖÜŠ£»ž2ßÈ+‹ŒŽ$•äöFÔhº²7¢ÓÅk£®©§ù7¶ÆT ¼¢‘Ê6 eØÍ¨\|PКóR7ºÊ™|øí·¥­½¯”Ù736ÖÎJŽƒ{ m/4ëÛ¿ÙÇ=þ¢Áü•vSf|´u ŽI'šíü+á=¶ÒA¡Y-²ÈAv.Ò;ãÕ˜’:ëƒn)ÉY™5g¡»EUàþ*-„QE3‡l«FƒŽ¼{ñ\F•ÕÌb' #7.ÿuEt_-¦²·MR ï/yTkr:O¨Íp~xî,Ì‘]0‘_æ@q´ö¯ŒÍé·Œç’Ñ[î>«,¢þ¤êEõ±¹©£i7Ïk"ùÓ!ÊÉ Gc^¯àûùu?Ú\Ü LAWà‘ý+Ì ®îÕ3\9Ç'&½cC±þÎÒàµÈ,€äRrwdp—µœâ­‹5’öqŒä_¢Š+éOòˆ²ì×üõ™ä†>p€Xmäw~b«éºKêaä·š8‚¦ìÊ3½½büTŽ]Y6ÆûÏ‚A¸.0Ñ‚wm>½sV´}«k¶×ŽèÉ€Á±‘_ Š¦ãŒœê+êô>ÃBQÁSœ]®Ic«K§_Ç-¤ çDûX“î+Ü”†PáäÞÒN§z±BŒÈç ë#qҽ슜ãNMü-éúž6o8ÊqKu¸µæ´­=‚c°´˜Ã6«p¶…Á ì9-‚: ôúòÿÚ K{¿Új1ÄÒÿe]­Ëªž‰‚¬s_CK—ÚG›kž!ÉR“†övõ(|Ò4Ûø®l®mK{h“ËN@^NzVï‡4½2ûÇÞ*Ò§±­lÜ»›êIïíYï-lä¾–âæ(ã–4ØÎØ ÏjÞðÅÅ¥¯Ä__ÍuÛ]­°‰ËŒ6Ô ×©ŒSUf¡{Y~‡Ïe¸ª3ÃÒu»o{yžYñ–Î8µ+ØtñöildY­]IÌL ƒÖ½ÏÁZ¹×¼%¤ê¬TµÝ²HÅzn#œ~9¯øÁqk:ÄN%aA|Ç`êM{_€ôƒ ø3FÒÜ{[d9Ãc‘ùÖY‚Š>ö×úûΜ–´ªÎ¿ò©Yvþ¶7«À¾9j ©øûJÑdpltøEä󇑉 Hè@½ö¼ãF˜l~"Xêou+al%ÏHÉ!Oå\øˆ6Çvk9à RT÷Kþð:†ú}¯ˆ¥Ô ¿RÁcN¨4ÿx?WÒ7KfÍ}h9ÊG¸ïøQð¦òÛI“P¸¾™bË^½O=‡z·âŸßÝ«Á£!µ€ðfa—aì;W«8VúËT×»¦û1O1Á¬gˆ•¦ïko¿õ¹å¶zœš'ÄM Y…¼––u²»#ÌG;@#¹Í}?_0A¦É®ø×CÓv¼Ò½ÚÝÊsÊ$gqcøàWÓõÁš(ªömOw ­:ØE9mwoOø{…p_üá-wKÔu kK¶[´…ä7±.PBðŇ's]í#(u*À2ž# ל{GÌ‹¡kzo†tgÒ5%ñHž%{.â!S^­9rÔWÖ×}-ýz‡> j7:ö¥o­Èözu¡ÄwJ.\¯ õÍy7‰av©p–7^kYÎLq’§å` úz_xkâ וí'´U“VV×_™ö‚µ6Ö¼!¢êrg}åœS¶}YþµµX^Ó_Gð^…¦ÍŸ2ÒÊ[>ª€ݯhüÙï QE(¢Š(¢Š(¢Š(¢Š(¢Š+óþBò¸¿Ö¶ëóþBò¸¿Ö€4 è*aPÁÐT˜…¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(÷ýd_õÕ?ô![õ{þ²/úêŸú®ƒkzC8‰:±ªeßølX¾¥a+Ÿ.ð²££)RBµUð‚&±¹þÜñAŽã_¨‡tV‹ýØóÔ‘Õ»ý+Ðv·¡£kzÉЦê{V½í®_<¹y/ ”RíoCFÖô5©QKµ½ [ÐÐñ ÂÏâ‹$ƒÌ’3nGOÔéÅbx'áéÑ­.m®±:LáË:`ñÀê[Ðѵ½ fèÁÏ­MÖ&ª¥ìT½ÛÞÞg+iá+[i7¢&õN tVy{7ÔÕ­èhÚÞ†­+hŒ[¾âS_;N:⟵½ [ÐÓå^?øy/‰u3x—Âåv° ¹O@=x­­ÁPÁ£ÚÙÝE˜`b¼‘ï]ÞÖô4moCY*4Ôœ’Õ›ËVtãJR÷c²9Ý?ÃpÙÜ3F3œ)À­è×b’~µ&Öô4moCZ˜ Q\Áͼ\F²C"”taÀõ¦ÚÞ†­èhĵ/‡Úß…n]ü5 zކϑc3„¸·¨G<:ŽpAÏZ¦.õkÜÛx ^¶BãHãÈõmÜz÷­èhÚÞ†ºáŽ­Ê™âbx{ˆ©íe7½¯ýySே7¬[ëÞ0’Ý®`ùí4Ûna·cüNÇï¸ìxž¼cÕiv·¡£kzç©RU%Í'vz¸|=<55J’´P•‰ã Xx¯D—LÔÕÄnC$±$‰ÇGCØŠÜÚÞ†­èj65i5fxTºм,Í þŽ|Gd‹û»Í81öh˜õ÷˜ñ.®~Ï£ø:öÎrÁLú©X¢Œâà’ßA^íµ½ [Ð×jÇ×J×<)ðÖ_:žÓ“å}#á×"𨸽¾ºþÐ×.ÀÝÚª£¢F¿Â¿'ŸaÛRíoCFÖô5Ç)9;½Ïn8ÓŠ„’Š]­èhÚÞ†‘f‹ì5MOD¸´Ñob²¸™ yΛ¶ƒÁÀõ¯žcýžµÛOi¥gqgë4ò0*~Bï“_QmoCFÖô4¸B¢r­&>b½3RRíoCFÖô4•‹«hÒ]Îg¶¹ò&+·p@sõ­½­èhÚÞ†³«JcÉQ] I§tq3økSóÉ–Ù”)V,ì ÷è/Öºk '¶†8̤…qZ[Ðѵ½ sáp0º1µÊIOâŠ]­èhÚÞ†»)êÚu®­¦ÜØj¬ö—c’6 |×ã?ƒ~!ѧy`õ£Ì´5˜=hó­MECæZ<Áë@QPùƒÖ0zÐÔT>`õ£Ì´5˜=hó­MECæZ<Áë@QPùƒÖ0zÐÔT>`õ£Ì´5˜=hó­MECæZ<Áë@QPùƒÖ0zÐÔT>`õ£Ì´5˜=hó­MECæZ<Áë@QPùƒÖ0zÐÔT>`õ£Ì´5˜=hó­MECæZ<Áë@QPùƒÖ0zÐÔT>`õ£Ì´5˜=iDƒÖ€%¢˜S ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ïýt?õÕ?ô![¦³¥é;µu++-À•ûLë@êFâ+ïýt?õÕ?ô!^oâ=,~4j:·‰<9w­h÷ZlpÚ4v¿iXܸmçi篽. zíö½£Ø[Á=þ­§ÛA8Ý “\¢,ƒÊ’pF9â™wâ-Ì@nõ6p¡áón‘|Å= äò¨¯ñ%•¥¯‹4WSð•õç†äУ·µ±KCpm$ŽÖi¨ÏøW3¨xgTo höwz5ÔóÿeêR[´¾F÷Ý“ƒµ°8GJ/ý}ÿä;_qôTž&Ðc¶72kzZÛ‰ FV»Œ(p2W9ÆqÎ)ñx‡Eš;™"ÕôçŽÙCÎËr„D§¡cŸ”M|ü<=*Ì–ŸØ3OíX˜Ä,›Ëɵ*[qýO­VÑ´Ýg@ð¶¤tÏ o›E¶Á%‹‘¼á¼‘‚œã¯u·õµÅÒÿÖö>‹¶ñuĶº¾4véæLñÜ£—Ÿ™ˆ<'Ò‹hÚÈ·Óõ}:êà®ñ(ìW×çõóLJô{¸ü_¬ÝÿgÝËk'†®a‘—Fû"I! |°Š>cÁòjÏ‚|3u¤ë {k•°º[ÙÒÕ”+²>ß1±ÁúúZûZÿ_—ùž¿âoˆVu¦Áo$‹]ß%„‚ÚáXÛÈÇxÇ9â»*ùCÃú‹Nðu”~Ô­µëiþñ¬Û™æ_ãÔ Wµx‚ÓN‹zÄÚF­5úEˆïbÿh†‡ã¯^ýÅ?ëðAý~'¢×%©øÒ+/§†–ÆY®ZÁõ"¸*î`÷%qø×[^)ñ×:ÇÆh&s¬ÛØ Óí:s{›=@XYÝ_ý¦3cö—C$¹š7Èâ» øeâøsãëm%5I>ÞåAud-s!“c 9ìÐïfÅìÏVÑ|Mg}á+~ý£ÒínbIOÚ¦Uî8PXàd’ãWtíkJÔæ–-7S±¼– ‰o:HÈL€N+ÃüCªÇû:j:]Η¨Û\XX[¤«{jÑ+0‘@Ú[©È϶EZøgd³üLÓït ]h¶6š(·ºy-¼”–RGB8~ß79ÅS·3KúÜI¾Tßõ·ùžé,‰O$¬©ÌÌpI¨lomu e¸°¹†æÝ²¸u8ëÈâzqg90ý£·îxýçwž9é^/¥ø ]½Ö£¥iöÞ C7™#ZÜ»Ë:ƒÂ´`ìNœÒê>‡·Veÿˆ4m:äÛê¾k8ŒsÜ¢0¡Á9ìkLt¯œþ(Á«ê¾+ñm­æ²Õ-Ñl¥ƒER^eYHùH9éÓšnÃG®ê^:²Ó¼oo ]Ç0Kcöÿí gT‰Ws sôçø«~}oJƒNŠþ}NÆ+p#¹{„¾za‰ÁÍ|æÖÛWð4ž"ð®§«ÙYxeb¸ŒX´¢9àR0Xc§QjM3A¾Óü7à[ÝoÃ÷ך­Åã\i¢ÜÊè$u1–‹ôcÏLûÓ]Ÿõ«&ý­¡_Ä4z|7ï«éËc;ùq\”ÈÜüªÙÁ<¡¨îüI¢ÚÇ|ÒêÚr›þ’­t€ÂNp'å'¯Ÿµ-þO‡z•Õž}‹øŽë[³Ÿ1 +UF1úWcðîÊ-sÅ¿§Ô?ÓlµíF{M9-%¶»kóB\åÀ”vè}­Pn¢¼vkû?Ä—šf—¯_ßÙÏá©o™ret™H ">~BAίµGáÍJâÏUøxm:¼²›W¾‚;.ö;ŒÉuç3Ñ´œ¬=Å…¿…‘c`a¶b}ðÜ>jÿ³œã;v§Ø]Í»«]%añ»Çaí<)¶_5ó_~Óã·ÐÖó@Ó§ÒØÉª[+™ÒvÜŒ}º~••â«»Û;Ûy-,í¨âXáÚòsšÐ½ñ2YG¥ÿcý–g‚ÐDÏ5±2Dßì±?â) ¥áëU´ñ͵«3²ErS)#FÄ ÿGáS\¶•ec«ë”·˜Ô¥µŽ#;| 1ä¶rOÔÕo ]iðê§QÕï'Žh\JŠ‘ïóO9ö¦AâK‹Yï„[Ëis;Oä\FCœýhè¿®À÷: KKM;]Ôã¶þÅ6n¼§l0 W?Ö°u ]:ç–¾›hö{äh^&È;‚yíMÅ˪Üj¥´óO’Ë$y@ŸÝÅVÕõ»N ‘ ‚Úõp[¦Ä_† s.Š( Š( Š( Š(  †[‰pDò¹çj)cù }ŕղ¹¶ž'ÉPOãP«2«> â•äwwfç4Ú(¢€4ôï"-2òæ[X®%‰J[0r~éÝš …ì¬n!¶ŠÝ¥¹c,AÃ`}âj­õÕ–ÿ²\KünòÜ®qÓ8úÒ^^ÜÞ²µÝijS#Àüh (¢€:¿ h¶Z唑^ ˜¤ /™¾Y3äàœn=ˆîy¬-qQ5[„ŠÉìQQnìK.“ÜõôçŽ*±¹œÚ­±–CnŽ]cÜv†=N=x]\Íw1–æWšR—v$àϰPUö‡†OüIìë‚è"¾/¯³ü1ÿ {úàŸú¡:úT•}*Jb (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€1nÿ×Cÿ]SÿBÓ×1wþºúêŸú®ž€3umKÕÚ6Õ4ûk¶ŒS4aˆ¨ËèöÖöP[é–‘Ãe!šÙ .I%”v9'ó®7ãWö¸Ð­E…õ­µœ—–ÑÊ) ’d–Yåädc'Ö¬k÷ºŸ€üYE§O|·+UIæKŽrÃïx÷úRº»DÞÑ&Šþ9t«'ŽýƒÝ+D™‡Bù¬ÝwÁz}ï…o´M) Òb»*]à„s†ä31×½bx§Å^!𞑺ÊhÏs}u¥£Â%Xaf Y¦Üs´míÖ³døŸw§i$¸Ôm¬¯K[s Æž[ÉœÌÛùŽFÖûÜô¦#´o ii¦“km§\^Û4s 0È#v8Î Î3G…¼!¤øzÚÓìÖV¦úV»XB¼˜zžqÏ5Ïø;ÆšŽ£â„Ñõ#¤Ý¬¶†å.ôÂûÆÁ‰ç#•èTö°UDÓl£ÔäÔRÖ¿’1ÜÙÈR}+€ðÿumKĆÊê} Éô–æÊáfŽèÆ£‚„®Ç¯ïTõˆ:òZx‹[²²ÓAÑoÚÆH¥2 ™ 0W ”d²íϯ&’ÖÃz—{¦Ù_Ko%å¬3Énþd-"‚coQèj®·áÝ]h›YÓ-/Z DfxÃíÆ~ƒò®â„ZÍþ·ákx/¬bÓ¯/6¬2A)Ü|¢q!YpÏ@Ǿ)þ8ƒZÓÁvvW–*^AE†\3…lŸõœ¦Ý'>ô/Ô<üEÓ4ë=*Í-4Ûh­m“%b‰B¨Ï^[®Wñß…µˆàñCÙ5™Óe¼óm!uy$Ž@UK2žä“Ú³µoˆ±bâÒM& : ­!³šåev™äMò&ÄÉ.0¯=(Óh®Á¾6¼Öo$ž l¡]B­Èq½¤Ï*W=1íN¹­EãÚ²Øk~1ñ¤Þ*›Rm3E[x`ŠÖIWËGs±Xù'<çÐJW ÃEx.¥®GeñC¾Ñ®îçÒtíÖ䬎ÄËo$Ï;É`6œžjOO?ü$~†K¹%ë"B$$9Y#Æ}qU}=e+jv¢¸…’I&¡ãO2Gpšì軘ªð=YøÇª]é5;>W†à˜¢'CȪØ=Ž ö$R–Ÿ‡â8¾cµ¢¼¾Ôø[Á:¾¦lßX“U±Ó%¸’ $™Öxn%w|¥²0õ¦xWâ©©jºWk¤\[jÁŽË6Ïå.¾fâAé´ã½sÔè¯+ÓüqâQ¦izÆ£ŽÚe椺yŠ”JHc¶>÷ltïÏu_Š:ŒwZôÚ|ZWÙt›¶´û Ã8»¹(ØvL áÈíEú õê+†ñ‰u´ñ/‡ô­?¥¼“´—Ï–ü$zôþUSÄ*×t{Ý7NžãDþÒš %–mn®ˆc´ª'!p%^™¥p±è”W“jz¬8øL¾#¸µk{ØDˆ›—Ëq&ÆÇC‚W½z˜ÛgÀG'=©5É|Xÿ’u®î…`Þ«çµq(6 —UðÕž«m£K%Á½YV era_©àäŽÆ•÷ mý[žÉEp±ø»P?ðƒ‚×þ'Äý£¿wû“ ÙϨs\‡…|I¯øwÃeÓAeu¡Ëª›¹ÜÝyYD…‰ÛÇ÷qÐuç‡×”:)ÓEp°i¶Ö?c{ExþÑc,ò(v*ÎdlgÍ7â ÷pø—ÂjE»ÙM¨**æD‘_cÙV—îi¥{yÿ„ݯåÿw”W#âÝsZƒ_ÒôO [Ø=íÔoq,׿¸¢Báy$“þq¿¡PéÉýº¶K–Þ,ÙŒxÏÜéŒÒ~Šá>3î VI$–öÛ ŽTó §±5Û] ´¢Õ£[§Ë2)e Û Hüi\v%¢¸?…÷ÏÿÞ±uv‘«ÃuæyO#+c’7³:gŸàÝ_ÅúäVZ­å®‡o£]“ ‰^SqD§?u›§§ð¦ô$îh¯Ò¾*ê:‚Yj1A¥¶Ÿwz¶ÃO âö(ËìÞüíÈûÄzÐÕ|iâ[Kjpã6•¢]˜J2KæÌƒ¹Ý€ÀB ôÇ'Ÿõýj?#Ôh¯8ñgµM;Ä ehúFŸhÖé.uD”¤®Hw¡@AKâ^XxƒMÑ,¥Ò ¸šÄ_ÍyzÎmØT${H%² çµѨ®oáÿˆfñ7†â¿º¶[k#Å"!%IV#rçøOQõ¤ñî»yáÍ"ßRµ†m£º‰/‚YafÚY#æÉ^¾¤ö –Šæ\ÿ…1ã_úAÿQÿð¦Mÿ…1ã_úAÿQÿð¦Mÿ…1ã_úAÿQÿð¦Yÿ…1ã_úAÿQÿð¦Zÿ…1ã_úAÿQÿð¦Zÿ…1ã_úAÿQÿð¦Sÿ…1ã_úAÿQÿð¦Mÿ…1ã_úAÿQÿð¦•%GJ’˜‚Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  [¿õÐÿ×TÿÐ…tõÌ]ÿ®‡þº§þ„+§  ohxNŽÎêia'Žà4XÉ(Á€ätÈ£Åz$ÓÊêi¡fŽ}Ñc$£‘Ó"¶h¥d;˜ž-ðå·‰´èínešÞHf[ˆ. lI ‹Ñ—z´ÌߨãE;ˆ9„1lž>öMQµøaac¢éZ~©¨ÚÜérË%½ìl¾hºœŒp3Çjô ( >ðů†-.ã¶žâækˆº¸žá²òÈØœqЕwÄ:5Ÿˆ4k­3QBö· `à‚!ìAƒê+FŠãl<w­qªj·ú¶lžÀ%Ù\yO÷†TI§h “F¸°1x‡VšÊËrÇi!M…pB©!rBŽœöØQ@rx É|7§èÆöí ²½KäíÜ̲o xÆ3Q]x~Ý©O¥kº¦—¡/Ú&†Ø¦ß0ýç”[½vÔQmn1n|=lj4Íeç›Ï°†HQ8ÚÁÀž3ž*ž·á5ÔuõÖ-µ;Ý>óì†ÉÚßi mßÄ=Çô®šŠV œ–•àk;Áxan®å´”ÊLÎÃÌܹ9[Z›>•eö{JçP ü²\Ü£cå=+NŠ`ex£FÄ:æ•<òÁÊ„i"Æà2vÇãVÅ”m¦}†bÒÄaòX·Vpsõ«TP§ü>†ÓPÐndÖu+”ÑK}–Jl©\.NuöZËá…¼Vvóë¥Õµà¾²ºìiCnàŒóÅz y‡‘ÎëþþÔÕ­u;mNóN¼·¢o´†RA †vª¾#ð„Úæ¡is&»}ÚL³ÁQŵ.3Ê’s’yõ®²Šåõ¿®¬úeËj·öú‚²%ì ªì Á†6p;VΉa&™§%´××WžåvÉÏ8§J¿EaxËÑx£FþΞîâÕ<ԛ̃²§#¨#®?*«cá½JÕn·x§U¦P¥X”w•qÓ#ñ®žŠå¼-áðû](Õï/-îd’ia™# ]ÎXüªáPø{Á?Ø3Û -wV:}»1ŽÅäS?)ã$ ú×_EpúÃõÒäHô½{U´Ó£¹ûBYFÈcQÆ0JîÚNN3Þ­ÝøÎçFñ›%åß“­ÎóÌÀ®c-Œ…ã§®k­¢‹i`¾·9-wÁ²jæhß^ÔౚÝ-ÞÒ2†Þ Œz½EPØÂðw†­¼-£› Yç¸Ý#JóNAwcÜ)t¿?LÔm#Ôï¤k¹$O#‚ð—þç»VåÅéÞHu» KTÖõ]^KÍl—Ž¥br0XaAÎ ÙšZC@®º«¤ôºÿ®ßû*Õ«®†ªé=.¿ë·þÊ´~Šãõe<;k¬xŸYŠêâýØB±y7n›FÏc@\諾•åµü}”ñ\C’»ã`à àŒC^ˆotŸx¦ÒiÞêïL˜ZYÊÿ~V‘G–©‡åUot½;KÔ4}T—Q¶Úf1b&$Ènb#òrr}hÔ¨¯)[§‡šÉ}Bá­a¾ <ÇÏŠëÑóŒðyÅ_ÐMœ~+²O ß^ÞZ•o·+Jòƒ)ÜÝzhÏG¢¼ëAðÄ:½­ýÛßêpÞ­õÊG,wní…ùsŒLVïµ¹µ/Ǩ6é¡2Ç+÷¼¶*[ñÛštwÃnî%H°P]° =K^e©jº¶½¡iÚ…Í„ZEÅý³Â³LÍIc'ÓÞ®øƒÇSYêZŒ6RhñÅ`vÈ—·%%™€„QìzóÍz7zïÙ¸nÆqžk’³ñqyµ'ºŠ8í!ÓÓQ²wšçQÓVåôv¶Ô"Cms¾âŒ®ñÓØúçq«º|¸8ÎàH§,h¨ŠT`:V^±ÿ ù~«ÿ¡ Ö¡*I©YG©G§=Ü *ܸóFrÁzãƒÍ>KÛXïa´’â%º™KG pÀê@êq^K¾–ú׌¼s$Bâ;FFÓÔ°á†ãž›K²œõ0«P\kÃâ^‡wâÛM*ÝâÓ®¤ì.îpî XÓÔÓ_×Ýq3Ö(¯Ó>/Üß\éWKŒÚ^£v–ñÀ—DÞÄŽp²:tO·Ö¶Ä]E<2Ó˧Aý¹ý°4tµù‚/µ\õ*¤søŠô9õ+(/౞îï.h gä©©«uãš¾·4^&ñw‹`¶[ÅðýºéöˆIÚdb •Î@Îë•ǧuã~ÛÂSjêžÔî¡¶µ6O$d»a·žÄp=þ”-@õ +É5Æ×õÏxsþ'–;îþÑrÿØ÷.õWh.pG%³jè>½Å«øC¸¹šê=+P1[É3a (eRÇ’G<ŸZ ô;º(¢€ (¢€ (¢€ (¢€ ÎÕ?ãïMÿ®Íÿ¢Þ´k;Tÿ½7þ»7þ‹z(¢¸Í[ÅÚŽ›©½ƒè $ò1nÇúHHùxþ”†vuBe «Û2Œ#pØïŒcùÕ›7šKXžæì ¼a÷„>™ÀÍAsÿ![/÷$ÿÙhíQ@Q@Q@Q@Q@Q@súoÜüO󮂹ý7î~'ùРإIQÇÒ¤¦ ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ïýt?õÕ?ô!]=së¡ÿ®©ÿ¡ é袹xÍ´ßÅ ézEÖ­©›oµË.¨"ˆ±PÄ·$ŠÙ¶Ö¢M ýmSF/ÃÇy2/–ÙÆ g€5¨ª-¬i‰`/ŸQ³["v‹ƒ:ˆÉÎ1»8ëO³Ô¬omšâÎòÚâÝ~ô±J®£êAÅ[¢³lõíúá-ìµm>âw¬q\£³c®9íK¹¤Ü^=¥¾§c5â­wÒ: ç4£Es~ñ†›¯YÛ¹š KÙ˲–t3pHû¹ÏðžÕb_[¯Š-ôXDSÊñ³ÊÉqèHÆGÜçÒ€7(¬ËßhÖ o}«éÖÓ¯9®Qqž„ç§5=橧؈íõ­¸—ý_2¦ÿ¦O=håݵì{+ˆn!$$NIdqÁTÖ­e¢ØIy©]AmóK" c‚v‚O'ƒÅ^¢¹xæÏÅ–SÞ@- µŽ$›wÛc‘•[wßU?&6÷þ•Ñiº¶ª#6™ix«Ëy–@>¸&€.ÑYiâK•¶M_Nk†,D.P±lãn3œçµjPEyýÇÄqjon.tKÅÒ,ï^ÊâùdR± eë·‘ÈÍkÙøÆÞ{Ï[Ém$M¡ªÈçp>r4{Ã/áΕô¸íÐêh¯<´ø‘5ø³o‡o.¥¹ÓWTòÒUDÎUz€ zë|7¯Yëúµ›·6á'6RC+zAéLFµÀÜüETÑìµmæxïµ?ìÛUóUL§k&OE%ýjCâjiº^·>¡¢^[ÞéR[$¶†EbÂy†Åš,&ìz%Ä/ ½Þ¥g«h÷7––/¨GJ®'‰>öÒ:sÏ­?CñнÕ4Ë-KHºÓN§›e,Ž®“|»¶äro8#±¢ã;J(®kÆ>3Ò¼1¥ÞÜÜ\A=Õ²6I:‰›8^½Çjéh¬ë­oL²òPÔ,ìä™7¤sΨÌ=<õ«+{j÷e[˜ëËó|‘ /³8Ý·®3Þ€,QXšÏ‰tí;D¸Ô£º´¹H÷**ÝF‚Yü³ ÇnïlÖ…½ôo¥Ç}pRÞ&ˆJåÝqÆN[8ã×¥[¢¨Xk:f¡’Øj6WQÆ7;Á:¸QêH< ];WÓu0çMÔ,ï ·ÙæY0=ðM^¢³lµÝ"þäÛXê¶79Š„wàðx­*(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š) -!  W] UÒz]×oý•jÕ×CUtž—_õÛÿeZÈñ«Ýkvz–{io$I[ˆKƒ¼©ÏtTžðüöZÆ««Þ‹íVxÄ&EbG9Ú«Øgšè¨¤3•Òü*l¼U>¦×öƒ{Û[„Ç’òc{gß©¨¯4OÇ­êº>§aoáBDÖåÙvŒqÈ×Ñ@zž’i6 .n—ÎmRþõãÂK*Œ"ç Á9ÐVÞ¹£ë2ëÉ©h·Övì-ü‚·—þ,ä`Šéè $ø>ùô­QgÔ¢“UÔgŠi§òqòñ€«ŸAë[š-ÁñŽ­at°2'“w)"xû}= oQ@Xâ ðï‰mVêÚÏZ²·³žâY‹-©iT;@$ã<úWG£è¶ºV‡•cn¨Q‹[9ÜO¹$šÔ¢€88|!¬Áek¥G«Z¶kÛxnêf†æH–i& ÊÎ[Ì-´ÿµÚaá/ÏâkKÄ:͕彭¬ÖÞT¦6a ’r}+®ûmçüø/ýÿáGÛo?çÁïøÿ o{ŠÚXá¼5à=kÃóXÙÚjº\º œ£Ê‚}<<â ÙÛæg¯¾)ºß‚ŸNñ­ïŒ¢žk‹Xb{¤Ò♤ºìVÎxÈÆ;滿¶ÞÏ‚ÿßñþ}¶óþ|þÿð¤õÉx@Ö4߆°[[\C»yºîy® ,¾d¹·'à㵎¿ oÚÏP2ê:|w——V×ìÖžT+åýÀzŸZô_¶ÞÏ‚ÿßñþ}¶óþ|þÿð§{;¡[K~1ðÅþ«¥êš&¤–µ†ôY%‹ÌÑÀÜ¥r=ZðG‡$ðí÷ÚîþÙv÷—3Ú¥Û ;â´¾Ûyÿ> ÿÇøQöÛÏùð_ûþ?‹…+;í·Ÿóà¿÷ü…m¼ÿŸÿ¿ãü(FŠÎûmçüø/ýÿáGÛo?çÁïøÿ Ñ¢³¾Ûyÿ> ÿÇøQöÛÏùð_ûþ?€4h¬ï¶ÞÏ‚ÿßñþ}¶óþ|þÿð  ÎÕ?ãïMÿ®Íÿ¢Þ¶ÞÏ‚ÿßñþVîKéæµqd C!r<áÎU—ÓÞÍ*âäŽGH󱊂W<Õ[í¿óä¿÷ø…h½ÿŸ%ÿ¿Ãü(íR¹ÿ­—û’ì´}¢÷þ|—þÿð¤†)å»[‹•XÄjU[w\d“øPê(¢€ (¢€ (¢€ (¢€ (¢€ (¢€ çôß¹øŸç]súoÜüOó ±J’£¥ILAEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP-ßúèëªèBºzæ.ÿ×Cÿ]SÿBÓКüG‹C}~½·ñ%®ª¶À&¡¢¤ŒeîË.AÁÁnã©®GPƒÄ7ZWµmr;¹ã¶’én{3r@sˆ^HAˆP>nÙÍzîµâ½Dºû6«©Ck6Á!WÏ œdñÀÍX^Òä¾³³Žú¸¼ˆÏnŠÙóPrYOB)ãrZÜ[xVa¤[Í%­ïˆU®Þ]cBœÉ ¸`0\w?ZÖø{m<~&ø€Êš”–3ÛÀÐÉuj!ó›lŠªªƒÈzf½óÅÚ ›Ü-Χ 5¼Ëo(Á;d œ¸Š»¢ëZn¹j×:Eì7p«f‰³µ‡cèhjé ZoÃoCkðÛLhôØmõ²ÈD’@e‘·I#9?ʳ>Î×GÓ¥ðµÅ®»u{‰4îRM­¹üìÈÎ{â½ûÄU…ÅÌwÐÅ-µ»]̤œÇõsè*ž•ã/j׋k§ê¶ó\²—XÁ!™@É Í?0g–Úè^_ü5wo£É«ÿ 2Í ¶+0S?ÌÌq»qžØ®‹\Õ,£ø«¤ß­­á†ÖÚâÞâT±€ä®9 ÏCÍušWŽ|3ªÜÚÁ§ëÓKt3Œ3èHÁ©Œü:u/°ZÙnüÓ‰\È äŒf…£þ½꿯S¼Ò¡¾ñ/g¸Òüã.ŸC#ÛgÌýÑ$)#“¿ô®wJƒìÞ¾ñ>“w{e'†"´U{Fœ¥ÇV ¸; SŒœg8=+Ü«_ðΕ®Ëº¤S»ÅÄ­Ô°üÀìaœàu©kúûÿÌiÿ_wùïÀõUøm¦ùqùHd•6íÚ Î@ÅuÚäK6x™˜_ ·';OOz‡Ã2iO¢[¯‡Ú&Ób¼¬í R9ç‚­JrWM i¹ã÷º6¥uû=Eiai$Z£ZÁæD#Ù#*J¥”Œdü€ŒwéÞ®xB+k߉jz>›£Ç£ù‡³6Á¤. ¸ˆQl{תU-/T²Õ¡–]:æ;ˆâ‘¡vC÷]NO¸4úÜcˆøU Z&©ÜßiQ-Ûj÷¬—àI€ÿ#Fp;ʽŠ)%ea·wsÃ5©.¯<'â¿ [iz‹êZž¯7E³™ƒ³‘€¼j×Å]Rƒ\‡û*ÒêoíÍ=4«™`ˆ Ȥ9Ç r}{MÖÉ[XOvÿ®çšj±øWÇÂg°¾–Î= -ãû%«H ¬‡åF…§G©hŸ ôo *½¾·®ÜÊ‘¦pðÂî]Øã¡F} ¯\Õµ;-"Í®õ+˜í­”…2Hp2Nú“F›}gªÚÇydë4$²¬›Hä£=E-ýopÒ÷þ¶±ç?´xl¼9àû¶°²Õàó’¾øâX¥æ_¨Ç&¸oés^è4ÃÖ:Áµº“Ndžòá¥)œ3~ñ•@Ï'ó¯¢è§q4xε£êº6»¯ý¨_k«©hSÇk}*fHTþãÝrdœÕ6Kx‡À)g§ßÄš$ %ä×4H¥¡ µK“¸:õÚ§w©ÙÙÞYÚ\Ü$w7ŒÉmÖBH€&’Óúõÿ1µ}?®Ÿä\¯Ÿ¼Qd¶¾ñ½î‡wu®]jò\ZH–m+Ë 0xwãv+è)XwÁ ÝÊÚµ·Ú¬Ì/ȈTº€@Ïž•êSÉÔŽÿhDÌqíìDZ4kšÏöUÆØ®®~Ù8‡t)¸EÆw7 ­j(+Äš¿ö&›ö¿±ÜÞ~ñ#ò­Ós|ÇÇ ëZˆÛ”6ÈÎ -QEQEQEQEQEQERZC@®º«¤ôºÿ®ßû*Õ«®†©iÎ"†öFû«!cô´¥EgÄoî#YD°B®2Ä\îw •¨ÏÝ¿þŸþ.Ë´U/+PÿŸ»ü?ü]V¡ÿ?vÿøøº»ERòµùû·ÿÀsÿÅÑåjó÷oÿ€çÿ‹  ´U/+PÿŸ»ü?ü]V¡ÿ?vÿøøº»ERòµùû·ÿÀsÿÅÑåjó÷oÿ€çÿ‹  ´U/+PÿŸ»ü?ü]V¡ÿ?vÿøøº»ERòµùû·ÿÀsÿÅÑåjó÷oÿ€çÿ‹  ´U/+PÿŸ»ü?ü]-¬Ó –¶ºØ\.õt 3ƒÇc@(ª—“Ê'ŠÞØ'šà±gä*Œdã¿QMòµùû·ÿÀsÿÅÐÚ*—•¨ÏÝ¿þŸþ.+PÿŸ»ü?ü]]¢©yZ‡üýÛÿà9ÿâèòµùû·ÿÀsÿÅÐÚ*—•¨ÏÝ¿þŸþ.+PÿŸ»ü?ü]]¢©yZ‡üýÛÿà9ÿâèòµùû·ÿÀsÿÅÐÚ*—•¨ÏÝ¿þŸþ.+PÿŸ»ü?ü]]¢©yZ‡üýÛÿà9ÿâèòµùû·ÿÀsÿÅÐÚ*—•¨ÏÝ¿þŸþ.+PÿŸ»ü?ü]]¢©yZ‡üýÛÿà9ÿâèòµùû·ÿÀsÿÅÐÚ*—•¨ÏÝ¿þŸþ.+PÿŸ»ü?ü]]¢©yZ‡üýÛÿà9ÿâé=EA"âÙÈþ$®ÇzІÎqsl’…+¸rb8"ªÇ-ÝÙw·’a UwÆ\¶ ê1ÍhQT¼­Cþ~íÿðÿñtyZ‡üýÛÿà9ÿâèíKÊÔ?çîßÿÏÿG•¨ÏÝ¿þŸþ.€.ÑT¼­Cþ~íÿðÿñtyZ‡üýÛÿà9ÿâèíKÊÔ?çîßÿÏÿG•¨ÏÝ¿þŸþ.€.ÑT¼­Cþ~íÿðÿñtyZ‡üýÛÿà9ÿâèíRÊyI¡¸ &‹•èÀô#Ó¡üªÝÏé¿sñ?κ çôß¹øŸç@3b>•%GJ’˜‚Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  [¿õÐÿ×TÿÐ…tõÌ]ÿ®‡þº§þ„+§ òKþ+^¶¤Öëöî3cnß8ç9ü+Ït"úO€|≟ÊM3Rž9¤ ŸôYee {¦:W´jÞÑ5‹‘qªiv—s„ò÷Ëc·9ÇÒ­ÝiVzaÓ®là–À¨O³²˜)-õÞàõ×k3£ÚFt ëé¸ÖP¿tž¾³³ø‡ñþ"LZ»Õí6·:t–ÛÇ%”‘^\£!*G¦8¬½7Â>Ó.ÒëOÑì­îJ¬‰  Œn)ô°š¹àþ –÷þo†±k6vÖZ½Œ¥ô Zc rbV ›©²=3Æï‰%±o…þ7Uh>ÜÚÜëãÌ2yÉ÷;îÇ¥{"xIM-%tëQ¦Å–Â1±pr0;Utð—‡ÓPûrèö_ló Þo”7o=[ëïGVÿ®Ÿä5²_×_ó5í7}’ÿbçëŠóßËu©|JþÆ›X½ÓtØ4ƒz¿e›É-/˜W,ˆ3cœƒ^Yº¶…¥kê–÷M*­*@=GÒ”•Áh ÐuKÛo øIÓîn£Óo®/ŒòÙΰÏ)ŽBSlŒB€I$÷=t¯qâKj:„šÛ #T[˜Cܤ’Ij1º)Ù ÝŒ–üzCx[Bm"--´›3§DþdvþPØœäÇ$š›Nðþ‘¦Ø\YXiÖ¶ö— ZhcŒ‘‚Hï‘LSÁw³ø£Pñ6¨÷·I£Ë ²³Xç*P|Ò¡åbN3þÍ3á•ÝÊx?Wf¸–êêÞòícó¤Þä# “×µuñèTz?öTz}ºi¿ó쩄랟^j-#Ã:&;M¥é–¶²° ´I‚sÖŽfq_ ¶V·sã N÷P¹ 4–mvžK¹1ˆ±Ðc‘žœW#á=_ÄÚµ†“¬¥ýøÔ¦ÔÚ’KØ…™Ì*Ѥ%·)ÛÓ#9潆ÓÂÚž¢/ít›(oC$@0' Q¿ƒü:ú‡ÛÛE±7žp¸ó|¡»ÌëïG[‡K}â›ËÍ/ÄÚ¥þ¯¨kJVëiZ’€q”x<ž§’GLW¯)  Ž„f±$ðž&£%üš=“^I ™æ1ÌãcïÀ­ÊÖ¹æßôÈn<‰Ø6ßBxÏ­zpðöŽ4“¥6ÐiÄî6þXØNsœ}j¼%áø¢»Š="Éc» .ˆ~ô)ÊîõÁæ›×úòíýyž}©Þj^Öï-¬umBñÃwãíӳŌ8¦w@ãÓ¦Ûªø§áíÆ³}¨^^Å,òEur%òÝ %™F>\—Ž=s^Ât«½K³i¹HMºÊPnœe3éÀãÚ¨YøGÃöWÐ^ZhöPÝA“© ™ÝOçI'{ÿ]CKXŽÜÅkt· Ÿè©ÿ¼q'÷{ôé]TM6É57ÔVÖ!|è#iÂüåGAŸJ·LGñoì¢ÓG.`R·9bmßùâ»ØÝdExÙYd2œ‚+Y𦃭]‹­[I³¼¸ I4aˆPIõ?jÙÚÁekµ¤I ¼J#A€£ÐP´@õ&¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¤4´†€)]t5›üƒõ?«ÿè±ZW] fÃÿ ýOêÿú,P4hÚǤ?î/ò©ªOøô‡ýÅþT^yŸdŸÉÿ[±¶}qÅ ·–­rmÖæp:Ä$¿.´Ùu 8fÍwo§€"†ü³^7öOü!ÚؼŸøMþÓvÿÇÏæû»ãnìöÅt±ølë¾)>%@îO,Íã÷cϥzMÅÄ6Ñù—3G }7HÁGæi{k <—0,-÷diSô=+È®ÿ³¼"|C=¤0ý…ŽýN’/7#nàpÛqÉ5v Hì4 iotýFÊçXvU·LB ¡ùxÞ…sÔíî!¹ŒIm,rÆxݘªV·×ëWÖr[F–ð"4s ƒ3–‚Wýkžð…½¶ŸãXX"CnŸg@œ*’‡$ÙÀ¬Ÿ¥ÈÔ|zúb·ÛM…¶ÓÞû­œ{âÏA‚òÖâGK{˜etûÊŽ¯Ô Èðξ5;yMë[Ap.e…"Y9eV rq\MŸööÇ…?áû7Û¼Ñö¿³}ï'aßæÀ±×½fݾ„ÞÕĆռ@nçû(\üÏ0ìÛŽ}(ÏW×õ4Ñô{»ùȰ!m€à±ì3Xš_ˆuK¯±ùúm‚‰î|—òu“ËM¹ÝÐe¿ÙÓáÅѼ KäF$Ýýì®Z‹\²Ó¬u ­µ½¼]K~Bîo"N~½(êdžu™u85oRÚîH©8Ú½Î{Ö½µÕ½Òµž)ÃÒ¼žþløoW·Y£T‹]Ýz®¥Õ -ÕÔJôÏ#Šè<in5»Û½?TÑ®-ÞG·Ó"1¨ ðÄnqB õRoù Gÿ^íÿ¡-]ªMÿ!¨ÿëÝ¿ô% `ÿò‡þ½ßÿBJ»TŸþCPÿ×»ÿèIWh9æŠÞ#$ò$QެìÄÓêÝ­þгÄ`ëæ>•Æ|Dû?ö¶`y³}«ÍÿUælýÖÿlîük˜º£øàh¾_ö ò<¿õ^gfßn™ Zµ¼¶» mn!œ/_-Ãcò¤–¦äÛ‹˜MÀÿ–AÆïË­yäJOè£Â?gmçûxµÆÝ›ÍàqØÅ`xjÖÉ4¥—VÑ-uˆïVY•íÙo@ÿ2ßÎFGJvÏ`:…¸ò ݸŸ8òÌ«»>˜ÎiºÅúizUÝô±¼‘ÛÆddLn ñšòíZÃN¸ð¯µ !€ÞÚÞLÑÜ`oŒ®Ò0zŽk½ñlªÞÕ%fZÅÎï\¥KÚã[Ø£QeƒûSGÔôÛyʬw3¢˜‰n€•'÷­}+\‡PÕõ]4C,7{¢¿™Œ:²åYpzWâÝsLÕ|tM.òÍR{+|¼œtÖ®xžSá}fÃ_o¹-™±¹Þp3ÿ¾²?TôbNèÓºñœ& ÐX^\‹;ıýÞÏÞÈØÎÜ‘ÀÈ5>•⸮µ(ôýCN¾Ò®æÏ’—h—v°$íšæï-AðFŒ/äHæ“P†ââG8Ù÷6OùéSx›V°ñ³áëmê;·±¿×2Âw,1"¶íÍÐ1H }_ÄòØkº„KI§é¶?iºÚ¤Ê]ȋȀIÏ·J~Ÿâå›Tµ±Ô4GM’ï"ÝîJH@ÎRyÇ­s,¦o‡ž(Öî$š§™*±þÇËü†õá!ðÛA¯Ç¯Ü­ÂÅöR©˜”†”yxÆÑýàzÐÇ+‹ÕAÕg¶³•¢šâ%FQ’q»v1íVµ/Enšciöz—ö„fXE¶Ñ•wë\£¶¬m|Wq¥_§Ù¢½:Þ(LWjïØäà63€V®ÿhh¶ø:kKØ“J[yDRÊà|»GR{ІΛ@ñ-¾­s-¤–×V„K½­n“kíþðÆACVÚúàkëb-£6¦)ŸÎƒgvuǽr¶·x‡â5þ(¸²±³–9î#û…œªsÁ©õ””øêáíý3ûQwoãõ µ/-d¸kt¹…§^±‰aøu¬'_:–¯m|ÖÖëit-á̘iÅlœ÷çµy­¯ö7ü#žþÈòá-ûDnÏøøß¸y»ûãÎkRúO›ŸxÙ›¯µ“g.Ý£®wg¥=VŠÄðP»ÒF£¿ígO3\㿽mÐÆŠZ7üƒÓýçÿÐÍ?üxúé'þ†ÔhßòO÷ŸÿC4hÿñâ?뤟úPÚŠââhÌ—Çc«HÁGæjZãþ"ÚÚÜA§És¨iö²C1x⿆sŒma‘ùÐLú’[¤ïyl°? !•B·Ðç¤P+Ä|ßõ8ùþžµæ2j¶—úF…iýŸ Ù[Jóü÷je·BŽTù@mÎîH$Ž=j=:<aªBÐHÚ.§,ÊaËò„¬.I!vœžÂ\õ#s™Aš bÈ ™ô¢Úæ ¨÷ÛMÉœnƒÌW”j±¼¾—Zg…-u}Y&¸’h‹ ¶PV=àJðQÖº/‡–¦§©ÝØêšEÕ´©´lE6ùˆ,Ü‘ü¨ ”—Ö‘Ü y. YÏHÚ@þinomm]VææY¾è’@¤ý3^MãkÈu ÙG¢]ØÉ©êÜM¨j%ë2ù±O€7/íÜw­ÝóXÿh ýb÷SK´ì¸T ƒ´(sÎhWµîRÒ|W&±ãÓtèám:-=.îd`|Äy0cQÎ9\“Ǫ:L²x—âåúK'öVЦÖV!e¸?}¸àí}I㊹áEámS´Ñï¦7W`ì¹¹Cj q¾•¯á ?xz×MŽO5ãË12Èyf?SO¨º‡ümªê~#6WhVH·Ò[+6;£Ž “ò»c½_ÔµÿOã»­C‡IAf—M5à“?1 /Ê}Gÿ¯¥N< gÔ¬nµ]sQÔRÊäÝCû0­’@ÈPp;sÚ¶­´`ñ]Þ¼'•§¹¶Kfˆãbª’AóÍ-tsȼGyqªx“Åñë1ÞOu¤ÚÀ–ÇMwÛHmÌÌïÈÂîQŒäàZõ_\j~ Яo\Iuqe’¸ÜÅFN=ë3ăUÕ/¯mu+Í9µ–Ô· ‹•vAçnWŽÆº2ÆßLÓ­ll£ZÛF±Dƒ¢ªŒùS[÷8¿éñj¾9ð­Ô“‹Yc»2$r2n!ƒØœ×-Ρg¡_\Eqwq…|J"ˆ4¤»ÛeAGby°lžÃêÞ'ð¿öÞ¥§_Å©]i÷V+*Æð´Àp1×.“âdÚä7— §èw–º|–¡ñ¢Lù’žBî\úm4jϪ[ø·Ä°é×ϯwýž¥²Ú±Ž9<ÎOÊ0\}H®ä|.Ðdе]>ò?µO©M,ó^È‹çä•8ãž‚·Êò[gŠ$‰.U˜ÀÎÇ ~´º¡Ê|.¸»»ðÆ¦ê Žð_\ %¤’=áÈ΋Ï`xíT¾˯LÚ»Ý^ØÍe©r’~êO1ˆ?ÀK«žÄt>ð¬žyÂkwpM,“Ð÷r,…fEa/«ÙvóÈÿßmþ4eÙÿÏ#ÿ}·øÐ‹‹xnbòî!ŽXÿ»"†‘¦%¬i%´*‘È¡ }G¥Eý—gÿ<ýößãGö]Ÿüò?÷ÛYƯ$ˆŠ²¸Ã:¨Ü}2{ÖG‡ôÒ'½¹{۫뻲¾d×, ‚F š½ý—gÿ<ýößãGö]Ÿüò?÷ÛKµ¼ŽööÐÄï÷™)o©ÕÓì–ãÏ[Kq>sæ—v~¸Í3û.Ïþyûí¿Æì»?ùäï¶ÿµ4QÍŽdI#=UÆAü)œÆ^4cÊ í>£Ò«eÙÿÏ#ÿ}·øÑý—gÿ<ýößã@ýš Ò·“eâC´|ÿ_Zm­­¦ï²ÛCî¾ZÏåQeÙÿÏ#ÿ}·øÑý—gÿ<ýößã@jŽCëCiÎÈlv%†?‘¥þ˳ÿžGþûoñ«öñ[¡XP ''è´Ä&±nXà4. žç*qú½QOW ²d½pj¿ö]Ÿüò?÷ÛYž§ˆÇ–3ko(0(R¸ ð1ŽÞ•cû.Ïþyûí¿Æì»?ùäï¶ÿ–ÖÎÞÔ"£fs"-õÅ!°´7"àÚÛ›€sæùcwçŒÔÙvóÈÿßmþ4eÙÿÏ#ÿ}·øÐæÖÜÇ$fŠJrë°aϸïOx£xŒnŠÑ‘‚„d銫ý—gÿ<ýößãGö]Ÿüò?÷Û>ÚÂÎÕËÚÚ[ÂÇ©Ž5Sú šxb6OH™k¨##¿5[û.Ïþyûí¿Æì»?ùäï¶ÿ±qoÌ~]Ä1Ë]²(aù££[\i7z}°1Ü¡žÙUéÆ3SeÙÿÏ#ÿ}·øÑý—gÿ<ýößã@µ°·¶Óa±XÕí¢ŒDÀ €1È¥¶Óìí\½­¥¼.z´qªŸÐS?²ìÿç‘ÿ¾Ûühþ˳ÿžGþûoñ  1Ã[ü¸Ñ7͵@Ü}O­@úmŒ‘$RY[4I÷Q¢Rè1Å7û.Ïþyûí¿Æì»?ùäï¶ÿ³1[Æ#‚$Š1ÑQBø ÈÓ<>–zåÖ«5íÝÝÌÊcA3 °¡mÛT8Ï­^þ˳ÿžGþûoñ£û.Ïþyûí¿Æ€%K;Xît¶…gn²ãÖ›&Ÿg$þt––ï6sæ4j[?\S?²ìÿç‘ÿ¾Ûühþ˳ÿžGþûoñ  ´ŒÁT³ä“Tÿ²ìÿç‘ÿ¾Ûüh]˜?êsìX‘ùf€ ĺ#êY‡Ð±"“G#쬙ùÒYOœŸäjè €*´ö6ÓÈ^HsÔ‚A?•Z¨.­-îÔ-Õ¼3¨9D çPÿeÙÿÏ#ÿ}·øÑý—gÿ<ýößã@Kci, ¶°<)÷chÁUú |vÐG‚8"HNAPçÛ¥Aý—gÿ<ýößãGö]Ÿüò?÷ÛYò"ò<Ÿ)<œmòö¸ôÅ2ÖÒÚÑXZÛÃnHçò¨²ìÿç‘ÿ¾Ûühþ˳ÿžGþûoñ ϧÙ\KæÏio,¿ßx•æEéösL³Kinò¯GhÔ°üqLþ˳ÿžGþûoñ£û.Ïþyûí¿Æ€.ÑT¿²ìÿç‘ÿ¾Ûühþ˳ÿžGþûoñ µ!õKÖS• dzÄÔUêŽc‚0 D…I@súoÜüO󮂹ý7î~'ùРإIQÇÒ¤¦ ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ïýt?õÕ?ô!]=së¡ÿ®©ÿ¡ é袊(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¤4´†€)]t5WIéuÿ]¿öU«W] UÒz]×oý•hý[RºûŸstc2y1´›Ál â¹kO\ï°}SAº²²½tŽ+¡*Ê»Ÿî†‘ŸZC;**„ÚÆ™úÙM¨ÙGzØ nó¨ç¦9§ê:†˜Šú•í­¢1´ò¬`ŸA’(åÏC¬É'Šç³ó :rY-ȹ'Ý1ZÐjV7í´¦áKòUo1GR¸<¥[¢©¶§`°Ï+^Ú¬P9ŽW2¨XØuV9àû];R±Ôãi4ëÛk¸Ôáš V@¡ к+[ñ:N¹¥iÓÀì/Ë80 1Œr@¥ñˆ¢Ñõ*ÌÀóË0‹ä`<¥ÈÛÛ$ Ý¢©^êºuŒñA{io4¿êãšeF 'š’úþÓO„Kuom!CÍ E'Ó$К+úÓ_ŪOs-¸¶¶¹xÒTo—`䞟jiú–¥—N¼¶»Œ‚UéhÕQ@Q@Q@â-qt7ÓLÖï$wKjÒ†B[î“íž)u­mtÝKI±Kw¸ŸP˜Æ¡X ŠYϰ±ER¶Õtû«¹-moí&¹ïÃÊΟU"³õOÙGizºn¡¥Ï¨A"K´P1×w9Qï@´WâOÜé×6¶ÿÙQÏ=¿Ú$’öïˈ»ò*èíõK'Ó~Ú×¶fÝ8’d™LJÂ7g¿Ecx§]MEmDÀ÷H$L!ˆži5ÏÛé^m\F×ì hFé èmQYÃX³I·Ô/® ²‚dW q* †q’qš´·–Ígöµ¸„Úíßç6úîézžŠç¬5õÔ¥«ø^öëÂï¡G—Ù ÿen;Lx;X£?á\Æ¥áÝFO hÖ·ºEÄó/P{x¥¥òw>è8;NÀê:R¿õ÷ÿíý}ÇÒâ-Ìï¬iËsÜ PàgnsŒãœSã×t™cžHµKKu3-ž…ŽxÔ×ÏØJ–GDÙÿjÄÆf|¼›R cÆþ§Ö«è¶:¦ámM´ï ¤÷¯¢Z‰­å³b’7œ7’1ó²©Î:ñG[[\]/ýocèû}sI¹Žy-õKcwÊÑÜ#×Õˆ<'Ò‹-sI¿¸Xê–73•Þ#†áŠúàãÞ¾tðö•q‹µ«‘g$¶’øfæ)Zìq¼„)òÂó£ÔÕørm'Yø`-ô‰moŸt·“%±\9GÛæ6:ñßÔPîºZÿ_—ùžËâoéz%Λ oû^_%ƒ‹i‘Œ1ÀÞ㜊ëëäïè‘E§ø:Ò/ j6þ!³ÖÑõ ·´nPIŸšOâøÅ{gˆ-ôÖø·¡Ë>Ÿ¬I¨,_»¹‡þ=Pa¸~:õïéOúü_‰èµÄø·Ç2è^&´Ðí4ÝRîê<g‘ |ÄtÀ?vÕä_4IõIu{+d°Ë{§ÈД$œ09Ç"Ž©[FÎÆïÅ’ZxNmSP²J¾Ãù6z…Ôq™t³Žj–©ãkÝ+Àv^"›C’÷}¿Ÿs•Â2À1’w†â¹ÏŠú^—¥ü7›D¾·Ôõë·†v²¹»·7óG!=KíùOÍì=©4«¤×~j&—c宎 0ÍhÐîbŒP@Ï*z{zÒoFÐÖé3Ѽ7®E­xbËZhÚÎ ˜å&a˜ÔŒüǧJµ§jÚv§»û7P´»Úo³Ì²`™Á5âZÅ÷ˆþK èÖ:¬µ•­°š9mž"ê².õRq»…<£ëG Þ/iÚ…œº„R%“Ç,cDÀ¶ÌÙ çüµM{Í"Ñ6{Ä’$Q¼’0D@Y™Ž©5•ݵõºÏeq Ä À’'§ñ·¤ +‚Ðý ym˜@ÌãîàñÏJñm'ÀÚõæ¬/ô-6?Äfß+CxòI:ƒ÷L ì¾:sK©]oª†³¥éÒõ JÊÕÈÜyÕ=pO±«ã¥xÅ{´ñ®¯«é¶×š¡¶Ž3gy£Ëi~QŽ>çNqÜj@zž¥ã{;Ævz ÊlK«&½[æ•V A$ûÚ½6¯¦ÃaìÚ…œvRceÃ΢7ÏL6ps^'â"Oxë³kzmáðܯsnmËÅŒ§8ÆAS×¥sºF™<øyu®è—÷ºŒ·±ÞÙf†iG–Z"2ÃŒÿZk³þµ`ü¿­£àÕ´ë…¶h5 IVä‘I•„¤uÛƒócÚ§7vâìZ›ˆEÑ]âãy_]½qï_5-_LГ^Ñ4‹øa_±ÓíE» ¶’¬U:¢î  ìþYëRxãS%ìךE„v_\«¤‡rÌÁ_º´-¯ z^g³’$ä“\Ÿ‡5CRñf­¾Ñ®tXÕ~ʶ“¬“üEðN9ö®±€e*ÀF=ëÈüekañ«ÆZU¼ǧ[슈ķ;G•ìÂÚ\ôØõ½*]@ØÇ©Ø½ðb¦ÝnÉ‘ÔmÎs\÷‡<}¦êÚ†³iuå鯧_5€ûTè¾{©#(3Ü`ãÞ¼;Ãú;-‡‡´…ðÝü^2¶ÕRK½E­˜eÁbgÇÌ1Û8?Sñ.Ÿo=ßħð¦¥u¬^êÄi·‚ɘ.«ãåziÿ_—ù‡õùÿ‘õuSIŽX´»8îfHQ_?Þf­Óz1'tQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@!¥¤4J롪ºOK¯úíÿ²­Zºèj®“Òëþ»ì«@ø•‚x{Rf8Ýòà&¼±5K,4Óõé5NÞH=1”:àò “’N1^ÊFFJjÅœª(> RZ êy޵8.¤ñ 7’èö—8Ž%µÍÌÀ`†×éŒã­ª\é>;¸ºñrÛý†[VÆ[¸·Æ;ÀÈ •é 1Ë"“êE+"¸Ã*°÷ ,y‰|«ýfìÈÖâÈi°Ë-¬?!¸·NÅÇAŽÝúUËí_IMkºürňm¥…&Û„ˆ°Tãîô#ð¯B  ä(éIå¦Ý»o¦8 V<›Î´¹ð—‰®cxfµ“Äh ÊɹG¨ë[\º]çÄkIü.°ùZJšƒÛG± ù|°Øë]ž³¤[êÖÒv’(ĉ.a!NTäv…c«x²/E_ÝLM§Ú!ÞÒðœ‡<õ¨]2ûÂw>4EûzSDé7¤W”Ù ·Œšõ¦Eb *’:(eV`ô"€±äWouá=B],ÄÚJëk,ÞT;ãò2 %Þ\à‘í[ÿÒÂë\¾Ô4ÍcO½V…b’++o%Fûö®ø"…Ú芟qU~ƒ 袊QEQEcø»I]s×öáå1·÷\r§ð W£Ü^xœjZ¼…Õ–˜ö00EÉS¼ýw+Ó)BýÐÒ€<‹ÁK¥Ýêzvú¶ŸíÜÖqX˜§Ý³®~½sÖ¬ZÃb~ë«¿¶íLÂ1»kqœgÒ½LF·PÞ¸æ—bíÛ´môÇ=A+E µ¸ñÍ„2$R¡Ñƒ*21¼zÖV¯¦É³wáµCöM^ú=B2€£e?Šûê½7hÎp3Œf±tA§j2ß=åýõÓ©E{ɼÏ)IÉTO~(ê+hgxöT† €“êpG`Iþ•|——VwqJ4&;‰ídc‘+HÑÿRãñèdŒ€qÈ͆y•å‚ÜxOSñ0‰´C¤â’â=ñ%ÆåëÁ•­Wš3×w6¶ì|0uèn +ØÖ€ä&>æîqŽ•ì…®ÒªWÓRíqJb±æ°ÞišŽ¹«ÿÂ!%«;hì¨m(s´p=+'ÁÑé——þ†ÛVÓ¡¾²Ã5¤v&)ó·®™=k×Ò4O¸Š¿AŠAÜCz㚀Ðú(¢…Q@Q@Q@Q@Q@Q@Q@Q@Q@súoÜüO󮂹ý7î~'ùРإIQÇÒ¤¦ ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ïýt?õÕ?ô!]=së¡ÿ®©ÿ¡ é袊(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š( ûH5 ‹;´ó-î#h¤L‘¹X`ŒŽG±<-ཱིÝÏ¡Ø}ž{¬yÒ4¯+>:rìMtTPEPEPEPEPEPEPEPEPEPEPEPEPEPHii Rºèj®“Òëþ»ì«V®º«¤ôºÿ®ßû*Ðú*…ìÎÓýž)<•Tóe—ªöǹÁçÚ±´bÃTšhô]Fy.a]æ9‹qœnœQHgQEax“X’ÇÂWº¥¨Q4p–@ãp Ó‘ß²<-¯yM ¾««^_\ÞOäÁçißfÃÜ@Àäc<ší(®CÆzú ø™t«™"Ô4ؾwU ÆÄ0HÁâ§³»ºo5£\ÊÖ˧$žY½MDJ‚]:Y•"¡a»z€AÁf€ó=¢ŠðÿÝxæOŠÞ&ŠåôWòÍ¿Úáóç1Ƥõ9qž dúUmcã…ü:¶²ºmŽ™-žvÖ¢ÚV—íWk2m=²E€÷š*¶—y£¦Ú^ÂEsL† üëÈüsw¯Z|nÓG†¡†êêMØAwrñÛ©Wo˜…ÎN ==(z;0Ý\öZ+ÈÅ;é|£êÌú›{s,ÐÜE¨M \Ævþì(,yÇ®3Ul¾3\¿ÃíW[›Nµ›Q´Ô×KŠ;yÉ™Ûn×ËÀa¿NÔí4WüD½ñxøaâ¹|GŸba¶ŠkIôÉä'ppX6pxÂû‘NðW5mkÃZ½ö™ƒXhö;PÜnó¦!Ý–ü¨O¨É+­|ƒ·™ë”W“øwâ6µªj>†K-9!×­&¹œ©}ÑìÝ€œû/_jÆÑ~*ø¶m'KÖu=FM"ãRlÞD²CnÚ]Aà.AëÍ>¶þ»~b¾—=ƨêÚ¾¤$/ª^Áh³H"ŒÌáw¹è£=M^¯0øÃ¨Þé“éÝi:>¡£}²ÓíÌñÌí·r…ÀÈî{ñGkŒôþ´Wžx»Åúü>0‹Ã^ÓôùïVËíÓË~%€UÛÎì¯ê:Wâ/êÞ!¼øq«i–59/n­d³k–HEÚ2Äg+•¡88¡ký|ƒúýO (®+á—Šõ/A«Á®Ù[ZjzeãZ̶ÎÍ`dÝÍ]ø—ây|àËýfÚÔ]ÜC±"„œfp£>Ã9ü(zÔê(¯ Ñ5ÏÅñjâo%•„©á÷ºx¢»ckÖnÕ¯|ZÔoüe£è“Ÿ^¦£æF%Óe™¼© ÛÔàšöŠ+üuã™>,ø–+—Ñ\Æ-ÍÜ|æ8Ðþ§#®3Ô ŸJ“ÇŸïü3©‹y|3{œû>Ê—µÉP@ áJëÁ•ÙÑHg-ã[ƒà+ûKHe¹¸1©å˜îà~uŒ!ºI|=«[Ù\]®pdšÞ]­BB÷ ‘ÅuÔP™Ïe{«hþ;{m:ú#¨*}ž;ˆLO&€yí[:{\7Œ¬õÓ¯’ÞóNnx±ä°lâOîšìè V (¢…Q@Q@Q@Q@Q@gß!ŠçíH^?*UQ–$‚~§?ZТ€8ÏèzV‰y-Ε%ÍÝÓ¡Ž8ØäF “éV¼icp~ê–v±IstÖìD¹gbrp>¤×SE´9Auåè:µÅ×ö}Àš[xTJ”*p;‘ž•‡%¥î¯iã©môë膡 kn—0˜™Ê¡y¯J¢‡¨%c‰ÓÉñ^“¨6|–÷:oÙØ¼X08`q þí¨¢€JÁEPEPEPEPEPEPEPEPEPEPEP¯3àTÏÒ³îßÐ'ñÆVþÐ俸¦rÛ"‰N7±÷ì+Ãçøñ­¼„ǦØ*v ¼ŸÏ5Ñ~Ò²–Ñ4¥Ïi?ú ¯ŸhÖ¿áyëÙÏö~Ÿù?ÿTáøÇ¬Ä>[/Ä7ø×˜ÑLVuÁÿ.“ÿ/ü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢Šõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢Šõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢Šõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢·ü á{¯x‚-&Æha™ÕŸ|ÙÀ¯AÖ=ãþv»ÿ>“ÿñTÂî×çÂÃòþ*¼ªEÙ#¦AÚJä{ShVÐõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿŠ®aü¨Áḵ›ûý&Æ9¢ó ¶¹ºÙ<ÉUÇ©H«±|2Ô†«­Ø]^Ù$ºU¿•¢c*²í$(#ñßõ¡é¸-v6¿áwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢Šõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿGü.íwþ|,?'ÿâ«Ê( Wÿ…Ý®ÿÏ…‡äÿüUð»µßùð°üŸÿН(¢€=_þv»ÿ>“ÿñTÂî×çÂÃòþ*¼¢Šõø]ÚïüøX~OÿÅQÿ »]ÿŸ ÉÿøªòŠ(Õÿáwk¿óáaù?ÿEyE÷ßúèëªèB²¼}ð×Oñž¹¦êW×·Víf» PíÛ2î †Èé‘Z·ë¡ÿ®©ÿ¡ éèóÂøƒáݶ§âÏøHm5KÍ;P6asV ?Þ“Ò£‡áŽšŸ$ðsÞÝÉfÒEÃæ/¿=1֥׵ܸÖ}B¼³µŽ=9nÁž3s—+ƒÈã±´/ê…σnîiº»Oep¡åå °ç8m¤ãÐPµ_×ó£þ»‘¾ž ÔÃí¦ÂY«³ȹ"2á6mòþî1ß×5‡cð’(¼)sáÛßê—ºT±lHdX×Ê}êáר+ߎMU>1ñü’I§Ëk¥Ïˆ—i+E¸ytŸíÃð]?‚µíNçÄö®µ´×Ú_”ââÝ ,‰ $ ¤ðF?QFúÿ_Ö¡¶†5¿Â[#w¨\êšÞ«©K{¦I¥JnÕ69Š“EøY›ªxzòMwPº+i ª*TƒœCÖªx³U’?ßéOÓ4[)Vh÷y·J…‰ÎyUÆ0=O¥dx_Ç:­î½ ZcKÖbÔ-¤šæ;8¾{2©¸*HwËÎ9¡wþºÿÁíýt:gðOÝxŽÃ[Ôl¥»hÍͼ[Lr„ÆÈÎ1ùÕ9>,7Ú¬º7ˆu]*ÛR™î'¶·+´ÈÙÜÀ‘‘’k™ðÏŽ|K6àÍjþûO»]ná-ä°Š²|ÌAd òÇŽ*ö£âïØi߈~Ñc.™¥êR[IfÐaÞ%p£kƒÃ|ÝèÙÛúéÿ{«ÿ]OW‚?*ãÜ[b…ÜÝNS\/Œ>§ˆ¼W¿»©é·°Ú}’?²A,Iä“»€®ê±$‹ÑÔ0ükžñ‹ìtmM4Ö‚úòý¡78<ÖŽ<ã{r02ן˜/#˜O„Zm½¯‡ÃR½¶»ÐÚf†èfs)Ë1Ô |_ t£á_F½¿¾»]Jð_½Ä„ aŒ0 ²)Þø†¯àíP×#–mOSšd·´³‡t²*HÄÏð®2{VÄ?4ilà¸ÅÚ¿l©$[ZÞb3‰A?(Æ9÷´ÀϺøt×ÞÕ´OÄšµìzŒqBÒLP˜Ñvcô'Ò¥´øqagªÝÞZ^]D—¶+ay…Û:ª xá»äVòx’Ö]_UÓmພëM‰%˜F€†Ü §<·Ž*€µùu½îúòmÂ;©£ËAä˜ÕX¬2rGBsFá±Íxoá-¶ƒ«èš„zî©ttxäŠÚ vlØà‚¼SŸÂ¹¯‡? näÑôé|Sq©ØIm¨=ói~to >íÊÙR}qÁìx¯EÐЇ'Îî˜àÐ:š+–¹ñ=ôfÚÚ-YuI"3Kj.y ’/Ðçb¢>23ÙéRéºd×Sß™@eXÌm!'Ž4×Q\Iñ½ÒB×2ø~í,á—Ⱥ”Ì„ÂùÁùGP29â´5Ëm¨Kg¥érêRÁÍpRUb QÏR@'€:j+’›Æ)ÓWGÒ¥¿–öÙ®•<刢©ƒžù5GYñEÍç‡4Ûí1%µ¸mR+Y vPÙÜCG»‘Î1šçwHHÉë\õα­Cc ©áÉ$ÙƒÂ.Ðy`t;\Ö6©©¦³?ƒ¯cŽXD—î'<«*8 ヂ(»¢Š(¢Š(¢Š¡t ÕòÚ–e…có$Úp[$€3éÁ  ÁèA¥ª2i–ÁsoÁ(û²F6}ýÆñeèÔnîÇÉ /öY|©2ÖíÒ€:z+•¿ñåüZn£Í¨N¶Ë;fáP…õ³q¬kQXÅ*xrI.Ù^vƒbޏõÏ¥t$ÔÒ׫ꉭGá;ÄŠX7êE<£¨ue8ààƒ]íQEQEQEQEQEQEQEG'Jʾl)­Y:VF¡÷Mx'íÙÒ4Ïúù?ú ¯¯vý¢¿ä¦×ÁÿÐMxM0 (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€=àfc¬øÜ&£Ý ky.b¶cÄÒ.6­vŸ µ½KZø­¦K©èvºCEop«6žNyèxç+Ätëë­6ò+» ä·¹ˆîI#l2šèçø‹âÉïí¯fÖîêÙYbªe qÞ›{X›nvúœvþ,øy£k•†ŸezuŰ’[XÄA¡,Î03‚yö®ŸX²†ã]ñw†g𥮃¦i=¥òÄ÷÷+oêw¼!õíMôQ¤5Ûÿg ŒþF<Ãü]3šÐ½ñlj¯tq¥]ëW’ØÛå3õ„õ#êjZÑ¥ýh¿Rï­ÿ­ÿÈõÝ4máíž‘á‹MfÓ]ˆ>¡y$!ÛqÆTÊã'ò¨+Ñ-#¶µÔX6úuýöžn­™ÜÀ@üŸÌòÍƾ#Ñ4×°Òµ‹«k6ÏëŒôü)ÚüI ÛOo¤j÷°ÌæIpw1êyš§»×ô‰¶‰±àŸ[Øi~3ÔµìÛ]jËQû;JÖ-w º`V%ç'’8Åp†Þ"¶›ÃÛ|¹m‘çÙlöèdîÊŽðxâ°4oëÚ6¡s}¦j—6÷w<Í l™s“œäÕ=w[Ôµûó{¬]Éwt@_1ñœÜTÛo"–—=wâ¾·smᯠèÉi§Ëk¥F¯$öêòÆwc(Ädq]uÝœPøëÇöšuª+7‡¾H-ãÁbQ†¨äž•󾩯ꚨ²…ãÎ,£[î÷j:WÓÆþ$Mqõ„ÕîRxÄM8À,£ #ý)É)6ûßñØ•t’]-ø[ü\ð>›¡èß ¼=ªÜÍa ^Ìßm’ëL{Ã&(Ë8ZËðä¾±ñO‹à³‰-£šå!ÓoïtÉ&‚ÙÏމчÊIÈÇJóÇ>%ÐÒtÒµ‹›džC,Џ!˜õ8#‚it¿ø›J¹¼¸Óõ‹ˆf¼2vI‘½NE;ÞWþº´°ï‰Zuî•ã]NÛSŽÒ;ሴ]±TTvv®b¬êW×Zôך„ò\]LwI,‡,Æ«TÅYY”ÝÝÐQEÄQEQEQEQEQEQE}ãwþºúêŸú®ž¹‹¿õÐÿ×TÿÐ…tôáµí;Ävþ4ŸXÐm,®RM9m¸œÇµÃ–ÉÀ95•7€u?†z~‘¥Ï×,.ì37²—,À{aŠý+¯¾ñM•‡‰WH¾+l ¯ÚEÔÒ*G÷¶íÉ=süëJãUÓ­­âžæþÒ%ÿW$“*«ñž 8"ŸRð¹¾ÓôË(txʽÄS—–QålÛÐpO<×g­XÜZÛÉ%ÌI5°ºòd•C¤dIè22zU‘fM¾.íÏÚ3äþð~÷'o<ñÏɵÏ#ðçÂÛ½¾¸Ò¡³´ñf›"µÔÑ·Q–>dlÀ|Ù‘ÆZ¿ð‡Š/´}oÃÏ„zn«©Ks%Ù˜—HYèÞîkÒíµ½*êu†×S±šfû©Â3 ´(ëëúÐc!ŒEF½B¸ÍsEÖ ñÉñµß™¦†i|½‡~ðÙÁÈ'úU¯øëKðÍÕ•¤ÒC5íÔ¾RÃöˆã(vîË–#hǯ¨­Í/U·¾¶´6¸¸nev GPAä{Ž)5pZhyM¿Ã½v- ÂÒÈ–ÒjÚT÷Fh#ºx¤îIÛ*a†:†· ðmÝß‚õý>æÂÞÂúö:·’\îuÚQÝ›œåy‚º?x¤húbê6vgT±„²ÛN˜Œ† ŽO<äqÓw^ÕçÓ4qo§Kxß$I"£"í$žzãǽ&=Ú0<¢ëÐu«È »ñýÓ\ÏÍ„$ G(ÏJ‡Àº.µ§éš–—«XÁµÜ× b¹ÞWÌbBãÔó]~‡¨ WH³¿Xž¹‰e¾2 Œàâ¯U5ºd®7ƒ-l¦»ŠfÈÚBc¯Ló\žðûWÒ-¬t™´Ëkë KÅtµI‘Da÷´0ÏqùײÑK­ÇÒÆMÙÖˆ,…ª[Ë´—'Ìü;}«ZŠ(ñV—}¯ø³F²–Ù†i›Û‰If•N%ðæ¹¥$3É¥´êöò¾Íë*$6#z먥aÜñÉüâ GBÔù,£ÕN¼5ˆ!ŽvU`nÏ1pÈpHÜ9üë£ð÷…ndþßMcKŠÕu e¶óF¡-Ô’®Åúž1^E YX¾§œxÃzׇ-µmOYŽ+Í[ìñÛ[ÇlühË’Ï¥uVš%ä_/õ·}Šm>;T!¾}ÊåŽG§5ÔÑJÛyûž_âj‡Ä¾!Ô4« A5¸#V{ɵœ‰‹tyª±Ñ«rÃþûð á›uúØÿß–ÿ?á›uúØÿß–ÿú+í·÷,?ï·ÿ 5U¯åµXîŽ$”¶÷Á Xc§ûó Ÿ?á›uúØÿß–ÿ?á›uúØÿß–ÿú+í·÷,?ï·ÿ >Ñ«rÃþûð á›uúØÿß–ÿ?á›uúØÿß–ÿú+í·÷,?ï·ÿ >Ñ«rÃþûð á›uúØÿß–ÿ?á›uúØÿß–ÿú+í·÷,?ï·ÿ >Ñ«rÃþûð á›uúØÿß–ÿ?á›uúØÿß–ÿú+í·÷,?ï·ÿ >Ñ«rÃþûð á›uúØÿß–ÿ?á›uúØÿß–ÿú ïTÔl¼¦ž6G}Ÿ#¶G÷Õü$óíýö‹†§€ÿÃ6ëô±ÿ¿-þ4Ã6ëô±ÿ¿-þ5ïßð\Ï´_÷Ùÿ ?á ¸ÿŸh¿ï³þ\,Ïÿ†mÖ?è;cÿ~[ühÿ†mÖ?è;cÿ~[ükß¿á ¸ÿŸh¿ï³þÂAqÿ>Ñßgü(¸Yžÿ Û¬ÐvÇþü·øÑÿ Û¬ÐvÇþü·ø×¿ÂAqÿ>Ñßgü(ÿ„‚ãþ}¢ÿ¾ÏøQp³<þ·Xÿ íýùoñ£þ·Xÿ íýùoñ¯¢-¯5[‹xæHlB¸Ü‘óÿ Ô¾v¯ÿ<¬?ïãÿñ4ó—ü3n±ÿAÛûòßãGü3n±ÿAÛûòßã_EK¨_Y¨–ú slÞй%®VÅ0>]ÿ†mÖ?è;cÿ~[ühÿ†mÖ?è;cÿ~[ükê*(òïü3n±ÿAÛûòßãGü3n±ÿAÛûòßã_QQ@.ÿÃ6ëô±ÿ¿-þ4Ã6ëô±ÿ¿-þ5õòïü3n±ÿAÛûòßãGü3n±ÿAÛûòßã_QQ@.ÿÃ6ëô±ÿ¿-þ4WÔTP)wþºúêŸú®ž¹‹¿õÐÿ×TÿÐ…tôæ3}>‰ö3ëz\÷Ö1鎡…£L‘»9ë€y b¸ý&×T²ðßì¯-$°ÓÞ{Éä»’Én¦µËDŠX&õcÎ8j÷ú(Z+Õßúì|ý¢é÷“|<ñ4&ÓQ»±O ¾Ï,T“Zâ6 1Ôá@x®ÇQ›B¼ðOŠ—Âú,–3›ŽWN0™8`  ØçŽÙ÷¯P¢“WVvwíoµ†V¶ÖWªö+0¹/lè±³ºá‰#õµëšf§eªC$ºuÌWÇ#DÍÈWS‚§Ü«ÿ ú±Ò…ü ¸¡NX»°qÐ㚢Z¢¥¥ç‡<7§húTßÚðëë<“ÁEòÖVÞþ`ÀÛ€3ÎÜb¾ˆ¬ýG²ÐtÈ´ý.# ¤Y؅ٱ““É$õ5¡UÒÂës‚ø…`³øŸÁÒ¥˜›þ&$LÞVàÆÃæã§Ö£×^â=–£=¬¿bm)í‘ €¸VWÜåd ìµ}cOÑâŽMRî+d‘¶!ãsc8¦¬ÙÜÃ{i ͬ‚H&@ñ¸èÊFA©KúùÏ%ðí­Óü »Agr³™®¥4DI´»}Þ¹Åw‰©C¯øBù´ôœŸ!âÙ,MnÙÓZÑÖµý/Dò¿µ¯b´ça“ c?ÌSõ]kNÒlã»Ônã·¶…Y8$‚4¹th|ÚÜÅøq«A}áË;TŽæ+›;xãš9áhÊœc¸ç¡é]UCeuõ¤WV’¬Öò¨d‘NCÜTÕmÝÝ••QE†QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE†–Ð+®†ªé=.¿ë·þʵj롪ºOK¯úíÿ²­\–D‰ ÈÁu$àTßÚÍ HæRÇ 9úg­C"‹XG/1ÑTô,ÄŒþZµuoÄ Š1ާ¸¤2j+âþK Ixn- •"$MuŸ,À-Žqô®sBñ•ÉÇö´ÚuÚXYý·ÎÓÕÑJ€Ä©W$çåëšîè®?HÕõñ¥É®k‹¥E¤}‘®Öe΋·p‰ÚN=¦èú§‰ît—Ö/“GŠÂK6º‚ÖC*å7 f'ßPeEq±x›P}'Á×&;O7Xh…ÀÚÛT4{ŽÎxç×5ORñ/ˆ-Û^¼tìí*èÀé*È$e §;ÀûßÝ .wÔWe®k–Ú¯Øõ•Ó$R]ÛIh®¸)Œ«cŸ¼9¨ãñN¢ÞðÆ b´ûF§u 3 ­µUó¿6Aã¹4 ç]o{ms=Ä0L’KnÁ%U9(qœ¬W¦\› ¯ÜG=­»Çx ËuŸ-O”˜-ŽqôªVÞ%½ÔôŸY_O§\<{N—zÈ‹†V!É ŒuÍ=ŠóëmkUÐ<asu.›2\GoˆÈ›YÇYNæÈžÎ=ëCÁÞ&»Ôõk­:þK ‡Ž%•.,’DC“‚¤9'#ëE…s±¢Š(QES$‘"Bò0TI8ŸT&Qqª¬RóQ ž…‰#?†?Z–-BÖY$ÊXô#?LÕª†âÞ)áhåPTËé\—ާvøi©Èò0a ‚A8p3Ç®(¬¹»·¶xRâd¦}‘†8.Þ‚§®Äë &‚·³i’^}¼}•¢†EE[gx,I?B)eñ~©§[jvÚ•µœºµµÄ6И7$2™¾é ’@ç“@®w´Wi®ëvÀÓ†·h¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(9zVF¡÷MkËÒ²5ºh‡…ÿä=?ýqÿÙ…vÇø_þCÓÿ×ý˜WaLGãÅ¡kz~‡ig%þ³~…íáV £œíü á¹Çðý)“x³QÓ].5­2Ýt€LSÞÚ\™E´€C©Pv‚1¸gš»ªë‹¥x¾ÒßS‚Þ6êÛ_¿gÌLÇ€ •+êCV»u.¥xŠÂùluµwÓ-ácç\4Û‹FÉÏÝ<î`ô CV ¡”‚¤dÞ¼ÿPø‘,$Õ4‹ k:‹i¥DóÛù{Wp ñ]¶‘Úé6Vò€²E#r ¯¿Ä×~:øþ × ´»¶HíZÑeiˆŒðŸã#¡ç)7a¤z燤+Æ>"\øjçáµÚü=}0YÇ{ÔEŒD¢Å¿ç2*Yzägšä¥Ó´ø¼?ã}SI×|3<'F6ïg¢[< 6¸Ã²–nyÆséK¸ûJXêún¡+Ga¨YÝH£,°Ì®@÷Ö¼s¤øÂÈÍe*A8šX¾Ë,©ç|ŒT¶ÐsŽ+Ílô½Añÿà ôKKKÔ,®ÆÞ0žî‚ØëÉýjo€má›XšÆé,#ñ„7—ŠQÐ †V=qœ`оK\ŽkÙ£Û¨¢Š‚Š( Š( Š( ¡½ÿ9ÿë›*š¡½ÿ9ÿë›*͹ÿ‘_Mÿ~ÏÿFÇZõ‘sÿ"¾›þýŸþŽ´®’I-¥H%òed!$ÚcÁÁëJC+˜ežhb•XH(9)‘‘š§¬ýëúù_ý«žø}§Þi×úüzŒn×-P3ü«Ðé ê4ùÚÿ×1SEs³Ï R£MD”ÈÈÈ÷‘ÿ »_úæ+“µÓ5 ~$j—¶…ì­PÂ'” av<±ò ôǯj¢NŸÄ_òÔ?ëƒÿ*Ù¬oÈPÿ®ülÐ…QLAEPEPEPEP)wþºúêŸú®ž¹‹¿õÐÿ×TÿÐ…tôç¾8×®<%âË]NæêC£ÝYM ß¤è ©í7O¹[ýOÄ6–Ó¯/52Ú¿ŸuxÐN‘܃€é»°P 69!xë^»¬èúvµl¶úµœ7p«T•rôÝ_DÓ5‹HíuK(.­ã`È’.B‘ÐJ@yÔwÞ ²øoâùonîV[F•¬'–dyÖ= €ìŒFàsø]?…4£Ñíîo5íbêK«%Y<래]pÒ:ZÖžÑ,ô«­6ÛL¶ŽÂé‹Ï ¯Ë!8É>½]ºÓ,îôß°\[£ÙíUò¹ Œ=0(ëqô8ï…&K¯ j÷WsÉ ¾¹‡Ì2~ðä¸sœcšÉøq ÁŒ<["Þêm%®¢Ývì²n„ °'’3ÔûzWu£xcEÑgi´­>YLyÏ'½^³Ólì§»žÎÞ8f»q$  o{ù -%°Öµ«VÇÁ-¨^V×Ry.îƒþñ¬×çBÇŒîÜ#¡í^Ë\§†¼-q§øQ×u{øïõ+¸ÒÑ[ù+JI Ny=s]]©æôhn|]á e»ÔSílWN?tܨ冒ºþ¼†ìÏ4³Ô'Õ¾^^j7-w)¸¸t­¸í[–Ÿ@  ¯CÖš)ü!zc)"}‘±·}ʱk éVš3i6Öñé¸e_î$·ä“OÒô];J³’ÓN´Š i -Ž #ôš¼ZýäÊ>–)<£ù.Œ¬tƒ–·«3EÐ4½Íþɱ†ÓÍÆñÀ8Î8üMiÕÉÝÜ„¬¬QE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@!¥¤4J롪ºOK¯úíÿ²­Zºèj®“Òëþ»ì«@ÝÛ4’$Ð8Žá‘ÀõzT/ýÂùr˜ ŒðÍb=²?ZТÌhqë: é«1¶FŽ@¡¶20eʞșcák¦»¿›\Ô ¾KËO±¼pÚyg9þ6ç“]]ÉèÞÔìíÛOÔ5ÅÔ4_³µ²Ú½š£ì#h@ÜàqÐS´jºb%•Öº/thá0Gjöj®Èœa]UÄØx7Q·“H†mu'Ót©„–Л $  ¦Mø8®Úµyá.´¯Ù6£¬Îfä¨Ê¨Æ7|ßw¯ÖQ@XÁ—@iu½6ùîŠÒÎKF‡ÊÿY¿oÍ»<}Þ˜5‹‚õ†jÚêI¥X]-Ì5ó)ÈO3NzíÍwP9GÂ^K­íÔÌpj/ë· a™ Ù'æhùHüjK ^}¯PŸXÔá¼7–‚Ь~@U矾Ù?1®ªŠã!ðŽ¢ú(Óoõï:;(ØÉšÆöíùXüÄ?`G±áí;V±3cV‡Q-€ž]’ÛíúáŽknŠÁEPEPUníšWI`qÄy Ädz‚=*ÉàdÕE¼yë{+™ãìë±Aún`hŽ—ó¯—!‚<3ÆÅØlÖ¢Öôh5_Ýi.Æ('„ÄFJú|UŸ´ÜÿÐ2ïþû‹ÿ‹£í7?ô »ÿ¾âÿâèžOjó˧Iªë±]½•ÈJXˆ²¡ íáÏ\ç?¥.§àõÔnµy¦½e7­ ”ˆ·’/ºÀ“†ç¶ti¹ÿ eßý÷ÿGÚnèwÿ}ÅÿÅÐ9û Þ›÷¿ÖõŸíÕ·{hmV…Xrv‚rNz—þ8åð]¶wvÒt@—(t9V “ÐÅmý¦çþ—÷Ü_ü]i¹ÿ eßý÷ÿ@_†/­£â _ûVâ^ p¶ËƯĀNX€J£‚õ×O´:â>“cv—P@ÖCÌ]§!<Íý9ë·5Öý¦çþ—÷Ü_ü]i¹ÿ eßý÷ÿ@Xà5ûkËM[\‡H‹W ¨‚eŒX,°É#.ÝË&FÞÙÏÒižš Ý òK†ÂÀÚ<7o$/;³Æ6úVßÚnèwÿ}ÅÿÅÑö›Ÿú]ÿßqñtKú;èãPuöut÷#÷{6nþ§=:ÖÅTûMÏý.ÿ︿øº>Ósÿ@Ë¿ûî/þ.€-ÑU>Ósÿ@Ë¿ûî/þ.¥¶¸YÃaY×Ge>ô5Q@Q@Q@Q@Q@Q@Q@ËÒ²5ºk^^•‘¨}Ó@…m2¨{»Ó´î¹n­TÕ´û][MžÂþ?2ÖuÚê©#¯r{ŠÉÓ<å—ÌÔ|Õ ôÛ€uÝšÝÿ„~ùý¾ÿÀ–£þø?çöûÿZ€*èºe¦‹§Gc§ÆÑÛÆY€g.IbY‰bI$’NO­ŸøžÝúv‡ÿB–­Â?üþßàKSG‡-D†Awz€¥¾ÒÙ gõ?Já]\¬0Aî+0h@°·±m¨³·M>XÛŒá€ìy5¡ÿüóû}ÿ-Gü#ðÏí÷þµ—yá÷Q…æa=è!¼ù!RùjèÓ¬†¢Ú€µ‡íÍ”ÓíùÊw>•?ü#ðÏí÷þµðÁÿ?·ßøÔDèzQµ¶¶:}±·¶K ~XÛƒÊ;Ô§…ôVóízžcuu€¾l°†lœÖ¯ü#ðÏí÷þµðÁÿ?·ßøÔ}á/ßÝKs{¢ØOq(I$ˆl£'Ø? Ö²¶·±µŠÚ΂Þ%Ú‘Æ0ª=§Â?üþßàKQÿüóû}ÿ-F P×Ncµÿ®ßû+V}t áëMêe¸»”)ÈpÄg¦Z›ûÃþšßÓJÃLâdÑtÙý^Ê[õ+t ß0CzñÅ*éz½“­œ¬“Ë·!î—Âú q]¯ö‡ý4ÿ¿¦ì;úiÿM £†:’t¿ìÓ§Z› îò cf}qQÃá­:[´»E²•·¼1±›ŽHõàW{ý‡aÿM?ïé£ûÃþšßÓE˜]^•¤iúDO—g¤nw2ÄA>õzºoì;úiÿMØvôÓþþš,Âèv‘ÿ »_úæ*åQMÞ4—Wª£€Ë`S¿²!ÿŸËïü jb"ñÿ‰øîвrFüëf³#ÒmVT’Igœ¡Ü¢i‹€}pkKrúΘ…¢“rúÎËê?:Z)7/¨üèܾ£ó ¢“rúÎËê?:Z)7/¨üèܾ£ó ¢“rúΊånÿ×Cÿ]SÿBÓ×1wþºúêŸú®ž€<çãDšÌz¨Ó®íí­$¼·ŠF!ćt€•aòž24ÿÛjZÂËä_²3Á8I&\¡b~FÜ\7#œ×SâßÅâM6+9ç’Kˆ®7 ’Œ~•Œ4 üG¦I`š”¶VÓFṈ̃ĮdÛ¦9üé+ê= ÝŒ:€®|àÐÚyñ”™Õ•Ägp9={Õ+/„þÚë3G-ÓCk [%ʨ,Ol¶IôÍt^‰-¶‡&™}-ìm”®ÑªM»qÅdEàu“Â÷Z§ªÝßéòF‘ÄUZ§ ‚:ò_Jrëoërc²¿õ±Ÿàÿ]ê~'Mý´{£-¡¹K*àÉ@(Ù=yÈöèÁ˜ô5Íxw×ú^ ·7ž ½ÔcX<‘ ±F«œœ•-Æ?éXRB0h~CG‹j¾'ñˆ´Ÿ jÞ]­Žy­@©äÊþxQ)]¯ØäŒ8ã¾kKÅ_ît­bAýyi¦Ü,rEm ÄŽ¨H|¡|¥psÁ>ÙϬ¿ âHt›Hõ«äÓ4»Õ½·´Ø»A¸)8É3I­|3MKNÖtä×o­´íRéîæ†4C‡lgæ#8ÈÂ’ºþ½ìÿ¯RÜëÿð±ü;sdÍïR,˜ÀUÉœœ t©|Sã›Í'ÄvZrhö÷ÆÈ^\\ê˜mÎI]‘ó’Ù㜠ßñ…¿µot»ëmFâÂûO ‘Í«nVAã°ªú¿ƒšÿT²Õ`Õî­5X-¾Ë-Äh¤O\2‘ósÅ-øÄM⟠ÛêrÀ¶ó3¼rF­¹w+%O¡ÆG±«~*:ŠèwM¤IoÂÆÇtêÌÚz`ŽzTÞÓ¦Ò´¨­.oæÔ&BÅ®&UV|’y ã8ü*ÝäæÒh ¡BGQ‘Š%ªvæyÏÂ+mF×Ák¬^Ïou-Õ’H®RÎÊûØŽ§ø@þU7‡¼_â+¯ j¶Zdz:ÚÍu$P ûXŒÆI+¬Ðt´o ÛhpÏ$‘A€JÀn çŸNõ—á{;/¯†¦gº±û3Ú¹] çÄÐïwa«®ã]wí>þÛ±ÓVÛ^‚I­ÑÜ´xO1w†Çqœw5‡eñ+Ä¥Õ¯,´a··½‘cówe“fN7§>Üõz7Ã屺Óe¼ÖouÓ#x¬£™QVË·°ç Ç>•]>Y¬3!ÔîËIeŽâ«ò¤R{“ÇáO¯õçÿGYuuª¦¿eµ‚K¥I‹£ ºç'?JÖ¬›­ç×ìµ5¿¹-£hͪŸÝÉ»»QZÔçß4˜®¼-6¨.o º°Ba6÷/ĸ)ù¿¡ñãËÕ|©ysiáùÖQy2HÑ©F¾Pw^FNî¼c9é]Wü1?Š,ÖÑu‹› R¤K1£y¼‚2XqŒ~´Û êrh¶v1øšþ rÍt‘G¾UÁH#=)Xw9‡ñÙø£CñŸu{y.“¦j¯¤¢áÒAE?3g$e˜óÛÎû½ጂ¹_xòóÔTr%æŸâ-2Áõ+›¨ZÆîI ÛIr=¹Àí¸ÒÑhºœÆ› õ A.vù‹´ðqÓð«Õåvz•õ¿„¼-§iÆê7¾3o’ÕPÊ2Ø]ä œþ@×Uàiõg[øuh¯„QÈ>Í-â"ÈèG9ÚHàÐ+›ú­ü:^›s}u¿ìöñ™dØ¥ŽÐ2pZž hc• ê0pGzòýJMK_ð¿‹59uYà··76Ég®Í±‚>lŒäóZÞ'½šKˆ-4ËÍn;˜-²G§ÂŒŠà¹|z;y.­â¹ŠÞIâK‰A1ÆÌ8p;⧯4g¸×õÍ%ÌÖ·wZt’4ðX¢“ŒƒK¬Þë3kWú}”úÑM2£W³Ž&3HÉ»|›ˆöZsÑæ‘!‰å•‚F€³3zÊÐ|I¥ëÏ:éW?“‚äÁ".pAeº™¬ËÛ›Ùþ\OªBÐ_&3FÀo=+ê ý3Ãz_Ùõ«üßÏk-´˜Uº…ã¾{úP=Šà®“TYx^rò%K'¾–÷j¥ýà@ƒŒ2{zU uXè×V ¨Éö«Mf<Þ¢¨y#v^HÆ3†ý(ÏL¢¼ëìÚ´ÚÖ¯¤'‰u{t¸ŽM‘äî†8äq[¾}CY²Ð5‡Ô8šÓ7ªƒl®GÞÏQŠê(¢Š(¢Š†ïþ=fÿq¿•O§È>×þ¹/òßüzMþã*ŸNÿ}¯ýr_ä($×ÖÝÃk5Ì s>|¨™ÀwÀÉÀêj–¥âMK¼KMKWÓí.ŸažáÛ'œòx®Z𽆟ñkš¤r×wSÝÉ#K38ÿP@PE'µ…ã=2}'þ Þ­áùo­¯­ÞkMALN Ä;qó˃ÏûEô¸[SÕµ?èZUжÔõ:Îà¨a÷(ŒG® Î(oèK¦&¢ÚΜ4÷m‹rnSËfô œf¼ãÄr-¯ÂÏ<6vâ=JÚÖÂÖâhU¤¤P7Ãwóéš’o išoŽ<᯲A>“g§NV)‚°gP£ySÕ<ûÓjÎħusÔ´ûëMJÎ+½>æ«YFèæ…Ã£PG©_xE°ÔÂûWÓí¯ŸnËyn$mÝ0¤çšå>G­ïŒ,ìð4øu‰D!~ê±Q½ìã¸á¦iº†¾(j¬æò=Ræ4»m­"*"mº®WÔ›¶¾Wü¿Ì¤®íçcÚ/o­,"Y/n`·˜*´®zOz³^7ãï[j~ ѵÝ@Ýi*XÄÎm‘þñIe^ŽH'Ò½cUs“xÈvºÀäq”óN^íïИ»ÛÌ­oâ=çT:e¾¯§Ë¨ÀÚ¥ÂF:ü çй5õ¤7pÚÍsw3çʉœ| œ¦¼6ÏN±µø]àZÚÚÞ-iõ;i±æO#;eYÇ-‘“×µuzç…ì4ÿ‹>Õ!{–»º¸ºyY™Çú‚€z(É8õ§oëä=BŠ(¤0¬Áÿ!«¿úãózÓ¬Áÿ!«¿úãózµERQEQEQEQEQEQE½+#Pû¦µåéY‡Ý4CÂÿòŸþ¸ÿì» ãü/ÿ!éÿëþÌ+°¦!Ö4gv Š2I8Vâ½'^šê->w-nF|ØÚ1"@‘ ûèH 0ã Ög,®õmcGÒçG¡];-ÊÆäI3wØ}åõÎ+ÇwZ>§q¨èY²jº-˜¼·›`òÔ|Ÿ'\•;—#ã<(Óª•¦«§Þ_]YZ_ZÏyh@¸‚9UžôÜ åsïSiæå¬¡7ë]mýâÄIP}‰¯±“Äöß–Cÿ@¯*°Šé<—¨Y¬5[7·º@Õ¹È)_pì{—wgªið_X•–Öu”+‘ô øÕ¯-?¸¿•y]úÇ.™á])´äº[@Ûlîź•X知Tšâö_ é0Íxjßf,'!ÊÒî3TÖ¶z\õÿ-?¸¿•Zq*ñMft™ÎÅû½¨ÏRòÓû‹ùQå§÷ò¯»}cQ¸Õ/m¦¾ûl7 [ôŠ8€# Qˆôï^½bÒ½•»\¦ÉÚ52.AÃc‘ǽ.>¶$òÓû‹ùQO¢€9K¿õÐÿ×TÿÐ…tõÌ]ÿ®‡þº§þ„+§ +âŒáðì–V±Èéu4Éæ²É*¤<–o”c >ãÒ¬ü1ñ þ&ð¥½ýëÆ÷LͿʉ‘')ß”¯#ŽkŸø‡ákúÔ“iÚ•Ä6ÉjâÜy‘…I_Ë·nq³qÉ9çŒWsá½1ô}ÚÆ[¹nÌ*I(PqØ|  #³¸=ÕˆüKâ ÙEs¨[Ε`†(P¼’¹ÎTu8þ¸_øNõK¿øN$³·6Ñé°½¬wPmHU‹‡äd WMñC½Ömôy´ÁÜ麄WË­µeÙŸ—=ºçð®bO xQ“Ç—Ö–¾³k‘$ùáUÔî8àüÚ‰^Ì¥k£¨Ñ¼ect’E;³Kef³ê(£ìð6ÀÌ…³÷ºœvj= ÇúN¯}ij–ú¤—ŠÍj×vÆ%¸ ’v×€OÓšæ´ÿê:n©ø~Åaú¾œñʆMæÎå£ÚÅCgr±äÿúéú†¼G6¡á&Ö­¬¬í|; ‘«Ã9‘§Ì~Xã:¾¤t6´ÿ‰:-à³s¥mmwuö(®.möFfÎg=Ià{Ñwñ'F´“RûM¾¥¶qök«¦·ÄQ6q’Ùû¾þãÖ¸OiZ'áû[xm"Ñ­µo· ¦“2ÒV;6zŸ\÷á§k¾$°ñƉ¢Áiö Í^HÚêi0ÐÊ\ìÇÌ8ã§CM_‡üoúûÏnV¡”åHÈ>µÎøƒÅö.¤šsÃ{w|𛃠¤&VŽ<ã{z ƒõÁ­ûxü¨#9Ø¡sôÆëš6±oã£âÞÞðI¦†Y|²‡ypÙÁÈ'ð©—×™CÂÿÕü¤j:âI.¥©M2[ÚYúYdaÂg²€Ií[Kã[KÏ_j:U¥í̶’<[yAe†E\áÔ‘Ó¿q\¿ÃÝz ³IjºT÷fh!»x7¤îI+*a†;ô5ÙøCB»±Ñu´¸Ób±»½‘Ø(½{“.SÝßœŸéCÙª3¼âÝ^ÿÃëzõ¥óGöhçH ±bs‘×býº[xßMyo`¸‚úÒêÒѯšÞâ²<#«(Ï>ŸZēšÏü)¸¼9‰¬–Ém”ª¶]ñcéï#ñ}ÅäZ.›£i’è²é¡-¥Ü娮¸ç¡ç®¡½ì w;µñfœí (µ½ßdÌ~ˆ_æçŽSð­þ°þ$Ö4Í^òÞé-#‰‘ãƒÊ$°$ç“é\æáßIªøíÖVPÚh[ÄÒ%Ææ|ÄÑ‚£ãõ®‡ì:ÖŸã‹Ëû;;{:ù!I¦ØñmÈ'çƒOíy ìù”¦ñ­ì>=¸Ò‘¨Kg§š‹*]›~Òà—û=ë» žÆ¸½WKÖ¬üzuÝ&ÎÞöÞk%µ–7ŸÊd*åõ®Ðd‘ƒéG@êÎ+Ä~-¸Ðüqi§Ë÷67-2Cknd˜È¯ÉëÂ…Å]ƒÆÚuæ§jZe¶£¨[ß;F‚ÖÜ»#.wç ‚äõª"Ó|A-u­ÎÎæÚ;¶‘fŸc.XcŽÜs\¤õ˜t¿ Ã{®§oe=ÕÅþœe)+nŒú6ÑÏ\жþ»ƒßúíþgt×/t½?Q²¼±´3 ÔlÚ%'ŒgƒÓ‘ž+•Ñ~ëV¾×­’ßN°½ŸX]NÎÞ'Ý… „8*F1]^§‰µï ëÚ~¥¦ØZËqja·]ÞÌ;ŽLT»¸»n8î®O¥xÒÒ{YRUž{›¹¿–Þ0ÑDû4yÏßëòþuu<[§9Ð[€u¼ý—1ã¢ù¹ã\¯‡|©x}nt>8WÃú•‰Yâó‰6—&=¬S?yXõüê-?þ&}GÁúÎÊM xžD¸ÜÏû¦@TcÜ~µOÈKÌÙ°ø—¢Þ-¤­¥mkuuö(î.möGçg ÏRxú×o^á +]ñ7„4KKXm"Ñ Õ¾Ú·m&d1¤ÌvìÇSëžâ½ÊŸAug#ãÍ¢Íe1\Ä&ž$7_gÄC66gp!½ñÞ­øÅö¥o§ËõåôÑ™„P]#ù؃ êkø¥«X\¾™¡ÛÝG&­ý£læÕ\Á²Gn9«>"Ðõ»oˆPx£@¶µ½gÓN›43ËålC ppsɹõ©WüB™µãä¼/¥¹Òu)¦†X〼ìê¿*ìC=+§ð¿‰,™qw ¸Ô-Ró?h†Úà¤r’0I\“ßgRð¥íعK«ûI¼•ÚÚ}žb¸9ú×CEs—^ÓçÓ4ë4šöÜié²Þh&)*®1Ýø¨ïücw$‹ÍRÞæ8VžÞé‘åUé¼ÿ÷ë]=BëK†ëE“L™æh$‹Ég/—#Îï_z¯y YÝØiö’´þUŒ‘ËWÃnO»“Þµè  _xrÏ[’ §’êÚê ˆî-e1Hõ\Ž àp}*ðŽ˜šLz}¡bŠå.÷ù¤É$ªÛƒ3O5ÐÑ@cC´­þ¢ ¢æö‚S¿«œcÐòj®Ÿák 4§·’ïþ%°˜!S1ÚTÿxt&·¨ Š( Š( nÿãÒo÷ùTúwüƒíë’ÿ!M AƒU ¨TGmv!÷UâÜ@ôÎE>ÿFµ¾Õ´ÝF3íÌ;[ç]§#¿­xJ×5·jÓjQîV6R\·ÙI^™¡þ½ó[uùý‹ÿÿû*6êóûþÿöTÍ[@±Õu-.öíd3i²4¶û[ ŒŽüUoøONñ0µ7Íuö¬^‹IŒ2¦F ;J¹·PÿŸØ¿ðÿ²£n¡ÿ?±à?ÿeNâ±€th¬¬ma7‘¥¥Ù¾Þ“óLz´Õ»~U¯ðçBÕu‹­BàßFnÊ››xnY œŒ¼c‚H?þºßÛ¨Ïì_øÿÙQ·PÿŸØ¿ðÿ²¢à&µ¡ÙêúbX\«­²::¬M·*>œ Šãöw%¶×%{ƒw»[* 1FÎr½úþƒÒ¦Û¨Ïì_øÿÙQ·PÿŸØ¿ðÿ²¤=¦ü7Ðl5H¯"ûk¤3ý¦YnYíá“’#< È®ŠÿFµ¾ÕtÝB3íÚ­–]§#¿›uùý‹ÿÿû*6êóûþÿöTî4è¬Íº‡üþÅÿ€ÿý•uùý‹ÿÿû*Ó¬Áÿ!«¿úãóz6êóûýøÿì©öÐyEÝÜÉ,‡.çŒú; C'¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(9zVF¡÷MkËÒ²5ºh‡…ÿä=?ýqÿÙ…vÇø_þCÓÿ×ý˜WaLF~­¥¦¥öfk‹‹ymä2G$ T©êèÆ±¢ð>šº¥þ£4÷·—ÖÿešId¦W  ?(®¦Š+GðÅŽ“â cXµ{–»Õ Ä’nA´`mºÖ寷ÃM+YÓç[©àÕnä¦I²ÑLzYxùºõàuª>ø¡ëš¼:mºj÷ScV­rH÷Åvuq’’ºfr„ í%cžŸÂÚÇö§§îþÓÇÛ2dýî:gž? Sïü7 ê6¶wštrÛÛ Žel¢Œ×õ©ªÞ¦¥Þ_J¬ñÛBó2¯RI ~Uá¯éZÞ NîlâÒÒ^]•qžœq‘ùÓJä^ƾ‰¦éÚ‚Ùi6ÂÚÕX°° 9'𽿝û_÷ɬ_ø–ßÂ^—V»‚YâI#Ë‹‰v :ûšÛ…üÈ‘ðFå ƒÛ4yŒO5Úÿ¾Mjÿµÿ|š’ŠÍ_ö¿ï“Gš¿íß&¤¢€#óWý¯ûäÑæ¯û_÷É©( üÕÿkþù4y«þ×ýòjJ(?5Úÿ¾Mjÿµÿ|š’ŠÍ_ö¿ï“Gš¿íß&¤¢€#óWý¯ûäÑæ¯û_÷É©( üÕÿkþù4y«þ×ýòjJ(?5Úÿ¾Mjÿµÿ|š’ŠÍ_ö¿ï“Gš¿íß&¤¢€#óWý¯ûäÑæ¯û_÷É©( Eó\á°@þïXZ„|9©j2_ÞéQËy& ˵Ál ã¯ÒQ@wÑ'}5¤°LéÄ›MªÊ"'®þµš|áC$2$…üÈòò­œçõ®ºŠæµOøkU¼k«ý&)§fÜ\«ŒŸRñ]¼h¡T0P0ÓRÑ@ù«þ×ýòh©( Rïýt?õÕ?ô!]=së¡ÿ®©ÿ¡ éèĺô´O"yóK"Æ,¨ŽÀ Äd äÕëÝJÊÁQ¯ï-­C‚WΕS8댚à~4\Ù6•ijÑ´·ñÝ[Ü*¤ äF%°@ã€*‰>Ãâx&uµ7vB[†Üð…g#Ž}})¥t&ìw±_ÚKfnâºí@$̲€¿6qÅSŸZ†M>kÅ«I•k:A>¹Àã'ð¯%¹Òçñ…¤z}Ìšt^"H£+¾Û3Ærx÷¯@ðTþ¸kÿøF´£bâ4·ØM¾ñóm gý3RÕÑMYØÖðÞ½·¡.¨Ð½”YpË;—i ’AÆ85¢/m ·æn?Ôþð~÷Œü¾¼sÅyß…îRóÁ:·‡|›„ÔÒ¥1K (9fŒî…ax·ú¯Ã;K;[µ–Éf­Ý&û3® #ä~´7ÛÈïæz'ˆ<_¥hoe’+™®ob²ò-äRñ´€Ys~•±&¥§[Ýý–KÛH®˜ƒä´ª®sÓåÎy¯±¶¶µÐ<e&“pÆâîåÂA`iöŠÉi »`Š ŒœuÅpÿæmoWÒüjçý4ý¦ü¯T¶CÈ?ï9àóMö/ÛøâÙáÐeº²žÒ-\Jè󲨉d3’xÈäWF5+b×¢öØÙ¯YüÕòÇo½œWã6Þóž ´¸²óìc–f(cÜ‹¶0=‡>µçú¶—=¾—ªF¶ËÅÉ=ͪBJµŸ”»€rî=~{Ro§õÐ-ý}ç¸ké¿ck¿í ?²)ÃMç.À} gYkeü‚;FÎæB»öÃ:¹Ûë€z{׌ÍeäÞ)¸°Ó%Ã×7zrÅ ¶(ŽË óH™Ç?…SÑtAmoH³’ËUºþÚ‚ÚUˆÆA'1sŽFEþ¾WÝ-uK ÉšKëYå\îH¦VaŽ¹Õ {Ä0é:Ž•aäKsw¨MåÇXʨå³Ñ@¯"ð¥»ëß"ÙßÙG4,äFC¬Ò—*ÜääŒÈ®çÁþojž*“泌› 7=<µ?<ƒýæî;j`uoâ-$’7Ö4å’3‡St€©÷â¡Ö™=ê95M>+5»–úÕ-XíYšeO lâ¼÷Ç-§Üx§Ãz¶¯§Ï¨èg”.-ÚUŠS‚¬ÉLãŽ++Äé¥5÷õm"Uð}¼÷bæÐY«+`Fæ¹ûáˆnÜúЯ[\Cs Ëm,sDÝ6 §ñÏÚø¾Òáõ·[{“g¥H"’áx‘ÿˆ 'f¸ U—Ã>ñ5外é¨j¯lñ”ݼ* TàœœqКïü?áK[šݙ$Y7N’²4²1Üä²y?˜ 𧌴ïÍiãþµn롪ºOK¯úíÿ²­Mç\Ï«ßkGqÿ>­ÿ}­Y¢ÊÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€+y×óêß÷ÚÑç\Ï«ßkVh  ÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€+y×óêß÷ÚÑç\Ï«ßkVh  ÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€+y×óêß÷ÚÑç\Ï«ßkVh  ÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€+y×óêß÷ÚÑç\Ï«ßkVh  ÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€+y×óêß÷ÚÑç\Ï«ßkVh  ÞuÇüú·ýö´y×óêß÷ÚÕš(·qÿ>­ÿ}­uÇüú·ýöµfŠ­ç\Ï«ßkGqÿ>­ÿ}­Y¢€)I4øÿVÿ¾Ö²¯å›iͳø­ézVF¡÷MføM¼Ap2ƒÈþð?Ä+³®?ÂÿòŸþ¸ÿì» b +È>!êZ’ücðÖ•§}›sb[{yÚ%bÒÈ’¤ª:ñŽ:šô9|3fñ²­æ°„‚.©s‘î2ô¹Ebø1¦ X}¦â[™‚i¥9wÖ=ÏçVº%ï‰~$øº¼Sâ[,^³Ãe}åÆ›Ð“òA掶—=~ŠñÍ3â¡ákÖ¾$uÕuM#PM6ÚmÂ/µ´Š fCŒ'[š’ŒrCcâ#ªhÖñßéV‰v‘Zj sÁÛhb—¯4G­\\Cm™s4pÇœn‘‚ŒúdÔµâ?µ­S^øp.µÍ.ÆÆõK6G·¿[¨Ý<Á’XŒg¿­høÃÆš¦©ðËÅÚ¾‹Øi–ÖøÓµ$œ¬·Õvü«œ€sÏ¥; ç®Ñ^aüA£xfŵÆÚäÚé±Cæý­™rYب)Œdä•~$_ØÃâxìÆ•cý¢–ÑÝ c¸‡¦D›FìŒb†¬ Ý&zUæz'Ä]dêx›Ã#LÓõ¶òìîà¼Ì 2+€£nF*ôʰ^áERQEQET7¿ñç?ýsoåST7¿ñç?ýsoå@ZÈ·ýz/þ^¢ÛÄžÇnBù ÀàtîúB†Ñ¬•†A·@GüW4>øu2"Žþ$$‘_ÌŠ2s†WŸÁË¢¢ícÓ˱ÑÂ99+Üá¼= ‹Åº盲:ÿÓ+Òü}¬Ùè>Ô/õ)n¢·DÚZÕ¶Ë–8c“Ö¡Ò|¢izŒ7Öñ\És|¶¸º’m„Œ±ÁÅlë:]–µ¦\iÚ¥º\ÙN»d‰ú0«Ááž—³næxüZÅVU²±ášÖµa©x‡H¾—Ä`¸ðü÷‹¹x·RÁV^œV²ô­%5§ø1i%åý’>r<û†‚U;#à:ò>•ìšÃ h7²]éÚk‹‰ 6ÌÓÝM>b=SÄcÚ “á_ƒäЭôwÒä:}´Í<1ý®`ѳ ­¿pì=«ÐR²·õ×üÏ5Æîÿ×Oò8/‰Òxˆü6½Òõý%­´ø¼ˆ¢¿þÒÜHþrc€q’Iî* VûWø{â}VÏM×5}JÙü1&£z¥É¹ò§I0sÈ=ç¿A\Ôü#¢ê~‹Ã÷Ö.“Ͱ™¤l ¯ÌqÁ½>o èÓë±kYï¿ŠÌØ+´ŽWÈ$’…s´òz‘š—m~‘Zéýu<²µ ÜøV·ñf¯ªI®^Ekwk}vf†D•73F‡î*0GLþÌ_.¹/‚|Kâ˜|]âHµ -aíc…o"7 YgkÇì¾øiá?jé©éZW•ya½Ä²ˆ÷c;تô«1ø Ñ躆’ºy}ýÏÚî"óäùåÜvwd|À;¯ëÕÁûÂÏOë¿õò<ækÍ[À~&ñ…®¹ªêÖËáÙµX«?Ú)£ÏCÇÊ}+À ã‰u kÒx’îÊóêO}©G5«Ç"ç1Æ>æÒx±ŠöÇð¾'ˆ[\’Ì>¤Ö¦É¤iƒ •)½ºã5¡|1𞃬Ǫi:l–÷‘±d"îbŠNs„.T˜ñŒ ˜Ýoýoÿî×oëDvtQEQEQEQEQEQEQEQEQEQEQEQEQEr—ë¡ÿ®©ÿ¡ é똻ÿ]ýuOýWO@å¾8ÕõI~#Á¢C.§žšwÚÀÓ&Š)^BåNã#(* œûU=CXñ‘ðûJÕ¯¯&ZÜØ]mö¾|Ò‡ ·×©^gâí(5¯hÚuĺ|q£Imð"Ä2ìO­sWž Ö­´©ôoí{¼ÂRº;j,ß¿ŽÝ£Þ[v0'õ¾_ˆKKüÿÓh®ÆóÝ^øÇCÑSZ¸Ñìg†[‡’ÙÂ<ì¤aƒœ‘Þ²üI-õ®±àß Áâ+óe¨Iu%Τ%Q3y@2¦ð»pKìz{ЧÉríóiÜ»†p}G½W¶Ô¬®o®líîb’êÛtJÙ1ç¦}:óÿ xªm3Þ,¸Õ菉ƒF¿–Þ ä É*€¥A#‰-Ó¨®ƒá¾6™ ­Dí]JCyvÝÃ7!}xÐêwöš]Œ·š„éok7Êç ¹ þ$V%·Žü-uuµ¾»c,òH±,k&IfÀ§"¶­nìµH$û4Ð]B®c}¤:†•>â¸?‡6Öíã?3C1êqª|ƒäÆ=(JânÇ£Ñ^g¤Yê> ñ§‹¢›Ä­µ­ÄQÛÃm0U]Èõ¹Oø—[¶µñV·¡Ü_­¾—}䇺ÔUPº‰C•ïËr}¤šv)«ïU[Q³]Mtãq¾h¼á~b™Æìzf¼ÓÇw:Ä:¼·òÜêË¡¦œ¬².âI`œ–hœ‚Üt=¥:úÜx‡ÇÚ)±Õo­#“B3}¢iTÈ6‚qÇ<œS_×ãþBz^ŸæzQ´Õôûµ­¯ •`Ã!W+’¿Zó­Å÷Öß Ööæg»ÕÞâm:Õ›—šQ+F„ã88ô5µÿ1ÿ„ ×AŠx£œ2Í<NYÙy1'† ãÒ•ûÖÑ……ý®£n“ØÜG<.»•‘²õ«5Äü.ð×…4·Žöâ9æ•WvW2)àÈ1¼sÇÊ1ÏZí©ˆ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š) -!  W] UÒz]×oý•jÕ×CUtž—_õÛÿeZ¿ERQEQEQEQEQEQETw ÉÝáI]Ý3Žõ%EtH¶”…,v*õW†þþdµ‚D8d,yaô5áH!½ñ7réþ'·º¶‰‚¶¥Ÿ)sÕAÉÍ_›þ&ÿaï[èöþczyÒp? þtËž*¿Ôt;OM-íLòϱ’ô3®ÆNxõ¨-uKJ¶¸ºñUÞ•%ª²¢¶› ¹BN>|³`S~#BfÓ¬°¹¿…/#yb·ˆÈÛrp+"ÚöÒ=3PµÐ|!ªZÍu…+sbR7$ãsô$Ð3¬“Äz\sj½Î„k%Ä…NÄ 2î„ã·Zç|AâÈu j—:×pMoåþñàxˆÜà|»‡%ð–«ok¡êö³ÆˆÂ;«p†B©ÉÉãõêmõ{GÔn4á+5ݬ)4 ©á[89èz¥/‹tx´;}Yîìw ²FÅäl‘…LdžjÁ–â};Äw«é:¬Öº¥„Q&Ü»Àé»åuŒî™g¦jVƒu/ì۹Γ$íqf¨<í²PÁI䌎3Þ€:ÄÝjºýÔ·r®›j‘°YÓË|¤·ZZ/Št½fèÛYÉ0Ÿg˜©4/uõ]Àd}+’¾³¿ñ%ŠÍ®Ÿ}jnã‡È[¸Ì-!NHéœc>õo°C}ânæÓüOoummK>R窃¸çð  /jšä~!‹NÑ¥Ò!F¶3—¾ŽFä0ùXzÒéG¨èVú¦«b·&]ðÙÚ³GpÈñ%1õæ£Ötxu_[CM{›%±uó]3bÃŒúâ{¥‹xb-6ÅÒÂØN ‰>H^3é“B#ð¾¾¶¾—Q×/$p/'Œ; Ìq+*ª€2xŠÒ‡Åú<ºmÝïŸ"EjÊ“$²È¥¾èØFyíÇ5Å+Qÿ„v8N¨ùº^­-ÃÇÈóÆÎçtMŸ›‡åVí­,'Òµ{Ë#År´æ‘n™Ûi%Z1»?)=;X¶ÖmäšÍnQö0šˆçèÀõ£\¯€æÕ%‚øjüÚ,€Z>¡Žr¸çpý 溪‚Š( Š( Š( Š( Š( åéY‡Ý5¯/JÈÔ>é  ÿôÿõÇÿfØWáùOÿ\öa]…1uã/jÚ¯ÄMĺlö&‘ÞáÝ™]›† Üþœc¾xêeŸÄ¦6iÚ:Ƀ´¶¡)û&·(  Ï ØÜiº ¥ô‘Iu~õ¢!bI;sÎ9®OxëMñ®¿ªøz ›=Q£8½óÌ‘„M£…àúõ¯N¢€<–ïá=Ìþ¸·“VKŸͨG«Éw2ñ'‹.,n5Y´v°·µÒÑÌIùÞ‹'ðý=–Š¿×ȇxOÃ>-ñ¯ƒåñ5΋¥yw°Åb²ùòÈ©µ›Æ¶qÞ½†Š)·vØ’²H(¢ŠC (¢€ (¢€ †÷þ<çÿ®müªj†÷þ<çÿ®mü¨tsÄžžBè"¨'Šô¸.­fe-·o˜:úU›Oùáÿ¯EÿÐ+ç}/@µ“M´yIwB¤«ÊÅNGuÎ?JáÆã>ª¢í{ž–_€XÇ$ݬ}#ogrû-®­æ|glr?¡«5àž Ò,ôÏhSYÚÃo#Îѱ‰vîSœuè+ܵ;è4Í:êúñü»khši[ÑTdŸÈV˜\RÄSö‰XÇ„xZžÍ»–h¯ž¾ë·Ö^1Ñ|C«j-$43n´óÄ‹hÙݨϠôïŽ+¥›Ç>:º»ñ]΋¦xzm'Ã÷²ÛIÒL.fXÀbT§¿zêéwÿ qõ²=‚Šò‹ßø—Yñ‰¦ø*ÏFTÔttÕ‹êÍ(* •)û¼óÓõ¬/ø–óű:»éº¶â;{Èíæ! €;H9*Ct4ìïoë{~bºµÿ­¯ùéExÞ¹ñÆq^xÂãGÓ´ tŸ ÌÉ2\<Ââe+·åÎzÕðÏ|Mÿ ^¥øªËFKmnÎK»)´ù%% ¨b’o§¨†z}äZĽM<{§è×·~Ô,oîžÚ?쫉â°2+|½°qÒ«ø‡âGŠ¢Ò¼Iâ OÑ“ÃÚ-ÃÛ0½2‹†F Åv dŒf…®¡ÖDzÑ^I㉺†•¯ÚhÚcø~ÊëìI{sq¬M"Ãóp=Ÿ19’:WâOˆóÝé¾ñ›é·t¿kY¬­Š3 +Ÿ÷2»²s€{Ѱ-O¢æ–8"if‘#FYÜ€÷&–7Y^6WF § Šñ¥Ó×OøMâ/jV¾"Õµ;&¹¸w‘žÕ”ˆ0€8Á8íYÍâCñCÃðxWû&ÚAá(e6—fo²¨ó˜¡NAÆ'•m9µû\—¬þLEK‡<¶A$v^/ˆw'Áv)–ÆÓ#‘ãÔÕ\³Â¶ù‘ãï/ÇR¥¥®GbžÑÒÖúÙtë³ß9–æ-¹Yõ$zÑ¡øsHÐŒÇHÓà´3$1Œn8é“ù×37Œ5h<ýµ.™Ú¯'HôëMäW !úžOâµ,­uÍWO¼´ñmµ„P²£FÚuÄ –“’pF^‡ži쇆´}>ò{«->gœ0•¼1ÉÈéɪö>ðõ…í­Ý¦“mÍ©& ÇAÛÏ :ÃðmäÚwÃ)ïQžâ{d¹•~Ñ#6⥰ <ãR]¬ž Y-`ƺÍ‚w¶ ÛùÍÈÙªÞ ðã^Gtt{_>9¾Ð´ü²g;€è@£UðW†õkÉ®µÖââm¾c²òøéœu¯8Õ¼Qâé^ÕV+{÷ZPÃ+ùû¥v¿c¸Œ8ã¾jî­ñ^{yõ¹­#Ò~É¥]5«Z\\ùw³”m®È™è‡=pi-¯Oó¹ëj¨ 0À˜ú”ž"[‘¯â€ÛÆYÉTRrp½>¾•¡o*Ü[Å2}É8úšç¾ ëo¡ørgµõ ’-­#ZWàc¿xéŠoA-M›½2ÊîöÒòæÚ9.m $aÌeºãëT®|1¢ÜÙßÚ\i¶ò[_ËçÜÆË‘,˜q÷À•pºÍž« ]xÃM¸{½Ctë/Ú®$¬P3;ã¨?,T’üDÔáÑ¥‰ôëS¯ÿl#ÆÜÊ˹X¶2­Á©Ú[øSB¶°’Ê 2Ù-¤‘fxÂðΧ!©-§‡4{9b–×O‚'‰ä‘ Ž'ß?ð.õÅÝxã\µMnÂk ?ûsOšÕk¹Öv ¤ÿ#95SFø“¨ª]Ýëö¶)§ÚÃzÒ5¦ò孈݀ǡ(¿õò³°ðO‡4ûø¯lt‹h.â.ÑÈ€‚…³¸LäÕÿh–^ÓEŽšŒ°ïi v,ÌÌrI'’k€ðÏÄ«­GWЭîÆ4¾à«awæKjvîQ*çCŽõ±«ÜMâˆ6ZE¤ò¥Ž’ÝóDåCÈÕÄHÿ¾ˆúv4X.o hBBãK¶ÞVU'o8æAÿÀÍAwà¯ÞK—:\2s¸zœÖŠõý^×^Ót_ÙÚ\_\ÆóÉ%Ü…cŠ% gåä’OCYñ?ˆtùü=£ýƒNÿ„‡V’l1ÚÙ#‹ Çv3¸©c@—ÐæÓ-tå²Xlmîé`„”Fpr7÷‡±®‚¹ xµõ /Z›Z† 9ô{™-îŒrf, ²“ƒŒõÃT¼¾´½ñ¢ó‰5Y|È`‘Ž!€d z9?Zé´½.ÇJŠXôëhíÒY ®¨1¹Sõ¢ÇK±°º¼¹³¶Žï͸uÈØÆOàX¹ó¾Ï/Ù|¿?iòüÌíÝÛ8çç¿ ®õçÞñF»¢ø{HžêÞÞçF¸ÔÍ$î÷-¾VU~xÆp1è+¤Ô;ñ·ˆ¾Åeg¤›i/#±‰¦2o2<nN03ŠŠk^Õ|Y ^é1Û›«Ížx&•żed傎I$•Í5¯õåq=?¯3¹Ñ–ãL–+O)t餸·‰ˆÄË1^„䓞Ʒë̬ü}¬êÚw†›LÓ좾Ôî.-¦K—b±Ž .Þ£‚pyäT’xúößúµÅðÒ­57Rù¯!‰ú6P(.Í´ð u´ ÏI¢¸?øÒïÄZ†½¥xcÔ4äÒF†X£HÒR@r§ú_†Nú·†uhïÃæKë˜% ;·ñm¬NTuÀŠvÎîŠã>Aöi"Øj^0Ðt ÒîÞù.$žgã1¢ eeÜàÄã##ƒÏµ­dF²ºõUfP¤ñÐWñÿ†4}Nm?PÕR;¨@iUb‘Äy鹕HSìNh°îXûvµÿ@ÿÀ¸ê­¢ßÚ\]Omá¡×NgqåØ  ŽçÆ §x—ZµÕÌšNŸkÇÚ‰9%É?—AÍmZøƒM¹¿ŽÆ)Ü^=°»É ‘°ˆô$2ŒcÈô¡jBŸÛµ¯ú·þÇGÛµ¯ú·þÇUßÇþ[}:oídÔDµ m+4ÌŒU€P¹È`GN{VÎ…­iúõ‡Û4›¤¹·ÞÑ–‚¬:©±X63¾Ý­Ð¿ð.:>ݭпð.:Ú¾ºŠÊÒ[™üÏ*%,Þ\m#cÙT~€V…©ðÜE{ˆhÈ###ƒZæ4ø»TÖ茎à³%Épª™ÉWÒ”Qëúô?ëúÔóŸøR¼Öíõ¯kPéš—ØÖÊà\Y­ÔR"ò>RF9æ¬?‚5+O—·ºµ£¶ŽÓµÂEb#[Ÿ3#Â`wÍwÔP$¬¬yÇà  °ñ.“¢êëg k°[ 2 Y˜åá…?Ý©5‡ºÂêº&¯ ø†+ VÇL\òÉd&Iã ¥†Óœúõ¯J¢šmI‘Û¬‰k;‰% ¸\=Î;T”QHaEPEPEPEPEPEPEPEPEPEP)wþºúêŸú®ž¹‹¿õÐÿ×TÿÐ…tôËx‡Â?ÚzôÍŽ¥q¦êÂmãEu–,ä)VãƒÎEW¸ðGö…Ž“g­êו­”­4ÑË»bI_0Œ.zcµgüMñ½ß…¦·‚ÊÝ.$”#•Œ%Q¼”…#ÙëØÔ%øs§ø6ÏV´†Ò[‹Ä—dpÈf`W8Ú eñ˜qŠZX}MDð ²øbïA:…ߨš>Ó g†Üª‡Ð•³áíûMkƒ¨kwz •UTNˆ¢#ÕDðþ¥ªIH–VÒ\A’Á¶¿çþñ¦°ú߆áÕntë»}v7eŽÕ6½£üÐî9ùH:Òµôí©y~*A¤ÙÅ®^&—¥Þ­í½§–¸>ॺ‘×ó5jãÀSGy©K¢ø‚÷L‚þcs$)rŸ”°$nôé]ÍqÚ—Ä] O»¼Š_¶=½”‚›È -/œgã>”ÀìmE\ç®wRð캗ŒtÝVîáÃO‰Œ6»N|öãÌ'§§¡¤ñŒtíR±°žÛ‹«Ôg-aó ׿½CªxãNÒôÍ6öò×RA)‚>Ì|ÝüðS¯j^c45}5wFÔÚæHŸMicP“xçò®Pøsgwm¨·ÜEusª ^ •U-k8@€¨<ëëWnüw§ZZÛÉqeª¤÷ÞCj|ù– žTdeºU‹oi¿Ö‘®­e¹â+,L¼•e=?Ûö#6?!MB[­ZêãQ¾ž ¥»tPq E 8ÇÛ‡6ËåMu5ͳ½ãMŠ1(¹ûêqÐÕzÇÇZuÝ„÷ÆÏT·³†ÔÝ™®-J+ þéîyàw«¾ñ,ì¯6©X’`×vÆ5en˜= õE1~ðuö=€OÞOedcµ’ÀeÁ ¬Àd…üªÿ‚¼=&ƒgv×· w©^Ü=ÅÍ©‰?(ó€0®ŽŠàŸá½´—h“S¹gÞF£bà}¤åáÎ>µ>§à‹»ÖÒBëÓC–c{dÈv²¦Ì“ß<œ{×mE @zœ¶¹á9uIôËÔÖ.­5[hÅÜ(¿¼VÆàÊxǪê^’þ-i5ËÏí.I HƆM² ã ôÙÑ@ ÏÃ÷¸Ò­µIIÔ/…Ö¥s2ü÷ ‘¹F0 é×Ö»¸bH`HaP‘¢„UQ€  )ôPO‡t™4{kˆ¥Ô.oŒ³¼Áç9(çhö›¡øVmW»¸²Õ§WröM• Ü7QÏ5ÔQ@ZxðΣ6¡pÑÙ_%òÊQw3,žfÒ=3UüAðóûZ nÖ-jîÒÇW”M<Æ„Àóƒ€H®òŠVÎ*ëÀ6÷:ÄWòjŠù/Ö0‹é”Ó?…@Ÿ¤‚ãL¸±×ïmg°¶{Tdåg,wרéšï(§afà=%<>¶·—Ò$žl¾ åûŽ9'§­Eyðú ÅëǨÜEq>¬šÂHO•*¨P0zŒ î(¡ë¨lqºWƒ.tÝW[Õ#×®dÔµD‰i!B"’FÕú1ÕŸøZçÃp?¶%»·žY'xžOÞ9É å]M-êszw†¦Ó¼Cw¨Zj³­­Ü¦i¬š5([n2¨è g?¦¼Ôl®5zóQŠÎèÝCÆ¥[$¸ àp?kµ¢… 燼=-‡ˆ5½cP¸K›ËùÄUHÀ£åŒgó=‰æº:( Š( Š( Š( Š( Š( Š( Š( ÒÒ¥uÐÖl?òÔþ¯ÿ¢Åi]t5›üƒõ?«ÿè±@ÑNU¿o‡qÿd ×¢ÑLi±_yÇ݈>æ¹_‡^ ×tÏ›ÝZX>Ïok´[íÓ{‚ZG䂎{íö¯Bð·ü‹šwýp_åR麤z…Åô1ÁuZMä»M Es€r„ýå稡hî'Ø¿^%­ø{Ä–þ Ö¼;g Ëw5Öª×QܤѬb6”I‘–0Æ1Žù¯m¢•‡sÉ|càÍWUñ¦£®XÁ"ÜØ¥½Æœd‘L3Ê„îR™ëƒ€N0qÍ[hüCgãoøJ#ðåÍ×öŽ––ÒÙ­ÄjÖ²£†%°AÎ2;Wbþ&¶Ä‘h²Ú_Gs6ï*VˆyNdÙþ•½BZXOVxŽ—a©xAþiòYíJnš[XœÚ͸OÊXn瓚ŠëOÕtM2ÚîóNXõ KÅRjéêà±Wè¬ÃåÝ€Nzt¯l–ÎÖ[¨®e¶…îaG+ .€õõ¬­+QÒ|Qö§Š¸:eô–¤Ï>\ñãq\ç¦ìd{Õ__븭§õÙ˜>Óu1âïkÚžŸý¨öËêÒŠ=¥˜©#žÜúÖ÷Žt¹µ¿ëe®Ï>êÙâMý2G­Ê)=U†´w<¢=7_ÔüMá;AžÊÏM°¸·¹¹–hÏÎð”l•Èú·µcøkÂzÅ•–¢jš>¯<mün’Çw¶c³(Æü ôëÍz½çˆl­5ø4góžú[wºÛd$KÕ˜öç©?‡õ{mwIƒQ± y)æ.Öà㥠ºµŒ+­2ý¾)Øê‰g&—-¼“nó ŠUHÎzgµqž7ðÿˆ5;ßG6ŸªjÝÀ Ó|‹á«&rî œà‚Jö*yt°îy~‡¦kšÚàѧ¼uÐ!²š(åŒ4S¢•‰8#9¦èÞÖ ø+“-‰]b9$—ì­"ò~ÒÒ¸rüëÔ¨éÖ›ïýwíý|ŽG]‹Qñ/Ãý^ÐiÓé÷Ó@ð¤ºÇœ©#¥hx>îö}*(u&çM–Þ8ÓlÎŒãiRzc¾:ÕŸ ë¶~"°{Ý7Í6«3²:m8%}Fr> Ö­1XÈñ}¼÷~Õmìâine¶tŽ5` 1Oçšnâ-CXðcÜh‹¥ÒäI3ÊŽGˆ"„ÚsÔsÛ?¥èº¤zµ™¹† ¨HÑ칄ÄÙS‚p{qÁïWèØ§ƒÅá_\CKÕn5[=b ÍFò{õh°²|Æ8÷ãnx€1Þ»;¯øgZ×–ÛêA}uö˜®!¸2kîçŒ{×¢ÖW‰5ë?XÇsæ°–d·Š8“sÉ#*êilÿ¯ Üó¿ø3UÕüi¨kVPÈ·Q[ϧ—‘L3J„îVLõÁÀ'¡Ç5®ç\; ¹w¹Ó Z¬Ñî‚`Ù NìOqÛ·jè5èZN ,ï¯âŽ`Ø”B6–Ëäð^+zÞhîm➪wdÒÀõ©«\_­¸`Í>0Ž qÛÚºélífº†æ[h^âDr²éž»OQŸjžšÑX®äsͼM$ò$Q¯VvÄ×–èšsx—Áß,tù£?Ú7×QÁ0l£eê3^ei¨Û5¶¡mÕ»`´SÆ9Š[+;k e·±¶†Úû±Â}╇sÌ4/Ħ·àù¯´9tÈtIã•æÌíŒl O¦r{Y–:‰/to è·>û °ÖcÔ&º’hÙDhÌÙÀ9Þsù×´ÑU}nM´°QE†QEQEQEQEQEQEQEQEG/JÇÔ:Ø—¥cê fxsþCÓÿ×ý˜WY\Ÿ‡?ä=?ýqÿÙ…u”Êx›U¾›\‡AÑ®"´›ìÿl½¼gìðdª• 1VÆz&¹ÛJK¥–_ xº]^þÊ$–W¨\(nX ‚pFF@ÈõÙñL’èþ#:¥Ò\K¡ßY ³ n6¥YÙe s´‰tÀ5ËhךF‡ ŸF×¥ñ.¨öÓO²¶T%b.-· Î2ÍŒhÓ´R kF³ÔmsäÜÆ$PzQø+—ñ÷Ä?øfÚúÎ÷[†ÓUìÑ ¤db§i!AŸ\WEá]>}+ÃÖWrù×FŒe'õ5åW7ÓxC\ñݾ£¡j×'ZÏkuaf× "´{B±QòGZ™^ÎÝŠ®®X🊵«kÀqj™6×Ú,··»•@‘×b{`Ò»[ŸˆÞµÐ-õ«YbÓ.Ç Ï«æ2v©]Ä{zñOÂzÖ¿aàM* {ËâÐ¥Y^H#:²Ÿ³ÊÃî«mÁº5֮޷áo_øSS··±µŸN¸²‚ÍžKYxÁD”!pãšÖv½½7ÿ eÛúìJ>=ðÀðÏü$'X·?™å’òFÒ1¸Ž˜£Ãž>𿉠¼›EÖ ¹K8üÙþVB‰ÏÌCqÁ¯ñTs§…¼Oâ94»› KÄwöÒE²gXØ+1N ±ß5Ó^]øÆMsNÑuK î ››ûF·i ©Ø€0Ë‚}³QÒÿÖ×ÿ€_[[Øît/‰þ ×µXôÝ'^·¸½‘ìuÞ}eôìk²¯›´-Dø«á¿‚¼7¦èz¤š¬öÒý®k6H"Tl³¬¤`ä8ëšúDt¦Õ¯ê$îQE!…Q@UϽrܤ8´Fsùb­UX—yqþ2$_~?ÈPw‹¾Ÿ¡ëßøóŸþ¹·ò¦Xüâi{K!aôü©÷¿ñç?ýsoå@Y¾€‚AŠAîWÎZf4¶ö·R_\y®‚BѪ«äŒä?Þžµô†œžnƒkþ+e_ÍyM—„¼Kik±ÒíåòTFn‚†€pG¥yy•*Õ}’=Œ¢½.~ÙÚö(x!otÿhåu]Vh§•¡’+›¶™ylݡʊö/kV~ЯumIÙ--#2HUKz=È®ÃÞ׈ô»«Û(mm­ei¼ðìÇc(þ×é]wÄ/·Šü©è±Î-二”Œ…`A¨­°1«V­½Ì3Ñdèíc›Ÿâ&§cáSÄ:Ï„/tûK;u¸E–ê6i+ÆÝ?7JÜŸÆPîøgJk9>Ñ­Ã$ëóܪ*±Ï¯Þ¹Ë#Æ-ð†­áßXivÜÙˆc¹´¹2–ÉRETѼ7âë¿xSR×ì´ëx´;i­šX.K™÷ªÁqÇÝ­z>îÇ—ïZÿ×BÆ›ñVæþÃQÕS—é¡Ù,û¯Úæ,3DH+·9#ÔÉñ2î/¶©©øRþɧ’´ÛsqµóÈ JŸ—dîÅQÑ|¯Züñ†î#´¥ä—mYr„JÅ—-Ž:š±ãoßø‡áï‡l<› uM"H. ­ÏÍìˆU£cès×-i÷Á+¯ßÿFø¶–ºWˆ¦Õ¼?yc¨è«ËdÓÆæD•‚©WS޵Ãÿnµ-r]Wðõ懨½‹ß[y³Ç0•àœ©à䎵ÇÏðëU¹ð?ˆì,¼'áí S¾û8ìW,Ë(Iƒ¶âWŒÆ;šìo|/«\üJе –ãO·Ñ屸>gβ??(Ç#Í;/ëÐM¿ëåÿÍøâ¿ø‹ÂúsëÚeÃÇ$ 'ö³ÏY˜66ìÿÖ¯S¯7øI¢ø§Âº|>Õlôã£Y«¬‘\–L±#)Œ­zE³wAõ (¢¤ ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(”»ÿ]ýuOýWO\ÅßúèëªèBºz«ai¨B"½·ŽxÃ+…qžTäÀÓ¯,­¯m$µ»‚9mäR¯¢¬Q@ hE¸€D‚ »vcŒcÊ™aem§ÚEme AkµJ±Efø—K߇u=-¥hEí´–æDê›Ô®G¸Íyß…üªÇ®xjký6ÃNƒCGÝ5³+5ë˜Ä@¸íPs×µzµ-Áêãw~Ö-.ÖtnâYÖ[ ìÉ :s_½Ò#ƒ÷—vvŒqŒó]ýúÜ:X(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š) -!  W] fÃÿ ýOêÿú,V•×CY°ÿÈ?Sú¿þ‹ è’ÛÃá I/^4µ[Pei ç=«ƒøe}¢[ÝxÆÂÂîÈ,š„­m R 4~X? ÏN½+м2¡¼7§+TÀ¹gxQlï4ŸEâ áéQ2,ìV—Ï}›»gàñT¢ž3ày?³d-áÏøJg&Vp’[•L®2ÁKsÔñšú@Á@†$(:.ÑQÛ›Iã‘-Ì"¹I<uúRJß×™MÜó¿„Im¯â1¥ê2éå¡ace3É«í9 °xp=z5åÌVv“\ܸHbBîÄàM>8£‹>Th™ë´še嬶²Û^Cöò©WŽE ¬==j™(á¾Ã-õ¦³âËå+s«dÂs²#_Ç“ëÈÏJàt½6ÆÃÃÞ׬¦«q®Ã —cÑÉ!WNN6íã²qÔ׺ -,Äàƒ+¶(r*£¢¯ µ:Ý­n G¶0Ër­ ¹öÅ/AúŸ:ê/iýâî$”|B]h M®â/Î_/jƒ·^p:tÏ5¡¯éMïÄ[´ÚC4×z¼ð\žrb»‚œt€?^z×¾˜b2y†4ßýí£?/•íÞZnÎsCZ[úéþBê|ê“ý¦÷ëâ°Ë¢ %²Ó¡ÐÖĽ+Pèh3ßòŸþ¸ÿìºÊä|>ÛuˆÃAÑFOÞÒý¬Ï û÷@O#šj¢)ʪƒì*µùáqÿ~èûXÿž÷î€,ÑU¾Ö?ç…Çýû£ícþx\ߺ³EVûXÿž÷îµùáqÿ~诈t7ÄV)g¬[}¢Ý%IÕ|ÆL:œ«eH<»wk ݜַ ¾ÞhÚ'\‘¹HÁëÒ™ö±ÿ<.?ïÝkóÂãþýÐZ“e¡i6Úf•‘cl›"{>ÕôËZ½U¾Ö?ç…Çýû£ícþx\ߺ7Í[ícþx\ߺ>Ö?ç…Çýû  4Uoµùáqÿ~èûXÿž÷î€,ÔW €ä«¯*ã¨5ÚÇüð¸ÿ¿t}¬Ï û÷@{¤á¢I?ÚVÆHcžã‰ÊÇtS’ÞÄÒý¬Ï û÷GÚÇüð¸ÿ¿te@P€*ßøóŸþ¹·ò¦}¬Ï û÷Q^]ƒi?î.>ãË?j³£È"Çþ¸'þ‚*ågéîcÐ-q•¶VöA^#eã¯Üy7"x‚HdX¼°  ¿FÍs×ÄÃn~§^SgÐ÷ê+Ê<#ãÝx›O±Ö£Ñä´»fu¢HŽŒ°?1 ”שÜM¼M<‹Q©gv8 RM]*Эh;£*øz”%ÉQY’Q^]wñÏUøƒá#ºí¥Õ¼ï/Û¡Ž0䪦Wæ=}+«—Çž‡]:4šå’êbA·/óo8ù}3È­LNšŠÀñŒ|=á˨-µÍZÖÊâqº8äo™†qœsV­$ñ‡‡¼2Ð.½«ÚØ´à˜Ä­ËÔj­ªø÷ºM•Þ£®ÙAmy›o#>D«ê1@=ÁxÃâV™£ø>ÓÄzL–ú¥„÷ÑY™\*nm¬Ä€OË銿/ÄŸæA¨M¯Ù%¤Ò4I!b:˜tÏíÔWèuÔU}>öÛQ±‚òÆdžÖtE*«©äíV) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Šå.ÿ×Cÿ]SÿBÓ×1wþºúêŸú®ž€ (¢€ (¢€ (¢€ *Ž¥«iúd–ɨÞAl÷RyP‰\/˜ÿݦ¯PEžúΚšÒi{Õ/9mK0§?6=>SùP…T7—PYZËsw*Co—’G8UQÔ“@QUí/m¯,c¼µž9mdMé*6U—ÔJJÔìµk5»Ó.¡»¶bTKRGQ‘@(¢Š(¢Š(¢Š(¢¨ÙêÚ}íõÝ¥ä3]Ú'‰ˆž›‡j½Edh"Óu÷¿M.f•¬f6ó†“kŽÜŸ¨«Zv«a©µÂé÷p\µ´†)„NËqÕO¡  ´QXç‰íô{HÒ¥³½ž]I™RXc8±ÝÉ#€4¿EŸ&³¦Ç­G¤=ì ªIœ–¥Ç˜ÉÏ̧Êß‘  (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ CKHh•×CY°ÿÈ?Sú¿þ‹¥uÐÖl?òÔþ¯ÿ¢ÅE Í^mᄚ­´+4ö–jFÍ€Ä/šæ¼%­ø¦çXÑ{«Ûý?P‚Cvói‚ÜZ1MÈÈÁpØ­+)|QâMk[’ÃÄPéVvW?e†ÔX¤ÙÚ,åˆlœôVô>Ð`¶Óa†ÕãþήmäYœH$lî%³–Îy¯ÍOÀz¥©\_Om2\\f0ÜIŽ…‚ 5ÐO\³¶O‰>`‰fxî HcµG'éV†§~~"Üé¦ä =4åbò×ï–#;ºþ¥â éºù¶mF9|Ëf-ÊÑ:0pTƒÒ Ôü#¤êRÛËt—&hmͨ‘.£#Xƒ–Ͻ-moPgžøoÅ~#ñ ·„ìU—’_Mqz–Ñ»‘̪ª¤mcœžõ¤ëÚ¯‡´Û[;Ú†£â»«[‹è ó -\ç {~êpøAƒNÓ¬­í¥†=8¹¶xçu’=ìY€ps‚XñšŠßáç†àÐeÑã²±IpnÈ39u”€ «çp<‡×Ö•ŸõêŠm?ëÔ¯ðÿR×nu-fÓZûEÅœ7³½¸´û3ÊêÈ)G\Ÿjí…±I5áß iþ’òM?í&K²¦Vžáå'hÀÆâqÔô«ú½—ö–™udgšÜOŒË Ó#‚3TÉG á…%ñ&µâ©×u¤¾§ýÅÏ™ ÿy¸öóƒ\¿…¯µßøÂ7±j‘É¥ßj‰`tál ,r;(mÿ{p<õÁã§\Ñô‹]'D·Ò­‹X"¨'’1É8îk9<£&™¦éënÿeÓ®RòÙL­••r’s“ÏcÅmýw ÷<âçÆ&>×<[©vZv¨l×J6¨QÑfXŽeûÀÙÏnx©u­wÄÿð˜Çgiâmmnõlb„YÂâ °y¡²W$äc“Ðúó]½ÇÃß\jrßKg!yf[‰"óÜBÒŒcÎÓùsVäðv‹&«¥%¼wïrŽel,Œ› 8û¼RiÛOëoø!Ôó›뺾¥¥éÞÝÙ2éß\\ÚéëtÓÈù®Ü*‚¹ãqÅt‹âÍbËá„Z޳hbñ¹µŠ » “*ŒdaëÚ¶fð,:|bÞxÍŒF$ŠæDp‡øK’>´è<¦A.‡åIwöm»ÛÛ¼¦E.ÙùضX°ÉÁÏzo[‚ÐäõM?Tð…ô+DÕ[O¸“Ζòá-ã•¥FÒ1ÂX~µIðÛ\×uÐaÔu‰$kÝ"ßQ†U¶‰ZÝœ°`\0à}àǼÖô i mB7sý›deÆå*zx&³­¼£Z@‘YÇsl©k’˜ndB±Frª>½ûÐ×â`|4¾šÓá½åëÇÏ×lV8 ̮ܕ@N2joÿÂO>—i¯êž$†îÚh^æKìQP¤ª,€î8äç8­ßøCJÐTÓÅו `ÑKs$ˆw±ÚÄŒ“Þ Ñü  è÷ñ]éöóFñnòã7×p á ÛОÔ=A‰âÏ#Â^$ºÕb¸³×µ$µm4Ú¢¬ !`6È>bWi뜒:WKâpšÖMѰµûUƳg³”».þ„‘ìwš¦‰cª_i÷w‘³Ïa!–WkŽ@<þ4kÚ†½¼:œM$vó¥Ìa\®$C•<*j×W½´9_ˆ/?…|+øH[érÏ} YŽ*<Æ IR0{~UŠîµ¿ ép¬Þ#¼¼½½»Ša› JNäPH^}[¦;×e®h¶:Ý´0j1´‘E2N€9\:©ãÞ™â O×á·QÛìò‰áxähÞ7e Š›2>ÐüO⻸b•aó9'©>˜ÖˆüI3Kðè+£Ý¸äÿÀWê2AàŠéÂúa—H•c‘eÒQÒÍĘáCõùO|ÔžÐ-|7¥ G–Pdidšb ÈìrYˆgð¦Ýåq%h¤yÖ³à]xÜ_A4Á„Žm¼ùÄl ©ÚÒÜ¥±ª»8Á9¯\¢ŠŠÁÖáEPEPEPEPEPEPEPEPrô¬}C¡­‰zV>¡ÐÐ%¨Þi6Ú½öš-ÍÜ»£ Z<ïQÈÆ{ŠæÅsÚi™ÿ®rñu¿âùk¿õçÿ³­xý}O„£Z”¥R7wý3ÄU¥QFÚ…ÿ c\ÿŸM7þý¿ÿGü-sþ}4ßûöÿü]qY«Û¬óÝCo9<Íı€ ö"¥m"é•Ù>Ó É,Y*ÃñÁ¯WêX4ìàŽ­bwægeÿ c\ÿŸM7þý¿ÿGü-sþ}4ßûöÿü]yüˆÑ»$ŠUÔàƒÔSjÿ³°¿Èˆúî#ùÙè_ð¶5ÏùôÓïÛÿñtÂØ×?çÓMÿ¿oÿÅמÕFÓìW>OŸ ÿ(mð¶åäg¥ýŸ…½¹þ¹ˆß™Ïü-sþ}4ßûöÿü]ð¶5ÏùôÓïÛÿñuç´SþÎÂÿ"×kÿ;= þƹÿ>šoýûþ.ø[çüúi¿÷íÿøºóÚ(þÎÂÿ"®×þvzü-sþ}4ßûöÿü]ð¶5ÏùôÓïÛÿñuç´Qý…þD]¯üìô/ø[çüúi¿÷íÿøº?álkŸóé¦ÿß·ÿâëÏh£û; üˆ>»_ùÙè_ð¶5ÏùôÓïÛÿñtÂØ×?çÓMÿ¿oÿÅמÑGövù}v¿ó³Ð¿álkŸóé¦ÿß·ÿâèÿ…±®Ϧ›ÿ~ßÿ‹¯=¢ìì/ò úíçg¡ÂØ×?çÓMÿ¿oÿÅÔsüRñÄM 6ÚR¼Ÿ.Z)çƒüuÀÔ–ßñóûãùÔË.ÃYûˆ¨ãkÝ{ÌúŽŠh‘§qá¿ËŠðm2âÚ >ÚŸ2)âŒFèñ0!”`öö¯ ­?ãÖ÷ùSö/÷Gå_žbðqÅ$¤ícî0XéàÛqW¹â¾d»ñv†,Ä’ùw $Œ#8E8É$qË îþ0iwº×ÃmvÃK‰§»–²%82a*> ì¨9ô¥«Ãa£‡§ÉF+,MOi$xm§ˆ4¯xçÀ£@Ó/mZÑdì!1`)b pxë\Ð{{†·ž¸Ð/[Æw* ZF›zËçcn6²÷í_KÑ]GÐñQ¨Zx/âV»wâÝ6îXõK5²ºŠÍ®F#@²'ÊÓ¸ƒLÔ5¼9ñÅÚž¡ovºwˆ´«o°ÜElî7$l…(ÊžAæ½¶Š.ì¨ùçÃÖEeðR Ë)ÛÉ’åf™¶G$u«:æ~ ø¸£L$µÛ=²­¹'vÄÚSïsÅ{íMó_Îÿ¿È"¹måoÂÿæ|ßâî´ïÛ꺵ÙÞèv‘Áuk¦%î]ToFVF*~aéÞµ<'¡Ùéž.øyefo5.=:ð$÷V…1™2)^‡¯|¢š•¿¯_óÏšï´û£àß-½…ËEaã?¶˜–É…e”ä}+{ÇZï‡õa¥kºî©¥êvë4VÙÑhdÎ7,‘”ã$ 7{µ›ºK·ùXiYßúîsŸõ+ý_Á:Eþ¯`º}ôÐæKtR¡9 `@ ŽÙ®ŽŠ(z± ¢Š)QEQEQEQEQEQEQEQEQEQEÊ]ÿ®‡þº§þ„+…×µ=S\ø½©øq¼Iq¡i¶:r\Göb¨Ò»I-žô®êïýt?õÕ?ô!Vµïè ¹ŽãYÒ­o'v¤’/̦hê™kwú¥÷‹ôß/Œ'±±ƒDŽóûBÝ•^öC¸$‚¤ à{ûW%ªx¿Ä—¾Ò¯^½µž éKb¹0I„f©#¯®+Ý5xkX´³µÔ´kKˆ,ã[£/  1Û$þðüëj²éVì¶°5´+‚FÃG=i[úûÆ¿¯Àñ&×ðê9Š÷^ûU‹â—ko­é0i²&eÓåaö™N(1Ó§~Ƨƒáç„­ï…äŒw"Q8‘S89Ü=+ZçÃúUÖ·o«ÜXÅ&§n»b¸9܃žŸ™§ý~ý~&¥x‡Äx^ã㆕ ãhrK¢Êê=¡ðˆ·¿ë^ß\î¿àŸ ø†üÞëz5¥í×” ó&\€’ó'ó¤×õòK[ž9¡ø¿]º´øk-æµq¶çR¹µº”0Uºm-Ç=1øš–ɵü'ñ”÷ž$ÔœYß]íhäSæD‰ãîwâ½zëÁ>ºÒ,´»Ñì,Ûu¼%~XϨüêÆ“á] HÓo4ý3K¶·²»bó‹òÈH I@ ]?ë·õóìÓþºÿ_#Ï~øvK…VñÁâX}¦Ê'9›ld•O—€zúW7ðœ\xá­¯éúÍåÅŵ­ä‘éòº´Hë’h»dýOµ{/‡¼)¡xpN4M2ÞÈNÈ#æ8?SùÔ:G‚ü9£]Íu¦hö–Ó̬’2/ÞVå0p*¤îÛ]IJÉ.Ç•øSXÖ,uÿÊþ*¹Õ×]I>Õi3+,_!oF®zæü7â?EkáíVãÄÚÀ¸ñ#éo+)á ž95îÚ'ü3¡j }¤hÖ–·Œ2¢óƒ×­,^ ðÜVÖVñéÂ)¾Ñn˜$G'0ç¯ò¥ÖÿÖáÒßÖÌè«Æü_â[ÍÅ׉áïÜj7Ìrt#`÷Cw÷¦6}3ž+Ù*mmà–Ya‚(䔿GD¹õ'½"Š>¼Ô5 ÎëY°þÏ¿‘3-¶íÛ}}Æ;gæß¼A®ØëþÑt).¡ME¦yZÖhá•ü½˜UyÑ÷Žsí^»Yzÿ‡ô¯ÛGµaäQ¶ôYFvŸPi½î.‡†jZ÷4ï Aaªjí§Íw¯Ãd—­q ÓÅ¡m¬Ñ’¡²3ÈéíZ¿l%üF±“ÄÉ)ŠÖ!ªFȲƒ†³»»tæ½;þo ÿcM¥bÚ:YÏ\©p0ëImà_ ZÙ]ÙÛè¶‘[]„ƪ@“aÊçžÇš?¯ÈàÊÞßè*±½×nÞíµíáº/žŠ£×÷äVgÀíÁâËÿ ¢Ím«K[<©¶çƒó8ÆIï‘é^“¡ü>ð®…¨¥þ‘¢ÛZÞ#YSvA ‚y>„þufßÁ¾¶×[YƒHµTg2…\1b0Oדùдwò·äUo3 ¯,ñ¼ú¦ñ+¦×[¿6zµéŠ[&aå"ˆú(=FyÏZõ:óíCÁÆ­ã+ZÕõØf°Òî$žÖÒ+]Œ7gÏ8;RÖêÁÑœ‡‹5}_SñŸŒ­×Ä÷:$µ¬2¯žLjä¾G<œqØzÁº½¼ñŽüw>¹ý‹}}á¶y¯! ¬Çs—v@ÉÉúf½³_ðO†üCz—šÖiytŠK"üØü)šÏ|/­ÜG>«¢Y]KÙÓîÄ !G ŸÎ‹mýwÿ0þ¿/ò1þ k7ú瀭îu[£yr“KÚ[¬ª®@oʻڧ£évZ6 †—m­œ# c µr©‰QHaEPEPEPEPEPEPEPEPEPHii Rºèk6ùêWÿÑb´®º͇þAúŸÕÿôX hŽÛVµÐ¼©~̶¶–bi ŒœØV6‰ãë‹Ý[J±¿ÑE™Õb’K)ñf Qwm“hÊe}séë›}*oÀ1é—,ë Õ˜‰™uƒáêz-í„y¢¼6±ºf--c•ÎÜ!.ŒwÇZHL³¦xòMCZƒD‡G‘uµ•…õ»N6ÚDýé}¿0`~Qž‡é|c«Üë®áyµ+K 2Ü›ØàÝ'pŠÝ@Èç"ªiï4Ý^×\ƒXμÓ3j7 )w tnîñ´öæ­·…uÛ WRŸÃúô6–—ÓyíÖ‚S3´äuÇzk¥Áõ©GuiñGjWßf¼Iž[f—1eT`í×5®5ö>/›CgÚ ¯´yƒœ’6íÇ·\Ô)ÐoõGLÔt›ø¬ï,¼À<è|Ôuq‚Èôªê^ÕeÖâÕ´ýNÚÚõ¬¾Éq¾Üº±êFî0rpsK§ßÿ“añ]RÇCþÊÑ}KUûCGl÷B4!‘‘™¤+߀3ϵcøwÇך~‡¨\jÐÍuª^x‚âÆÊÆI‚ùx¬M'*¡TžœÖ®‰ðîóE³ðëXêÐGI[˜üÙ-ÉŽTšFr6îÈ<ŽsÚª¯Âû™<=-­î¯ ƦugÕ’áíAˆ»€ ¼dᇿ§¥%{ÿ]ÑNÝ?­Î§Â+:ö¡ªé×v&ÇQÓš?65˜M+®U•Àô9ãŠê+˜ðo‡¯4I¯ä½—MNSÊ[+!o°s’ -’ ÛÖëiWk¥ù_n1°‡Íb¨d€N*™(ä­5ÝwÇ£YÝË‹£@mÙPágºa’O²)úd‚ZÃðwŽ5K]/@]kL¸–Êþóì ©Ér¥ÚFb˜ÀÎÒx>§3ÚxCÃߨ>‹L2+ܲ3O0ç|¯’ÍùšÄOÏÿ߇4·ÔcfÒu(oÚA¢7-´ ñœã<Ñ·õ÷†åK‰¯wúŒz²ørÆðYM¨-Òî ¼#7Œää`QªüDÔ­ä\DòEæ)à‚Èì}hŒÁñ>³¯YøƒÂâ6k«‘:˧Av<¦ 3HTp?w¾0j[ˆ-sel7«6‘slnùœ† 0TöëZ²xPºÖôRúúݧÓÒU™b€ªË¿Œ±Û€=ë øå-µ³êq¥äúÙÖ Ã•Œ•EØFyû§ž:Ñ{5Ûþn¿¯2]kÇsØêzå…žŽ.®4é,â@×B11¸ÝŽv¸ÛïœÖ=¿ïõÝ"þ#¦ÿd»Ù]¼QÞ ŠËq£¡9¿z_Àš›êºÎ£y¬[Ëq¨ÜÙÏòÛXÄŽÐ7sÀgÛ½E§|9¹±°ŽÝ5Hœ„¿Vf€òn_w÷¿‡ëP¹­¯Ÿå§âV—_/ø#¼/ã‹ÿ3ú~¹£\[¶«jïkt× !‘£MÍæ(!#ž§¨¸v™ñöxô{»ÿ}“KÔï…„w z²2ÈĪ’GÊ[ŒçÔžÙv“àMN=KA¸Õõļ‹E·–+dŽßË,Ò&ÍÎrsÛŽ‚¦OOÿׇ´·Ôc-¥jPß™$ roØxÎqšÒO[¯ë_ò"+K2—ˆ¾)EáëÛÅÔ¬mÖÖáas¥ÎUˆü3ŽzŸjÔñv«}iã YªOÕÃ(’  ¾aIY#(r£¯ 9®{\ø_¨ßèZÞk¬YÁg©Þ=Û»Yn—–ܶáøWGâ ëš¾»¦ß[jZ|iÓ™íÑíY˜’›Hc¼g©èjQéqË­„ñÌwVÚ¶‡wi©_Aç^Åo$1ˈ™y'+êjï|T|6ÚdQYÇs=üþBy×"Þ4ã9g þXÉ©ÕmÍÇ•8@’Ü’r8#8Å3þBÓÁ1ønÆö‚êôË}!0°³e’%ÇLc=Íz%¼1Û[Ç *(Ô"¨ì˜uð—Zñ¨÷K¯ÇŸä]:¼’r]ÌÜñÝÔžñWÚ¼’i×H-Y-Õ )®Þ:ŒzÖ•êˆbÿ|:™lÇÑõLOåié&3² ØõÀ¯µø·­\4rÅ¡y¿Ì¨!u}<ÒvƒîF+טgI uò?öZð]`“H²a$dˆUOÌ8 `ο(̱5((º}OÓrœ5,Cšª¯kÿ…þ#ÝêÚõ¦›¨ør]=n‹,s­äs®à¥°BàŒ€y®ËÅÕ·‡<=¨k¡½œ-3„œvZòˆÓÆTt.n˜…’Rdþ¢ºÿ,አ²ãŸ÷–µÀ×z<óÜÃ0ÃSÃ×P§¶…}/Ç~"½¶µ¸⸹Ž%Û¬@åbqŸ4ÓÝë]ë^Ú¤ž[ÜÀ²gL€ý+ÅüUáïhz‚K±²³[ÝVÊW1à X'ÿÕ\ž±áíQÐ~/k7V°É«éÚµÁ·º,|È1´ŒñónýkÐkWnŸð?Ìó"î•úÿÁÿ#éy®`€<ÑF[¦÷?ÜÁ «M4Q«t,à^ ñQ±Öî4M#S³ðóHš*_­öµ3 $‚6Å´Œ¶FOÛ‘¼…–M¨¸yÁô¡F÷^çþ@ådŸ•ÿSéæŠXüȤGûÊÀΛ Õ¼ÁŒ3Å ^»WΓ¢ø;âž“¡4¶övZ¼J°Û- có6’ÝÙükwÅ«á]?á'‰ÂÙôãyäCöƒ¦ÍæJ#óqp¤°ùKgÛ5/EB–®Ç·Cuo>ï&x¤ÛÉØàãò¢ ¨.3äO¸ë±Ãcò¯<á½=µ[ûÄÞ°°—FžÖuÒnX:†Q¶iœà®G$Œf·þ\éW‰&ðý•Ž…-ߨ•Æ«¢Üy±\"œ~ðdís×í­¿®¢¾—=ÊŠ(¤0¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(”»ÿ]ýuOýWO\ÅßúèëªèBºz(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š) -!  W] fÃÿ ýOêÿú,V•×CY°ÿÈ?Sú¿þ‹ <-ÿ"æÿ\ùV¥eø[þEÍ;þ¸/ò­JQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG/JÇÔ:Ø—¥cê s-¤ÿnKM7[ ‹m¾t`_œãµS 4üs©]ŸøÖÿ‡?ä=?ýqÿÙ…u•ÑCZ‚jœ­s¸zUæ®y¯ü*M?þ‚W÷ÊÑÿ “Oÿ •ßýòµÞjÆ™¦Ê‘ê:•¤Ž¥‘gc,€O#‘V,®í¯­Ö{+ˆn l$.N:àŽ+í¡‡þSÍáRiÿô»ÿ¾VøTšý®ÿJ¢íø‘£kšÄ:l6ú¥­ÌἯµÚ4Jä ïŠêu}F×HÓ.µ BQ ¥´fY\ÿ ЍÊ-]=”%i-B}6Æâ;xî,­¥ŽÜ†…^%a ‚8#Ú™ý‘¦ùWqÿgYùwŒ^å<…Ääõ.1ó­dø?Åö¾* e§jö±Ã‚$¾³hPz'ï ×ÖuK-K¹ÔuK…·²¶MòÊÀ‹ëÇ5zE{ è÷ÐÃî•as#lI5º8Œz(#¤]LChWN³ShI·Ä <œõÙÇËœ•fÖx®­¢¸·Ã*‡FÁdjZ[åXtëd¹xlí£{£™ÙbPf>®qó~5Ÿ¡é:o™ý¥ØZy£kùéñèp9­(&×Ãz§™öMLƒÌRåZÆ»”õ‘ÇJŸLÑ´Í+ö^›ee¿ï}žw×h¦M­éÐk¶ú4·JºÄ-‡EP ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¨oãÎúæßʦ¨oãÎúæßÊ€"´ÿ‘núô_ý¼;F€ÿcY,ÈO”ƒµ{¶hÖJà ۠#þ+œ<8¹A{’NÈoçFNxUpyøü±J*.Ö=<»“’½ÎÃИ¼[ œ²?ò •×üv$|"ñAì„ã¶4¯èš^¡ õ´s|·¸º–m™$ocƒŠÔñaâëJÖ-þÑar»%‹{&áz©rCZ`°¯KÙÉÜÏŒXªÊ¢V<‹ZÒ§ðþ™áM"ÃÄZì6þ º†+›‹Aä’X·l…˜æ=Äv¬/‹½Nø‡áTÖoõm948ï—íó™ç‚BáJ—<à€õî¿…´mc@‹EÔ¬R}6%DŽ&v`.ÀŒsš¥¡xÃ:•}§išTQÚ_ \«»Êevà³’ØÀéž+µ½[<ø«$½6Õ´é¼;áo Xéž!Ö¢ƒÄW–Ö÷7ï+[Çå’Vc˜óŒqVí-%Ò¼eâ/Iâbm"MjææùžæÆ@ûr%Î@ ÇøóÙY|0ð}–}¥[èàX^•iâ{‰_%~霕#¶Ò*M?á¿…4íPÒì´³ ž¡µ¹›| tMûñíœuõ4IÝ·ýmúnVIz~{üö<»á¦£}ãiõeñ'ˆuCe§yV_g¹–Ù¦„çý8€@bxqßµ KJDO1'{„û»ºd\Ο©k:Çß-Ö¹¬[Üjºäð]ÍÛ¤¥7È6Î@FlW°ß|9ð­öŸ¦Ù\iŒaÓcòm \Ì’DŸÝ+†#Øš–?xb-?K±KUµÒî Ý¢ ¤ýܧ9lîËOÞÍ[”[Ûþ÷'•¥dÿ«[þ ä—>Ôã¼ñýªxÇÅA|;3iìu'æ‡Ì"N>~AêzT±Ù?Ž>#xF[íKS°– -Ì“i×&ÞFc/ °ççìwÑn$Ö^k-ͬ"Ç||×rªí¯ËÇ.+TøYàÝV==/´mãO¶[Kb·3#G ’BnWŽO\Öqm=­-«­?­¿àžSy«jׇHÄz›ÅoâäÒP‚ä­Ä¶åO äžzû ét+é<âéz†µ¬ê&¦Á|ytgž2ÊÛö¹ägèà/ ¶‹§i?ÙQ®Ÿ§Ü-Õ´)#®ÉW8rCeÌ~ñ9Ï5jo hsêÚž¥5‚Éy©[‹K¶i¬±€¥s·ñ4ݹm×þýu[ÿ[ßòÐòOÞk>?°·iœ×·×iðËšðÖaÒÔDÞx’K‰dOïf*á]•BÙ"Þí…Q@Q@Q@Q@Q@Q@Q@Q@¥ßúèëªèBºzæ.ÿ×Cÿ]SÿBÓÐEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHii Rºèk6ùêWÿÑb´®º͇þAúŸÕÿôX h¹áoù4ïúà¿Êµ+/Âßò.ißõÁ•jP ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(9zV>¡ÐÖĽ+Pèh3ßòŸþ¸ÿìºÊä|> k—X¡0}ጘz×Köyçòûå?øš³EVû<¿óù?ýòŸüMg—þ'ÿ¾Sÿ‰  4Uo³Ëÿ?“ÿß)ÿÄÑöyçòûå?øš³EVû<¿óù?ýòŸüMg—þ'ÿ¾Sÿ‰  4Uo³Ëÿ?“ÿß)ÿÄÑöyçòûå?øš³EVû<¿óù?ýòŸüMg—þ'ÿ¾Sÿ‰  4Uo³Ëÿ?“ÿß)ÿÄÑöyçòûå?øš³EVû<¿óù?ýòŸüMg—þ'ÿ¾Sÿ‰  4Uo³Ëÿ?“ÿß)ÿÄÑöyçòûå?øš³EVû<¿óù?ýòŸüMg—þ'ÿ¾Sÿ‰  5 ïüyÏÿ\ÛùS>Ï/üþOÿ|§ÿQ^[ËöIÿÓ&?# z»@trdOÈOýVrx»@yÄ+«Z™ mûügëÒ­ÚÈ·ýz/þ_;é^³“N³’HžRЫ$®Täw\í?•pãqŸUQv½ÏK/À¬c’nÖ>µÔ¬näòí/m§|glr«}©î'†ÚšæXá…9g‘‚¨ú“^àí&ÏNñ®ƒ5¤ò5ÃFZ$Ù•19ÁÇ^‚½ã%àÓþø†èÚZÞˆ­òmœn†ƒùZaq+O+ã0 WÙ·s±ÖDWƒ#U”äê)Õä·>&ñrøƒLðÏ„¬ü9—¢ÃzßllÂc@‡€01šÎÑ~%x¾M7@×õm7BOê—‚É¢·y¾Ó oh÷ØR7)àJëk[]Ž$î¯ýw=®ŠòüJÕ<1â9c‚û·Zt2Ç–^d¢øn`ûö=kW\ñw‰¯|Qªi>µÒc‡H´ŽæöçS8f‘w"F±°?t6IöüRÕ\oGcÒh¯Ô>-jQx3Âw‘C¤[êúù¬·m ´#å‹󜎀wëT¢øÁ­ÉáKû„ƒD}B×UŠÁ¯LÖ"'¼ö|(޽yö§`¹î”W=à]GPÕ|?Þ­u£]NìØ—Iwh{c9õÐÒz (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€9K¿õÐÿ×TÿÐ…tõÌ]ÿ®‡þº§þ„+§ Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( ÒÒ¥uÐÖl?òÔþ¯ÿ¢Åi]t5›üƒõ?«ÿè±@ÑsÂßò.ißõÁ•jV_…¿ä\Ó¿ë‚ÿ*Ô AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPrô¬}C¡­‰zV>¡ÐÐg‡?ä=?ýqÿÙ…u•ÉøsþCÓÿ×ý˜WY@Žê‹¹Ø*ú“Yzvª—:†©ËKiÖ8Èa’ HßÍjÑEy­Æ»âoxÏVÒ<-uo¤éÚ9\ÞÜ[‰ži˜±‘€ÈÒ¨¯ñÄ/øsÃÞ2Óµ)-%ñ‰6÷‘DsÃ$ª¹1ç†ÆF>•/¼c{sÿ…òé'²0j«Ömn©-æc'äUrsÏ\S¶‰Šý?­j¢¸K?ŠÚ.­¨][êz|šd‰ öW–Þ]Î÷ETÉ·aŸÊ¢³ø«¢Ë¬×Ú~·¥Ï¦Ú­ìÖ×ö~T­ 8£$xê?CHg Q\çƒÛq§Ú‰aµ8'Û†'ƒÔU­sâ.¦Å¥›8¯µ›J#=µ¶•Ÿ+Ä&M¹^Ùõ ÊŠäõŸÿeéVWÍá¯Ý I‚ÖÈI,?õÑw|¿™®WÅŸ剼á¸n¯´í^ædžÚ®Q3µUˆÃy¦•ôvW=ZŠó/ÅP^xížâ}wMt¶M§ß‘à †?;üÅ•ð1Þ¥Ò¾+躅ý”-a­YÙßÈb³Ôní<»[†É ±÷gæÇ¨¢Ã¹è4W›jô}>ûT…ôoÍk¥Ü5µåô;íáu89mÙÇ ôéZÞ#ø‡§èÚµ¾›—­j÷sZ‹Âš]¯žcˆ¶0ÜÉÏ@zR³¨oãÎúæßÊŸo'rìtÞ¡¶¸Ã.{ØÓ/ãÎúæßÊ€LA&‡h‡£["ŸÅEyå¿Ã­fÖ‚ jÁ¡åO6ÉËm"@:{W¢èßò±ÿ® ÿ Š¹XÕ¡N²J¢½èâ*ÐmÓv¹Àh~Ô-5Ë íGTµš;Gi+{fs*2YÛ€ô­ÿˆox;TÐ’ìYµì^_žcó6rväg§­tUS¥ Q傲&­iÖ—<ÝÙÅi¾ ¸³ñ­®¾úªÈ"ÒcÓ³ã{)Ï™»w?ÃÆ²›á”ßð„èžMeGöv¢/šckŸ4y­&Í»¸ûØÎOÒ½*ŠÕ»ïýks­·õ¥¿#É5¯„Úܺ̉ µÒµ[ß·MºjË*¾àHYwŽn+7ÇÖz¾‹â½FïÂñø‡íZ…’Ax`ÒòÚà¢mM¤È»Ï×Ûh©µ•‘W»»<‡Døo¨ÜøÁ¨×Í£x“DVx¥hVeO0aÑãÎW޼WBžñbør[øJ´ÿ·É(fº$aÏ/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Er?g—þ{ÜßÖÿ>Ï/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Er?g—þ{ÜßÖÿ>Ï/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Er?g—þ{ÜßÖÿ>Ï/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Er?g—þ{ÜßÖÿ>Ï/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Er?g—þ{ÜßÖÿ>Ï/ü÷¸ÿ¿­þ4×Q\Ùåÿž÷÷õ¿Æ³Ëÿ=î?ïëuÔW#öyç½Çýýoñ£ìòÿÏ{ûúßã@uÈýž_ùïqÿ[ühû<¿óÞãþþ·øÐ]Hk’û<¿óÞãþþ·øÒyç½Çýýoñ Šë¡¬Èä©ý_ÿEŠÅ¹·—÷÷÷õ¿Æ­xz3‹«y™\娱ÿV¾´ žÿ‘sNÿ® ü«R°|=¨ØÛèVO{k‹ ‚­2‚8úÖ‡öÆ™ÿA?ûþ¿ã@‹ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühõGûcLÿ Ÿýÿ_ñ£ûcLÿ Ÿýÿ_ñ  ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühõGûcLÿ Ÿýÿ_ñ£ûcLÿ Ÿýÿ_ñ  ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühõGûcLÿ Ÿýÿ_ñ£ûcLÿ Ÿýÿ_ñ  ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühõGûcLÿ Ÿýÿ_ñ£ûcLÿ Ÿýÿ_ñ  ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühõGûcLÿ Ÿýÿ_ñ£ûcLÿ Ÿýÿ_ñ  ÔUí3þ‚6÷ýÆí3þ‚6÷ýÆ€/QT¶4ÏúÙÿßõÿ?¶4ÏúÙÿßõÿ½EQþØÓ?è#gÿ×ühþØÓ?è#gÿ×ühÔ½+PèjÔš¾›ùÙÿßõÿÉ¿Õ4ò/íý¶_ñ <9ÿ!éÿëþÌ+¬®;Â×0Oâ ÄÅ)dìpßÄ=+± ®­ ¼„ÃwSÂy)*SøÃÓ|-§[ê”ÒišqI§I Ê(öãæV ¹»ð¤úqÒµ 󆼆OÝù{Lœ7Lžœšõº*”šŠg†øËÀ:öµ¬øÞ{{GqucygºeQwä Ü™ ”'‘“Š®þ ¿½ðïŠM·‚µ7Wº°khëWŽçÎ˃±~c´u<žÕïTTÌÎðÜSÁáí6¸Œ7ÛF²FX6Æ ò¯^xÛ©©Y¿»¼ŠçS¸º¦§l€#¶G³Ò½šŠmÞ\ÂJÑå<‚[/øróÅVÚG††¯m®L×pÜ ¸ãí"md1Ëm r=i–þñ/‚¯|3©ébx¬ô†Òî­£`pżÀêÎqŒ€1Ö½ŠŠKM¿­-ù1½¯™ä3°ñV·ªé”þÔæÓ~ÊË>“kª¤äðÌÁ€qCXúƒüS¥xWÁpèq©hZ…Äòéæî?1ã0¼'i#>µîôS‹åØ$¹·<›û_ñŒu[ûýM*ßSðù±,÷ËäÌK §iÉê@Åg[i2Õ´ ø_Pðìzu¦‘< >¦n£t•`áJ"Ãv^™¯j¢‹ÿ^µùŠß×Êß¡ãSøSÄxCâ•öS_Pž{3Gþ€\/ÝÏÍŽµü?®][i ¥xWQ:Ütp¦©i©Å…ÀÏ–êOΡ²{Šöª(nêÞŸ‚°%g_ÇS;ɨG éé­:I©,.Y:1ócñ«w¿ñç?ýsoåST7¿ñç?ýsoåIê4GbÅ<=nÊpËj¤ø|÷§Mâ)£¶»mrá$‘D¥“y`OÃÛ¯cn]6*é~9Òm!{)Þå¢Ò¬"šÿPm¦Áò;–ÉõÀ¨¦øwân‘­jÖz{Øk:\·¨^Íõ;O!.°ÄlIÜps\$ñ%‡‡õŸÙZùš«iö˜.åu2ZÝ3Å/9pX}îzûVÕµ¿Š¼Uâ WÃm¡Úh²ý¦y帎O9‚m rzúW ìåý|ÿ¯ó<ÈÝE^Ÿy¯eñkG½[Ù­ô5…¢ÊÒ_ÿg·ÙÉï(|ãóNÿâç‡ì¯æ‰­u™¬ ¸[YuX,™ìã°\AìNxô®>ã]žÂ?Š÷·:¾©m¥üÃ5®%{p|°6#¸ËsìMdAðÿP²¹»Òu êúŤ—ÒN·–š¸ŠF}ëº&n ŸNÕ¥âŸøŒÚ|JÓ,´I®"Ö%·¹±ž9£ÚûZ P‚À‚'ž84FÖ×·ùW¾›\ô+¯ˆZ6†×6ZÓÞÛÏe§­é–æ%Qt€ ùlÖ|6ñɺµØ[Âí®Ïmwml¶ÍvÐ̃ÍT [•ŒàtÍyÄ?kÞ7ԢŒÙÛèV‹q§™Ö6û]àÃnOîþ]¤uÏjè®5?ëZ6¡§^øY­EƉ!2ý¡ú[#/’zÐôæ¦[;_×õ¸G¥ÿ¯ëú؇~,hÚÞ§¦Ú.™¯Ø¦§‘eu{`b‚àíÝò>Nr9£^ø³ èÚ•õ´–zÍݾŸ ŠòþÎÈËmlùÁW ñ‘Ž+›_ ëãÃ_ aþÉ“í:<ñý¹<ÄÌ #(I9çðÍsú¯€õmwÄ–·þÕµÝ?SÔe½ŽâÇWû<~\‡;6` zwª’IÙyþŸðD›jïËõ;;?ˆ·éñCÄZ$ºf¥¨évæÌZI§Z BX•‹JÙ “Î8êuäÚ~¯xOâ^³{§xr}KHÖ!²E–+˜×ì¾T~Y äãâ½dtô¢Vè¿P¢Š*J (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€3 Ôf0jj((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”yCÒ§¢€ ò‡¥Pô©è <¡éG”=*z((zQåJžŠƒÊ”ÇJ±Hh.ê!Ž”í¶¼w)˜åzçä^*[®†“BÿUuÿ]þ‚´Ï±éÿôÿÒ±éÿôÿÒµ¨¤3'ìzý£ÿÀt£ìzý£ÿÀt­j('ìzý£ÿÀt£ìzý£ÿÀt­j('ìzý£ÿÀt£ìzý£ÿÀt­j('ìzý£ÿÀt£ìzý£ÿÀt­j('ìzý£ÿÀt£ìzý£ÿÀt­j('ìzý£ÿÀt£ìzý£ÿÀt«×w+nmi$s„EêMCö›¿ùðoûú´_ìzý£ÿÀt£ìzý£ÿÀt«?i»ÿŸÿ¿«GÚnÿçÁ¿ïêÐo±éÿôÿÒ±éÿôÿÒ¬ý¦ïþ|þþ­i»ÿŸÿ¿«@¾Ç§ÿÐ?üJ>ǧÿÐ?üJ³ö›¿ùðoûú´}¦ïþ|þþ­VûŸÿ@hÿð(ûŸÿ@hÿð*ÏÚnÿçÁ¿ïêÑö›¿ùðoûú´[ìzý£ÿÀt£ìzý£ÿÀt«êéFZÁð:âE'ò«1\G-¸÷Ewdñï@ßcÓÿè þ¥cÓÿè þ¥N—³Ê¡à²wŒò¬Î#×ö›¿ùðoûú´[ìzý£ÿÀt£ìzý£ÿÀt«?i»ÿŸÿ¿«GÚnÿçÁ¿ïêÐo±éÿôÿÒ±éÿôÿÒ¬ý¦ïþ|þþ­i»ÿŸÿ¿«@¾Ç§ÿÐ?üJ>ǧÿÐ?üJ³ö›¿ùðoûú´}¦ïþ|þþ­VûŸÿ@hÿð(ûŸÿ@hÿð*í­×œïÆÑLƒ%ž=AïLšñ„í ¼ 4Š|ç§'½UûŸÿ@hÿð(ûŸÿ@hÿð*ÏÚnÿçÁ¿ïêÑö›¿ùðoûú´[ìzý£ÿÀt£ìzý£ÿÀt«?i»ÿŸÿ¿«GÚnÿçÁ¿ïêÐo±éÿôÿÒ±éÿôÿÒ¬ý¦ïþ|þþ­i»ÿŸÿ¿«@¾Ç§ÿÐ?üJ>ǧÿÐ?üJ³ö›¿ùðoûú´ë{²óy3ÂÐÊFà 0ö"€*}Oÿ 4ø”}Oÿ 4ø•ræìÅ2ÃM4ÄnÚ¤ RM3í7óàß÷õh·ØôÿúGÿ€éGØôÿúGÿ€éV~Ówÿ> ÿV´ÝÿσßÕ  ßcÓÿè þ¥cÓÿè þ¥YûMßüø7ýýZ>Ówÿ> ÿV€)Égaù§þ¥eßZÙm8ÒÛ­É.nñÿ ÿV²¯çºÚsdGýµZ§àèáOÜy6«ò:„ Ÿ˜zWq\W„d•üAp$„Æ<Ž»ÏÌ=+µ¦ ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¨oãÎúæßʦ¨oãÎúæßÊ€FÿEýpOýUÊÏÓÜÇ [:ã+j¬3삼>ËÆþ6œAr.¢U È#òÀ<à¨]Øÿf¹ëâa‡·?S¯ ‚©ŠoÙô>€¢¼ŸÂ>5ñ=ω´û-gû&k;¶h³mÆèÁ ó‘òšê>/ëþøk¯jšDÿg¿¶€ÖÖ¿mX›*ZGͪ*މ«XëštWúUÀ¸³—;%U 68Èz€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€3¨¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š) -!  w] 7BÿUuÿ]þ‚´ë®†›¡ªºÿ®ÇÿAZÓ¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š¥qÿ!k?úç/þÉWj•Çü…¬ÿ뜿û%]  íWV‡MžÆ)’FkÉ„ìÃ`ž}¸­ä|{ ¸¹ðü"Ya2_æDpËò7CƒX7ך†‡7Š4¨uKÛ¨­ôÑ{ óÈX\’ îÇN£ u=2Šó­R½*ÃG´µÖ5RÚÕÔqMs<þcÀ» ,‘ò“Ò¡¾U°¾×|=c«j7[tÅÔ-çžlÍ Ê”ÞÈ8Þ‡ “¹étW'ˆn[´ÔÍÌŸÙKf-¤Èn3.â=@¤ðëK¤iWšÎºönÓNÖeŠFmÀ°êT/ ãÒ€¹êTWšjw·º¶°mtùµ©lmìâ–#ap‘I)p~y OAÀã®Ev~mI¼?iý¶Œ—ê É¿Ž㌑Šsb±Çü‹×ÿöóÿ¡½lV?üË×ÿöóÿ¡½jÅþ­>‚ŸMýZýWÔî×M»¸wµ/úøú*:»EÂÜGwâOë:t𮣧Ziq@cKDM+H‹1Á$ N´Ô[êðϯ^iI‚{X£™Ü”‡Î÷àÖ•yýÕÔºOˆ¼[soºY­´›vBü–*$Á>¦«ødk‘jšeÛj[)£g¾’öâ)"*P°dU?.JsÒ(¯1Òo®‡ˆtkË9¼BtýFåÐý¾txdBŒÃjg+Èàq[>´ŸPÖ5-FçTÔØÛ_K [ý£÷%@1Ž3@îvµJïþBV?WÿÐjíR»ÿ‡ÕÿôHä1wÿ\bþrUê¥ü†.ÿëŒ_ÎJ»@Åx…®õÛø}uÍ:Í,õžÎA’¿˜.ì2xõ¨|J›n,t[k¯\^Anfg³¹Tb™ÀiY° È8Zîè¯5K»ýWIð|êöíy<±\I ž\¬7#$wùGJmxsþ.Ž+Ùï$Ó¤mfº>c “hËs·q<úP="^•‰«È!¶–Vª)b^kZµºð—…uMFÛWÕ5¯$m7³ 7(À®}8¬K{m^Úæ_0kRi²ZKæ¾£0žúÚÒðÛÍàæYí~p\97Š¥œøá ©§Ü¥ÄЉ¶Û°tIqŒŸZú Šìç×úîßêqrio+~ ~‡Î¾#i´='⧇¯l¯þÙ®^Íy§É ³ÉË*.åç>µ{â&}âë_ øF±xµ?L7wW²bya~ÌÆL„àŒœÞžùEKm«?êÅY'uçøœßýJSÁÚdöútºb¬B#g*0•à®n:÷®’Š(nîà•´ (¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  ê(¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ (¢€ CKHzP;®†›¡ªºÿ®ÇÿAZ’àdnŠ1ÏývÿÙV€4h¢ŠC (¢€ (¢€ (¢€ (¢€ (¢€)\ÈZÏþ¹Ëÿ²ÕÚ‚îÙnrÌŽ‡(ëÕMCö[¯úKÿ~Óü(wC²× Š+õˆŸÌFŽF•±Œ‚¤¥káÛL¾°ŽÝÌW£mü¬ÒH=Üœþµ§ö[¯úKÿ~Óü(û-×ý%ÿ¿iþ£¡Øj:Ri×y–¨(ÜC.:ÝA÷¨´éºÎ,!}óãÌ’YGp:ÌIǵ[û-×ý%ÿ¿iþ}–ëþ‚ÿß´ÿ Ëÿ„;EþÆ—Kû+}ŽIþÒÉæ6wç9ÎsøzTšÏ…´Ý^hå»ûP)”—Jú¤Zeºÿ „¿÷í?²ÝÐB_ûöŸá@z¯ƒômL[yðI"íæx˜ è¹R ³aiœ6¶û¼¨—jîbÇäòj/²ÝÐB_ûöŸáGÙn¿è!/ýûOð  µŽ9ðõþ;ý§ÿCz´m.aµ ˆ=pˆæYŠâ·XQG–Ü{P¢æ4Ç § ŒƒÁ¨¥Œ±(H/fŽ1÷Wj¶¦HÍ;ì·_ô—þý§øP$~Ðã»Yã‚tÛ(™b[‡9 œ~•6¯àÝV¿{Ë»y<éySÓ3O.X¶\õ<ô§}–ëþ‚ÿß´ÿ >ËuÿA ïÚ…]ªWò±ú¿þƒGÙn¿è!/ýûOð§ÛÚysyÒÌóK¡ŸØ(8ä1wÿ\bþoWª­Í§›*˯ ÀmÞ˜äzx4ϲÝÐB_ûöŸá@üAá½7^0¶¡™aÏ—,R4n õ”ƒŽZãÁÚLéjn‘í¢òRXîdY g;YË úÖ¯Ùn¿è!/ýûOð£ì·_ô—þý§øP ivñéi2Ódim³+¬À‚O<ðOZ˜hw™©»ÛùŸÚ@ •v,®ÇCÀãÒ¬}–ëþ‚ÿß´ÿ >ËuÿA ïÚ…diÞÒ4È®c·Šw†â?*HçåB¾€1 V]·…´Í!ä{5¸ù¦ÙnE {Ä]<–·8ÿ„¿÷í?²¯íîœßJíš…PðŸo¥ê2ZY+$ *¥‹c,8íí]Ýq^ŽDñÁ’vy ˜z íiˆ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*ßøóŸþ¹·ò©ªßøóŸþ¹·ò é¯åhV²c;-‘±ë…äv¿uÛ†ŠXôD’>eŒBê öim£ë·Õë6`Ÿ@_²/þ^¢ýô›3çE•‰TüÂüëÌ̱((º}Oc)ÃQ®æª«ÚÇáˆ÷º®¿i¦ê^m=.‹,s¥òN-‚ÈšôY$H¼Ž¨ƒ«1Àâžò‡Œ<>±ÈŒæéŽÕ œ¤ÉýGç]Ç– ð‡Ää¶ÜZðsŽw­m¯:ô¹ç¹Ï˜áéЬ¡KkÕtò…Åõ¡QÁ>rà~µ9¹€[ùæh„Ϙ\mÇ×¥xRøÂgǾµ‹H³:}þ“,ÓÀîæp¨UÈÏ'“Ͻr7?ü+›]6;˜†ƒkâË›)Zç|–én (²í ìËãµv½¿­ìyËUëkŸQAuo—åM¡_ Ÿ±È'W~ºú:ýÿ“céýwG±xs]’ÿ@·Ô5«ht‰¥, -v’ªààaÆÏZÚŠD–0ñ:ºŒ§ þ5ón}`Ÿ ü¢\ØhRÛjFf7ZÁamCŸá#.sÀȯBýž%Ýá-JÝgµ–;mJxãû#–…W<É'o§4í«]…}ïÿüS¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠÎ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¤4´P2®EPx¤RÆ)dqÉ {ÿ‘ZdS  w}®¦ü阼ÿŸ¹ÿ:Ø1JO$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÔÿkù#Ò$zPF/?çêÎŒ^ÏÜÿkù#Ò$zP,ŸlÇü}ÏùÖU÷Û0Ò§üë«–Ž•‘Úx  Ï ¿á"º2Í$ƒìü?í ôâ| ›|Aqÿ\?öa]µQEQEQEQEQEQEQEQEQE ïüyÏÿ\ÛùTÕ ïüyÏÿ\ÛùPèÿò±ÿ® ÿ Š©'†t)dg—GÓÝØä³[¡$þUgKq‰hí÷VÝ ÿ¾Ey¼?t™nÊWW<,r”d“ŽÙ¬êU…;s»Ñ¡Vµýš½F±Ñ4½>o6ÇN´¶“Ý*§ônîÚ Ëw‚î§Æ9P2°÷ƒ\w‡¾$iÞ±™ž¯gs8o(ÞÙ´Jä ®+¨Öõ[-J¹ÔµYÖÞÆÙwË+B\MTe+Åèg8J.ÒVdËej²Ã"Û@$…vDÂ1”_E=‡°¨—JÓÖÒkU°´Ó1y!®ÇcÔ²ãð95Íøwâg„òßQ±‚òÊA-´è$À 2‘y«Ÿ&‹¥Id–ri¶Mf‡+@†5>ËŒ ±cci§ÃåXZÁms²Â.~‚¬Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ÔQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%¥¢€b–ŠLQŠZ(1F)h Å¥¢€b–ŠLQŠZ(1F)h Å¥¢€b–ŠLQŠZ(1F)h Å¥¢€b–ŠLQŠZ(1F)h Å¥¢€b–ŠLQŠZ(1F)h Å¥¢€b–ŠLQŠZ(1E-£ŠÈÔÊkb^•‘¨}Ó@¼)ÿ!ùÿë‡þÌ+²®7¿òŸþ¸ì»,Z(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ(£#ÖŒZ*ßøóŸþ¹·ò©²=j+¾m&’Q°? †ÓþE¸ëÑô ðý:5’«0O!>Pxè;Wºé@ "Î9ÈEe?îŽ+œ<4¹òຉI'd7ÓF£'<*¸¼ü~X¥kž]ŽŽ ÉÉ^ç áè|ŸèdvGþA’»Ï‹ÿòK¼Qÿ`ùô·¥x7BÒõom •®aÏ–óÜÉ6ÌŒocŽ=+S]Òì5Ý&ëLÕ¡[‹•Ù,EÊîÎ2=»¬&Тé7½ÈÆã&²ª•­cÊ´ÝÅž-·ðck6ZV¦i~MÚÍí,ÒmŒ·=ý«Gö{û#øcV’B^}RçûU¤½2y‡nìó›qÛ¯½zM½¦•¦[Ú[‚ÎÒ%†%-Â"€É9àÖ¸½wµÍZ]KSƒO{é@YeŽñ¢2ýàŽ~5ÞÝäßsÎQ´RôýÌã¼}¡£øÖë]צ׿ág³h÷æ(­xo6÷cÔg½¶ h#–#º7PÊ}AWœÍà†“^Eu5¦$Ñ"F¥¯œ®Õû ®ü{ŠîXÒQB¦¡bª£ ”?:Kk ïsFŠÏþÚÒÿè%eÿ×ühþÚÒÿè%eÿ×ühBŠÏþÚÒÿè%eÿ×ühþÚÒÿè%eÿ×ühBŠÏþÚÒÿè%eÿ×ühþÚÒÿè%eÿ×ühBŠÏþÚÒÿè%eÿ×ühþÚÒÿè%eÿ×ühBŠŠ ˜.#ßo4R§÷‘ƒÒ¢¹Ôl­X-Íݼ,yÄ’?©  TVöÖ—ÿA+/ûþ¿ãGöÖ—ÿA+/ûþ¿ã@VöÖ—ÿA+/ûþ¿ãGöÖ—ÿA+/ûþ¿ã@VöÖ—ÿA+/ûþ¿ãGöÖ—ÿA+/ûþ¿ã@VöÖ—ÿA+/ûþ¿ãGöÖ—ÿA+/ûþ¿ã@VöÖ—ÿA+/ûþ¿ãGöÖ—ÿA+/ûþ¿ã@U(u]>yC}k#žŠ²©'õ«RKh^IPrK@¢³ÿ¶´¿ú Yßõÿ?¶´¿ú YßõÿТ³ÿ¶´¿ú Yßõÿ?¶´¿ú YßõÿТ³ÿ¶´¿ú Yßõÿ?¶´¿ú YßõÿТ³ÿ¶´¿ú Yßõÿ((¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠŽ^•‘¨}ÓZòô¬Cáù\×ý˜W[\—…ÿä9qÿ\?öa]m!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@šߨA 33)èH«ûWû«ùUÿù éçþºè5su;jÿu*ŒḚ̈–ˆLÀ²¡#q©Òº¼¿Å×Zµ¯Æ-%ôM. Foìiw$·NÑæŽCyäŒc½ {Úç©m_î¯åL™á†&’cq¨Ë3æ¼Â?[øRðeÝΓ,w¯ysß¶2‹wŒ Ù 1 ;{ã§¹ªž8ñN«®øSÄI§h1Ë¢DÝïe»ÙS†"=¹8#ךˆ¬õÅòÙC(B¤dŽiÉè^ŠEŒ¡duW?‰õ.ít? èÑêw0YG4æK¡n‘«.ƒœà×1áÏ/„ü¦ ±ŽMORÔn"ŠÔαF¬–Ý!à3ߊmka_DÏcÚ¿Ý_Ê«ýÕü«Ì[â}ÂéZ£6Ôìnáµ6ñÞŽO7£,>+wÃ>+Ô/¼Gu¢kš0Óob·[¤1܉ÒD,GPEatv;Wû«ùQµº¿•7u© ¢Áa×aò” žÞF/в`ý~cF†¨Ö 3*™e%ˆåŽMó®Ùÿ×´ÿúTšÆ•úÿ3@;Wû«ùQµº¿•7uqŸæ·‹C°ûU”—ŠúŒª%Ó[•f$ܽG==èZ»Ñ\ív¯÷Wò£jÿu*ó­kÇzÕ®­¯Ûé~Žö×GA$Ó5è˜lÞp¥Np=êæ¥ã©|=I}GQÖ-þ׳ΰ„ŒX³œŒŒE…tv’MoÑÅ#’ɈÄØëÞ¥Ú¿Ý_ʼÎòkëÏx*}jÂ;ï*óuºL& ‚àr0}³ŠµÄ9ŸÀpø‹û-CÉz-¿Ú8Á—ËÝ»oãŒUr·°¹’=„%Wò¦C$Æ$…¢‘FB?ˆ®/]ñV¨úÕîáý=J[Ku’ñå»,^b’€dÙþUËxÆ áoxfÎ (§ÔõIg@÷ k±‹9i €=I(mžÁµº¿•Wû«ùW'à_ÂNº”SZ-¥í„þL±¤Âdß”.#zÐ+£Ðö¯÷Wò¨£šÞYdŽ'…äŒáÕH%¸í\KøÇ[½Öoì¼;áØõ´ùE½ÕÄ—Ë ¬¸•A\°ñ\üéð¼¿u˜íæHu(Æ_fíæ4ûØ=7Óµ4® Øõ½«ýÕü¨®jOJž2Óô±©V-xÓy¿pƒ¸Ç=zæŠ,Ù-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ÉÒ²oÇÊk]Ç›x™S@¾\k—þ˜캺æ´0±jó3 ÃŒ“âÐyðÿÏXÿï¡Hd´T_iƒþ{Gÿ} O´Áÿ=¢ÿ¾ÅMECö˜?ç´_÷Ø£í0Ïh¿ï±@QPý¦ùíýö(ûLóÚ/ûìPÔT?iƒþ{Eÿ}Š>Óüö‹þû5Ú`ÿžÑßb´Áÿ=¢ÿ¾ÅMECö˜?ç´_÷Ø£í0Ïh¿ï±@QPý¦ùíýö(ûLóÚ/ûìPÔT?iƒþ{Eÿ}Š>Óüö‹þû5Ú`ÿžÑßb´Áÿ=¢ÿ¾ÅMECö˜?ç´_÷Ø£í0Ïh¿ï±@QPý¦ùíýö(ûLóÚ/ûìPÔT?iƒþ{Eÿ}Š>Óüö‹þûKQãSÓþ¯ÿ ÕÕOVmæí^%„“°Èp#gÖ©ÿkÉßMºü%ƒÿŽPÆêæî4KÉ> Úk©-¸²ŠÁ­2[Ì,_vGÇNþµsûbOúÝÿߨ?øåÛÐ6ïþþÁÿÇ(NÚƒ×CÒ<ªÙj4ò\iì¶ZÕÜ›]òÉ(ÀmûÞ§Z«wàŸZi:Ö‰¢_è²hÚŒ²J¿mIVxŒ‡,2¹ž‡á]ÏöÄŸô »ÿ¿°ñÊ?¶$ÿ mßýýƒÿŽPõVw9«¯ ëö ]g׺_Ú'³ŠÒî ô“cycåddäu<ÿV[ü<Ôåðþ—ç]im®é·“]ùíÍ'r¸?1?ç­w?ÛÐ6ïþþÁÿÇ(þØ“þ·÷öþ9Eú…º…Ï„5Ûß=œéá›K϶Ap¦Â)cŒªÙÉ'Óµt+¡Þÿny¶ßbûµ1å¼ÍÁ‹g¦1ÈïW¿¶$ÿ mßýýƒÿŽQý±'ýnÿïìürÂÈØÝFêÇþØ“þ·÷öþ9GöÄŸô »ÿ¿°ñÊC.±Î¹iÿ^ÓèQStc6ÇùšO‘§½û]ÈKeHÚ8ãiU˜ä‚IÁ }ÑÜÕe¹›Olnb”’£èNpC2àý3@[«›ñÞ‰y¯é–vú|¶ñËì7DÎ[iT9#€yéW?¶$ÿ mßýýƒÿŽQý±'ýnÿïìür„ìî] eðÖ º‡‹çó¬ü½b–ã-”o/gÏÇLúf²ÿá Ö¬ì¼5w¤ÞéË®èö¦Ì‹…v¶‘ 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'AIDATxœíÝÝUyÇñw.kÌìd2+®c3#&bˆ1ü0@ Ò,X­VÇZEJ-Vkªh"Œ˜ ¦@ù!D !Æ$Ƹjˆ41ÝÉlÓ¸^úÇçÞÍîæîÞ_ßçœïç5s§3&9甽ϳç{~<ß1cÆŒÁeZ0˜L¬|º+VýŒÆ@Gå3XåÓìö{}v/»*ŸÀó•?s5Æ@&ŒŽf3i•Ï(¸cÚüè©|6[€­(‘¸ó>ÀÜÊç`0(ÅT ÊÀ6`=ð °®òé‹9(7”'€øNNŽfqðôà`%ðZdt†<Ø™œœ ̧¸¿å[UÖ÷÷ ]—0OÉ:8¸=×{Ð'£ŒÖ îî^Ž;œüðоpp p6Ú²svö+€Û€Qrp-òк‰À¥À‡€©q‡RXÛ¥À·Ñá$×$OÍ› \žïÇF‹“ýhà&tîÀ5È@cJÀ™À•h¯Þ¥×àËÀýøãA]žF×,®Fûõ.;¶7w¡H®OµuŸEgî]võ×·ã‰à ž†*¡m¼ëðÀÏ›àshÑ *<pzvœ{ ÎÔ´–ó`ì¤'=Ûß,Š=Ô*àSh­ °Š|R­ ¸ø9üE´ýìo&~M…h)è àRtšìdŠ‹®¼ø[àÀ³q‡^Ñf_ĈK¥ÕÀG)ÐcAQfcÏ£­ #"Å¥×ÑÑîW¡EŽ;{E˜Ì¾ { )TÝÏk¢vl>@Îç9tW×ÿ/ø>t1fGå³x•ÛÚÍʾ}•Ï~>ÓfJ•Oµšp7*[öz`ªs0]€ÊûÍÇ~àZàKäôQ^ÀÀ¨Gžô¡šzQõÝM¨ðæNÂn)¡„0Í®f¢z†³È_Å£µÀ…¨úq®ä1,A }b¤MÕzyk€§€§Q©í´Ÿb+¡æó€cÑå©<Ô=ìE „Ëc$IyJ(ð/Ž=6ôp @æj4}σn´óR-„šåcÖËP"ÈEñÒ¼$€é¨v\ÖúúQ©«•è\BOÜá3 UO: •NËÚÍ&Tëq[ì´+ à´ÊŸ•)?ª|û}à>òó[¾UÝègø~T19+É íÜ{ íÈr(¡ÚÏçË ¨ŽÝÝxùª‘LD·1/!—²ÊÀÑNSÚ×fjÊjVùÏŽ=:zÑ¢ÑRr¾Ÿl`:”³„ôÏîV ]‚½±Ò¬,&€)ÀI÷oˆ­è’ÉídðK‘2ãQq–ËÑîBZmÞKÆÞ_µ0øÚN£Ç€ÑUÓLN S¬„nð}½Z-vïAÉ ²”NA+ýi›–Ñk¬®GFœ½ù¨\Û¤oý§í<{ HÛ¼‘,F¿ùÓü÷ï@ÛYüá¬EÿÍß~i2}WÇH#²>‚üÒTƒ5p<úúâ^<ëÑÏàxô3I‹±è;û‘Ø©'í×ÿø éITÛPñˆ«€ßG‹;à÷ÀwP…Ÿ9Àk£ŽFÆ G”}À“‘Ç2¢4'€Ï£çê4 °ýßP b´øðGtáÕq‡ÃtüùàñÈc©)­ à:t¸" îBoz_ÙÏ‚êkÅ¿L&ÇÃOF'=áÒ˜þøBìA «ŸKP©pßËÏž½h×è§h6ðš¸Ãaª0”¦µŠÔ%€«Ð´?¦2ð5ôòOŸîg_z{ðôbט_ø…Àÿ¡ `©¦sAsÁo;ºà‘ª,í³]›q eàch­"º´Ìß"nð/Cw 2ÅÓè9t!ë ÀÑ‘ÆPÝ؆®G•†À)èàD¬}þ\VzquÅ®µŽzb0v˜¶Gbý6¢c›E)Äᆚ† gEê¿8‘ˆwbN¹'÷xï2T³Îƒ¿¸zÐw`Y¤þ«Ç†'Gê?Z®ôƸÕ×®–~€œÔusméC߅ˉSú{Š…ñúŽòP~@œb{Дÿ‘}»ô[ˆ b¼,tð>6‹± ðO¨ÒKh=èDÖº}»løúåt:phà¾ßBàÓ‚¡gç  úÑãi´âZôœ®1ÝèÙ|^à~Ëh†¬ÐhÈ0øáý@gýyß5£Un>#p¿½¨ÎAó(¡~w§šÏrT§Íƒß5«}wBŸ™€b%ÈëÕB­ü;ºÒ7R€W<;3e4=pLÀ~þ‚!À´ðÒ­Àߣ{ØÎµãTv¬›°kG¿~aÙ‰õÀÀ³„úß |"`®8n>°¿^àí¾•ØrÐ2盬:¨á›è7¿s~Œî„zx5ðNtƒÑä|€eø aßÔ»=óû´ßYzx3ð¶@ýMþ„Ñu«G€9èö¡nø­@û§1Žrºâé@+õ¡N³îGw¯@m‘:Ð~¨Ww­Þoõ¹°:Ñ©½P ƒÐù€DÉYœøá‚¿Õ…÷àw¡õ¡Ó¥¡n“ÎF±•¨¤g3Ъˆ©ÿ´@â|\LÓQáшö£]ÄjU&=ø:a‚¿=ó{ð»Ø¶nýi,бÄ$¹ p)pYRÕqª×ï\üøt‹ÐÚÑ›žM¢±¤ºÐ©¥î$«c*àà\Ú|—0[ß»ÑVäžvJjðet×ÞÚFT4áOúr®Y¢EéÃûé¬|VµÛP3€£Ðt¤£Ý†êèE'°¼†ŸK³iÀ3ØïG ‚m•ObðFìƒTºÛƒß¥]ú®Zë@±×–vÀiÀ¢vÑ€exÝ~—Ë SixŠÁ–µóPBSëC?ÛÑÕÈ^ã~œKÒàçØ¿†lz4né²P;3€ó±þ2Zñ÷àwYÓ‹¾»ÖU~g£XlI«»ÕËÖ•S¿FJ^¢è\ ž^‡ý}Ùè€PÓɦÕÀEhµÓÒvàjã>œ³v5ú.[š†b²i­¬t¿Ä>œNûœÎ¥À"TLÄRðVš<’ÜÊ `1öÁü.?Vat}ŠÍ¦4;(¡"…3ší¨ ½è-)» ûp.´‰À¯°= ´U*jx- ÙÀ™Ø?Àuxð»üÙ…¾Û–f mX³3€Ÿ'4óš´ ˜‰—örùÔlF5¬<¼«Ñ¿ÜÌ `.¶Áp%ü.¿úÑwÜÒ ¨&gCšIW4?–¦¬&àK‹ä>Œ*üò©Fÿb£Ñ¡Ëj?Çk Ûw.-Žž4l?*Rw-­ÑÀ¥Øÿýxð»âXƒ¾óVÆ¢˜­«‘@ ø v—ʨ¸ç:£öK£¹¨˜¨Õº·£·rº%ØHç§a{£é<ø]ñ¬Cß}+Siàªp#3€ïÑÆm£‹^î‘W?@k(¡|¸Û¨íó±_ l*—WóÑ´¬Ü üõh¡^%ŸC±}ýÑcä;øA7µ¦ìÏ2ÙLD_ÚP¶ì+†µ(N2jÿlÃ/ôê=œŒKrDô]ÒȹŒ³ŒqÔ™½×K&7–ƒlÃ/ü8· ÛÜŒã%€)h¿ÒÊÍØWKq.íÊ(¬‡b¹¦ÑÀyuþ¼½„)šè\,îì] Åòˆ8’s“Ë€åÀ^ÃöË’½ØV½1–GJ“±]í]jضsYdóQLd¤pæ(Ö® Àz£¶Ëªõ(6,”¡NÀHA~–Ñ@n3lÛ¹,³Œš1]+t ÑÝ)5ç²înìêa,D±=D­pv‡ÁË}97’](F,Œ£Æ‰ÃZ àT£Üiضsy`#Åv­pŠQçýØÞv.îÇî1à Øžîêïj`·QÛÎåÅnìJ†Í@1>`xXPãKÊJ£vË«X)¡ò? v¢QÇàÓçe+Cb|x°*ûÝSù8ç곌—!1>8tGuú Q»ÎåÕÃFíÅ óƒÀ\êWjÕµë\^=dÔnŠu`h˜gÔaû!8—7«±«—1ëƒÀÛ:Û‚oÿ9׬Ý(v, ÄúàÐðûÄšä/üp®5V±3ëÕ0»7–Z–=v.Ϭbg:•û>Õp$v€ž6j×¹¼³ŠŠù ·:þÛl5jÛ¹¼ÛŠbÈ 8fu²¯üë\«Ê(†,Ì„ `šQ'V%Žœ+ «šö `³Q»Î…U IVû˜Î…U $€.`‚Q'–¯8Ã#ªwèàe`v‡wNæc»ãÛ`WÀþ,ûÚEØŸUÈÿn1ÌG1`eŠá3fL½F?NjD5Ü_vÅ´Û r§«Fû $€ð`j2c:HxveÉK£¹ÀO±[ß¼‰:;yt^–&0 ÑÆp­aûÎ¥Ñ5Øî‚-¥müFf [JÏcÛÔhŽÇvÛѹ´8xÒ°ýýÀi` ¥Ñ ´‹Q$äãöK ëïú=4¸€ÚÌä«­¥a €sŒûp.¶s°QÎMþÅFª~œÐôp· ˜‰Ý Jœ‹©ØŒíÁŸ'€w5ú—›]„¸±É¿ß¬éÀ'ûp.–ObüÐdŒ6;(¿À½À[Èÿ!W,_awéTõ÷m4q‰¯Ù@ûŒ د78ÚW± ~Pl6uƒ·Ùè9æ—Ø¿ê«î)&ç2Âú4-@ðVš\?kå B?p} ÿ®Y_Ç‹†¸ì¾ËÖ®§…ÅóVO"ÝŽ2Ž¥©øÙ—}7`wŒ¾ªÅdÓiáôœñßÀy­üã&ÌEµÌŸ3îÇ9 €¯-Y>llå¶²PUžÁ®’iÕvàh´;à\VL~Žýoÿ À1´X¾¯ËeàÊ6þ}£¦·èǹ$Ý‚}ðƒb°åÚíÞFz0+õc[5Õ¹$-!LÑÛU([ÖÎ#@ÕQÀ³Ø½Ò¨ªMu¬kÇ4ôhl½çß¼ØÔN#IÜGÞükíÔ3Õ7ï Зs­èDßQëàÅ\[ÁÉ̺€_ÝI4VÇ2àúq®Yß%ÌÔ7ðf`O» µº 8Ü>àÀ{“h¬Ž£?¢rJÎ¥ÅeÀUúúðT %5¨zû»Î çŸ¿Äî5æÎ5c!ðدƒ¬NLª±¤À ´ hY:¬jðNTCÀ¹X¦£ÙhW€¾ö£…¿-I5˜tQÂ-À—ns$]À³îà\-Ýè;"øA±•XðCò3Ð4ègØŸ¬Z ¼è ÔŸs ÿGyúÛ€Êç'Z-Ë¢,q?ðA4] a>p'až¿œ}×î$\ðïG1•x©¼¤v†{¡òZ4^Ã[ÐŒû€Wõ銩„¶¢­/ öà{ [<Tu "¢ó­:¨á›ÀßìÏÏ¿ØßZTäÓ¤P®e8í „8Uu+Ú'u.i·Ø_/Zõÿ­UVUvçZv2Ì<´:k]‚ÉKèà¸íû›±N *ÂG |¡Ì&àk®=%àÀG÷» ¸ÎºëG€ªNtX⨠²Ýð¸Vt óý¡¯¢oB‡ÜÌ·¶C%Љ©Ÿv=4 x?~NÀ5§ø>pFà~{Ñ~®–¯'númÜrõ’lø‰Aרnô üe#ÁŽ·‡Xl+ð*Â\ì À_¡ Ü·Ë–éè’ÙÛ"ô}=aJˆùPU~œºctè\ü¡«m!*èêlÿ`+€÷x†ò ª \¬Ðwš\¡o—n—¡ïFŒà_b"ôãq”@Õd´30)RÿËÐÖŽ/['šv‡¨äSËN´âÿ|ŒÎc&ÐÁÇ ¿3Pµ=x¡Ñbš†¦ü³"õß‹Š{lˆÔ”G€Á6  usp¸Y¨‚«—/ž%èg+ø÷£ï~´à‡ø àaâlVMî@>bÍD\8ÐÏúâý¼«Û}Gê@èmÀ‘l^Bû®±t4pº¼äï"̧h¡/±šz-(£wù}'â¤%¬CÓ¢S"Ž¡ eæ×¡«Ì±M\²Æ7¡u¾&òX>üKä1 HSxø3á ‰Ô2]&ZŒNdùa¶-V¢*Ò±¿ìŸ¾y C¤-€®?vþ´àp¯.ŽžöÆŽkÒD`)*¤û·>À?ׯÄpiL sد'ǺÁøaôò“uÄ[¬té@‡zîEï’LƒkPY¯ÔIkø_Tñ7ö _¦’‹Ñ­q‡ãFp:f~!ú™ÅVFo JÕ´°4'ÐÔ;öîÀ`¯EI`!Zø}ÜḊã€Û+ÑÏ( ª«ý7ÇÈhbŸlÔb´wâC͸MïbÜkp0=WŸ{ ÃìG»IwÅH=YI íÁP¯^nF¹UpuöæŸE3Ã4f¬ð‹~ȧYJ »?"Þ¢znVá‹…I+¡u˜O'Åʈvï!òñÞfd-L~H¸Wµb+zö»ß>l×x´{9Ú’M« À{QìÌÈb})î NQ‘fô¢Â¤Kñu‚fÍ>„.í¤í±o¸hç!sÉ>« 4%¼­LÛs`-€Û€»]‘Ç’VóKH÷ ¯ªŒ¶ø®!£|YNUç­›|ý¨$Ù÷Ñ» wÇNtÝègø~´½š•—¼ö¢•þûb¤yH BŽ÷þ½íêG÷V¢idQîLCog' ¯Ú„VúƒUïµ’—ñK;%¡m=„îDäevÐîvœŠ¶s§ÅN[rUJ.O  j JYy$IجžžF» iÖ,¡ÕúyÀ±è”Þ ²±N3š^øËc$IyL wÞAØW“‡Ð‡ên6£©è6´ÿ:1”ÐyŒéèÑk&*¯5 ÍÆòd-Zå7{Ko,yM çʫРmÖž1›µØŽö w „ðºG±½aJ }è¨êð÷%v £Ö•OWåÓ ¼ü”Êg*0Îìÿ£tèG;M_"§ï—Ìs¨šƒv ²¶@BõK÷ÙŠMh•?×ç7Ò~0 /ßBõމ;œT)‘ýgó¤íG÷:."R­þŠ0lZ Œ]mÈ¥Ój´Ð·%ö@B)Zöß‚*Â~ül±¹öíF߉)PðC1jyÏïDe£Š–ô·¢7G?y,Qí –¨dô¢ØqA­>EÁ~ãç¿ùô8•ÎÌ=nײ èg}:~ð0؃èqàŠs&¿HzÐÏöô³vø#ÀH:Ð6ÐgÉö¹u§À¿gÉåažvx]*Hz5Z+pÙ±¸æôÀ'€Æ”PåÙ+Ñå—^k€/£ŠÍi¿8'€æÍ®Î#}eÊ‹j?pÚÍÉõÑݤyhÝDàRT·njÜ¡ÖvtžãÛx™µ–xh_ 8 Õ±;›üß‹mªžtZÍ÷i~<$ëPTÔòTêÊ·Y“QF¥ÓîDEU_Ž;œüð`g Z'8&ñdМ2*Äq/z¾ÏT½ý¬ðÆdô«³PåÛ¼UÌIJª˜¼½n-÷×qcó^'zµUµ@fê嵪Z÷°Zõ1rRl3+<ÄwZ/81˜E~+ôô£z†k€GÑsýKQGTpžÒ§5˜‹Î­ÏA…7³6K(£‚¥ëg€u•ÿ†OOÙ0•ÚžªïN«|Ž@…;cÚƒªåöT>›Ñ´~+Ú²s)æ ûºÐ"ãt8i"p8ªæÛ5è3%’Nôˆ1ü1£¿òéC»—Õ„÷ ª9/¢7»Ðªüó•?sõÿæ+Ñ‹m‘*RIEND®B`‚dkopp/images/run.png0000664000175000017500000002646315002404054013421 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®' IDATxœíy¸Ve½÷?ë‘—wÇ˵_ù‡Pq¨ÌŒÈ)§Q©ˆRËLËY+-Q4ÓËYlðäìISÉy Å2Í©©]Àrà5àYààÀ*ÏçŽä@ @e Œ-Y–Þ¬n¾¼T²,‘ˆ šœ¾ô!S®NV”,K¤¢¨Ãk)e bd °?ðDÙ‚DÞI4Õa,ðs4ǯ"À¡À-e YÇÑT‚±À¯€MË$ƒ€iÀBà…’e‰tGá3ø5Õýò÷f °ðhÙ‚Dü»‰"Ù¹–æQ~€ÁÀ­Àˆ²‰D:Ÿ§z ~i \T¶‘8™6àU`tÙ‚x¢l\üˆXÑ6Ò"4Û U¢ýp:¶j¹Û?^Ž£ùîM$ò4ÓK¾;ð<ŠEoËpœ K€CS†H¤iipðù*ìNÀÓ¨éF¤µŠF[£jLM¨Ô kg¢á¾F ÔÚ½±E·EªÅHT°todðGõ±Í `>úÐÌ&Gª>8Êß ¸ØÙóy"ų p;ðà2T~­/ågÌ~‹Z Y³6ƒ£Êàlà´‚ÎÕŽjòïZÐù"~ü]™Ž}$\CkN?£ÂkEU5gRœò7ŠöîŸ7’/ã‘âF>ïÿd`05‡cN €Ï9ÿúh=J:$û ïΘœ; ¸8>çãz§j Låo0ó¿±È£±¡‡c·:S€»ÑóóA •:Ÿééø^¨’àt²)ÿrà^$´°¾ u8Vpðྠ2\MŸE«Ð[gj©ü ÎG#k}ÄâI’$ôßéI’¼åø{3I’Ï­çøµ$I®ÏpŽ¿'I2%Åu N’dZ’$÷'I²6Ãùzÿàõþv¼·'I2©û™ä!Ç”îãæu¯­¿SxþB_8 ÷/*“}Ûz¶«£šö×9žg0úÂô· < >~Š&Q½µ—"Œ\r÷#?û7ïÝ•©ÿåï͹À©%ž½„ü"ž†Ç…†òÏI¹}ø îõéF`Z܈’‹NCÑf>èðtÜ2|u:Ö†#SQÒJ…mÊ‚ôó;-ðëÍDCm—óÖ’$¹*ã¹OJòæ4lÀóêý•óu®M4=ŸâÜS“lÃþI’’$ÉØ$I†v_ËÔ$IÊx ANJ ß©nò›I’LÏxþZ’$—e|ØEý®àyõõ”$Éß<\ïÚ$IîI’dB?ç–èp=þ%ݲ÷w]Ç$Ù ûi<›wü6H’ ZƒŠ†L.t¡wd”á-äÚ{7°]Æcùd>*xò÷²éƒ:JªÙ*çã&(;ï0Ô<åÔ4ýºwÏÖw¸ éÓ¨rÔÔnY¬ì¬E¨‚ $Uù¿€æáyðŠýìÓ1ó¢¸)ÿ—,Ë@¼ŒW}¸š`Kà(`´¾rm†s]œ”rÛçh"#bòPþõ­ö»òì /󤎾ö£0ÖG•%Êca*p3Ù ´øæBÜÚ”\ûBú·s÷ÍF3q˜ð­ü Œ¯6ŠM^F3çG sÈ߯ ëø?À¦„g\•4X„¢A]Go¿r<.”mf².tÒJФ»5?qúå!äRÚÅã9:€Ð yra¾üÍã9‹à/¨áEÀ3HYÆÿ«L¡È¦ü žGFàÓ¸>NÉF LpîñÒ å¿%?qÖËÃè!ï–ã1ë(ðå[haë.4¿ ±ÛOVÖ¢Ÿ;K‘â¼›rré/ ¿¤çQ~I%@Y`:ZÈr9yÊßà—èEΚ¼ U þ"p9úÒ¯ÍxÌ*ñw”“-š"¼…<ÿ»€sŸGþ9ϯ¢5W#PCïW¡”aÆ v—ù`™ÊßàQô»¤¿„¾<‡¢µ…¿æ(WUù3z.þ„Ü|YB€â<4ÚòÁ d3»Q‚(ÃÜŒºôX Aù<†ææ{¥Ü~p,ò3ϧµ¾öiY<…BŸ6£ÿú|.øTþ•3E€I¸%÷t_BÆ#þ U’íï&>| 9‹†º‘y ÅüÞlIvCpÅ•«”(Ú\½Så‹ß”»4Ùy=¨ÍÑÈÔÑ‹û5à›h(âÆ«È<|·JIç Ÿ{‘äaþ‚> ^)2hú ZnH ûCTþÞ´£ˆ´e4g†^Ù Ž@#È´†  åïÉçPF¨K„b°'ž#‹Lžn<_ û« ü è¼…Då÷Eò˜lÙýçú\¥gQ®òƒÔ¾€d·2ø1n%ëRS¤ØÓ¸ý (0&éÉ ´ º#ZPíMp8pF‘B Àm(IÍÅlŒçë(r ð_¤ï±ö0vƒi=j¨‡ãÈ­¼er†˜+1-b[§È#òÇÜ%¢80-j¤åÓ(|4i&\S–ÏÂÓH ¨)€uõö/RD"å2•ª³Nò Pœ°Z¼‡p‘HÌA ƒ–|±x*_”è]}Œð"E$·aO„ó’Ž^”X¶þMÞAlÅivÎÅÖÅ%|~½eVa[Åü&ÕéZ‰¸°[G©Ñ>„(2À²°· J—DšK˜x»Š4÷·?Õã‹Dš• Ûz)S¤˜ƒÝýq"ÑDšËʾÏX‘`)é[uõäD`VβD"e3ØÇ°ýbBÝð|܆2'@¤y¨¡¶ô–DŸç}Rt=€¥À{QW+† òØ‘HU©¡~Óû}™¦e4i`/ Ò rΑH4¾ü‡÷{DÉ2Úƒ¯DÍV9îî½"‘²¨¡êF‡8ì{M¾¢¬£¬²àKQYèé¸üìŒu<œ§P‘ˆ'jÀ¨÷ƒ•åÈh¬ÉS e6Y„ŒÀ~¸]ˆF >5TéøHÇýg ´^(»5Ø«h=`:¶ ˆ» ãñHžBE"9r)êïè¨ú‘·jÒeP—çÐHÀÅìJ4‘0ùêóèÂ2TrÞkÉ ¨Zp4‘fâ{ÀñŽû®DÊÿr~âôMnÀ˜‚zÅ vÜß[養SC‘g#P]ÆvSÑX{éBÙi+ÑgY÷¯›”Á,ä­r¡¡üóò§B3ÝŽ´"5Ôdsò¿õcƒ}¡µ …Ÿ.B=  ŒÎWˆ†a ÎŽùè*¤üOä'ÎÀ„h ›è6¡uÊŠ&£>…;“¾ò²+¨7â(Ÿ}‘çóU‰³qoA¶ ØÝÛÂÕ€Ú*ÝŠ›Ø•]jV¶Bµæ?×ý÷2y ¸•¼~¥dYÊätÜú^‚¦_ûâ¹ P_„lÀÝ|µ…j&†_AuðCdŠv»½Ô­ÂièëïÂjàS”ÏRF(°…»pó¡Z‹†Ì(úü¤\¡*?H¶k€ÿ@2çÙÞ;TNÂ]ù;QŒÒ‚ÙB7 ,@+Þ»ªÀà"T6j&þçöy2 Éü;t ÍZåù¸§©7”¿ÔìÖЧ[£Õg‹¡š|”ê®T·¡¯Ê <7†,U¨²Ó…4Ïèl´êòíD1/–¢ ^ÝÜ<i©£¦‘ULG$—-ˆ'Þ@M_]*C…Æ¿áV¾~ RþŸæ+Ž!€í€_c³°—£Øéª1e‹}²lA b.zNKÊÄ‘vÔìÖúõ_|–€ú^†¼p&6ù–S~?xBåžZEùA£ºçñØóÎ3#±ëNrÝ£ünóñÀ$ã>g ÞñUa(pðù‚Ï[GÁ<«Y—c>…£¸Â0àÇ(òíhÜ Ä”Árt-÷ê[8õ u p;¶ši¯Ä^v¼,¶~‚§†Ý,F¡» P°Îâî_ý/ÖbŽéþ½…Î4ݼ„ªD-ôx޼yMSÓ2­úEˆ`Ê´X×ýкöÃ$dàòîôÒV•ü’üç×£‰(äx2ù»%W¢ùñ/r>®/¦w¶¯£ÔK~Äq#Dpê ”–gÛ¯ ®/¯©W'ú²\üÉ^ÊFõÁ`d ¾„æóm9· Mª’Ìe\’Õ‚!40E¼ 1ìó ð§¦`&ÊËc޽Uš¹ {çå¼VôCÏ/+u4_¶¶Ï.ƒIØZÞuaë ì•P ‚48Ûjø3¸ç]ÉÙ¨VAÖ›½yGDA(ÿ“ñxyð?hÊq)Æ›ïM‚Fµîã†ÌïPwŸ´ë#ƒ€¿¢‚ ÉÔЪð?ö9xÑ8¹‘%V¼A'ð]´ÖñðfV¡<ð&*^yRâ d›êìü 1sâÏÈ §esàb<Öù³ÒÀ:œZlIØ!¿‡!…È2ìU”}-‰Šc nš˜áu4gþQ.ù¡†b,`¦­!YË&ÿ€°•R×{¼ ¥þîEõ”d ÷B×àêã¯!šÅˆø¦ŽÞE Á,†2ÿÒæý¯D=C Ù ­»ºú棢&ÍRmgªë0Þqÿ•ÈÓjœÀôþ¦]íBïoÙ ¸™ÝQÀ‘6²&ÂVôã:ÂUþ¡È?ìªüסÅÐfÉš²¡Ñ!û·£À©í ó¹¯f“¾ ð TÜåbo¥Ä:‰ü¾{"k>†âɃ ¨èÁ¸…÷VÉý•…,îЀ/æ+Nnl…¤Ó^×<”¹Z*i Àôr~’òóCBäóÈXéBsåÙùŠ,£ÊA.ïÒ€›ò'7vJ¹mØ …g—Æú¬ÕTìñITŸ¯låR32æXéBóýVQ~еˆ[îÆeøÍKÈÂõ†mkØò]¼0Ø%’°žíŠd5ª@"W`¯£/UòòätíVOÎ0Ü m܆míæ3¾IKŠýyi6²@YÒ0—0¦á–Ïÿ-ZëËß›ÙèX™‚îyh4²Ò²%ëX_à‘çÚ™Ç'7—-@´¡Â—V®£ùüÒpnFð"òKBÊ“[ ÛÖPfeiô^Ü xˆ0olðŠËxK‹KCˆùÈ-ÖL®¾,´¡4kœÀ(Ç"$†!½uàVȦŽÞŸåÈÍúï(jÔ;ÒÓ GsþPX~„æŒ!1%„Xª÷®B 3Íä“ã€ß`¿—›@@M/¬ÅlóäY´5›˜žS€ó WùW#ùBãd쥻¿NTþ¾X„î…¡è„ÆÝ%ž{[>ý*)j.6FïG ¡¬ö÷¤¹Œî*[^Œ^Ç6]zÅÇGúçl±ÿÀ¦ÀR?â81 …‡ Ow¡‘sŸÓŒ†€3CØžt"÷ØG OùA_+‹òwâÖæ¬Õ°†A·_õ$‹+^([ˆn¦¢òúcûúÏ$I’¡Àb/âðškÌCÖ%Ï̼.´¸j‘Okòhµû?â4ç£á´¬@•vBjHú=Ô:,–  ïÈ,M’$±7uÞ=ƒ°Óq}òel‰ËÑbÕJ?â4íhquCÃ>‡"×j(L~V¶½XˆòÞžÔ€r!jÀѪÊö|îYDå·°õ´`­'á›ÇÏÍ»pmÏH’$±$0,ÞG¸Có"Ø xÙ°ýr`žV—iÖûPˆP¸8ªl!úàíÑR ¥ô¦åZZ[ùÁVÿ Ô¯0*¿ÕèÞY°>ßÌ@A_¡q>Ýk~5ló¬/¦h,\„›¸R.Á6Œ.=»®«ÐûJŠ`‰zT$I²–ô.ÀѪ«2X¤å6”êqÇÚ&ns ´ŽšˆäÕQ©ññÞEZsw^6„ÍÿßÊ `Oܰä‡GúæZl`2”ÚêƒÀ}Ž{1òñߌ­KÑX`BhÁ?¡c‰âë@‘‘l<ˆîeZZ1Òò54Õxʸß>Ѥ§ìlØþ^š÷U•5Ørìw&¼¨Ö"Xš«Zêelߊ7Ê•­°Íß,MN"ó€aÛaèYµ"o`«­°E4é™`Ü>ÿóÃz/­Ïª™xȰí¨hÒó!ö‹QBH$–tÿÒbyV͆%+²-€ôXz¿=ãMŠÖÅâ~¶<«fÃâ©«Ežq†mx“¢uyÞ°­åYµ4ѤÃ2j×¢*c¹§ßíTÄ›”ŽØš¢,ö$G+cé<[ˆ{Ë @:F·Ë­õž†ÖÓ"H¢H‡%%µŽ[™çÈÀX«NåsßÔDKåßb΄êØB‚]Û³·ѤÃR/1æþûÃro­5.[’hÒaYŒñÿþ°£ ¡“uðD‰´0ѤÃòå ±©j³`ùª·zéºTDŽ8÷ ƒ¸“3ѤÒc=Œx_}PÃæÚ‹eØS_ÔtXüú5lq‘t Çö¾Z\†-K4é°¶ŸÞØ‹­õž†Ô,4X¢HÇ2l‹Jc<ÉÑÊôÙܲÖ †,‘õ @:êØ|Z9Ý–{ú13ѤÇRk¾•+Òøâƒ†mCì $5l–²•£«,ùè­\“Î;¶õRRÃÖz©•S,-U~Æ£=ÉÑŠŒÆv?cE¦”Ô°¯ÜÓ— ÀZço/R´&ÛÇšŒ)© ÛL뺸^Áæ[ÞÛ— -ˆµ#SH-ƒ¦Ç×ÐÚ.ª4Œ¾fÜçJÂTþm€³Q,C[ɲôæ`U_#€Ãk)…‰Ô?uàÿVlÀHàulöAla­­Èب:M ¯Ð4àFÂS|>}xe z+€OR~³Ë²¢!±}y,L¾ìA–fá0ìÑ“—žòoA¸Êp5Ýùz҆܄Gcó3æÉ ÀK:wŒ~‡-VaðaâT 7ã€ß`¿—›V hªª¡½+ ½èÉ8d¥?Òý÷ q·t#H¿:¾xá5|88ӸϳÀŽØj143mÀ¯±\ÎÎÊ_œL þL˜^ŸN”ÒÿDã\ @ž\bØ~/4‰!(øÂš&=¹]# @;ظÏ(ð'4#:¸»l!ú |-þ½MÙ5ï5n¿¿)²±ø¦Ã~có"4+3±+?À „§ü % 5h-ï–ÞÿQöT¾)­³MBìÀ{7vI8Z‘ƒ‘§Éú!š‹¨C£øaÕËXŠ2Rû¬eQö”Rœ–aÀT_‚däXìÝhjhÁhZþâÏttíÖw°ƒp¬¦Žò×›P5å~ Ù„`n7nÿ/Rdg n/æ TeÉe\UnÆ-2îXt¯C$„w³ eEn¦#6H a ð<°uÊmëÈõ³Ø›4Ùø1pÃ~uà[(d¸™™‰âÖ]>>!º‚lÞã¢YƒtáYD5ƒ[408ß°ýyHYBdð4îÝf£Øî¸²Ð\…ûHçôU 1ääͲd}žB¶µŸ:Rþ2Tã ÅŒFÉ5i¿ ËPòGˆ‹ H°§q÷ÏGf ‡¦z®AdHùCËTm0øÒ?ï5À{  ak 9Å¿?8À“,y°Ø÷ ¥ñ(*.Ôh2 _F×âªü]h;Tå{ÍŠ¹ üŽ{>÷ ^¤ÈQè´ëðl(Z%}A«Æ8$û5¸—u«£éPhÁ_=©a¯ò!ˆ !€¹Ø’:¶%ü²ÛW“½ÀD´¸4‹0ÃK{Ó|Éœõùœü(³D~™Ž­ Ï"àaO²˜Ù 5€µ(yÃ>£Ðª{È<Ž ínŽ1øp8 !õ›ÙEË•!èKx;Š7ÏZüâ,ä-ÙÀ?¶?—±øeÊ"`ƒQvååù(Õèw"úŠç1êZÒ`/¡üL¸¨zï1Ø*$õG8U K nPöâ&T×"¤À_‘?ÕRÛ}$}Ä8Èh±ód7ïv¾|øò¯ÍxÜ´ öE_³+P#ÔwåpÜ.Tþ²Žå›ÊùeØçjàN?â¸ÚTãì׆íëÈET…Qh^|'ùÏç;PrÕhÑ,ïh¹Ñ¨4Û¾èËgiו†•Èsò‚_O¦aSæ:êZôšqÜÑ€Š’îjØþ.à3ždñÁèåIýèÂbd Ž9‹Q í@mÜj¨¾Ãh` fú°~»B¿€”?dW_O¡Eέ ûÙ&.Tp6WÉàÿR­è¹!è]†³PG_ÛU¬‹S„\uíïšÜ¥¡FøõÅ1ئ)u´V5ß8î„hÆabVµ)äh÷:t:âWaý¦'Ãߢ‘RZ~ |Ê8Ù)Ü•Âk"š–[PÊæÜ²)»Ð5WMùAe¾-Ê_Ç^2®0B2Y”!å»Ã²°¸Øò+0ûä tŸ!Ü”ÞØ8ʸÏ\^ Å 8­þºƒgàüÊ—Q©ñ7ÑœÑÒ7dV!—ᔓ2›5àlA?uàsüq a0¹®\•ÿ&µЉ¢à6¾Oµ6{Ó‰®a3tMU¾–±'4]‡<ÁR¶ØŽlʪ¬ëœ0ËP±ÑMQýƒ`¢ÇR°8ÉþMþØÎaŸ¹K‘3e€í€ûɦü^Ÿ€¼YŠŠŸl„ ˆZZ“Í<$ãFÀ· ¯cO\z_\„ºKYnÀ¨üÙØ ]ÿtÜ+åÅKèyÜHuy\øªÝh¥ŽêZÛÈB 1ìw …½ ;lUåïÍ8š»°ù$ä Dª2{?pÕŒ½p¡U­rùhÕQÂÔå¹J”E€¬Ê * •¿ojht0…ð¾åªÁîQh›| }å wÖ+4çšKÃ^¸¦A¥K_œŸ8Ù)Ò쀾Qù‹§ã?}ÁÚQ(r#íº …â®D_ø¥¨dU«*ú@¸ô‚lPf ÏHeâ—?ÒLœ†"]9¸0'Y2QD ÐÆÀ#¸ÏMoB¥¤£òGBáQTƒa/Çý÷BµÍM"G|€ª˜bI›ìÉMèË_T¡‹H$-ÿ”ÙE‰öÞB©ï¥áÛ|¥öºÐPþ8„ʯѺÉ$ÜŒÀÇÑ:Ì#y eÁ§ ÌÁ­Yâ hØ•?:óЂ龸])q:àÓ…2¿¬Ü@ó†÷Fš“§Q¸ódÜŒÀî(ˆªð¼Ÿ^€À6Æ}¢òGªÌa¨Ê“Kˆý*»QhÍ@_¹c‰Êi=®F-Â]Þᡨ2T¡ø2–‚ž ’IQù#ÍÀu(!Êå]žÔý+ _ÀR×îV3 ‘Ù¸®œ³,âËŒ1l{Í‘/‰ôä&Ü’Öv§Àf°¾ €%cê%O2D"es°?6#P¦ú§ï“•MŒò‹43sPùs {ú¤/|K‰îÍ<ɉ„ÂÕÀ/ Û÷$Ç?àËüѰídÜ¢#‘*aqñ@nAïø2–ÒÏ¢šw‘H3ó”qû^¤è…/ð„qû™¨¾]$Ò¬X«:»!5ã˼€-¤Á¡\¡IDAT±ÜL«Ÿ‘HÁX‡ô…Ô¿ðé¸Á¸ý TuuŠY"‘²±6YîEŠ^øL ¼Ž}(³eþ4w‰²ÓìlºÜ<…*äÆ(ÆÈú¸•OCðOeyß5¿œè°_'2÷æ+N&Ž@×Ó»®á|äçµ.òDZ‡=PMÌ´#îGÝü‰³ß@gãÖí¶ ¸}mCàL”æÙWQÓñ¨2Ìeø¯É©Û·cӵŠƒø6+qOŠh~LÌU";§wÿ¢ü¶ûÏAoq¤ M¿ q‘å@£å½õÃð²ôI¡À¢†.´¡–Ì{ä'މ™ØjÀoˆF/"·f¡ÖÍÀ¤EFö?»ÿ<‚pïñx4ì·*ÿsØê¾ˆ²à ùq ·yÍ à³¨öÚëy µN.pÜ÷ÝhÁçÀŸhîžy>ÙõÔ»x°Aÿ{7ð)`KànÂZˆ<ˆÛBÞ‰ÈBÑ­ÁÎNuÜw5*¼XÄüèxÔÙ5/æ³PÍ^ÔP™„:è¤þ…Ô,6Kœç€Sà;RFsÐYèëêÂ*ôUõiŽ~€Ÿ¡åkȰ\‡®%²ŽÁÀhºhõ™CF`'àç¸)ø·/«=xV#°/ò¿çÍ1À%³8zJ)¼l`l ‰ jf]Ü+³sôÎHù]“x¾ œ“Ÿ8é(Ë€¾„Ç;î»Ø›|­åH!‹^Tz¸õ?l•QAŠø< ÷ó¼çe]Q,Wå¿ ^ Ÿ–i@Cí¯9eÊ#ç˨ís™+Ê«¹('â>ÙL Få®>‹<$–ªQVŠl&»;òT¹*ÿ¤ü¥<ï² hÈ}œã¾È<“áü‡×–;i%šÓ΢Ú„ ÑBÞ'P݇"¥Š0{ åw­g1{ɰ\ ÁÔïü(Çý;P %ßéAÀõ„¥ü=éDÃÙ9e ’’v4Þ )Ƕ”{o}‰Èýèªüe®W¼Mô’\{#Ñh$`1?Æ-j¯ŽV«?ˆ¦>_òN`{ ô ;ÐŽF+‡PP»F`R~×k-rŠ2 E­·PâÏ{q«‡ö.4·ü°4ÅöÓqWþÃQ€Ê=èë<Ø·¾pëcðôÅ‘vÔâú“„½5*³}zϲ²O÷±\•ÿ&4ò £n’$!ýjI’\“$É[Ž¿¿$I2q=ç8,I’7¿6I’#ú9î„$IîéÞÆUþþ~K’dPϧ¯ßU®×ÇïÆDïW–kÜý,\e¸>òþ1èI -Êâ¸- ]<‹\kÿy÷ Çý*pùz¶œL#ß©Á?“ntS$ÃQ¨óàÎõ(š²ÍÂ}1ñ&4ôvq·}eõ¹~ù¯#ÀX!â\†J!P¾\jØgZ'8˜|*¼n,Éá8y2ù¿}Ñ…ŒùE¬s÷62ì\݈.Mh§ åw5t?BÓÆ ”Â]ý®£‡tSÙ‚tóMlʰ8ؤ{ÿEy ¾|ùKóн;wÆzÌG^ŸÇc[=?ÓȦüW¨òC¸Ö[J–cð/ö_|e­í ›-È'êhw?¤øß¢ÿþE騔—«òÿ+šv©ü¶ ¿Hy+à'ßÍéX=_ð÷¢u‚ˆr!NAJo1ó‘ë7‹¸›¾ó¡´Ü›q÷l\ŽJÅ«üî@oƒ§xNßÉ£?¶q àóÈjå9TòmÙ“¡& ô[×éÈ*d˜_ÞD†h`T™.EkFA+?TÇ€ŒÀS6ü,à Ïçh5° ë ,ÎYެF Oþ÷ X…ú '](ØÇçt Ž¾ü¾•¿Y޾Œ‹=û4}päÅ÷©òCµ hnx RÒ¼‡W«º]xNv$ž¢\#p!òöTŠª⟃ª§äÿ àC„nIGÃXÚÓçÁhÁ¸rTÑ4˜‡ê§} 7CPF/ÌÞØzFÂå)ä(ÊœG…=:!&oX¨³»ãÏ tÔmè{Ahr=„:š18'²Î¸çLË9h:ZYªnz2Ÿw¦G)¡”Úe´NÉ­ˆŒÀž(T9ïF"uôÕ¿0çãN3€Þ¬ÀÞ“=Ò\<|¹*wÍé˜ËQRÏÜœŽW*U^ˆDÒ°ø8ŠÊ[–á8u”Ñ÷šDù!€HkPG\6E†`>éÝÈË€‹Qg¢CÉfD‚£™§‘HoV#Cp%ŠÄܹǠEãZ'ú#ê=8M#‚éu%€H«²¥›‡’r^ q ‰´0ÑD"-L4åaW†ø¬,25í<ºÊ„øRµ Æí‹ìª“‹LÖë@4åÑ­1ľÉÀ.†m›Ê}Ö,DPul HGÖó‹­8KL¶ ^¨VäYöÛgûÄÈT/ÏR,óß=ÉÉ@4åòqûSQü"qôÇÆÀ#ÀvÆýô K$#Uª ØŒŒDu­Yo ¸ô§Q“ïö’uOÔ±ÉÚ÷YT»!Ñ”ÏO€©e á™#QüH`DP>€'iÞéØÔ99º¤Y_º*ñ åw?òÉÉDå–8ƒ çÑ<»™¸ •i‹J„Ár`š«gàB”? ˜ â ~Š”NªþP–?•-Hd`¢‹PÄܪ;:[ˆ”?FþU€ª¾dÍÌ Èß¾´lA˜ ìHTþÊ @˜< |õ;¨BíQƒ–O+1WŠèŸmhm ­dYzóp òY]²,¢¨í¨oýnÈ(ŒAùøƒñ?’«#_ŠæøO÷b«® ÿš@M|7ÒzIEND®B`‚dkopp/images/quit.png0000664000175000017500000002232315002404054013566 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®' IDATxœíUyÇ?¹l·a'2‘ÉDÌÄè`Cˆc S~) UQPj¡Óiµv,ZÑRTƱV;jÑ*¢5àˆ5è A!Ò4 1©ÒcšÆ5bL·ÛKÿøîKînîîÞûœó¾ïyßû|fîè=çždßï÷=?žç93f̘SiŽæÏŽæÏæ³[>³€™ÀÐ7öiŒõÑFÇ>€ƒÀÀpËg/ðs`°ø °ØŸëßÎÉ•n•`X<ö9X8ö9‰»L†Àö±Ïf`ëØç@‰ãr:À =€cŸËEz[W…&° Ø<<<öqSH7€ò™ œœ œ œ€¦çudØ<Ü|-)œ’p(ž$ø³€—K¨ÞÛ=M` pðud>C(7€b˜œ ¼‰~f¹ÃI–ƒÈ ¾ ¬v•;œúã €óW+éÝ·¼•&°¸¸x¼ÜáÔ7€¸ \\„Öó.ú84ѾÁ­ÀíÀ¾r‡SÜÂig—¢i~¹Ã©=#hyp#p/2Lj€c€Ë€ËÑtß)žÇ€Ï à$§KܺçDà­h}ïoû4Aû¾_òX*…@g4Ðôþhmï¤ËÀÑ2Á—Óà05}Àë+Ñy½S¶×ŸGHNÜÚÓ¼xй¯ ­É>M½!cŸÖd¡º°x?p3n‡á0žp!ð^ª#ü,çq”÷S^»eðeÙ|ûÑZ¹úQ–a–I8…,Ïž²F2R§l®n×OápˆA´v<¡ìLÂ^”X³ x Mq·ý÷2™ƒ’•–ÇKQÓœ25Ñ^ÎÚ²’nz`?ˆÎòSá Š‚[<„²èv–:¢î™2OB‘+I+ú^d›ÊH™ô²Ì®® ü5ï0|%Ä—ó÷J ` ÿ’œ¿§‰6õ®EµïòYŒ–z’¿Ü„Œ Ƕu1€EÀÉw£¯‰Žð®Á…Ÿ*‹QÇùäk× cÝJS‡šu«¯øïE—t¼ÊlE¿£¡ßY^œ€ž¹Õ9~G!TÙhþEò[ïoBõûÿx4§ïpâó(úE~±þG¡gïZ*¬£ª.f¡ ±çæÔÿ>´¦ü'|W¿ê4P¾Çµ¨js¬A• ŸÈ©ÿܨ¢Ì¾‚|bÓDgøï¢ü4['.sPaËÈç½8ŠemVÍ–¡[còÝ‚âÀ‡rèÛI‡U($o»ÐíO•Y.Vií2ˆ.”Œ-þQà}À qñ÷Cèwý>âG÷ÍCÏè`ä~s£*3€ Ï?¤wŠðªŒc;QY†"F—FîwxŠIš#*`WŸ&nÑŽ&ð!”qV©5›•Ý(ak&Š*Œ%†#ÐáÏMÝþø{â.Uv¯>IM<œ®E•˜†Ð±á¬HýÎ^ŽÊ»}/RŸÑIÙÞr²cp-ZŸõt8§-ÿ–/ž©Ï¨üÙ“ho 9R5€kQÈm,šÈP® ‚gµNaü†C‰œF¼—Ïéh ûíHýE#Ex7qÅ¿ø#´Ö{2b¿N=yf]ŽôŽŒÔï)$8HÍþMûc± 8ƒÄ7bœ$ù1Jþz)º%§¿%¡=” à ´ák@÷ xð_DêÏé=~‰ö–¿©Ï—’Ðé@*p!:ꋵÛÿQTà"õçô.#è<ÿéè’“P²Óm$°‚ ·眿 ¼x¾Þwâñ$J/þz^CE3%²­G÷:–FÙ‘€ËЦHŒtÞÕw{„¾g2.@Ë‚Q©ûS)1µL˜î½‹Ûíôû…N _BeèBÙ…–¥D¤–• 4 ¥ôÆÿ0ÚXqñ;E±=s1´ÃLº¦ h )TŒ|þa¾éUy¢Yž½&°¸)\elþ-ð'úÙ¼ŒDŽSœžäg(à5„ -FID…F ½°ÕQ uºacíâwR`2‚Ùý4‘™Ü<¢)Ò¡Jª¡;þÐú˧ýNJ¬¾IøZ~?ðûTr¼¨5Ǻ;Tü#h·ßÅï¤Æzôl†Þ(œU.dS°(øáUWšèœßwûTY‹žÑÐJÒ' ÍäN›€£ÛZBy; vœ”ÙŒ¦ñgöó”´1xDS÷ÀBà§þE·Á:NUøð}ìG·åv+qžÐ|—ð+ºïAõÖý‚§J4€»QâOë? §òuy.þxc`›PJ¯gõ9UãItcй„Õ˜ü/*R¼fË K˜Ø‡ŽCJÍ–rœ@ŽEÇß!×’/F·E%S€~T~+DüMT²ÛÅïTèYYÂÆÐT[òX¼¥L†ðtGŸãÔýïé}<-+Ö¦…ØK€%À sªµhÝï›~Nh_#ìÚ°t­Ù–(#"®4PqU}ìFçŸ{¢ŒÈqÒb.ð¯À1} ¡""Q^1÷.!LüY¤Ÿ‹ß©+{\…´…X{³Ñ‘GHüò‡ÐµÍŽSgvONèã%À§ÐµcAÄ2€¢›T¬lB;¥~WŸÓ |ÝOi@Uµ¾:{KÑÆŸµªï(:ï÷+º^bŠÑÍ ,-cðY”ëoå}ÀBá8c7:-;ÅØ¾‚Œn Dè `øz@û-ÈÅBs¨§Šô£Ùó’€>Î" E>Ä(Óo™±}g Yà85`:>·žÈ=Š2M' !Ç€b?(ÒÏÅïô:C„E½.CZ4aô¡| û€ç{í§NÌ~„=ah;p†S4ë àbì⸿ãdìEš°²i²k,3€Ð·ÿ&îë±þŽsˆ ¶ÖÎ4Í,3€×ööÿ+\üŽ3‘&Ò†•…H›]Ñí  üöc‹{Q¦Ÿã8íùö‚¢[€çÓÅ ¶ÛÀ9ØÅß®2¶uœ^á*ì3ä%H£Ó­\ÙåÏ·r9”4J€yè>¤î›3=èþ¼…äP'!6 ­XéJ£ÝÀ‰Ø3˜šÀ5ƶ)2x?ðstAäfàè(çmÔû-šAtåÖ¯Ðæó¿ýÿAhÔ‘k°ÏNFwvD7R—ÿ6`k@û”XŽÿN/ì°ø0ðš8vÀÇP¨ùŒOš™‰ÊÎ=BxíýÙŠ4cå/;ýÁN7~ŠíÍÖާ°=ts:øÙí¨ÜÎ\GTO(ɬӳí77ç7œRXŒ^4–“ºàÙ(áhJ:íü2ìÓÚ/SñƒÞHˆ´Vý6ªëîtN·âøG:ÿ½T…­H;ú‘f§¥h—*RÒý-/nÝa?èê¹K¢¦|B´s9è»DS_ CÔç*ï3°MÇÜ:Ã*þŒ3#Ž%ÖcO˜[@ˆ;y /5tAb]xV@[7© ?Ô÷ß6DCÓjw:8Õ.³ð8p—±mŠü6°½›@{bˆà@„±¤È]HK^Å4†ÓÀØ7ÿn ^1ÿ1jº Œ'–ø!°6^Â4±WËîgš[º¦3€‹Œ_©¯^5Øâß}¦Z%¬šj«'ûé"V¾pÚ1¯Óú¿•*Ål®Ã6^ŠÌCü§cß$« à?±…˜¯^V-Îk0¡FXXBîÈÛ\üõÀªÅ¹ àÆÆu»þ;Uò2¸øë‚U‹Ïèæï1¶sº'3ˆs:°øö `&ââ/«ç4°€- Á±{&àâ¯V-Î1€ac;ÇNlÅÅŸV-À>c;'ŒTLÀÅŸV-ÎnÐæÆÐyÂØÎ §lpñ§…U‹VÅ –MY&àâO¤Én1€ß”E›€‹?],šh`» ÀßþéP” ¸øÓÆ¢É>«X¦N~äm.þô±h²¯­¨@zäe.þj`ÑdÃ"~'mb›ssìãÔ¶_®eÙàäKìÄžŒ²¯!s:âÉfãÚÁÐÆÉ¼ÄŸá&>¦½<«ôÚ8ù·ø3ÜÒÆ¢ÉÑÆóCC'>E‰?ÃM ]LñÀTÔMünå²[)ðQ¤u`¼6sš±]Ý©«ø3ÜÊã4c»M´TŸxì7dìôLc»:Swñg¸ ”ƒUsã4>Ñî7vz2~[P+½"þ 7b@š³0Nã à;Øn îÇ—½&þ 7â8 ÛÆ{iü)&ÀìaÁ¯4¶«½*þ 7b°jm+j€¶ ý]kìüc»ºÐëâÏpÈ«ÖÓv;ø†±óùônr‹X5vi{í à@»ìóíªŠ‹jÜâcÕØ:ÚÜ>YúïWŒ_²ˆÞ‰ tñw†›@û€/¿|°ÚØ6EŽ lïâoO,¨k:újì×ixR:yCßhür€·´MŸ´uñOM Øi,©¢¡iµÛ‰¬Åþ஢>UƒïÃv*ââïŒPøfı¤ÂJl›ð çmÚÌÞN   Ü`À;Ú¦ÄvàÞ.Û¸ø»ÃjûÏÄNé„hç:xaMw ˜r ÑŽÇ^h$%Ž~ÝÁϺøít{DøÇÔÏ£ <-éÃwÚùnd¡\ml›;€³˜~½¹8¿•l&ðÉi~nøKê'~f¬§hwÐa N§3Ð%T§Yhð.àŒ ø ð à£Øk*8ãy9p%Z g‚8ˆ–c×b¿Ò.eBÞþ¿OKíÿ©èƾ‡½éíÀkmS¥2Òf£ZkuŠ|L£ÐqØ(2Ú:ì¿Û>¼¤ÓîÖÎÅ^+ ‰œ©ŽŽí8±XŽö™¬oÿó€5þp·Ðþ XÒå 2îþÐØÖqz¯aϦÝ<Ÿ.Ž«»u™&p}—mZ9 hï8uf°Túëé2V¥Û@ðJô° xõ-â8À¿Kí·ÇÑå¿–uÆ(p¡]ÆR”Wï8Î!®À.~&»¾ÝÛ2€ðYÀ>àyÀ^c{Ç©s€ÑY€Y;Lo€#ŒÐþ{ò#Ñ_önc{Ç©¡‹£»6ü9Z>tuZ><‚=O¾‰¢å¬W’;NX…nìµû= ¼ãžZHÁŽ&ŠÐ²Ò@ ÖJ'ŽSuú‘Btx%ê¡{ÖÒ}†\+K¨Ož€ãtËÕØcj@Ú³^æ „-2–?DƒFQ„࣡qœ ± Eü…èæ…èXÝLŒš}›€ h߇Ê:ÏŒ0Ç©3Ñ3o?HsAâû)ÀDÞŒ½.Û\ôb½šÜqªÄûW´ß ü¢bÀAt,x^@+Q&ÓŽrœDyðq DxŽ^ºÁÄØÈh ã k 3P:í Pj­ãÔ¹è¼þ˜€>†Ðñy”Pú˜uû›¨~¹õÐ?Ì-Ôë>Ç=Ó·&þ¤±hy4±–ÿür(+ÏEÁû£ŒÈqÒàj´OÂuLqѧ…˜K€Œ~t¼rSKÕ :ãtœDDyþ!3Û踜©És •t«PHÍ3Ðnè+ñãA'MѺ?´ÆÅF´ë xDÓP”,BÇ!Gösx)¾pÒb%º¢Üš—±o Qr» ¸”ðÝüT±"xDŽ‡è™ i¤ñC1{­<†B…O ìg&p>pðóÐA9NËÑ’ôéúºe F‘K€Œp'aùÐÃèºîïGèËqºåDàëèrØP¾ŒªlïR†€"¤îGîÊ0Êð=§HV¢i ño@Á>¹oúM¤¬´Û'Ptß®}ÍF›/~ç Sƒè™‹!þ]H …‹ÊÍ»ß ¼‚ðHAÐæËÝØïTwœN¹=k¡~ gÿH ¥Pô&àDv£éÏk#û:­¡öãË'Þ†‚|BŠyfŒ 7ÿú2S¶üÝm¶šðè©èzå£ÑÎì“ý9h¦ü(¼7†`š(ÆM„¾‚HÁ@å÷ ä‰: ]—t7‘ (8=Ç,à.àâHý5?nŠÔ_©Àèºð™‘ú[œ‹Ži~©O§·8Åš„\Ü9‘w¢Ë@“ %øš¶Ÿ©¿¹hªõ(Zj8N§ ¢—dz#öy ð¾ˆý“š€âúÌ8¸M½†ð}gjÀUÀ?g§?ãï€÷Dì/ )À·‰;˜œ2¬Ö¿‰Ô¯S/æ¢(Õ·g/*ã?¤k ™ÀoQæ_¬A>- ~üG¤>zpºš.´nE+M´æOjÚßJÊÚˆy:ÚÕ}ð4T³0·rKN%˜‰îêûz&b‘íö'³á׎²rºåBೄZ˜È&m:½Ç2t[Oh¡š‰Œon‹ÜotRŸdlB翱߹¨îÚL´AøûvÒ¥•Ûú,aWuµc?º¹÷+‘ûÍ…ªÌ2–_æåÐ÷tïÚP};é° ¸X’Cß»Plef”U»„óQå—Ç% KÐÆã§€99ôï”Ëô»½Ÿ|Ä¿=›•?Tg ÐÊ~àóhÝö¼È}Ï@5 ÞŒR–ÁãªNøSÎûâïe¬AÅj÷æÐw®TÑ@›,·£ ÀUÄÿ¥‰¦r¯F„EXM/¡:{GæÐ•ñºøŸúϪí´c5p#áÕ†§â^à]Tlz×Ã,CG{gçøû‘±Ü•ãwäN ”øóE®$ŸŽ&pŠêÚšã÷8vïEcóÜßÚ¼†«÷æEÕ6'c ó½)Çïh j0›[ÑÃæ¤Ábô;ÙŒ~Gy>×7¡g­òâ‡úÌZ¹øù. @3‚5Àõx¢²X \‰Ò¾ó~™íG‘}ŸËù{ ¥Ž°Ex…^MÞ)C(äó.üó¼i }Ÿ·¢ à"X"F·ô}…QW¥¿¸šø!Ä“ñtÖüTïЉÇ1Àe(Sï9}çp-ðjš3RgÈXŽN òÜ œÈZ܈N|V`£vò/EÓü¢Œ´Ñw)ù%C/èÁ¹ ÍŠ|ˆ@á¡·_@eÏœéY¼%åö=#è=PO²W c Š/jí8‘mè(ñNjþf1°^ŽuË`åƒl)éû §× 4­¼íÞ—ó¿¸U.^GI7ÔÈp ¡=˜_âXö¢Ó„›è±åZ/@Æl´Ásq.za½}¾|-ê6ýìGSûSPåçU¿›È(º÷tÇdÏÑ˱ø ù†vËAtô´xBi×G™:Ž]‰ê.¤Â½À;P­‰žÅ àƒÈŠ<-膽hß`ðZ§n£ü ´9h;8êrÒM©Þˆ„¿¶ì¤€Àxhçù½(˜¨ ;€ÇÑ,᧨ŽâdÃcŸýt¾¬èG‘”³Ç>sPõ¤¹¨Nþ|`º8#Æ ¹E°Mõo£ÇÖùSáО>à(°*FÐ £h³ñ A&„ÆØg&Úœ+{O$&ÛQfàÍÔ4˜'7€©é^vˆó¨"ãäÇtÒóy\ø“âÐ ‰öàä’ÇâLÍh/g >ÕŸ7€î9%¢œOùÇXŽAVAÕ£q°“%§\Ž6ÄœâyEvzò•7€pÊLXéE<Ñ*"nq9U¤¹íÔ¥âRÙ4ÑÚþVT v_¹Ã©nù±í¼EÁ¹tGEBÞ‰Ö÷—;œzâP óÐòàÀËH+$6%÷¡ÛŸÖ Tj'GÜŠg%Äœ…Ì` ½;;h¢óúû€¯£D¨^ËŠ,7€ò™‹ áT´opõŠÄkeÅâ?€®èú YvJ =PÝ àE(±fÕ›%4Q²ÒtÅÚÃcÃ'„@5@µïÇ£ü„…¤‘Œ“%#mûlF§lÅÅžfïAIDATÍ*šhzÞš,ô‡2 ‡QfáÏÑ”}7ª€¼e:åÿÙ=-ý*`ŸƒIEND®B`‚dkopp/images/editjob.png0000664000175000017500000001744015002404054014230 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'šIDATxœí}´Uu™Ç?\o ±î"†aC.cŒÈˆJDS߉1+MÊ^¬ì•ÑÊ5«·Õršéeic“öb/Že𙕩$¢"¡‘1 ±r†!"bÝîÜ®×ùã{÷âp8çÞó²÷~~¿ß~>k¹.çì/÷žç»ûù=¿ç7nÜ8Ç©&=ÖDZà Àq*Œ€ãT7Ç©0nŽSaܧ¸8N…qpœ ãà8Æ Àq*Œ€ãT7Ç©0nŽSaܧ¸8N…qpœ ãà8Æ Àq*Œ€ãT7Ç©0½Ö*J/0˜0òû~`?0l¦È©$nÅ3 X œÌfüYýêkØ l6?VÛJSêTŽqÞ¼&—/¡»G­ÀÝÀ­ÀÚ®•9N nù2¸x#ÐWÀûo>|=28NW¸äÃdàCÀ;ñ%\ïpp °·„ë9‰r”@×\ ÜœUÒ5ÿX¼mä÷O–tm'!|Ð9“WY A‰ÃËÖBœ¸pèŒùÀ·Ñ3Hܼ="8Θ¸´Ïk€¯rh?4vá;N x =Þ‹²ðO³2 Ï^ü7g |Ð:W´Ñ&·o@•†Žsn­ñ àýÖ":d=ðrTeè8‡á061ÆàL`«±ŽéEåÛs€™À³£Ñ.Ð$” êE¥Ü(»Øü'*ÞÚ‚¾÷ÁõpO¡çþ؃L`£µÀ™ˆÎnœ†j-æ’OqW?°ïxxä×Þ·+Üš“RðgìG&ð¨µÀ˜œ¼8…rvxúQÝÆÀ½%\¯!nI1ø3ö£ªÅõÖB`1ðVà<Ê)á®çZil0¸6àЈ”ƒ?cúàm²b@/*ß¾8ÁPÇZ­6ÔàPG‚?c7p2Õé7ÐƒŠ¸> g¬ex:æmJÊ-Áf£¢V›žT)øA™ìûF~M…À#À-Øÿ0ð&~H×f¡Ór·2¶ T-ø3޾2ß)2 øðCt~#®n¶‘‘¢dÁ?mä÷¯bt¨jðgÌGwÆÔ> ‹_o!œÛ?ÿb-¢–ÔÎÔíŸÏ¾ÃáÅ× G®:ÇOVZ ÉàŸÐÿÆZjù<~ÖR2€fÁ_ûõ_shËåàŠt•ÅAÔè·ÀïÐa h}{ë$à—è®+Sï¯Bú`ߎÚÄ=e-¤žTvÆ ~€/¢¦ÃÄüè¬tê¼5Ù 6yM/ Œ™#ÿ=•µÎçÈ~ýÀ‹QWâØ8XŽ}’¯žè,F³Ÿ))@êÁ?¬¾…Zåy¨g:ª|;8íl^D\MEæ¡©ÖBêX¼Œ€OcÆn)ÿ~¤ýzÔä£hz€À…hÕð®®™ {ÐáœØ ¼]KÌjð÷£Lñ'°» g;&CF×o•Àýh»/$v œÊ.cc«¤ü÷"ÍeÜñcg6Úߟb-¤ŽÝ(ø·[ i…PöGÛ!Åàï.ÎŃ¿¦¡ehÁ8›H‚â› ˜bðï@Yâ3‡v4BbÕ÷GÕo!&h7ø? ,+AW7¬Gwý=ÖB"â:4o14.´Ñ.±äR þµh¹Óv›5ç£y ¡ñYâÙ59Œ ÀƒßÕ,üœðžû×¢í¾ÐwLú#@ªÁ&*ÝuZçz þ}hK”Áaïxð;磶]¡ý–m¨üNÆtHi¦±Žzî@MD£&Ä€¿SË2 þDšô«'4ðàwj™¼ÏZD>B"“–B2~§že„—øÛÜ`-"/B1~§ž „¹Ìþžíï„ `&üΑ¼ŽðÎ÷?A@ =ó ØÉè%”µÁáÿ<øóàrk øïù7"”mÀ^àkhpC-õÁÿî’uµËTáçÁßóP˳þ­’!„ÈU_ ÜVógüÕåk XCbÁa•g& "þjÒ\`-¢÷X (‚PjéEïÁ_MæY‹hÀI(¹›¡<Ô2„•9ÇZ@†‰¬ÑG ôB˜üUæ4k ØEÀ­½;à-hG#¨@†ué%Ìn?©tlênD»m; <ˆ!øW£6^üù3}HC#…Ê¿¹h¸L69i&0-¤G€Ô‚)ªp m`EÈœ`-  ±OðcŽ›vB(bð 8x€ðJZCåùÖš0ÝZ@‡L¾‰ž÷'4øúœ –àoõ™? þl*ï<´8ºiIÚ`ÏŒ©ÄgY5åh5ϱ6€˜‚¿•,p}ðgÌŽÉWZrÌ´0 §Z hƒwÓxÉ_ÏŒ< `6í=+U%ø3f¡QVÇæ#-IB^%]d- &£¶é×ÑüsXËô¼ `.úp/§µ,îõT+ø3f¢•Àñ])K—КÔr2ñPYü 5Pm•\væ¢D×4kþ>FŸÖz=ðö®[$EÆ1ÈBÍx[ÑGëßC z€OY‹hÀ¤ëa`F›¯Ô­ÔÆB4²¹ÑöWÕƒ?cJ .hóu)rðg,.µQÃhXÊ{鬪wB7Ð(ø34øšÿáLAF¹°Ã×§F ÚR;ÅXÃ,à;hµÝÍÎIo§0ZðgÌãиc&!XÜåû¤@,v& £Ág\{&*åýù Jêä8p+Á_Ë~¯†³þZЉ»s|ÏØ˜ üÖZD >Jñ¥Â €÷ =ý<Ë÷¶kí XÆ ê„sGï=À ÷„j3¶B?·áß÷hð¯G«é"ØÝŽxðüCÀeÀM^#d~Çè;H!³ ø*2‚­¼¾úbTr¾âÍðñV Àƒ¿øàÏFýð“>ÑÿNØ{í­²X‡öå·£)BÐ*o<2¹ihKø9¨Rtå:ZÕŠxð—üÃh$Öµ%^3À¢eróXKŒƒa?héw ðá’¯kÍ6kãW£@ªÁ.a->axý²ù¥µ€Š±¥™xðÛÆûQElÙñNØl- blj”H1ø^N|Á_ËMÀ›Èw«)4¦ÿK5ÌΚÀŸÖ£=øÃ ~P ú­„×Ç1OöÑÙšÓ>ë€áZðà7ø3.@5à!kì–ÑÅ:ùñ0Zjyð‡üKP-zì*›q¿µ€Š°4¬êÁ úÐÅüµ¬AIÍÖBrf"ð7±tòa7ðLF>Euƒ`Ãȱ±ˆôŒôs[a-"qîd$™Ü\HœЈvƒÔé÷lâœý65™f-$gnµ8ßÌþ'ÛÌšSuê¨ : þZ¦¢ÄÈìÜ•ÇVàe¤3¿~ðß„Œàtâ] tü{Ñ÷ ÆrÔÔ:7[‹H”kS_ãJ à¯e&5X ]h%°ÅZHÌB¥Á^”ýÀ³Ð 8ò›ÛJ ïàMM=eJcc:2¯¹ÖBr`+p—µˆÄø 5ÁG®2bX üµdÓ|b̲ïεÒ%óGðU@  gÿÃòD;±¡¯¤õƒ=²}bÜgŸŒ¶­»×vËzà»Ö"áK4Hæ¬ûÐ]dSQŠº`åÌ‘ß@ë½B£Mj:ÃZH—|€â›n¦Î~àêF_kiµ%•B39”7v{-zÔ(áZy3ø>ù´¶b ðYk‘ó1`O£/´Úp* ¸ÐÆYmBµ·„kÅt^ ž!Ô]öÖB:¤íøtåöÙ¼ˆ&sZM®„º8MG)#QwðZâ`QK/ð5àÍÖB:ä ðVk2„zH4ý̶“] ÕæÑ|aÞÜŽÚvÇØ”£ø°ÌZH‡ÜKµ§tÂÕ(‘Ú”£Ú ÒúžŸüEçºrçéÀ÷(gï~#2ÃsK¸VÞŒC?»'Qg䘸xú78c³šnVí„gÙž÷OK¼æO€? mÂYŒêí°Ò½À¿ă¿Uv¡˜øÝX±€pLàÚæ²(xùúpƺ׾%wïž2ÖÒŒ>àÛÀÅÖB"b8‡;,wj`oË=bp팇Д— 5tÃÔâÂ3ÀJdTNk ¯~Ðê º1°3ƒèο®Äk6cä8ßZH‡ÌC%¢wNr3-œµÈX|¹äQc]öîÀAtç!ø3Þ|ÝZD\L85KÑÑæéÖB"ã*à3í¾(¯Ce™@Ö½gmÁ×i—aà Ä=Úû<´“bÙlô Ôõ8Õ†§E0 ü=ðÉN^ÜÎxðV(²b0 þ5¼w^ŒGàs¬…tÁj´Åy°Äkö×o/ñš)0ˆ ¤nêô ò6(ÆúѲ?äàϘ€áœj¬£Eßïý%\«õ¨‹Ù4-ؼš.ç(a Óz C;ÝÒîH«rx¯²èCÕ‰/±Ò‘ 4B5[=-FÇp'X é‚Atç¬?q;o1Ðc5@íöpVºAÍ^ˆvAf¡ÕL+ (÷³-é†E‚Ú²4Ð79”#¨,A /Bxí”!Ôzê&¨é¿ø|ï߃VÓÐêfúwŽü·mÿF1PÆÚ¸¸}ˆbexÚ鱬鿊OÅU7€0¸5rðxóqà¬EÄF·œ|ˆ¹ÓpÜ€òN›¸„C솭¸ÕȇÖÓ0 ÜÂ"öNÃes'šÖô¤µXqØ; —ÅJTÅ÷Gk!1ã&±w.šµh·!ƉÍAá.Ëg/°QKøß[ I7€py UHÎ&î†"y²UPþÆZH*x@ø¤Ði8v'OX I 7€8H¡Óp7ìN&°:úðʳ8È¥„4 ©,ö[µ`qˆ‡¬Óðk!%’ý›»í.í4Á .ö£³ê¥uŒ1$[õXŒ~¯ nñ‘B§á±$Î&žÑá')tnFÖУŠMbJÇ ^v ó÷1w®g5ô¸ÍZHUpˆ›mh%°×ZHN ÷8µèpˆŸÍ”7É·h&¢f±~²$ÜÒ`j&Rz[éèCEOgX ©né°ÍSLá„ÜD4Oàd-$ÜÒc. çX )ˆSPÓÔÖBRÀ -–¢ú?³R0'¢¦©Ëñy]áË€/÷´áv˜š¦ÞMµÌv…·‹Ÿ4‘÷ÖBŒ¸¸M)vÚÄ nú€oâ CïFP)TA–Š@¼ƒÊeçZ „•hÛ³ßZHL¸ÄÉ\à`ºµÀXƒE° ±—ŠV‘¥ÀñàoÄ"tœxеXpˆ‹eÀ·Ñ³¿Ó˜ù¨"… ÈÂñG€8¨z¦¿¶¢–i>IhÜÂÇ3ý³8mäW§naã™þîy­¶Z Ï„Ë\à<ø»åàaÒ;• na²ÏôçÉÑ(18ßZHh¸„Ç24Ü3ýù2m.´žÏô—C?ê0ô µpÏô—Ë:;Pùñc~Øž¬{ÏÉÖB*D/ð*´3ð c-¦xÀ–X2ýO—‘F»ñŒñÀ-À¥Æ:Lñ€KОл÷ì£\›Ñ3•G:[±Xo¬Å7–_&üî=û¿Aš7ð¨W*&u>üØXK鸔KðàÄ@ýÀYÀOêþ|3ÔñrâŸ91 X}  U7€òèî.±Òƒ(À›Ãch$ù¹¥)*‡ÓÐ\•ÖBÊ  bÊô¡y|÷Œñ÷~‚uœ^¸¢r9 ˜ ÜG:»OL}ú‡7·µø÷„e^Z˜"ÏBuI›€@±Ä’éÏx7pc›¯Y…oR+±}!ð\à»$ÜvÜ  8bÉôg|¸¦Ã×Þ:ðüu~r‚àyhw'𤱖BðRàü‰±¦ÿ“ÀU]¾G2¼K»V+€W`Ça7€|‰±¦ÿ‹À[sz¯àVà‚œÞ/$V£‘¤:»äGŒÝ{¾¼ž|Ÿq{QãÒ¥9¾g(¬Gõû¬…ä…@>ÄØ§ÿ.à•3Rk<2Ã3 xok6"Øc-$B¯F‹»÷<\HqóôÑ3óê‚Þß’¹¨@*¦ŸwSܺ#Æî=ëЭ¢çèe7-ø:L¦±Ž®ñG€Îˆ1Ó° íì/ñš“Q!TL¹‘VÙ‰*!£í8ìÐ>1fúAÒ“±yv†–ÍÇ\»hv#Ø<Ö_ 7€öˆ1ÓºS<ò«ÓѲùXC E±%7X iÏ´ÎBâèÞSOv‡² ~€]h@‡µŽ"˜J¤‡ÝƦøgâËôƒžõÏ$œgÔÈŒvë(‚Éèá©Æ:ÚÂF§µÄ¸Ÿ}Û:k! ˜ƒƒS­…ÀÚýµVð@s&"G1øÐV_ˆÁJ˜I¹»e1øp¾µVphÎ-DøL‡Š{.$üÁ€³ÑJ%5Æ£¢¿³2nY†_ÆÆ0ðTæëP±PÑEIôÿ¼ÅZÈhx?€#™‰žûŸf¬£Þ|ÅZD›ì~ŠNe+%wÆ¡¾‰Ávv8’Ï¡n0±qªNŒ‘mÀãèpRj«Ò¬ãð“x6 àpf!ˆí›òOÀÕÖ"ºä—À¯P=¶ï+,&ÀŽÃn‡ó~`‘µˆ6¹¸ÒZDNü -!M®ã°Àá܈ :báfÔÍ'ˆSNl@Ûƒg[ )ˆÀ3QÿóŸ›À!ŽC{báN൤٬òàÿÐX²™‡ÚÄß…qÇa7€Cœ_ÆÀJThòGk!²m¥b-¤ žœ€úI˜™xj×n8ÎZ@‹¬EU~)ênƇ€k­EÈy¨jЬu¼À!þÜZ@ lDûÊɵ§…+Ï[‹(³€åu•r8Äxkc°…tëçÇâ(á™*§¢á*¥' Ýr9êt²/‰N´•8ßa-¤@ ZRq8ÄÿX hBÖÐã k!Æ ¡©1 \†vvJÅ à¡4ͨe þmÖBa• UM×%Yð›œáp8ÄzkuDÅ0Q6›,A´ ²ÆZH˜?¸Ô²“pV¤ÛS?úÑnHh¦ÝæÁnõÜn-Ýá^ ¬2Ö:ЮÈFk!Dðƒ÷¬g:‘Öktýaàà6£ëÇHl3‚ ~ð@=;±ÛoF{<øÛcj7¾ÝZH üà+€FLGgÓ'•|ÝkIçX¯3ÑJ`†±ŽfüàÐŒ7££Áe±xÕ*ñ-‚YÈ޶RGÁ~°Ð¥Œ)@ÐIÄ]%\+u~ƒšm\€ºï„@°ÁžË(¾ê¬íi?^ðuªDH3‚~p¬êì½ÿ>T質 ÷¯2!Ì>øÁÆâITŸý{à¥ä·=¸q;ž@­¸_CùÛºQ?ø  U®^ÜÛåûìÞ…šCưm;«P—á2›§Düà»°ñy´ÞÄaðeô¡HqVèœFu½ˆ*øÁ  & â ¿Bõ}(w°áÿšÑçw{{.¾Fq«Þè‚ÜœjñFTß‘· DüàI@§Z<†v_ÎÉñ=£ ~ppªÇ£ä7s êà7§šä1s úà7§º<„|ØÁk“~ppªÍ tph~¯I&øÁ Àq–ÏF…^c‘Tðƒ€ã<…†t>˜=ÊßK.øÁë'£ ê\ÒàkI?¸8N-ã{8|‹0Ùà7Ç©g"j*²ˆÄƒÜ§“аÎ/pðƒ€ã4£ìJ7Ç©0ÞÄq*Œ€ãT7Ç©0nŽSaܧ¸8N…qpœ ãà8Æ Àq*Œ€ãT7Ç©0nŽSaܧ¸8N…qpœ ãà8Æ Àq*Œ€ãT7Ç©0nŽSaþØ~9Oà-ÝIEND®B`‚dkopp/images/edit-document-icon.png0000664000175000017500000001167515002404054016303 0ustar micomico‰PNG  IHDR\r¨f pHYsÄÄ•+,iTXtXML:com.adobe.xmp 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'7IDATxœíݬ_u}Çñg/7]Ó5]S+!Ž4]‡ÚÂØÂ°²ò£ÃV0D ÝX*Šk&#ÂÄ0&C 0€Îå‡X´ë~ØmÌÌ¥s¤a]%¦iiX皦際››ýñî­-½·÷žó=Ÿó9çûy>’Ú~Ï9ïô~Ïëœó9ŸsÞsæÌ™CKæçg§‹€Ñ¶ 2›»mÀ?ÏÛs4§…8¸¸–Øá%íàv`cÛNŸ>,L¹iHl®vµµÁT°xX•båÒÛ \A„Ar)`!ð]ଦW,b ¸ x"õ†š€Qàyà¼&W*hŒ0ß’r## ¯o=îüRæƒèÉœÐàÀiÄuÓ¡"•êgŸ6¤Ú@“—Ï÷ù%5gø`kŠ•7u´^\Ðк$ýÔpSÊ•7áš×%éh—·Ö×ÔN{QCë‘t¬y$\o"§6°IÓûµ+mâAœeÔ ’ÍÀg€= Ô õÁ1VöEâ¨^ŲæËi&–ÔXfð¾Cÿ•Jò2qkïÓ—K2 ‰K€¹5–Ù…;¿Êõï5–©³ŸÍ(×ÈýD¦íJ:‚·î¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì 6š»€!p&°xj*ÿì6{3×2T €úNî'@í8ÜÜŒg®e(xĪç<à_pçoÛ|à§ðàÕ º“€oórR°‹õ¹‹@uë€Å¹‹7KrÑw@u—ç.@@œ]”»ˆ¾3ª™,Í]„{[îúΨƧnñû; ÿ«Ù¼š»ö_¹ è; º§s Æ€¹‹è; º;‰3åu°;w}gT· ¸†8)-À-¹‹@=OçóÓÕž1àÏ€÷3×2Õ®o qê\àl਩þ“¸æw¶AÀ`&€Í‡~I½ãK*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`£¹ "#¨©Œç.`Xƒ¹ø°Xœ¹–a6ì6÷{ò–3< €zæÈ]H!F·úu=°Ø’µ¢Áìki™yÊZÏ×pçÏåDàyàôÜ… àE`¢â2[SbT· xî" 7¸?wØ lªðù à‘…Õݘ»p&pvî"ppp–Ÿ}€8khœPÍ(ýþÒ ›sr0€—€+€3|î9t €j€ê†7æ.`@Ï¿Lì䯿չ ø$p0–ªïT³ŸøaÌÍ]ˆøŸÜ4`;p °88Àì"ΪVfT3ü+qß_ù½»€íþ®íz PÝ—s Žœ}ž Ð @u_¾“»ˆÂ×ÑÂ)ò°3êYƒ!Ë~âßÿŸr2 €zöï>L\‡ú°Jz»¯ï FÏÕë›=ôk.°5•ƒÀÞÜE # cÀk¹‹ªòˆ%Ì fH3¤‚RÁ ©`€T0@*˜ Ì fH3¤‚RÁ ©`€T0@*˜ Ì fHó€RZK‰vòo!Þ&ý·tè•ò€”ÎZàNŽn({3ÑÓ` ñªó¬¼ÒøpSw“^ü#ðæ6 šŠ 5o-ðù>óVà{À²ôåLÏšu=p÷,?»”åéÊ9>@j·ˆîÑUö«“€¿ÎLRÑ ©«‡¨·O-þ8»ÑŠfÁw)ðƒÝU[D„À¹T4K€4˜ó€o b5ø6pqëš@ªïাÕW×<àIbòPr€TÏ\àâ¨bÝ×&X÷Q ©ž1à2Òµ…þ¸!ÑúoDõƒ6ïN¤?:üؼœ¹–>û!ðëÄüþ7%XÿðçÀà®ë7pp?™gr ènீO{3×ÒWÛùi,M°þà ÀÏŸM±rU·Š¸eÓçâç%1mqæZúìe"~œp·AÐ( º¥ÄàO·}ºâ4ૹ‹è¹DlO¸›©>Ó𸠀êÖ×dÃær"Tß+ÀoÛnãêÏ8<†PÝå¹ Hdx_î"†Àn"¶&ÜÆÕÄãÆ3ªYD<¼1¬NÍ]@‡,ž§ÞYÑàÝÀ÷­èhëh`’PͰß5ñû;ÿ…ÔRo/ðâí?),¤'ýW³øIî"rN@ÌìûpÖ¡ß/¦þ“zûˆØÜLiÇX8è €êžÍ]@BÏä. ³¹ÀSÀÊ×ýù"à»Süùl.ž¤°i üNA º;ƒ¹‹H`i¯Y»nx¸`š¿?ò² ªƒÄë†z¥Mi1q @uÛ‰™s¹ iÐNà#¹‹Èhøñ\ÿñÌ#Î’VÕØÆp1órPÀïÐÀwШçA⇙êA6m&žex5w!™Œݬžåççg WÖØÖ8pðpe'M7O°ŽÃ†}T;¥'ˆ«w¯uê‹qbÀïà…̵äöeâ¾z£À׈0x¸â²ÀG‰Ë‚:Oú­ܔ €Áì'¾ç-C5}‘x‹o“ëΧú9|œ üý Ë­§á§½P©n§ÚÎ7•àKÄý:nî˜ågÿø\ÍíLËP‰n>Ýк&×½µæò·0óc¾÷§þ3TšßnK°ÞÛ˜¹Ðtþø¦ÕôKÂPIn ®ûS™ìXÇ]{kïQàºA‹:@¥¸šx½VêïüZâÝ u¶s/ð1"ž æf$oâ]•àýĈ}[¼ë‰IC%n¹Vñ11kKe+ó @ÃîRâ Nmì®&fÖysÔfbæ`r€†Y“]{êXMÌl²qH£ «]{ê¸ôP)ˆ ÌÐ0:›x¦¿+;ÝDϿνKÒа9ƒØÙ~YFÃV/Y”¹Ž£&ˉwtj';ÂYD‘Î<8fhX,#Ž°Ù¹¦qñžÁN¼\ÖÐ0x3±ówb§š…Ü“‡껓I×—/…D߀ßç×@}vqä?%w!³ô Ñ/à•Ü…L2ÔWKˆ¿/ÍL^#vþ” D+3ÔG“]{–ç.d–&;íÈ]Èëê›Ä}þ»â´d²CPʆ¡µê“yÄ ¿:]zrؼ—´Bb¨/¦ëÚÓUˆ¿Óo]6Ô£ÄS}uºòä0Ù hKîBfb¨ëFˆçùëtãÉa²Ðwr2€ºl²kÏr2KãÀ‡éQY@]ö%àÚÜEÌÒñÏ¿Î]H€ºê À'r1KDÃØ‡3×Q™ .ºúÝvrX|%wuêšOQ¿ËNŸ¥á~}m2Ô%k©ß]'‡;ˆ®>½e¨+~¸;wü)Ñ×¯× uÁ‡¨ßM'‡¿~/wMèË?¸†×jà!úó]|øxî"šÒ—t §KÇèO‹ºVúõµÉP.¹»öTõp-ôëk“ ºÒµg¶6kh©__› µmÝêÚ3“-ÀeÄ~CÇPÛVѽ®=Óù>ñLÿÜ…¤b¨mçç.`–¶¿I¼ÕghjÓ\úñ:¯mÄ{üöæ.$5@m:‹î_û¿Lœ¥ìÉ]H µéݹ ˜ÁN¢ÆNtíiƒ 6›»€ã˜ìÚ³+w!m2Ô–À¯æ.b¯§ýêÚÓ@mYA7gýMvíÙž» µ¥‹×ÿ{‰[}ìÚÓ@méÚõÿdמË]HN€Ú°8=wG8\BÇ»ö´ÁPVÒïÚð[À?d®£ºòCÑpëÊõÿ8ðAzÒµ§ €ÚÐ…ëÿqà`CîBºÄPj'§f®a²kÏ×3×Ñ9€R[™yûÀô°kO ¥–ûúp_æ:ËPj9¯ÿ×Óã®=m0”Ò[¥™¶}ð¹LÛî @)å:úßËtíiƒ ”r\ÿ?@ úiúÒAýÔæÀN¢kÏú·Ù{€RY,I¼ÄÄž')ü¡žº ¥²2Ñz_"Zt=NÁñ6%W8ö0üÞÕàº^$vø D¨!M@vIK‰æûؾºiÅ€Ëo¾Aíw ^ަÒDÔy}òBâºí35—ױƈ·Ùv¡yåÉÀ›j,÷Ñ0ô <Ò·bΜ9s]ÇBàñ´¾ Ûf¬cðÔ,?ë5}FMì´û(ô…Š4X üX›±ŽÓføû‰Ûu¿¼ãÐÿ»ógÐÔ àFàí ­Kƒî~BÜoÛ/¼î÷D¯½'‰<¯é;¢‰Kˆ{¾ÿ—]³x í¶>\Lt×½/ªáF_4ß.jjejÌGhÿYø3€W)¨ÅV_5yÄ^G7F u´wfØæVÜù{¡ÉØF<‚©nY”»uWÓ×ìljxÌk¹ Pw5ãÀbðGÝð½Ü¨»š<ÒàbBˆòÙ ü"õ¦k«'$ €1bJç…üLŠè¸Æ+€å.DÝ•*&ýx8x0/åÆtØ>àJb‚–4­T—S™\œOÜ'>…¡öÍ~ 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'‚IDATxœíÝ{”VÕyÇñï‹SÖˆS:*‹â¢ÄD‹Æ(¤jÄ{¼kHP‰Ukâ55êªÚxY.S­‰×z!ÆÞ !ZWD$Õ¨E´„j !È¢”L)kÖˆÓédÖ´<ófîïõì½Ï>ç÷Yë]ÂðÎy¶0ûy÷~Î>{ …"’O ¡ "‰ Œïýu+Ð9ÒGyiŽˆ¸6¸xøø¨÷õqï×.Æ þ¦‚¦"QknÎÆ>ùKÙœ ,-~A @$^‡Oãªøžn`.°”Dbu p+µMã;™À*ÕDâsp;µ×ðù €HlÎîOèZ³vP‰Æ€;$t½ÿQ‰Ã` Мà5wPI¿ÑÀËÀî _wGEÒïNløŸ´±J"év.p¾£ko×]‘ôÚxƒò+üjµF#‘tšüw`¥€HúŒÆ:ÿøro¬Ó뚈¤ÏýØ‚—º€?Õ@$]ÎÅ}çXlÓ@$=^Çí¼`;ðy`­F"é0[æëºóƒí °´#H4bßuÑàFà™âo4 ïAàëâ¼ ô¿  Ö7{<ÄYì´õÿ¢€H8¾Š~Ø@«ÿj"aLÄýJ¿¢³¦óƒ€HXç¯f3ÏZÝ‚Ýó–¦"þ= üµ‡8K€£±€‡¥ â—¯¢ß¬è·µÔ›”DüñYô;XYøá³èwt~Pñ¡x?E¿;€Ç+}³¦"îù*ú-Ž DÑo0%·þ¸ËCœMØ~­Õ|“€ˆ;¯ Žãt³€åÕ~£j"nL~ˆûÎp5t~Pq¡x?E¿{µ~³¦"É{8ÃCœ·C°ýýj¢€H².ÂOçß |…::?h ’¤ƒñSôëŠ~ïÖ{!D’±+þŠ~óH óƒ€H|®ô{øARÓ@¤~å!Îrlèß™Ô5©ÏEøéü­XÑ/±ÎˆÔã`üýÞJúˆÔfWl/E¿ËpÐùA#‘Z4?öñëQì$'4©Þýøéü`›{8£ RKñSôÛ |™„‹~ƒi R¹CWq?ïïÆ6öXæ8ŽF"š„¿•~Wà¡óƒF"•ƒýööëi`®‡8€F"•¸?ð q~O @¤´K¯yˆ³ +úuxˆõ{šˆŒìpàeüýŽ;Ž3„F"Û<…Ÿ¢ß5èü €Èp|ýs<Ä–F"C=ˆŸÎ¿8ËCœ)ˆ t9pª‡8mÀ‰Àv±F¤)€HŸ#q?ïïŽ^r§,DÌdüý®'4+ú½L÷ëylèŸ ˆØé½>:ÿ‡ÀéâTL @òîo“=ÄiÇVúµ{ˆU1M$Ï|ý¾ü“ã8UÓ@òÊgÑïFRØùA#ɧ1À¿{zˆõv˯ÇC¬ªi yô0~:ÿ:à4RÚùA @òçJüý¶c·ûÚ<Ī™¦’'GaE?|§ =Ä©‹F’»aE??óß%‚ÎH>4a+ý|ÌûG“âyHø*ú­Ç6ôŒ¢óƒ€dß•ÀW=ÄéÀûló+1šH–ü?t§Ozˆ“($«vžÀÏÏømDØùA#ɦ&l¥ß4±–_ÂvöŽF’Eà§óoÄî÷GÙùA @²çï€Ùâtb÷nõËM$K|ýÎxˆã”€dÅlÞßì!ÖÝÀ<qœS,‹­ôó1ï 8 èòË9Õ$ ÆOçß„-öÉDç%‰ßÕø)úua¿ÕC,o4˜<‡Ÿ²s€<ÄñJ @bå³è÷}àq¼SÅ:ÿT±Þf‘¡yªHŒÁOçßBÆŠ~ƒ)Hl®Nò§ ˜lö+M$&>‹~÷zˆ”€Äb ð6ÿwm¶Ô7ó”$>‹~+€/bûdžjƒÇðÓù[±'ürÑùA @ÒïZàqº±gû7yˆ•J’f'×yŠu°ÌS¬ÔP @Òj*6ï÷Qô{8ÝCœÔQ4òYô[ €më;šHÚŒÂ_Ño+VôËeç%IŸëðWô› lð+µ”$MNžï÷áÛÀO±RK5I ŸE¿…Ø-¿ÜS4‹-óâ!Öj`&9ž÷÷§)€„6 x ?¿œýSЮÇöów­;Às‡XÑPfã¯èw ð’§XÑP @B™†ýš<ÄzÛÙGQš±¢ßnb}ˆýÚ=ÄŠŽ¦âÛ(à ütþvàDÔùG¤ ¾Ý€¿¢ßéÀZ±¢¥ >ÍÆŽïöá;ÀóžbEK5ñeð üý^À†þ=bEM#ñe>~:ÿZì~¿:”ć#£<ÄÙŽ­ôSѯBJâÃUžâœ ¬ñ+”ĵ݀ƒ=ÄùlÁTA @\óqŒ×bìù~©’€¸v€ãë¯ÇvöQѯJâÚd‡×îÀŠ~ÛÆÈ4%qÍå?ßV9¼~æ5„n€¤ÆÞØ6Yûã°ã±6ï` kj}Ž~{"­ê6àiG×Î ­”fàNàŒ2ï[ œGõ‰àY’/.¾„íì+uÐ ߦïS¾óÚûÞ¯VãõjUÆl¤¢ÎŸòëPìÓ¹–9úÝØYz]¼wgà#`L qkÃŽî^ÀµØA —Žžvªñûg`Cð%X§,¥«5Rc¬¢6àhàƒ:¯#ýh?‡?¸Ö6à,Ê?vÛ¼Fí+7`O÷©âŸ0òe¶1fÃq€±E8MÀŒ¼§›nìGõëžÆ:ÿ†Úš(¥¨˜“±O~ä^Ž%€‰%ÞÓŽM.Æå,g6m˜‹ú8£)@>ŒÅîçOsg+ö,þâ2ïÖÛ¦f¬VÐݶòò@ ~Dõ·ïjÕƒ=™w ZŸŸzJÙ÷Màžq—aÃ÷-bK…”²m ð¯$Wô«V+––Š/eè.@¶-ÄÏ¡›#Ù « €7ÿ ؆FÙu ðbèFô³K•ÜOt0». Ý€AŽÄ¦#†nˆôÑ ›€Ifµ_Òº±MBo ÝÑ «&ÎΖœ¾‡-Jj Ü–ÜSȦþ]æ3B7$ÏbøA‘êµÇóò»bwΠݼRȦ`eèFTh4p¶+‘~=Ó:€ìúcàˆÐ¨ÂLà3Ø>Z/à‰îdW ðÜîÊ뽤ïffiMÓ°-³÷!Ü2àZí‡%®Ÿ‡nHG£Sc±³ÜÆ`û½µb1üÛŠi9¶]´¤Ïtl÷Ý9ÀžÛR¯6àÏѪAç …B¡Û®iŸ Þß…%‚°[8«° ;œµPF2[Uw,p–¸³äFtÞŸs…B¡08¿Žktc{ůĆmk°Ä°¡îÖÉ`S°=ýŽÀvõm~_ ÀŸ…nDÖ …Âã\»K«_ôþúC`“ƒXY5 û”?ëð¥¶ÜÊ¢O£Ÿ§ …Báwø="¬Ûîi V[X‹ðºŽ|O%ÆûbE°½¯¼/•‰ÕÄ‘B¡Pø/ìð†Ðz€ÍX"Xü _­Ú•´fì“}*°V¼›ŽÇ'í…¶wªP(^dž—i× 7b‰b3ðØ–S[°±a„2 ûÔž€ ×'bÃØÉX‘n2éH¶1èþˆ| kÀ¶sŽ!ŒÁŠ`åv¸éÂnmë}µ÷{mǓݎÝÎìêýo76)nb9ªß«¡7v¶¥öNXñ­ ëìÍX§nÁ>Åuâr2–£Îï\¡P(LÃæâ"irð@èFd]q!Ð;À·E¤h¶H‹Î+>}uWÐVˆ têü^G À¿Sý¹m"I{ [Ý(êÁ c'†mŽä\+v~à'¡’ýnÀVìM ×ɱ¬ó/ Ý<é¿K7pY¨†HîÝ‚:¿wÃmò*¶‡»ˆ/˱çbØÇ0S†KS°§úÒº­´dËvlÉïúРɣávúo¬óä¿9’C ÃCƒiOÀFlÃÅ¥…À)¡‘g¥6=ø)ÚªYÜXü%aÞʽR›‚nvA'·HòÖc»eåïh•Û| VÌÚ~sâ·ØÏÍ8†’»EÀ<´ág*Tr.ÀAØ#Ú H%>Gß©Dã°ŽZ°…>kPÇO•JÎø;eFO J%žÀ¦`Kz‹íèôz¾?u*ýT¿ ÛÜS¤œØ"ɵJ@'pzDSÊSˆH5óúUØ¢ ‘R²|VAæT[Ø[€Nk‘Ò4ˆH-•ý3±5Ü"ƒi‘Zoí= |¸ÝÖ‘v Ý©\=ǃŒ=:|'ö0ǯ±Û®ÄN(VçŸ@DÒ®n"]£©@D*ÙÌ¥ý7IG"’äü:å' ¡;Þ­)hƒ$O£€H„ì|3Ð>ƒY¥cå"2Ì [ÜÒ !€*þÙ5:t¤2¡@:w0Ë4ˆD¨Ђ>%²L ¡€*ÿÙ¦ä‰PqºOœeJ‘•º°C$%›”"r(þJÀØâ––uG"d¸M²J !À:à€ñÓ¦=t¤)@$BWã¯@µ€‡°„(âUèÐïà `¥§X•hæ`‡ªì¸-I ýs%JÃ?Ô`&IÊ:àqà,à/z¿·ƒ8µXìl®ÛÉ«´kZ±ƒ<¾\L¬òû;°OöÕÀ/±£ÌWaë À¶{;™(´Nàà6lEäIÏ¿ƒäLš~ðz°¢à£Øá§`[wÈG7Öá/ï2ò…3€ûIGajv²òlôõÕ'»¤ad)HS(êžé}#6{Ä´-lì}_9××:hcµ:±‘Ímô%ª›#ƒµH„t&€Á¶ô¾ªu'pQÂm©Å?çkû}í àò0Í¢‹äGGZ߉¬Õn%|çߊg1°ó„MIÒÂÅŠL%€Hd1\\0~7ð}ìXôƒþlð,é¨G€M©žrpÝJ¦g’1Lªq8ð½€ñ—ßbøµ»¯’®ãÊ—b·!“ÖSþ-’YLÀ>ÍB$µ5ÀñÀ! ßùǯ‘¾Šÿ| ÙÁu5ˆD–ÀíÀΞc®§o‘Ñ #¼ggàu`НFUh ðCß­º÷IþQä°´$å²2«jÏÁ:p%°[¡÷ðSìÁ¢Ö:â_ ü=éUÍgà}ú¤oMv¡Î,Ц`ÛÇÀÑÀ6ìþ l}þJ`{ñš€“¸–kÀ§±š X¢ü$á[]¾¦8’¥@ÑZàôÞ_Çî´õ¾’6ø1é¼Í7œEôu~€Éb¸ø{G²˜ú«å)ÂJÓšÆHÚ=ƒ~ï"l+ÿI‹¬'š±ŽôµÐ ©Ò lã”þ\¬QPˆˆ@uÇæûiØZ¬Zw óµÝÄ©§*ž¥½bc±•s¯gçoóuµ —Ó.I˜FåÍÆvJÛ“|ÕøCçŒötë·®)Ž(Œl26l>&tCêÔÅÐâØüßEÓÅþ∦C57aÛ‹ÇÞùÁ–H7,wu>‚@D4èÓœíÞëb}|(·ðõ™Žâm,ÿI úì‹U˜¥Î¿„‘BÛßA¼4ˆŠ@Ÿw=€ï’M-GÚ±7S€Ídçï.”êÀFŸ§òÇÓêì§á웉uÂqd”†·ø"v ‡‹•míXué¦vˆ£˜ë]WQ(íà³ØÞIéÀŽD¿9Ákö!}G« çPGqåèºâˆ@y[±=¡¶MBúëNÞ–×y­Rnbä½ù[°]‹\¨÷ïGŒ='Pjí@6…x|Ð×€ß%Üž5Ø!%# ÿ±µúcŽ ¶Þàs®+iPŸ…X‘ðnFît0´óƒ›9®)ѰΉΰÊÑuÅ!%€úµó°¥µƒoí} {o8I?]ø6¥+ÿ§$³¿÷^[QHÎ `?àb,)\ÜQâý“ŽY™? œpÌþ\¯kô0P²z€ÄÖ ”Ûsj‚qŸdèv_ƒŠ»ó ºù™I1ܨdgÜ=ŠÕNùO°U®¬"™-ÖÅ3%€p¦'të(¿ × ìiGWÊ>$¥”Âh$™)ÀrlÊQμb•ò¦ãë‹#ZƾÀ{u^£+:®.ó¾ Àopóô_ѧÐ>QÒ ŒI”¾__‰+(ßùÁnEºìüëPç–@‹°OÍyØ9…Õ&ƒ¨lèßœ_嵫µ¤ü[$­”ÂÙ‚­ œE_2XFùd°[Z\‰+ptÙkޝ/©>ã“°U{2p­Fp¶æ¿’ëüw÷þÁŽÚ»)RH·qØÁ$s°[yÇc£„JÌÇÏðÿÇ1Ä!%€xŒ¦ò 7§?ÇýJÏ‹©¬!)¥@<ªÙm÷.ü,ó~ÞC qH {NÅÝžý­6xˆ#)dK ¶A‰OyŠ#)dËdly°ëÃ9Ú€‡ÇȚfìÙÿ9À‘$¿ð 1 2 3 0 0 256 256 1 ŒójSôeXIfMM*JR(‡iZHH0232‘’Wœ 0100 ÿÿfotocx-25.1: Color Mode: mode:2 blend:1.00|fotocx-25.1: Color Mode: mode:2 blend:1.00|¡Ü®'”IDATxœíÝ]e}Çñ÷s²îì줙Ì6]1fÒH-FÈ`e–´ 4b€€ˆZ+X:è¬Ej1£”qœ–Q‡tÐ2þhQ´V;Ê”BH!Ĉ”I™©ÕJÓÆmfµëΚY·—þñ½×].»÷<çÜsÎsÎ}>¯™; ¹wŸûÝdÏçžóœç‡sÎ!"qJB "á(D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ |ïجmÿ÷$ð= °&‘¨„€íÀ¥ÀÙÀê®ç¦»€›G+®K$„ÀEÀ6à8ì˜Èrf> Ü\ Ìf}sçœËú=ymÆì Ï× ¸8RZE"á¬n. ˜Kñ€3Éx]U¼ûa³žq^LŽ-¥d@8çžËñø±sn“sŽ®Ç¨sîáŒm]¶D;8ç®ÏØÎß;ç’%Ú9Û9÷ó í<íœ_¢µÎ¹fhçgιS—hgÈ9÷õŒ?ÛŸ-ówôîŒíÜïœY¢ çÜO3¶¥Ç<òÀ»¥!qÎmpþÜ=ÚIœsßõlçœs+{´uS†ŸmkvÎÉзz´3æœû_Ïv¾Ó£œsz¶ó3çÜ1=Úy†ŸMy佸½ÇsÏàšûÕϵ€ðlç^ì’$Ïû,6 <ÔÇûø¾ç¶™c¿í€í4ããàpç{ý›Ê€Êi×ý~Ѳ¼îGžíUOZ;-×ø¾gQG¾õ¤½.O‡4\è»™¶2nP;u4È?›ä:D$ €HÄ"SˆDL 1€HÄ"KÐýa‘h)D"–`SVE$B þ“[DdÀ aÛ¯,¸ÝOc; §I[@ä!àí<™òü¤g;S¯ù°ÊãuÓ)Ïßü«G;{Ržß‹ßÏöý”çz¶#Ĺ…íÁE$2º (1€HÄ"SˆDL 1€HĆÚ_Ö‡,D˜£÷ªÌ›ñ‡!:ã.>¶/MyÍw€‰ j‰Bçà.òm¡%" Ö €#äßODjq'à'ƒU!"A,€{'B""Õë¾ x5Z D$óݰ¸µú:D$€©¥]‰Í1‘ÁöäR0 ¼žô$D¤Ùî^n(ð!àlOy<3Àz͘^ \‹ˆÈàøËô,6|ø5àlÀˆ4Û.à/Á6àalñЗgE+ ‹4ÍnàÍ´o÷õ|é Í;Û°Y lÖk€_ÆÚñöóë)~åañwûÔ¿ŽEó~²@·p°ýH3ŽÍâ:¸M?)Û°›ì÷i¬sÿyB- ž7bg " éÀ¾ZØeù¡öã ðl×ÿObû\ôÙÛï@^-à Øâÿ¨‘º™Áà^÷a š¾*:žÎÃR}8l)"¥Zü©Ý9Àÿ‹…»óçi;J*t€mëõYàŠÐ…ˆä4ÍóâƒÀóüƒý5œhW‡øR?óصtç ^êtüj©.°ë° ]ˆDcš…ëéΧö³]v˜~j©.ÐÂnWl ]ˆ4Þ<~×ÚÄF}ìM$Í?aùƒ{à?µ‹T§P"Çk;x÷y¼ö­%ו:€v)\sØôΧö³<¿w|}jQ§Ð\f™cáZ{ˆÞ£óæ°IdR3u €cB ¿0Å ouõúÔÞÜ×£½•À6!Ej¤N°!t˜c¡£ì v÷€•¬ ÀLz¼f ~“ƤBu €UØ´aÉïðÖKþCì <‚u®Na_êäœ|ŠCP;u €M¨°; {í3N;úÖP]ºÍ¡ h¸ñÀï?Cú™…:yk¨.pJèjî')χ^S¡EzŸ †ê °-t5u¸¸<åuÇ~:õ\Êóu¹Ü”EêhP·ðààËXÇ^/CÀ‰e•"-B”,¡ð¦ÐÔÌ ¶jëå,t®í'ýû´k*B~פKþQ~7t5Òّ鎮?ŸÁnñõzŒ¼ÏR3¡àøöCl`Îk°’–òPÊ÷Ov±Ì‘”ç5 °†BÀï~ÿº˜Âîã÷ÚõNv>PL9™%ÀhÊk52àmß¿.æ±kþý)¯ÛEúˆ»7çQTF«Iÿ]jì²Yƒ,dlCãÿÁ6_ÝíñºyàV×Ý‚íÒT%Ÿ‰\ÚW²†BÀÛ¾w]ìÆ6]õõ Òo·c— UÞZõ Ÿ CR±P0œè½ëb»Õ—erÎl|@šßþ™êΰ^žòüQ¬ŸCj&T¼ ù(ð½ß÷AüN§7ÿ‚mÁV¶Rž?€Vü©¥0¼3ÀûÖÉp}ßû.Ï׎‡í¼tå…nÚd®´N $Äøì ¨¾“ªnþšô >½ÜŽÝ6¼ÌóõÀW±ðØ| »åxwÐϧó(éÃ÷öѾ”(ÄîÀ±ïî: ü*ý÷ŠcËpÖg;Ï`};s~ÿ9ÀÝ)¯yð9Û—U} pqü`ŸÞEܛìÇúlgv× ïš oNy¾<š³m)YÕpMÅïWG·ØÖ4p&vZßaàÏs|ß(vI×Ë“h @mUÀ¾_)¸ÍiàuØ]…~®å+Ç÷\BúR_÷ähW*Re\[ñûÕÑí%µ;\u ¦­°œ¬cõG€÷y¼î+9j‘ŠTu@žˆS]ZgY¿va÷ä¯!ûi÷½_ÿ§Àú”×ìeùÙRUÝø:š÷? ü2Õ͋Ž»üêö‡“ñ_¶{v7'm ðåø\”@ª€“°_–ØOÿï^è½×a»÷œ‚¼Çbƒ„fÛuíÀú'|¬¾ lLyÝaàehp­U1è:tð|3à{>Û~t a†Y:G±[†i?ÀGÐÁ_{e˜[°"{BÐežlÿà~`«Çk÷ŸÊS”T«ìÈ;Þ}д€'Bч-ØeÜ×¶°kÿ´iËReÀ¹À©%¶ß$èoì(«€‘mjñ ¤¯_(5QV@‚]ŠiÚ± øCì>–]‡vcã=¤!Ê €Ë°Þf1÷‡.ÀÓ‰À¥Ø¿Õ¿÷)l^€–ÿn2n®þ m÷ÝÑ^ŠÝ«›»M{ðò‡ö~àtl_i2Îv ƒ±}ÔçàÖ ›ÀÆœJÿk>†oЄŸ*:Öï-¸Í¦Ë:Ķ#Ø5ûzlðÏ:à×±5û6RüB,_Äzüg nW*Rô%ÀmÀ…E68¦°OÇì@é<޶sí¯-îÍϳp‡&i?†Û‘öceû±ë´§º-¸§±ÉG[ÑûIIŠ €-ÀÃhÔß »¸ ÿ¡ÃRcE¬ ¶f½þÁµëè;ü£¨>€Ë°Þd,-¬ãFàÀµH Џ¾K¶#Roß¾|žü ŒHqp3:ø›nxø¶z¯–ñŽD¿p>é‹BJýLa““æ)?‚vïR?—«€§Ñ ŸºšÁ:ë`kÿ?mE¶ÿ•dÀõsðtð/§… ­ÁÑXÙþúKí¯‹ïç°°e×v'¥óïÒÐÂÆ tÆ tÆL?nÂvà=ÒþÚÄÙ‡R±¼gZæ«·'W….B$Mž8Á:þtð/¯Êá¿"¹å9ˆßF¾M$bҔ鿹¬—£ØTߨw÷íe[þ[ bJíe=øtð§yüÒY` ~[AÅî¾ÐˆøÊ×¾¤ô¿S¯He|ûÖÿŽõÈò/ ]„ˆ/ß3€kÐÁïcgèD²ð €c°é¾’N·ÿ¤Q|`úô÷ÑBgÒ0i}«ÿD>4üW'í àÐÁïKÃ¥qzÀpeU… ]ÿKãô € ±@I7‹i”^ðžÊªh> ÿ•FZ.¶aEŠ ÿ•FZnE 1lŸwñóµÐˆäQÆîÀ"ÒZÕG$b ‘ˆ)D"¦‰Øâ» šô“ÕQlí~‘FZ¿Üª†:­$ ¶øà¼`U4“†ÿJã%‹¾nYHiø¯4^'&°¹ÿâOÃ¥ñ:°5hͤki¼NüfÐ*šç0¶H£uठU4Öþ“`{×o\GÓhõ p˜…Vÿ•‘ Oÿ¬ö“¡‹)B¬]DÃèÓ_F¼8t s60º‘"$ضßâopmè"DŠ €y¼ë<i4MÎgøDè"DúÕ ÙmG¨¤á4 ×….@¤ êÑîÇf쮀H#%,¿9ˆø¹*t"yé  ÛÑ`*i(ô/ÁvRiœ-kU„7„.@$@16#¡‹É*A3ÛŠ0 º‘¬à@è"ĆЈd•O‡.b@¬ ]€HV ðDè"Ä\èD²J€ÇéÐ… €ƒ¡ É*Á6·¼'t! 7ì ]„HV‰@ŸZEóíFgQÒ@؉6ºèÇM¡ É£-`GÈBlpWè"DòXáœëü÷€uh— ,ç? ]ˆHnQ€ gýpZjšåp:ºt’ë^ è(6±åÞµ4É~àtðKíè:Ðr0‹ý’¿¨ê¢jlø8ðVl‡`‘F[*ž¾ Ü ¬6÷l·iààb,¶‘bt÷,g8x-6õu#ƒ»¡H ›!¹&ý vŸ_Ó¦eàøÀRF¨ÿjBCØTÝ!¬ÞîGÇ<0LaÿlµeŠ„ÑOˆHÃiO‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆ)D"¦‰˜@$b ‘ˆý?{«fqÅÚ×£IEND®B`‚dkopp/doc/0000775000175000017500000000000015002404054011374 5ustar micomicodkopp/doc/changelog0000664000175000017500000000444515002404054013255 0ustar micomicodkopp Change Log ================ 2025.04.24 v.8.2 Toolbar removed, leaving only text menus. Copyright and license update. 2024.06.23 v.8.1 Compensate for long growisofs disc format response time to avoid misleading "dkopp is not responding" from Gnome/GTK. 2022.01.27 v.8.0 Refresh embedded libraries in binary package 2022.01.01 v.7.8 update libraries to those available in early 2020. 2020.11.22 v.7.7 Internal rationalizations. 2020.11.01 v.7.6 bugfix: stop SearchWild() wasting time searching outside target folders. 2020.08.18 v.7.5 bugfix: allow file search to span multiple file systems. 2020.04.09 v.7.4 internal code changes - no user functionality changes. 2019.08.01 v.7.2 remove multi-disc backup option (more complex than one-disc-at-a-time). audit user guide and bring up to date. 2018.09.01 v.7.1 remove mount commands and root user requirement. 2018.03.29 v.7.0 internal code cleanup. 2016.04.01 v.6.9 add gtk main loop calls to prevent dialog window lockup. 2016.01.03 v.6.8 Stop meaningless error messages from latest GTK changes. 2014.11.02 v.6.7 Accept growisofs status 997 as "good status". 2014.10.22 v.6.6 Minor cosmetic improvements. 2014.06.01 v.6.5 Bugfix: find command fails when embedded blanks in filespec are represented as "\040" (as in /etc/mtab). Replace with " ". 2014.02.10 v.6.4 Rely on desktop manager (e.g. Gnome) and user to perform disc mounting (discontinue internal mount commmand causing a fight with Gnome). Update method used to find DVD/BlueRay devices. 2013.04.01 v.6.3.1 Display online help file from menu Help > contents. 2012.11.01 v.6.3 Replaced deprecated GTK functions with new versions. Improved clarity of some GUI and report texts. 2012.03.17 v.6.2 Bugfix: Some DVDs have a top directory with embedded blanks. These would not work because "\040" was being substituted. Fixed. 2012.02.01 v.6.1 Dkopp was converted to use GTK3 and Cairo. It will no longer build or install on older Linux distros lacking these libraries. Bugfix: /etc/mtab is no longer reliable for DVD mount status. Using /proc/mounts improved the reliability of DVD mounting. dkopp/doc/copyright0000664000175000017500000000045415002404054013332 0ustar micomicoUpstream-Name: dkopp Upstream-Contact: Michael Cornelison Source: https://kornelix.net All files copyright: Copyright 2007-2025 Michael Cornelison All files license: GNU General Public License 3.0 or later https://www.gnu.org/licenses/gpl-3.0.en.html dkopp/zfuncs.h0000664000175000017500000015636415002404054012327 0ustar micomico/******************************************************************************** zfuncs.h include file for zfuncs functions Copyright 2007-2024 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. *********************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERTICAL GTK_ORIENTATION_VERTICAL // GTK shortcuts #define HORIZONTAL GTK_ORIENTATION_HORIZONTAL #define PIXBUF GdkPixbuf #define GDKCOLOR GdkColorspace #define int8 ch // number types #define int16 short #define int32 int #define int64 long long // long long is always 64 bits #define uint8 unsigned char #define uint16 unsigned short #define uint32 unsigned int #define uint64 unsigned long long #define ch char #define uch unsigned char #define cch const char #define VOL volatile #define STATB struct stat // stat() file status buffer #define mutex_t pthread_mutex_t // abbreviations #define mutex_init pthread_mutex_init #define mutex_lock pthread_mutex_lock #define mutex_trylock pthread_mutex_trylock #define mutex_unlock pthread_mutex_unlock #define mutex_destroy pthread_mutex_destroy #define XFCC 1000 // max. file pathname cc tolerated #define null NULL #define true 1 #define false 0 #define NOP // trace execution: source file, function, line no, caller address #define TRACE trace(__FILE__,__FUNCTION__,__LINE__,__builtin_return_address(0)); // system functions ============================================================ void *zmalloc(int64 cc, ch *tag); // malloc() wrapper void zfree(void *pp); // free() wrapper int zmalloc_test(int64 cc); // test if cc free memory available double realmemory(); // get available real memory in MB units double availmemory(); // get available memory (+swap) in MB units ch * zstrdup(ch *zstring, ch *tag, int addcc = 0); // strdup() wrapper with added space int zstrcopy(ch *&zstring, ch *string, ch *tag, int addcc = 0); // replace string with new string + added cc void zmalloc_report(GtkWidget *parent); // memory usage by tag > popup report void zmalloc_growth(GtkWidget *parent); // memory growth by tag > popup report void Plog(int lev, ch *format, ...); // add message to stdout log file, fflush() void xmessage(ch *message); // popup message not needing GTK void zexit(int popup, ch *message, ...); // exit a process and kill child processes void zbacktrace(); // produce a backtrace to stdout void zappcrash(ch *format, ...); // crash with popup message in text window void catch_signals(); // catch signals and do backtrace dump void trace(ch *file, ch *func, int line, void *addr); // implements TRACE macro void tracedump(); // dump program trace data ch * combine_argvs(int argc, ch *argv[], int Nth); // combine argv[ii] elements Nth to last double get_seconds(int init = 0); // get elapsed seconds since last call void print_seconds(ch *label); // print seconds since last call double get_seconds(timespec *ts); // real seconds from timespec struct int zstat(ch *file, uint &mtime, uint &size); // get file mod time (secs-1970) and size void start_timer(double &time0); // start a timer double get_timer(double &time0); // get elapsed time in seconds double CPUtime(); // get elapsed process CPU time int memused(); // get process real memory, MB void compact_time(time_t DT, ch *compactDT); // time_t DT to yyyymmddhhmmss void pretty_datetime(time_t DT, ch *prettyDT); // time_t DT to yyyy:mm:dd hh:mm:ss void secs_datetime(double secs, int datetime[6]); // seconds to year/mon/day/hour/min/secs void datetime_secs(int datetime[6], double *secs); // year/mon/day/hour/min/secs to seconds int parseprocfile(ch *pfile, ch *pname, double *value, ...); // get data from /proc file int parseprocrec(ch *prec, int field, double *value, ...); // get data from /proc file record int coretemp(); // get CPU temperature (package temp) int disktemp(ch *disk); // get disk temp, e.g. "/dev/sda" void zsleep(double dsecs); // sleep specified seconds void zloop(double dsecs); // loop specified seconds void spinlock(int lock); // lock/unlock enclosed code block int global_lock(ch *lockfile); // obtain exclusive lock, multi-process void global_unlock(int fd, ch *lockfile); // release the lock int resource_lock(int &resource); // simple lock/unlock usable with GTK void resource_unlock(int &resource); // (never waits, returns lock status) int zget_locked(int ¶m); // lock and get multi-thread parameter void zput_locked(int ¶m, int value); // put and unlock multi-thread parameter int zadd_locked(int ¶m, int incr); // increment multi-thread parameter pthread_t start_detached_thread(void * tfunc(void *), void * arg); // start detached thread function pthread_t start_Jthread(void * threadfunc(void *), void * arg); // start joinable thread function int wait_Jthread(pthread_t tid); // wait for completion (join thread) void synch_threads(int NT = 0); // synchronize NT threads int main_thread(); // return 1 if main() thread, else 0 int zshell(ch *options, ch *command, ...); // do shell command and get status int kill_procname(ch *wildname); // kill processes matching wilcard name int signalProc(ch *pname, ch *signal); // send signal to process by process name ch * fgets_trim(ch * buff, int bcc, FILE *, int bf = 0); // fgets() + trim trailing \n \r (blanks) ch * fgets_pend(ch * buff, int bcc, FILE *); // fgets() non-blocking wrapper int samefolder(ch *file1, ch *file2); // returns 1 if files in same folder int parsefile(ch *path, ch *&folder, ch *&file, ch *&ext); // parse components of a file name void parsefile_free(ch *&folder, ch *&file, ch *&ext); // zfree file name components int renamez(ch *file1, ch *file2); // rename, also across file systems int check_create_dir(ch *path); // check if folder exists, ask to create int cp_copy(ch *sfile, ch *dfile); // same, using shell "cp -f -p" uint diskspace(ch *file); // get disk space for given file, MB ch * get_file_extension(ch *file); // get correct .ext if file.ext is wrong int zreaddir(ch *folder, ch **&files); // return all files in a folder, sorted int zreadfile(ch *filename, ch **&rrecs); // read file, return array of records int zwritefile(ch *filename, ch **&rrecs); // write array of records to file void zreadfile_free(ch **&rrecs); // free zreadfile() memory // string functions ============================================================ int strmatch(cch *str1, cch *str2); // true if strings equal, null strings allowed int strmatchN(cch *str1, cch *str2, int cc); // true if leading N characters equal int strmatchcase(cch *str1, cch *str2); // true if strings equal except for case int strmatchcaseN(cch *str1, cch *str2, int cc); // true if leading N chars equal except case ch * substringR(ch *string, ch *delims, int Nth); // get Nth delimited substring, thread-safe ch * substringR(ch *string, ch delim, int Nth); // same, single delimiter ch * substring(ch *string, ch *delims, int Nth); // same, no zfree() needed, not thread-safe ch * substring(ch *string, ch delim, int Nth); // same, single delimiter int get_substrings(ch *instr, ch dlm, int cnt, int maxcc, ch **outstr); // get all substrings from delimited string int strHash(ch *string, uint max); // string --> random int 0 to max-1 int strncpy0(ch *dest, ch *source, uint cc); // strncpy, insure null, return 0 if fit void strnPad(ch *dest, ch *source, int cc); // strncpy with blank padding to cc int strTrim(ch *dest, ch *source); // remove trailing blanks int strTrim(ch *string); // remove trailing blanks int strTrim2(ch *dest, ch *source); // remove leading and trailing blanks int strTrim2(ch *string); // remove leading and trailing blanks int strCompress(ch *dest, ch *source); // remove all blanks incl. embedded int strCompress(ch *string); // remove all blanks int strncatv(ch *dest, int maxcc, ch *source, ...); // catenate strings (last = null) int strmatchV(ch *string, ...); // compare to N strings, return 1-N or 0 void strToUpper(ch *dest, ch *source); // move and conv. string to upper case void strToUpper(ch *string); // conv. string to upper case void strToLower(ch *dest, ch *source); // move and conv. string to lower case void strToLower(ch *string); // conv. string to lower case int repl_1str(ch *strin, ch *strout, int maxcc, ch *ssin, ch *ssout); // copy string and replace 1 substring int repl_Nstrs(ch *strin, ch *strout, int maxcc, ...); // copy string and replace N substrings int breakup_text(ch *in, ch **&out, ch *delims, int cc1, int cc2); // break long string into substrings void strncpyx(ch *out, ch *in, int ccin); // conv. string to hex format void StripZeros(ch *pNum); // 1.230000E+12 --> 1.23E+12 int blank_null(ch *string); // test for blank/null string int clean_escapes(ch *string); // replace \x escapes with real characters int utf8len(ch *utf8string); // get graphic cc for UTF8 string int utf8substring(ch *utf8out, ch *utf8in, int pos, int cc); // get graphic substring from UTF8 string int utf8_check(ch *string); // check utf8 string for encoding errors int utf8_clean(ch *string); // clean utf8 string: '?' > bad characters int utf8_position(ch *utf8in, int Nth); // get byte position of Nth graphic char. int zsed(ch *file, ...); // replace string1/3/5... with string2/4/6... ch * zstrstr(ch *haystack, ch *needle); // work like strstr() and strcasestr() ch * zstrcasestr(ch *haystack, ch *needle); // (but "" does NOT match any string) ch * zstrcpy(ch *dest, ch *source); // strcpy with overlap allowed ch * zstrncpy(ch *dest, ch *source, int cc); // strncpy with overlap allowed int zstrcmp(ch *s1, ch *s2); // like strcmp, but \n terminates like null int zstrcmp2(ch *s1, ch *s2); // strcmp using ch *args, not cch* int zstrcasecmp(ch *s1, ch *s2); // strcasecmp using ch *args, not cch* ch * zescape_quotes(ch *string); // escape quotes for use in shell commands // number conversion =========================================================== int convSI (ch *s, int &i, ch **delm = 0); // string to int int convSI (ch *s, int &i, int low, int hi, ch **delm = 0); // (with low/high limit checking) int convSD (ch *s, double &d, ch **delm = 0); // string to double int convSD (ch *s, double &d, double low, double hi, ch **delm = 0); // (with low/high limit checking) int convSF (ch *s, float &f, ch **delm = 0); // string to double int convSF (ch *s, float &f, float low, float hi, ch **delm = 0); // (with low/high limit checking) int convIS (int iin, ch *outp, int *cc = 0); // int to string, returned cc int convDS (double din, int prec, ch *outp, int *cc = 0); // double to string, precision, output cc double atofz(ch *string); // atof() for comma/period decimal points ch * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. // wildcard functions ========================================================== int MatchWild(ch * wildstr, ch * str); // wildcard string match (match = 0) int MatchWildCase(ch * wildstr, ch * str); // wildcard string match, ignoring case ch * SearchWild(ch *wpath, int &flag); // wildcard file search ch * SearchWildCase(ch *wpath, int &flag); // wildcard file search, ignoring case int zfind(ch *pattern, ch **&flist, int &NF); // wildcard file search using glob() // search and sort functions =================================================== int bsearch(int seekint, int nn, int list[]); // binary search sorted list[nn] int bsearch(ch *seekrec, ch *allrecs, int recl, int nrecs); // binary search sorted records int bsearch(ch *seekrec, ch **allrecs, int N, int nrecs); // binary search sorted pointers to recs typedef int HeapSortUcomp(ch *rec1, ch *rec2); // return -1/0/+1 if rec1 rec2 // single-thread versions void heapsort(int vv[], int nn); // Heap Sort - integer void heapsort(float vv[], int nn); // Heap Sort - float void heapsort(double vv[], int nn); // Heap Sort - double void heapsort(ch *vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order // 4-thread versions void HeapSort(int vv[], int nn); // Heap Sort - integer void HeapSort(float vv[], int nn); // Heap Sort - float void HeapSort(double vv[], int nn); // Heap Sort - double void HeapSort(ch *vv[], int nn); // Heap Sort - char *, ascending order void HeapSort(ch *vv1[], ch *vv2[], int nn); // Heap Sort - parallel char *, ascending void HeapSort(ch *vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order void HeapSort(ch *recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order int MemSort(ch * RECS, int RL, int NR, int KEYS[][3], int NK); // memory sort, records with multiple keys int zmember(int testval, int matchval1, ...); // test if value matches any in a list // hash table class ============================================================ class HashTab { static int tries1; // insert tries static int tries2; // find/delete tries int cap; // table capacity int count; // strings contained int cc; // string length ch *table = 0; // table[cc][cap] public: HashTab(int cc, int cap); // constructor ~HashTab(); // destructor int Add(ch * string); // add a new string int Del(ch * string); // delete a string int Find(ch * string); // find a string int GetCount() { return count; }; // get string count int GetNext(int & first, ch * string); // get first/next string int Dump(); // dump hash table }; // list processing functions =================================================== typedef struct { // list data type int count; // count of member strings ch **mber = 0; // member strings, null == missing } zlist_t; zlist_t * zlist_new(int count); // create zlist with 'count' empty members void zlist_free(zlist_t *zlist); // delete zlist, free memory void zlist_dump(zlist_t *zlist); // dump zlist to stdout int zlist_count(zlist_t *zlist); // get zlist member count ch * zlist_get(zlist_t *zlist, int Nth); // get a zlist member void zlist_put(zlist_t *zlist, ch *string, int Nth); // put a zlist member (replace existing) void zlist_insert(zlist_t *zlist, ch *string, int Nth); // insert Nth member, old Nth >> Nth+1 void zlist_remove(zlist_t *zlist, int Nth); // remove a zlist member (count -= 1) void zlist_clear(zlist_t *zlist, int Nth); // clear zlist from Nth member to end void zlist_purge(zlist_t *zlist); // purge zlist of all null members int zlist_add(zlist_t *zlist, ch *string, int Funiq); // add new member at first null or append int zlist_append(zlist_t *zlist, ch *string, int Funiq); // append new member at end (if unique) int zlist_prepend(zlist_t *zlist, ch *string, int Funiq); // prepend new member at posn 0 (if unique) int zlist_find(zlist_t *zlist, ch *string, int posn); // find next match from posn int zlist_findwild(zlist_t *zlist, ch *wstring, int posn); // find next wildcard match from posn zlist_t * zlist_copy(zlist_t *zlist1); // copy a zlist zlist_t * zlist_insert_zlist(zlist_t *zlist1, zlist_t *zlist2, int Nth); // insert zlist2 into zlist1 at posn Nth zlist_t * zlist_remove(zlist_t *zlist1, zlist_t *zlist2); // remove all members of zlist2 from zlist1 void zlist_sort(zlist_t *zlist); // sort zlist ascending void zlist_sort(zlist_t *zlist, int ccfunc(ch *, ch *)); // sort zlist via caller compare function int zlist_to_file(zlist_t *zlist, ch *filename); // make file from zlist zlist_t * zlist_from_file(ch *filename); // make zlist from file zlist_t * zlist_from_folder(ch *foldername); // make zlist from files in a folder // random number functions ===================================================== int lrandz(int64 * seed); // returns 0 to 0x7fffffff int lrandz(); // built-in seed double drandz(int64 * seed); // returns 0.0 to 0.99999... double drandz(); // built-in seed // spline curve-fitting functions ============================================== void spline1(int nn, float *dx, float *dy); // define a curve using nn data points float spline2(float x); // return y-value for given x-value // FIFO queue for text strings, single or dual-thread access =================== typedef struct { int qcap; // queue capacity int qnewest; // newest entry position, circular int qoldest; // oldest entry position, circular int qdone; // flag, last entry added to queue ch **qtext; // up to qcap text strings } Qtext; void Qtext_open(Qtext *qtext, int cap); // initialize Qtext queue, empty void Qtext_put(Qtext *qtext, ch *format, ...); // add text string to Qtext queue ch * Qtext_get(Qtext *qtext); // remove text string from Qtext queue void Qtext_close(Qtext *qtext); // close Qtext, zfree() leftover strings // compute variance for a list of numbers (std. deviation = sqrtf(variance)) float variance(float *vals, int N); // application initialization and administration =============================== int zinitapp(ch *appvers, int argc, ch *argv[]); // initialize app (appname-N.N, custom homedir) ch * get_zprefix(); // get /usr or /usr/local ... ch * get_zhomedir(); // get /home/user/.appname/ ch * get_zdatadir(); // get data folder ch * get_zimagedir(); // get image folder ch * get_zdocdir(); // get document folder void zabout(GtkWidget *parent); // popup app 'about' information void zsetfont(ch *newfont); // set new app font and size int widget_font_metrics(GtkWidget *widget, int &fww, int &fhh); // get widget font char width/height int get_zfilespec(ch *ftype, ch *fname, ch *filespec); // get installation data file void showz_logfile(GtkWidget *parent); // show log file in popup window void showz_textfile(ch *type, ch *file, GtkWidget *parent); // show text file [.gz] in popup window void showz_docfile(GtkWidget *, ch *docfile, ch *topic); // show docfile topic and assoc. image /******************************************************************************** GTK utility functions *********************************************************************************/ void zmainloop(int skip = 0); // do main loop, process menu events void zmainsleep(float secs); // do main loop and sleep designated time /********************************************************************************/ // cairo drawing region for GDK window typedef struct { GdkWindow *win; cairo_rectangle_int_t rect; cairo_region_t *reg; GdkDrawingContext *ctx; cairo_t *dcr = 0; } draw_context_t; cairo_t * draw_context_create(GdkWindow *gdkwin, draw_context_t &context); void draw_context_destroy(draw_context_t &context); /********************************************************************************/ // connect KB and mouse events to widget response function #define G_SIGNAL(widget,event,func,arg) \ g_signal_connect(G_OBJECT(widget),event,G_CALLBACK(func),(void *) arg) /********************************************************************************/ // txwidget functions - scrollable text widget for text reports and line editing // widget = zdialog_gtkwidget(zd,txwidget) where txwidget is a zdialog "text" widget type void txwidget_clear(GtkWidget *widget); // clear all text void txwidget_clear(GtkWidget *widget, int line); // clear text from line to end int txwidget_linecount(GtkWidget *widget); // get current line count void txwidget_append(GtkWidget *widget, int bold, ch *format, ...); // append line void txwidget_append2(GtkWidget *widget, int bold, ch *format, ...); // append line and scroll to end void txwidget_insert(GtkWidget *widget, int bold, int line, ch *format, ...); // insert line void txwidget_replace(GtkWidget *widget, int bold, int line, ch *format, ...); // replace line void txwidget_delete(GtkWidget *widget, int line); // delete line int txwidget_find(GtkWidget *widget, ch *matchtext, int line1, int hilite); // find matching line void txwidget_insert_pixbuf(GtkWidget *widget, int line, GdkPixbuf *pixbuf); // insert pixbuf image void txwidget_scroll(GtkWidget *widget, int line); // scroll line on screen void txwidget_scroll_top(GtkWidget *widget, int line); // scroll line to top of window void txwidget_get_visible_lines(GtkWidget *widget, int &top, int &bott); // get range of visible lines void txwidget_dump(GtkWidget *widget, ch *filename); // dump all text into a file void txwidget_save(GtkWidget *widget, GtkWindow *parent); // same, with save-as dialog ch * txwidget_line(GtkWidget *widget, int line, int strip); // retrieve line (strip \n) void txwidget_highlight_line(GtkWidget *widget, int line); // highlight line ch * txwidget_word(GtkWidget *, int line, int posn, ch *dlims, ch &end); // retrieve word void txwidget_highlight_word(GtkWidget *widget, int line, int posn, int cc); // highlight word void txwidget_bold_word(GtkWidget *widget, int line, int posn, int cc); // make word bold void txwidget_underline_word(GtkWidget *widget, int line, int posn, int cc); // make word underlined void txwidget_font_attributes(GtkWidget *widget); // set font attributes for all text typedef int txwidget_CBfunc_t(GtkWidget *, int line, int posn, ch *input); // widget event function to receive void txwidget_set_eventfunc(GtkWidget *, txwidget_CBfunc_t func); // mouse click and KB events /********************************************************************************/ // functions to simplify building menus, tool bars, status bars typedef void cbFunc(GtkWidget *, ch *mname); // menu or button response function GtkWidget * create_menubar(GtkWidget *vbox); // create menubar in packing box GtkWidget * add_menubar_item(GtkWidget *mbar, ch *mname, cbFunc func = 0); // add menu item to menubar GtkWidget * add_submenu_item(GtkWidget *mitem, ch *subname, // add submenu item to menu item cbFunc func = 0, ch *mtip = 0); // with opt. function and popup tip GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize = 24); // toolbar in packing box (no vert gtk3) GtkWidget * add_toolbar_button(GtkWidget *tbar, ch *lab, ch *tip, // add button with label, tool-tip, icon ch *icon, cbFunc func); GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box int stbar_message(GtkWidget *stbar, ch *message); // display message in status bar /********************************************************************************/ GtkWidget * create_popmenu(); // create an empty popup menu void add_popmenu(GtkWidget *popmenu, ch *item, cbFunc func, ch *arg, ch *tip); // add menu item to popup menu void popup_menu(GtkWidget *, GtkWidget *popmenu); // pop-up menu at current mouse posn. ch * popup_choose(zlist_t *zlist); // popup to choose item from list ch * popup_choose(ch *file); // same, list from text file /********************************************************************************/ // create vertical menu/toolbar in vertical packing box struct vmenuent { // menu data from caller ch *name = 0; // menu name, text ch *icon = 0; // opt. icon file name ch *desc = 0; // description (mouse hover popup) cbFunc *LMfunc; // menu func for left mouse click cbFunc *RMfunc; // menu func for right mouse click cbFunc *setupfunc; // opt. setup func for menu func ch *arg = 0; // callback arg for menu func ch *setuparg = 0; // callback arg for setup func ch *RMarg = 0; // callback arg for RMfunc PIXBUF *pixbuf; // icon pixbuf or null PangoLayout *playout1, *playout2; // normal and bold menu text int namex, namey; // menu name position in layout int iconx, icony; // menu icon position int ylo, yhi; // menu height limits int iconww, iconhh; // icon width and height }; struct Vmenu { GtkWidget *vbox; // parent window (container) GtkWidget *topwin; // top-level window of parent GtkWidget *layout; // drawing window float fgRGB[3]; // font color, RGB scaled 0-1 float bgRGB[3]; // background color, RGB scaled 0-1 int xmax, ymax; // layout size int mcount; // menu entry count vmenuent menu[100]; }; Vmenu *Vmenu_new(GtkWidget *vbox, float fgRGB[3], float bgRGB[3]); // create new menu in parent vbox void Vmenu_add(Vmenu *vbm, ch *name, ch *icon, int iconww, int iconhh, // add menu item with response function ch *desc, cbFunc func, ch *arg); // function may be popup_menu() void Vmenu_add_setup(Vmenu *vbm, int me, cbFunc RMfunc, ch *arg); // add opt. setup function void Vmenu_add_RMfunc(Vmenu *vbm, int me, cbFunc RMfunc, ch *arg); // add function for right mouse click void Vmenu_block(int flag); // block or unblock menu /********************************************************************************/ // spline curve edit functions typedef void spcfunc_t(int spc); // callback function, spline curve edit struct spldat { // spline curve data GtkWidget *drawarea; // drawing area for spline curves spcfunc_t *spcfunc; // callback function when curve changed int Nscale; // no. of fixed scale lines, 0-10 float xscale[2][10]; // 2 x-values for end points float yscale[2][10]; // 2 y-values for end points int Nspc; // number of curves, 1-10 int fact[10]; // curve is active int vert[10]; // curve is vert. (1) or horz. (0) int mod[10]; // curve is edited/modified int nap[10]; // anchor points per curve float apx[10][50], apy[10][50]; // up to 50 anchor points per curve float yval[10][1000]; // y-values for x = 0 to 1 by 0.001 }; spldat * splcurve_init(GtkWidget *frame, void func(int spc)); // initialize spline curves int splcurve_adjust(void *, GdkEventButton *event, spldat *); // curve editing function int splcurve_addnode(spldat *, int spc, float px, float py); // add a new node to a curve int splcurve_resize(GtkWidget *); // adjust drawing area height int splcurve_draw(GtkWidget *, cairo_t *, spldat *); // spline curve draw signal function int splcurve_generate(spldat *, int spc); // generate data from anchor points float splcurve_yval(spldat *, int spc, float xval); // get curve y-value int splcurve_load(spldat *sd, FILE *fid); // load curve from a file int splcurve_save(spldat *sd, FILE *fid); // save curve to a file /********************************************************************************/ // functions to implement GTK dialogs with less complexity // widget types: dialog, hbox, vbox, hsep, vsep, frame, scrwin, label, link, // entry, edit, text, radio, check, button, togbutt, spin, // combo, hscale, vscale, imagebutt, colorbutt, icon, image #define zdmaxwidgets 300 #define zdmaxbutts 10 #define zdsentinel 0x97530000 #define zdialog_max 20 struct zwidget { ch *type = 0; // hbox, vbox, label, entry ... ch *wname = 0; // widget name ch *pname = 0; // parent (container) name ch *data = 0; // widget data, initial / returned int size; // text entry cc or image pixel size int homog; // hbox/vbox: equal spacing flag int expand; // widget is expandable flag int space; // extra padding space (pixels) int wrap; // wrap mode for edit widget int rescale; // widget is rescaled for more resolution double lval, nval, hval; // scale range and neutral value double lolim, hilim, step; // range and step value for number widget zlist_t *zlist; // combo box list of text entries GtkWidget *widget; // GTK widget pointer }; struct zdialog { int sentinel1; // validity sentinel1 int uniqueID; // unique ID, monotone increasing ch *title = 0; // dialog title void *eventCB; // dialog event user callback function void *popup_report_CB; // callback function for popup_report int zrunning; // dialog is running (0,1) int zstat; // dialog status (from completion button) ch zstat_button[40]; // completion button label int disabled; // widget signals/events are disabled int saveposn; // save and recall window position each use int saveinputs; // save and recall user inputs each use GtkWidget *dialog; // dialog window or null (box parent) GtkWidget *parent; // parent window or null ch *ptype = 0; // null or "window" or "box" parent ch *compbutton[zdmaxbutts]; // dialog completion button labels GtkWidget *compwidget[zdmaxbutts]; // dialog completion button widgets zwidget widget[zdmaxwidgets]; // dialog widgets (EOF = type = 0) ch event[40]; // active event or widget GtkWidget *lastwidget; // last widget active int sentinel2; // validity sentinel2 }; zdialog *zdialog_new(ch *title, GtkWidget *parent, ...); // create a zdialog with opt. buttons void zdialog_set_title(zdialog *zd, ch *title); // change zdialog title void zdialog_set_modal(zdialog *zd); // set zdialog modal void zdialog_set_decorated(zdialog *zd, int decorated); // set zdialog decorated or not void zdialog_present(zdialog *zd); // zdialog visible and on top void zdialog_can_focus(zdialog *zd, int Fcan); // zdialog can/not have focus (e.g. report) void zdialog_set_focus(zdialog *zd, ch *widget = null); // set focus on window [ widget ] int zdialog_add_widget_long(zdialog *zd, // add widget to zdialog ch *type, ch *wname, ch *pname, // required args ch *data = 0, int size = 0, int homog = 0, // optional args int expand = 0, int space = 0, int wrap = 0); int zdialog_add_widget(zdialog *zd, // add widget to zdialog ch *type, ch *wname, ch *pname, // (alternative form) ch *data = 0, ch *options = 0); // "size=nn|homog|expand|space=nn|wrap" int zdialog_valid(zdialog *zd, ch *title = 0); // return 1/0 if zdialog valid/invalid int zdialog_valid2(zdialog *zd, ch *title = 0); // silent version of above int zdialog_find_widget(zdialog *zd, ch *wname); // find zdialog widget from widget name GtkWidget * zdialog_gtkwidget(zdialog *zd, ch *wname); // GTK widget from zdialog widget name int zdialog_set_image(zdialog *zd, ch *wname, GdkPixbuf *); // set "image" widget from a GDK pixbuf int zdialog_add_ttip(zdialog *zd, ch *wname, ch *ttip); // add popup tool tip to a widget int zdialog_resize(zdialog *zd, int width, int height); // set size > widget sizes int zdialog_put_data(zdialog *zd, ch *wname, ch *data); // put data in widget (entry, spin ...) ch * zdialog_get_data(zdialog *zd, ch *wname); // get widget data int zdialog_set_limits(zdialog *, ch *wname, double min, double max); // set new widget limits (spin, scale) int zdialog_get_limits(zdialog *, ch *wname, double &min, double &max); // get widget limits (spin, scale) int zdialog_rescale(zdialog *zd, ch *wname, float, float, float); // rescale widget, lo/neut/hi vals typedef int zdialog_event(zdialog *zd, ch *wname); // widget event callback function int zdialog_run(zdialog *zd, zdialog_event = 0, ch *posn = 0); // run dialog, handle events void KBevent(GdkEventKey *event); // extern: pass KB events to main app int zdialog_send_event(zdialog *zd, ch *event); // send an event to an active dialog int zdialog_send_response(zdialog *zd, int zstat); // complete a dialog, set status int zdialog_show(zdialog *zd, int flag); // show or hide a dialog int zdialog_destroy(zdialog *zd); // destroy dialog (caller resp.) int zdialog_free(zdialog *&zd); // free zdialog memory int zdialog_wait(zdialog *zd); // wait for dialog completion int zdialog_goto(zdialog *zd, ch *wname); // put cursor at named widget void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor); // set cursor for dialog window int zdialog_stuff(zdialog *zd, ch *wname, ch *data); // stuff string data into widget int zdialog_stuff(zdialog *zd, ch *wname, int data); // stuff int data int zdialog_stuff(zdialog *zd, ch *wname, double data); // stuff double data int zdialog_stuff(zdialog *zd, ch *wname, double data, ch *format); // stuff double data, formatted int zdialog_labelfont(zdialog *zd, ch *lab, ch *font, ch *txt); // stuff label text with font int zdialog_fetch(zdialog *zd, ch *wname, ch *data, int maxcc); // get string data from widget int zdialog_fetch(zdialog *zd, ch *wname, int &data); // get int data int zdialog_fetch(zdialog *zd, ch *wname, double &data); // get double data int zdialog_fetch(zdialog *zd, ch *wname, float &data); // get float data int zdialog_combo_clear(zdialog *zd, ch *wname); // clear combo box entries int zdialog_combo_popup(zdialog *zd, ch *wname); // open combo box pick list int zdialog_load_widgets(zdialog *zd, spldat *sd, ch *fname, FILE *fid); // load zdialog widgets from a file int zdialog_save_widgets(zdialog *zd, spldat *sd, ch *fname, FILE *fid); // save zdialog widgets to a file int zdialog_load_prev_widgets(zdialog *zd, spldat *sd, ch *fname); // save last-used zdialog widgets int zdialog_save_last_widgets(zdialog *zd, spldat *sd, ch *fname); // load last-used zdialog widgets int zdialog_geometry(ch *action); // load/save zdialog window positiion/size void zdialog_set_position(zdialog *zd, ch *posn); // set initial/new zdialog window position void zdialog_save_position(zdialog *zd); // save zdialog window position int zdialog_inputs(ch *action); // load or save zdialog input fields int zdialog_save_inputs(zdialog *zd); // save zdialog input fields when finished int zdialog_load_inputs(zdialog *zd); // restore zdialog inputs from prior use ch * zdialog_text(GtkWidget *parent, ch *title, ch *inittext); // get N text input lines from user ch * zdialog_text1(GtkWidget *parent, ch *title, ch *inittext); // get one text input line from user ch * zdialog_password(GtkWidget *parent, ch *title, ch *inittext); // same, but input is replaced with '*'s int zdialog_choose(GtkWidget *parent, ch *where, ch *message, ...); // show message, return button choice int zdialog_edit_textfile(GtkWidget *parent, char *file); // popup zdialog to edit a text file // write text report in popup window zdialog * popup_report_open(ch *title, GtkWidget *parent, int ww, int hh, // open popup report - pixel size, int wrap, int Fheader, txwidget_CBfunc_t CBfunc, ...); // wrap, header line, callback function void popup_report_header(zdialog *zd, int bold, ch *format, ...); // write non-scrolling header line void popup_report_write(zdialog *zd, int bold, ch *format, ...); // write text line void popup_report_write2(zdialog *zd, int bold, ch *format, ...); // write text line and scroll to end void popup_report_top(zdialog *zd); // go to top of report window void popup_report_bottom(zdialog *zd); // go to bottom of report window void popup_report_clear(zdialog *zd); // clear report window void popup_report_clear(zdialog *zd, int line); // clear from line to end void popup_report_insert(zdialog *zd, int bold, int line, ch *format, ...); // insert new line void popup_report_replace(zdialog *zd, int bold, int line, ch *format, ...); // replace existing line void popup_report_delete(zdialog *zd, int line); // delete line int popup_report_find(zdialog *zd, ch *matchtext, int line1); // find matching line void popup_report_insert_pixbuf(zdialog *zd, int line, GdkPixbuf *pixbuf); // insert pixbuf image after line void popup_report_scroll(zdialog *zd, int line); // scroll to make line visible void popup_report_scroll_top(zdialog *zd, int line); // scroll to put line at top void popup_report_get_visible_lines(zdialog *zd, int &top, int &bott); // get visible lines range ch * popup_report_line(zdialog *zd, int line, int strip); // retrieve line (strip \n) ch * popup_report_word(zdialog *zd, int line, int posn, ch *dlims, ch &end); // retrieve word void popup_report_highlight_line(zdialog *zd, int line); // highlight line void popup_report_highlight_word(zdialog *zd, int line, int posn, int cc); // highlight word void popup_report_underline_word(zdialog *zd, int line, int posn, int cc); // underline word void popup_report_bold_word(zdialog *zd, int line, int posn, int cc); // bold word void popup_report_font_attributes(zdialog *zd); // font attributes for entire report void popup_report_close(zdialog *zd, int secs); // close window after seconds int popup_command(ch *cmd, int ww=400, int hh=300, GtkWidget *p=0, int top=0); // shell command to popup window int monitor_file(ch *file); // monitor a text file in a popup window // popups: message, dialog, image void zmessageACK_init(); // initz. for thread usage void zmessageACK(GtkWidget *parent, ch *format, ... ); // display message, wait for OK int zmessageYN(GtkWidget *parent, ch *format, ... ); // display message, wait for YES/NO zdialog * zmessage_post(GtkWidget *, ch *loc, int s, ch *f, ...); // show message, timeout or cancel void poptext_screen(ch *text, int px, int py, float s1, float s2); // show popup text at screen posn void poptext_mouse(ch *text, int dx, int dy, float s1, float s2); // same, at mouse posn + offset void poptext_widget(GtkWidget *, ch *tx, int x, int y, float s1, float s2); // same, at widget posn + offset int poptext_killnow(); // kill current popup window // file chooser dialogs for one file, multiple files, or folder ch * zgetfile(ch *title, GtkWindow *parent, ch *action, ch *file, int hidden = 0); ch ** zgetfiles(ch *title, GtkWindow *parent, ch *action, ch *file, int hidden = 0); ch * zgetfolder(ch *title, GtkWindow *parent, ch *initfolder); // print an image file, choosing printer, paper, orientation, margins, and scale void print_image_file(GtkWidget *parent, ch *imagefile); // drag and drop functions typedef ch * drag_drop_source_func(); // user function, set drag-drop text typedef void drag_drop_dest_func(int x, int y, ch *text); // user function, get drag-drop text void drag_drop_source(GtkWidget *window, drag_drop_source_func); // connect source window to user function void drag_drop_dest(GtkWidget *window, drag_drop_dest_func); // connect dest. window to user function // miscellaneous GDK/GTK functions PIXBUF * get_thumbnail(ch *fpath, int size); // get sized thumbnail for image file GdkCursor * zmakecursor(ch *iconfile); // make a cursor from an image file PIXBUF * gdk_pixbuf_stripalpha(PIXBUF *pixbuf); // strip alpha channel from pixbuf PIXBUF * text_pixbuf(ch *text, ch *font, int fsize, GtkWidget *); // create pixbuf with text using font int move_pointer(GtkWidget *, int px, int py); // move the mouse pointer to px, py void window_to_mouse(GtkWidget *window); // move GtkWidget/window to mouse position