dkopp/0000755000175000017500000000000013774024600010636 5ustar micomicodkopp/zfuncs.h0000644000175000017500000017465113774024600012335 0ustar micomico/******************************************************************************** zfuncs.h include file for zfuncs functions Copyright 2007-2020 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. *********************************************************************************/ #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 char // 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 uchar unsigned char #define cchar 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(size_t cc); // malloc() wrapper void zfree(void *pp); // free() wrapper int zmalloc_test(size_t cc); // test if cc free memory available double availmemory(); // get available real memory in MB units char *zstrdup(cchar *string, int addcc = 0); // strdup() wrapper with added space void printz(cchar *format, ...); // printf() with immediate fflush() void zexit(cchar *message, ...); // exit a process and kill child processes void zbacktrace(); // produce a backtrace to stdout void zappcrash(cchar *format, ...); // crash with popup message in text window void catch_signals(); // catch signals and do backtrace dump void trace(cchar *file, cchar *func, int line, void *addr); // implements TRACE macro void tracedump(); // dump program trace data void beroot(int argc, char *argv[]); // restart program as root if password OK int runroot(cchar *command); // run command as root via su or sudo double get_seconds(); // seconds since 2000.01.01 void start_timer(double &time0); // start a timer double get_timer(double &time0); // get elapsed time in seconds void start_CPUtimer(double &time0); // start a process CPU timer double get_CPUtimer(double &time0); // get elapsed CPU time, seconds double CPUtime(); // get elapsed process CPU time for main() double CPUtime2(); // " include all threads double jobtime(); // " include all threads + subprocesses void compact_time(const time_t DT, char *compactDT); // time_t DT to yyyymmddhhmmss void pretty_datetime(const time_t DT, char *prettyDT); // time_t DT to yyyy-mm-dd hh:mm:ss int parseprocfile(cchar *pfile, cchar *pname, double *value, ...); // get data from /proc file int parseprocrec(char *prec, int field, double *value, ...); // get data from /proc file record int coretemp(); // get CPU core temperature, deg. C int disktemp(char *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(cchar *lockfile); // obtain exclusive lock, multi-process void global_unlock(int fd, cchar *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 void 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 void set_cpu_affinity(int cpu); // set cpu affinity for calling thread int zshell(cchar *options, cchar *command, ...); // do shell command and get status char * command_output(int &contx, cchar *command, ...); // get shell command output int command_status(int contx); // get exit status of command int signalProc(cchar * pname, cchar * signal); // pause/resume/kill subprocess char * fgets_trim(char * buff, int maxcc, FILE *, int bf = 0); // fgets + trim trailing \n \r (blanks) int samefolder(cchar *file1, cchar *file2); // returns 1 if files in same folder int parsefile(cchar *path, char **dir, char **file, char **ext); // parse a filespec int renamez(cchar *file1, cchar *file2); // rename, also across file systems int check_create_dir(char *path); // check if folder exists, ask to create int copyFile(cchar *sfile, cchar *dfile); // copy file to file or file to folder int zreaddir(cchar *folder, char **&files); // return all files in a folder, sorted int readfile(cchar *filename, char **&rrecs); // read file, return array of records void readfile_free(char **&rrecs); // free memory from readfile() char * combine_argvs(int argc, char *argv[], int Nth); // combine argv[ii] elements Nth to last char * zescape_quotes(cchar *file); // escape quote marks (") in file name // measure CPU time spent in a function or code block within a function extern VOL double cpu_profile_timer; // internal data tables extern VOL double cpu_profile_table[100]; extern VOL double cpu_profile_elapsed; void cpu_profile_init(); // initialize at start of test void cpu_profile_report(); // report CPU time per function inline void cpu_profile_enter(int fnum) // at entry to measured code block { cpu_profile_timer = cpu_profile_elapsed; } inline void cpu_profile_exit(int fnum) // at exit from measured code block { cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; } int pagefaultrate(); // monitor own process hard fault rate // string macros and functions ================================================= #define strmatch(str1,str2) (! strcmp((str1),(str2))) // TRUE if strings equal #define strmatchN(str1,str2,cc) (! strncmp((str1),(str2),(cc))) // TRUE if strings[cc] equal #define strmatchcase(str1,str2) (! strcasecmp((str1),(str2))) // TRUE if strings equal, ignoring case #define strmatchcaseN(str1,str2,cc) (! strncasecmp((str1),(str2),(cc))) // TRUE if strings[cc] equal, ignoring case char * substringR(cchar *string, cchar *delims, int Nth); // get Nth delimited substring, thread-safe char * substringR(cchar *string, cchar delim, int Nth); // same, single delimiter char * substring(cchar *string, cchar *delims, int Nth); // same, no zfree() needed, not thread-safe char * substring(cchar *string, cchar delim, int Nth); // same, single delimiter int strParms(int &bf, cchar *inp, char *pname, int maxcc, double &pval); // parse string: name1=val1 | name2 ... int strHash(cchar *string, int max); // string --> random int 0 to max-1 int64 strHash64(cchar *string, int64 max); // string --> random int 0 to max-1 int strncpy0(char *dest, cchar *source, uint cc); // strncpy, insure null, return 0 if fit void strnPad(char *dest, cchar *source, int cc); // strncpy with blank padding to cc int strTrim(char *dest, cchar *source); // remove trailing blanks int strTrim(char *string); // remove trailing blanks int strTrim2(char *dest, cchar *source); // remove leading and trailing blanks int strTrim2(char *string); // remove leading and trailing blanks int strCompress(char *dest, cchar *source); // remove all blanks incl. imbedded int strCompress(char *string); // remove all blanks int strncatv(char *dest, int maxcc, cchar *source, ...); // catenate strings (last = null) int strmatchV(cchar *string, ...); // compare to N strings, return 1-N or 0 void strToUpper(char *dest, cchar *source); // move and conv. string to upper case void strToUpper(char *string); // conv. string to upper case void strToLower(char *dest, cchar *source); // move and conv. string to lower case void strToLower(char *string); // conv. string to lower case int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout); // copy string and replace 1 substring int repl_Nstrs(cchar *strin, char *strout, ...); // copy string and replace N substrings int breakup_text(cchar *in, char **&out, cchar *dlm, int cc1, int cc2); // break long string into substrings void strncpyx(char *out, cchar *in, int ccin); // conv. string to hex format void StripZeros(char *pNum); // 1.230000E+12 --> 1.23E+12 int blank_null(cchar *string); // test for blank/null string int clean_escapes(char *string); // replace \x escapes with real characters int utf8len(cchar *utf8string); // get graphic cc for UTF8 string int utf8substring(char *utf8out, cchar *utf8in, int pos, int cc); // get graphic substring from UTF8 string int utf8_check(cchar *string); // check utf8 string for encoding errors int utf8_position(cchar *utf8in, int Nth); // get byte position of Nth graphic char. int zsed(cchar *file, ...); // replace string1/3... with string2/4... cchar * zstrstr(cchar *haystack, cchar *needle); // work like strstr() and strcasestr() cchar * zstrcasestr(cchar *haystack, cchar *needle); // (but "" does NOT match any string) // number conversion =========================================================== int convSI (cchar *s, int &i, cchar **delm = 0); // string to int int convSI (cchar *s, int &i, int low, int hi, cchar **delm = 0); // (with low/high limit checking) int convSD (cchar *s, double &d, cchar **delm = 0); // string to double int convSD (cchar *s, double &d, double low, double hi, cchar **delm = 0); // (with low/high limit checking) int convSF (cchar *s, float &f, cchar **delm = 0); // string to double int convSF (cchar *s, float &f, float low, float hi, cchar **delm = 0); // (with low/high limit checking) int convIS (int iin, char *outp, int *cc = 0); // int to string, returned cc int convDS (double din, int prec, char *outp, int *cc = 0); // double to string, precision, output cc double atofz(cchar *string); // atof() for comma/period decimal points char * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. // wildcard functions ========================================================== int MatchWild(cchar * wildstr, cchar * str); // wildcard string match (match = 0) int MatchWildCase(cchar * wildstr, cchar * str); // wildcard string match, ignoring case cchar * SearchWild(cchar *wpath, int &flag); // wildcard file search cchar * SearchWildCase(cchar *wpath, int &flag); // wildcard file search, ignoring case int zfind(cchar *pattern, char **&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(cchar *seekrec, cchar *allrecs, int recl, int nrecs); // binary search sorted records int bsearch(cchar *seekrec, cchar **allrecs, int N, int nrecs); // binary search sorted pointers to recs typedef int HeapSortUcomp(cchar *rec1, cchar *rec2); // return -1/0/+1 if rec1 rec2 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(char *vv[], int nn); // Heap Sort - char, ascending order void HeapSort(char *vv1[], char *vv2[], int nn); // Heap Sort - parallel char *, ascending void HeapSort(char *vv[], int nn, HeapSortUcomp); // Heap Sort - char, user-defined order void HeapSort4(char *vv[], int nn, HeapSortUcomp); // Heap Sort - same, use 4 parallel threads void HeapSort(char *recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order int MemSort(char * 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 // list processing functions =================================================== typedef struct { // list data type int count; // count of member strings char **mber; // member strings, null == missing } zlist_t; zlist_t * zlist_new(int count); // create zlist with 'count' empty members void zlist_delete(zlist_t *zlist); // delete a zlist void zlist_dump(zlist_t *zlist); // dump zlist to stdout int zlist_count(zlist_t *zlist); // get zlist member count char * zlist_get(zlist_t *zlist, int Nth); // get a zlist member void zlist_put(zlist_t *zlist, cchar *string, int Nth); // put a zlist member (replace existing) void zlist_insert(zlist_t *zlist, cchar *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, cchar *string, int Funiq); // add new member at first null or append int zlist_append(zlist_t *zlist, cchar *string, int Funiq); // append new member at end (if unique) int zlist_prepend(zlist_t *zlist, cchar *string, int Funiq); // prepend new member at posn 0 (if unique) int zlist_find(zlist_t *zlist, cchar *string, int posn); // find next match from posn int zlist_findwild(zlist_t *zlist, cchar *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(cchar *, cchar *)); // sort zlist via caller compare function int zlist_to_file(zlist_t *zlist, cchar *filename); // make file from zlist zlist_t * zlist_from_file(cchar *filename); // make zlist from file // 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 char **qtext; // up to qcap text strings } Qtext; void Qtext_open(Qtext *qtext, int cap); // initialize Qtext queue, empty void Qtext_put(Qtext *qtext, cchar *format, ...); // add text string to Qtext queue char * Qtext_get(Qtext *qtext); // remove text string from Qtext queue void Qtext_close(Qtext *qtext); // close Qtext, zfree() leftover strings // application initialization and administration =============================== int appimage_install(cchar *appname); // make appimage desktop and icon files void appimage_unstall(); // uninstall appimage int zinitapp(cchar *appvers, cchar *homedir, int argc, char *argv[]); // initialize app (appname-N.N, custom homedir) cchar * get_zprefix(); // get /usr or /usr/local ... cchar * get_zhomedir(); // get /home/user/.appname/ cchar * get_zdatadir(); // get data folder cchar * get_zimagedir(); // get image folder cchar * get_zdocdir(); // get document folder void zabout(); // popup app 'about' information void zsetfont(cchar *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(cchar *ftype, cchar *fname, char *filespec); // get installation data file void showz_logfile(GtkWidget *parent); // show log file in popup window void showz_textfile(cchar *type, cchar *file, GtkWidget *parent); // show text file [.gz] in popup window void showz_html(cchar *url); // show html via preferred browser void showz_docfile(GtkWidget *, cchar *docfile, cchar *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); /********************************************************************************/ // textwidget functions - scrollable text widget for text reports and line editing // widget = zdialog_gtkwidget(zd,textwidget) where textwidget is a zdialog "text" widget type void textwidget_clear(GtkWidget *widget); // clear all text void textwidget_clear(GtkWidget *widget, int line); // clear text from line to end int textwidget_linecount(GtkWidget *widget); // get current line count void textwidget_append(GtkWidget *widget, int bold, cchar *format, ...); // append line void textwidget_append2(GtkWidget *widget, int bold, cchar *format, ...); // append line and scroll to end void textwidget_insert(GtkWidget *widget, int bold, int line, cchar *format, ...); // insert line void textwidget_replace(GtkWidget *widget, int bold, int line, cchar *format, ...); // replace line void textwidget_delete(GtkWidget *widget, int line); // delete line int textwidget_find(GtkWidget *widget, char *matchtext, int line1); // find matching line void textwidget_insert_pixbuf(GtkWidget *textwidget, int line, GdkPixbuf *pixbuf); // insert pixbuf image void textwidget_scroll(GtkWidget *widget, int line); // scroll line on screen void textwidget_scroll_top(GtkWidget *widget, int line); // scroll line to top of window void textwidget_get_visible_lines(GtkWidget *textwidget, int &top, int &bott); // get range of visible lines void textwidget_dump(GtkWidget *widget, cchar *filename); // dump all text into a file void textwidget_save(GtkWidget *widget, GtkWindow *parent); // same, with save-as dialog char * textwidget_line(GtkWidget *widget, int line, int strip); // retrieve line (strip \n) void textwidget_highlight_line(GtkWidget *widget, int line); // highlight line char * textwidget_word(GtkWidget *, int line, int posn, cchar *dlims, char &end); // retrieve word void textwidget_highlight_word(GtkWidget *widget, int line, int posn, int cc); // highlight word void textwidget_bold_word(GtkWidget *widget, int line, int posn, int cc); // make word bold void textwidget_underline_word(GtkWidget *widget, int line, int posn, int cc); // make word underlined void textwidget_font_attributes(GtkWidget *widget); // set font attributes for all text typedef void textwidget_callbackfunc_t(GtkWidget *, int line, int posn, int KBkey); // widget event function to receive void textwidget_set_eventfunc(GtkWidget *, textwidget_callbackfunc_t func); // mouse click and KB events /********************************************************************************/ // functions to simplify building menus, tool bars, status bars #define G_SIGNAL(window,event,func,arg) \ g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) typedef void cbFunc(GtkWidget *, cchar *mname); // menu or button response function GtkWidget * create_menubar(GtkWidget *vbox); // create menubar in packing box GtkWidget * add_menubar_item(GtkWidget *mbar, cchar *mname, cbFunc func = 0); // add menu item to menubar GtkWidget * add_submenu_item(GtkWidget *mitem, cchar *subname, // add submenu item to menu item cbFunc func = 0, cchar *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, cchar *lab, cchar *tip, // add button with label, tool-tip, icon cchar *icon, cbFunc func); GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box int stbar_message(GtkWidget *stbar, cchar *message); // display message in status bar /********************************************************************************/ GtkWidget * create_popmenu(); // create an empty popup menu GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, // add menu item to popup menu cbFunc func, cchar *arg, cchar *mtip = 0); void popup_menu(GtkWidget *, GtkWidget *popmenu); // pop-up menu at current mouse posn. /********************************************************************************/ // user editable graphic menu in popup window // menus can be added and arranged using the mouse typedef void Gmenuz_cbfunc(cchar *menu); // caller-supplied callback function void Gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, Gmenuz_cbfunc); // show window, handle mouse drag/click /********************************************************************************/ // create vertical menu/toolbar in vertical packing box struct vmenuent { // menu data from caller cchar *name; // menu name, text cchar *icon; // opt. icon file name cchar *desc; // description (mouse hover popup) cbFunc *func; // callback func (GtkWidget *, cchar *arg) cbFunc *RMfunc; // alternate func for right mouse click cchar *arg; // callback arg for func cchar *RMarg; // 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, cchar *name, cchar *icon, // add menu item with response function int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg); // function may be popup_menu() void Vmenu_add_RMfunc(Vmenu *vbm, int me, cbFunc RMfunc, cchar *arg); // add auto function if 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 #define zdialog_button_shortcuts "Done OK Cancel Apply Reset" // buttons that may have KB shortcuts struct zwidget { cchar *type; // dialog, hbox, vbox, label, entry ... cchar *wname; // widget name cchar *pname; // parent (container) name char *data; // 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 char *title; // 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) char 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 cchar *ptype; // null or "window" or "box" parent cchar *compbutton[zdmaxbutts]; // dialog completion button labels GtkWidget *compwidget[zdmaxbutts]; // dialog completion button widgets zwidget widget[zdmaxwidgets]; // dialog widgets (EOF = type = 0) char event[40]; // active event or widget GtkWidget *lastwidget; // last widget active int sentinel2; // validity sentinel2 }; zdialog *zdialog_new(cchar *title, GtkWidget *parent, ...); // create a zdialog with opt. buttons void zdialog_set_title(zdialog *zd, cchar *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, cchar *widget = null); // set focus on window [ widget ] int zdialog_add_widget(zdialog *zd, // add widget to zdialog cchar *type, cchar *wname, cchar *pname, // required args cchar *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 cchar *type, cchar *wname, cchar *pname, // (alternative form) cchar *data, cchar *options); // "size=nn|homog|expand|space=nn|wrap" int zdialog_valid(zdialog *zd, cchar *title = 0); // return 1/0 if zdialog valid/invalid int zdialog_valid2(zdialog *zd, cchar *title = 0); // silent version of above int zdialog_find_widget(zdialog *zd, cchar *wname); // find zdialog widget from widget name GtkWidget * zdialog_gtkwidget(zdialog *zd, cchar *wname); // GTK widget from zdialog widget name int zdialog_set_image(zdialog *zd, cchar *wname, GdkPixbuf *); // set "image" widget from a GDK pixbuf int zdialog_add_ttip(zdialog *zd, cchar *wname, cchar *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, cchar *wname, cchar *data); // put data in widget (entry, spin ...) cchar * zdialog_get_data(zdialog *zd, cchar *wname); // get widget data int zdialog_set_limits(zdialog *, cchar *wname, double min, double max); // set new widget limits (spin, scale) int zdialog_get_limits(zdialog *, cchar *wname, double &min, double &max); // get widget limits (spin, scale) int zdialog_rescale(zdialog *zd, cchar *wname, float, float, float); // rescale widget, lo/neut/hi vals typedef int zdialog_event(zdialog *zd, cchar *wname); // widget event callback function int zdialog_run(zdialog *zd, zdialog_event = 0, cchar *posn = 0); // run dialog, handle events void zdialog_KB_addshortcut(cchar *key, cchar *menu); // KB shortcut for zdialog compl. button void KBevent(GdkEventKey *event); // extern: pass KB events to main app int zdialog_send_event(zdialog *zd, cchar *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, cchar *wname); // put cursor at named widget void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor); // set cursor for dialog window int zdialog_stuff(zdialog *zd, cchar *wname, cchar *data); // stuff string data into widget int zdialog_stuff(zdialog *zd, cchar *wname, int data); // stuff int data int zdialog_stuff(zdialog *zd, cchar *wname, double data); // stuff double data int zdialog_stuff(zdialog *zd, cchar *wname, double data, cchar *format); // stuff double data, formatted int zdialog_labelfont(zdialog *zd, cchar *lab, cchar *font, cchar *txt); // stuff label text with font int zdialog_fetch(zdialog *zd, cchar *wname, char *data, int maxcc); // get string data from widget int zdialog_fetch(zdialog *zd, cchar *wname, int &data); // get int data int zdialog_fetch(zdialog *zd, cchar *wname, double &data); // get double data int zdialog_fetch(zdialog *zd, cchar *wname, float &data); // get float data int zdialog_combo_clear(zdialog *zd, cchar *wname); // clear combo box entries int zdialog_combo_popup(zdialog *zd, cchar *wname); // open combo box pick list int zdialog_load_widgets(zdialog *zd, spldat *sd, cchar *fname, FILE *fid); // load zdialog widgets from a file int zdialog_save_widgets(zdialog *zd, spldat *sd, cchar *fname, FILE *fid); // save zdialog widgets to a file int zdialog_load_prev_widgets(zdialog *zd, spldat *sd, cchar *fname); // save last-used zdialog widgets int zdialog_save_last_widgets(zdialog *zd, spldat *sd, cchar *fname); // load last-used zdialog widgets int zdialog_geometry(cchar *action); // load/save zdialog window positiion/size void zdialog_set_position(zdialog *zd, cchar *posn); // set initial/new zdialog window position void zdialog_save_position(zdialog *zd); // save zdialog window position int zdialog_inputs(cchar *action); // load or save zdialog input fields int zdialog_save_inputs(zdialog *zd); // save zdialog input fields when finished int zdialog_restore_inputs(zdialog *zd); // restore zdialog inputs from prior use char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext); // get short text input from user int zdialog_choose(GtkWidget *parent, cchar *where, cchar *message, ...); // show message, return button choice int zdialog_choose2(GtkWidget *parent, cchar *where, cchar *message, ...); // " " " including KB inputs // write text report in popup window zdialog * popup_report_open(cchar *title, GtkWidget *parent, int ww, int hh, // open popup report - pixel size, int Fheader, textwidget_callbackfunc_t CBfunc, ...); // header line, callback function void popup_report_header(zdialog *zd, int bold, cchar *format, ...); // write non-scrolling header line void popup_report_write(zdialog *zd, int bold, cchar *format, ...); // write text line void popup_report_write2(zdialog *zd, int bold, cchar *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, cchar *format, ...); // insert new line void popup_report_replace(zdialog *zd, int bold, int line, cchar *format, ...); // replace existing line void popup_report_delete(zdialog *zd, int line); // delete line int popup_report_find(zdialog *zd, char *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 char * popup_report_line(zdialog *zd, int line, int strip); // retrieve line (strip \n) char * popup_report_word(zdialog *zd, int line, int posn, cchar *dlims, char &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 // shell command to popup window int popup_command(cchar *command, int ww = 400, int hh = 300, GtkWidget *parent = 0, int top = 0); // popup message dialogs void zmessageACK(GtkWidget *parent, cchar *format, ... ); // display message, wait for OK int zmessageYN(GtkWidget *parent, cchar *format, ... ); // display message, wait for YES/NO zdialog * zmessage_post(GtkWidget *, cchar *loc, int s, cchar *f, ...); // show message, timeout or cancel zdialog * zmessage_post_bold(GtkWidget *, cchar *loc, int s, cchar *f, ...); // " " with big red bold font void poptext_screen(cchar *text, int px, int py, float s1, float s2); // show popup text at screen posn void poptext_mouse(cchar *text, int dx, int dy, float s1, float s2); // " " " at mouse posn + offset void poptext_window(GtkWindow *, cchar *tx, int x, int y, float s1, float s2); // " " " at window posn + offset void poptext_widget(GtkWidget *, cchar *tx, int x, int y, float s1, float s2); // " " " at widget posn + offset int poptext_killnow(); // kill current popup window int popup_image(cchar *imagefile, GtkWindow *parent, int Fnewin, int size); // popup window with image // file chooser dialogs for one file, multiple files, or folder char * zgetfile(cchar *title, GtkWindow *parent, cchar *action, cchar *file, int hidden = 0); char ** zgetfiles(cchar *title, GtkWindow *parent, cchar *action, cchar *file, int hidden = 0); char * zgetfolder(cchar *title, GtkWindow *parent, cchar *initfolder); // print an image file, choosing printer, paper, orientation, margins, and scale void print_image_file(GtkWidget *parent, cchar *imagefile); // drag and drop functions typedef char * drag_drop_source_func(); // user function, set drag-drop text typedef void drag_drop_dest_func(int x, int y, char *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(cchar *fpath, int size); // get sized thumbnail for image file GdkCursor * zmakecursor(cchar *iconfile); // make a cursor from an image file PIXBUF * gdk_pixbuf_rotate(PIXBUF *, float deg, int alfa = 0); // rotate pixbuf through any angle PIXBUF * gdk_pixbuf_stripalpha(PIXBUF *pixbuf); // strip alpha channel from pixbuf PIXBUF * text_pixbuf(cchar *text, cchar *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 /******************************************************************************** C++ classes *********************************************************************************/ // dynamic string class ======================================================== class xstring { static int tcount; // total xstring count static int tmem; // total memory used int wmi; // internal ID int xcc; // actual cc (excl. NULL) int xmem; // memory allocated cc char * xpp; // memory pointer public: xstring(int cc = 0); // default constructor xstring(cchar * ); // string constructor xstring(const xstring &); // copy constructor ~xstring(); // destructor operator cchar * () const { return xpp; } // conversion operator (cchar *) xstring operator= (const xstring &); // operator = xstring operator= (cchar *); // operator = friend xstring operator+ (const xstring &, const xstring &); // operator + friend xstring operator+ (const xstring &, cchar *); // operator + friend xstring operator+ (cchar *, const xstring &); // operator + void insert(int pos, cchar * string, int cc = 0); // insert substring at position (expand) void overlay(int pos, cchar * string, int cc = 0); // overlay substring (possibly expand) static void getStats(int & tcount2, int & tmem2); // get statistics void validate() const; // verify integrity int getcc() const { return xcc; } // return string length }; // vector (array) of xstring =================================================== class Vxstring { int nd; // count xstring * pdata; // xstring[nd] public: Vxstring(int = 0); // constructor ~Vxstring(); // destructor Vxstring(const Vxstring &); // copy constructor Vxstring operator= (const Vxstring &); // operator = xstring & operator[] (int); // operator [] const xstring & operator[] (int) const; // operator [] (const) int search(cchar * string); // find element in unsorted Vxstring int bsearch(cchar * string); // find element in sorted Vxstring int sort(int nkeys, int keys[][3]); // sort by designated subfields int sort(int pos = 0, int cc = 0); // sort by 1 subfield (cc 0 = all) int getCount() const { return nd; } // get current count }; // 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 char * table; // table[cc][cap] public: HashTab(int cc, int cap); // constructor ~HashTab(); // destructor int Add(cchar * string); // add a new string int Del(cchar * string); // delete a string int Find(cchar * string); // find a string int GetCount() { return count; }; // get string count int GetNext(int & first, char * string); // get first/next string int Dump(); // dump hash table }; // Queue class, FIFO, LIFO or mixed ============================================ class Queue { char wmi[8]; Vxstring * vd; // vector of xstrings mutex_t qmutex; // for multi-thread access int qcap; // queue capacity int qcount; // curr. queue count int ent1; // first entry pointer int entN; // last entry pointer private: void lock(); // auto locking and unlocking void unlock(); // (for multi-thread access) public: Queue(int cap); // create queue with capacity ~Queue(); // destroy queue int getCount(); // get current entry count int push(const xstring * entry, double secs); // add new entry with max. wait time xstring * pop1(); // get 1st entry (oldest) xstring * popN(); // get Nth entry (newest) }; /* ============================================================================= Tree class - sparse array indexed by names or numbers - every element of a Tree is a Tree put(): cc is data length to store get(): cc is max. data length to retrieve actual length is returned, = 0 if not found nn is array count for nodes[] arguments */ class Tree { int wmi; // for ID checking char *tname; // tree name int tmem; // tree data memory void *tdata; // tree data[tmem] int nsub; // no. sub-nodes (Trees) Tree **psub; // pointer to sub-nodes public: Tree(cchar * name); // create Tree ~Tree(); // destroy Tree int put(void * data, int cc, char * nodes[], int nn); // put data by node names[] int put(void * data, int cc, int nodes[], int nn); // put data by node numbers[] int get(void * data, int cc, char * nodes[], int nn); // get data by node names[] int get(void * data, int cc, int nodes[], int nn); // get data by node numbers[] void stats(int nnodes[], int ndata[]); // get nodes and data per level void dump(int level = 0); // diagnostic private: Tree * find(char * nodes[], int nn); // find a sub-node by names[] Tree * find(int nodes[], int nn); // find a sub-node by numbers[] Tree * make(char * nodes[], int nn); // find/create a sub-node by names[] Tree * make(int nodes[], int nn); // find/create a sub-node by numbers[] }; /********************************************************************************/ dkopp/dkopp.cc0000644000175000017500000047357713774024600012311 0ustar micomico/************************************************************************** dkopp copy files to / restore files from BRD/DVD media Copyright 2007-2020 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ***************************************************************************/ #define dkopp_release "dkopp-7.7" #define dkopp_license "GNU General Public License v.3" #define debug 0 #include #include #include #include #if defined __linux__ #include #endif #include "zfuncs.h" // order important // parameters and limits #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 char subprocName[20]; // name of created subprocess char scriptParam[200]; // parameter from script file char mbmode[20], mvmode[20]; // actual backup, verify modes double pctdone; // % done from growisofs char scrFile[XFCC]; // command line script file char backupDT[16]; // nominal backup date: yyyymmdd-hhmm char homedir[200]; // /home/user/.dkopp char TFdiskfiles[200], TFdvdfiles[200]; // scratch files in homedir char TFjobfile[200], TFfilepoop[200], TFdatetime[200]; char TFrestorefiles[200], TFrestoredirks[200]; // available DVD/BRD devices int ndvds, maxdvds = 8; char dvddevs[8][20]; // DVD/BRD devices, /dev/sr0 etc. char dvddesc[8][40]; // DVD/BRD device descriptions char dvddevdesc[8][60]; // combined device and description // backup job data char BJfile[XFCC]; // backup job file char BJdvd[20]; // DVD/BRD device: /dev/hdb char BJbmode[20]; // backup: full/incremental/accumulate char BJvmode[20]; // verify: full/incremental/thorough char 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 char *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 char dvdmp[100]; // mount point, /media/xxxxx int dvdmpcc; // mount point cc int dvdmtd; // DVD/BRD mounted char mediumDT[16]; // DVD/BRD medium last use date-time time_t dvdtime; // DVD/BRD device mod time char dvdlabel[32]; // DVD/BRD label // current files for backup struct dfrec { // disk file record char *file; // directory/filename double size; // byte count double mtime; // mod time int stat; // fstat() status int inclx; // include rec for this file char disp; // status: new modified unchanged char 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 char *file; // directory/file (- /media/xxx) double size; // byte count double mtime; // mod time int stat; // fstat() status char 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 char *RJinex[maxnx]; // file restore include/exclude recs. int RJnx; // actual RJinex count < maxnx int RJval; // restore job data validated char RJfrom[XFCC]; // restore copy-from: /home/.../ char RJto[XFCC]; // restore copy-to: /home/.../ struct rfrec { // restore file record char *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, cchar *menu); // process toolbar button event void menufunc(GtkWidget *item, cchar *menu); // process menu select event void script_func(void *); // execute script file int quit_dkopp(cchar *); // exit application int clearScreen(cchar *); // clear logging window int signalFunc(cchar *); // kill/pause/resume curr. function int checkKillPause(); // test flags: killFlag and pauseFlag int fileOpen(cchar *); // file open dialog int fileSave(cchar *); // file save dialog int BJload(cchar *fspec); // backup job data <<< file int BJstore(cchar *fspec); // backup job data >>> file int BJvload(cchar *); // load job file from DVD/BRD int BJedit(cchar *); // backup job edit dialog int BJedit_event(zdialog *zd, cchar *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(cchar *); // backup menu function int FullBackup(cchar *vmode); // full backup + verify int IncrBackup(cchar *bmode, cchar *vmode); // incremental / accumulate + verify int Verify(cchar *); // verify functions int Report(cchar *); // report functions int get_current_files(cchar *); // file stats. per include/exclude int report_summary_diffs(cchar *); // disk:DVD/BRD differences summary int report_directory_diffs(cchar *); // disk:DVD/BRD differences by directory int report_file_diffs(cchar *); // disk:DVD/BRD differences by file int list_current_files(cchar *); // list all disk files for backup int list_DVD_files(cchar *); // list all files on DVD/BRD int find_files(cchar *); // find files on disk, DVD/BRD, hist int view_backup_hist(cchar *); // view backup history files int RJedit(cchar *); // restore job edit dialog int RJedit_event(zdialog*, cchar *event); // RJedit response int RJlist(cchar *); // list DVD/BRD files to be restored int Restore(cchar *); // file restore function int getDVDs(void *); // get avail. DVD/BRD's, mount points int setDVDdevice(cchar *); // set DVD/BRD device and mount point int setDVDlabel(cchar *); // set new DVD/BRD label int mountDVD(cchar *); // mount DVD/BRD, echo outputs, status int unmountDVD(cchar *); // unmount DVD/BRD + echo outputs int ejectDVD(cchar *); // eject DVD/BRD + echo outputs int resetDVD(cchar *); // hardware reset int eraseDVD(cchar *); // fill DVD/BRD with zeros (long time) int formatDVD(cchar *); // quick format DVD/BRD int helpFunc(cchar *); // help function int fc_dialog(cchar *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(char *rec, char *&rtype, char *&fspec); // parse include/exclude record int BJvalidate(cchar *); // validate backup job data int RJvalidate(); // validate restore job data int nxValidate(char **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(char *recs, int RL, int NR, char sort); // sort file list in memory int filecomp(cchar *file1, cchar *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 cchar * checkFile(char *dfile, int compf, double &tcc); // validate file on BRD/DVD medium cchar * copyFile(cchar *vfile, char *dfile); // copy file from DVD/BRD to disk int track_filespec(cchar *filespec); // track filespec on screen, no scroll int track_filespec_err(cchar *filespec, cchar *errmess); // error logger for track_filespec() cchar * kleenex(cchar *name); // clean exotic file names for output int do_shell(cchar *pname, cchar *command); // shell command + output to window int track_growisofs_files(char *buff); // convert %done to filespec, output // dkopp menu table struct menuent { char menu1[20], menu2[40]; // top-menu, sub-menu int lock; // lock funcs: no run parallel int (*mfunc)(cchar *); // 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, char *argv[]) { GtkWidget *mbar, *tbar; // menubar and toolbar GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; GtkWidget *mDVD, *mHelp; int ii; zinitapp(dkopp_release,0,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),dkopp_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); 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","burn.png",buttonfunc); add_toolbar_button(tbar,"run DVD/BRD","run job on DVD/BRD","burn.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; char *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] = (char *) zmalloc(50); home = getenv("HOME"); // get "/home/username" if (! home) home = (char *) "/home/xxx"; strcpy(BJinex[0],"# dkopp default backup job"); // initz. default backup specs sprintf(BJinex[1],"include %s/*",home); // include /home/username/* sprintf(BJinex[2],"exclude %s/.Trash/*",home); // exclude /home/username/.Trash/* sprintf(BJinex[3],"exclude %s/.thumbnails/*",home); // exclude /home/username/.thumbnails/* 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 textwidget_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, cchar *button) { char 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 *, cchar *menu) { static int ii; static char 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 textwidget_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, textwidget_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; char buff[200], menu1[20], menu2[40]; cchar *pp; char *bb; fid = fopen(scrFile,"r"); // open file if (! fid) { textwidget_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 textwidget_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. } textwidget_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(cchar * 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(cchar * menu) { textwidget_clear(mLog); return 0; } // kill/pause/resume current function - called from menu function int signalFunc(cchar * menu) { if (strmatch(menu,"kill job")) { if (! menuLock) { textwidget_append2(mLog,0,"\n""ready \n"); // already dead return 0; } if (killFlag) { // redundant kill if (*subprocName) { textwidget_append2(mLog,0," *** kill again: %s \n",subprocName); signalProc(subprocName,"kill"); // kill subprocess } else textwidget_append2(mLog,0," *** waiting for function exit \n"); // or wait for function to die return 0; } textwidget_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(cchar * menu) { char *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(cchar * menu) { char *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(cchar * fspec) { FILE *fid; char buff[1000]; cchar *fgs, *rtype, *rdata; char rtype2[20]; int cc, Nth, nerrs; BJreset(); // clear old job from memory nerrs = 0; textwidget_append2(mLog,1,"\n""loading job file: %s \n",fspec); fid = fopen(fspec,"r"); // open file if (! fid) { textwidget_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) { textwidget_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); // include/exclude or comment rec. if (++BJnx >= maxnx) { textwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx); nerrs++; break; } continue; } textwidget_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(cchar * fspec) { FILE *fid; int ii; fid = fopen(fspec,"w"); // open file if (! fid) { textwidget_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(cchar * menu) { char 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(cchar * 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,"entry","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,"edit","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, cchar *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) { textwidget_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"); textwidget_clear(editwidget); for (int ii = 0; ii < BJnx; ii++) textwidget_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; char 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 = textwidget_line(editwidget,line,1); // include/exclude recs. if (! pp || ! *pp) break; strTrim(pp); // remove trailing blanks BJinex[BJnx] = zstrdup(pp); // copy new record if (++BJnx >= maxnx) { textwidget_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(cchar *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 textwidget_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) textwidget_append2(mLog,0,"ready \n"); return 0; } // full backup using multiple DVD/BRD media if required int FullBackup(cchar * BJvmode) { FILE *fid = 0; int gerr, ii, zstat; char command[200], Nspeed[20] = ""; char *dfile, vfile[XFCC]; double secs, bspeed, time0; dGetFiles(); // get files for backup if (Dnf == 0) { textwidget_append2(mLog,0," *** nothing to back-up \n"); goto backup_fail; } vFilesReset(); // reset DVD/BRD files data textwidget_append2(mLog,1,"\n""begin full backup \n"); textwidget_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) { textwidget_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,"=","\\\\="); // replace "=" with "\\=" in file name fprintf(fid,"%s=%s\n",vfile+1,dfile); // directories/file=/directories/file Dbytes2 += Drec[ii].size; } fclose(fid); textwidget_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 textwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; textwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); textwidget_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; textwidget_append2(mLog,0," backup is being repeated \n"); commFail = 0; } } createBackupHist(); // create backup history file textwidget_append2(mLog,0," backup job complete \n"); ejectDVD(0); return 0; backup_fail: commFail++; secs = get_timer(time0); // output stats even if failed textwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Dbytes2/1000000.0/secs; textwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); textwidget_append2(mLog,1," *** BACKUP FAILED \n"); textwidget_append2(mLog,0," media may be OK: check with Verify \n"); ejectDVD(0); return 0; } // incremental / accumulate backup (one DVD/BRD only) int IncrBackup(cchar * BJbmode, cchar * BJvmode) { FILE *fid = 0; int gerr, ii, zstat; char command[200], Nspeed[20] = ""; char *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) { textwidget_append2(mLog,0," *** no files for backup \n"); goto backup_fail; } if (! Vnf) { textwidget_append2(mLog,0," *** no DVD/BRD files \n"); goto backup_fail; } textwidget_append2(mLog,1,"\n""begin %s backup \n",BJbmode); textwidget_append2(mLog,0," files: %d bytes: %s \n",Mfiles, // files and bytes to copy formatKBMB(Mbytes,3)); if (Mfiles == 0) { // nothing to back up textwidget_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) { textwidget_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,"=","\\\\="); // 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,"=","\\\\="); // 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 textwidget_append2(mLog,0," backup time: %.0f secs \n",secs); bspeed = Mbytes/1000000.0/secs; textwidget_append2(mLog,0," backup speed: %.2f MB/sec \n",bspeed); textwidget_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++; textwidget_append2(mLog,1," *** BACKUP FAILED \n"); vFilesReset(); ejectDVD(0); return 0; } // verify DVD/BRD disc data integrity int Verify(cchar * menu) { int ii, comp, vfiles; int dfiles1 = 0, dfiles2 = 0; int verrs = 0, cerrs = 0; char *filespec; cchar *errmess = 0; double secs, dcc1, vbytes, vspeed; double mtime, diff; double time0; STATB filestat; vGetFiles(); // get DVD/BRD files textwidget_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 { textwidget_append2(mLog,1,"\n""verify ALL files on DVD/BRD \n"); if (Fgui) textwidget_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) { textwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } if (strmatch(menu,"incremental")) // verify files in prior incr. backup { textwidget_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; textwidget_append2(mLog,0," %s \n",kleenex(filespec)); // output filespec errmess = checkFile(filespec,0,dcc1); // check file on DVD/BRD, get length if (errmess) textwidget_append2(mLog,0," *** %s \n",errmess); if (errmess) verrs++; vfiles++; vbytes += dcc1; if (verrs + cerrs > 100) { textwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } if (strmatch(menu,"thorough")) // compare DVD/BRD to disk files { textwidget_append2(mLog,1,"\n Read and verify ALL files on DVD/BRD. \n"); textwidget_append2(mLog,0," Compare to disk files with matching names and mod times.\n"); if (Fgui) textwidget_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) { textwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n"); goto verify_exit; } } } textwidget_append2(mLog,0," DVD/BRD files: %d bytes: %s \n",vfiles,formatKBMB(vbytes,3)); textwidget_append2(mLog,0," DVD/BRD read errors: %d \n",verrs); if (strmatch(menu,"thorough")) { textwidget_append2(mLog,0," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); textwidget_append2(mLog,0," compare failures: %d \n",cerrs); } secs = get_timer(time0); textwidget_append2(mLog,0," verify time: %.0f secs \n",secs); vspeed = vbytes/1000000.0/secs; textwidget_append2(mLog,0," verify speed: %.2f MB/sec \n",vspeed); if (verrs + cerrs) textwidget_append2(mLog,1," *** THERE WERE ERRORS *** \n"); else textwidget_append2(mLog,0," NO ERRORS \n"); verify_exit: if (! Vnf) textwidget_append2(mLog,0," *** no files on DVD/BRD \n"); if (! Vnf) commFail++; if (verrs + cerrs) commFail++; if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // Reports menu function int Report(cchar * 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(cchar *menu) { char *bytes; int ii; dFilesReset(); // force refresh dGetFiles(); // get disk files if (! BJval) { textwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } textwidget_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) textwidget_append2(mLog,0," %6d %9s", BJfiles[ii], bytes); if (BJfiles[ii] < 0) textwidget_append2(mLog,0," %6d %9s", BJfiles[ii], bytes); if (BJfiles[ii] == 0) textwidget_append2(mLog,0," "); textwidget_append2(mLog,0," %s \n",BJinex[ii]); } bytes = formatKBMB(Dbytes,3); textwidget_append2(mLog,0," %6d %9s TOTAL \n", Dnf, bytes); report_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences summary int report_summary_diffs(cchar *menu) { char *bytes; if (! BJval) { textwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); textwidget_append2(mLog,0,"\n disk files: %d DVD/BRD files: %d \n",Dnf,Vnf); textwidget_append2(mLog,0,"\n Differences between DVD/BRD and files on disk: \n"); textwidget_append2(mLog,0," %7d disk files not on DVD/BRD - new \n",nnew); textwidget_append2(mLog,0," %7d files on disk and DVD/BRD - unchanged \n",nunc); textwidget_append2(mLog,0," %7d files on disk and DVD/BRD - modified \n",nmod); textwidget_append2(mLog,0," %7d DVD/BRD files not on disk - deleted \n",ndel); bytes = formatKBMB(Mbytes,3); textwidget_append2(mLog,0," Total differences: %d files %s \n",nnew+ndel+nmod,bytes); report_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences by directory, summary statistics int report_directory_diffs(cchar *menu) { int kfiles, knew, kdel, kmod; int dii, vii, comp; char *pp, *pdirk, *bytes, ppdirk[XFCC]; double nbytes; if (! BJval) { textwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } dGetFiles(); vGetFiles(); setFileDisps(); SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'D'); textwidget_append2(mLog,0,"\n Disk:DVD/BRD differences by directory \n"); textwidget_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 = (char *) 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) textwidget_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 textwidget_append2(mLog,0," %5d %5d %5d %8s %s \n",knew,kmod,kdel, bytes,ppdirk); } SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore ascii sort SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'A'); report_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // report disk:DVD/BRD differences by file (new, modified, deleted) int report_file_diffs(cchar *menu) { int dii, vii; if (! BJval) { textwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } report_summary_diffs(0); // report summary first textwidget_append2(mLog,0,"\n Detailed list of disk:DVD/BRD differences: \n"); textwidget_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; textwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } textwidget_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; textwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); if (checkKillPause()) goto report_exit; } textwidget_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; textwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); if (checkKillPause()) goto report_exit; } report_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // list all files for backup int list_current_files(cchar *menu) { int dii; if (! BJval) { textwidget_append2(mLog,0," *** backup job is invalid \n"); goto report_exit; } textwidget_append2(mLog,0,"\n List all files for backup: \n"); dGetFiles(); textwidget_append2(mLog,0," %d files found \n",Dnf); for (dii = 0; dii < Dnf; dii++) { if (checkKillPause()) break; textwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); } report_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // list all files on mounted DVD/BRD int list_DVD_files(cchar *menu) { int vii; textwidget_append2(mLog,0,"\n List all files on DVD/BRD: \n"); vGetFiles(); textwidget_append2(mLog,0," %d files found \n",Vnf); for (vii = 0; vii < Vnf; vii++) { if (checkKillPause()) break; textwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); } if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // find desired files on disk, on mounted DVD/BRD, and in history files int find_files(cchar *menu) { int dii, vii, hii, ftf, nn; cchar *fspec1, *hfile1; static char fspec2[200] = "/home/*/file*"; char hfile[200], buff[1000], *pp; FILE *fid; zlist_t *zlist = 0; dGetFiles(); // get disk and DVD/BRD files if (dvdmtd) vGetFiles(); else textwidget_append2(mLog,0," DVD/BRD not mounted \n"); textwidget_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); textwidget_append2(mLog,0," search pattern: %s \n",fspec2); textwidget_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) textwidget_append2(mLog,0," %s \n",kleenex(Drec[dii].file)); } textwidget_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) textwidget_append2(mLog,0," %s \n",kleenex(Vrec[vii].file)); } textwidget_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) textwidget_append2(mLog,0," no history files found \n"); if (nn == maxhist) textwidget_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); textwidget_append2(mLog,0," %s \n",hfile1); fid = fopen(hfile1,"r"); // next history file if (! fid) { textwidget_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) textwidget_append2(mLog,0," %s \n",buff); } fclose(fid); } report_exit: if (zlist) zlist_delete(zlist); if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // list available backup history files, select one to view int view_backup_hist(cchar *menu) { cchar *fspec1; char fspec2[200], histfile[200]; char *pp; int ii, jj, nn; int zstat, ftf; zdialog *zd; zlist_t *zlist = 0; textwidget_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 = (char *) strrchr(fspec1,'/') + 12; // get yyyymmdd-hhmm-label if (nn == maxhist) break; zlist_append(zlist,pp,0); // add to list nn++; } if (nn == 0) textwidget_append2(mLog,0," no history files found \n"); if (nn == maxhist) textwidget_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 textwidget_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_delete(zlist); if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // file restore dialog - specify DVD/BRD files to be restored int RJedit(cchar * menu) { zdialog *zd; textwidget_append2(mLog,0,"\n Restore files from DVD/BRD \n"); vGetFiles(); // get files on DVD/BRD textwidget_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,"entry","entfrom","vb2",RJfrom); zdialog_add_widget(zd,"entry","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,"edit","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, textwidget_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, cchar *event) { char 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 = textwidget_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); // copy new record if (++RJnx == maxnx) { textwidget_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 textwidget_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(cchar * menu) { int cc1, cc2; char *file1, file2[XFCC]; if (! RJval) { textwidget_append2(mLog,0," *** restore job has errors \n"); goto list_exit; } textwidget_append2(mLog,0,"\n copy %d files from DVD/BRD: %s \n",Rnf, RJfrom); textwidget_append2(mLog,0," to directory: %s \n",RJto); textwidget_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)) { textwidget_append2(mLog,0," *** not within copy-from: %s \n",kleenex(file1)); RJval = 0; continue; } strcpy(file2,RJto); strcpy(file2+cc2,file1+cc1); textwidget_append2(mLog,0," %s \n",kleenex(file2)); } list_exit: if (Fgui) textwidget_append2(mLog,0," ready \n"); return 0; } // Restore files based on data from restore dialog int Restore(cchar * menu) { int ii, nn, ccf; char dfile[XFCC]; cchar *errmess; if (! RJval || ! Rnf) { textwidget_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; textwidget_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); textwidget_append2(mLog,0," %s \n",kleenex(dfile)); errmess = copyFile(Rrec[ii].file,dfile); if (errmess) textwidget_append2(mLog,0," *** %s \n",errmess); } restore_filepoop(); // restore owner/permissions dFilesReset(); // reset disk file data restore_exit: if (Fgui) textwidget_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, contx; char *buff, *pp; char command[50] = "lshw -class disk 2>/dev/null"; // better than udevadm dvdrw = ndvds = 0; contx = 0; while ((buff = command_output(contx,command))) { 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; } } 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]); } textwidget_append2(mLog,0," DVD/BRD devices found: %d \n",ndvds); // output list of DVDs for (ii = 0; ii < ndvds; ii++) textwidget_append2(mLog,0," %s %s \n",dvddevs[ii],dvddesc[ii]); return 0; } // set DVD/BRD device and mount point int setDVDdevice(cchar *menu) { cchar *pp1; char *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,"entry","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; textwidget_append2(mLog,0," DVD/BRD and mount point: %s %s \n",BJdvd,dvdmp); if (Fgui) textwidget_append2(mLog,0," ready \n"); zdialog_free(zd); return 0; } // set label for subsequent DVD/BRD backup via growisofs int setDVDlabel(cchar *menu) { cchar *pp; if (*dvdlabel) textwidget_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); textwidget_append2(mLog,0," new DVD/BRD label: %s \n",dvdlabel); return 1; } // Mount DVD/BRD with message feedback to window. int mountDVD(cchar *menu) // menu mount function { int err, reset, contx; char command[200], mbuff[200], *pp; cchar *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 contx = 0; while ((pp = command_output(contx,"cat /etc/mtab"))) // get mounted disk info { pp1 = substring(pp,' ',1); // get /dev/xxx if (! pp1 || ! strmatch(pp1,BJdvd)) { zfree(pp); // not my DVD/BRD continue; } pp1 = substring(pp,' ',2); // get mount point if (! pp1) { zfree(pp); continue; } repl_1str(pp1,dvdmp,"\\040"," "); // replace "\040" with " " dvdmpcc = strlen(dvdmp); textwidget_append2(mLog,0," already mounted: %s %s \n",BJdvd,dvdmp); dvdmtd = 1; } if (dvdmtd) goto showpoop; 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) { contx = 0; while ((pp = command_output(contx,"cat /etc/mtab"))) // get mounted disk info { pp1 = substring(pp,' ',1); // get /dev/xxx if (! pp1 || ! strmatch(pp1,BJdvd)) { zfree(pp); // not my DVD/BRD continue; } pp1 = substring(pp,' ',2); // get mount point if (! pp1) { zfree(pp); continue; } repl_1str(pp1,dvdmp,"\\040"," "); // replace "\040" with " " dvdmpcc = strlen(dvdmp); textwidget_append2(mLog,0," %d %d mounted \n",BJdvd,dvdmp); dvdmtd = 1; } if (dvdmtd) goto showpoop; // mounted OK 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; } textwidget_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(command,99,"volname %s",BJdvd); // get DVD/BRD label fid = popen(command,"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); } textwidget_append2(mLog,0," DVD/BRD label: %s last dkopp: %s \n",dvdlabel,mediumDT); commFail = 0; return 0; } // unmount DVD/BRD int unmountDVD(cchar *menu) { char command[100]; vFilesReset(); dvdmtd = 0; dvdtime = -1; snprintf(command,100,"umount %s 2>&1",dvdmp); // use mount point do_shell("umount",command); if (Fgui) textwidget_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(cchar *menu) { char command[60]; vFilesReset(); dvdmtd = 0; dvdtime = -1; sprintf(command,"eject %s 2>&1",BJdvd); do_shell("eject",command); if (Fgui) textwidget_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(cchar * 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) textwidget_append2(mLog,0," ready \n"); return 0; } // Erase DVD/BRD medium by filling it with zeros int eraseDVD(cchar * menu) { char 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) textwidget_append2(mLog,0," ready \n"); return 0; } // Format DVD/BRD (2-4 minutes) int formatDVD(cchar * menu) { char 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) textwidget_append2(mLog,0," ready \n"); return 0; } // Display help/about or help/user guide int helpFunc(cchar * menu) { if (strmatch(menu,"about")) zabout(); // 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(cchar *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; char *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 = (char *) g_slist_nth_data(flist,ii); if (! file1) break; file2 = zstrdup(file1,2); // extra space for wildcard g_free(file1); err = stat(file2,&filestat); if (err) textwidget_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) textwidget_append2(editwidget,0,"include %s""\n",ppf); if (arg == 102) textwidget_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) { textwidget_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; char file[XFCC], dirk[XFCC], pdirk[XFCC], *pp; STATB dstat; fid = fopen(TFfilepoop,"w"); if (! fid) { textwidget_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) { textwidget_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) { textwidget_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; char file1[XFCC], file2[XFCC]; char poopfile[100]; textwidget_append2(mLog,0,"\n restore directory owner and permissions \n"); textwidget_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) { textwidget_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 textwidget_append2(mLog,0," owner: %4d:%4d permissions: %3o file: %s \n", uid, gid, perms, kleenex(file2)); err = chown(file2,uid,gid); if (err) textwidget_append2(mLog,0," *** error: %s \n",strerror(errno)); err = chmod(file2,perms); if (err) textwidget_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; char backupfile[200]; char disp; snprintf(backupfile,199,"%s/dkopp-hist-%s-%s", // create history file name: homedir,backupDT,dvdlabel); // dkopp-hist-yyyymmdd-hhmm-dvdlabel textwidget_append2(mLog,1,"\n""create history file: %s \n",backupfile); fid = fopen(backupfile,"w"); if (! fid) { textwidget_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) textwidget_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(char * rec, char *& rtype, char *& fspec) { char *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(cchar * menu) { int ii, err, nerr = 0; int year, mon, day; struct tm tm_date, *tm_date2; STATB dstat; textwidget_append2(mLog,1,"\n""Validate backup job data \n"); BJval = 0; if (! BJnx) { textwidget_append2(mLog,0," *** no job data present \n"); commFail++; return 0; } textwidget_append2(mLog,0," DVD/BRD device: %s \n",BJdvd); err = stat(BJdvd,&dstat); if (err || ! S_ISBLK(dstat.st_mode)) { textwidget_append2(mLog,0," *** DVD/BRD device is apparently invalid \n"); nerr++; } textwidget_append2(mLog,0," backup %s \n",BJbmode); if (! strmatchV(BJbmode,"full","incremental","accumulate",null)) { textwidget_append2(mLog,0," *** backup mode not full/incremental/accumulate \n"); nerr++; } textwidget_append2(mLog,0," verify %s \n",BJvmode); if (! strmatchV(BJvmode,"full","incremental","thorough",null)) { textwidget_append2(mLog,0," *** verify mode not full/incremental/thorough \n"); nerr++; } textwidget_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) { textwidget_append2(mLog,0," *** date must be > 1970.01.01 \n"); nerr++; BJtdate = 0; } nerr += nxValidate(BJinex,BJnx); // validate include/exclude recs textwidget_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; char rdirk[XFCC]; DIR *pdirk; if (RJval) return 1; textwidget_append2(mLog,0,"\n Validate restore job data \n"); if (! RJnx) { textwidget_append2(mLog,0," *** no job data present \n"); return 0; } textwidget_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) { textwidget_append2(mLog,0," *** invalid copy-from location \n"); nerr++; } else closedir(pdirk); cc = strlen(RJfrom); // insure '/' at end if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); textwidget_append2(mLog,0," copy-to: %s \n",RJto); pdirk = opendir(RJto); // validate copy-to location if (! pdirk) { textwidget_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 textwidget_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(char **inexrecs, int nrecs) { char *rtype, *fspec, nxrec[XFCC]; int ii, nstat, errs = 0; for (ii = 0; ii < nrecs; ii++) // process include/exclude recs { strcpy(nxrec,inexrecs[ii]); textwidget_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) { textwidget_append2(mLog,0," *** cannot parse \n"); // cannot parse errs++; continue; } if (nstat == 3) { // bad filespec textwidget_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() { cchar *fsp; char *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 textwidget_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); 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) { textwidget_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); textwidget_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) textwidget_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); textwidget_append2(mLog,0," files for backup: %d %s \n",nfiles,mbytes); if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match textwidget_append2(mLog,0," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); textwidget_append2(mLog,0," *** bug: nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); goto errret; } SortFileList((char *) 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)) { textwidget_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; char command[200], *pp; char 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 textwidget_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 textwidget_append2(mLog,0," %s \n",command); err = zshell(0,command); // list all DVD/BRD files to temp file if (err) { textwidget_append2(mLog,0," *** find command failed: %s \n",strerror(err)); commFail++; return 0; } fid = fopen(TFdvdfiles,"r"); // read file list if (! fid) { textwidget_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) { textwidget_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,"\\=","="); // replace "\=" with "=" in file name Vrec[Vnf].file = zstrdup(fspec2 + dvdmpcc); // 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((char *) Vrec,sizeof(vfrec),Vnf,'A'); // sort Vrec[Vnf] by Vrec[].file textwidget_append2(mLog,0," DVD/BRD files: %d \n",Vnf); return 0; } // get all DVD/BRD restore files specified by include/exclude records int rGetFiles() { char *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; textwidget_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 textwidget_append2(mLog,0," %s \n",rjrec); // output nstat = inexParse(rjrec,rtype,fspec); // parse if (nstat == 0) continue; // comment repl_1str(fspec,fspecx,"\\=","="); // 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); // add matching files Rnf++; ninc++; if (Rnf == maxfs) zappcrash("exceed %d files",maxfs); } textwidget_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++; } textwidget_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; textwidget_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)) { textwidget_append2(mLog,0," *** not under copy-from; %s \n",Rrec[ii].file); RJval = 0; // mark restore job invalid continue; } } SortFileList((char *) 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; char 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(char * recs, int RL, int NR, char 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(cchar * rec1, cchar * rec2) // ascii comparison { dfrec *r1 = (dfrec *) rec1; dfrec *r2 = (dfrec *) rec2; return strcmp(r1->file,r2->file); } int fcompD(cchar * rec1, cchar * 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(cchar *file1, cchar *file2) // special compare filenames { // subdirectories compare before files cchar *pp1, *pp10, *pp2, *pp20; char 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 cchar * copyFile(cchar * vfile, char *dfile) { char vfile1[XFCC], vfilex[XFCC]; int fid1, fid2, err, rcc; char *pp, buff[vrcc]; cchar *errmess; STATB fstat; struct timeval ftimes[2]; strcpy(vfile1,dvdmp); // prepend DVD/BRD mount point strcat(vfile1,vfile); repl_1str(vfile1,vfilex,"=","\\="); // 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 cchar * checkFile(char * 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 char vfile[XFCC], *vbuff = 0, *dbuff = 0; cchar *errmess = 0; double dtime, vtime; STATB filestat; tcc = 0.0; strcpy(vfile,dvdmp); // prepend mount point repl_1str(dfile,vfile+dvdmpcc,"=","\\="); // 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(cchar * filespec) { int cc; char pdirk[300], pfile[300], *pp; if (! Fgui) { printf(" %s \n",filespec); return 0; } pp = (char *) 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); } textwidget_replace(mLog,0,-2," %s \n",kleenex(pdirk)); // output /directory textwidget_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(cchar * filespec, cchar * errmess) { if (Fgui) { textwidget_replace(mLog,0,-2," *** %s %s \n",errmess,kleenex(filespec)); textwidget_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 cchar * kleenex(cchar *name) { static char 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(cchar * pname, cchar * command) { int scroll, pscroll; int err, gerr = 0, contx = 0; char buff[1000]; const char *crec, *errmess; textwidget_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; textwidget_scroll(mLog,-1); // scroll to last line while ((crec = command_output(contx,command))) { strncpy0(buff,crec,999); 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 textwidget_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) textwidget_append2(mLog,0,"\n"); // transition from scroll to overlay textwidget_replace(mLog,0,-1," %s: %s \n",pname,kleenex(buff)); // output, overlay prior output } if (killFlag) { textwidget_append2(mLog,0,"*** %s \n","pkill %s",subprocName); err = zshell(0,buff); } zmainloop(); while (pauseFlag) zmainsleep(0.2); } errmess = 0; err = command_status(contx); 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) textwidget_append2(mLog,0," %s status: %d %s \n", pname, err, errmess); else textwidget_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(char * buff) { static double bbytes, gpct0, gpct; static int dii, dii2, err; static char *dfile; if (! buff) { // initialize dii = 0; dii2 = -1; bbytes = 0; dfile = (char *) ""; 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/zfuncs.cc0000644000175000017500000210203613774024600012461 0ustar micomico/******************************************************************************** zfuncs.cc collection of Linux and GDK/GTK utility functions Copyright 2007-2020 Michael Cornelison source code URL: https://kornelix.net contact: mkornelix@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See https://www.gnu.org/licenses This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. *********************************************************************************/ #include "zfuncs.h" /******************************************************************************** System Utility Functions ------------------------ zmalloc zfree replace malloc() etc. to add checks and statistics zmalloc_test check if planned allocation is available availmemory get available real memory (incl. file cache) in MB units zstrdup duplicate string in allocated memory with added space printz printf() with immediate fflush() zexit exit a process and kill all child processes zbacktrace callable backtrace dump zappcrash abort with traceback dump to desktop file catch_signals trap segfault, crash with zappcrash() TRACE trace() and tracedump() implement the TRACE macro beroot restart image as root, if password is OK runroot run a command or program as root user get_seconds get time in real seconds with millisecond resolution start_timer start a named timer get_timer get elapsed time with millisecond resolution start_CPUtimer start CPU process time timer get_CPUtimer get elapsed CPU process time CPUtime get CPU process time for current process CPUtime2 get CPU process time for " " + threads jobtime get CPU process time for " " + threads + child processes compact_time convert time_t type to yyyymmddhhmmss format pretty_datetime convert time_t type to yyyy-mm-dd hh:mm:ss format 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 + 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 set_cpu_affinity set CPU (SMP) affinity for calling process or thread zshell run shell command with options log, ack command_output start shell command and read the output as records signalProc pause, resume, or kill a child process fgets_trim fgets() with trim of trailing \r \n and optionally blanks 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 copyFile copy file to file or file to folder zreaddir return all files in a folder, sorted combine_argvs catenate argv[ii] elements from Nth to last zescape_quotes escape quote marks (") in file names cpu_profile measure CPU time spent per function or code block pagefaultrate monitor and report own process hard page fault rate String Functions ---------------- substringR get delimited substrings from input string substring same, not thread-safe, no zfree() needed strParms parse a string in the form "parm1=1.23, parm2=22 ..." 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 substring within 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 UTF-8 functions deal with UTF-8 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 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 Misc Functions -------------- zlist functions list processing functions - array of string pointers 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 Application Admin Functions --------------------------- appimage_install make desktop and icon files for appimage menu integration appimage_unstall uninstall appimage program and desktop 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 zinitapp initialize application folder and data files 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_html show a local or remote HTML file 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 textwidget text report, navigation, line editing menus / toolbars simplified GTK menus, toolbars and status bars Gmenuz customizable graphic popup menu create_popmenu implement popup menus 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_set_group set a common group for a set of radio buttons 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_event response handler for "focus-in-event" signal zdialog_KB_addshortcut set KB shortcuts for zdialog completion buttons 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 zduakig_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_restore_inputs retrieve prior zdialog input fields zdialog_text popup zdialog to get 1-2 lines of text input from user zdialog_choose popup zdialog to show a message, select a button, return choice zdialog_choose2 popup zdialog, same as above with addition of KB inputs popup_report popup window and scrolling text report popup_command run a shell command with output in a popup window 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 zmessage_post_bold popup message, printf format, big bold red font poptext_screen popup message at given absolute screen position poptext_mouse popup message at current mouse position + offset poptext_window popup message at given window position + offset poptext_widget popup message at given widget position + offset poptext_killnow kill popup message popup_image show an image in a small popup window 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_rotate rotate a pixbuf through any angle 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 C++ Classes ----------- xstring string manipulation (= / + / insert / overlay) Vxstring array of xstrings with auto growth HashTab hash table: add, delete, find, step through Queue queue of xstrings: push, pop first or last Tree store / retrieve data by node names or numbers, any depth *********************************************************************************/ #define Bapply "Apply" #define Bcancel "Cancel" #define Bdelete "Delete" #define Bno "No" #define BOK "OK" #define Bopen "Open" #define Bsave "Save" #define Byes "Yes" #define Bfind "Find" 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 cchar *zcontact = "mkornelix@gmail.com"; // author contact cchar *build_date_time = __DATE__ " " __TIME__; // build date and time char *progexe = 0; // executable image file char *appimagexe = 0; // appimage executable image file int monitor_ww, monitor_hh; // monitor dimensions int appfontsize = 10; // application font size cchar *appfont = "sans 10"; // application font defaults cchar *appboldfont = "sans bold 10"; cchar *appmonofont = "mono 10"; cchar *appmonoboldfont = "mono bold 10"; char zappname[40] = "undefined"; // appname without version char zappvers[40] = "undefined"; // appname-N.N char zprefix[200], zdatadir[200], zdocdir[200]; // app folders char zimagedir[200], zhomedir[200]; 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 zdialog *zdialog_list[zdialog_max]; // active zdialog list int zdialog_count = 0; // total zdialogs (new - free) int zdialog_busy = 0; // open zdialogs (run - destroy) float splcurve_minx = 5; // min. anchor point dist, % scale } using namespace zfuncs; /******************************************************************************** system-level utility functions *********************************************************************************/ // malloc() free() and strdup() wrappers with added functionality void * zmalloc(size_t cc) { double memavail, fcc; static int ftf = 1, memcheck = 1; if (ftf) { // first call ftf = 0; parseprocfile("/proc/meminfo","MemAvailable:",&memavail,0); // 'MemAvailable' available? if (! memavail) memcheck = 0; // no, memory check not possible } if (memcheck && cc > 10000) // large block { memavail = availmemory(); // real avail. memory, MB fcc = cc / 1024 / 1024; if (memavail - fcc < 700) zexit("OUT OF MEMORY"); // < 700 MB, quit before OOM killer } void *pp = malloc(cc); // allocate memory if (! pp) zexit("OUT OF MEMORY"); memset(pp,0,cc); // clear to zero return pp; } void zfree(void *puser) { return free(puser); } // test if a given about of free memory is available // return 1 if OK, return 0 if NO. int zmalloc_test(size_t cc) { double memavail, fcc; memavail = availmemory(); // avail. real memory, MB fcc = cc / 1024 / 1024; if (memavail - fcc > 800) return 1; // > 800 MB remaining, return OK printz("planned memory allocation of %.0f MB failed \n",fcc); return 0; // not OK } // get available memory in MB units // indlude free real memory and file cache, but not virtual memory double availmemory() { double freememory, cachememory; parseprocfile("/proc/meminfo","MemFree:",&freememory,0); // get amount of free memory parseprocfile("/proc/meminfo","Cached:",&cachememory,0); freememory = (freememory + cachememory) / 1024; // megabytes return freememory; } // duplicate string in allocated memory, with additiona space at end char *zstrdup(cchar *string, int addcc) { if (! string) zappcrash("zstrdup() null arg"); char *pp = (char *) zmalloc(strlen(string) + 1 + addcc); // add additional chars. strcpy(pp,string); return pp; } /********************************************************************************/ // printf() and flush every output immediately even if stdout is a file void printz(cchar *format, ...) { va_list arglist; va_start(arglist,format); vprintf(format,arglist); va_end(arglist); fflush(stdout); return; } /********************************************************************************/ // Output a status or error message and kill all processes in the process group. // Use the function killpg(0,SIGKILL) to kill all processes, including the caller. void zexit(cchar *errmess, ...) { va_list arglist; char buff[200]; if (errmess) { // output error message va_start(arglist,errmess); vsnprintf(buff,200,errmess,arglist); printz("zexit: %s\n",buff); } else printz("zexit\n"); killpg(0,SIGKILL); // kill all processes in group sleep(10); // wait here to die } /********************************************************************************/ // 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 library program addr2line(). void zappcrash(cchar *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]; char OS1[60] = "", OS2[60] = "", OS3[60] = ""; char message[300], progexe[300]; char buff1[300], buff2[300], hexaddr[20]; char *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); } printz("\n*** zappcrash: %s %s %s %s %s %s \n", arch, OS2, OS3, zappvers, build_date_time, message); nstack = backtrace(stacklist,nstack); // get backtrace data if (nstack <= 0) zexit("zappcrash backtrace() failure"); if (nstack > 100) nstack = 100; fid1 = fopen("zbacktrace","w"); // open backtrace data output file if (! fid1) zexit("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("zappcrash fopen() failure"); fid2 = fopen("zappcrash","w"); // open zappcrash output file if (! fid2) zexit("zappcrash fopen() failure"); fprintf(fid2,"\n*** zappcrash: %s %s %s %s %s %s \n", arch, OS2, OS3, zappvers, build_date_time, message); fprintf(fid2,"*** please send this crash report to mkornelix@gmail.com *** \n" "*** if you can, 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) { fprintf(fid2,"addr2line not available \n"); 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"); zshell(0,"cat zappcrash"); zshell(0,"mv zappcrash $(xdg-user-dir DESKTOP)/%s-zappcrash",zappvers); // move zappcrash file to desktop zexit("exit zappcrash"); } /********************************************************************************/ // 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) { const char *signame = "unknown"; if (signal == SIGTERM) zexit("TERMINATED"); if (signal == SIGKILL) zexit("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 { char filebuff[50][100]; // last 50 TRACE calls char 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(cchar *file, cchar *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; printz(" *** tracedump *** \n"); kk = ii; while (linebuff[kk]) { printz("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; } /********************************************************************************/ // restart the current program as root user (after sudo success) // argv[0] must be executable program void beroot(int argc, char *argv[]) { int err; char *args, command[500]; if (getuid() == 0) return; // already root err = zshell(0,"which xhost xterm"); // check for necessary programs if (err) zexit("xhost and xterm must be installed"); printz("become root user \n"); zshell("log ack","xhost +si:localuser:root"); // wayland, allow root for x11 args = combine_argvs(argc,argv,0); snprintf(command,500,"xterm -fa 'Mono' -fs 15 -geometry 40x5+400+300" // start root process " -e sudo -b -S %s",args); err = zshell("log ack",command); exit(err); // exit old process } /********************************************************************************/ // run a command or program as root user // command: shell command or filespec of the program to start // returns 0 if successfully started, else returns an error code int runroot(cchar *command) { char xtcommand[500]; int err; err = zshell(0,"which xhost xterm"); // check for necessary programs if (err) zexit("xhost and xterm must be installed"); printz("become root user \n"); zshell("log ack","xhost +si:localuser:root"); // wayland, allow root for x11 snprintf(xtcommand,500,"xterm -fa 'Mono' -fs 15 -geometry 40x5+400+300" " -e sudo -b -S %s",command); err = zshell("log ack",xtcommand); return err; } /********************************************************************************/ // get time in real seconds // theoretically uses a precise system clock but the precision is poor double get_seconds() { timespec time1; double time2; clock_gettime(CLOCK_MONOTONIC_RAW,&time1); time2 = time1.tv_sec; time2 += time1.tv_nsec * 0.000000001; return time2; } /********************************************************************************/ // 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; } /********************************************************************************/ // start a process CPU timer or get elapsed process CPU time // returns seconds with millisecond resolution void start_CPUtimer(double &time0) { time0 = CPUtime(); return; } double get_CPUtimer(double &time0) { return CPUtime() - 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 elapsed CPU time used by current process, including all threads. // Returns seconds with millisecond resolution. double CPUtime2() { struct rusage usage; double utime, stime; int err; err = getrusage(RUSAGE_SELF,&usage); if (err) return 0.0; utime = usage.ru_utime.tv_sec + 0.000001 * usage.ru_utime.tv_usec; stime = usage.ru_stime.tv_sec + 0.000001 * usage.ru_stime.tv_usec; return utime + stime; } /********************************************************************************/ // get elapsed process time for my process, including threads and child processes. double jobtime() { double jiffy = 1.0 / sysconf(_SC_CLK_TCK); // "jiffy" time slice = 1.0 / HZ char buff[200]; double cpu1, cpu2, cpu3, cpu4; FILE *fid; char *pp; fid = fopen("/proc/self/stat","r"); if (! fid) return 0; pp = fgets(buff,200,fid); fclose(fid); if (! pp) return 0; parseprocrec(pp,14,&cpu1,15,&cpu2,16,&cpu3,17,&cpu4,null); return (cpu1 + cpu2 + cpu3 + cpu4) * jiffy; } /********************************************************************************/ // convert a time_t date/time (e.g. st_mtime from stat() call) // into a compact date/time format "yyyymmddhhmmss" void compact_time(const time_t DT, char *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" void pretty_datetime(const time_t DT, char *prettyDT) { 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; } /********************************************************************************/ // Read and parse /proc file with records formatted "parmname xxxxxxx" // Find all requested parameters and return their numeric values int parseprocfile(cchar *pfile, cchar *pname, double *value, ...) // EOL = 0 { FILE *fid; va_list arglist; char buff[1000]; const char *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,char *); if (! pnames[ii]) break; 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(char *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) break; 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 processor core temperature // depends on file "temp1_input" somewhere under /sys/devices/* int coretemp() { FILE *fid; static int ftf = 1; static char *Tfile = 0; static char buff1[200], buff2[200], *ptemp; int temp; if (ftf) { // find file "temp1_input" ftf = 0; fid = popen("find /sys/devices/ -name temp1_input","r"); if (! fid) return 0; Tfile = fgets(buff1,200,fid); pclose(fid); printz("coretemp file: %s \n",Tfile); } if (! Tfile) return 0; snprintf(buff2,200,"cat %s",Tfile); fid = popen(buff2,"r"); if (! fid) return 0; ptemp = fgets_trim(buff2,200,fid); pclose(fid); if (! ptemp) return 0; temp = atoi(ptemp); while (temp > 200) temp = temp / 10; // may be deg. C x 10 or x 100 ... if (temp < 10) return 0; return temp; } /********************************************************************************/ // get current temperature for given disk, e.g. "/dev/sda" // depends on "smartctl" command from package smartmontools int disktemp(char *disk) { int id, temp; char *pp, *pp2; char 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. // // spinlock(1); // ... protected code // only one thread at a time can be in here // spinlock(0); pthread_mutex_t spinmutex = PTHREAD_MUTEX_INITIALIZER; void spinlock(int lock) { if (lock) pthread_mutex_lock(&spinmutex); else pthread_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" // // fd = global_lock(lockfile); // ... protected code // only one process/thread at a time // global_unlock(fd,lockfile); int global_lock(cchar *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, cchar *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, usable within or across threads // 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"); // bug, 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 { 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. void 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; zsleep(0.001); if (err == EAGAIN) continue; // this shit happens break; } zexit("pthread_create() failure: %s",strerror(err)); } /********************************************************************************/ // 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 ii, err; for (ii = 0; ii < 1000; ii++) { err = pthread_create(&tid, null, threadfunc, arg); if (! err) return tid; zsleep(0.001); if (err == EAGAIN) continue; // this shit happens break; } zexit("pthread_create() failure: %s",strerror(err)); 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("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() /* 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 } /********************************************************************************/ // set a CPU affinity for the calling process or thread // cpu is in the range 0 to (processor core count) - 1 void set_cpu_affinity(int cpu) { int err; static int ftf = 1, Nsmp; cpu_set_t cpuset; if (ftf) { // first call ftf = 0; Nsmp = get_nprocs(); // get SMP CPU count } if (cpu >= Nsmp) return; CPU_ZERO(&cpuset); CPU_SET(cpu,&cpuset); err = sched_setaffinity(0,sizeof(cpuset),&cpuset); if (err) printz("set_cpu_affinity() %s \n",strerror(errno)); return; } /******************************************************************************** int err = zshell(cchar *options, cchar *command, ...) Format and perform a shell command, wait for completion, return status. 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 command: shell command with optional '%' printf formats ... : optional arguments to stuff into printf formats returns: status of the shell command ***/ typedef struct { char *command; int done; int err; } zshdat_t; int zshell(cchar *options, cchar *command, ...) { void * zshell_thread(void *); zshdat_t zshdat; int Flog, Fack; va_list arglist; int jj, cc, cc2; Flog = Fack = 0; if (options) { if (strstr(options,"log")) Flog = 1; // set options if (strstr(options,"ack")) Fack = 1; } cc = strlen(command) + 1000; zshdat.command = (char *) zmalloc(cc+1); // allocate memory va_start(arglist,command); // format command cc2 = vsnprintf(zshdat.command,cc,command,arglist); va_end(arglist); if (cc2 >= cc) zappcrash("zshell: buffer overflow: %d",cc2); if (Flog) printz("zshell: %s \n",zshdat.command); // command > log file zshdat.done = 0; start_detached_thread(zshell_thread,(void *) &zshdat); // do command in parallel thread if (pthread_equal(pthread_self(),zfuncs::tid_main)) // caller is main thread { for (jj = 0; jj < 1000; jj++) { // if < 1 second, no GTK main loop if (zshdat.done == 0) zsleep(0.001); else break; } while (zshdat.done == 0) zmainsleep(0.01); // after 1 second, do GTK main loop } while (zshdat.done == 0) zsleep(0.001); // wait for completion if (zshdat.err == 127) zshdat.err = 1; // "not permitted" if (zshdat.err) { if (Flog) printz("zshell error: %s \n",strerror(zshdat.err)); // log error if wanted if (Fack) zmessageACK(mainwin,strerror(zshdat.err)); // popup error to user if wanted } zfree(zshdat.command); // free memory return zshdat.err; // return completion status } void * zshell_thread(void *arg) // thread function { zshdat_t * zshdat = (zshdat_t *) arg; int err = system(zshdat->command); // run command, returns when done zshdat->err = WEXITSTATUS(err); // get command status zshdat->done = 1; return 0; } /******************************************************************************** Run a shell command and get its outputs one record at a time. The outputs are returned one record at a time, until a NULL is returned, indicating the command has finished and has exited. The new line character is removed from the returned output records. Use contx = 0 to start a new command. Do not change the returned value. Up to 9 commands can run in parallel, with contx values 1-9. To get the command exit status: status = command_status(contx). If the command is still busy, -1 is returned. To kill a command before output is complete: command_kill(contx); ***/ FILE * CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 }; int CO_status[10]; char * command_output(int &contx, cchar *command, ...) // simplify, allow parallel usage { FILE *fid; va_list arglist; char buff[10000], *prec; if (contx == 0) // start new command { for (contx = 1; contx < 10; contx++) if (CO_contx[contx] == 0) break; if (contx == 10) { printz("*** command_output(), parallel usage > 9 \n"); return 0; } va_start(arglist,command); // format command vsnprintf(buff,9999,command,arglist); va_end(arglist); fid = popen(buff,"r"); // execute command, output to FID if (fid == 0) { CO_status[contx] = errno; // failed to start printz("*** command_output: %s\n %s\n",buff,strerror(errno)); return 0; } CO_contx[contx] = fid + 1000; CO_status[contx] = -1; // mark context busy } fid = CO_contx[contx] - 1000; prec = fgets_trim(buff,9999,fid,1); // next output, less trailing \n if (prec) return zstrdup(prec); CO_status[contx] = pclose(fid); // EOF, set status CO_contx[contx] = 0; // mark context free return 0; } int command_status(int contx) // get command exit status { int err = CO_status[contx]; return WEXITSTATUS(err); // special BS for subprocess } int command_kill(int contx) // kill output before completion { FILE *fid; if (! CO_contx[contx]) return 0; // context already closed fid = CO_contx[contx] - 1000; CO_status[contx] = pclose(fid); // close context and set status CO_contx[contx] = 0; // mark context free return 0; } /********************************************************************************/ // 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(cchar *pname, cchar *signal) { pid_t pid; FILE *fid; char 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. char * fgets_trim(char *buff, int maxcc, FILE *fid, int bf) { int cc; char *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; } /********************************************************************************/ // 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(cchar *file1, cchar *file2) { cchar *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 allocated in static memory (no zfree needed). Next call replaces the data in the static strings. Limits: folder: 1000 file: 200 ext: 8 Missing components are returned as null pointers. input ppath outputs /name1/name2/ folder /name1/name2/ with no file /name1/name2 folder /name1/name2/ if name2 a folder, otherwise folder /name1/ and file name2 /name1/name2.xxx if .xxx < 8 chars, returns file name2 and ext .xxx, otherwise returns file name2.xxx and no ext returns 0 if no error, else 1 *********************************************************************************/ int parsefile(cchar *ppath, char **pfolder, char **pfile, char **pext) { STATB statb; static char folder[1000], file[200], ext[8]; char *pp; int err, cc1, cc2; *pfolder = *pfile = *pext = null; cc1 = strlen(ppath); if (cc1 > 999) return 1; // ppath too long strcpy(folder,ppath); *pfolder = folder; err = stat(folder,&statb); // have folder only if (! err && S_ISDIR(statb.st_mode)) return 0; pp = (char *) strrchr(folder,'/'); if (! pp) return 1; // illegal pp++; cc2 = pp - folder; if (cc2 < 2 || cc2 == cc1) return 0; // have /xxxx or /xxxx/ if (strlen(pp) > 199) return 1; // filename too long strcpy(file,pp); // file part *pfile = file; *pp = 0; // remove from folder part pp = (char *) strrchr(file,'.'); if (! pp || strlen(pp) > 7) return 0; // file part, no .ext strcpy(ext,pp); // .ext part *pext = ext; *pp = 0; // remove from file part return 0; } /********************************************************************************/ // 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. // file names with embedded quote (") will fail int renamez(cchar *file1, cchar *file2) { char *pp1, *pp2; int err, Frename = 0; if (*file1 != '/' || *file2 != '/') return 1; // not absolute pathnames pp1 = strchr((char *) file1+1,'/'); pp2 = strchr((char *) 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; else return 0; } err = zshell(0,"mv -f \"%s\" \"%s\" ",file1,file2); // not 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. int check_create_dir(char *path) { int err, yn; STATB statbuf; err = stat(path,&statbuf); // check status if (! err) { if (S_ISDIR(statbuf.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("log ack","mkdir -p -m 0750 \"%s\" ",path); // create. rwx, rx, no access if (! err) return 0; zmessageACK(mainwin,"%s \n %s",path,strerror(errno)); // failed to create return errno; } /********************************************************************************/ // Copy file to file or file to an existing folder. // Missing output folders will be created. // If inut file is a symlink, copy the symlink, not the file. int copyFile(cchar *sfile, cchar *dfile) { #define BIOCC (1024*1024) // read/write block size int fid1, fid2, err, cc, dlevs; char *pp1, *pp2, buff[BIOCC]; STATB statB; static char *dfile2 = 0; if (dfile2) zfree(dfile2); // stop memory leak dfile2 = 0; err = stat(dfile,&statB); if (! err && S_ISDIR(statB.st_mode)) { // output is an existing folder pp1 = (char *) strrchr(sfile,'/'); // get source file base name if (pp1) pp1++; else pp1 = (char *) sfile; cc = strlen(pp1); // construct output file path: dfile2 = zstrdup(dfile,cc+4); // output folder + base name pp2 = dfile2 + strlen(dfile2); if (pp2[-1] != '/') *pp2++ = '/'; // insure '/' after folder strcpy(pp2,pp1); dfile = dfile2; // output file full path } else { // output is a file path pp2 = (char *) dfile; dlevs = 0; while (true) { pp2 = strchr(pp2+1,'/'); // create missing folder levels if (! pp2) break; // (check and create from top down) *pp2 = 0; err = stat(dfile,&statB); if (err) { err = mkdir(dfile,0731); if (err) { printz("%s \n %s \n",strerror(errno),dfile); return errno; } dlevs++; } *pp2 = '/'; } } err = lstat(sfile,&statB); // get input file attributes if (err) { printz("%s \n %s \n",strerror(errno),sfile); return errno; } if (S_ISLNK(statB.st_mode)) { // input file is symlink cc = readlink(sfile,buff,XFCC); if (cc < 0 || cc > XFCC-2) return errno; buff[cc] = 0; err = symlink(buff,dfile); // create output symlink if (err) { printz("%s \n %s \n %s \n",strerror(errno),buff,dfile); return errno; } return 0; } if (strmatch(sfile,dfile)) return 0; // source and dest files are same fid1 = open(sfile,O_RDONLY); // open input file if (fid1 == -1) return errno; fid2 = creat(dfile,0700); // open output file if (fid2 == -1) { err = errno; close(fid1); if (err) printz("%s \n %s \n",strerror(err),dfile); return err; } while (true) { cc = read(fid1,buff,BIOCC); // read huge blocks if (cc == 0) break; if (cc == -1) { err = errno; close(fid1); close(fid2); if (err) printz("%s \n %s \n",strerror(err),sfile); return err; } cc = write(fid2,buff,cc); // write blocks if (cc == -1) { err = errno; close(fid1); close(fid2); if (err) printz("%s \n %s \n",strerror(err),dfile); return err; } } close(fid1); // close input file err = close(fid2); // close output file if (err) { printz("%s \n %s \n",strerror(errno),dfile); return errno; } 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-zero, it is subject to zfree() int zreaddir(cchar *folder, char **&files) { struct dirent *dirent1; int Nfiles = 0, maxfiles = 100; DIR *direc; char **ufiles, **ufiles2; files = 0; // nothing returned yet ufiles = (char **) zmalloc(maxfiles * sizeof(char *)); // starting space direc = opendir(folder); // open caller's folder if (! direc) return -1; while (true) { if (Nfiles == maxfiles) // out of space { ufiles2 = (char **) zmalloc(2 * maxfiles * sizeof(char *)); // allocate new space = 2x old space memcpy(ufiles2,ufiles, maxfiles * sizeof(char *)); // 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); // 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 = readfile(cchar *filename, char &**rrecs) // // Read a text file into a list of char * 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] (char *) // 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 readfile(cchar *filename, char **&rrecs) { FILE *fid; char *recs[1001]; char 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,"readfile() 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] = (char *) zmalloc(cc+1); // allocate memory memcpy(recs[NR],pp,cc+1); // copy record NR++; if (NR == 1000) { zmessageACK(mainwin,"readfile() too many records %s",filename); errno = EFBIG; return -1; } } fclose(fid); recs[NR] = 0; // last record + 1 = null cc = (NR + 1) * sizeof(char *); // allocate caller rrecs list rrecs = (char **) zmalloc(cc); memcpy(rrecs,recs,cc); // copy record pointers + null return NR; } // free allocated records and their pointer list void readfile_free(char **&rrecs) { for (int ii = 0; rrecs[ii]; ii++) // loop until null pointer zfree(rrecs[ii]); zfree(rrecs); rrecs = 0; // set no data return; } /********************************************************************************/ // appimage problem: // command line args are separated by blanks even for strings enclosed in // quotes: "aaaa bbbb" becomes two argv[] elements, "aaaa" and "bbbb" // this makes it impossible to get file path args with embedded spaces // // char * catenate_argvs(int argc, char *argv[], Nth) // combine argv[ii] elements from Nth to last // a single space is inserted between each argv[ii] element // command ... aaaaa bbbbb ccccc produces "aaaaa bbbbb ccccc" char * combine_argvs(int argc, char *argv[], int Nth) { int ii, ccv, outcc = 0; static char 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; } /********************************************************************************/ // escape quote marks (") in a file name for use in shell commands // returned file is subject for zfree() char * zescape_quotes(cchar *file1) { char *file2 = 0; if (strchr(file1,'"') == 0) { file2 = zstrdup(file1); return file2; } file2 = zstrdup(file1,20); repl_1str(file1,file2,"\"","\\\""); return file2; } /******************************************************************************** utility to measure CPU time spent in various functions or code blocks cpu_profile_init() initialize at start of test cpu_profile_enter(fnum) at entry to a function cpu_profile_exit(fnum) at exit from a function cpu_profile_report() report CPU time per function Methodology: cpu_profile_init() starts a thread that suspends and runs every 1 millisecond and updates a timer. cpu_profile_enter() and cpu_profile_exit() accumulate the time difference between entry and exit of code being measured. This may be zero because of the long interval between timer updates. Accuracy comes from statistical sampling over many seconds, so that if the time spent in a monitored function is significant, it will be accounted for. The accuracy is better than 1% as long as the measured function or code block consumes a second or more of CPU time during the measurement period. The "fnum" argument (1-99) designates the function or code block being measured. cpu_profile_report() stops the timer thread and reports time consumed per function, using the "fnum" tags in the report. The functions cpu_profile_enter() and cpu_profile_exit() subtract the timer difference and add to a counter per fnum, so the added overhead is insignificant. They are inline functions defined as follows: enter: cpu_profile_timer = cpu_profile_elapsed; exit: cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; *********************************************************************************/ VOL double cpu_profile_table[100]; VOL double cpu_profile_timer; VOL double cpu_profile_elapsed; VOL int cpu_profile_kill = 0; void cpu_profile_init() { void * cpu_profile_timekeeper(void *); for (int ii = 0; ii < 99; ii++) cpu_profile_table[ii] = 0; cpu_profile_elapsed = 0; start_detached_thread(cpu_profile_timekeeper,null); } void cpu_profile_report() { cpu_profile_kill++; printz("elapsed: %.2f \n",cpu_profile_elapsed); for (int ii = 0; ii < 100; ii++) { double dtime = cpu_profile_table[ii]; if (dtime) printz("cpu profile func: %d time: %.2f \n",ii,dtime); } } void * cpu_profile_timekeeper(void *) { timeval time0, time1; gettimeofday(&time0,0); while (true) { gettimeofday(&time1,0); cpu_profile_elapsed = time1.tv_sec - time0.tv_sec + 0.000001 * (time1.tv_usec - time0.tv_usec); zsleep(0.001); if (cpu_profile_kill) break; } cpu_profile_kill = 0; return 0; } /********************************************************************************/ // Returns hard page fault rate in faults/second. // First call starts a thread that runs every 2 seconds and keeps a // weighted average of hard fault rate for the last few intervals. // This is a means to detect if a process is thrashing for lack of memory. namespace pagefaultrate_names { int ftf = 1; int samples = 0; int faultrate = 0; double time1, time2; void * threadfunc(void *); } int pagefaultrate() { using namespace pagefaultrate_names; if (ftf) { ftf = 0; start_detached_thread(threadfunc,0); time1 = get_seconds(); } return faultrate; } void * pagefaultrate_names::threadfunc(void *) { using namespace pagefaultrate_names; FILE *fid; char *pp, buff[200]; double pfs1, pfs2, fps, elaps; while (true) { sleep(2); time2 = get_seconds(); elaps = time2 - time1; time1 = time2; fid = fopen("/proc/self/stat","r"); if (! fid) break; pp = fgets(buff,200,fid); fclose(fid); if (! pp) break; pp = strchr(pp,')'); // closing ')' after (short) filename if (pp) parseprocrec(pp+1,10,&pfs1,11,&pfs2,null); fps = (pfs1 + pfs2) / elaps; faultrate = 0.7 * faultrate + 0.3 * fps; } printz("pagefaultrate() failure \n"); return 0; } /******************************************************************************** substringR() char * substringR(cchar *string, cchar *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). 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 *********************************************************************************/ char * substringR(cchar *string, cchar *delims, int Nth) { char *pf1, pf2[2000]; // 2000 char. limit cchar quote = '"'; int nf, fcc = 0; if (! string || ! *string) return 0; // bad call if (Nth < 1) return 0; pf1 = (char *) 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); // returned string (needs zfree()) } // alternative with one delimiter char * substringR(cchar *string, cchar delim, int Nth) { char delims[2] = "x"; *delims = delim; return substringR(string,delims,Nth); } // non-thread-safe versions without zfree() requirement. char * substring(cchar *string, cchar *delims, int Nth) { char *s1; static char s2[2000]; s1 = substringR(string,delims,Nth); if (! s1) return 0; strcpy(s2,s1); zfree(s1); return s2; } char * substring(cchar *string, cchar delim, int Nth) { char delims[2] = "x"; *delims = delim; return substring(string,delims,Nth); } /******************************************************************************** stat = strParms(begin, input, pname, maxcc, pval) Parse an input string with parameter names and values: "pname1=pval1 | pname2 | pname3=pval3 | pname4 ..." begin int & must be 1 to start new string, is modified input cchar * input string pname char * output parameter name maxcc int max. length for pname, including null pval double & output parameter value stat int status: 0=OK, -1=EOL, 1=parse error Each call returns the next pname and pval. A pname with no pval is assigned a value of 1 (present). Input format: pname1 | pname2=pval2 | pname3 ... null Leading blanks are ignored, and pnames may have embedded blanks. pvals must convert to double using convSD (accepts decimal point or comma) ***/ int strParms(int &begin, cchar *input, char *pname, int maxcc, double &pval) { static int ii, beginx = 3579246; cchar *pnamex, *delim; int cc, err; if (begin == 1) { // start new string begin = ++beginx; ii = 0; } if (begin != beginx) zappcrash("strParms call error"); // thread safe, not reentrant *pname = 0; // initz. outputs to nothing pval = 0; while (input[ii] == ' ') ii++; // skip leading blanks if (input[ii] == 0) return -1; // no more data pnamex = input + ii; // next pname for (cc = 0; ; cc++) { // look for delimiter if (pnamex[cc] == '=') break; if (pnamex[cc] == '|') break; if (pnamex[cc] == 0) break; } if (cc == 0) return 1; // err: 2 delimiters if (cc >= maxcc) return 1; // err: pname too big strncpy0(pname,pnamex,cc+1); // pname >> caller strTrim(pname); // remove trailing blanks if (pnamex[cc] == 0) { // pname + null ii += cc; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } if (pnamex[cc] == '|') { // pname + | ii += cc + 1; // position for next call pval = 1.0; // pval = 1 >> caller return 0; } ii += cc + 1; // pname = pval err = convSD(input + ii, pval, &delim); // parse pval (was strtod() if (err > 1) return 1; while (*delim == ' ') delim++; // skip poss. trailing blanks if (*delim && *delim != '|') return 1; // err: delimiter not | or null ii = delim - input; if (*delim) ii++; // position for next call return 0; } /********************************************************************************/ // Produce random value from hashed input string. // Output range is 0 to max-1. // Benchmark: 0.8 usec for 99 char. string 3 GHz Core i5 int strHash(cchar *string, int max) { uint hash = 1; uchar byte; while ((byte = *string++)) { hash *= byte; hash = hash ^ (hash >> 7); hash = hash * 7; hash = hash & 0x00FFFFFF; } hash = hash % max; return hash; } int64 strHash64(cchar *string, int64 max) { uint64 hash = 1; uchar byte; while ((byte = *string++)) { hash *= byte; hash = hash ^ (hash >> 7); hash = hash * 7; hash = hash & 0x00FFFFFFFFFFFFFF; } 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(char *dest, cchar *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(char *dest, cchar *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(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strTrim(dest); } int strTrim(char *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(char *dest, cchar *source) { cchar *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(char *string) { return strTrim2(string,(cchar *) string); } /********************************************************************************/ // Remove all blanks from a string. Returns remaining length. int strCompress(char *dest, cchar *source) { if (dest != source) strcpy(dest,source); return strCompress(dest); } int strCompress(char *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(char *dest, int maxcc, cchar *source, ...) { cchar *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,cchar *); } 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(cchar *string, ...) { int match = 0; char *stringN; va_list arglist; va_start(arglist,string); while (1) { stringN = va_arg(arglist, char *); if (stringN == null) { va_end(arglist); return 0; } match++; if (strmatch(string,stringN)) { va_end(arglist); return match; } } } /********************************************************************************/ // convert string to upper case void strToUpper(char *string) { int ii; char jj; const int delta = 'A' - 'a'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta; } void strToUpper(char *dest, cchar *source) { strcpy(dest,source); strToUpper(dest); } /********************************************************************************/ // convert string to lower case void strToLower(char *string) { int ii; char jj; const int delta = 'a' - 'A'; for (ii = 0; (jj = string[ii]); ii++) if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta; } void strToLower(char *dest, cchar *source) { strcpy(dest,source); strToLower(dest); } /********************************************************************************/ // Copy string strin to strout, replacing every occurrence // of the substring ssin with the substring ssout. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout) { int ccc, cc1, cc2, nfound; cchar *ppp; cc1 = strlen(ssin); cc2 = strlen(ssout); nfound = 0; while ((ppp = strstr(strin,ssin))) { nfound++; ccc = ppp - strin; memcpy(strout,strin,ccc); // memcpy instead of strncpy strout += ccc; strin += ccc; memcpy(strout,ssout,cc2); strin += cc1; strout += cc2; } strcpy(strout,strin); return nfound; } /********************************************************************************/ // Copy string strin to strout, replacing multiple substrings with replacement strings. // Multiple pairs of string arguments follow strout, a substring and a replacement string. // Last pair of string arguments must be followed by a null argument. // Returns the count of replacements, if any. // Replacement strings may be longer or shorter or have zero length. int repl_Nstrs(cchar *strin, char *strout, ...) { va_list arglist; cchar *ssin, *ssout; char ftemp[XFCC]; int ftf, nfound; ftf = 1; nfound = 0; va_start(arglist,strout); while (true) { ssin = va_arg(arglist, char *); if (! ssin) break; ssout = va_arg(arglist, char *); if (ftf) { ftf = 0; nfound += repl_1str(strin,strout,ssin,ssout); } else { strcpy(ftemp,strout); nfound += repl_1str(ftemp,strout,ssin,ssout); } } va_end(arglist); return nfound; } /********************************************************************************/ // 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 char between cc1 and cc2 if present. // Break at last delimiter char 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 and txout[*] are subjects for zfree(). int breakup_text(cchar *txin0, char **&txout, cchar *delims, int cc1, int cc2) { char *txin; uchar ch; int p1, p2, cc3, Nout; int Np, Bp, Sp; txin = zstrdup(txin0); txout = (char **) zmalloc(100 * sizeof(char *)); if (strstr(txin0,"\\n")) // replace "\n" with real newline chars repl_1str(txin0,txin,"\\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 ch = txin[p2]; if (ch == '\n') { Np = p2; break; } // break out if newline found if (cc3 >= cc1) { if (ch == ' ') Bp = p2; // remember last ' ' found after cc1 chars if (strchr(delims,ch)) Sp = p2; // remember last delimiter found after cc1 } if (ch < 0) while ((ch = txin[p2+1]) < 0xC0) p2++; p2++; cc3++; if (cc3 == cc2) break; } if (! cc3 && ! Np) break; if (Np) cc3 = Np - p1; else if (Bp) cc3 = Bp - p1 + 1; else if (Sp) cc3 = Sp - p1 + 1; else cc3 = p2 - p1; txout[Nout] = (char *) zmalloc(cc3+1); strncpy0(txout[Nout],txin+p1,cc3+1); Nout++; p2 = p1 + cc3; if (Np) p2++; p1 = p2; if (Nout == 100) break; } zfree(txin); return Nout; } /********************************************************************************/ // Copy and convert string to hex string. // Each input character 'A' >> 3 output characters "41 " void strncpyx(char *out, cchar *in, int ccin) { int ii, jj, c1, c2; char cx[] = "0123456789ABCDEF"; if (! ccin) ccin = strlen(in); for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3) { c1 = (uchar) 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(char *pNum) { int ii, cc; int pp, k1, k2; char 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(cchar *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(char *string) { char *pp1 = string, *pp2 = string, *pp; char char1; char 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 char of multichar sequence is negative (0xC0 to 0xFD) // - subsequent multichars are in the range 0x80 to 0xBF int utf8len(cchar *utf8string) { int ii, cc; char 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(char *utf8out, cchar *utf8in, int pos, int cc) { int ii, jj, kk, posx, ccx; char 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 string for valid utf8 encoding // returns: 0 = OK, 1 = bad string int utf8_check(cchar *string) { cchar *pp; unsigned char 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; } /********************************************************************************/ // 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(cchar *utf8in, int Nth) { int ii, posx; char 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(cchar *infile ...) { int err, ftf, nn; FILE *fid1, *fid2; char *outfile, *pp; char buffin[1000], buffout[1000], buffxx[1000]; cchar *stringin, *stringout; va_list arglist; fid1 = fopen(infile,"r"); if (! fid1) return -1; outfile = zstrdup(infile,8); strcat(outfile,"-temp"); fid2 = fopen(outfile,"w"); if (! fid2) { printz("%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, char *); if (! stringin) break; stringout = va_arg(arglist, char *); if (! stringout) break; if (ftf) { ftf = 0; nn += repl_1str(buffin,buffout,stringin,stringout); } else { strcpy(buffxx,buffout); nn += repl_1str(buffxx,buffout,stringin,stringout); } } va_end(arglist); fputs(buffout,fid2); } fclose(fid1); err = fclose(fid2); if (err) { printz("%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. const char * zstrstr(const char *haystack, const char *needle) { if (! needle || ! *needle) return 0; return strstr(haystack,needle); } const char * zstrcasestr(const char *haystack, const char *needle) { if (! needle || ! *needle) return 0; return strcasestr(haystack,needle); } /******************************************************************************** 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 itoa(inum) counterpart to function atoi(char *) string input (cchar *) or output (char *) inum input (int) or output (int &) dnum input (double) or output (double &) delim optional returned delimiter (null or cchar **) 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(cchar *string, int &inum, cchar **delim) // use glib function { char *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(cchar *string, int &inum, int lolim, int hilim, cchar **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(cchar *string, double &dnum, cchar **delim) // use glib function { char *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(cchar *string, double &dnum, double lolim, double hilim, cchar **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(cchar *string, float &fnum, cchar **delim) { double dnum; int err; err = convSD(string,dnum,delim); fnum = dnum; return err; } int convSF(cchar *string, float &fnum, float lolim, float hilim, cchar **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, char *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, char *string, int *cc) { char *pstr; snprintf(string,20,"%.*g",digits,dnum); pstr = strstr(string,"e+"); // 1.23e+12 > 1.23e12 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); // 1.23e02 > 1.23e2 if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e0"); if (pstr) strcpy(pstr+1,pstr+2); pstr = strstr(string,"e-0"); // 1.23e-02 > 1.23e-2 if (pstr) strcpy(pstr+2,pstr+3); pstr = strstr(string,"e-0"); if (pstr) strcpy(pstr+2,pstr+3); 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(cchar *string) { char 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 char * formatKBMB(double fnum, int prec) { #define Bkilo 1024 #define Bmega (Bkilo*Bkilo) #define Bgiga (Bkilo*Bkilo*Bkilo) cchar *units; static char *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] = (char *) zmalloc(20); } 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. Benchmark: 0.032 usec. wild = *asdf*qwer?yxc 3.3 GHz Core i5 match = XXXasdfXXXXqwerXyxc *********************************************************************************/ int MatchWild(cchar *pWild, cchar *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(cchar *pWild, cchar *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. cchar * SearchWild(cchar *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). ***/ cchar * SearchWild(cchar *wpath, int &uflag) { static FILE *fid = 0; static char buff[XFCC]; static char wpath2[XFCC]; static char command[XFCC]; cchar *fcomm = "find \"%s\" -type f 2>/dev/null"; int cc, err; char *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,"\"","\\\"","$","\\$",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 '/' snprintf(command,XFCC,fcomm,wpath2); // result is input to find command 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 ***/ cchar * SearchWildCase(cchar *wpath, int &uflag) { static FILE *fid = 0; static char buff[XFCC]; static char wpath2[XFCC]; static char command[XFCC]; cchar *fcomm = "find \"%s\" -type f 2>/dev/null"; int cc, err; char *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,"\"","\\\"","$","\\$",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 '/' snprintf(command,XFCC,fcomm,wpath2); // result is input to find command 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(cchar *pattern, char **&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(cchar *pattern, char **&flist, int &NF) { char **zfind_filelist = 0; // list of filespecs returned int globflags = GLOB_PERIOD; // include dotfiles int ii, jj, err, cc; glob_t globdata; char *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) printz("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(char *); zfind_filelist = (char **) zmalloc(cc); 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]); // 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(cchar *seekrec, cchar *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 char N. // The records need not be sorted. // The string length of seekrec is compared. int bsearch(cchar *seekrec, cchar **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. void HeapSort(char *plist[], int nn) ------------------------------------ Pointers are sorted in order of the strings they point to. The strings are not changed. void HeapSort(char *plist1[], char *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(char *plist[], int nn, compfunc) void HeapSort4(char *plist[], int nn, compfunc) ----------------------------------------------- Sort list of pointers to strings in user-defined order. Pointers are sorted, strings are not changed. HeapSort4 uses 4 parallel threads and is 3x faster for lists > 1000. void HeapSort(char *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(cchar *rec1, cchar *rec2) -------------------------------------- compare rec1 to rec2, return -1 0 +1 if rec1 < = > rec2 in sort order. Benchmarks: (3.3 GHz Core i5) 10 million integers: 1.5 secs 10 million doubles: 2.4 secs 10 million pointers to 100 character recs: HeapSort: 11.33 HeapSort4: 4.25 *********************************************************************************/ #define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp) // heapsort for array of integers static 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) { 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); } } // heapsort for array of floats static 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) { 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); } } // heapsort for array of doubles static 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) { 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); } } // heapsort array of pointers to strings in ascending order of strings // pointers are sorted, strings are not changed. static void adjust(char *vv[], int n1, int n2) { char **bb, *temp; int jj, kk; bb = vv - 1; jj = n1; kk = n1 * 2; while (kk <= n2) { if (kk < n2 && strcmp(bb[kk],bb[kk+1]) < 0) kk++; if (strcmp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]); jj = kk; kk *= 2; } } void HeapSort(char *vv[], int nn) { char **bb, *temp; int jj; for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn); bb = vv; for (jj = nn-1; jj > 0; jj--) { SWAP(bb[0], bb[jj]); adjust(vv,1,jj); } } // 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. static void adjust(char *vv1[], char *vv2[], int n1, int n2) { char **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(char *vv1[], char *vv2[], int nn) { char **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. static void adjust(char *vv[], int n1, int n2, HeapSortUcomp fcomp) { char **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(char *vv[], int nn, HeapSortUcomp fcomp) { char **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); } } // heaport array of pointers to strings in user-defined order. // pointers are sorted, strings are not changed. // 4 parallel threads are used, each sorting 1/4 of the list, then merge. // speed for large arrays is roughly 3x faster. namespace heapsort4 { int nn1, nn2, nn3, nn4; char **vv1, **vv2, **vv3, **vv4; pthread_t tid1, tid2, tid3, tid4; int tt1 = 1, tt2 = 2, tt3 = 3, tt4 = 4; HeapSortUcomp *ttfcomp; } void HeapSort4(char *vv[], int nn, HeapSortUcomp fcomp) { using namespace heapsort4; void * HeapSort4thread(void *arg); char **vv9, **next; if (nn < 100 || 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(HeapSort4thread,&tt1); // sort the 4 sub-lists, parallel tid2 = start_Jthread(HeapSort4thread,&tt2); tid3 = start_Jthread(HeapSort4thread,&tt3); tid4 = start_Jthread(HeapSort4thread,&tt4); wait_Jthread(tid1); // wait for 4 thread completions wait_Jthread(tid2); wait_Jthread(tid3); wait_Jthread(tid4); vv9 = (char **) malloc(nn * sizeof(char *)); // 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(char*)); // copy output list to input list free(vv9); // free output list return; } void * HeapSort4thread(void *arg) // thread function { using namespace heapsort4; 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. static int *vv1, *vv2; static void adjust(char *recs, int RL, int n1, int n2, HeapSortUcomp fcomp) { int *bb, jj, kk, temp; char *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(char *recs, int RL, int NR, HeapSortUcomp fcomp) { int *bb, jj, kk, temp, flag; char *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,fcomp); bb = vv1 - 1; for (jj = NR-1; jj > 0; jj--) { SWAP(bb[1], bb[jj+1]); adjust(recs,RL,1,jj,fcomp); } vv2 = (int *) malloc((NR+1) * sizeof(int)); for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj; vvrec = (char *) 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 (char *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 = char ascending 2 = char 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: 2 million recs of 40 bytes with 4 sort keys: 2.5 secs (3.3 GHz Core i5). ***/ int MemSortComp(cchar *rec1, cchar *rec2); int MemSortKeys[10][3], MemSortNK; int MemSort(char *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(cchar *rec1, cchar *rec2) { int ii, stat, kpos, ktype, kleng; int inum1, inum2; float rnum1, rnum2; double dnum1, dnum2; cchar *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: // char 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: // char 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); } va_end(arglist); return matchval; } /******************************************************************************** list processing functions // replace pvlist functions typedef struct { list data type int count; count of member strings char **mber; member strings, null == no members } zlist_t; zlist_t *zlist; zlist = zlist_new(N) make new zlist with N null members void zlist_delete(zlist) delete zlist, free memory void zlist_dump(zlist) dump zlist to stdout N = zlist_count(zlist) get member count 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_purge(zlist1); purge zlist of null members void zlist_clear(zlist_t *zlist, int Nth); clear zlist from Nth member to end 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 *********************************************************************************/ // create zlist with 'count' empty members zlist_t * zlist_new(int count) { zlist_t *zlist = (zlist_t *) zmalloc(sizeof(zlist_t)); zlist->count = count; if (count > 0) zlist->mber = (char **) zmalloc(count * sizeof(char *)); for (int ii = 0; ii < count; ii++) zlist->mber[ii] = null; return zlist; } // delete a zlist void zlist_delete(zlist_t *zlist) { for (int ii = 0; ii < zlist->count; ii++) if (zlist->mber[ii]) zfree(zlist->mber[ii]); zfree(zlist->mber); zlist->count = 0; zfree(zlist); return; } // dump zlist to stdout void zlist_dump(zlist_t *zlist) { printz("count: %d \n",zlist->count); for (int ii = 0; ii < zlist->count; ii++) printz("%5d %s \n",ii,zlist->mber[ii]); printz("\n"); return; } // get zlist member count int zlist_count(zlist_t *zlist) { return zlist->count; } // get a zlist member char * 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, cchar *string, int Nth) { if (Nth < 0 || Nth >= zlist->count) zappcrash("zlist_get() invalid Nth: %d",Nth); if (zlist->mber[Nth]) zfree(zlist->mber[Nth]); if (string) zlist->mber[Nth] = zstrdup(string); 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, cchar *string, int Nth) { int count, newcount; int ii1, ii2, cc; char **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 = (char **) zmalloc(newcount * sizeof(char *)); if (Nth > 0) { // copy 0 - Nth-1 ii1 = 0; ii2 = Nth; if (Nth > count) ii2 = count; cc = (ii2 - ii1) * sizeof(char *); memcpy(newmber,zlist->mber,cc); } newmber[Nth] = zstrdup(string); // insert Nth if (Nth < count) { // copy Nth - last ii1 = Nth; ii2 = count; cc = (ii2 - ii1) * sizeof(char *); memcpy(newmber+ii1+1,zlist->mber+ii1,cc); } 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; int ii1, ii2, cc; char **newmber; if (Nth < 0 || Nth >= zlist->count) zappcrash("zlist_remove() invalid Nth: %d",Nth); newcount = zlist->count - 1; newmber = (char **) zmalloc(newcount * sizeof(char *)); if (Nth > 0) { // copy 0 - Nth-1 ii1 = 0; ii2 = Nth; cc = (ii2 - ii1) * sizeof(char *); memcpy(newmber,zlist->mber,cc); } if (Nth < newcount) { // copy Nth - last ii1 = Nth; ii2 = newcount; cc = (ii2 - ii1) * sizeof(char *); memcpy(newmber+ii1,zlist->mber+ii1+1,cc); } zfree(zlist->mber); zlist->mber = newmber; zlist->count = newcount; return; } // purge zlist of all null members void zlist_purge(zlist_t *zlist) { int ii, jj; char **mber; for (ii = jj = 0; ii < zlist->count; ii++) if (zlist->mber[ii]) jj++; if (jj) mber = (char **) zmalloc(jj * sizeof(char *)); 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; } // clear zlist members from Nth to end void zlist_clear(zlist_t *zlist, int Nth) { int ii; char **mber = 0; if (Nth >= zlist_count(zlist)) return; if (Nth > 0) mber = (char **) zmalloc(Nth * sizeof(char *)); // 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; } // 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, cchar *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); 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, cchar *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, cchar *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, cchar *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, cchar *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]); 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]); 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]); 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]); 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(cchar *, cchar *)) { HeapSort(zlist->mber,zlist->count,ccfunc); return; } // make file from zlist int zlist_to_file(zlist_t *zlist, cchar *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(cchar *filename) { FILE *fid; zlist_t *zlist; int ii, count = 0; char *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); } fclose(fid); 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(char *); qtext->qtext = (char **) zmalloc(cc); 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, cchar *format, ...) { int qnext; va_list arglist; char 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->qnewest = qnext; return; } // remove oldest text string from Qtext queue // if queue empty, return a null string // returned string is subject for zfree() char * Qtext_get(Qtext *qtext) { int qnext; char *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; } /******************************************************************************** Create appimage desktop file and icon file in /home//.local/... This will make the appimage work normally within the system menus. Executed at application startup time. Source files in AppImage file system (.../AppDir/) /usr/share/appname/appname.desktop /usr/share/appname/appname.png Destination files in /home// /home//.local/bin/appname-NN.N-appimage // appimage executable file /home//.local/share/applications/appname.desktop // XDG desktop file /home//.local/share/icons/appname.png // XDG icon file returns: 1 already done 2 new desktop file created OK 3 not an appimage 4 failure *****/ namespace make_appimage_names { char appname1[60], appname2[40]; } int appimage_install(cchar *appname) { using namespace make_appimage_names; FILE *fid; int err, cc, nn; char *pp, *homedir; char buff[300]; char desktopfile1[100], desktopfile2[100]; char iconfile1[100], iconfile2[100]; char exectext1[100], exectext2[100]; char icontext1[100], icontext2[100]; pp = getenv("APPIMAGE"); // appimage executable file if (! pp) return 3; if (! strstr(pp,appname)) return 3; // not my appimage appimagexe = pp; pp = strrchr(appimagexe,'/'); if (! pp) return 4; strncpy0(appname1,pp+1,60); // appname1: appname-NN.N-appimage pp = strchr(appname1,'-'); if (pp && pp[1] > '9') pp = strchr(pp+1,'-'); // bypass '-' in appname if (! pp) return 4; cc = pp - appname1; strncpy(appname2,appname1,cc); // appname2: appname appname2[cc] = 0; cc = readlink("/proc/self/exe",buff,300); // get own program path if (cc <= 0) return 4; buff[cc] = 0; // /tmp/mountpoint/usr/bin/appname pp = strstr(buff,"/usr/"); if (! pp) return 4; pp[4] = 0; // /tmp/mountpoint/usr/ homedir = getenv("HOME"); if (strchr(homedir,' ')) { // check /home/user has no blanks printz("user home \"%s\" has embedded blank",homedir); return 4; } // /.../usr/share/appname/appname.desktop >> /home//.local/share/applications/appname.desktop snprintf(desktopfile1,100,"%s/share/%s/%s.desktop",buff,appname2,appname2); snprintf(desktopfile2,100,"%s/.local/share/applications/%s.desktop",homedir,appname2); // /.../usr/share/appname/appname.png >> /home//.local/share/icons/appname.png snprintf(iconfile1,100,"%s/share/%s/icons/%s.png",buff,appname2,appname2); snprintf(iconfile2,100,"%s/.local/share/icons/%s.png",homedir,appname2); fid = fopen(desktopfile2,"r"); // open existing desktop file if (fid) { while (true) { // read desktop file pp = fgets(buff,300,fid); if (! pp) break; if (strmatchN(buff,"Exec=",5)) break; // look for: Exec="my appimage" } fclose(fid); if (pp) { cc = strlen(appimagexe); // appimage executable file nn = strmatchN(appimagexe,buff+6,cc); // compare to Exec="my appimage" if (nn) return 1; // same, do nothing } } err = copyFile(desktopfile1,desktopfile2); // /home//.local/share/ if (err) { // /applications/appname.desktop printz("cannot create %s \n",desktopfile2); return 4; } err = copyFile(iconfile1,iconfile2); // /home//.local/share/ if (err) { // /icons/appname.png printz("cannot create %s \n",iconfile2); return 4; } snprintf(exectext1,100,"Exec=%s",appname2); // Exec=appname2 snprintf(exectext2,100,"Exec=\"%s\"",appimagexe); // Exec=appimagexe snprintf(icontext1,100,"Icon=/usr/share/%s/icons/%s.png",appname2,appname2); snprintf(icontext2,100,"Icon=%s",iconfile2); err = zsed(desktopfile2,exectext1,exectext2,icontext1,icontext2,null); // make text substitutions if (err < 0) { // failure printz("cannot update %s \n",desktopfile2); return 4; } chmod(desktopfile2,0751); // make fotoxx.desktop executable printz("appimage desktop file created at %s \n", desktopfile2); printz("appimage icon file created at %s \n", iconfile2); return 2; } /********************************************************************************/ // Uninstall AppImage program, desktop and icon files // exits if program is uninstalled // returns if not (user cancels, program is not an appimage) void appimage_unstall() { using namespace make_appimage_names; char *homedir; char desktopfileloc[200]; char iconfileloc[200]; if (! appimagexe) { printz("not an appimage, nothing was done \n"); return; } homedir = getenv("HOME"); snprintf(desktopfileloc,200,"%s/.local/share/applications/",homedir); snprintf(iconfileloc,200,"%s/.local/share/icons/",homedir); zshell("log ack","rm -f %s/%s.desktop",desktopfileloc,appname2); zshell("log ack","rm -f %s/%s.png",iconfileloc,appname2); zshell("log ack","rm -f %s",appimagexe); zexit("appimage uninstalled"); } /******************************************************************************** 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/logfile log file with error messages zprefix install location normally /usr subdirs: /bin /share /doc zdatadir installed data files /prefix/share/appname/data/ zdocdir documentation files /prefix/share/doc/appname/ zimagedir images /prefix/share/appname/images 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/logfile ***/ cchar * get_zprefix() { return zfuncs::zprefix; } // /usr or /home/ cchar * get_zhomedir() { return zfuncs::zhomedir; } // /home//.appname or /root/.appname cchar * get_zdatadir() { return zfuncs::zdatadir; } // data files cchar * get_zdocdir() { return zfuncs::zdocdir; } // documentation files cchar * get_zimagedir() { return zfuncs::zimagedir; } // image files int zinitapp(cchar *appvers, cchar *homedir, int argc, char *argv[]) // appname-N.N, alternate home folder { char logfile[200], oldlog[200]; char buff[300], Phomedir[200]; char cssfile[200]; char *pp, *ch_time; int ii, cc, err; time_t startime; STATB statb; FILE *fid; startime = time(null); // app start time, secs. since 1970 catch_signals(); // catch signals, do backtrace for (ii = 0; ii < argc; ii++) printz("%s ",argv[ii]); // log command line printz("\n"); setlocale(LC_NUMERIC,"C"); // stop comma decimal points setpgid(0,0); // make a new process group strncpy0(zappvers,appvers,40); // appname-N.N [ -test ] printz("%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 err = appimage_install(zappname); // if appimage, menu integration if (err == 4) zexit("failed to create ~/.local/share/applications/%s.desktop \n" "desktop menu integration did not work",zappname); if (argc > 1 && strmatch(argv[1],"-uninstall")) { // uninstall appimage appimage_unstall(); // (does not return) exit(0); } progexe = 0; cc = readlink("/proc/self/exe",buff,300); // get my executable program path if (cc <= 0) zexit("readlink() /proc/self/exe) failed"); buff[cc] = 0; // readlink() quirk progexe = zstrdup(buff); if (appimagexe) printz("program exe: %s \n",appimagexe); // print appimage path (container) else printz("program exe: %s \n",progexe); // else 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); if (ch_time[4] == ' ') ch_time[4] = '0'; // replace month day ' d' with '0d' printz("build date/time: %s \n",ch_time); if (homedir && *homedir == '/') // homedir from caller strncpy0(zhomedir,homedir,199); else { snprintf(zhomedir,199,"%s/.%s",getenv("HOME"),zappname); // use /home//.appname snprintf(Phomedir,200,"%s-home",zhomedir); // check /home//.appname-home fid = fopen(Phomedir,"r"); if (fid) { pp = fgets_trim(Phomedir,200,fid); // if found, read pointer to homedir if (pp) strncpy0(zhomedir,pp,200); fclose(fid); } } printz("%s home: %s \n",zappname,zhomedir); // forbid space in home folder if (strchr(zhomedir,' ')) zexit("home folder name contains a space"); cc = strlen(zhomedir); // stop humongous username if (cc > 160) zexit("home folder name too big"); err = stat(zhomedir,&statb); // does app home exist already? if (err) { err = mkdir(zhomedir,0750); // no, create and initialize if (err) zexit("cannot create %s: %s",zhomedir,strerror(errno)); } if (! isatty(fileno(stdin))) { // not attached to a terminal snprintf(logfile,199,"%s/logfile",zhomedir); // /home//logfile snprintf(oldlog,199,"%s/logfile.old",zhomedir); err = stat(logfile,&statb); if (! err) rename(logfile,oldlog); // rename old log file fid = freopen(logfile,"a",stdout); // redirect output to log file fid = freopen(logfile,"a",stderr); if (! fid) printz("*** cannot redirect stdout and stderr \n"); } 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' printz("start %s %s \n",zappname,ch_time); fflush(0); err = stat(zdatadir,&statb); if (! err) zshell(0,"cp -R -n %s/* %s",zdatadir,zhomedir); // copy MISSING files > user home tid_main = pthread_self(); // thread ID of main() process // GTK initialization setenv("GTK_THEME","default",0); // set theme if missing (KDE etc.) setenv("GDK_BACKEND","x11",0); // wayland if (gtk_clutter_init(&argc,&argv) != CLUTTER_INIT_SUCCESS) // intiz. clutter and GTK if (! gtk_init_check(0,null)) zexit("GTK initialization 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(); printz("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("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("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 } snprintf(cssfile,200,"%s/widgets.css",get_zhomedir()); 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); return 1; } // popup window with application 'about' information void zabout() // 22.01 { int zabout_dialog_event(zdialog *zd, cchar *event); FILE *fid; zdialog *zd; int cc; char *pp = 0; char installed_release[80]; char current_release[80]; char command[80]; char title[40]; char *execfile; /*** __________________________________________________ | About Appname | | | | installed release: appname-N.N Mon dd yyyy | | current release: appname-N.N Mon dd yyyy | | contact: mkornelix@gmail.com | | executable: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx | |__________________________________________________| ***/ snprintf(command,80,"wget -T 5 -q -O - https://kornelix.net/%s/current-release",zappname); fid = popen(command,"r"); if (fid) { pp = fgets_trim(current_release,80,fid,1); pclose(fid); } if (! pp) strcpy(current_release,"NO RESPONSE"); if (appimagexe) execfile = appimagexe; else 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,null,"OK",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","hbcrel","dialog"); zdialog_add_widget(zd,"label","labcr1","hbcrel","current release:","space=3"); zdialog_add_widget(zd,"label","labcr2","hbcrel",current_release); 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_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_run(zd,zabout_dialog_event,"mouse"); return; } int zabout_dialog_event(zdialog *zd, cchar *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(cchar *newfont) { char font[40], bfont[48], mfont[48], mbfont[56]; char 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); appboldfont = zstrdup(bfont); appmonofont = zstrdup(mfont); appmonoboldfont = zstrdup(mbfont); appfontsize = size; return; fail: printz("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) { printz("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(cchar *filetype, cchar *filename, char *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) { char buff[200]; fflush(0); snprintf(buff,199,"cat %s/logfile",zhomedir); 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(const char *type, const char *file, GtkWidget *parent) { char filex[40], filespec[200], command[200]; int err; 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); 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; } // show a local or remote html file using the user's preferred browser // to show a local file starting at an internal live link location: // url = "file://folder/.../filename-livelink void showz_html(cchar *url) { static char prog[40]; static int ftf = 1, err; if (ftf) { ftf = 0; *prog = 0; err = zshell(0,"which firefox"); // use xdg-open only as last resort if (! err) strcpy(prog,"firefox"); else { err = zshell(0,"which chromium-browser"); if (! err) strcpy(prog,"chromium-browser --new-window"); else { err = zshell(0,"which xdg-open"); if (! err) strcpy(prog,"xdg-open"); } } } if (! *prog) { zmessageACK(mainwin,"html file reader not found"); return; } zshell("log ack","%s %s &",prog,url); return; } /******************************************************************************** void showz_docfile(GtkWidget *parent, char *docfile, char *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 1 linkable topics in col. 1 text text text text text text text text text text text text topic text (col. 1 blank) text text text text text text text text text text text text ... 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 text text text text text text text text text text text text \bsubtopic line underline total line text text text text text text text text text text text text text text text text text \_UNDERLINED TEXT\_ text text text underline subtext text text text text text \bBOLD TEXT\b text text text bold subtext text text text text text http......... text text text web link text text text text text text text text text text text text ... *********************************************************************************/ namespace showz_docfile_names { #define TMAX 1000 // max. markups zdialog *zd = 0; GtkWidget *textwidget; int ii, jj; char *Tname[TMAX]; // topic names (link targets) int Tline[TMAX]; // topic lines, count int Uline[TMAX], Upos[TMAX], Ucc[TMAX]; // underlined texts: line, posn, cc int Bline[TMAX], Bpos[TMAX], Bcc[TMAX]; // bold texts: line, posn, cc int Lline[TMAX], Lpos[TMAX], Lcc[TMAX]; // link texts: line, posn, cc char *Lname[TMAX]; // link name int Ltarg[TMAX]; // link target (line number) int TN = 0, UN, BN, LN = 0, UF, BF, LF; // counts, flags int currline; } void showz_docfile(GtkWidget *parent, cchar *docfile, cchar *utopic) { using namespace showz_docfile_names; void showz_docfile_clickfunc(GtkWidget *widget, int line, int pos, int kbkey); void audit_docfile(cchar *docfile); FILE *fid; char filespec[200], buff1[200], buff2[200]; // limits: filename, rec.cc char topic[50], image[100]; // limits: topic name, image name char *pp1, *pp2; int Fm, line, pos1, pos2, cc; GdkPixbuf *pixbuf; GError *gerror; 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; for (ii = 0; ii < TN; ii++) zfree(Tname[ii]); // free prior if any for (ii = 0; ii < LN; ii++) zfree(Lname[ii]); TN = UN = BN = LN = 0; snprintf(filespec,200,"%s/%s",get_zdatadir(),docfile); // open docfile fid = fopen(filespec,"r"); if (! fid) zexit("%s %s \n",filespec,strerror(errno)); zd = popup_report_open(docfile,parent,999,700,0,showz_docfile_clickfunc, // popup window for docfile text display "<",">",Bfind,BOK,0); // buttons if (! zd) zexit("cannot open docfile window \n"); popup_report_font_attributes(zd); // use high contrast font textwidget = zdialog_gtkwidget(zd,"text"); // text widget in zdialog zdialog_show(zd,0); // hide until markups done for (line = 0; ; line++) // loop docfile recs/lines { pp1 = fgets_trim(buff1,200,fid); // line with null at end if (! pp1) break; // EOF if (strmatchN(pp1,"EOF",3)) break; // end of displayed text pp1 = strstr(buff1,"+image:"); // line has image names if (pp1) { while (pp1) { popup_report_write(zd,0," ",0); // leading spaces pp2 = strstr(pp1+7,"+image:"); // next image file if (pp2) *pp2 = 0; strncpy0(image,pp1+7,100); strTrim2(image); 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_insert_pixbuf(zd,line,pixbuf); // write image to output line g_object_unref(pixbuf); } else printz("cannot load image file: %s \n",image); pp1 = pp2; } popup_report_write(zd,0,"\n",0); // write image line EOL continue; // next line } if (buff1[0] > ' ' && buff1[0] != '\\') // line is a topic name { popup_report_write(zd,1,"%s \n",buff1); // write topic line to output, bold strncpy0(topic,buff1,50); // add topic and line number to list strTrim(topic); Tname[TN] = zstrdup(topic); Tline[TN] = line; if (++TN == TMAX) zexit("exceed %d topics \n",TMAX); continue; } strncpy0(buff2,buff1,200); // line is text, not topic Fm = 0; // buff1: line with \* markups for (pp1 = buff2; (pp1 = strchr(pp1,'\\')); ) { // buff2: line without \* markups cc = strlen(pp1+2); memmove(pp1,pp1+2,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 UF = BF = LF = 0; // intiz. no markups active pos1 = pos2 = 0; // char. posn. with/without markups while (buff1[pos1]) // loop chars. in line { if (buff1[pos1] != '\\') { // not a \* markup if (UF) ++Ucc[UN]; if (BF) ++Bcc[BN]; // count cc for active markups if (LF) ++Lcc[LN]; pos1++; pos2++; continue; } if (buff1[pos1+1] == '_') { // \_ markup, underline if (! UF) { UF = 1; // start underline Uline[UN] = line; Upos[UN] = pos2; Ucc[UN] = 0; } else { UF = 0; // end underline UN++; } } if (buff1[pos1+1] == 'b') { // \b markup, bold if (! BF) { BF = 1; // start bold Bline[BN] = line; Bpos[BN] = pos2; Bcc[BN] = 0; } else { BF = 0; // end bold BN++; } } if (buff1[pos1+1] == '>') { // \> markup, link to topic if (! LF) { LF = 1; // start link Lline[LN] = line; Lpos[LN] = pos2; Lcc[LN] = 0; } else { LF = 0; // end link Lname[LN] = (char *) zmalloc(Lcc[LN]+1); memcpy(Lname[LN],buff2+Lpos[LN],Lcc[LN]); // get link name Lname[LN][Lcc[LN]] = 0; LN++; } } pos1 += 2; // skip over \* markup if (UN == TMAX || BN == TMAX || LN == TMAX) // check limits zexit("exceed %d markups \n",TMAX); } // loop character if (UN == TMAX || BN == TMAX || LN == TMAX) // EOL, check limits zexit("exceed %d markups \n",TMAX); if (UF) UN++; // end markups still running if (BF) BN++; } // loop line fclose(fid); for (ii = 0; ii < UN; ii++) // do all text underlines popup_report_underline_word(zd,Uline[ii],Upos[ii],Ucc[ii]); for (ii = 0; ii < BN; ii++) // do all text bolds popup_report_bold_word(zd,Bline[ii],Bpos[ii],Bcc[ii]); for (ii = 0; ii < LN; ii++) { // do all link underlines popup_report_underline_word(zd,Lline[ii],Lpos[ii],Lcc[ii]); for (jj = 0; jj < TN; jj++) // search topic list for link if (strmatchcase(Lname[ii],Tname[jj])) break; if (jj < TN) Ltarg[ii] = Tline[jj]; else printz("link topic not found: %s \n",Lname[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); 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) printz("topic not found: %s %s \n",utopic,topic); } popup_report_scroll_top(zd,currline); // scroll to topic line zdialog_show(zd,1); return; } // handle clicks on document window and KB inputs void showz_docfile_clickfunc(GtkWidget *textwidget, int line, int posn, int kbkey) { using namespace showz_docfile_names; int ii, jj, cc; int top, bott, page, posn8; char *text, *pp1, *pp2; char text2[200], weblink[200]; static int Btab[10], maxB = 10, Bpos = 0; // last 10 links clicked gtk_widget_grab_focus(textwidget); // necessary for some reason textwidget_get_visible_lines(textwidget,top,bott); // range of lines on screen if (kbkey == GDK_KEY_Left || kbkey == '<') { // left arrow, go back Btab[Bpos] = currline; if (Bpos > 0) Bpos--; currline = Btab[Bpos]; textwidget_scroll_top(textwidget,currline); // scroll line to top of window return; } if (kbkey == GDK_KEY_Right || kbkey == '>') { // right arrow, go forward Btab[Bpos] = currline; if (Bpos < maxB-1 && Btab[Bpos+1] >= 0) currline = Btab[++Bpos]; textwidget_scroll_top(textwidget,currline); // scroll line to top of window return; } if (kbkey == GDK_KEY_F || kbkey == GDK_KEY_f) { // key 'F' same as [find] button zdialog_send_event(zd,Bfind); return; } if (kbkey >= 0xfd00) { // navigation key page = bott - top - 2; // page size, lines if (page < 0) page = 0; currline = 0; // default if (kbkey == GDK_KEY_Up) currline = top - 1; // handle some navigation keys else if (kbkey == GDK_KEY_Down) currline = bott + 1; else if (kbkey == GDK_KEY_Page_Up) currline = top - page; else if (kbkey == GDK_KEY_Page_Down) currline = bott + page; else if (kbkey == GDK_KEY_Home) currline = 0; else if (kbkey == GDK_KEY_End) currline = 999999; if (currline < 0) currline = 0; textwidget_scroll(textwidget,currline); // put line on screen return; } if (line < 0 || posn < 0) return; // clicked line and position if (posn > 198) return; text = textwidget_line(textwidget,line,1); if (! text) return; strncpy0(text2,text,posn+1); // compensate utf8 chars. before posn posn8 = posn + strlen(text2) - utf8len(text2); for (ii = 0; ii < LN; ii++) { // search topics for clicked topic if (line == Lline[ii]) { if (posn8 >= Lpos[ii] && posn8 <= Lpos[ii] + Lcc[ii]) { currline = Ltarg[ii]; // linked topic top line textwidget_scroll_top(textwidget,currline); // scroll topic to top of window if (Bpos == maxB-1) { for (jj = 0; jj < maxB-1; jj++) // back tab table full, Btab[jj] = Btab[jj+1]; // discard oldest Bpos--; } Btab[Bpos] = top; // curr. top line >> back tab Bpos++; // advance back tab position Btab[Bpos] = currline; // >> back tab return; } } } 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 if (text[posn] == '\\') posn += 2; // eliminate preceding markup \* 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; strncpy0(weblink,pp1,cc+1); // copy clicked text string if (strmatchN(pp1,"http",4)) showz_html(weblink); // if "http..." assume a web link return; } // validate the F1_help_topic links and the internal links in a docfile void audit_docfile(cchar *docfile) { #define LMAX 10000 // max. docfile lines/recs char *textlines[LMAX]; char *Tname[TMAX]; char filespec[200], buff[200], image[100]; // limits: filename, rec.cc, image name char topic[50]; char *pp1, *pp2, *pp3; FILE *fid; int Ntext, Ntop, Nerrs; int ii, cc, line; GdkPixbuf *pixbuf; GError *gerror; printz("\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) { printz("%s %s",filespec,strerror(errno)); return; } for (line = 0; line < LMAX; line++) // read docfile text lines { pp1 = fgets_trim(buff,200,fid); // line without \n EOL if (! pp1) break; // EOF textlines[Ntext] = zstrdup(pp1); // copy text line to memory if (++Ntext == LMAX) zexit("exceed LMAX text recs"); if (pp1[0] <= ' ') continue; // not a topic line strncpy0(topic,pp1,50); // add topic and line number to list strTrim(topic); Tname[Ntop] = zstrdup(topic); if (++Ntop == TMAX) zexit("exceed TMAX topics"); } fclose(fid); for (line = 0; line < Ntext; line++) // process text lines { pp1 = textlines[line]; pp2 = strstr(pp1,"+image:"); if (pp2) // line contains images { while (pp2) { pp3 = strstr(pp2+7,"+image:"); // next image file if (pp3) *pp3 = 0; strncpy0(image,pp2+7,100); strTrim2(image); 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 { printz("cannot load image file: %s \n",image); Nerrs++; } pp2 = pp3; } continue; // next line } if (pp1[0] <= ' ') // not a topic line { pp1 = strstr(pp1,"\\>"); // get topic links in line while (pp1) { pp2 = strstr(pp1+1,"\\>"); // ... \>topic name\> ... if (! pp2) break; // : : pp1 += 2; // pp1 pp2 cc = pp2 - pp1; if (cc < 2 || cc > 50) { printf("bad topic, line %d: %s \n",line,pp1); // topic name > 50 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("bad topic, line %d: %s \n",line,topic); Nerrs++; } pp1 = strstr(pp2+1,"\\>"); } continue; // next line } } printz(" %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 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) { if (++xskip < skip) return; xskip = 0; } if (! pthread_equal(pthread_self(),zfuncs::tid_main)) return; // thread caller, do nothing while (gtk_events_pending()) // gdk_flush() removed gtk_main_iteration_do(0); // use gtk_main_iteration_do return; } // Iterate the main loop and sleep for designated time void zmainsleep(float secs) { while (secs > 0) { zmainloop(); zsleep(0.01); secs = secs - 0.01; } return; } /********************************************************************************/ // cairo drawing context for GDK window cairo_t * draw_context_create(GdkWindow *gdkwin, draw_context_t &context) { if (context.dcr) zappcrash("draw_context_create(): nested call"); 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 (! context.dcr) zappcrash("draw_context_destroy(): not created"); 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; } /********************************************************************************/ // textwidget functions // -------------------- // // High-level use of GtkTextView widget for text reports, line editing, text selection // In functions below, textwidget = 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 textwidget_clear(GtkWidget *textwidget) { GtkTextBuffer *textBuff; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return; gtk_text_buffer_set_text(textBuff,"",-1); return; } // clear the text widget from given line to end void textwidget_clear(GtkWidget *textwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_linecount(GtkWidget *textwidget) { GtkTextBuffer *textBuff; int nlines; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_append(GtkWidget *textwidget, int bold, cchar *format, ...) { va_list arglist; char textline[2000]; GtkTextBuffer *textBuff; GtkTextIter enditer; GtkTextTag *fontag = 0; cchar *normfont = zfuncs::appmonofont; cchar *boldfont = zfuncs::appmonoboldfont; va_start(arglist,format); vsnprintf(textline,1999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_append2(GtkWidget *textwidget, int bold, cchar *format, ...) { va_list arglist; char textline[2000]; GtkTextBuffer *textBuff; GtkTextIter enditer; GtkTextTag *fontag = 0; cchar *normfont = zfuncs::appmonofont; cchar *boldfont = zfuncs::appmonoboldfont; GtkAdjustment *vadjust; double upperlimit; va_start(arglist,format); vsnprintf(textline,1999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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(textwidget)); upperlimit = gtk_adjustment_get_upper(vadjust); // does not work within a zdialog FIXME 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 textwidget_insert(GtkWidget *textwidget, int bold, int line, cchar *format, ...) { va_list arglist; char textline[2000]; GtkTextBuffer *textBuff; GtkTextIter iter; int nlines; GtkTextTag *fontag = 0; cchar *normfont = zfuncs::appmonofont; cchar *boldfont = zfuncs::appmonoboldfont; va_start(arglist,format); vsnprintf(textline,1999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_replace(GtkWidget *textwidget, int bold, int line, cchar *format, ...) { va_list arglist; char textline[2000]; GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; GtkTextTag *fontag = 0; cchar *normfont = zfuncs::appmonofont; cchar *boldfont = zfuncs::appmonoboldfont; va_start(arglist,format); vsnprintf(textline,1999,format,arglist); va_end(arglist); textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_delete(GtkWidget *textwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget_find(GtkWidget *textwidget, char *matchtext, int line1) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int line, nlines, cc; char *textline = 0, *pp1, *pp2; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 } 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 textwidget_insert_pixbuf(GtkWidget *textwidget, int line, GdkPixbuf *pixbuf) { int nlines; GtkTextBuffer *textBuff; GtkTextIter iter; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget to put a given line on screen // 1st line = 0. for last line use line = -1. void textwidget_scroll(GtkWidget *textwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter; GtkTextMark *mark; GtkAdjustment *vadjust; double upperlimit; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return; vadjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(textwidget)); 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(textwidget),mark); } return; } // scroll a textwidget to put a given line at the top of the window void textwidget_scroll_top(GtkWidget *textwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter; GtkTextMark *mark; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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(textwidget),mark,0,1,0,0); return; } // get the range of textwidget lines currently visible in the window void textwidget_get_visible_lines(GtkWidget *textwidget, int &top, int &bott) { GdkRectangle rect; GtkTextIter iter1, iter2; int y1, y2; gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(textwidget),&rect); y1 = rect.y; y2 = y1 + rect.height; gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(textwidget), &iter1, y1, 0); gtk_text_view_get_line_at_y(GTK_TEXT_VIEW(textwidget), &iter2, y2, 0); top = gtk_text_iter_get_line(&iter1); bott = gtk_text_iter_get_line(&iter2) - 1; return; } // dump the entire textwidget contents into a file void textwidget_dump(GtkWidget *widget, cchar *filename) { FILE *fid; char *prec; int line, err; fid = fopen(filename,"w"); // open file if (! fid) { zmessageACK(mainwin,"cannot open file %s",filename); return; } for (line = 0; ; line++) { prec = textwidget_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 textwidget contents into a file, using a save-as dialog void textwidget_save(GtkWidget *widget, GtkWindow *parent) { char *file; file = zgetfile("save text to file",parent,"save","noname"); if (! file) return; textwidget_dump(widget,file); zfree(file); return; } // Get a line of text. Returned text is subject for zfree(). // trailing \n is included if strip == 0 char * textwidget_line(GtkWidget *textwidget, int line, int strip) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int cc, nlines; char *textline, *ztext; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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); 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 textwidget_highlight_line(GtkWidget *textwidget, int line) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; int nlines; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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) char * textwidget_word(GtkWidget *textwidget, int line, int posn, cchar *dlims, char &end) { GtkTextBuffer *textBuff; char *txline, *pp1, *pp2, *ztext; int pos, cc; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return 0; txline = textwidget_line(textwidget,line,0); if (! txline) return 0; pos = utf8_position(txline,posn); // graphic position to byte position if (pos < 0) return 0; pp1 = txline + pos; if (! *pp1 || strchr(dlims,*pp1)) return 0; // reject edge position or delimiter while (pp1 > txline && ! strchr(dlims,pp1[-1])) pp1--; // find start of word pp2 = pp1; while (pp2[1] && ! strchr(dlims,pp2[1])) pp2++; // find following delimiter or EOL end = pp2[1]; // return delimiter while (*pp1 == ' ') pp1++; // no leading or trailing blanks while (*pp2 == ' ') pp2--; cc = pp2 - pp1 + 1; if (cc < 1) return 0; // all blanks? ztext = (char *) zmalloc(cc+1); strncpy0(ztext,pp1,cc+1); zfree(txline); return ztext; } // highlight text at line and positiion, length cc void textwidget_highlight_word(GtkWidget *textwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; char *txline, *pp1, *pp2; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return; txline = textwidget_line(textwidget,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 textwidget_bold_word(GtkWidget *textwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; cchar *boldfont = zfuncs::appmonoboldfont; char *txline, *pp1, *pp2; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return; txline = textwidget_line(textwidget,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 textwidget_underline_word(GtkWidget *textwidget, int line, int posn, int cc) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; char *txline, *pp1, *pp2; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); if (! textBuff) return; txline = textwidget_line(textwidget,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 textwidget (black on white) // this does not do anything to the text font - why? FIXME void textwidget_font_attributes(GtkWidget *textwidget) { GtkTextBuffer *textBuff; GtkTextIter iter1, iter2; GtkTextTag *fontag = 0; textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textwidget)); 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 textwidget // + 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 *textwidget, int line, int position, int KBkey) // this function receives KB keys and mouse click text line/position void textwidget_set_eventfunc(GtkWidget *textwidget, textwidget_callbackfunc_t userfunc) { int textwidget_eventfunc(GtkWidget *textwidget, GdkEvent *event, textwidget_callbackfunc_t userfunc); gtk_widget_add_events(textwidget,GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(textwidget,GDK_KEY_PRESS_MASK); gtk_widget_add_events(textwidget,GDK_POINTER_MOTION_MASK); gtk_widget_add_events(textwidget,GDK_FOCUS_CHANGE_MASK); G_SIGNAL(textwidget,"key-press-event",textwidget_eventfunc,userfunc); G_SIGNAL(textwidget,"button-press-event",textwidget_eventfunc,userfunc); G_SIGNAL(textwidget,"motion-notify-event",textwidget_eventfunc,userfunc); G_SIGNAL(textwidget,"focus-in-event",textwidget_eventfunc,userfunc); return; } // textwidget 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 textwidget_eventfunc(GtkWidget *textwidget, GdkEvent *event, textwidget_callbackfunc_t userfunc) { #define TEXT GTK_TEXT_WINDOW_TEXT #define VIEW GTK_TEXT_VIEW static GdkCursor *arrowcursor = 0; GdkWindow *gdkwin; GtkTextIter iter1; int button, mpx, mpy, tbx, tby; int line, pos, top, bott, page, KBkey; 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(textwidget),TEXT); // set arrow cursor for window if (gdkwin) gdk_window_set_cursor(gdkwin,arrowcursor); // (must reset every event) gtk_widget_grab_focus(textwidget); if (event->type == GDK_KEY_PRESS) // KB key press event { KBkey = ((GdkEventKey *) event)->keyval; if (userfunc) { userfunc(textwidget,-1,-1,KBkey); return 1; } if (KBkey >= 0xfd00) // navigation key { textwidget_get_visible_lines(textwidget,top,bott); // range of lines on screen page = bott - top - 2; // page size, lines if (page < 0) page = 0; line = 0; // default if (KBkey == GDK_KEY_Up) line = top - 1; // handle some navigation keys else if (KBkey == GDK_KEY_Down) line = bott + 1; else if (KBkey == GDK_KEY_Page_Up) line = top - page; else if (KBkey == GDK_KEY_Page_Down) line = bott + page; else if (KBkey == GDK_KEY_Home) line = 0; else if (KBkey == GDK_KEY_End) line = 999999; if (line < 0) line = 0; textwidget_scroll(textwidget,line); // put line on screen } return 1; } if (! userfunc) return 0; 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(textwidget),TEXT,mpx,mpy,&tbx,&tby); if (tbx && tby) { // can happen gtk_text_view_get_iter_at_location(VIEW(textwidget),&iter1,tbx,tby); line = gtk_text_iter_get_line(&iter1); // clicked textwidget line pos = gtk_text_iter_get_line_offset(&iter1); // clicked position } else line = pos = 0; userfunc(textwidget,line,pos,-1); // pass line and posn to user func return 0; // retain default processing } 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 cchar * menu or toolbar name or label tbar GtkWidget * reference for toolbar tip cchar * tool button tool tip (popup text via mouse-over) icon cchar * stock icon name or custom icon file name (see below) func see below menu or tool button response function arg cchar * argument to response function stbar int reference for status bar message cchar * 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 *, cchar *) 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, cchar *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, cchar *mlab, cbFunc func, cchar *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, cchar *blab, cchar *btip, cchar *icon, cbFunc func) { GtkToolItem *tbutton; GError *gerror = 0; PIXBUF *pixbuf; GtkWidget *wicon = 0; char 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, cchar *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; } /******************************************************************************** Customizable Graphic Popup Menu void Gmenuz(GtkWidget *parent, cchar *configfile, callbackfunc) Open a popup window with a customizable graphic menu. parent parent window or null configfile menu configuration file, will be created if missing callbackfunc callback function to receive clicked menu entry: typedef void callbackfunc(cchar *menu) This function allows an application to offer a customizable menu which a user can populate with frequently used (menu) functions, arranged as desired. A menu entry selected by the user is passed to the application for execution. The initial popup window is blank. Right click an empty space on the popup window to define a new menu entry. Right click an existing entry to modify it. Use the resulting dialog to define or change the menu entry. menu text optional text appearing in the popup window menu func text that is passed to the application callback function menu icon optional menu icon: an optional .png file can be selected icon size rendered icon size in the popup window, 24x24 to 64x64 pixels close checkbox: option to close the popup window when menu is used Left drag a menu entry to move it somewhere else on the popup window. The popup window can be resized to fit the contained menu entries. Left click a menu entry to select the menu. The callback function will be called to execute the menu function. If "close" was checked, the popup window will close. All menu settings are saved in the supplied configuration file whenever the popup window is closed, if any changes were made since it was opened. Icon files are copied into the same folder as the configuration file and these copies are used. The icon selected when the menu entry is created can disappear without consequence. The file name NNN.png is used (000-999). If the [x] window kill button is pressed, the window is closed and the calling program is informed by passing "quit" to the callback function. layout of menu configuration file: popup xpos ypos width height popup window position and size posn xpos ypos ww hh menu position in popup window menu menu text menu text on popup window func funcname argument for user callback function icon NNN.png optional menu icon file size NN optional icon size (default 24) kill optional flag, kill window when used menu menu text next menu entry ... *********************************************************************************/ namespace gmenuznames { #define maxME 200 // max. menu entries #define maxText 1000 // max. text size, all menu fields typedef void callbackfunc(cchar *menu); // user callback function callbackfunc *gmenucallback; cchar *menuFont, *menuBoldFont; char *menufile = 0; // menu configuration file from user char *menudir = 0; char iconfile[200]; // icon file full path < 200 GtkWidget *mPop, *layout; // popup and drawing windows GtkWidget *pWin; // parent window int winposx=100, winposy=100, winww=400, winhh=300; // initial popup WRT parent window int mdcc; struct menuent { // menu entry on popup window int xpos, ypos, ww, hh; // layout position, extent char *menu; // text on window or null int bold; // text is bold char *func; // func name for user callback char *icon; // icon file "NNN.png" or null PIXBUF *pixbuf; // icon pixbuf or null int size; // icon size or zero int kill; // kill popup window when menu used int Fnewicon; // flag, icon was changed }; menuent menus[maxME]; // menu entries int NME = 0; // entry count zdialog *zdedit = 0; // active edit dialog int mpx, mpy; // mouse click/drag position int me; // current menu entry int Fchanged = 0; // flag, menu edited or resized int Fpopquit = 0; // popup is being closed int Fpopbusy = 0; // popup is active int deficonsize = 32; // default icon size or last size set void wpaint(GtkWidget *, cairo_t *); // window repaint - draw event void resize(); // window resize event void quit(); // kill window and exit void update_configfile(); // update menu configuration file void mouse_event(GtkWidget *, GdkEventButton *, void *); // mouse event function void KB_event(GtkWidget *, GdkEventKey *, void *); // KB event function void draw_text(cairo_t *, char *, int px, int py, int &ww, int &hh); // draw text and return pixel extent void draw_bold_text(cairo_t *, char *, int px, int py, int &ww, int &hh); // draw text and return pixel extent void edit_menu(); // dialog to create/edit menu entry int edit_menu_event(zdialog *zd, cchar *event); // dialog event function void drag_drop_event(int mx, int my, char *file); // drag-drop event function } // user callable function to build the menu from user's configuration file void Gmenuz(GtkWidget *parent, cchar *title, cchar *ufile, gmenuznames::callbackfunc ufunc) { using namespace gmenuznames; FILE *fid; int nn, xx, yy, ww, hh, size; int pposx, pposy; int xpos, ypos; char *pp, buff[maxText]; PIXBUF *pixbuf; GError *gerror; if (Fpopbusy) return; // don't allow multiple popups menuFont = zfuncs::appfont; menuBoldFont = zfuncs::appboldfont; pWin = parent; // get parent window if (menufile) { zfree(menufile); zfree(menudir); } menufile = zstrdup(ufile); // get menu configuration file menudir = zstrdup(ufile); // folder of same pp = strrchr(menudir,'/'); if (! pp || pp < menudir + 10) zappcrash("Gmenuz() bad menu: %s",menufile); *(pp+1) = 0; mdcc = strlen(menudir); if (mdcc > 190) zappcrash("Gmenuz() too big: %s",menufile); gmenucallback = ufunc; // get user callback function NME = 0; fid = fopen(menufile,"r"); // read window geometry if (fid) { nn = fscanf(fid," popup %d %d %d %d",&xx,&yy,&ww,&hh); // get popup window position and size if (nn == 4 && ww > 50 && ww < 1000 && hh > 50 && hh < 1000) { winposx = xx; // OK to use winposy = yy; winww = ww; winhh = hh; } while (true) { pp = fgets_trim(buff,maxText-1,fid,1); // read next menu entry if (! pp) break; if (strmatchN(pp,"posn ",5)) { // position in popup window if (NME == maxME) { zmessageACK(mPop,"exceeded %d menu entries",maxME); break; } me = NME; // new entry NME++; // entry count memset(&menus[me],0,sizeof(menuent)); // clear all menu data nn = sscanf(pp+5," %d %d ",&xpos,&ypos); // position in popup window if (nn != 2) xpos = ypos = 100; if (xpos > 1000) xpos = 1000; if (ypos > 1000) ypos = 1000; menus[me].xpos = xpos; menus[me].ypos = ypos; } if (strmatchN(pp,"menu ",5)) { // menu text if (strlen(pp+5) > 0) menus[me].menu = zstrdup(pp+5); // get menu text else menus[me].menu = 0; } if (strmatchN(pp,"bold",4)) // bold text menus[me].bold = 1; if (strmatchN(pp,"func ",5)) { // function name if (strlen(pp+5)) menus[me].func = zstrdup(pp+5); else menus[me].func = 0; } if (strmatchN(pp,"icon ",5)) { // menu icon file NNN.png pp += 5; if (*pp) { // icon file present if (*pp == '/') { // old style icon file /.../filename.png menus[me].Fnewicon = 1; // new icon is needed Fchanged = 1; gerror = 0; pixbuf = gdk_pixbuf_new_from_file(pp,&gerror); if (! pixbuf && gerror) printz("*** %s \n",gerror->message); menus[me].pixbuf = pixbuf; if (strmatchN(menudir,pp,mdcc)) remove(pp); // delete file if in /menudir/ } else { // new style icon file NNN.png menus[me].icon = zstrdup(pp); // (relocatable if menudir moved) strncpy0(iconfile,menudir,200); strcpy(iconfile+mdcc,pp); // /menudir/NNN.png gerror = 0; pixbuf = gdk_pixbuf_new_from_file(iconfile,&gerror); if (! pixbuf && gerror) printz("*** %s \n",gerror->message); menus[me].pixbuf = pixbuf; } } else menus[me].pixbuf = 0; // no icon file } if (strmatchN(pp,"size ",5)) { size = atoi(pp+5); if (size < 24) size = 24; if (size > 256) size = 256; menus[me].size = size; } if (strmatchN(pp,"kill",4)) // kill window flag menus[me].kill = 1; } fclose(fid); } if (Fchanged) update_configfile(); mPop = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create popup window for menu entries if (! pWin) { // no parent window pposx = pposy = 0; gtk_window_set_focus_on_map(GTK_WINDOW(mPop),1); // put on top when created } else { gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (NW corner) gtk_window_set_transient_for(GTK_WINDOW(mPop),GTK_WINDOW(pWin)); // popup window belongs to parent } if (title) gtk_window_set_title(GTK_WINDOW(mPop),title); winposx += pposx; // popup position relative to parent winposy += pposy; gtk_window_set_default_size(GTK_WINDOW(mPop),winww,winhh); // set size and position gtk_window_move(GTK_WINDOW(mPop),winposx,winposy); layout = gtk_layout_new(0,0); // create drawing window gtk_container_add(GTK_CONTAINER(mPop),layout); // add to popup window G_SIGNAL(mPop,"destroy",quit,0); // connect signals to windows G_SIGNAL(mPop,"delete-event",quit,0); G_SIGNAL(mPop,"notify",resize,0); G_SIGNAL(mPop,"configure-event",resize,0); // GTK 3.18 API changed G_SIGNAL(mPop,"key-press-event",KB_event,0); // connect KB key event G_SIGNAL(layout,"draw",wpaint,0); gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK); // connect mouse events gtk_widget_add_events(layout,GDK_BUTTON_RELEASE_MASK); gtk_widget_add_events(layout,GDK_BUTTON_MOTION_MASK); gtk_widget_add_events(layout,GDK_POINTER_MOTION_MASK); G_SIGNAL(layout,"button-press-event",mouse_event,0); G_SIGNAL(layout,"button-release-event",mouse_event,0); G_SIGNAL(layout,"motion-notify-event",mouse_event,0); drag_drop_dest(mPop,drag_drop_event); gtk_widget_show_all(mPop); // show all widgets zmainloop(); // after initial window events Fchanged = 0; // no changes yet Fpopquit = 0; // not being closed Fpopbusy = 1; return; } // paint window when created, exposed, resized void gmenuznames::wpaint(GtkWidget *, cairo_t *cr) { using namespace gmenuznames; PIXBUF *pixbuf; char *text, *text2; int xpos, ypos, ww, hh, size, yadd; int bold; for (int me = 0; me < NME; me++) // loop all menu entries { xpos = menus[me].xpos; // window position ypos = menus[me].ypos; text = menus[me].menu; // menu text bold = menus[me].bold; // bold text flag pixbuf = menus[me].pixbuf; // icon size = menus[me].size; // size if (pixbuf) { // draw icon at window position gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); cairo_paint(cr); if (! size) size = deficonsize; // use default if not specified } else size = 0; yadd = 0; if (pixbuf) yadd = gdk_pixbuf_get_height(pixbuf) + 2; // icon height + extra space if (text) { text2 = (char *) zmalloc(strlen(text)+100); // replace "\n" with newline repl_1str(text,text2,"\\n","\n"); if (bold) draw_bold_text(cr,text2,xpos,ypos+yadd,ww,hh); // bold text else draw_text(cr,text2,xpos,ypos+yadd,ww,hh); zfree(text2); } else ww = hh = 0; if (ww < size) ww = size; // menu entry enclosing rectangle hh += yadd; menus[me].ww = ww; menus[me].hh = hh; } return; } // resize event - save current window size void gmenuznames::resize() { using namespace gmenuznames; int xx, yy, ww, hh; if (Fpopquit) return; // ignore bogus call when killed gtk_window_get_position(GTK_WINDOW(mPop),&xx,&yy); gtk_window_get_size(GTK_WINDOW(mPop),&ww,&hh); if (xx != winposx || yy != winposy) Fchanged = 1; // window size or position changed if (ww != winww || hh != winhh) Fchanged = 1; winposx = xx; // save window position winposy = yy; winww = ww; // save new size winhh = hh; return; } // [x] kill window // Save current menu status for next session. void gmenuznames::quit() { using namespace gmenuznames; Fpopquit = 1; Fpopbusy = 0; if (Fchanged) update_configfile(); gtk_widget_destroy(mPop); gmenucallback("quit"); // inform host program return; } // menu changed, save all menu data to menu config. file void gmenuznames::update_configfile() { using namespace gmenuznames; static int ftf = 1; int64 DT; int err, ii, me; char *pp; int pposx, pposy; FILE *fid; STATB statb; GError *gerror; if (ftf) { // initialize random generator ftf = 0; DT = time(null); drandz(&DT); } if (pWin) gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (may have moved) else pposx = pposy = 0; winposx -= pposx; // popup position relative to parent winposy -= pposy; fid = fopen(menufile,"w"); // open for write if (! fid) { zmessageACK(mPop," %s \n %s",menufile,strerror(errno)); // diagnose permissions error return; } fprintf(fid,"popup %d %d %d %d \n",winposx,winposy,winww,winhh); for (me = 0; me < NME; me++) // write all menu entries to file { if (! menus[me].menu && ! menus[me].pixbuf) { // no text and no icon printz("*** Gmenuz: empty menu entry \n"); continue; } fprintf(fid,"\n"); // blank line separator fprintf(fid,"posn %d %d \n",menus[me].xpos, menus[me].ypos); // menu position in window if (menus[me].menu) // menu text fprintf(fid,"menu %s \n",menus[me].menu); if (menus[me].bold) // bold text flag fprintf(fid,"bold \n"); if (menus[me].func) // menu function (text) fprintf(fid,"func %s \n",menus[me].func); if (menus[me].pixbuf) { // menu icon if (menus[me].Fnewicon) { // new or changed icon strncpy0(iconfile,menudir,200); // menu file folder pp = iconfile + mdcc; // pp --> NNN.png for (ii = 0; ii < 999; ii++) { sprintf(pp,"%03d.png",ii); // find first available NNN.png file err = stat(iconfile,&statb); if (err) break; // (if full, 999.png is re-used) } gerror = 0; gdk_pixbuf_save(menus[me].pixbuf,iconfile,"png",&gerror,null); // write pixbuf to icon file if (gerror) printz("*** %s %s \n",pp,gerror->message); else fprintf(fid,"icon %s \n",pp); // file name is NNN.png menus[me].icon = zstrdup(pp); menus[me].Fnewicon = 0; } else if (menus[me].icon) // no change fprintf(fid,"icon %s \n",menus[me].icon); } if (menus[me].size) // icon size fprintf(fid,"size %d \n",menus[me].size); if (menus[me].kill) fprintf(fid,"kill \n"); // kill window flag } fclose(fid); Fchanged = 0; return; } // mouse event function - capture buttons and drag movements void gmenuznames::mouse_event(GtkWidget *, GdkEventButton *event, void *) { using namespace gmenuznames; static int bdtime = 0, butime = 0; static int Lmouse = 0, Rmouse = 0, Fdrag = 0; static int elapsed, mpx0 = 0, mpy0 = 0; int Fclick, dx, dy, xpos, ypos, size; int raster = 10; // alignment raster mpx = int(event->x); // mouse position in window mpy = int(event->y); if (event->type == GDK_BUTTON_PRESS) { Lmouse = Rmouse = Fdrag = 0; if (event->button == 1) Lmouse++; // left or right mouse button if (event->button == 3) Rmouse++; bdtime = event->time; for (me = 0; me < NME; me++) // look for clicked menu entry { if (mpx < menus[me].xpos) continue; if (mpy < menus[me].ypos) continue; if (mpx > menus[me].xpos + menus[me].ww) continue; if (mpy > menus[me].ypos + menus[me].hh) continue; break; } if (me < NME) { // menu item clicked (selected) mpx0 = mpx; // set new drag origin mpy0 = mpy; } else me = -1; // indicate empty space clicked } if (event->type == GDK_BUTTON_RELEASE) { Fclick = 0; butime = event->time; elapsed = butime - bdtime; // button down time, milliseconds bdtime = 0; if (elapsed < 500 && ! Fdrag) Fclick = 1; // mouse clicked if (me >= 0 && Fclick && Lmouse) { // menu entry was left-clicked if (menus[me].kill) quit(); // close menu if specified if (menus[me].func) gmenucallback(menus[me].func); // caller user function(func) } else if (Fclick && Rmouse) // menu entry or empty space right-clicked edit_menu(); // edit menu else if (me >= 0 && Fdrag) // menu entry drag ended Fchanged = 1; // mark menu revised Lmouse = Rmouse = Fdrag = 0; // mouse click action completed } if (event->type == GDK_MOTION_NOTIFY) // mouse movement { if (me >= 0 && Lmouse && bdtime) { // menu drag underway dx = mpx - mpx0; dy = mpy - mpy0; if (abs(dx) + abs(dy) > 4) // ignore small drags { Fdrag++; mpx0 = mpx; // set new drag origin mpy0 = mpy; menus[me].xpos = mpx; // center menu on mouse menus[me].ypos = mpy; size = menus[me].size; if (size) { menus[me].xpos -= size / 2; menus[me].ypos -= size / 2; } else { menus[me].xpos -= 15; menus[me].ypos -= 8; } xpos = menus[me].xpos; // align to raster ypos = menus[me].ypos; xpos = raster * (xpos / raster); ypos = raster * (ypos / raster); menus[me].xpos = xpos; menus[me].ypos = ypos; if (menus[me].xpos < 0) menus[me].xpos = 0; // not off the window if (menus[me].xpos > winww-20) menus[me].xpos = winww-20; if (menus[me].ypos < 0) menus[me].ypos = 0; if (menus[me].ypos > winhh-20) menus[me].ypos = winhh-20; gtk_widget_queue_draw(layout); // repaint window } } } return; } // KB press event function - send certain keys to main app void gmenuznames::KB_event(GtkWidget *, GdkEventKey *kbevent, void *) { using namespace gmenuznames; int KBkey = kbevent->keyval; if (KBkey == GDK_KEY_F1) KBevent(kbevent); if (KBkey == GDK_KEY_Escape) quit(); // escape = cancel return; } // draw text into layout and return pixel dimensions of enclosing rectangle void gmenuznames::draw_text(cairo_t *cr, char *text, int x, int y, int &w, int &h) { using namespace gmenuznames; static PangoFontDescription *pfont = 0; static PangoLayout *playout = 0; if (! pfont) { pfont = pango_font_description_from_string(menuFont); // first call, get font sizing poop playout = gtk_widget_create_pango_layout(layout,0); pango_layout_set_font_description(playout,pfont); } pango_layout_set_text(playout,text,-1); // compute layout pango_layout_get_pixel_size(playout,&w,&h); // pixel width and height of layout cairo_move_to(cr,x,y); // draw layout with text cairo_set_source_rgb(cr,0,0,0); pango_cairo_show_layout(cr,playout); return; } // draw bold text into layout and return pixel dimensions of enclosing rectangle void gmenuznames::draw_bold_text(cairo_t *cr, char *text, int x, int y, int &w, int &h) { using namespace gmenuznames; static PangoFontDescription *pfont = 0; static PangoLayout *playout = 0; if (! pfont) { pfont = pango_font_description_from_string(menuBoldFont); // first call, get font sizing poop playout = gtk_widget_create_pango_layout(layout,0); pango_layout_set_font_description(playout,pfont); } pango_layout_set_text(playout,text,-1); // compute layout pango_layout_get_pixel_size(playout,&w,&h); // pixel width and height of layout cairo_move_to(cr,x,y); // draw layout with text cairo_set_source_rgb(cr,0,0,0); pango_cairo_show_layout(cr,playout); return; } // dialog to create a new menu entry from user inputs void gmenuznames::edit_menu() { using namespace gmenuznames; if (me < 0) { // new menu entry if (NME == maxME) { zmessageACK(mPop,"capacity limit exceeded"); return; } me = NME; memset(&menus[me],0,sizeof(menuent)); // clear all data } /** menu text [________________] [x] Bold menu func [________________________] menu icon [_______________] [Browse] icon size [___] [x] close window [Apply] [Delete] [Cancel] **/ if (! zdedit) // create dialog if not already { zdedit = zdialog_new("edit menu entry",mPop,Bapply,Bdelete,Bcancel,null); zdialog_add_widget(zdedit,"hbox","hb1","dialog"); zdialog_add_widget(zdedit,"vbox","vb1","hb1",0,"homog|space=3"); zdialog_add_widget(zdedit,"vbox","vb2","hb1",0,"homog|expand"); zdialog_add_widget(zdedit,"label","lab11","vb1","menu text"); zdialog_add_widget(zdedit,"label","lab12","vb1","menu func"); zdialog_add_widget(zdedit,"label","lab13","vb1","menu icon"); zdialog_add_widget(zdedit,"label","lab14","vb1","icon size"); zdialog_add_widget(zdedit,"hbox","hb2","vb2"); zdialog_add_widget(zdedit,"zentry","text","hb2",0,"size=30|space=2"); zdialog_add_widget(zdedit,"check","bold","hb2","Bold","space=5"); zdialog_add_widget(zdedit,"zentry","func","vb2",0,"size=30|space=2"); zdialog_add_widget(zdedit,"hbox","hb3","vb2",0,"expand|space=2"); zdialog_add_widget(zdedit,"zentry","icon","hb3",0,"expand"); zdialog_add_widget(zdedit,"zbutton","browse","hb3","Browse","space=5"); zdialog_add_widget(zdedit,"hbox","hb4","vb2",0,"space=2"); zdialog_add_widget(zdedit,"zspin","size","hb4","24|256|1|32"); zdialog_add_widget(zdedit,"check","kill","hb4","close window","space=30"); zdialog_run(zdedit,edit_menu_event,"mouse"); } if (menus[me].menu) // stuff menu text into dialog zdialog_stuff(zdedit,"text",menus[me].menu); else zdialog_stuff(zdedit,"text",""); if (menus[me].func) // stuff menu function zdialog_stuff(zdedit,"func",menus[me].func); else zdialog_stuff(zdedit,"func",""); zdialog_stuff(zdedit,"bold",menus[me].bold); // stuff bold text flag if (menus[me].icon) // stuff icon file zdialog_stuff(zdedit,"icon",menus[me].icon); else zdialog_stuff(zdedit,"icon",""); if (menus[me].size) // stuff icon size zdialog_stuff(zdedit,"size",menus[me].size); if (menus[me].kill) // stuff window kill flag zdialog_stuff(zdedit,"kill",1); else zdialog_stuff(zdedit,"kill",0); if (me == NME) { // new menu entry menus[me].xpos = mpx; // initial position from mouse menus[me].ypos = mpy; } return; } // menu entry dialog event function int gmenuznames::edit_menu_event(zdialog *zd, cchar *event) { using namespace gmenuznames; char text[maxText], *pp; int size; PIXBUF *pixbuf; GError *gerror; GtkWindow *parent = GTK_WINDOW(zd->dialog); if (strmatch(event,"browse")) { // browse for icon file pp = zgetfile("select icon",parent,"file",menudir); if (pp) { zdialog_stuff(zd,"icon",pp); zfree(pp); } } if (! zd->zstat) return 1; // wait for dialog completion if (zd->zstat == 2) { // [delete] - delete menu entry if (me < 0 || me >= NME) return 0; for (int me2 = me; me2 < NME-1; me2++) // remove menu entry and close hole menus[me2] = menus[me2+1]; NME--; Fchanged = 1; // mark menu revised gtk_widget_queue_draw(layout); // repaint window } if (zd->zstat != 1) { // not [apply] - kill dialog zdialog_free(zdedit); return 1; } // [apply] - update menu from dialog data zdialog_fetch(zd,"text",text,maxText); if (*text) menus[me].menu = zstrdup(text); // menu text, optional else menus[me].menu = 0; zdialog_fetch(zd,"bold",menus[me].bold); // bold text flag zdialog_fetch(zd,"func",text,maxText); // menu function name strTrim2(text); if (*text) menus[me].func = zstrdup(text); else menus[me].func = 0; zdialog_fetch(zd,"icon",text,maxText); // menu icon file, optional strTrim2(text); if (*text) { if (strlen(text) < 8) { // have local "NNN.png" strncpy0(iconfile,menudir,200); strcpy(iconfile+mdcc,text); // make /menudir/NNN.png } else strncpy0(iconfile,text,200); // new icon file selected zdialog_fetch(zd,"size",size); // icon size gerror = 0; pixbuf = gdk_pixbuf_new_from_file_at_size(iconfile,size,size,&gerror); if (! pixbuf) { if (gerror) zmessageACK(mPop,gerror->message); // bad icon file zd->zstat = 0; // keep dialog open return 0; // do nothing } menus[me].icon = zstrdup(text); menus[me].pixbuf = pixbuf; menus[me].size = size; deficonsize = size; // set new default menus[me].Fnewicon = 1; // mark icon changed } else { // no icon menus[me].icon = 0; menus[me].pixbuf = 0; menus[me].size = 0; menus[me].Fnewicon = 0; } zdialog_fetch(zd,"kill",menus[me].kill); // popup window kill flag if (me == NME) NME++; // if new menu entry, incr. count Fchanged = 1; // mark menu revised zdialog_free(zdedit); // destroy dialog gtk_widget_queue_draw(layout); // repaint window return 1; } // function to accept drag-drop of a .desktop file or file name. void gmenuznames::drag_drop_event(int mpx, int mpy, char *file) { using namespace gmenuznames; FILE *fid; char buff[200], filetype[60], dtfile[200]; char *pp, name[100], exec[200], icon[200]; char wildiconfile[200]; GError *gerror = 0; PIXBUF *pixbuf = 0; int ii, jj, uflag; int size = deficonsize; // default or last size set by user #define Ndirs 8 cchar *icondir[Ndirs] = { // compensate Linux chaos "/usr/share/app-install/icons*", "/usr/share/pixmaps*", "/usr/share/icons/*64*", "/usr/share/icons/*48*", "/usr/share/icons/*32*", "/usr?share/icons/*scalable*", "/usr/local/share/icons*", "/usr/local/share/pixmaps*" }; if (! file) return; // drag motion event #define Next 3 cchar *iconext[Next] = { "png", "svg", "xpm" }; // file extensions searched if (NME == maxME) { zmessageACK(mPop,"capacity limit exceeded"); return; } me = NME; memset(&menus[me],0,sizeof(menuent)); // clear all data pp = strrchr(file,'.'); // look if .desktop file if (pp && strmatch(pp,".desktop")) { strncpy0(dtfile,file,200); // have a .desktop file file = 0; // and no data file } else // find .desktop file for given data file { snprintf(buff,200,"xdg-mime query filetype \"%s\"",file); // xdg-mime query filetype fid = popen(buff,"r"); if (! fid) { zmessageACK(mPop,strerror(errno)); return; } pp = fgets_trim(buff,200,fid); // should get major/minor file type pclose(fid); if (! pp) { zmessageACK(mPop,strerror(errno)); return; } strncpy0(filetype,pp,60); snprintf(buff,200,"xdg-mime query default %s",filetype); // zdg-mime query default fid = popen(buff,"r"); if (! fid) { zmessageACK(mPop,strerror(errno)); return; } pp = fgets_trim(buff,200,fid); // should get appname.desktop pclose(fid); if (! pp) { zmessageACK(mPop,strerror(errno)); return; } snprintf(dtfile,200,"/usr/share/applications/%s",pp); // /usr/share/applications/appname.desktop pp = strrchr(dtfile,'.'); if (! strmatch(pp,".desktop")) { zmessageACK(mPop,".desktop file not found"); return; } } fid = fopen(dtfile,"r"); // read .desktop file if (! fid) { zmessageACK(mPop,strerror(errno)); return; } *name = *exec = *icon = 0; while (true) { // save .desktop parameters pp = fgets_trim(buff,200,fid); if (! pp) break; if (strmatchN(buff,"Name=",5) && ! *name) // app name strncpy0(name,buff+5,100); if (strmatchN(buff,"Exec=",5) && ! *exec) // executable strncpy0(exec,buff+5,200); if (strmatchN(buff,"Icon=",5) && ! *icon) // usr/share/app-install/icons/*.png strncpy0(icon,buff+5,200); } fclose(fid); if (file) { pp = strrchr(file,'/'); // menu name = file base name if (! pp) pp = file; else pp++; menus[me].menu = zstrdup(pp); pp = strrchr(menus[me].menu,'.'); if (pp) *pp = 0; pp = strrchr(exec,'%'); // get rid of %f %u etc. if (pp) *pp = 0; snprintf(buff,200,"%s \"%s\"",exec,file); // menu function = executable + filename menus[me].func = zstrdup(buff); } else { menus[me].menu = zstrdup(name); // menu name = appname menus[me].func = zstrdup(exec); // menu function = executable } if (file && strmatchN(filetype,"image",5)) { // if image file, make icon from image pixbuf = gdk_pixbuf_new_from_file_at_size(file,size,size,&gerror); if (pixbuf) { menus[me].icon = zstrdup(file); menus[me].pixbuf = pixbuf; menus[me].size = size; menus[me].Fnewicon = 1; } } else if (*icon) // make icon from .desktop file { if (*icon == '/') { // icon filespec is given pp = icon; goto found; } for (ii = 0; ii < Ndirs; ii++) // search icon locations for (jj = 0; jj < Next; jj++) // try known icon file types { snprintf(wildiconfile,200,"%s/%s.%s",icondir[ii],icon,iconext[jj]); uflag = 1; pp = (char *) SearchWild(wildiconfile,uflag); if (pp) { uflag = 2; SearchWild(null,uflag); goto found; // break out 2 loops } } found: if (! pp) zmessageACK(mPop,"icon file not found: %s \n",icon); else { pixbuf = gdk_pixbuf_new_from_file_at_size(pp,size,size,&gerror); if (! pixbuf) zmessageACK(mPop,"icon file error: %s \n",pp); else { menus[me].icon = zstrdup(pp); menus[me].pixbuf = pixbuf; menus[me].size = size; menus[me].Fnewicon = 1; } } } menus[me].xpos = mpx; // position from mouse menus[me].ypos = mpy; menus[me].kill = 1; // popup window kill flag NME++; // incr. menu count Fchanged = 1; // mark menu revised gtk_widget_queue_draw(layout); // repaint window edit_menu(); // user can edit dropped menu return; } /******************************************************************************** Popup Menu GtkWidget *popup, *mitem cchar *label, *arg, *tip void func(GtkWidget *, cchar *arg) popup = create_popmenu() create a popup menu mitem = add_popmenu_item(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_item' 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 *, cchar *menu) ***/ // create a popup menu GtkWidget * create_popmenu() { int popmenu_event(GtkWidget *, GdkEvent *); GtkWidget *popmenu; popmenu = gtk_menu_new(); gtk_widget_add_events(popmenu,GDK_BUTTON_PRESS_MASK); G_SIGNAL(popmenu,"button-press-event",popmenu_event,0); return popmenu; } // handle mouse button event in a popup menu int popmenu_event(GtkWidget *popmenu, GdkEvent *event) { if (((GdkEventButton *) event)->button != 1) // if not left mouse, kill menu gtk_menu_popdown(GTK_MENU(popmenu)); return 0; } // add a menu item to a popup menu GtkWidget *popmenu_widget; GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, cbFunc func, cchar *arg, cchar *mtip) { void popmenu_item_select(GtkWidget *, cchar *mtip); GtkWidget *wmitem; wmitem = gtk_menu_item_new_with_label(mname); gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),wmitem); if (func) { if (arg) G_SIGNAL(wmitem,"activate",func,arg); // call func with arg else G_SIGNAL(wmitem,"activate",func,mname); // call func with menu name } if (mtip) { popmenu_widget = wmitem; // trigger popup tool tip G_SIGNAL(wmitem,"select",popmenu_item_select,mtip); G_SIGNAL(wmitem,"deselect",popmenu_item_select,0); } return wmitem; } // show popup tip for selected menu item void popmenu_item_select(GtkWidget *wmitem, cchar *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) { int mx, my; if (mouse) { gdk_device_get_position(mouse,&screen,&mx,&my); // offset popup menu from mouse gdk_device_warp(mouse,screen,mx+30,my); } gtk_widget_show_all(popmenu); // GTK change: show before popup gtk_menu_popup_at_pointer(GTK_MENU(popmenu),null); return; } /******************************************************************************** Vertical Menu / Toolbar Build a custom vertical menu and/or toolbar in a vertical packing box Vmenu *vbm; cchar *name, *icon, *desc, *arg; int iww, ihh; void func(GtkWidget *, cchar *name); void RMfunc(GtkWidget *, cchar *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 menu item Vmenu_add_RMfunc(vbm, me, RMfunc); // add right-click menu item Vmenu_block(flag) 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, (cchar *) popup); i.e. use create_popmenu() as the response function and use the previously created menu 'popup' as the argument (cast to cchar *). ***/ 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; char *menufont1, *menufont2; PangoLayout *playout; cc = sizeof(Vmenu); Vmenu *vbm = (Vmenu *) zmalloc(cc); memset(vbm,0,cc); 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); // set menu fonts, normal and bold menufont2 = zstrdup(zfuncs::appboldfont); 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, callback function void Vmenu_add(Vmenu *vbm, cchar *name, cchar *icon, int iconww, int iconhh, cchar *desc, cbFunc func, cchar *arg) { using namespace Vmenunames; int me, cc, xpos, ww, hh; char iconpath[200], *mdesc, *name__; cchar *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); // create new menu entry from caller data if (icon) { vbm->menu[me].icon = zstrdup(icon); vbm->menu[me].iconww = iconww; vbm->menu[me].iconhh = iconhh; } if (desc) { // pad description with blanks for looks cc = strlen(desc); mdesc = (char *) zmalloc(cc+3); mdesc[0] = ' '; strcpy(mdesc+1,desc); strcpy(mdesc+cc+1," "); vbm->menu[me].desc = mdesc; } vbm->menu[me].func = func; // 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 printz("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,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 alternate function for right-mouse click void Vmenu_add_RMfunc(Vmenu *vbm, int me, cbFunc func, cchar *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; cchar *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; cchar *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 (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 me0 = me; // remember last match return; } if (me0 >= 0) // mouse left layout { paint_menu(cr,vbm,me0,0); // unhighlight prior desc = vbm->menu[me0].desc; // remove prior tool tip if (desc) poptext_mouse(0,0,0,0,0); 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 (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].func) vbm->menu[me].func(widget,vbm->menu[me].arg); // caller function(widget,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); 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 = gtk_widget_get_allocated_width(drawarea); int hh = gtk_widget_get_allocated_height(drawarea); if (hh < ww/2) gtk_widget_set_size_request(drawarea,ww,ww/2); 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; 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) { char *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 = (char *) substring(buff,"/ ",2*jj+1); if (! pp) goto fail; err = convSF(pp,apx[ii][jj],0,1); if (err) goto fail; pp = (char *) 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(cchar *title, GtkWidget *parent, ...) // parent added { zdialog *zd; GtkWidget *dialog, *hbox, *vbox, *butt, *hsep; cchar *bulab[zdmaxbutts]; int cc, ii, nbu; va_list arglist; static int uniqueID = 1; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("zdialog_new() called from thread"); va_start(arglist,parent); for (nbu = 0; nbu < zdmaxbutts; nbu++) { // get completion buttons bulab[nbu] = va_arg(arglist, cchar *); if (! bulab[nbu]) break; } 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); if (zdialog_count == zdialog_max) { // add to active list for (ii = 0; ii < zdialog_count; ii++) printz("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); // 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); 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, cchar *title) { gtk_window_set_title(GTK_WINDOW(zd->widget[0].widget),title); return; } // set a zdialog to be modal void zdialog_set_modal(zdialog *zd) { 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; 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; 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) { GtkWidget *widget = zdialog_gtkwidget(zd,"dialog"); gtk_window_present(GTK_WINDOW(widget)); return; } // set zdialog can or can not receive focus (informational or report dialog) void zdialog_can_focus(zdialog *zd, int Fcan) { 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, cchar *wname) { GtkWindow *window; GtkWidget *widget; 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 ( zdialog *zd, cchar *type, cchar *wname, cchar *pname, // mandatory args cchar *data, int size, int homog, int expand, int space, int wrap) // optional args (default = 0) { GtkWidget *widget = 0, *pwidget = 0, *fwidget = 0; GtkWidget *image, *vbox; GtkTextBuffer *editBuff = 0; PIXBUF *pixbuf = 0; GdkRGBA gdkrgba; GError *gerror = 0; cchar *pp, *ptype = 0; char vdata[30], iconpath[200]; double min, max, step, val; double f256 = 1.0 / 256.0; int iiw, iip, kk, err; 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); // initz. widget struct zd->widget[iiw].wname = zstrdup(wname); zd->widget[iiw].pname = zstrdup(pname); 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","edit","text", "button","zbutton","togbutt","check","radio", "imagebutt","colorbutt","combo","spin","zspin", "hscale","vscale","icon","image",null) == 0) { printz("*** 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); } 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); 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 } if (strmatch(type,"edit")) { // 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 } if (strmatch(type,"text")) { // 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),3); 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); if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); } 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(); if (! size) size = 10; // scale widget for font size gtk_widget_set_size_request(widget,size*appfontsize,2*appfontsize); G_SIGNAL(widget,"changed",zdialog_widget_event,zd); } 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","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 (*type == 's') { widget = gtk_spin_button_new_with_range(min,max,step); gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (*type == 'h') { 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); } if (*type == 'v') { 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,6,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 (cchar *) pixbuf in call // all widget types come here zd->widget[iiw].widget = widget; // set widget in zdialog if (strmatchV(type,"zentry","zspin","edit",0)) { // add frame around these widgets fwidget = gtk_frame_new(0); // removed "text" 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); // 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, cchar *type, cchar *wname, cchar *parent, cchar *data, cchar *options) { int stat, size = 0, homog = 0, expand = 0, space = 0, wrap = 0, begin = 1; char pname[8]; double pval; while (true) { stat = strParms(begin,options,pname,8,pval); if (stat == -1) break; if (stat == 1) zappcrash("bad zdialog options: %s",options); if (strmatch(pname,"size")) size = (int(pval)); else if (strmatch(pname,"homog")) homog = 1; else if (strmatch(pname,"expand")) expand = 1; else if (strmatch(pname,"space")) space = (int(pval)); else if (strmatch(pname,"wrap")) wrap = 1; else zappcrash("bad zdialog options: %s",options); } stat = zdialog_add_widget(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, cchar *title) // title is optional { int ok, ii; if (! zd) { printf("*** invalid zdialog %s \n",title); return 0; } for (ii = 0; ii < zdialog_count; ii++) // find in valid zdialog list if (zd == zdialog_list[ii]) break; if (ii == zdialog_count) { printf("*** zdialog not in valid list %s \n",title); return 0; } ok = 1; if ((zd->sentinel1 & 0xFFFF0000) != zdsentinel) ok = 0; if (zd->sentinel2 != zd->sentinel1) ok = 0; if (! ok) { printz("*** zdialog sentinel invalid %s \n",title); return 0; } if (title && ! strmatch(title,zd->title)) { printz("*** 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 expectedly possibly destroyed int zdialog_valid2(zdialog *zd, cchar *title) { int ok, ii; 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, cchar *wname) { if (! zdialog_valid(zd)) return 0; for (int ii = 0; zd->widget[ii].type; ii++) if (strmatch(zd->widget[ii].wname,wname)) return ii; printz("*** zdialog bad widget name %s \n",wname); return 0; } // get GTK widget from zdialog and widget name GtkWidget * zdialog_gtkwidget(zdialog *zd, cchar *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, cchar *wname, GdkPixbuf *pixbuf) { GtkWidget *widget; int ii; 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, cchar *wname, cchar *ttip) { GtkWidget *widget; int ii; 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; } // set a common group for a set of radio buttons // (GTK, this does not work) int zdialog_set_group(zdialog *zd, cchar *radio1, ...) { va_list arglist; cchar *radio2; GtkWidget *gwidget, *widget; GSList *glist; gwidget = zdialog_gtkwidget(zd,radio1); glist = gtk_radio_button_get_group(GTK_RADIO_BUTTON(gwidget)); if (! glist) zappcrash("no radio button group"); va_start(arglist,radio1); while (true) { radio2 = va_arg(arglist,cchar *); if (! radio2) break; widget = zdialog_gtkwidget(zd,radio2); gtk_radio_button_set_group(GTK_RADIO_BUTTON(widget),glist); } va_end(arglist); return 0; } // 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 (! 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, cchar *wname, cchar *data) { GtkWidget *widget; GtkTextBuffer *textBuff; GdkRGBA gdkrgba; int iiw, nn, kk, err, Nsteps; cchar *type, *pp; char *wdata, sdata[12]; double dval; double f256 = 1.0 / 256.0; double lval, hval, nval, F, F2; double fdata, lolim, hilim, step; // double 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"); // replace bad UTF-8 encoding else wdata = zstrdup(data); // 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,"edit")) { // 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,6,sdata); // double --> string, precision 6 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","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 cchar * zdialog_get_data(zdialog *zd, cchar *wname) { 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, cchar *wname, double min, double max) { GtkWidget *widget; cchar *type; int iiw; iiw = zdialog_find_widget(zd,wname); if (! iiw) { printz("*** 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, cchar *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 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, cchar *wname, float lval, float nval, float hval) { int iiw; iiw = zdialog_find_widget(zd,wname); if (! iiw) return 0; if (lval > nval || nval > hval) { printz("*** 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, cchar *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 // "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. DEFAULT. // // 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, cchar *posn) { int zdialog_KB_press(GtkWidget *, GdkEventKey *event, zdialog *zd); int zdialog_focus_event(GtkWidget *, GdkEvent *event, zdialog *zd); GtkWidget *dialog; if (! zdialog_valid(zd)) return 0; if (zd->zrunning) { printz("zdialog is already running \n"); return 0; } if (posn) zdialog_set_position(zd,posn); // put dialog at desired position else zdialog_set_position(zd,"save"); // use default if (evfunc) zd->eventCB = (void *) evfunc; // link to dialog event callback dialog = zd->widget[0].widget; gtk_widget_show_all(dialog); // activate dialog G_SIGNAL(dialog,"focus-in-event",zdialog_focus_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 zfuncs::zdialog_busy++; // count open zdialogs 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; cchar *wname, *wtype, *wdata; char 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"; 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,"edit","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; } } printz("*** 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; if (strmatch(wtype,"button")) wdata = gtk_button_get_label(GTK_BUTTON(widget)); // button label if (strmatch(wtype,"zbutton")) { // checkbox as smaller button wdata = gtk_button_get_label(GTK_BUTTON(widget)); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),0); // reset checkmark = off } if (strmatch(wtype,"edit")) { gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); } if (strmatch(wtype,"entry")) wdata = gtk_entry_get_text(GTK_ENTRY(widget)); if (strmatch(wtype,"zentry")) { gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); } 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","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); // 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; } // zdialog response handler for "focus-in-event" signal // private function zdialog *zdialog_focus_zd; // current zdialog int zdialog_focus_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 } // set KB shortcuts for common zdialog completion buttons int Nkbshortcuts = 0; cchar *kbshortcuts[10], *kbevents[10]; void zdialog_KB_addshortcut(cchar *shortcut, cchar *event) // e.g. ("Ctrl+D", "Done") { if (Nkbshortcuts > 9) return; kbshortcuts[Nkbshortcuts] = zstrdup(shortcut); kbevents[Nkbshortcuts] = zstrdup(event); ++Nkbshortcuts; return; } // 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 *, cchar *, void *); GtkWidget *focuswidget; int KB_Ctrl = 0, KB_Alt = 0; // track state of Ctrl and Alt keys int KBkey = kbevent->keyval; cchar *type; int ii, cc, Ftext; if (kbevent->state & GDK_CONTROL_MASK) KB_Ctrl = 1; if (kbevent->state & GDK_MOD1_MASK) KB_Alt = 1; if (KBkey == GDK_KEY_Escape) { // escape key if (zd->eventCB) zdialog_send_event(zd,"escape"); else zd->zstat = -2; return 1; } if (KBkey == GDK_KEY_F1) { KBevent(kbevent); return 1; }; // these keys handled by main app if (KBkey == GDK_KEY_F10) { KBevent(kbevent); return 1; }; if (KBkey == GDK_KEY_F11) { KBevent(kbevent); return 1; }; for (ii = 0; ii < Nkbshortcuts; ii++) { // look for dialog button shortcut if (strstr(kbshortcuts[ii],"Ctrl") && ! KB_Ctrl) continue; if (strstr(kbshortcuts[ii],"Alt") && ! KB_Alt) continue; cc = strlen(kbshortcuts[ii]); if (KBkey == kbshortcuts[ii][cc-1]) break; // compare key to last char. in shortcut if (KBkey-32 == kbshortcuts[ii][cc-1]) break; // (case indifferent compare) } if (ii < Nkbshortcuts) { zdialog_send_event(zd,kbevents[ii]); // send corresp. event, e.g. "Done" return 1; } 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; if (! type) return 0; strncpy0(zd->event,zd->widget[ii].wname,40); // save event name Ftext = strmatchV(type,"zspin","zentry","entry","edit","text","spin",null); // input widgets if (KBkey == GDK_KEY_Left || KBkey == GDK_KEY_Right) { // left/right arrow key if (! Ftext) { KBevent(kbevent); // not input widget, pass key to main() return 1; } } if (KBkey == GDK_KEY_Return && strstr("zentry",type)) return 1; // entry or zentry, disallow new line if ((Ftext == 1) && (KBkey >= 'A') && (KBkey <= 'z')) { // zspin widget and alpha character KBevent(kbevent); // pass to main() (shortcut key?) return 1; } return 0; // pass KB key to widget } // event function for "zspin" widget (text entry works as spin button) 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 char *wdata, sdata[20]; int time, elaps, Fchanged; static int time0 = 0, time1 = 0; if (! zdialog_valid2(zd)) return 1; // event after dialog destroyed if (zd->disabled) return 1; // 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,6,sdata); // round to 6 digits gtk_text_buffer_set_text(textBuff,sdata,-1); 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); // set new data zd->lastwidget = widget; // remember last widget updated strncpy0(zd->event,zd->widget[ii].wname,40); // event = widget name 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_KEY_PRESS) return 1; // stop new line from enter key if (event->type == GDK_SCROLL) return 1; // stop scroll of parent window return 0; // propagate others } // 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; cchar *wname; char 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,"edit","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, cchar *cliptext, void *arg) { GtkWindow *window; GtkWidget *widget; GtkTextView *textView = 0; GtkTextBuffer *textBuff = 0; zdialog *zd; int ii, cc = 0; cchar *wname; char 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,"edit","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 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 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() } else zdialog_free(zd); // no callback, kill now return 0; } // Send an event name to an active zdialog. // The response function eventFunc() will be called with this event. int zdialog_send_event(zdialog *zd, cchar *event) { zdialog_event * evfunc = 0; // dialog event callback function int ii; if (! zdialog_valid2(zd)) return 0; // zdialog canceled if (strmatch(event,"escape")) goto send_event; // escape key --> "escape" event if (zd->disabled) return 0; // zdialog busy if (strstr(zdialog_button_shortcuts,event)) { // dialog completion buttons (zfuncs.h) for (ii = 0; ii < zdmaxbutts; ii++) { // find button if (! zd->compbutton[ii]) break; // EOL if (strmatchcase(event,zd->compbutton[ii])) break; // english event } if (zd->compbutton[ii]) { // found zd->zstat = ii+1; // zdialog status = button no. strcpy(zd->event,"zstat"); // event = "zstat" } else if (strmatchcase(event,Bcancel)) // no [cancel] button in zdialog, zdialog_destroy(zd); // handle same as [x] } else if (strmatchcase(event,Bcancel)) zdialog_destroy(zd); // no dialog buttons, handle as [x] send_event: evfunc = (zdialog_event *) zd->eventCB; if (! evfunc) return 0; zd->disabled = 1; 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 (! 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 (! 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 (! 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; zdialog_busy--; } 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 (! 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((char *) zd->widget[ii].type); // free strings zfree((char *) zd->widget[ii].wname); if (zd->widget[ii].pname) zfree((char *) 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_delete(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 printz("*** 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) { while (true) { zmainloop(); if (! zd) return -1; if (! zdialog_valid2(zd)) return -1; if (zd->zstat) return zd->zstat; zsleep(0.01); } } // put cursor at named widget int zdialog_goto(zdialog *zd, cchar *wname) { GtkWidget *widget; 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 (! 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, cchar *wname, cchar *data) // stuff a string { if (data) zdialog_put_data(zd,wname,data); else zdialog_put_data(zd,wname,""); // null > "" return 1; } int zdialog_stuff(zdialog *zd, cchar *wname, int idata) // stuff an integer { char string[16]; double min, max; 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, cchar *wname, double ddata) // stuff a double { char string[32]; double min, max; 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, cchar *wname, double ddata, cchar *format) // stuff a double, formatted { char string[32]; double min, max; 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, cchar *labl, cchar *font, cchar *txt) // stuff label text using specified font { GtkWidget *widget; cchar *format = "%s"; char txt2[1000]; 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, cchar *wname, char *data, int maxcc) // fetch string data { cchar *zdata; 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, cchar *wname, int &idata) // fetch an integer { cchar *zdata; zdata = zdialog_get_data(zd,wname); if (! zdata) { idata = 0; return 0; } idata = atoi(zdata); return 1; } int zdialog_fetch(zdialog *zd, cchar *wname, double &ddata) // fetch a double { int stat; cchar *zdata; 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, cchar *wname, float &fdata) // fetch a float { int stat; cchar *zdata; double ddata; 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, cchar *wname) { int ii; 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, cchar *wname) { int ii; 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//.fotoxx/funcname // where zdialog data is saved for the respective function. // return 0 = OK, +N = error int zdialog_load_widgets(zdialog *zd, spldat *sd, cchar *funcname, FILE *fid) { using namespace zfuncs; cchar *mess = "Load settings from file"; int myfid = 0; char *filename, dirname[200], buff[1000]; char *wname, *wdata, wdata2[1000]; char *pp, *pp1, *pp2; int ii, kk, err, cc1, cc2; 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"); zfree(filename); if (! fid) { zmessageACK(mainwin,"%s \n %s",filename,strerror(errno)); return 1; } 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; pp2 += 3; if (*pp2 == ' ') pp2++; wdata = pp2; // widget data cc2 = strlen(wdata); if (cc2 < 1) wdata = (char *) ""; if (cc2 > 1000) continue; repl_1str(wdata,wdata2,"\\n","\n"); // replace "\n" with newline chars. kk = zdialog_put_data(zd,wname,wdata2); if (! kk) goto baddata; } 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, cchar *funcname, FILE *fid) { using namespace zfuncs; cchar *mess = "Save settings to a file"; int myfid = 0; char *filename, dirname[200]; char *wtype, *wname, *wdata, wdata2[1000]; int ii, cc; cchar *editwidgets = "entry zentry edit text togbutt check combo" // widget types to save "radio spin zspin hscale vscale colorbutt"; 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"); zfree(filename); if (! fid) { zmessageACK(mainwin,"%s \n %s",filename,strerror(errno)); return 1; } myfid = 1; } for (ii = 0; ii < zdmaxwidgets; ii++) { wtype = (char *) zd->widget[ii].type; if (! wtype) break; if (! strstr(editwidgets,wtype)) continue; wname = (char *) zd->widget[ii].wname; // write widget data recs: if (strstr("defcats deftags",wname)) continue; wdata = zd->widget[ii].data; // widgetname == widgetdata if (! wdata) continue; cc = strlen(wdata); if (cc > 900) continue; repl_1str(wdata,wdata2,"\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, cchar *funcname) { using namespace zfuncs; char filename[200]; FILE *fid; int err; 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, cchar *funcname) { using namespace zfuncs; char filename[200], dirname[200]; FILE *fid; int err; 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,0760); if (err) { printz("%s \n %s \n",dirname,strerror(errno)); return 1; } fid = fopen(filename,"w"); // open again } if (! fid) { printz("%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 { char 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(cchar *action) { using namespace zdposn_names; char posfile[200], buff[100], wintitle[64], *pp; float xpos, ypos; int xsize, ysize; int ii, nn, cc; FILE *fid; 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) { printz("*** 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; } printz("*** 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, cchar *posn) { using namespace zdposn_names; int ii, ppx, ppy, zdpx, zdpy, pww, phh; float xpos, ypos; int xsize, ysize; char wintitle[64], *pp; GtkWidget *parent, *dialog; 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 = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); // get window title, used as ID 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 title if (strmatch(wintitle,zdposn[ii].wintitle)) break; if (ii == Nzdposn) { // not found - zdialog_destroy() will add zdpx = ppx + 0.8 * pww; // supply reasonable defaults zdpy = ppx + 0.2 * phh; gtk_window_get_size(GTK_WINDOW(dialog),&xsize,&ysize); // use current size } else { 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) return; 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; } } // 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; char wintitle[64], *pp; GtkWidget *parent, *dialog; 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 = (char *) gtk_window_get_title(GTK_WINDOW(dialog)); if (! pp) return; if (strlen(pp) < 2) return; if (strstr(pp,"/tmp/.mount")) return; // volatile appimage path names 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 { char *zdtitle; // zdialog title int Nw; // no. of widgets char **wname; // list of widget names char **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(cchar *action) { using namespace zdinputs_names; char zdinputsfile[200], buff[ccmax2]; char zdtitle[ccmax1], wname[Nwmax][ccmax1], wdata[Nwmax][ccmax2]; char *pp, *pp1, *pp2, wdata2[ccmax2+50]; FILE *fid; int Nw, ii, jj, cc, cc1, cc2; snprintf(zdinputsfile,200,"%s/zdialog_inputs",zhomedir); // /home//.appname/zdialog_inputs if (strmatch(action,"load")) // load dialog input fields from its file { Nzd = 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) break; if (! strmatchN(pp,"zdialog == ",11)) continue; strncpy0(zdtitle,pp+11,ccmax1); // save new zdialog title pp = fgets_trim(buff,ccmax2,fid,1); // read widget count if (! pp) break; Nw = atoi(pp); if (Nw < 1 || Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } for (ii = 0; ii < Nw; ii++) // read widget data recs { pp = fgets_trim(buff,ccmax2,fid,1); if (! pp) break; 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 = (char *) ""; 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],"\\n","\n"); // replace "\n" with newline chars. } if (ii < Nw) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); continue; } if (Nzd == Nzdmax) { printz("*** zdialog_inputs() too many dialogs \n"); break; } zdinputs[Nzd].zdtitle = zstrdup(zdtitle); // save acculumated zdialog data zdinputs[Nzd].Nw = Nw; cc = Nw * sizeof(char *); zdinputs[Nzd].wname = (char **) zmalloc(cc); zdinputs[Nzd].wdata = (char **) zmalloc(cc); for (ii = 0; ii < Nw; ii++) { zdinputs[Nzd].wname[ii] = zstrdup(wname[ii]); zdinputs[Nzd].wdata[ii] = zstrdup(wdata[ii]); } Nzd++; } fclose(fid); return Nzd; } if (strmatch(action,"save")) // save dialog input fields to its file { fid = fopen(zdinputsfile,"w"); if (! fid) { printz("*** zdialog_inputs() cannot write file \n"); return 0; } for (ii = 0; ii < Nzd; ii++) { fprintf(fid,"zdialog == %s \n",zdinputs[ii].zdtitle); // zdialog == zdialog title Nw = zdinputs[ii].Nw; fprintf(fid,"%d \n",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,"\n","\\n"); // replace newline chars. with "\n" fprintf(fid,"%s == %s \n",pp1,wdata2); } fprintf(fid,"\n"); } fclose(fid); return Nzd; } printz("*** 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; char zdtitle[ccmax1], wname[ccmax1], wdata[ccmax2], *type; int ii, jj, Nw, cc; 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) { printz("*** 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 type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame scrwin" // skip non-input widgets "label link button zbutton",type)) continue; Nw++; } if (! Nw) return 0; // no input widgets if (Nw > Nwmax) { printz("*** zdialog_inputs() bad data: %s \n",zdtitle); return 0; } zdinputs[ii].zdtitle = zstrdup(zdtitle); // set zdialog title cc = Nw * sizeof(char *); // allocate pointers for widgets zdinputs[ii].wname = (char **) zmalloc(cc); zdinputs[ii].wdata = (char **) zmalloc(cc); for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) { // add widget names and data type = (char *) zd->widget[jj].type; if (strstr("dialog hbox vbox hsep vsep frame scrwin" // skip non-input widgets "label link button zbutton",type)) 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); zdinputs[ii].wdata[Nw] = zstrdup(wdata); 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_restore_inputs(zdialog *zd) { using namespace zdinputs_names; char *zdtitle, *wname, *wdata; int ii, jj; zd->saveinputs = 1; // flag, save data at zdialog_free() zdtitle = (char *) 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 // returned text is subject for zfree() // null is returned if user presses [cancel] button. char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext) { zdialog *zd; int zstat; char *text; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("zdialog_text() called from thread"); zd = zdialog_new(title,parent,BOK,Bcancel,null); zdialog_add_widget(zd,"frame","fred","dialog"); zdialog_add_widget(zd,"edit","edit","fred"); if (inittext) zdialog_stuff(zd,"edit",inittext); zdialog_resize(zd,300,0); zdialog_set_modal(zd); zdialog_run(zd,0,"mouse"); zdialog_present(zd); // help wayland zstat = zdialog_wait(zd); if (zstat == 1) text = (char *) zdialog_get_data(zd,"edit"); else text = 0; if (text) text = zstrdup(text); zdialog_free(zd); return text; } /********************************************************************************/ // Display a dialog with a message and 1-5 choice buttons. // Returns choice 1-N corresponding to button selected. // nn = zdialog_choose(parent, where, message, butt1, butt2, ... null) // 'where' is null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window int zdialog_choose(GtkWidget *parent, cchar *where, cchar *message, ...) { zdialog *zd; va_list arglist; int ii, zstat, Nbutn; cchar *butn[6]; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("zmessage_choose() called from thread"); va_start(arglist,message); for (ii = 0; ii < 5; ii++) { butn[ii] = va_arg(arglist,cchar *); if (! butn[ii]) break; } Nbutn = ii; if (! Nbutn) zappcrash("zdialog_choose(), no buttons"); zd = zdialog_new("choose",parent,butn[0],butn[1],butn[2],butn[3],butn[4],null); zdialog_add_widget(zd,"hbox","hbmess","dialog","space=3"); zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5"); zdialog_set_modal(zd); zdialog_set_decorated(zd,0); zdialog_resize(zd,200,0); zdialog_run(zd,null,where); zdialog_present(zd); // wayland zstat = zdialog_wait(zd); zdialog_free(zd); return zstat; } /********************************************************************************/ // Display a dialog with a message and 1-5 choice buttons. // Returns choice 1-5 corresponding to button selected. // Returns -1 if cancel button [x] is selected. // Returns the KB character 20-127 if one is pressed. // nn = zdialog_choose2(parent, where, message, butt1, butt2, ... null) // 'where' is null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window // "parent" center dialog in parent window int zdialog_choose2(GtkWidget *parent, cchar *where, cchar *message, ...) { int zdialog_choose2_event(zdialog *zd, cchar *event); int zdialog_choose2_KBevent(GtkWidget *, GdkEventKey *event, zdialog *zd); GtkWidget *widget; zdialog *zd; va_list arglist; int ii, zstat, Nbutn; cchar *butn[6]; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("zmessage_choose2() called from thread"); va_start(arglist,message); for (ii = 0; ii < 5; ii++) { butn[ii] = va_arg(arglist,cchar *); if (! butn[ii]) break; } Nbutn = ii; if (! Nbutn) zappcrash("zdialog_choose(), no buttons"); zd = zdialog_new("choose",parent,butn[0],butn[1],butn[2],butn[3],butn[4],null); zdialog_add_widget(zd,"hbox","hbmess","dialog","space=3"); zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5"); zdialog_set_modal(zd); zdialog_resize(zd,200,0); widget = zd->widget[0].widget; G_SIGNAL(widget,"key-press-event",zdialog_choose2_KBevent,zd); zdialog_run(zd,zdialog_choose2_event,where); zstat = zdialog_wait(zd); zdialog_free(zd); return zstat; } // button events and [x] event int zdialog_choose2_event(zdialog *zd, cchar *event) { if (strmatch(event,"escape")) zd->zstat = -2; // escape key if (! zd->zstat) return 1; zdialog_destroy(zd); // a button was pressed return 1; } // KB input events int zdialog_choose2_KBevent(GtkWidget *, GdkEventKey *event, zdialog *zd) { int KBkey = event->keyval; if (KBkey == GDK_KEY_Escape) { // escape key zdialog_destroy(zd); return 1; } if (KBkey >= GDK_KEY_A && KBkey <= GDK_KEY_z) { // key A ... z zdialog_destroy(zd); zd->zstat = KBkey; return 1; } return 0; } /********************************************************************************/ // 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: // CBfunc(GtkWidget *, int line, int posn, int KBkey) // ... - optional event buttons terminated with null: // [OK] [Cancel] [Find] and [Esc] are processed here // others are passed to callback function (1st character) // zd->zstat = 1/2 for buttons [ OK ] / [Cancel] zdialog * popup_report_open(cchar *title, GtkWidget *parent, int ww, int hh, int Fheader, textwidget_callbackfunc_t CBfunc, ...) { int popup_report_dialog_event(zdialog *zd, cchar *event); va_list arglist; cchar *butn[6]; int ii, NB; zdialog *zd; GtkWidget *mHead, *mText; va_start(arglist,CBfunc); // get button args, if any for (ii = 0; ii < 5; ii++) { butn[ii] = va_arg(arglist,cchar *); if (! butn[ii]) break; } 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 zdialog_add_widget(zd,"text","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_text_view_set_editable(GTK_TEXT_VIEW(mText),0); gtk_widget_grab_focus(mText); textwidget_set_eventfunc(mText,CBfunc); // set mouse/KB event function zd->popup_report_CB = (void *) CBfunc; return zd; } // dialog event and completion function int popup_report_dialog_event(zdialog *zd, cchar *event) { textwidget_callbackfunc_t *CBfunc; GtkWidget *mText; static char findtext[40] = ""; int linem, line1, line2; zdialog *zdf; if (strmatch(event,"focus")) return 1; if (zd->zstat) { // [x] cancel or escape, kill dialog zdialog_free(zd); return 1; } if (strmatch(event,BOK)) { // [OK] hide dialog zdialog_show(zd,0); return 1; } if (strmatch(event,Bcancel)) { // [Cancel] kill dialog zdialog_free(zd); return 1; } if (strmatch(event,"escape")) { // Escape key hide dialog zdialog_show(zd,0); return 1; } if (strmatch(event,Bfind)) { // [Find] zdf = zdialog_new("find text",zd->dialog,Bfind,Bcancel,0); // popup dialog to enter text zdialog_add_widget(zdf,"entry","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 } } mText = zdialog_gtkwidget(zd,"text"); CBfunc = (textwidget_callbackfunc_t *) zd->popup_report_CB; // other event if (CBfunc) CBfunc(mText,-1,-1,*event); // pass to callback function (1st char.) return 1; } // write a non-scrolling header line void popup_report_header(zdialog *zd, int bold, cchar *format, ...) { va_list arglist; char message[1000]; GtkWidget *mHead; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mHead = zdialog_gtkwidget(zd,"header"); textwidget_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, cchar *format, ...) { va_list arglist; char message[1000]; GtkWidget *mText; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); textwidget_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, cchar *format, ...) { va_list arglist; char message[1000]; GtkWidget *mText; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); textwidget_append2(mText,bold,"%s",message); return; } // scroll window back to top line void popup_report_top(zdialog *zd) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_scroll(mText,0); return; } // scroll window back to bottom line void popup_report_bottom(zdialog *zd) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_scroll(mText,999999); return; } // clear the report window void popup_report_clear(zdialog *zd) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_clear(mText); return; } // clear the report window from line to end void popup_report_clear(zdialog *zd, int line) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_clear(mText,line); return; } // insert a new line after a given line void popup_report_insert(zdialog *zd, int bold, int line, cchar *format, ...) { va_list arglist; char message[1000]; GtkWidget *mText; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); textwidget_insert(mText,bold,line,message); return; } // replace a given line void popup_report_replace(zdialog *zd, int bold, int line, cchar *format, ...) { va_list arglist; char message[1000]; GtkWidget *mText; va_start(arglist,format); vsnprintf(message,999,format,arglist); va_end(arglist); mText = zdialog_gtkwidget(zd,"text"); textwidget_replace(mText,bold,line,message); return; } // delete a given line void popup_report_delete(zdialog *zd, int line) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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, char *matchtext, int line1) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return textwidget_find(mText,matchtext,line1); } // insert a pixbuf image after a given line void popup_report_insert_pixbuf(zdialog *zd, int line, GdkPixbuf *pixbuf) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_insert_pixbuf(mText,line,pixbuf); return; } // scroll to bring a given line into the report window void popup_report_scroll(zdialog *zd, int line) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_scroll_top(mText,line); return; } // get the range of visible lines in the report window void popup_report_get_visible_lines(zdialog *zd, int &top, int &bott) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_get_visible_lines(mText,top,bott); return; } // retrieve a given line and optionally strip the trailing \n char * popup_report_line(zdialog *zd, int line, int strip) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return textwidget_line(mText,line,strip); } // retrieve the word starting at a given position in a given line char * popup_report_word(zdialog *zd, int line, int posn, cchar *dlims, char &end) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); return textwidget_word(mText,line,posn,dlims,end); } // highlight a given line of text void popup_report_highlight_line(zdialog *zd, int line) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_bold_word(mText,line,posn,cc); return; } // set font attributes for entire report // temp. kludge void popup_report_font_attributes(zdialog *zd) { GtkWidget *mText = zdialog_gtkwidget(zd,"text"); textwidget_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 (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) { zdialog_destroy(zd); return; } /********************************************************************************/ // execute a shell command and show the output in a scrolling popup window // returns: 0 = EOF 1 = command failure int popup_command(cchar *command, int ww, int hh, GtkWidget *parent, int top) { FILE *fid; char buff[1000], *pp; zdialog *zd; printz("run command: %s \n",command); zd = popup_report_open(command,parent,ww,hh,0,0,Bfind,Bcancel,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; } /********************************************************************************/ // Display popup message box and wait for user acknowledgement. // May be called from a thread. // Messages are presented sequentially from main() and from threads. void zmessageACK(GtkWidget *parent, cchar *format, ... ) { va_list arglist; char message[1000], xmessage[1000]; cchar *posn = "mouse"; zdialog *zd; va_start(arglist,format); // format the message vsnprintf(message,1000,format,arglist); va_end(arglist); printz("%s \n",message); // output to log file if (! pthread_equal(pthread_self(),zfuncs::tid_main)) { // caller is a thread snprintf(xmessage,1000,"xmessage -center %s",message); // use xmessage() zshell(0,xmessage); return; } zd = zdialog_new("ACK",parent,BOK,null); // caller is main() 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); if (parent) posn = "parent"; else posn = "desktop"; zdialog_run(zd,0,posn); zdialog_present(zd); // help wayland zdialog_wait(zd); zdialog_free(zd); return; } /********************************************************************************/ // display message box and wait for user Yes or No response // returns 1 or 0 int zmessageYN(GtkWidget *parent, cchar *format, ... ) { va_list arglist; char message[500]; cchar *where; zdialog *zd; int zstat; va_start(arglist,format); vsnprintf(message,500,format,arglist); va_end(arglist); printz("%s \n",message); // output to log file if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("zmessageYN() called from thread"); if (parent) where = "parent"; else where = "desktop"; zd = zdialog_new("YN",parent,Byes,Bno,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_run(zd,0,where); zdialog_present(zd); // help wayland zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat == 1) return 1; return 0; } /********************************************************************************/ // display message until timeout (can be forever) or user cancel // or caller kills it with zdialog_free() typedef struct { zdialog *zd; int uniqueID; } zdx_t; zdialog * zmessage_post(GtkWidget *parent, cchar *loc, int seconds, cchar *format, ... ) { int zmessage_post_timeout(zdx_t *zdx); va_list arglist; char message[1000], xmessage[1000]; static zdx_t zdx[100]; static int ii = 0; zdialog *zd; va_start(arglist,format); vsnprintf(message,1000,format,arglist); va_end(arglist); printz("%s \n",message); // output to log file if (! pthread_equal(pthread_self(),zfuncs::tid_main)) { // called from thread, use xmessage if (seconds < 1) seconds = 1; // disallow zero snprintf(xmessage,1000,"xmessage -center -timeout %d %s",seconds,message); zshell(0,xmessage); return 0; } zd = zdialog_new("post",parent,null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_set_decorated(zd,0); zdialog_run(zd,0,loc); // mouse position zdialog_present(zd); // help wayland if (parent) gtk_window_present(GTK_WINDOW(parent)); // return focus to parent 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; } // same as above, but message is big, bold and red zdialog * zmessage_post_bold(GtkWidget *parent, cchar *loc, int seconds, cchar *format, ... ) { int zmessage_post_timeout(zdx_t *zdx); va_list arglist; char message[400], messagebold[460]; static zdx_t zdx[100]; static int ii = 0; zdialog *zd; va_start(arglist,format); vsnprintf(message,400,format,arglist); va_end(arglist); printz("%s \n",message); // output to log file if (! pthread_equal(pthread_self(),zfuncs::tid_main)) return 0; // called from thread 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_set_decorated(zd,0); zdialog_run(zd,0,loc); // mouse position zdialog_present(zd); // help wayland if (parent) gtk_window_present(GTK_WINDOW(parent)); // return focus to parent 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 { char *ptext = 0; GtkWidget *popwin = 0; char *pcurrent = 0; #define GSFNORMAL GTK_STATE_FLAG_NORMAL } // timer function to show popup window after a given time int poptext_show(char *current) { using namespace poptext; 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(char *current) { using namespace poptext; if (current != pcurrent) return 0; if (popwin) gtk_widget_destroy(popwin); if (ptext) zfree(ptext); popwin = 0; ptext = 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(cchar *text, int px, int py, float secs1, float secs2) { using namespace poptext; GtkWidget *label; int cc, millisec1, millisec2; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("poptext_screen() called from thread"); poptext_killnow(); pcurrent++; // make current != pcurrent if (! text) return; cc = strlen(text) + 4; // construct popup window ptext = (char *) zmalloc(cc); // with caller's text *ptext = 0; strncatv(ptext,cc," ",text," ",null); // add extra spaces popwin = gtk_window_new(GTK_WINDOW_POPUP); label = gtk_label_new(ptext); 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(cchar *text, int dx, int dy, float secs1, float secs2) { int mx, my; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("poptext_mouse() called from thread"); if (! text) { poptext_killnow(); return; } gdk_device_get_position(zfuncs::mouse,0,&mx,&my); // mouse screen position poptext_screen(text,mx+dx,my+dy,secs1,secs2); // add displacements return; } // Show a popup text message at the given window position. void poptext_window(GtkWindow *win, cchar *text, int dx, int dy, float secs1, float secs2) { int px, py; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("poptext_window() called from thread"); if (! text) { poptext_killnow(); return; } gtk_window_get_position(win,&px,&py); poptext_screen(text,px+dx,py+dy,secs1,secs2); return; } // Show a popup text message at the given widget position. void poptext_widget(GtkWidget *widget, cchar *text, int dx, int dy, float secs1, float secs2) { GdkWindow *win; int px, py; if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("poptext_widget() called 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; } /********************************************************************************/ // Show an image file in a popup window at mouse position. // Re-use most recent window or create a new one if Fnewin != 0. // Returns 0 if OK, +N otherwise. namespace popup_image_names { GtkWidget *window[10], *drawarea[10]; // up to 10 popup windows open char *filex[10], reqfull[10], isfull[10]; int Nval[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int Nw = 0; } int popup_image(cchar *file, GtkWindow *parent, int Fnewin, int size) { using namespace popup_image_names; static int ftf = 1; cchar *tipmess("zoom via mouse wheel or Keys +/=/-/↑/↓"); int popup_image_draw(GtkWidget *, cairo_t *, int &Nw); int popup_image_scroll(GtkWidget *, GdkEvent *event, int &Nw); int popup_image_KBevent(GtkWidget *, GdkEventKey *event, int &Nw); int popup_image_mousebutt(GtkWidget *, GdkEvent *event, int &Nw); int popup_image_state_event(GtkWidget *, GdkEvent *, int &Nw); if (! pthread_equal(pthread_self(),zfuncs::tid_main)) zappcrash("popup_image() called from thread"); if (ftf) { ftf = 0; poptext_mouse(tipmess,0,0,0,8); } if (Fnewin) if (++Nw == 10) Nw = 0; // new window, re-use oldest up to 10 if (! Fnewin) while (Nw > 0 && window[Nw] == 0) Nw--; // else re-use latest still active if (window[Nw]) { gtk_widget_destroy(drawarea[Nw]); drawarea[Nw] = 0; zfree(filex[Nw]); filex[Nw] = 0; } else { window[Nw] = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create new popup window if (! window[Nw]) return 1; if (! size) size = 512; gtk_window_set_default_size(GTK_WINDOW(window[Nw]),size,size); if (parent) { gtk_window_set_transient_for(GTK_WINDOW(window[Nw]),parent); gtk_window_set_destroy_with_parent(GTK_WINDOW(window[Nw]),1); } gtk_window_set_position(GTK_WINDOW(window[Nw]),GTK_WIN_POS_MOUSE); gtk_widget_freeze_child_notify(window[Nw]); // improves resize performance G_SIGNAL(window[Nw],"destroy",gtk_widget_destroyed,&window[Nw]); // set window = null if destroyed } filex[Nw] = zstrdup(file); drawarea[Nw] = gtk_drawing_area_new(); // new drawing area always required if (! drawarea[Nw]) return 2; gtk_container_add(GTK_CONTAINER(window[Nw]),drawarea[Nw]); reqfull[Nw] = isfull[Nw] = 0; // not fullscreen gtk_widget_add_events(window[Nw],GDK_SCROLL_MASK); gtk_widget_add_events(window[Nw],GDK_KEY_PRESS_MASK); gtk_widget_add_events(window[Nw],GDK_BUTTON_RELEASE_MASK); G_SIGNAL(window[Nw],"draw",popup_image_draw,&Nval[Nw]); G_SIGNAL(window[Nw],"scroll-event",popup_image_scroll,&Nval[Nw]); // connect events G_SIGNAL(window[Nw],"key-press-event",popup_image_KBevent,&Nval[Nw]); G_SIGNAL(window[Nw],"button-release-event",popup_image_mousebutt,&Nval[Nw]); G_SIGNAL(window[Nw],"window-state-event",popup_image_state_event,&Nval[Nw]); gtk_widget_show_all(window[Nw]); return 0; } // resize image and repaint window when resized int popup_image_draw(GtkWidget *window, cairo_t *cr, int &nn) { using namespace popup_image_names; PIXBUF *pixb1, *pixb2; GError *gerror; int ww1, hh1, ww2, hh2; int sww, shh; double area; char *file; cchar *pp; file = filex[nn]; if (! file) return 1; gerror = 0; pixb1 = gdk_pixbuf_new_from_file(file,&gerror); // load image file into pixbuf if (! pixb1) { printz("*** file: %s \n %s \n",file,gerror->message); return 1; } pp = strrchr(file,'/'); // window title = file name gtk_window_set_title(GTK_WINDOW(window),pp+1); ww1 = gdk_pixbuf_get_width(pixb1); // image dimensions hh1 = gdk_pixbuf_get_height(pixb1); sww = monitor_ww; shh = monitor_hh; gtk_window_get_size(GTK_WINDOW(window),&ww2,&hh2); // current window dimensions area = ww2 * hh2; ww2 = sqrt(area * ww1 / hh1); // fit window to image, keeping same area hh2 = area / ww2; if (ww2 < sww && hh2 < shh) // prevent GTK resize event loop gtk_window_resize(GTK_WINDOW(window),ww2,hh2); pixb2 = gdk_pixbuf_scale_simple(pixb1,ww2,hh2,GDK_INTERP_BILINEAR); // rescale pixbuf to window if (! pixb2) return 1; gdk_cairo_set_source_pixbuf(cr,pixb2,0,0); // draw image cairo_paint(cr); g_object_unref(pixb1); g_object_unref(pixb2); return 1; } // respond to mouse scroll button and zoom window larger or smaller int popup_image_scroll(GtkWidget *window, GdkEvent *event, int &nn) { using namespace popup_image_names; int scroll, ww, hh; int sww, shh; double ff = 1.0; if (event->type == GDK_SCROLL) { // mouse wheel event scroll = ((GdkEventScroll *) event)->direction; if (scroll == GDK_SCROLL_UP) ff = 1.33333; if (scroll == GDK_SCROLL_DOWN) ff = 0.75; } gtk_window_get_size(GTK_WINDOW(window),&ww,&hh); // current window dimensions ww *= ff; // new dimensions hh *= ff; sww = monitor_ww; shh = monitor_hh; if (ww > sww || hh > shh) { // request > screen size, fullscreen reqfull[nn] = 1; gtk_window_fullscreen(GTK_WINDOW(window)); return 1; } reqfull[nn] = 0; gtk_window_unfullscreen(GTK_WINDOW(window)); if (ww + hh > 500) gtk_window_resize(GTK_WINDOW(window),ww,hh); // rescale up or down else gtk_widget_destroy(window); // if very small, delete window return 1; } // respond to KB events F11 (fullscreen/unfullscreen) // Escape (destroy) // +/= or up-arrow make image larger // - or down-arrow make image smaller int popup_image_KBevent(GtkWidget *window, GdkEventKey *event, int &nn) { using namespace popup_image_names; int KBkey = event->keyval; int ww, hh; int sww, shh; double ff = 0; if (KBkey == GDK_KEY_Escape) gtk_widget_destroy(window); if (KBkey == GDK_KEY_F11) { if (reqfull[nn]) { reqfull[nn] = 0; gtk_window_unfullscreen(GTK_WINDOW(window)); } else { reqfull[nn] = 1; gtk_window_fullscreen(GTK_WINDOW(window)); } } if (KBkey == GDK_KEY_plus || KBkey == GDK_KEY_KP_Add || KBkey == GDK_KEY_equal || KBkey == GDK_KEY_Up) ff = 1.33333; if (KBkey == GDK_KEY_minus || KBkey == GDK_KEY_KP_Subtract || KBkey == GDK_KEY_Down) ff = 0.75; if (ff) { gtk_window_get_size(GTK_WINDOW(window),&ww,&hh); // current window dimensions ww *= ff; // new dimensions hh *= ff; sww = monitor_ww; shh = monitor_hh; if (ww > sww || hh > shh) { // request > screen size, fullscreen reqfull[nn] = 1; gtk_window_fullscreen(GTK_WINDOW(window)); return 1; } reqfull[nn] = 0; gtk_window_unfullscreen(GTK_WINDOW(window)); if (ww + hh > 500) gtk_window_resize(GTK_WINDOW(window),ww,hh); // rescale up or down else gtk_widget_destroy(window); // if very small, delete window } return 1; } // respond to mouse button - destroy window int popup_image_mousebutt(GtkWidget *window, GdkEvent *event, int &nn) { gtk_widget_destroy(window); return 1; } // track window fullscreen state int popup_image_state_event(GtkWidget *window, GdkEvent *event, int &nn) { using namespace popup_image_names; int state = ((GdkEventWindowState *) event)->new_window_state; if (state & GDK_WINDOW_STATE_FULLSCREEN) isfull[nn] = 1; else isfull[nn] = 0; if (isfull[nn] != reqfull[nn]) { // compensate GTK bug: FIXME if (reqfull[nn]) gtk_window_fullscreen(GTK_WINDOW(window)); // the window fullscreens itself after else gtk_window_unfullscreen(GTK_WINDOW(window)); // being requested to unfullscreen } return 1; } /******************************************************************************** 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() char * zgetfile(cchar *title, GtkWindow *parent, cchar *action, cchar *initfile, int hidden) { if (! strmatchV(action,"file","save","folder","create folder",null)) zappcrash("zgetfile() call error: %s",action); char **flist = zgetfiles(title,parent,action,initfile,hidden); if (! flist) return 0; char *file = *flist; zfree(flist); return file; } // version for 2 or more files // returns a list of filespecs (char **) terminated with null // returns null if canceled by user char ** zgetfiles(cchar *title, GtkWindow *parent, cchar *action, cchar *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; cchar *button1 = 0, *buttxx = 0; char *pdir, *pfile; int ii, err, NF, setfname = 0; int fcstat, bcode = 0, hide = 1; int fcdes = 0; char *file1, *file2, **flist = 0; STATB fstat; 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 = Bsave; 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 Bcancel, 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,&fstat); if (err) { pdir = zstrdup(initfile); // 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 = (char *) initfile; gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pfile); } zfree(pdir); } else if (S_ISREG(fstat.st_mode)) // select given file gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile); else if (S_ISDIR(fstat.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 = (char **) zmalloc((NF+1)*sizeof(char *)); // allocate returned list for (ii = 0; ii < NF; ii++) { // process selected files file1 = (char *) g_slist_nth_data(gslist,ii); file2 = zstrdup(file1); // 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; char *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(). char * zgetfolder(cchar *title, GtkWindow *parent, cchar *initfolder) { GtkWidget *dialog; GtkFileChooser *chooser; int nn; char *pp1, *pp2 = null; dialog = gtk_file_chooser_dialog_new(title, parent, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, Bcancel, GTK_RESPONSE_CANCEL, Bopen, 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); g_free(pp1); } gtk_widget_destroy(dialog); return pp2; } /******************************************************************************** print_image_file(GtkWidget *parent, cchar *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; cchar *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, cchar *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, cchar *imagefile) { using namespace print_image; GtkPrintOperationResult printstat; GError *gerror = 0; int err; 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; char 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,BOK,Bcancel,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_restore_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, cchar *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); 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; char *file = 0; 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) printz("%s \n",gerror->message); return; } gtk_drag_set_icon_pixbuf(context,pixbuf,64,64); // hot spot is middle of image return; cancel: printz("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; char *file = 0; // char *files[2] = { file, null }; ufunc2 = (drag_drop_source_func *) ufunc; file = ufunc2(); if (! file) goto cancel; gtk_selection_data_set_text(data,file,-1); // drops text // gtk_selection_data_set_uris(data,files); // does nothing FIXME return; cancel: printz("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 *); 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) { char * drag_drop_unescape(cchar *escaped_string); drag_drop_dest_func *ufunc2; char *text, *text2, *file, *file2; int cc; text = (char *) 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); // 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); 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 char * drag_drop_unescape(cchar *inp) { int drag_drop_convhex(char ch); char inch, *out, *outp; int nib1, nib2; out = (char *) zmalloc(strlen(inp)+1); 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(char ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; return ch; } /******************************************************************************** Miscellaneous GDK/GTK functions *********************************************************************************/ // Get thumbnail image for given image file. // Returned thumbnail belongs to caller: g_object_unref() is necessary. PIXBUF * get_thumbnail(cchar *fpath, int size) { PIXBUF *thumbpxb; GError *gerror = 0; int err; char *bpath; STATB statf; err = stat(fpath,&statf); // fpath status info if (err) return 0; if (S_ISDIR(statf.st_mode)) { // if folder, return folder image bpath = (char *) zmalloc(500); *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(cchar *imagefile) { GError *gerror = 0; PIXBUF *pixbuf; GdkDisplay *display; GdkCursor *cursor = 0; char imagepath[200]; 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 printz("*** %s \n",gerror->message); return cursor; } /******************************************************************************** PIXBUF * gdk_pixbuf_rotate(PIXBUF *pixbuf, float angle, int acolor) Rotate a pixbuf through an arbitrary angle (degrees). The returned image has the same size as the original, but the pixbuf envelope is increased to accommodate the rotated original (e.g. a 100x100 pixbuf rotated 45 deg. needs a 142x142 pixbuf). Pixels added around the rotated image have all RGB values = acolor. Angle is in degrees. Positive direction is clockwise. Pixbuf must have 8 bits per channel and 3 or 4 channels. Loss of resolution is about 1/2 pixel. Speed is about 28 million pixels/sec. on 3.3 GHz CPU. (e.g. a 10 megapix image needs about 0.36 seconds) NULL is returned if the function fails for one of the following: - pixbuf not 8 bits/channel or < 3 channels - unable to create output pixbuf (lack of memory?) Algorithm: create output pixbuf big enough for rotated input pixbuf compute coefficients for affine transform loop all output pixels (px2,py2) get corresp. input pixel (px1,py1) using affine transform if outside of pixbuf output pixel = black continue for 4 input pixels based at (px0,py0) = (int(px1),int(py1)) compute overlap (0 to 1) with (px1,py1) sum RGB values * overlap output aggregate RGB to pixel (px2,py2) Benchmark: rotate 7 megapixel image 10 degrees 0.31 secs. 3.3 GHz Core i5 ***/ PIXBUF * gdk_pixbuf_rotate(PIXBUF *pixbuf1, float angle, int acolor) { typedef unsigned char *pixel; // 3 RGB values, 0-255 each PIXBUF *pixbuf2; GDKCOLOR color; int nch, nbits, alpha; int ww1, hh1, rs1, ww2, hh2, rs2; int px2, py2, px0, py0; pixel ppix1, ppix2, pix0, pix1, pix2, pix3; float px1, py1; float f0, f1, f2, f3, red, green, blue, tran = 0; float a, b, d, e, ww15, hh15, ww25, hh25; float PI = 3.141593; nch = gdk_pixbuf_get_n_channels(pixbuf1); nbits = gdk_pixbuf_get_bits_per_sample(pixbuf1); if (nch < 3) return 0; // must have 3+ channels (colors) if (nbits != 8) return 0; // must be 8 bits per channel color = gdk_pixbuf_get_colorspace(pixbuf1); // get input pixbuf1 attributes alpha = gdk_pixbuf_get_has_alpha(pixbuf1); ww1 = gdk_pixbuf_get_width(pixbuf1); hh1 = gdk_pixbuf_get_height(pixbuf1); rs1 = gdk_pixbuf_get_rowstride(pixbuf1); while (angle < -180) angle += 360; // normalize, -180 to +180 while (angle > 180) angle -= 360; angle = angle * PI / 180; // radians, -PI to +PI if (fabsf(angle) < 0.001) { pixbuf2 = gdk_pixbuf_copy(pixbuf1); // angle is zero within my precision return pixbuf2; } ww2 = int(ww1*fabsf(cosf(angle)) + hh1*fabsf(sinf(angle))); // rectangle containing rotated image hh2 = int(ww1*fabsf(sinf(angle)) + hh1*fabsf(cosf(angle))); pixbuf2 = gdk_pixbuf_new(color,alpha,nbits,ww2,hh2); // create output pixbuf2 if (! pixbuf2) return 0; rs2 = gdk_pixbuf_get_rowstride(pixbuf2); ppix1 = gdk_pixbuf_get_pixels(pixbuf1); // input pixel array ppix2 = gdk_pixbuf_get_pixels(pixbuf2); // output pixel array ww15 = 0.5 * ww1; hh15 = 0.5 * hh1; ww25 = 0.5 * ww2; hh25 = 0.5 * hh2; a = cosf(angle); // affine transform coefficients b = sinf(angle); d = - sinf(angle); e = cosf(angle); for (py2 = 0; py2 < hh2; py2++) // loop through output pixels for (px2 = 0; px2 < ww2; px2++) { px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15; // (px1,py1) = corresponding py1 = d * (px2 - ww25) + e * (py2 - hh25) + hh15; // point within input pixels px0 = int(px1); // pixel containing (px1,py1) py0 = int(py1); if (px1 < 0 || px0 >= ww1-1 || py1 < 0 || py0 >= hh1-1) { // if outside input pixel array pix2 = ppix2 + py2 * rs2 + px2 * nch; // output is acolor pix2[0] = pix2[1] = pix2[2] = acolor; continue; } pix0 = ppix1 + py0 * rs1 + px0 * nch; // 4 input pixels based at (px0,py0) pix1 = pix0 + rs1; pix2 = pix0 + nch; pix3 = pix0 + rs1 + nch; f0 = (px0+1 - px1) * (py0+1 - py1); // overlap of (px1,py1) f1 = (px0+1 - px1) * (py1 - py0); // in each of the 4 pixels f2 = (px1 - px0) * (py0+1 - py1); f3 = (px1 - px0) * (py1 - py0); red = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0]; // sum the weighted inputs green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1]; blue = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2]; if (alpha) tran = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3]; // 4th color = alpha if (red == acolor && green == acolor && blue == acolor) { // avoid acolor in image if (blue == 0) blue = 1; else blue--; } pix2 = ppix2 + py2 * rs2 + px2 * nch; // output pixel pix2[0] = int(red); pix2[1] = int(green); pix2[2] = int(blue); if (alpha) pix2[3] = int(tran); } return pixbuf2; } /********************************************************************************/ // 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; 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(cchar *text, cchar *font, int fontsize, GtkWidget *widget) { char 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 (! 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; window = gtk_widget_get_window(widget); gdk_window_get_root_coords(window,px,py,&rpx,&rpy); gdk_device_warp(mouse,screen,rpx,rpy); // wayland fails FIXME 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; gdk_device_get_position(mouse,&screen,&px,&py); // get mouse position gtk_window_move(GTK_WINDOW(window),px,py); return; } /******************************************************************************** xstring class (dynamic length string) xstring(int cc = 0) default constructor xstring(cchar * ) string constructor xstring(const xstring &) copy constructor ~xstring() destructor operator cchar * () const { return xpp; } conversion operator (cchar *) xstring operator= (const xstring &) operator = xstring operator= (cchar *) operator = friend xstring operator+ (const xstring &, const xstring &) operator + (catenate) friend xstring operator+ (const xstring &, cchar *) operator + friend xstring operator+ (cchar *, const xstring &) operator + void insert(int pos, cchar *string, int cc = 0) insert substring at pos (expand) void overlay(int pos, cchar *string, int cc = 0) replace substring (possibly expand) static void getStats(int & tcount2, int & tmem2) get statistics void validate() const verify integrity int getcc() const { return xcc; } return string length *********************************************************************************/ #define wmiv 1648734981 int xstring::tcount = 0; // initz. static members int xstring::tmem = 0; xstring::xstring(int cc) // new xstring(cc) { wmi = wmiv; xmem = (cc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory xcc = 0; // string cc = 0 *xpp = 0; // string = null } xstring::xstring(cchar *string) // new xstring("initial string") { wmi = wmiv; xcc = 0; if (string) xcc = strlen(string); // string length xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; // allocate if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory *xpp = 0; if (xcc) strcpy(xpp,string); // copy string } xstring::xstring(const xstring & xstr) // new xstring2(xstring1) { wmi = wmiv; xmem = xstr.xmem; // allocate same length xcc = xstr.xcc; xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tcount++; // incr. object count tmem += xmem; // incr. allocated memory strcpy(xpp,xstr.xpp); // copy string } xstring::~xstring() // delete xstring { validate(); delete[] xpp; // release allocated memory xpp = 0; tcount--; // decr. object count tmem -= xmem; // decr. allocated memory if (tcount < 0) zappcrash("xstring count < 0",null); if (tmem < 0) zappcrash("xstring memory < 0",null); if (tcount == 0 && tmem > 0) zappcrash("xstring memory leak",null); } xstring xstring::operator= (const xstring & xstr) // xstring2 = xstring1 { validate(); xstr.validate(); if (this == &xstr) return *this; xcc = xstr.xcc; if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } strcpy(xpp,xstr.xpp); // copy string return *this; } xstring xstring::operator= (cchar *str) // xstring = "some string" { validate(); xcc = 0; *xpp = 0; if (str) xcc = strlen(str); if (xmem < xcc+1) { delete[] xpp; // expand memory if needed tmem -= xmem; xmem = (xcc & 0x7ffffff8) + 8; // mod 8 length xpp = new char[xmem]; if (! xpp) zappcrash("xstring NEW failure",null); tmem += xmem; } if (xcc) strcpy(xpp,str); // copy string return *this; } xstring operator+ (const xstring & x1, const xstring & x2) // xstring1 + xstring2 { x1.validate(); x2.validate(); xstring temp(x1.xcc + x2.xcc); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings strcpy(temp.xpp + x1.xcc, x2.xpp); temp.xcc = x1.xcc + x2.xcc; temp.validate(); return temp; } xstring operator+ (const xstring & x1, cchar *s2) // xstring + "some string" { x1.validate(); int cc2 = 0; if (s2) cc2 = strlen(s2); xstring temp(x1.xcc + cc2); // build temp xstring strcpy(temp.xpp,x1.xpp); // with both input strings if (s2) strcpy(temp.xpp + x1.xcc, s2); temp.xcc = x1.xcc + cc2; temp.validate(); return temp; } xstring operator+ (cchar *s1, const xstring & x2) // "some string" + xstring { x2.validate(); int cc1 = 0; if (s1) cc1 = strlen(s1); xstring temp(cc1 + x2.xcc); // build temp xstring if (s1) strcpy(temp.xpp,s1); // with both input strings strcpy(temp.xpp + cc1, x2.xpp); temp.xcc = cc1 + x2.xcc; temp.validate(); return temp; } void xstring::insert(int pos, cchar *string, int cc) // insert cc chars from string at pos { // pad if pos > xcc or cc > string validate(); int scc = strlen(string); if (! cc) cc = scc; int pad = pos - xcc; if (pad < 0) pad = 0; if (xmem < xcc + cc + pad + 1) // allocate more memory if needed { int newmem = xcc + cc + pad; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pad) memset(xpp+xcc,' ',pad); // add blanks up to pos for (int ii = xcc + pad; ii >= pos; ii--) // make hole for inserted string *(xpp+ii+cc) = *(xpp+ii); if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null xcc += cc + pad; // set new length xpp[xcc] = 0; validate(); } void xstring::overlay(int pos, cchar *string, int cc) // overlay substring { validate(); int scc = strlen(string); if (! cc) cc = scc; if (xmem < pos + cc + 1) // allocate more memory if needed { int newmem = pos + cc; newmem = (newmem & 0x7ffffff8) + 8; // mod 8 length char * xpp2 = new char[newmem]; if (! xpp2) zappcrash("xstring NEW failure",null); strcpy(xpp2,xpp); // copy to new space delete[] xpp; xpp = xpp2; tmem += newmem - xmem; xmem = newmem; } if (pos > xcc) memset(xpp+xcc,' ',pos-xcc); // add blanks up to pos if (cc > scc) memset(xpp+pos+scc,' ',cc-scc); // blank pad if cc > string if (cc < scc) scc = cc; strncpy(xpp+pos,string,scc); // insert string, without null if (pos + cc > xcc) xcc = pos + cc; // set new length xpp[xcc] = 0; validate(); } void xstring::getStats(int & tcount2, int & tmem2) // get statistics { tcount2 = tcount; tmem2 = tmem; } void xstring::validate() const // validate integrity { if (wmi != wmiv) zappcrash("xstring bad wmi",null); if (xmem < xcc+1) zappcrash("xstring xmem < xcc+1",null); if (xcc != (int) strlen(xpp)) zappcrash("xstring xcc != strlen(xpp)",null); } /******************************************************************************** Vxstring class (array or vector of xstring) Vxstring(int = 0); constructor ~Vxstring(); destructor Vxstring(const Vxstring &); copy constructor Vxstring operator= (const Vxstring &); operator = xstring & operator[] (int); operator [] const xstring & operator[] (int) const; operator [] (const) int search(cchar *string); find element in unsorted Vxstring int bsearch(cchar *string); find element in sorted Vxstring int sort(int nkeys, int keys[][3]); sort elements by designated subfields int sort(int pos = 0, int cc = 0); sort elements by 1 subfield (cc 0 = all) int getCount() const { return nd; } get current count Sort with keys: keys[N][0] = key position (0 based) of key N keys[N][1] = key length keys[N][2] = sort type: 1/2 = ascending/descending, 3/4 = same, ignoring case *********************************************************************************/ Vxstring::Vxstring(int ii) // constructor { pdata = 0; nd = ii; if (nd) pdata = new xstring[nd]; if (nd && !pdata) zappcrash("Vxstring NEW fail",null); } Vxstring::~Vxstring() // destructor { if (nd) delete[] pdata; pdata = 0; nd = 0; } Vxstring::Vxstring(const Vxstring & pold) // copy constructor { pdata = 0; nd = pold.nd; // set size if (nd) pdata = new xstring[nd]; // allocate memory if (nd && !pdata) zappcrash("Vxstring NEW fail"); for (int ii = 0; ii < nd; ii++) pdata[ii] = pold[ii]; // copy defined elements } Vxstring Vxstring::operator= (const Vxstring & vdstr) // operator = { if (nd) delete[] pdata; // delete old memory pdata = 0; nd = vdstr.nd; if (nd) pdata = new xstring[nd]; // allocate new memory if (nd && !pdata) zappcrash("Vxstring NEW fail",null); for (int ii = 0; ii < nd; ii++) pdata[ii] = vdstr.pdata[ii]; // copy elements return *this; } xstring & Vxstring::operator[] (int ii) // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } const xstring & Vxstring::operator[] (int ii) const // operator [] { static xstring xnull(0); if (ii < nd) return pdata[ii]; // return reference zappcrash("Vxstring index invalid %d %d",nd,ii,null); return xnull; } int Vxstring::search(cchar *string) // find element in unsorted Vxstring { for (int ii = 0; ii < nd; ii++) if (strmatch(pdata[ii],string)) return ii; return -1; } int Vxstring::bsearch(cchar *string) // find element in sorted Vxstring { // (binary search) int nn, ii, jj, kk, rkk; nn = nd; if (! nn) return 0; // empty list ii = nn / 2; // next element to search jj = (ii + 1) / 2; // next increment nn--; // last element rkk = 0; while (1) { kk = strcmp(pdata[ii],string); // 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; } } } } static int VDsortKeys[10][3], VDsortNK; int Vxstring::sort(int NK, int keys[][3]) // sort elements by subfields { // key[ii][0] = position int NR, RL, ii; // [1] = length HeapSortUcomp VDsortComp; // [2] = 1/2 = ascending/desc. // = 3/4 = + ignore case NR = nd; if (NR < 2) return 1; RL = sizeof(xstring); if (NK < 1) zappcrash("Vxstring::sort, bad NK",null); if (NK > 10) zappcrash("Vxstring::sort, bad NK",null); VDsortNK = NK; for (ii = 0; ii < NK; ii++) { VDsortKeys[ii][0] = keys[ii][0]; VDsortKeys[ii][1] = keys[ii][1]; VDsortKeys[ii][2] = keys[ii][2]; } HeapSort((char *) pdata,RL,NR,VDsortComp); return 1; } int VDsortComp(cchar *r1, cchar *r2) { xstring *d1, *d2; cchar *p1, *p2; int ii, nn, kpos, ktype, kleng; d1 = (xstring *) r1; d2 = (xstring *) r2; p1 = *d1; p2 = *d2; for (ii = 0; ii < VDsortNK; ii++) // compare each key { kpos = VDsortKeys[ii][0]; kleng = VDsortKeys[ii][1]; ktype = VDsortKeys[ii][2]; if (ktype == 1) { nn = strncmp(p1+kpos,p2+kpos,kleng); if (nn) return nn; continue; } else if (ktype == 2) { nn = strncmp(p1+kpos,p2+kpos,kleng); if (nn) return -nn; continue; } else if (ktype == 3) { nn = strncasecmp(p1+kpos,p2+kpos,kleng); if (nn) return nn; continue; } else if (ktype == 4) { nn = strncasecmp(p1+kpos,p2+kpos,kleng); if (nn) return -nn; continue; } zappcrash("Vxstring::sort, bad KEYS sort type",null); } return 0; } int Vxstring::sort(int pos, int cc) // sort elements ascending { int key[3]; if (! cc) cc = 999999; key[0] = pos; key[1] = cc; key[2] = 1; sort(1,&key); return 1; } /******************************************************************************** Hash Table class HashTab(int cc, int cap); constructor ~HashTab(); destructor int Add(cchar *string); add a new string int Del(cchar *string); delete a string int Find(cchar *string); find a string int GetCount() { return count; } get string count int GetNext(int & first, char *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 char 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 char [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(cchar *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(cchar *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() bug",null); // exceed tries2, must not happen return 0; // (table too full to function) } // Find a table entry. int HashTab::Find(cchar *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() bug",null); // cannot happen return 0; } // return first or next table entry int HashTab::GetNext(int & ftf, char *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) printz("%d, %s \n", ii, table + pos); if (table[pos] == -1) printz("%d, deleted \n", pos); } return 1; } /******************************************************************************** class for queue of dynamic strings Queue(int cap); create queue with capacity ~Queue(); destroy queue int getCount(); get current entry count int push(const xstring *entry, double secs); add new entry with max. wait time xstring *pop1(); get 1st entry (oldest) xstring *popN(); get Nth entry (newest) constructor: cap is queue capacity push: secs is max. time to wait if queue is full. This makes sense if the queue is being pop'd from another thread. Use zero otherwise. Execution time: 0.48 microsecs per push + pop on queue with 100 slots kept full. (2.67 GHz Intel Core i7) *********************************************************************************/ Queue::Queue(int cap) // constructor { int err; err = mutex_init(&qmutex, 0); // create mutex = queue lock if (err) zappcrash("Queue(), mutex init fail",null); qcap = cap; // queue capacity ent1 = entN = qcount = 0; // state = empty vd = new Vxstring(qcap); // create vector of xstring's if (! vd) zappcrash("Queue(), NEW fail %d",cap,null); strcpy(wmi,"queue"); return; } Queue::~Queue() // destructor { if (! strmatch(wmi,"queue")) zappcrash("~Queue wmi fail",null); wmi[0] = 0; mutex_destroy(&qmutex); // destroy mutex qcount = qcap = ent1 = entN = -1; delete vd; vd = 0; return; } void Queue::lock() // lock queue (private) { int err; err = mutex_lock(&qmutex); // reserve mutex or suspend if (err) zappcrash("Queue mutex lock fail",null); return; } void Queue::unlock() // unlock queue (private) { int err; err = mutex_unlock(&qmutex); // release mutex if (err) zappcrash("Queue mutex unlock fail",null); return; } int Queue::getCount() // get current entry count { if (! strmatch(wmi,"queue")) zappcrash("Queue getCount wmi fail",null); return qcount; } int Queue::push(const xstring *newEnt, double wait) // add entry to queue, with max. wait { double elaps = 0.0; int count; if (! strmatch(wmi,"queue")) zappcrash("Queue::push wmi fail",null); lock(); // lock queue while (qcount == qcap) { // queue full unlock(); // unlock queue if (elaps >= wait) return -1; // too long, return -1 status usleep(1000); // sleep in 1 millisec. steps elaps += 0.001; // until queue not full lock(); // lock queue } (* vd)[entN] = *newEnt; // copy new entry into queue entN++; // incr. end pointer if (entN == qcap) entN = 0; qcount++; // incr. queue count count = qcount; unlock(); // unlock queue return count; // return curr. queue count } xstring *Queue::pop1() // get 1st (oldest) entry and remove { xstring *entry; if (! strmatch(wmi,"queue")) zappcrash("Queue::pop1 wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { entry = &(* vd)[ent1]; // get first entry ent1++; // index pointer to next if (ent1 == qcap) ent1 = 0; qcount--; // decr. queue count } unlock(); // unlock queue return entry; } xstring *Queue::popN() // get last (newest) entry and remove { xstring *entry; if (! strmatch(wmi,"queue")) zappcrash("Queue::popN wmi fail",null); lock(); // lock queue if (qcount == 0) entry = 0; // queue empty else { if (entN == 0) entN = qcap; // index pointer to prior entN--; qcount--; // decr. queue count entry = &(* vd)[entN]; // get last entry } unlock(); // unlock queue return entry; } /******************************************************************************** Tree class, tree-structured data storage without limits Store any amount of data at any depth within a tree-structure with named nodes. Data can be found using an ordered list of node names or node numbers. Tree(cchar *name); create Tree ~Tree(); destroy Tree int put(void *data, int dd, char *nodes[], int nn); put data by node names[] int put(void *data, int dd, int nodes[], int nn); put data by node numbers[] int get(void *data, int dd, char *nodes[], int nn); get data by node names[] int get(void *data, int dd, int nodes[], int nn); get data by node numbers[] A Tree can also be thought of as an N-dimensional array with the cells or nodes having both names and numbers. Data can be stored and retrieved with a list of node names or numbers. The nodes are created as needed. Nodes are sparse: those with no data do not use any memory. Node numbers are created when data is stored by node numbers. Node numbers are also added when data is stored by node names: the numbers are assigned sequentially from zero at each level in the tree. nodes array of node names or numbers nn no. of nodes used for a put() or get() call dd data length to put, or the max. data length to get (i.e. the space available) put() returns 1 if successful and crashes with a message if not (out of memory) get() returns the length of the data retrieved (<= dd) or 0 if not found there is no assumption that the data is character data and no null is appended data returned has the same length as the data stored (if dd arg is big enough) example: char *snodes[10]; // up to 10 node names (max tree depth) int knodes[10]; // up to 10 node numbers char mydata[20]; // data length up to 20 Tree *mytree = new Tree("myname"); // create Tree snodes[0] = "name1"; snodes[1] = "name2"; mytree->put("string1",8,snodes,2); // put "string1" at ["name1","name2"] snodes[1] = "name3"; mytree->put("string22",9,snodes,2); // put "string22" at ["name1","name3"] snodes[1] = "name2"; mytree->get(mydata,20,snodes,2); // get data at ["name1","name2"] ("string1") knodes[0] = 0; knodes[1] = 0; mytree->get(mydata,20,knodes,2); // get data at [0,0] ("string1") knodes[1] = 1; mytree->get(mydata,20,knodes,2); // get data at [0,1] ("string22") When data was stored at ["name1","name2"] these node names were created along with the corresponding node numbers [0,0]. When data was stored at ["name1","name3"] a new node "name3" was created under the existing node "name1", and assigned the numbers [0,1]. Benchmark Execution times: 2.67 GHz Intel Core i7 Tree with 1 million nodes and average depth of 8 levels (peak 15 levels) put() all data by node names: 2.1 secs get() all data by node names: 1.5 secs put() all data by node numbers: 2.0 secs get() all data by node numbers: 0.72 secs Internal code conventions: - caller level is node 0, next level is node 1, etc. - node names and numbers in calls to get() and put() refer to next levels - number of levels = 1+nn, where nn is max. in calls to put(...nodes[], nn) *********************************************************************************/ #define wmid 1374602859 // integrity check key // constructor Tree::Tree(cchar *name) { wmi = wmid; tname = 0; tmem = 0; tdata = 0; nsub = 0; psub = 0; if (name) { int cc = strlen(name); tname = new char[cc+1]; if (! tname) zappcrash("Tree, no memory",null); strcpy(tname,name); } } // destructor Tree::~Tree() { if (wmi != wmid) zappcrash("not a Tree",null); if (tname) delete [] tname; tname = 0; if (tmem) zfree(tdata); tmem = 0; tdata = 0; for (int ii = 0; ii < nsub; ii++) delete psub[ii]; if (psub) zfree(psub); nsub = 0; psub = 0; } // put data by node names[] int Tree::put(void *data, int dd, char *nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) zfree(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // put data by node numbers[] int Tree::put(void *data, int dd, int nodes[], int nn) { Tree *tnode; if (wmi != wmid) zappcrash("not a Tree",null); tnode = make(nodes,nn); if (tnode->tdata) zfree(tnode->tdata); tnode->tdata = new char[dd]; if (! tnode->tdata) zappcrash("Tree, no memory",null); tnode->tmem = dd; memmove(tnode->tdata,data,dd); return 1; } // get data by node names[] int Tree::get(void *data, int dd, char *nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // get data by node numbers[] int Tree::get(void *data, int dd, int nodes[], int nn) { Tree *tnode = find(nodes,nn); if (! tnode) return 0; if (! tnode->tmem) return 0; if (dd > tnode->tmem) dd = tnode->tmem; memmove(data,tnode->tdata,dd); return dd; } // find a given node by names[] Tree * Tree::find(char *nodes[], int nn) { int ii; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strmatch(nodes[0],psub[ii]->tname)) break; if (ii == nsub) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find a given node by numbers[] Tree * Tree::find(int nodes[], int nn) { int ii = nodes[0]; if (ii >= nsub) return 0; if (! psub[ii]) return 0; if (nn == 1) return psub[ii]; return psub[ii]->find(&nodes[1],nn-1); } // find or create a given node by names[] Tree * Tree::make(char *nodes[], int nn) { int ii; Tree **psub2; for (ii = 0; ii < nsub; ii++) if (psub[ii]->tname && strmatch(nodes[0],psub[ii]->tname)) break; if (ii == nsub) { psub2 = new Tree * [nsub+1]; if (! psub2) zappcrash("Tree, no memory",null); for (ii = 0; ii < nsub; ii++) psub2[ii] = psub[ii]; delete [] psub; psub = psub2; nsub++; psub[ii] = new Tree(nodes[0]); if (! psub[ii]) zappcrash("Tree, no memory",null); } if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // find or create a given node by numbers[] Tree * Tree::make(int nodes[], int nn) { Tree **psub2; int ii, jj; ii = nodes[0]; if ((ii < nsub) && psub[ii]) { if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } if (ii >= nsub) { psub2 = new Tree * [ii+1]; if (psub2 == null) zappcrash("Tree, no memory",null); for (jj = 0; jj < nsub; jj++) psub2[jj] = psub[jj]; for (jj = nsub; jj < ii; jj++) psub2[jj] = 0; delete [] psub; psub = psub2; nsub = ii + 1; } psub[ii] = new Tree("noname"); if (! psub[ii]) zappcrash("Tree, no memory",null); if (nn == 1) return psub[ii]; return psub[ii]->make(&nodes[1],nn-1); } // dump tree data to stdout (call with level 0) void Tree::dump(int level) { cchar *name; if (! tname) name = "noname"; else name = tname; printz("%*s level: %d name: %s subs: %d mem: %d \n", level*2,"",level,name,nsub,tmem); for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->dump(level+1); } // get node counts and total data per level // level 0 + nn more levels, as given in calls to put(...nodes[],nn) // caller must initialize counters to zero void Tree::stats(int nn[], int nd[]) { nn[0] += 1; nd[0] += tmem; for (int ii = 0; ii < nsub; ii++) if (psub[ii]) psub[ii]->stats(&nn[1],&nd[1]); } dkopp/dkopp.png0000644000175000017500000000541413774024600012465 0ustar micomicoPNG  IHDR00WsRGBbKGD pHYs  ~tIME ѪlA IDAThkLWU@faFuzijFFMWP61O&Ƭm6lɶҤش-u6"ơ trQ0 ȠtL9?9'D%i[o2XS?Y@F]b2*++fqgOOO3PX ԙLڭ[}vjdddVBLipa'w+Ǐf5ĉ&M6Q\\L^^4ɚm$+,fr9=o.huݵ@ǣjZK/> S.`4E1 t:sccㅞ-"1g2:ĪU())IxrFq&S+l(JLeoxػwΤN9m۶ ~x#"`e^ߔe oh晀H~߾}/"k֬A&%L&cBL{R`PQ&6?*IKjSU@D^$41عs'jn'}jkjjOn1|QI"D$H6 \ cc@dYi(yC.2ish*V\{پgySOhf-QKSGd:HbkIexzz[u/K,~kz{{BT*|UR~QmA_EZ4{ȄeQ) #dB23)++0_B`={FqVOj>D"y*~FFFzTT_CӧOhѢDj5/qٌ ~? #=@YYYd<=xݞP9bôۺukb@ >x1I.m:ѬRuhor(W_}voOvbgΟ^k/҉; | \>%E%?vqQQ^^.{'n޼)Q]]- D}} v-śo):;;E{{8q0#-DU(G"4ރ*~0ŏQ\B"bv{ҥܸqcʻ˗5Ysaѳ)666:t;zGt]xΓG"R.9<|*]W 9c/0ʟ fW 88~x3PF ݭ1P(+|`"ɣ%єaz*r^x2eMgZ[lA()(3(RR}ǃfrȑ摑*Ou1`X/*FҠLG.F ztΝ>1mjFcպu& Cb@oo/^>:;;c?+xA/%x3Qy huufIENDB`dkopp/doc/0000755000175000017500000000000013774024600011403 5ustar micomicodkopp/doc/copyright0000644000175000017500000000213613774024600013340 0ustar micomicoFormat: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: dkopp Upstream-Contact: Michael Cornelison Source: https://kornelix.net Files: * Copyright: Copyright 2007-2021 Michael Cornelison License: GPL-3+ License: GPL-3+ 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. . 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. . You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the complete text of the GNU General Public License can be found in "/usr/share/common-licenses/GPL-3". dkopp/doc/README0000644000175000017500000000272713774024600012273 0ustar micomicoInstallation of dkopp from source tarball Building dkopp requires the following packages: g++ the Gnu C++ compiler and linker libgtk3.0-dev GTK graphics library (GUI base) Build and install dkopp as follows: 1. Download the tar file (dkopp-N.N.tar.gz) to Desktop 2. Open a terminal window 3. $ cd Desktop # go to Desktop 4. $ tar -xzf dkopp-N.N.tar.gz # unpack to ./dkopp 5. $ cd dkopp # go there 6. $ make # build program 7. $ sudo make install # install program Missing dependencies will cause error messages in step 6. Install these from your repository and repeat step 6. Step 7 moves all files to the following locations: /usr/bin/dkopp binary executable /usr/share/dkopp/ icons, images /usr/share/doc/dkopp/ user guide, README ... To run dkopp, the programs growisofs and either mkisofs or genisoimage are needed. These are normally included in Linux distributions. If needed, use your package manager to install them. The program wodim is also recommended (currently used only to reset a hung DVD drive). Normally dkopp is run as root so that DVDs can be mounted. Use "sudo dkopp". Please review the user guide (Help menu) before trying dkopp. NOTES FOR PACKAGE BUILDERS: If $PREFIX is defined, files go there instead of /usr. If $DESTDIR is also defined, files go to $DESTDIR$PREFIX. dkopp/doc/dkopp.man0000644000175000017500000000430413774024600013216 0ustar micomico.TH DKOPP 1 2010-10-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/doc/changelog0000644000175000017500000000351113774024600013255 0ustar micomicodkopp Change Log ================ 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 + make appimage package 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). Therefore 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 has improved the reliability of DVD mounting. dkopp/Makefile0000644000175000017500000000352313774024600012301 0ustar micomico# dkopp make file # defaults for parameters that may be pre-defined CXXFLAGS += -Wall -g -rdynamic -O2 -Wno-format-truncation -Wno-stringop-truncation 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 \ # target install directories PREFIX ?= /usr BINDIR = $(PREFIX)/bin DOCDIR = $(PREFIX)/share/doc/dkopp MANDIR = $(PREFIX)/share/man/man1 MENUDIR = $(PREFIX)/share/applications SHAREDIR = $(PREFIX)/share/dkopp DATADIR = $(SHAREDIR)/data IMAGEDIR = $(SHAREDIR)/images ICONDIR = $(SHAREDIR)/icons install: dkopp uninstall mkdir -p $(DESTDIR)$(BINDIR) mkdir -p $(DESTDIR)$(DOCDIR) mkdir -p $(DESTDIR)$(MANDIR) mkdir -p $(DESTDIR)$(MENUDIR) mkdir -p $(DESTDIR)$(DATADIR) mkdir -p $(DESTDIR)$(IMAGEDIR) mkdir -p $(DESTDIR)$(ICONDIR) cp -f dkopp $(DESTDIR)$(BINDIR) cp -f -R doc/* $(DESTDIR)$(DOCDIR) cp -f -R data/* $(DESTDIR)$(DATADIR) cp -f -R images/* $(DESTDIR)$(IMAGEDIR) # man page gzip -fk -9 doc/dkopp.man mv -f doc/dkopp.man.gz $(DESTDIR)$(MANDIR)/dkopp.1.gz # desktop file and icon cp -f dkopp.desktop $(DESTDIR)$(MENUDIR) cp -f dkopp.png $(DESTDIR)$(ICONDIR) uninstall: rm -f $(DESTDIR)$(BINDIR)/dkopp rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/dkopp.* rm -f $(DESTDIR)$(MENUDIR)/dkopp.* rm -f -R $(DESTDIR)$(SHAREDIR) clean: rm -f dkopp rm -f *.o dkopp/images/0000755000175000017500000000000013774024600012103 5ustar micomicodkopp/images/media-pause.png0000644000175000017500000000641113774024600015005 0ustar micomicoPNG  IHDR00W IDATxZ pU>' H @D(JjŒo3ֱt,:u(TmCQ,VE@ CF$DB cs=7pR:ݙ朳{wO.]I]M˧ gi ô/hGŷlM#ڵDK"vڿhojFF7ħ d^%6H/&p%:s'+%:υtާ@]L{v'Ay P]&pThҞhBi=9A đ8WgT943 ٽ="nѽlQ{+ַɲsq$N-SA m!ԷB{6(%1|& O.!}T;+ @gkze-m PNY9d~qyM;.^ېSykt+UC*=S :o]ܑ7=!I 2vJju==^YAVo|Nzz.Bl]!t) W|Q) sSyˀu!MbZ~n!srmY=_(߮ǒS,aߡ݁#I"wĠEU*1"ttnxP=~"~űc#%Hb)S0"/=Q-̟V, L&ꞔQ+ź7HIժ0ؠJOra" sRQ'("D9n]5ڈ.GZzܘ]}A\ԗ Z Y[։6hS9+A8sDgv+^ScamCEciz m8҈&>a$7ݱ`7ze |{Kɾa,˜3$0T0"ISP薯4({OOEöc SOL;VȦ=&Qxĉ"\L 1,3Q>7r[m>$|}`[Ol׷K@HNjΉ-%rIZ02 @r$ࡷ{0!!EB#*,qfT6-,<6 6WP}[XBd7**r jC1<(m8*ӡAJHIc]<7g%`C+Łz-!"Ȃ A7.ؽZ%mqD} ȹ 'Q]("˧ [Uk}8ݞyc_]wT?Ԏ*xTsa^qz}+mI!w"~V)qg2>xxc=SUZӉo6UAƶF3$ϨMQ0=z'Ơ3S0H#h3$e4{RBօ(::V>)'8xSy\ҙmzZ:|DXNczƭ@Mߐ^N ;Kh%;$!0 *?XNU~G5f(Ř4j6 8B :ĸdF0:@)\JhOցVi^3f՟b`߇հ{O0w11ƠS=.B:I 7*"7%L0cK! 3|[b X.\(2EJ3 }`X)N;+ z [Vᓰ |`*N=0 ȘiUB&7uDJYa/z10b -!nxTxfDB2Kt8\h\[WTELV-wI,{$,! 񳧁XqF>!VCUʫ%4>ud})۫Qz-Z>L@JB>[P] ?YwHsЂ)E͆(fu9 &Al[& 0@}Ubrd^d!>{8lR :~rl[wJ΅.'їqW(9n ȅ(!7 KH|ViIX” ȸZ%DIM藀cm0hרL 9q|em&E鎈TL 3~9LTBMSo -rQaC]&ȭ20@:N 3m(mH:끠_^&=}Et9>48PFf(U&xa`ttV ћiσ6 \L Z'%da 2?ݶ̈́u(mFbX0"v&gNwШILM_5Cy |(|&n2pE{N&9! # lXyQ-*zS-q6)ɔM eX-AGd/è! I0Q QS ԫ,쇁{  .jFM='j}K9VMzSN^q?k9/trzrĪUkpcU˝|^ۥV~# m?NCni>F|aౙ >u6zу-Ubnw$z dj]"+ˉ]dβˊu"!\};;>f:]=:ZT%r[4 F%D.H.u1tRޤ[ceoR b`p.վMu]U"nZ".CgZ`H)n' úGx<';:mn <3_C<^2JKqҀ\kgM#[nd@yOxY3e姥4/0l)'e,e9;qKkMJq xr[}bIs }Tb.<%YE&<'+m ]|Ɂ$2Lj/E ՝# X*% i>JglnN 9[ 9ȗNNyQZ)5 ١H4s K/&fk/rϬ֢։"WFnnX`I:c>w[4'pYCgI##vf'. I 4^ՠkgX{wO`8NIENDB`dkopp/images/burn.png0000644000175000017500000001012413774024600013555 0ustar micomicoPNG  IHDR00WIDAThY{\gu}Ν}y׻zmN68V(QEДRDARRЇBTDjT -` pl8~Ư{wvuwǽw1)B3νwf~s~Dq]ʷ4~Qwyܶ?6qp|{Ǩտ zG[[&ш&+`Bu^8wztK! "Z Nj1-V`FdC%1D BFLf]9XO/>WyC^hW՝j˥M>ɛ[H⊯XeD.R8S'Fn4sL`ΓOsdĦ3%L~ VhTu4Xߟk^/čFcӽKHN)Rۓ[wRTbhO@`RP`b0gF`PLP*>I}u^m !`i ny@fਂbBPPJʎunJk⨳k̀cI^ßкs!0:8u AҖLW{lH( ZA;Zg^ - _-}|p/@RY7u@Jf>g4AkojcdWYSYL]tZkh\Lq4 ܤ6*~bK&Pjo~CkCa[8]֩<=% XBxaHʹl8CΏE>p_ '0l@"ᚷ^cqE]) i1UAH,8ML j " 2 0M2C=D"R*aLfNwGWu0t T@~/$f(D`/JpN Q9q9&J vҜYw] - 熅}$&~H8hPn.%F $AS ETD(/yt0KyϽFFvIx㍷#s1 &{.몀hu P{ V#B mǬ]sUs+}Nu3iaI˻ b5bs֙+l.f8sɧN-] 1C&TQs$矛$6dA8a0ГVp &EthEb _p:ecR[14;*cZ"A.<40J{Y}8JMAZ٤;cFZVspi" Cmy+޽7 { ?GIb%΀ h܈OmitzUtj}skE&fs~*b@AEy>&!zigϿV{^3 $Ix`o !{v-s6H29-$C XjX%uTL[&''/̲ܲ 7vфP$P'D CsQ$p$sNu+~q/l19|y-@@P4ӳ hb2aˎDZ'7amdJ ZpT00bJ=z1IMZ WDa%k^\߳$^?@9%} ݗ6`JKv8[?sre0AJ l)BeiYK=ِgؼ^BwMfc0]dK91|nڇS!,vE>,YcxV~u6TY 󸝏7T_qh2%;NvF?9GG.Sg_>Y-[Ȼ.m۶+~>׾5:~8TKٷ9l 9A H ' *Կq-nظn͊[qi XPV҈4϶yn 4si}H̜jS)JR+VnMva'''/FÇaoe=ãÃ*e˚-s\әm6yn:OgY~>2L/x4Mcf$I(Jݮ9{ݳg{Ν;lrEt:*"f ,N0zWէ;2wzAPC ?!њg_+D[k5ik@HlML6O̙3c՝N v0i*"gSrtKu#)=cuьOfCV(`[Ir7 ffkAh4ؿ?~aQl(TZZ92cްh k_Q8X`fW\")R,Yegט9]tn2%"QJ8뺢jEi"T&K+4A]1xMhy2!"CDA%D0sBD iaJT)eRVk-RɖeYreF I687R"js1 U"h$S<3st)(@"J/E$bṮR*)8qc)'Q&QRr)IƵJ+e?eJϱx8GS.\L9jWQQFy]"EdLr@D1@F@Jå# th w`)SuGyr)A%H08>pD4 {Hw[m(wRk P\OL&sϠ2I0CNKLz\ԆA4v=ͽ |-a" nI&)K( PG Ky22GYۃq\yzq}T?$!_*h@ZIʩ[6PrR),~H#'= Nǩc2MČ:Ȱ&bNm;Љn¡ֽdig\ u(7Q&hwJxY<$|Xl0b_a10^iUI@u/=b06.Lx:9+9Y,S2zB+-;4@FkwHQ]%74#oDNYruษ4R'aT );UNy2:Q"Im>L_ۡHNڛ(*J)R\Q[V,Lh꜔QīJz5Bʪp竘)(-F|R4u)7#9:ڈ#m}}v]Ij\4 &z4ZD,53 Nsr –\=yM %ى![,u)L?oCN14 z_*bD5U, RřHRn&1EDT2Ȕ|\C HeK B%BauZqI\X |TiOiӱT>9O^3fQ[z4/w_GQ  ϷK<xʢR'+t8s=y,;TwT_mD@7,Uˀ~QHU";D?c%gw ]h!e< J=) }wǗat .`:κCJ 6 Y rEޯl?gK_!:fӆj}z?P)5<+0'6V{J{M鰡t8^{*4#r:!4FRMZ,*XN"3KHVDgZ{d_FYʊuiX+~;[~ﰶU6wKdQŨ,q2\ZIh&$. u~\e~%ed;jaofmO\ #].(m0"p,y͙y/pB_٬;K`9 ,D*Bˁ -VԎ}T?7 mpః?1axI%Z=t*LviI:Fđ'T汁%u#ifsrV=yTt?n({UdEINY%.TzV(bb$练R4p[aݱJQl>-3"`ӴĖv\+7Rf0}JGҥʶ.=^I<\|f7'P Tn{\ L-aN,Ev;/ ?tǶ8uDI ˬR *+$6\ضh; #QF~ne`w!3IENDB`dkopp/images/editjob.png0000644000175000017500000000501713774024600014234 0ustar micomicoPNG  IHDR00WsBIT|dtEXtSoftwarewww.inkscape.org< IDATh՚}pǿ%10yb8UgJSu3N0tp2m1(RRhDg,HH\LqwI\ H o晹}|=/{Mr|FAƘ65(*t(Dtb%Iz*.4iRvRIq1vPzLx1"xDQ V[nI )S4D"Aݴjժ,icFQlhh[JII fΜxucхF{VhSS~ R8dmݺN'M<3Q';l6ۉ+Vd~?R(H$BxT à͛7)S =}:>JQ&M6iS^|H,koV3gر~ڴ'-d7J3fx< ܞh[[ݺ~U1:_(+'M$ xǁ8jjjń oX>y %Pp8yii2cu(=c&Il2λz!"`ZNڻ]wcvbC{4M-ѣGsC?|pY-]*Λ7 ~4;)~rin80_72?ݻwC$F "s9ΝkH?*2ͰGںiK!n;fϞ=~ݺu~/GOPGyyy6L6uww,--f]t8-F<(PUO< "jJTWC^U,zAdB`4I Ѱ.gQusGG}DƘ &fٮ;Z"͙Lc4Z O?P*ZV E6|4̤(RDpL;::,uQ1H?8}TD@tCR08p'2 ̔_T_%}z'#(sg\iY@HJ$ #Mذ1 6&"J8O {U7N=*/+THj%Dgp'Ra_^8:zlX& $78*Eo7˕?vVA׺rHjfRh떱H+#^̬ʋgn}ъVYz!X~/f2h풱|6kD4tY0 ܭ:~|~XŐT/x޷@iݹ^7S(kS2xW Qf4A_6;΅Wx^'HCGzVW,>9eů 2pMw s`S#uQ OXp=Dd&<0,q C*t hd8WH+hN 53}iH.m/`źfo'꿙 ",>gc]Qdbyi >}9!({<.vcxTsŊ-x -G[l&mEqfEEŻh`0kѴ /lD}=h!/r~,ybD,Dg|-6˃z8V16D"Q(J%G1j*_|DπIENDB`dkopp/images/quit.png0000644000175000017500000001137513774024600013602 0ustar micomicoPNG  IHDR00WzTXtRaw profile type exifxڍa({ ?z߼y R*3s+ܯڬ&e7?*!}_ߛB,$ xB }Ի@߯{ς%\j_)$AO湕~qOz?x0Ryi|Ϳ/XyNI~^~ |.}:3ׯOZ߰z10uNs3pM,$Z8$ԧ_Oi ڻ4iVd勇*Yƽ3/hi%T2ӷ_\=;ovi3XιJz[]i׻f!o.2HP˥WEn~c^8/|S }*MC&.=_4x1";/}[t32!fʵ;2"n1y)z[zDBv>1,eE[g֖خnI1m&9ϺT&3D2hwjFK]E{~ ^z4V\gͰZ/װ9}~{(h1uϾ.TRTMQӖ#Nw]2f5X) N`9uhj$Fe24UkԇP4MKqƪN)O JrݮHnvKuTɹbK<@YS\N&)-!/3AaЊ%e6#ݦkN)6NvĀ6lKY}Њh?Jo*k [|ZCa8)#*AL?Y3`jw>gޙ[ӈ)gVSr5£ꑭMx1TQrj-d~5\bsi'a rr)y(hV) |i[3KlcGkH#P^  {/ \Ոs^Vժ18:LsR4ZڥG#Qvau0,(Zaie YX}FUI+I{:zuO9 `H0ۉ-_0j2ܳcWT&Q` Ĩ51fQP>$,} `p0i"Z ۸"{mHd\f&u%<8? š{fF~F&2 wN4-^7rBbR>=6W13M0$j,:O";H ? Vێ{i"l6S4Dng^|k[몛@;,U[2aԴYSY>C;c;-Ԋ]%G7~nGOfߔ۵q#q1Z9S6wjNܢc{ ^={~ esBIT|d IDATh{UyᄏK $XZFh*3 ^Zu:ѿҪ8Q8XBEH#5$r{N8M"3s|yn9'Zx%.؈U tƵ#xc~ ya.[2"ZQg()x w`~-%oR޴%&>b(TQ vAp.|\s:/ƛ\MTKQfnNn섪u2 [ XT:)5zk$@QR)Z&hN0يxCV> #/\Sg'ʒ!S#ͣ(Mj.9=T{;Z >ř-!4[ހ%u[SV;ޭ/ Ed0zj\2S%):yNɪAYTǎGF3YشQΒb& *G9Hw(I+,C65؎6{.M |M,z{y ͨB%M6L9R;Hu|A$oƿX4̢QjR$s InHbl~+cg{q I}.HRMmIAQ9-AV`YnŒ#Hs+ P?}7٦^^@7l\ܚْ@8yxP M%3{k_Ūa= VPbNC:iIAhN#XƝԚ }q/]Ƌp2N[2Q*OwyOwO%5.DO!9UgzdtC.94VF$ĝld&6nad/TJz[2nRᘟmKqN«ū11ڢnroGXvM\ve K9MٲEo˶m|{ߓ|B:=i#ػw!MYZ9xEu.*}U(O u\)WeydRdz(+3'M%\"FgGV^z2M=SpJ`<.96N Iºuj*_/ݺUs!ˤ\"җ$6>NJ~!IfSHf`N8'9+OL A{nVSRYD5UQ뮓qƼc!4Z5yF+q+kVf$_Š5o}J,_"dٲ䭷#-%}m ) iB6T.*шuZ?ws?fbǝJm ttyJc3Z ͺ0p&a>;>: fܬ#K3ÖJw JƏ~VjI&Q"ey}qH:EeQT|flŊ=7۾oSłA]k,H2D`2~%l#./TzꂠfӮ[O|ÞmWPݼصp )cDGs?~[lZ Ps$,]:V7f?m?˓W}[kA۷`8̿mMG:H,~Ae9/ǿu{}/HY`=;x~r:m<>6fX|˖juSgbQBì\޽^l56o|͞~efƧ{=ª8SB&u_=yQN9Tc@?َ?@փ5iS݅׭bE>8 ϭ]kWXqk''4ͪy%ynbշ͒M'꿺Fs?2vIP[^f$xOƿȒ,-5PgE%|!MU4nY_י~I"`ui`gʡa#p+`_8vO6v(mvlhК)CEaqYw@/Θx/^K1t.vUͼ]/lg+2i]'"al:NDu4Tb}bYs1}aP*9S2PjEGϧ?;>0;Ep`G!v'zDv/ lZsKm<Л+T3;.x>92wD}&A?.S^45tv.9/g]b> fɦ 1(=x43(rju:F` |Ǜ3%'%YS88RƷGb';+z̓R L+*k6+0h#'>WO〗]S,7IENDB`dkopp/images/dkopp.png0000644000175000017500000000541413774024600013732 0ustar micomicoPNG  IHDR00WsRGBbKGD pHYs  ~tIME ѪlA IDAThkLWU@faFuzijFFMWP61O&Ƭm6lɶҤش-u6"ơ trQ0 ȠtL9?9'D%i[o2XS?Y@F]b2*++fqgOOO3PX ԙLڭ[}vjdddVBLipa'w+Ǐf5ĉ&M6Q\\L^^4ɚm$+,fr9=o.huݵ@ǣjZK/> S.`4E1 t:sccㅞ-"1g2:ĪU())IxrFq&S+l(JLeoxػwΤN9m۶ ~x#"`e^ߔe oh晀H~߾}/"k֬A&%L&cBL{R`PQ&6?*IKjSU@D^$41عs'jn'}jkjjOn1|QI"D$H6 \ cc@dYi(yC.2ish*V\{پgySOhf-QKSGd:HbkIexzz[u/K,~kz{{BT*|UR~QmA_EZ4{ȄeQ) #dB23)++0_B`={FqVOj>D"y*~FFFzTT_CӧOhѢDj5/qٌ ~? #=@YYYd<=xݞP9bôۺukb@ >x1I.m:ѬRuhor(W_}voOvbgΟ^k/҉; | \>%E%?vqQQ^^.{'n޼)Q]]- D}} v-śo):;;E{{8q0#-DU(G"4ރ*~0ŏQ\B"bv{ҥܸqcʻ˗5Ysaѳ)666:t;zGt]xΓG"R.9<|*]W 9c/0ʟ fW 88~x3PF ݭ1P(+|`"ɣ%єaz*r^x2eMgZ[lA()(3(RR}ǃfrȑ摑*Ou1`X/*FҠLG.F ztΝ>1mjFcպu& Cb@oo/^>:;;c?+xA/%x3Qy huufIENDB`dkopp/images/stop.png0000644000175000017500000001011713774024600013576 0ustar micomicoPNG  IHDR00WUzTXtRaw profile type exifxڍVk& )r q*7xfnRIL)Z-S~ՌvaVqɐAk<<۫C|}<[|!m3@c#z[p{_7^sφ\pyp=wevޠkaAm}l|ů?T{/msٞ_/?/`Iz+9_;g9L͢0qŽyO3@@nNQWk4Bk/fmh96yNDBԒ}2unNN51I߉ιl{5~M7.1M^zbƿ #=H\gyz-Om@@ qF2AAk&>U 'kEkoF ϫ=n ?WkHLE46ޑJ1pz_ֳ ɫO0)'VbuΦj1,x]+%3vJ0m֩<;βAE D4Qaݣ.m8O=#kξ8ao4y/OXv&k3gqYU ǙKgδՖxkw3]C1TּqYX[FΎ9Ԏ4wYn~h3>7]&ۭ :FHat&ie^^ȽoĂMFWq%chϵ&6*0%ze*K6sa?7PPec57D0 (8ηTQ't/k00\g@u"gtBOzm3OSF9 8+4{VxS  -6=Sr.sGKޚcFzf ,>4k Կ Bip8EА:rGbm@ Udc[!,א|;PAڥsC rnj 0HxLcJtI tWBis-ŠJX`zTׁ!31\̭Id" Zg_ &"R"8ې{F9Q `G$% b+ [6sE?T~C 9ӆë .zyTB3;l_u؁ \!1<U#U5d H.P&]siK.I?kCD$E5 M"LdosS~vV(dWp]lqZ0$r}̭mdtnB9#k| $W P!cP<ll]7j6tBi~ #dr'a  [~ 4IBRߦ8aK"{klPRTx&XsW3㉼tő;& SQ 0@qy{G蕆3<Yw-^M F]Ec_ s@:zKs!&vQ85sBIT|d IDATh՚kpTאN. A vGESdZEi ֊S[mMF ~Pt4VX$Z̒H&9{%Kܴ@|y{y}y_)^RRxB\#2O},&Yq?wرʕ+Wv*c+++۔,[Nx%l1_4z\!իWN'1I6VmOh{Wc<,scO:?7-%5VL)KW?ǖNG ̤uB0ͰcNLA@ hAtUE pɋ2`MKE+ T }NCבV3,tU0 Q$./shS&Z-3#O6w?y^IoV7=͊%5>5#wN"Y,߽kZ*CG9gQ<>r-YkV:"+}yHV -'ȼz2> ى/*4ZvI>@`h6# l33^6 g;rF⼹|AZ dEmTjAtEAlw m0дDmbAz>NzOb'?D%Ł4 +X314] w0x3| QIdνwdfՅY3 <_:Qp8|[UL̎Hd60BqXȺyٷ݄)~F-; SBu6ŖFrIq֌tNuW&16qνOr&@HV+D.7M;_A+ zP嵽U\ϣ+ GP^'̯% q$y0B8ҟ#4;0S:YH3.쭡w#-ڎNiO>7h1-3-dHTс$aMKE%? X3ӱDM:iCWH {'~2WF$5hԽ~D8Ӄ ^g Mcs:cAgύY&hH*gg_ɽ%d:cR?{ZyV>KHK ,/+U{97] 29+oaz\!ѣUݻ򆤌`JUWlv""YG(VZ5(TWW***YYJC.3Vb6s7oBje}}}$6Z]]*//opdeN+;$C"bft/*?0]4m#fri" ǘ9ef8R^j;M61aRi*Ie/jm2ЅTyKgBMMx#16gxkBIENDB`dkopp/images/kill.png0000644000175000017500000001431613774024600013551 0ustar micomicoPNG  IHDR00W =iCCPiccxڝSgTS=BKKoR RBTi@숨"q"Ay((6T}7o9g}>F`DdJ<6.'w T @- m@n8P $ B2r22 t%[j;eOv$(S*@@&X`(ʑs`̔`)d` SGE3(xW\!Sd咔Tn!\]x87CP؄ ee3FvD9;:;8:|?E񋖴e /B_TBfgk+ m_ _׃  2r<[&q?.wL'bPGKĹi ˒$ IHk`~B[P. %w߂1w0hْ 4P6h>؀#;x̆P8XBHLC.,UP%BZF8-p<^0 o`A2DX6b"ֈ#Ef!~H0!H "ERd5R#U^9E.!==F~C>@٨jڡ\ Bh G h%ZBѳڋ>G03l0.Bx,c˱b6b#{";!0 $,",'̈́ Ba$nD>1B%+uc[!\H8Ri D:C!d6ٚA% ry;4:yBP)xR@\ RƩjTS5*.Qkmԫ8MfNEhhFyC+:nDw%JaEz=Ca1J~=+&ib3 z9c; _EBZY U| գWUGԨjfj<5rjjwY/i544D4i01VjYYlۜgK߱٣34545Ojr0qpns>Lћ=Ejkh4km8ndn4ר1͘klŸx$dI}S)4ti[3sf-fCZ||L OE57-I\t˝׬P+'Tj֨zu44ii50lmrlll9-/L6u}wϰ0ۡ7G+GcWLor ]3:B:;}rvq;7:$pesø܋DW'\߻9)܎n~}hLڙFY4xx>2yy z[zy~c#9[;vi{o?$L 10(pS_ȯvlvG#(2*IU<- 999-(yr`GryPGTԊ OR%y;mzh􉌘LJfbq4]ڑ#z-ںhT$Fg* Ki\˙S.7:hz4k]BX"\Ҿp骥}˼],OZ޾xEኁ+J_S}Ay1 W XPR$/}uuu맯߾sr}IERaofbC2]Ioot\<s--.zbFmmmMo*VOuw)y}׮zKv#swo}}9Fv~N~:],k@ Ç]FƽMpXy>t(h?8:V܌4/nmImmk9>x{{۱mDI͓eh OM?=vFvflŞ}> uzwq%K/s/\qu'u;w7_uzZ[̞S={M+=; wz˸~+?R{TXqϖ?7:zA?q)iŠ`Љak=x.{>>R/;^XW_FcG^_NVJ3^=~fm;wsw~08姶ANdNL%c3 cHRMz%u0`:o_FbKGD pHYs.#.#x?v vpAg00W IDAThY[]yk>xNǗ+i@UPj*xp*U!(/Qmڇ4(R@"6@!{at0sZ۷!\x!1d~\rp}wرcͩ)n;x@$pխ[~ppDQ!IRKӴJw=3??> L;v@E;^a!qs0 DZ`-RJsL{.cǎ(EZ>CMӄp]BHdY&qEQapZ;hY<̠Rh`bbKKK1EٳDQ<iB4!s/9TUE6E>Goo/ooqzc||ӨjP}}}BPBj4?z/(}=,,//G"㿒$qjT ||صkưe0 )9s <ea``|bb»x&IT*l?xW_šC~9T Q#,ˇ u4ӌ1xZq 2,BTBOO~޽`q\t  @!!igM|!;|Ir mp(ciڃin6M !PTQwز,⮻,˂z 8}4* 2lFaB!ckw:~T*?xGd$i@u]a  Nk{سgۇ!PJQV&&&p9\zQub[gE,_0Mg/oڴͣG8q{'&, tAJ??ߏ^0@)4 [n(v܉͛7C$eLNNb||.]׌!s4MPJb8iY b?}饗tkKKK7&BR J?ADQ$2LP,YTj@ёQuB,b۶meYf .̙3@ZRR)4MZ-Hd&韞!_BcZ#\Epg~ppt^Z>j^@)%I"10 'O⭷BрA,wBP( QJeB)VWWW_䜋 \ 2oj5ʕ+L)}s~zuu9_v]y)y1֑$Aq8qfgg!c rX,P(@UUZ-FhUUA ʆ h `C7әLFT*p#۶ r|Va)"cq ۶C6,˨T*8{,^{5ò,EQ`r ,(#HJ)s]EY`% &B.J-A>+a04|200 ò,r9j5PLJya&Ν;)Jr iiM朿aZv_^^ley,!X8<σ1t:L&lbqq/_Ni?0joƷ$IZݐR:m,#Vˉ$I BqppL=w(3;;_t)9߿˶m4 8f CP@/By0??L&Qr9lڴ Bh:FEdvGL&'гeW .oVGwwIHt8C4ItHlGX9wo?cf_}MQ?r_*yxgfQȝw###_UB!:TAg & Rq貙z9J#@eaH5M!HEcQE$+++?RU4 }̲fLRڙ3N8rr(b0N˲4W*mö6 l<۶TFTU}EQ+_E1JihV4JRj5hEQ:t: uuN`Y֟j'''ş{)pЍCE(JyEQ~u/0 0a=REQ{eY4 181==(y_ ̞<ϻ~Zl6۶]:5?nYVL$jAeaH3#p= Haq6 PBe۶ dYF*8;EJr 眧(m% @ZU4Mە4Sm0 P7rNoƧeYIW圃s(:#y9W.pK=,!眬7qs Yxu"csnv1  @"+0T|-ρ[[/>n$<ٶm7ޤ[i|E$I+IK$A}}A Hmd[E RשeY$!$?`|= ֫ϊ߃$IdYh$Ar6h_Wjs='3 $T&!d@GdYautPOt:%!NɲL|gsF)%q 5|r=nupjiiiJ$S圦iݺ*2H)- !fI0\!n;w@jNRVc,(& ABB-Y\ -k4ЮD1hE4"R:MaB@m?큍zii脷mdER%tEXtcreate-date2008-03-22T12:13:02+00:00͚%tEXtmodify-date2008-03-22T12:13:02+00:00|0IENDB`dkopp/images/clear.png0000644000175000017500000000532213774024600013701 0ustar micomicoPNG  IHDR00WsBIT|d IDAThil1;۱c{m'v GI(DTDBJ PR@Ԧm*(*>RЦ)Qp%M8Ďwwfޫ9H+hyyo3c SynJ͔.|[+cRRQr=p'kg2n+횆h ˥~7m_wL$=BdzWۏV>v˽R)+OgwB8]2(\9X+PV=:ڳv7[F1@Kc&\ }(lگj{?iޮF% 쎉ƜUntƭlL69xⶂ"INhje†'6>Js@(n(Pwt0j9/ojػ80N&lV,MxB9>tW7S9C_D6uOMw‹p TV<{N/!mL)eavElnF:֊06^s7c_ছnseƪ6\8'`U׃BA.@_%<ǻ+V`0vZ>00C-ho?b% qٲ(<#Rv9:$\գSf# v7К e]:l;[5P"R\`4UwM5@Β+?ZSH Q]]p8!9x<-BccoON)z[ք8g& ZZز5Dk"ˡ b#鿽ntO֝eCT/!z" R\Fz10{> 磪!]sm&0KeO:yw4 }0)0n@h6E|1C]lBS[l2恖%wqoL~eh-`@.)82Ҕ(mzʟ䖲u[_]1#͋7^.cP҃p3+Ƽ.i{/m[KYFք9GZ*d F 1C FNA\3k|onj?esI9|F `<]h-a8QK84YpNZw2 PE. 2H-R` o\PR@+ @!&'WghQx#aΫ=HǺހV.PQ0!oS'Y Z  C I70=,en#!;X C/L,BqZZ|C+A+ڜ2*N&FgFs!$Zfm|n*KdۡTs`^ 3 f0IM)JJPZhy>!+YP5cZˈq]!?J:'eBu7S- ;1!h 9r*hȔ0F ]gc t_PLr}6n -*Y{ٕRdv9,m̲A` @xS DA+%|-FԪDntظjor<봳œHxoXi$^4FÀP V#ziQ`V*&>09C4I]Pv8av&3A`s3qw=u~uNĺ8ܴ#,Y ɏHJ@k jU25nG6 ~7\랎d ~`q{E=sCs`nšU wD }σSZ4#X}m^[oEY}ju7 Oq2D+`;E;x By),Ҋr XʿCgXN=೺dWs[UP`,Ce:)uD,VUxAM,*DZݢiV ?D\qq[ʋ 4) %Ӧߴ8*ߴջğ'p 6 9f9w=&,h&쬣> Ok~" AIENDB`dkopp/images/run.png0000644000175000017500000001026313774024600013417 0ustar micomicoPNG  IHDR00WsBIT|djIDAThYypםFB'q]/ `DZwk'xwrVE6kW6fmg&1&B0AB tgzfzx:Ut~~~VWl!Š4& @5rp>h4?LJg >M\dNIUVR5z|yN+, ީn;n9F/]/(|m&@odʹYH$R$ ,!¬۽M@_wq d%q8Mf#UG)IRaA8g)RjOFV5xx܁sKlzw/[HeX1x>=d$Y Rp,٬TeaΑ]|CѲ5O/*%+WBoOM#=׭0yr9-0\n[WT2+7;k^}OOVW5l˫jޥ9sNo$@ʲ|x<~|Rڒ/\ U[y~ߜ,9F L6Epww*,jܨ G{-o̜Y{0PUgL'5_l!fEu "!U%D0@@ PcC ƥ`cqݖx Fxwy  Vx0yW|6n]h>TM2Q:i??("9B8 BsN\wu|v{1*j0ux!F%PBA)G(ʊyYJ]$׵m*FϘ>4)c@$*!i ݽr0A)J"D F`%I:[t޽1U?m4cZ!={q_wwFiJ((% vNfM)qçÞ t`l*&_7xX$z]{bOa&1zBep)@Aii)'7xA'`JEN76)>E ߸W;yI܅xB|7w70Dzd٨H7 ;g+O~ir۶i7[‰jZCB!(\ }!\oݟ4 $3G&f[{vE*7JP>t9P3-I i߫ I q×_|%^pIL3Z&9ܲZI8}:䄚?'6"S-gc咕g:)J F!ʵ^ttA)C~̙ ?!2ɭv֡/#Gx/oo\-[P ^Ha0N@Ml=MضmLcyvK h\R2% ADEinv.j;P2i懗^C{nuk\ʲѲbRPg˦9&x|h!nN{pW{˝nVoZWYUgv{P< l l i >BC É'Fo=cj+gB͊BQ5xBE4Ie=]~:-fݍ_o{5`60cZQf{Nxb Nϣ V| u#ɐe cY);΁#>% ŠZƿ}".) K* z0+H}(}` =񸌫ABJ9 LȭH H  ; P1ɰ<:zAY^o .wHƼET -B5d٬~6jl1Cb&YG y69P^|; KNqd;P4CsZ U zz3tz+k&@)Aaz:^ŮG*L@Tڬ-G}O@}n؝RƬ>ϹՂHDJc'w|՛Nc_f˓kfL P  q Fz-c /k7zB:/c TH2V2AQ zKߩ;t~mq,cAnK7UNy53Ǐbq`@(enn7a{%U^t9f4|Rj䛟i+"PX ,6فcU)TU@@Md*O ŕ!%2 V\?p AJBВcRi  ro._Y1q}o\ #ǣ~ΩX{b2 PJa2)Q2PJnn YR!%:pz02k("c]i *4At:cFu;fp 6D]Lͳ"/ RdV/MѠ %"pK``33A K&M~1*hjjJO^!i˯Nݰa] ) P -m5O @c*7S?۷?$nw< <4UriԫRoZzZ7.YxW` xvRB w뎿6~z!?I0qNXlXzt{햿in>u'6ۮ7;/py^USA@/>~kH*ڐ6#6>tO@A\K\罩Q t=@sڲʙ Nv4qFE]W>j@OwNUzEiKv~G߽O)II`٣O8&BඓG>( |^{y'eg/7b箷n0hkςegOihXK?5[1Qw꛴ȠNPٵېM^yIF`<9k;ߤ3dlԷXwF d.F] 8x}($1;o6#+P!Lq~hj8]v?jffMKKPGh4p<t*_^n,ϗ4IR]n'1tѸV#h{c Y,D5%xg)CyK`_ 0'IENDB`dkopp/images/dkopp-jobedit.jpg0000644000175000017500000034741213774024600015353 0ustar micomicoJFIF(ExifMM*bj(1ri%gnome-screenshot0231/Ơ0100Fotoxx:trim/rotate|sharpen|trim/rotate|resize|NE0Photoshop 3.08BIMfotoxx,  http://ns.adobe.com/xap/1.0/ 2160 3840 0 C   %# , #&')*)-0-(0%()(C   ((((((((((((((((((((((((((((((((((((((((((((((((((('" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?BG8>.cKծ'&*=3k^H!5}&~6gI _?C>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾm>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾm>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾmQײ$2N"`}} _??vk_8C%C:4r]`AXKo߁45?ƾm>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾm>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾm>j|5ChvkG. sPk(/]~ _?Ko߁45?ƾm>j|5ChvkJ>6g_6Q@M[|kv5Zmi ՜5$C¾?g ؑ̊Jig"(((((((((((((((((((((((((((((((((((((((((((((((((((G')]g |s_bxEmg |wI((((((((((((ޗm~o'H-!2偸mGs Y#>ki 7[8AIb` ,2te> m-q 9"$r3:ӾkrthgRV/__rARKh{[ݝV33(؁TSHksqZl|0⦛Q\ŤE\CvF1.-.=6#]{lzFeg\jxܬFq թ57O|2R7!L5eq9 I= E-ngVS)] iPiZc3iu" C&&s;8!HKr@jBV;kx#E QޥY1GY?n8tc1\&.oy|cSp'$ t29[C  ݁~bjz摧Ŧo-!g׏5m5 J[SX)d?3C-Q@Q@Q@Q@Q@Q@Q@Wuch伝V  (.i]j1E>X!8Rx8>g,/%XX89`T-.dI H2Lt<>q-l90ʷD'#(Q@^ԒΦq$$`%z^o4ZL&t0q{j:tg'$ 8=G4Ms=oܓ esg=}>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 -wSvE]SB쨌BzIu?j?o }iooo!F Y>*ͭ/4Ow%) KAu?o xOھѢ>.ſ.u?jFfNZMirӨ`ޘZ#л;Q]A<)h@<[BG -wSv5mZK{$YX \qZ4w -wSvл;W4PxOڏ@<[B_hs,αwcu$_xOڏ@<[B_d[\Y%.+J[+{H{y6 =A -wSvл;W5  fkiYFл;Q] Z-Ny^$}sz8ZTw -wSvл;W5FҮek1M2`4 -wSvл;Wٖ[JB*Tw -wSvл;W4PxOڏ@<[B_hwvm'xdw>5ſ.u?jFjZlnumm LW'ɠo xOھҵ3Vt[{ɔ>> л;Q]Og.ſ.u?jFл;W i=m ` zb^^OL*: SQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEb^+jo(# [TЅnK'ԩh<o6Ҵn >9f3%ѴY/'y3c!DWd>mٙ O163[xCL:OvnD"ݫHt4m,rZj:olk{x&"T70ccdͧ[IwS"lr+Rv{5-gpD)2<5xV-bkp.Q.{{SDžt>ϩ?<; =xHe٭̋i$W2 ].7wwrKg>R 7–e̗p\\k˅CfwAg>sm!N3)$E6G=:yWqxKU9lϴS-To4QCmzximWu?' Ԉm k~zyzτ4RL֗.d{;uFcYִ+BJ2;*Qnnn4Oqs+zpGf5O滘4KX5Xi[ PZBA@sϩsSNPy.% ǠG__r/<;hMi$3T=8S֏澻QIy&?F[Ƣ񎛪. ?ɕDb`@;X("cB(PS5o]4f62_Pǟ=+m\V_K`_G3E>UĶ[!..N:n9$KxlĶ?1d@(H#u_`1mo&CGɜOz|"cW.no!L׵ڏGC&ujW;In lVjzԾ  buq#H4/JSgLzmVPH!Xh=>] ؄A;}s/^})اj3#vҤ ہ$6G7͓Kewqnn{5q맵mJ/37h -nGN𽖝=שCo$3} 7@o<~u~k6Ave :oCf>yoMJOuͿI QO5~$m3U5rXUmL^uZ\kyyoDѺfZx3NQVO4[%wg?s=P}2^{[I-̂0lNv`9Ȭm[5׭L2aV;kZ_u\stf*jxw\R}fST,wpamɁT5+ ^$TEbW#"cztoU]%)X~p҈AvcaMƏ-#J۳0dKSx=sO2&qe:7ڞCUc9uh~ozVP}CCsoj#=kgkc(;+xaխkYc@\ȠG' +t5}. v[Ʊ閗K-Н}XϪ@}2ߘ s9ZϨZK+mnZ4J3Bg?yuskx]GclI5=Q쯫j `v)tnK.7 F]HRF:{-|m ;Q+6mJJW;oCMb|?[VaѽIПuuǚ&)|VeoCMbEjfV??(~ aѭiW%w :r2|J6s 6qϒa}kh??wGeF~JL>OR5#te8JJ̭(P_ViHFJʈ,p&CMb c~ <ko/[u؝T~ص>4-E>ֺ#%tybEٳ~54}kkμ 2[f?ti2#n}ӫAH4^oCMbY^' 8OEQܟJb>55Ox+|p[<ʚ]lQN^ uz+(~аZg{gءƏCMrwc+Y!T;npGZkvgBjJ(P_Vk|qX<~t KjQÍq4zءƏCMqjZ6Ww~X>X߅vD="!??(4PoCMbEV??(Rkk_j|+}GVΐ}w*CG1 U(7-SG](P_VhH+}kh??ӯళT2I#P2I5>2kz/!OӇ yu$xYVNy;#߾54}kk'ZX涄Tֽ?׵_ۥ&D2Fhchvdx yNS~54}kjyoCMbEV??(4PoCMbEV??(4PoCMbEV??(4PoCMeǪ:pz's[y!y?_@t0`*aLBEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE{/YuOV&0Ojz~g0,m ;R0rMM-rA77I'lQ'/+vUyFӢ2+$Fh7k7T 6 eͨ\|PКR7ʙ|736ÖJ{ m/4ۿ=vSf|u I'+=¶AY-Av.;՘:n)Y5gEU*-QE3lF{\Fb' #7.uEt_-MR /yTkr:Op~x,̑]0_@q鷌[>,Ei7k"! Gc^u?\ LAW+̠3\9'&cC,Rrwdp5q_+O>pXmw~bKa䷚83bT]Y6ςA.0тwm>sV}k׎ɀ_ ㌜+>BQS]IcK_- DX+ܔPáNzB#qҽ슜NM-6o8qKu=c6p 9-: K{j1e]˪s_CKGk!Rv(|4lmK{hN@^NzV42*ҧl»IY-l侖(4 jť__u]ˌ6 שSUf{Y~e3uo{yY8+tildY]ILֽZ׼%TݲHzn#~9qk:N%aA|`M{_3F{[d9cY>ΜοYv79j JdpltE󇑉 H@Fl~"Xou+al%H!O\6vk9 RTK:} RcN4x?W7Kf}h9ʏGQIPb^O=zݫ!faa;W8VT׻1O1gkoz'M Yu#G;@#}?_0AɮCvҽs$gqcW(mOw :E9mwoO{p_-wKu kK[7.PBŇ's]#(u*2# ל{G̋kzotg5%H%{.!S^9rW}-z> j7:ozuwJ.\y7avp7^kYLq` z_xk 'UVV_6ּ!rg}S}YX^_G^͟2[>ݯhQE(((((+BֶBր4 *aPT˜(((((((((((((((((((((((((((((((((((((((((((((((((((d_?![{/kzC8:elXa+.𲣣)RBU&A_tVԑջ+vkzЦ{V_b3RRoCF4h]g&+p@shކJcQ] Itq3kSɖٔ)V, /ֺk '8̤qZ[ѵ sp01ʝIO]hކ)ujc6 |?~!ѧy`5=hMECZ<@QP֏0zT>`5=hMECZ<@QP֏0zT>`5=hMECZ<@QP֏0zT>`5=hMECZ<@QP֏0zT>`5=hMECZ<@QP֏0zT>`5=iDր%S((((((((((((((((((((((((((((t??![;u++-L@F+t??!^o=,~4j:<9whZlp4viXmi篽.z[=A8 \,ʒpF9w-@n6pn|= %4WSУKCpm$֏iW3xgTo hwz5eR[F8GJ/};_qT&c72kzZۉ FV(p2W9q)xE;"CrDcM|<=*̖3OX,ɵ*[qOVѴg@t oE%ἑu>uĶ4vLܣ<'ҋhڍȷ}:(WLJ{_gk'aF"I! |>cjς|3u {k[Ք+>1ZZ_oVuo$]%Xx9*CNu~ԭi_ WxNzF5Ebh^?A~'%+/YZ"*`%q[^)×:h&s :s{=@XY_3cC$7 esm%5I>ޏAud-s!c9fV|Mg}+~nbIOڦU8PXdWtkJ-7S o:HLN+C:j:]Η\XX[{j+0@[϶EZgdLt ]h6(y-RGB8~79S3KIT,O$pIlomu eݲu8zqg90xw9^/ ]i C7#Zܻ:´`N>Ve4m:k8sܢ09kLt(+m捲-lER^eYHH9nG^:Ӽoo]0Kc gTWs s~}oJN}N+p#{za|W4"𮧫YxebX9R0XcQjM3A7[oך\i$u1cL]&_4z|7c;q\<I|rtNp''-Oz՞}[1 +UF1Wc-sſ?lF{M9-%kB\v}Pnvk?ėf_oretH ">~BAGJUxm:W;.;u3с¬=Ņc`ab}>j;v]ͻ]%aa<)_5_~@ӧɪ[+v܌}~⫻;y-,Xsн2YGcgD52D?) U͵3ErS)#F GS\ecԥ#;|1rOo ]iQ'h\JO9AKY[is;O\FCh迮: KKM;]㶍6nl0 W?ְu ]:–h{h^&;yM˪jO$y@VN p[_† s.(((( [pDj)c }ŕղ'POP2>wwf4(4"-2[X%J[0r~n!ݥc,A`}jՖ\Knܮq8^^޲ijS#h (: hZ唑^ /Y3n=y-qQ5[QQnK.*ڭCn]cv=N=x]\w1WRv$ϰPUOI"/1 {:T}*Jb ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (1nC]SB1w3umK64kS4aP[閑e! .I%v9'7WЭE) dYdc'֬kYEO|+UIKrxRD&9t'+DBùwz}oM) b*]s31׽bx^!hs}u%Xaf Ysmֳdwi$mK[s ƞ[ɜF#o iikm\^4s 0#v8 3G!zVVXBzq5;ƚ#ݬ.#TUDlR1R}+umKĆ} fƣǯT:Zx[AoH2 0W dϯ&z{_Ko%3nd-"coQj]hY-/Z Dfx~Zkx/bӯ/62A)|q!Yp@Ǿ)8ZvvW*^AE\3l'>/$ l]Bq*W=1N¹Eڲk~1*Rm3E[x`IWGsX'<ЏJW Ex.GeCѮt䬎o$;`6jOO?$~K%"B$$9Y#}qU}=e+jvI&O2Gp軘=YǪ]5;>W'CȪ= $R8c[:lXU% $xn%w|0xWjWk\[j6.fAs+qQizƣe椺yJHc>ltu_:wZ|ZWt 8(vLE +u/?ϖ$zUS*t{7NDҚ %mnc'!p%^pWjz8L#k{Dq&CWzgG'=5|Xu`q(6 U՞mK%YV era_ƕ m[EpP?'w Ϩs\|IweAeu˪yYDquה:)Epi?c{Exc,(v*dlg7 pjEM**D_cVi{yݯwW#sZ_O [=oq,żBy$qPK,ٌx~>3 VI$ T 5] գ[2)e Hi\v%?ޱuvuyO#+c7:g_VZ宇o] ^SqD?u$hҾ*:Yj1AwzO (z|i[Kjpã6]J2K̃݀B 'j?#h8gM; ehFh.uDHw@AK^XxM, _yzmT${H% Ѩof7⿺[k#"!%IV#rOQy"Rm/YafY#^\1_AQM1_AQM1_AQY1_AQZ1_AQZ1_AQS1_AQM1_AQ%GJ((((((((((((((((((((((((((( [TЅt]+ ohxNia'4X(tȣz$if}c$"hd;-巉neHf[. lI їzE;91l>MQaacZ~r%lhp3j ( >ů-.㶞kˆqw:54k3QB `!A+Fl<wqjl%\yOTIh F1xVri!MpB!rBQ@rx |7K̲o x3Q]x~ݩOk/&ئ0Qmn1n|=lj4eϰHQ83*5u-;>i m=V k;xan崔Lܹ9[Z>e{JP \ܣc=+N`exF:<ʄi"2vVŔm}baXVpsTP>Pndu+K}Jl\.NuZᅝVvՍiCnz yխu;mNNoRA v#is&}LQŵ.3ʒsyej%  6p;VΉa&%Wv8JEaxÑxFΞ<ԛ̃##?*cJnxUPXwq#񮞊-](/-dia# ]XP{?3 -wV:}1S?)$ _EpH{UӣBYFcQ0JNN3ޭF%ߓc-ki`9-wjh^ౚ-2ޠzEPw- Y#JNAwcܐ)t?Lm#k$O#VHu KT]^Klbr0XaA ٚZC@*ի=.ʴ~e<;kxYBy7nFc@\諾}\C` C^otxiLZY~VGUot;K4}TQf1b&$nb#rr}hԨ)[}Ba <ϊÑy_M~+O ^Zo+Jƒ)zhGA:pޭG,wn퐅sLV/62+*[ۚtwn%HP] =K^ejiڅ͝ZELIc'ޮSYZ6Rh`vȗ%%Qzz7zٸnqkqy'8!QwQVv"Cmsq|8H,hT`:V^ ~ ֡*IYGG= *ܸFrz>KXa%KG p@q^K׌s$B;FF԰㞛K0P\k^wM*Ӯ.p X_q3(>/\WK^vDĎp:tOֶ]E<2˧A4t/\*s9+(/౞.hgu㚾4^&w`[Idb @u~Sj6O$dap=-@ +5xsþ';r.Wh.pG%j>ūC=+P1[3a (eRǒGAs![/$hQ@Q@Q@Q@Q@Q@soO󮂹7~' ؏IQҤ (((((((((((((((((((((((((((t??!]=s xʹŠzE֭o."Pķ$ٶ֢M mSF/y2/ g5-i`/Q["v:18OԬom~JA[l-m>wq\c9K^=c5w:4Es~񆛯Y۹ Kt3pHb_[-XDSqHGҀ7(h o}ӯ9Qq5=橧؈_2O=hݵ{+n!$$NIdqT֭eIy]AmK"cvO'^xŖS@-$wc[wU?&6i#6ixy@>&.YiKM_Nk,D.Pln3jPEyqjon.tK,^dR e뷑k{[m$Mp>r4{/Εh<5oo.WTUDUz z|7Y6'6RC+zALFETmx?UULk&OE%jCji^>^[R[$EbyŚ,&z%/ ޥgh7/GJ'>:sϭ?Cн4-KHNe,|ro8#;J(k>3Ҽ1\A=ղ6I:8^jhoLP,7sΨ=<+{je[| /8ݷ3ހ,QXωt;DԣH**FY nlօo}pR&JqN[8ץ[Xk:fj6WQ7;:QH< ];Wu0M, Y0=M^l"X79wx*(((((((() -! W] Uz]ojCUt_eZkvz{io$I[KtTZƫދVx&EbG9ګg訤3*lU>{[ǒc{g4Oǭ>aoBDvq@zi6 .nmRK*"9V޹2ɩhv-,`$>QgԢUgiq񀫟A[-at2'w)"x}= oQ@X mVZY-iT;@$xnfHi& [-ڝa/kK:͕彭T6a r}+m/Go? o{X5=kXj\ ʂ}<< g)߂NkXb{⍙Vx;滿ς}|x@4߆[[\Cyy,d' oP2:|wV֞T+zZ_ς}|{;[K~1&Y%̍ܥr=ZG$v3ڥ ;ⴾy> Q_?‹+;m(Fm/Go? Ѣy> Q_?€4hς}| ?MޏςVKqdC!rnzD6Ck6!W dX^侳nPrYOB)rZ[xVa[%U]cB `0\w?Z{m<~&ʚ3uj!lzf -Χ 5o(;d Znj:E7pfchjZooCkLhmD@eI#9?ʳ>GӥŮu{4RM{Uw-]̤s*/j׋k\X!@ ?0g^_5wo 2 +0S?qqخ\,߭T9 CuW|3Kt33H:u/Zn\ f꿯Sҡ/g.C#g$)#wJ>w{e'"U{FV ; Sg8=+ܫ_ΕSĭ԰auki_wUmqHd6 @uK6x_ ';OOz2iO[&b R9JrWM i6u=Eiai$ZZD##*JdwޮxB+k߉jz>ǣ6. Ql{תU-/Tա]:;②vC]NO4cUZ&iQ-jI#Fp;ʽ)%eaws5.<' [izZ7E™j]R\*o=4` Ȥ9 r}{M[XOvjWg= -%H FGh o *ʑp]F} \յ;-"ͮ+2Hp2NF}gyd4$H=E-op?xl9X_&os^4:Nd)3~@'q4xε6_khSk}*fHTrd՝6Kx)gĚ$ %4H K:ڧwY\$w7mBH&1}?\Qdwu]j\ZHm+ 0xwv+)Xw ڵڬ/ȈT@SԎhDqDZ4kUƝخ~8t)Ew7j(+Ě&~#s|ǠZ۔6 -QEQEQEQEQEQERZC@*իi"F!cEgo#YDB2\w ݿ.˴U/+P?]V?vERsjo U/+P?]V?vERsjo U/+P?]V?vERsjo U/+P?]- \.t 3c@(''g*dQMs*ݿ.+P?]]yZ9s*ݿ.+P?]]yZ9s*ݿ.+P?]]yZ9s*ݿ.+P?]]yZ9s*ݿ.+P?]]yZ9=EA"$zqsl+rb8"-wa Uw\ 1hQTC~tyZ9K?Gݿ..TC~tyZ9K?Gݿ..TC~tyZ9RyI &#ӡs?κ ߹@3b>%GJ((((((((((((((((((((((((((( [TЅt]+K+^3cn89+t"O|≟M3R9 Yee {:Wj5qivsc9ҭiVzaӮlO)-k3Ft Pt"LZ6:t%^\!*G87>.OJ n) ok6vZ Zc rbV =3%o7Uh>2y;ǥ{"xIM-%tQō1pr0;UtPr_l o7o=[GV5__57}bu|JƛXt4ze-/W,3c^YkM**@=GҔh uKo Ino/ΰ)BSlBI$=tqKj: #T[CܤIj1) ݌zCx[Bm"--3DdvP؍$N\YXiֶ ZhcHLSwP6I X*P|ҡbN33x?Wfc#׵uTz?Tz}i쩄랟^j-#:&;M閶 Is֎fq_ Vs NP 4mvK1cW#=_ڵԦڒK؅*Ѥ%)#9潆/t(oC$@0' Q:E7p|G[K}/ڥkJViZqx<6Bxϭzp46i6XNs}j%="c .~)y}j^-umBwŌ8w@۪Ƴ}^^,Eur%ݠ%F>\=s^tKiHMPne3ڨYGW^ZhPA OI'{]CKXkt q'{]TM657V!|#iGAJLGoG.`R9bmdExYd2+Y𦃭][I I4aPI?jekI J#AP@&(((((((((((4)]t5?ZW] f O,P4hǤ?/OT^yd[}q rmp:$.u 8fwo"^7O!ؼMvϝntl)>%@O,͍cϥzM63G }7HGi{k <0,-diS=+ȝ"|C=0N/7#npq5v H4 iotFXvULBxޝs!Im,rxVWr[F"4s 3WkXX"Cng@*$|zbM{AGK{etʎ ξ5;yM[Ap.e"Y9eV rq\MDž?7ۼ}'a׽fݾĆռ@n(\0ێ}(W4{Ȱ!m3X_uKm|uMeѼ KF$Z\Ӭu ]K~Bo"N~(džuu85oRH8ڽ{ֽս)ҼloWYT]z -J#<in5۽?TѮ-G"1 nqB Ro G^-]M!ݿ%`BJTCP׻IWh9#$$Qݭг`>|D?`y}Ullkh_ <^gfnZ mn!/_-cۋMA˭yJO?gmxݛq`xj4V-uVYo@2FGJv`: ݸ8̫>iizUddLn ZN𯍵 !L`o0zkl%fZ\K[أQeSGyʬw3n'}+\P]4C,7{:YpzWsL|tM.R{+|t֮xS}f_o-p3?TbNӺ&X^\;ıܑ5>⸮(CNҮϒhv$-AF/HPG86OSxVm;2w,1"1H }_kKI?iڤ]ȋIϷJ~T4GM"JH@Ryǭs,o($*!AǯܭRyxzЁǍ+ՏAg%FQqv1V/EncizfXEѕw\m|Wq_٢:(LWj63Vhh:kKؓJ[yDR|GR{ІΛ@-s-VKnkACVkb-6)gvuǽrx5(9#s3Qwo/-dkt^au'_:m|it-̘ily7#-Dn߸ykROxٛg.ݣwg=VPFgO3\㿽mƊZ7?x'hOC4h?뤟Pڊh̗cHGjZ"AsiC1xsmaL[yl? !BP+|82jFi[JjeBT@mH$=j=:<aBH.,a.I!v\#sA b  MɜnWjZg-u}Y&hPV=JQֺ/EմlE6,ܑ ֑ y.YH@inomm]VY@3^Mku G]ɩMj%2O7/wóXh bSKT (shWR|W&tm:-=.d`|y0cQ9\:LxK'VV!e?}}I㊹EmS7W`칹Cj q ?xzMO512yf?SOm~#6WhVH[+6; c_ԵO㻭CIAfM5?1 /}GN< gԬn]sQRC0@Pp;sڶ`]޼'KfbA-tsȼGyqx1OuMwHmQZ_\j~ Яo\IuqeFN=3U/mu+9Է vAnWƺ2LӭllZFDS[8j9ԓYc2$r2n!؜-Ρg_\Eqwq|J"4eAGbyl'ޥ_ũ]iV+*p1.d7 w|LB\m4jϪ[İwڱ9?OM,^ȋ8㞂[g$.U ~|.Ʀ _\%=`xT˯Lڻ^er~O1?Kt>ykwpM,r,fEa/vm4e#}xnb!X"%i%*ȡ }GEgsv~3.yƏ?4QdI#=UA)^4c>ҫe#}g<@ ҷeC|_ZmCZQe#}g<@jCCilv%?˳Go[XP ''&nX4.*qQOW dpj]?Y3ko(0(R 1ޕc.yƏ?"fs"-!7"ۛscwvm4e#}$fJraϸOxxnёd銫g[ǩ5S xb6OHk##5[.yƏ?qo~]1](a[\i7z}1ܡU3Se#}g<@aXD 1ȥ\.zqS?h˳Go 1[7͵@}O@m$RY[4IQR17.yƏ?1[#$1QB <>z֫5cA3 mT8ϭ^˳Go.yƀ%K;Xtgn֛&g$t6s4j[?\S?h˳Go TTh]?sXf ĺ#Yб"G#쬙YOj *6^HsԂA?Z.--ռ39D Pe#}g<@Kci, <)chU |vG8"HNAPۥAg^G en;Lx;X?\ƥFO hַE/P{xw>8;N:R}-isܠPgnsStcHKKu3-xJGDjf|R c֫:mM Zb771:G[[\]/oc}sIy-Kcw#Ո<'ҋ-sIX73#޾tqg$f)Zq)Տrm'Y`-mot%\9G6:PZ_oz%Λ o^_%i1㜊E:/ j6! nPIO{g->I,_=Pa~:O_2^&4R<g |t?v_4IIu{+d{Д$09"[FŒZxNmSPJ6zqtjk+v^"C}s21wϊ^7D뷆v7G!=KÓ=4~j&c宎 0hbP@*z{zoF3Ѽ7ExbZh &aԌǧJjv7Po̲`5ZK:9m".Rq<G /iڅR%,cD¶ M{"6{Ē$Q0D@Y5ݵeq ' +ym@Jm'/-6?f+CxI:L :sK]o Jy=pOx{񮯫3gyi~Q>Nqj@z{;vz lK&[VA$6aڅvRce΢7L6ps^'"Ox³kzmܯsnm8ASץsF<yufiGZ"2ÃZk`մ녶h5 IVIuۃcڧ7vZE]y_]q_5-_LГ^4a_E U:  YRxS%ךEv_\r_- z^g$\5CRfѮtX~ʶEN9e*F=ekaZUǧ[ķ;G\*]@ǩؽbnɑms\<}چiu鯧_5T{#(3`޼;;-^2RKEebg18?S.o=u^iɘ.zi_uSIX8fHQ_?fz1'tQE!Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@!4J롪OKZj@x{Rf8&5K,45NH=1:򁍠N1^FFJj(>RZ y8. 78%`\>;r[V[;  1"E+"*,y|fi-?!NAU_IMkºrm&ۄT#B (Iݻo8V<δ𗉮cxfh ɹG[\]kI.ZJG |][v(ĉ.a!NTvcx/E_LM!ð<]2w>4EzSD7W Eb *:(eV`"Wou=B],Jk,T;2 %\[\4cOVb++o%F"qU~ èQEQEcI]s×1\r W^xjZՖ00ESw+)BҀqҪWRqJbi!%;hm(sp=+'闗Vӡ5v&)=k4OAACz(Q@Q@Q@Q@Q@Q@Q@Q@Q@soO󮂹7~' ؏IQҤ (((((((((((((((((((((((((((t??!]=s ((((((((((( H5 ;-#hLX`G<-³ϡ}{y4+>:rMtTPEPEPEPEPEPEPEPEPEPEPEPEPEPHii RjV**)MDJ]:Y"azAf=xO&WͿ1Ƥ9qdUmc:m-v֢VWk2m=E*y^EsL swZ|nGMAwr۩WoN ==(z;0\Z+;|{s,EM \v(,yǮ3Ul3\W[NQK;yənaN4WDxa|GbakI'ppX6pxNW5mkZXh;Pn!ݖO+|Ww6j>K-9!׭&}݀/_j~*m'Ku=FM"RlDCn]A.A>~b=ƨھ$/^hH"w=M^0èi:>}r{GkWx>0^V~%U:W/!qi59/ndkHE2g+88k|O(+ᗊ/A[ZjzeZ̶`d]y|f]C"fp>9(z( 5jo%xcknկ|Zoe蓟^F%e +üu>,+\-|8Ё#3 Jǟ3y|3{>ʗP@ JHg-[+KHe1~u!I|=[\]pd]BB uPe{h;{m:#*};LO&y[:{\7ӯNnxlOV (Q@Q@Q@Q@Q@g!H^?*UQ$~?ZТ8zVy-Ε%ӡ8F Vicp~vIstDgbrp>SE9Au:}[xTJ*p;%im膡 kn0ʡyJ%cӍ^6|:oؼX08`q JEPEPEPEPEPEPEPEPEPEPEP3Tҳ'V俸r"N7+񭼄Ǧ*v 5~Ҳ4i? hֿy~?TǬ>[/7טLVu./.w|,?'(WݮυU(=_v>T*]X~OQ ] (wka?G.w|,?'(WݮυU(=_v>T*]X~OQ ] (wka?G.w|,?'(WݮυU(=_v>T*]X~OQ ] (wka?G.w|,?'(WݮυU(=_v>T* {x-&ha՟|Aց=v>T*E#AJ{ShV]X~OQ ] (wka?G.w|,?'(WݮυUaḵ&9󠶹<ɐUǩH|2Ԇ]^$Uc*$(#-v6wka?G.w|,?'(WݮυU(=_v>T*]X~OQ ] (wka?G.w|,?'(WݮυU(=_v>T*]X~OQ ] (wka?EyEB}O񞹦W׷Vf P2 Z ݶHm5K;P6asV ?ң᎚$sfE/=1֥׵}B=9n3s+/σniOep 8mP_ Yȹ"26m15c()sꗺTlHdX}+ߎMU>1Ikψi+Eyt]?N_ , $ F?QF_֡5[#w\ޫK{IJn69EYxzMwP+i *TC֝xU?O4[)VhyJyU0=Odx_:ZcKb-;8{2*Hw9wt:gOx[lhͼ[Lr19>,7ڬ7u]*R'+kώ|K6jO]n-䰊|Ad ǎ*i߈~c.R[Ifa%pk|{]OW?*[bNS\/>W鷰}?A,I$0ktmM4ւ78<֎<{r02/#OZmRffs)1 |_ t_F]J_Ą a0 )P#mOSdt*HÄ2{V?4ill$[Zb3A?(9ϺtմOĚzqBLPvc'ҥqagZ^]D+ay: xVx]_UmພM%F <*um;AX2rGBsFxo-蚄zttx vlS¹? n|SqIm=i~to >R}qxEЇ':+=f-YuI"3Kj.y /b>23RdSߙ@eXm!'4Q\IB2~,Ⱥ̄GP295mKgrRpRUb QR@':j+)WGҥٮ<刢5GYE41%mR+YvPCG1wHH\αCc $ك.y`t;\6?cXD'<*8 ヂ(((t ږec$p[$3 A2iso(F6}en /Y|2Ҁ:z+ZnͨN;fPqkQX*xrI.^vbϥt$ꉭG;ĊX7Ey$!qT'+-#X6un@ƾ#4װҵk6)IOojIpw1y􉶉[i~3Ե]jQ;J-w `V%'8p"|mldʎx4o6s}j6w< ls=w[Ե{]wt@_1To"=w⾷sm ikF$wc(dq]uݜPu+7H-bQ䞕󾩯ꚨ,[j:W$MqRxM8,#))6ؕt]-[\> =a ^mL{&(8ZO೉-!ot&މчIJ>%tҵdC,!8#itJf2vINE;WZu]NS;ሴ]TTvvbWZך\]LwI,,ƫTYYQEQEQEQEQEQEQE}wTЅt;v4Xm,RM9mǵÖ957u?z~,.37,{a+MWH+l E*G=sJUӭ%W$* 8"R(txʽSQlpO<gXZ%I5dCdI22zUfM.3~'o<ɵ#۽¾ҡf"ѷQ>dl|Z/}oznKs%٘HYk*uSf3(c!EFB¸sE ߙi|~'UKՕC5ԾR(v˖#hǯ/U6nev GPA{)5pZhyMýv- ȖjTFh#xI*a:m߂>:\uQݛy?xhb6vgTN OlfBcL\W-tk KtIDa0qײKM,[';}Z(V}FنiۉIfN%湥$3ɥ*$6#z먥a GB,N5!vU`n1ppH9ndMcKu eF-Ԓ1^E YXxzׇ-mOY+[[lh˒ϥuV%_/}m>;T!}G5Jy_jľ!4 A5#V{tyѫruߖ?uߖ+,? 5UX$ Xc?uߖ?uߖ+,? >ѫruߖ?uߖ+,? >ѫruߖ?uߖ+,? >ѫruߖ?uߖ+,? >ѫruߖ?uߖ Tl6G}#G$‹6-46-5\ϴ_ ? h\,m?;c~[hm?;c~[k߿ hAq>g(Y ۬v ۬v׿Aq>g(}Qp<XoXo-5[xHlBԾv]m?;c~[hm?;c~[k*(3nAG3nA_QQ@.6-46-53nAG3nA_QQ@.6-4WTP)wTЅt3}>3z\1鎡L9y b&T߁-${仒nDX&c8j(Z+||<4&QO ,TZ6 1@xQBO,3WN08` PWVvwoVW+0/l#feC$uW#DWS ҅ NXqZ<7hT<EV`ۀ3bGtȴ.# Y؅ٱ$5Us`ҥ&$LV֣^==bm)푠VWd }cOMR+d!sc8{i ͬH&@FAK% Agr4DI}޹wCB!,MnZֵ/D򿵯ba c?S]kNln㷶Y8$4th|qA};T+;x9hʜc]UCeuWVdNCTmݐQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE+=.ʵj롪OK\D u$T HRǠ9gC"XG/1T,ČZuo 12j+K Ixn-"$Mu,-qsBuXYJĩW$?HɮkE}e΋pN=t/GK6C*7 f'PeEqxP}'&;O7XhT4{x5OR/-^t**$e ;ݠ.wWekگ$R]Ih)c9NƠbFu 3 U6A4 ]o{ms=0LKn%U9(q¬W\ G=x u-O-qV%Y_O\<{NzȋV! u=mkU<asu.2\GoțYYN=C&k:K %.,DC9'#Es(QES$"B0TI8T&QqRQ #??Z-BY$X#?Lժ)hPT\vi0a A8p3Ǯ(xRd}8.ނ &i^}}EE[gx,I?B)e~[jvڕ6И7$2 @@wWivh((((((((9zVFMkҲ5h=?qمv_CWaLGškz~ig%~V  )xQ].52tLS\ECPv1g닥xS6_gLǀ +CVu.xluw-c\4ۋF<`CV d޼P,$4 k:iD{Wp ]6VE#r ~: HZei#)7az+>"\j=}0Y{EDſ2*YzgӴ?}SI|3<'F6g[< 6ònysKJXn+GaYH,̮@se*A8X,|Ts+lA KKK,0jomX,#7Q V=q`K\k٣ۨŠ(((9*9*͹_M~FZs"I-H%ed!$cJC+ehbXH(9)_}izn-P3 41SEs RMD _+5 ~$jP'av< ǯjN_?*٬oPlЁQLAEPEPEPEP)wTЅt8׮<%]NCYM  7O¹[O6ӯ/52ڿuxN܃P69!x^vl7pTr_D5HuK(.`Ȓ.BЏJ@yw oonV[F'dy=Fs]?4o5bK%Y<]p:Z֞,6L !8>],߰\[U =0(q8&K jWs 2~scq<["m%vn '3zWuxcEgi>YLy'^l짻8fq$  o{ -%ֵV-^VRy.Bnj#^\-qQu{+[+JI Ny=s]]hn|] eSlWN?tܨ4'վ^^j7-w)t[@C֚)!zc)"}}ʱkV3i6鍸e_$O];JN i - #Z>)<.t3E4ɱ88Mi܄QE!Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@!4J롪OKZj@4$8zT/r b=?ZТhq: 1F@20eʞck\ԠKOpyg96]]O54_ڽ#h@qSjb%ֺ/th0Gjja]Ux7QHmu'tЛ $  M8ڵy.6fʨ7|wQ@X@iu6KFYoͻ<}ޘ5jIX]-5)O3NzwP9G^Kpj/ a 'hHjK ^}PX7Ь~@U矾?1!(o:;(X?`G;V3cVQ-]knEPEPUnWI`qy dz=*dEy{+An`h!<3؏l֢h5_i.('FJ|U27? Oj˧I]ȝJX \?.nye7 /tieGnw}9 ޛշ{hmVXrvrNz8]wvt@(t9V Ёm_]ie@_/ _V^ pƯĀNXJO:>cvP@C]!<95_]ie@X5kM[\HW eX,#.&Fi K<7o$/;6Vnw}]qtKú;Puut#{6n=:TM.︿>s@˿/.-U>s@˿/.YaYGe>5Q@Q@Q@Q@Q@Q@Q@Ҳ5k^^}@m2{ӴnTմ][M?2u#r{<| ۀuݚ~?Z*eGcYg.IbYbI$NOvB?KSG-DAwz g?J]\0A+0h@mM>Xy5}-G#yQa=!!RjӬڀw>?#?DzQ:}K ~X;Vzcuull֯#?}/Ks{Oq(I$l'? ֲ%ڑ0=?KQ}-FPNc+V}t Me)pgZJLdt^[+t 0Cz*z˷! q]4;iM :tӧZ cf}qQ:[E1HW{aM?E]^iDOgnw2āA>zo;iMv,v _*QM4W`S! jb"вrFf#mVTIgܢi}pkKrΘr΍?:Z)7/ܾr΍?:Z)7/ܾrΊnC]SB1w HtH4սػA)83I|3MKNtoR4Clg#8’R;sdR,Uɐ t|S'vZrh^\\mI] otmFO nVA㰪T`5X--hO\2s-M r3rFw+%OG~*:wMIotz`zTӦҴ.o&BŮ&UV|y 8*h BGQ%vy+mFk^ou-ՒHR؎@U7_+ jZdz:u$P XI+to hp$AJn N{;/g3ڹ] wa]w>۱V^IܴxO1wqw5e+ĥկ,acwefN7>z7屺eou#xQV˷ >]>Y3!IeR{OGYuueKI '?J֬5-hͪɻQZ4-6.oBa6/)|ysiQy2HѩFPw^FNc9]W1?,u RK1y2Xq~ۏ rhv1rtGUH#=)Xw9Cu{y.jAE?3g$eᏌ_xTr%-2+ZI Ir=hƛ A.vqvz-i73oP2] @Uig[uhQ>-"G9H+:^s}udإ2pZ hc 0pGzJMK_59uY෷76gͱ>lZ'K-4n;-GŒ|z;y.⹊IKA18p;⧯4g%ַwZt4XK3kW}M2W&3Hɻ|Zs!啂F3z|I:W?".pAeۛ\OB_&3Fo=+ 3z_k-U{P=ஓTYx^r%K'j@2{zU uXV Mf<ޢy#v^H3(Lڴ֯'u{tM8q[}CY587lGQ((=fqO>/zM*N}r_($֐k5 s>|wjMKKMKW.a'xZ𽆟kšrwS#K38P@PE'=2}' ޭokMALN ;q˃E[Sյ?ZUж:a(G (oK&Μ4mrnSf fr-<6v=JhUP7w隒o io<᯲A>gNV)gPySՏYFãPG_xEWnyn$m0>G,4uD!~Qi(j=R4m"*"mWԛW̤c/o,"Y/n`*zOz^7[j~ ѵ@i*XmIe^H'ҽcUsxvqN^И̭o=T:e˨ڥF:犹57psw3ʉ| 6N]Z-i;iO#;eY-׵uz4>!{yYz(8o=B(0!zӬ!zERQEQEQEQEQEQE+#PY4C» /!+!4gv 2I8V'^->w-nF|1"@ H 0 g,mcGG];-I3w}+wZ>qYj-`|'\;#<(Ӫ_]YZ_Zyh@9UܠsSi嬡7]mIP}C@*<Y5[7@չ)_p{wgi_Xu+ կ-?y].])[@lźX知T_ 0xjf,'!î3Tֶz\-?Zq*MftRQ}cQ/ml7 [8# Q^bҽ\52.Acǽ.>$QO9KTЅt]++Vu4*Ҭ1 &LͿʉ')ß#kkԓiڕ6jyI_˷nqq9Ws1}[n*I(Pq| #=ՈK Es[Ε`(PTu8_NKN$6wPmHUd WMCmy麄Weٟ=bO xQǗ֖k$U8Ú^̥kѼectE;Kef(6̅vj= N}ijjv% v׀OӚ:n~aúʆMCgrG6&֭|; 9~X:t6:-smmwu(.mFfg=I{w'FRMqkQ6qָOiZ'[xm"ѭo 2V;6z\k$Ɖi ^Hi0А\8CM_onVH>.s{w| &V<{z x#9ءs6oIY|yp'𩗐יCj:I.M2[YúYdagI[K[K_j:U̶<[yAeE\ԑӿq\z ³IjTfh!x7I+*a;5CBub({.SߜCُ3^zGhH bsb[xMyo`ѯ<#(>Zēš)9m]ñc#}Z.i-خ w;f(d~_S$4^-#$$\IVPh[%|т:֟;;{:!Im'Oy >=Kg*]~= ƸWK֬zu&k%7d*dG@+~-qi67-2Ckndȯ…]u揧jZe[;Fܻ#.w "|A-u;fc.Xcs\t {oe=e)+n6Ñ\жgt/t?Q3l%'gӑ+~V׭NX]N' 8*F1]^ ~Zqja];LTn8Ox{YRU{0D4yuu<[9[u1\|x}nt>8WY6&=S?yX-?þ&}GM xD@Tc~OKٰ-mkuu(.mGg Rxo^ +]74KKXm"Ѡվڷm&d1vSʟAug#͢e1\&$7_gC66gp!ޭoљP]# kX\G&l\Gn9>"oPx@gN43lC ppsWB䍼/u)X〼*C=+,qw -R?hr0I\gRعKI}b9CEs^44ih&)*1cw$R8VU]=BKELh$g/#_zyYiUWnO޵ _xr[ -e1H\p}*Lz}b.$ۃ3O5@cC Scjk 4%!S1Txt&((noTw!M AU TGmv!U@E>FմF3;[]#xJ5jjQV6R\I^[u*6T[@u-.d3i4[ UoON07u^I2F ;JPؿn??eNⱍthma7پޓLzջ~UBuBFnʛxnY cH?ۨ_QPؿ&bX\::M*> öw%%{w[* 1FrҦۨ_QPؿ=7l5H"k3YnYᓒ#< ȮFtB3]#u*6T4ͺu*Ӭ!z6yE,.; C'(((((((9zVFMkҲ5h=?qمv_CWaLF~fkym2G$ TƱ>4eIdW ?(+GŎ cX{ ĒnA`mƷÍM+Y[nILzYxu>뚼:mjScVrHvuqfr%c2d:g? S76wtr elަ_JB2RI ~UZޝNl^]qqJ^ƾi6X 9'_ɬ_^VYI#ˋv :ۅȑF 4yO5Mj|_G&#W_ɩ(k4yjJ(?5Mj|_G&#W_ɩ(k4yjJ(?5Mj|_G&#W_ɩ(E\@XZ|9j2_Qy& ˵l Q@w'}5LěM"'|C$2$򝭜OkUk&)f\R]hT0P0R@h(Rt??!]=s ĺO"yK"Ɛ, d JQ-CWΕS8댚~4\6ijѴ[* F%@*>x&u7vB[g#}})t&w_Kfn⺁@$̲6qSZM>kūIk:A>'%z}̚t^"H+3rx@TkFb4Mm g3RMY޽.нYp;i A85/m n?~sy߅R:|1K (9fax;K;[f&3 #~7z'<_hoe+ob-R񴍀Ys~&[KH䴪sy<e&pA`ii ` upmoWj4TC?9M/e-\J󲨉d3xWF5+bעٯYoW6ž cf(c܋0=>=F=ͪBJr=~{Ro-}kck ?)M.} gYke;FB:z{׌e)%7zr ( H?StAmoHUڂUA'1sFEW-uK ɚKY\HVa {0:aKswMXʨ坳@""G4,FCҗ*Ȯoj*泌 7==95M>+5-XYeOl-xzϨg.-USɏL++5m"U}bY+`Fn[\Cs m,sD6 [{gH"x 'f U>5坤jlݼ*TqК?K[šݙ$Y7N41䲐y?𧌴in롪OKM\ϫkGq>}Yuy՚(q>}uf\ϫkGq>}Y+y\ϫkVh uy՚(q>}uf\ϫkGq>}Y+y\ϫkVh uy՚(q>}uf\ϫkGq>}Y+y\ϫkVh uy՚(q>}uf\ϫkGq>}Y+y\ϫkVh uy՚(q>}uf\ϫkGq>}Y+y\ϫkVh uy՚(q>}uf\ϫkGq>}Y)I4VֲiͳzVFMfMAp2?+?» b +>!Zc֕}sb[{y%b::9|3f氄.s2Eb1 X}[i9w=V%~$S[,^e}ƛГA掶=~3k־$uuM#PM6m/ fC'[rCc#hVvZj shb4G\\Cms4pǜndԵ?S^p..K6G[u2"$$_̊2s†Wc˱99+= ź盲:+}>/)nDZն˖8c֡|iz7\s|ml:]\iڥ\Nd0nxZVUֵaxH`xRV^V%51i%>r<U;#:> h7]k 6M>b=Scڠ_Эw:}<1`ѳ p=R5O8/x6%HrcqI* VW{}VM5}J1&zɹI0s=A\#~֍.Ͱl q>o kYᅧ+W$szm~Zu< VfI^Ekwk}vfD73F*0GL_./|K|]H -aco"7 Ygki?jZWyaIJc;ت1 Ñ躆y}"vwd|;Ox'[\>֦ɤi )5|1𞃬Ǫi:ld"bNs.T oooDvtQEQEQEQEQEQEQEQEQEQEQEQEQEr 똻]uOWO@8I~#C.w&)^BN#(* U=CXJկ&Z]m|҇ ^g(5huĺ|qIm"2OsW ֭o{R;j,߿ݣ[v0'_KKh^CSZg[?6?3C1q|=(Jnǣ^gY> 񧋢Qm0U]O[V_}䇺UPCr}v)U[Q]Mtqh~bzfw::ˡ.I`ht=:x)o#B3}iT6qWdD8d,ya5H!7r')sA_&a[czyp?t˞*t;OM-L3Nx-uKJUޕ% BN>|`S~#BfӬ/#ybrp+"=3P|!Zu+sbR7$s$3z\sjk%ąN 2Z|Au j:pMox|%okƈ;pBm{Gn4+5ݬ)4[89z/tx;}Yw FlLdj};w:ֺQ&ܻugjVu/۹Γ$qf<PI䌎3ހ:jԷrjY|ZZ/tfY0g4/u]d}+%ͮ}jn[-!NHc>o°C}nOoummK>R窃 /j~!Nѥ!F3F0XzGVb&]ڳGpñ%1txu_[CM{%u]3bÌ{xb-6N >H^3B#𾾶Q/$p/'; q+*2x҇rp 溪(((((Y5/J> fWO\a]1u/jگMĺl&] cxeĦ6i:Ƀ)&( i Iu~!bI;s9OxM񮿪z =Q8̑MN<=VKͨGw2'.,n5YvI'=xO>-5΍ywbȩq޽)vؒH(C ( ( <mj<mtsĞB"'.fe-o:UOE+}/@MyIwBNGu?J>{_X$ݬ}#ogr-|glr?5 ,hSYo#ѱvSu+ܵ;4:khi[TdV\RSXxZͻh^1|Cj-$43nċh+>:]΋xzm'IL.fXbTzw q=Y*FTttՋ(* )/:麶;{! ;H9*Ct4o{~bEx޹q^xGӴ t 2\m9\LEK{->g01ɪ>ݦmͩ& ǐA :mw)Q{d~#6⥰ 9Ѝg;@UWkɮmcu8ռQ^V+{ZP+vc8j^{y#~ɥ]5Z\\wmș=pi-Oj 0"[YTRrp>o*[2}8 org -#ZWcxoA-M29.m $aeT|1\i[_ˑ,qp͞]xM{Ct/ڮ$P3;?,TDѥSl#˹X2[SB 2-fxΧ!-4{9bO' '?.x\Mnk ?sOkv #95SF])z5孈݀ǡ(O4lth..ȀLh^Ei v,rI'kīGWЭƍ4awKjvQ*CM6ZE򥎒DCHv4X.o hBBKVU'o8AAwK:\2sz^^t_\_\%܅c% gOCY?t=NVl1# v3c@-tXlm`Fpr7 x /ZZ 9{-rf,T5Y|`!d z9?Z鴽.JXhY 1SK͸uOX/|?i8 F{HF͏$-VU~xp1+;egi/#2o2<nN03k^|Y^1ۛx&żed傎I$5q=?3іL+O)t餸ď1^䓞Ʒ̬}wL좾.-Kb .ޣpyTxúҭ57R!6P(.ʹu I?Zxc4FXHR@r_NuhK% ;mNTuv>Ai"j^0t .$g1 ee##dFUfPW4}Nm?PR;@iUby鹕HSNhXv@ꭢ\]OmNgq  xZNkډ9%?AmZM)^= $2cjB۵G۵U[}:odD m+4̌UP`GN{V΅i4і:X63ݭ.:>ݭ.:ھ[*%,\m#cT~VE{h###Z4T茎%pWҔQ?RkP隗\YR">RF9?5+O—ӵEb#[3#`wwP$y .gk[ 2 Y?ݩ5&+ VL\d&I ӜJmI۬k;% \=;TQHaEPEPEPEPEPEPEPEPEPEP)wTЅtx?z͎qmEu,)VEWGg4bI_0.zcgM߅.$#%Q#%s6V[ėdpf`W8ڠeqZX}MD bA:ؚ> gܪMkkwzUTN#DIHV\A߆nt}v7e69H:ҵy~*AŮ^&ޭ>ॺ5jSGyKLcs$)r$n]qڗ] O_=Ƞ-/g>mE\wR캗tVO6N|'tRۋg-a ׿CxN6RA)>|Sj^c45}5wFHMicPxPsgwmEus ^ U-k8@<WnwZZqeCj| TdeUoi֑e+,Le=?#6?!MB[ZQ tPq E 86Mu5ͳM1(qzZu݄Tݙ-J+ yw,6X`v5en= E1~u=OOedce d=&gv׷ w^=©?(0ὴhSgFb}>>BCc{dv̓<{mE @z9uI.5[h(Vx^-i5.I HƆM  @ ҭII/֥s2 F0 ֻbH`HaPUQ )POt4{k.o9(hVmWէWrM Ü7Q5Q@ZxΝ6p_%Qw3,f=3UAZ n-jWM<ƄHV*6:Wj/0?@Lmg{Tdg,wר(af=%<>$l Ž9'Ey ǨEq>HO*P0z (lqW.tW[#׮dԵDi!B"F1՟ZÏp?%Y'xO9 ]M-szwӼCwZjܦi5([n2 g?l5zQCƥ[$ p?k燼=-5cPKUHg=:((((((((ul?i]t5?@NUoqd עLi_y>_^ tZX>ok[{ZG{Bwp_R麤z1uZMM Esr稡h'ؿ^%{Ė ּ;gw5֪QܤѬb6I01ms|cWU񦣮X"إƜdL3ʄR냀N0q[hCgoJ#٭jֲ%A2;Wb&đh_Gs6*VyNdBZXOVxaxAiYJn[X͸OXn瓚OtM2NX KRjW݀Nztl[eaG+ .+Q|Q:e>\q\d{__븭٘>u1kڞ=#te>M2G)=Uw<=7_M;AMhlckzŕj><mnwc( zl54g[wd$K՘珩?{mwIQ y). +2)g&-n UHzgq75;G6j |&r J*yty~kѧu!(4S8#9֠+-]b9$"~rԨ֛w|G]Q/^i@#hx>}*(u&M8lΌiRzc:՟ ~"{763²:m8%}Fr>֭1X}~minet5` 1On-CXchI3ʎG"ss?躤z H칄Sp{qW_\CKn5[=b F{h|8nx1޻;gZזÏA}u!2k{עW5?Xs氖d8s#*il 3UikVPȷQ[ϧL3JVL'5\Ķ w Z` NOq۷j5ZN,`ؔB6^+zhmwd\_`>0 qںlf[h^Dr鞻OQjXsM$$QVvזsx,t?7Q0le3^ei5mջ`S9[+;k e}╇s4/9ttIl Or{Y:/to > c&hDh9s״U}nMQEQEQEQEQEQEQEQEQEG/J:ؗc fxsCWY\?=?qمuxU\AѮ"lgd 1Vz&JK_ x]^$W\(nXpFF@L#:\KY n6Ye st5hךF Fץ.OT%b.-2͌hӴR kFms$PzQ+?f[UѠdbi!A\WE]>}+WrFe'5W7xC\ݾj'Zkuaf "{BQGZ^݊X🊵kqj6,@b{`һ[-Yb. 2v]{zOzֿaM* {ХY^H#:m5֮o_SSN͞KYxD!pv7 eJ>=$'X?F1Þ> E K8VBCqTsO94 KwEgX+1N5^]MsNuK  Fi ؀0}Q_[[t/ ׵X'^u}ek-Dῂ7zk6H"Tl`8Dtկ$QE!Q@UϽrܤ8FsbUXyq2$_~?PwXi{K!a?so@YAAWZf4R_\yBѪ?􆜞nk+e_yMKikTFnpGyy*}=.~(x!othu]Vh+ylʊ/kV~ЯumI--#2HUKz=Ȯ(mmeic(]w/-二`A1V3ѝdc&cS:τ/tK;uE6i+?7JܟPîgJk9>ѭ$ܪ*ϯˍ#-Xivوc2RETѼ7xSRx;iX.Kqz>ǗZBƛVQS—,,3DH+9#2/Rɧsq JdQ|Z#mYrJŗ-:ol< uM"H. Ucs-i+FWռ?ycdDWSn-r]W懨[y0䎵U?,' S8W,(IW;o|/\JеO屸>gβ??(#;/Mse$ 'Y66֯S7Iº|>lY\L#)zEwA (((((((((((((]uOWO\BzaiB"x+qTӯ,m$9mRQ@ hED vccʙaemEme AkJEfK߇u=-hEDԮGy߅Ǯxjk6NCG5+5@Ps׵z-w~-.tnY[  :s_#vvq]:X(((((((((((((((((() -! W] f O,VCY?S  I/^4[Pei =e}[x,m R 4~X? N+м27+TgxQl4E Q2,V}gT3y?d-Jg&Vp[L2Ks@@$(:.сQۛI-"I<uRJיMIm12ace39 xp=z5Vv\ܸHbBM>8>The^CWE ==j(-+sds#_ǓJt6׬q c!WNN6q׺ -,+(r*:ݭn G0r /A:/i$|B]h M/_/j^p:t5M[C4z\rbt?^z׾b2y4?/ZnsCZ[B|ëˢ %Ľ+Ph3ߺ|>uˆAFO @O#j)ʪ*q~X,U?cx\ߺEVXq~t7V)g[}%I|L:eH<wk ݜַ h'\Hҙ<.?kZei6fcl"{>ZU?cx\ߺ7[cx\ߺ>? 4Uoq~X,W 䫯*5t} @{I?VHctS Gte@P*} Q^]i?.>?j"'*gc-qVA^#ey7"xHdX  Fsn~^Sg+<#xO֣䴻fuH?1 שMM<Qgv8 RM]*Эh;*z%QYQ^]wU#ºռ/ۡ0䪦W=}+Ǟ]:4bA/o8}3ȭLN|=˨-Zq8oqsV$񇇼2.شĭԏjºMޣAmyo#>D1@=xV>zLY\*nmĀO銿/ğæAM%4I!b:tWuU}>QdtE*V) (((((((((((.C]SB1w ( ( ( *idɨAlRyP\/PEΚi{/9mK0?6=>SPT7PYZsw*CoG8UQԓ@QU/m,c9mdM*6UJJk5.bTKRGQ@((((}ݝ3]'jEdh"uM.ff6󆍓k܁Zvap\)NqO QX{Hҥ]IRXc8#4E&ǭG= Iǘߑ ( ( ( ( ( ( ( ( ( ( ( CKHhCY?Sul?E ^mᄚ+4jF̀/%X{?PCviZ1Mp+)|QMk[PVvW?eX,lV>`aΝmYH$l%yOz\_Om2\\f0I 5O\O>`fx HcG'V~~" =4b#; mF9|f-:0pTҠ#Rt&hmͨ.#XϽ-moPgo~# U_Mqzѻ̪mcگ[;چ⻫[ -\{~pANӬ=8xu=YpsXeIpn39u p<֕m?ԯRnu-fZEŜ73)G\jI5 iM?&KV'hqudgO #3TG %&uϙ y\7jɥj`tl,r;(m{p<\]'DҭX"'18k9<&neӮRLrscmw <&><[vZvlJ6QfXenxuwgimmnlbY yW$c]\jrKg!yf["BcsVv&%wrel, 8RiOo!뺾2\\t*qtbZhb *daڶf,:|bx͌F$DpK><A.IwmۼE.ضXzo[M?T+D[OΖ-㕥F1ÂX~I\uau$k"QUZݜ`\0}Ǽ imB7sde*zx&Z@YslkndBFr>`|4lV8 ̮ܕ@N2joO>i$h^KQP,88CJTו `Ks$wČޠ ]Fn7p О=A#^$b׵$m4ڢ !`6>bWi뜒:WKpMѰUƳg.wc_iwa!Wk@<4k:M$va\$C<*jW9_/?|+H[r} Y*< IR0{~U p#a JNPH^}[;eh:ݴ0j1E2N9\:ޙ O᷏Qxh7e 2>O⻸ba9'>ֈI3K+ݸW2AaHceQčáCO|Ԟ-|7 GPdidb rYgq%hyֳ]x_A4ml ܥ89\EPEPEPEPEPEPEPEPr}CzV>%i6ڽ- Z ,Y*WX4bwge c\M7G-s}4]yѻ$USjȈ#_5t?Moמ՝FW>O (mgߙ-s}4]5uS"k;= ƹ>o.[i("vz-s}4]5uQD]/[i?lk߷h; >__5t?MoמGv}vпlk߷Ϧ~=/ g?MosRM 6R.Z)uԖ.Yk{h§qˊm2 >2)F0!`?S/G_bq$c0XqWdv,Ēw $#8E8$q 0iwmvK%82a*>9aF+,MOi$xm4x@/mZd!1`)bpx\{{/[w* ZFzcn6_K]GQZx/Vw6XK5ͮF#@'ӸL59ڞovwoEl7$l(ʞA潶.EeR )ɒfG$u:~ L$='vSs{M_"mo|꺵vuk%]ToFVF*~a޵<'.yefo5.=:$V12)^|_Ϛ-Ea?Ʌe}+{Zakv4Vhd7,$ 7{KXiYs+_:E`}KtR9 `@ ٮ(z)QEQEQEQEQEQEQEQEQEQE]+׵=S\qIqi:r\GbһI-t??!V Yҭo'v/hkw/'DBݕ^C${W%xėү^ Kb0If#+5xkXԴkK,[/ 1$jV춰5+FG=i[ƿ&9^Uko0i&eaN(1ӧ~Ƨ焭w"Q8S89=+ZUַoX&nb9܃~~&xx^㆕ hrK=^\ z5ה &\'K[9]k-qR0Um-=1ɵ'$ԜY]hSDwz>,,u%~XϨƓ] Ho43Kb‹H I@ ]?_#~vKVX}'9ldOzW7\xŵHhdO{/)xpN4M2N#8?S:G9]uh̬2/V0p*]IJ.ǕSX,u*]I>i3+,_!oFz7?EkVڍ#o+) 95'3j }h֖2׭,^ VV)n$G'0_[׉j7rt#`Cw6}3+*mmYa(GD'">5 YϿ3-}};gAt).MEyZhUys^YzGaQYFvPi.jZ4 Aajwdq mђ3Zl%F)!FȲt;o cMb:Y\p0Im_ Z]趑[]ƪ@aǚ?*n/Vg mK[<8I^>𮅨Z#YSvA y>uf[YHTg2\1b0OדдwUo3,+¦[6z[&a"(=FyZ:Cƭ+Zf$+]7g8;Rќ5}_S񟌭:$2LjG}}y! sv@f_OCz֏iytK")ρ|/G>Y]K !G΋mw0/1 k7瀭u[yrK[@oʻڧvZ6 m# c rQHaEPEPEPEPEPEPEPEPEPHii Rk6Wb͇AXhVм~̶bi V6[JEbK)f Qwmhe}s덛}*o1, ՘uz-턍y6f--c!.wZHLxMCZDGuN6D}0`~Q|cy+K 2ܛ'p@"i4^\Xμ3j7 )w tn歷u WR6yւS3uzkGuiGjWfI[f1eT`55>/Cg y6Ƿ\)oGLt,<|uq^eմNqܺF0rpsKa]RC}KUCGlB4!+3ϵcwך~\ju^xIxM'*T֮EXНGI[-ɎTFr63xC>L2+ܲ3O0|O߇4cfu(oA7- <ѷKwzrYM- #7`QDԭ\DE)}h>Y6k:˧Av< 3HTp?w0j[-sel76sln 0TZxPRݧUb˿ۀ= -q ÕEFy:{5n2]ksz兞.4,@B11ݎv="#d]Q q9z_Σy[qX7sg۽E|95HVfn_wPV_/#/3ú~\[jkt !M(!#vx{}Kw z2ĪG[ԞvMN=KAļE+d,&rsێOOׇc-jPߙ$ roxqO[_"+K2)EԬmasU3zjv}i YO( aIY#(r 9{\_ZޏkYg=ۻYnWG 뚾[jZ|iәYHcgjQq˭wVڶwi_A^o$1ˈy'+j|T|6dQYs=By"49g XɩmǕ8@ܒr8#83B1n}!0e%Lc=z%1[ *("uZK]:r]ԞWiH-Y- ):z֕b|:lLOi&3 \4ršy̨!u}MyF^@dSXȤGΛ ռ3 ^WΓ;➓4vZJ- c6kwū]?'yCJ#qpKg5/EBǷCuo>&x .3Oc<=[FunX:QiG$f\W&-ؕƫy\"~ds=ʊ(0((((((((((((]uOWO\Bz((((((((((((((((((((((((() -! W] f O,VCY?S <-"\Ve[E;/JQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG/J:ؗc s-nKM7[ mt`_S 4s]?=?qمuCZjszUy*M?W Ojƙʑ:gc,O#V,{+n l$.N:+SRiVTJk:l6Ἧ4J u}FH. BQ fY\ -]=%i-B}6;x,܆^%a 8#ڙWqgYw^<.1d?* ejÂ$hPz' uK-KuKM5zE{as#lI58z(#¤]LChWNShI <˜fx*FdjZ[Xtdxl{bPf>q~5:oZykp9(&zMLRZƻJLѴ+^ee}whMk4J-EP ((((((oʦoʀ"n_;FcY,O{hJ ۠#+<8A{NoFNxUpyJ*.=<И[? v$|"A¶4^ s|m$ocaJ-ar%{&zrCZ`KXʢVKJDO1'{d\Οk:ß-ֹ[j]ۤ76@FlW|9\iacm \̒D+#ؚ?xb-?KKU ݢ ܧ9lO[['d[ >xA|;3iu'"N>~AzT?>#xF[KS -̓i&Fc/ wn$^k-ͬ"||r.+TYV==/mO[Kb3#G BnWO\qm=-?SyjHzoPĶO z t+<z&|ytg2g/ i?Q-մ)#W8rCe~95jo hsڞ5y[Kis4ݹmu[[Ok>?i׷iaDxKdOf*]B"Q@Q@Q@Q@Q@Q@Q@Q@Bz.C]SBEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHii Rk6Wb͇AXho4ʵ+/.ijP (((((((((((((((((((((((9zV>Ľ+Ph3ߺ|> kX0}ጏzKy?EV/O|Q^[I&?# z@trdOOVrx@y+Z mgҭȷz/_;^NHRЫ$Tw\?pqUQvK/cn>Ԭn/m|glr}'X9g^&N55FZ$ٕ19^%ZވmnZaq+O+0 WٷsDW#U)>&rLτ9zllc@01~%xM7@m7BOꗂɢy ohR7)Jk[]$w=J<19c·Zt2^dn`=kW\w|Qi>cHS8fw"F?t6IR\oGch>-jQx3wC[m #󜎀wTKD}BUL"'|(y`W=]GP|?ޭu]NؗIwh{c9z ( ( ( ( ( ( ( ( ( ( ( ( (9KTЅt]+(((((((((((((((((((((((((ul?i]t5?@s.ijV_\ӿ*ԠAEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPr}CzV>g?=?qمusCWY@ꋹ*Yzv:Ki8a H͍jEyƻoxV<-uo9\[iҨ/s2ӵ)-%6Ds$1F>/c{s'0jmn-c'Urs\S?jK?.][z|d W]ETaʢ~Ϧڭ~T 8$x?CHgQ\qډa8'ۆ'Us.ť8J#=+&M^ʊeVW II,?w|Wş剼n^dQ3UyvW=Z/P^x}wMtM‘ ?;ŕ1ޥҾ+躅-aYbn<[ gù4Wj}>Tok5;u89m Z#ڵjsZš]c0@zRoʟo'rtޡ.{/ʀLA&h["Eyíf jO6m"@:{W XաNJ*mvh~-5 GT;Gi+{fs*2Yۀox;TВY^_c6rvgtUS Q傲&i֗/4Q\Ə=?uW#yo{@u_q[h<]Er?g{>/4Q\Ə=?uW#yo{@u_q[h<]Er?g{>/4Q\Ə=?uW#yo{@u_q[h<]Er?g{>/4Q\Ə=?uW#yo{@u_q[h<]Er?g{>/4Q\Ə=?uW#yo{@u_q[h<]Er?g{>/4Q\Ə=?uW#yo{@u_q[h<]Hk<yo롬_EŹƭxz3y\رV sN R|=VO{k 28ևƙA?@U36Ə36ƀ/QT4?4EQ?#gh?#ghGcL_cL_ U36Ə36ƀ/QT4?4EQ?#gh?#ghGcL_cL_ U36Ə36ƀ/QT4?4EQ?#gh?#ghGcL_cL_ U36Ə36ƀ/QT4?4EQ?#gh?#ghGcL_cL_ U36Ə36ƀ/QT4?4EQ?#gh?#ghGcL_cL_ U36Ə36ƀ/QT4?4EQ?#gh?#ghԽ+PjԚɿ4/_<9!+;0O )dp=+wSy)*S|-[iqII (V qҵ 󆼆O{L7L*g:{{GqucygeQwܙ ' M7WkhW˃~cukqCXSxWpqhZ?10'i#>S$<_u[M*S,K i@g[i2մ _Pzu< >nt`J"v^j^ߡSSxC╏S_P{3G\/͎?][i xWQ:tpiϖOΡ{(nޟ%g_S;ÉG:I,.Y:1cw?soST7?soI4Gb<=npj|M)mr$Dy`Oۯcn]6*~9m!{)Ҭ"Pm;ɐwnjz{k:\^;O!.lIps\$%Zi.u2Z3/9pX}zVյU Wmhy帎O9m rzW|N扭u [YuX,㐰\ANx>]?:m5%{p|6#sMdAPu ŤNF}&nNե|J,I"%9Z P'84F׷W\+Z66Ze%Qt l|6[mwmlṽT [ty?k7ԢŒVq6]nO]uj5?Z6^YEƉ!2[#/z[;_G~,hާ.ئeu{`b>Nr9^ڕzݾ mlW +_ _ aɓ:<< #(I9smwĖյ?SeW<~\;6` zwIyDj;;?CZ$fvZIZ BXJ 8u~xO^{xr}KH!E+T~Y dtVP*J ( ( ( ( ( ( ( ( ( (3 f0jj((zQJyCҧ Pǧ?Jo}|V@h(@h*no[ztztFZ:E'1\G-Ewd@c c Nʡw#o[ztzt?iGnoҏҬ|i@ǧ?J>ǧ?Jo}|V@h(@h*לL%=AL 4|'U@h(@h*no[ztzt?iGnoҏҬ|i@ǧ?J>ǧ?Jo{y3F 0"*}O4}O4r2M4nڤ RM37hGGGV~w> Vσՠ c c YM7Z>w> V)gaeZm8Ґ.n VsdGZOy6: zWq\WdAp$<=+ (((((((((oʦoʀFEpOUǠ[:+j3삼>6Ar.U #<]fa?S o>>5=ω-g&k;hm >/kjDgֿmX*ZGͪ*XtWU;%U 68z ( ( ( ( ( ( ( ( ( ( ( ( ( (3((((((((((((((((((((((((() -! w] 7BUu]뮆AZӢ) (((((q!k?/Wj뜿%] WVM)FkɄ`}|{ "Ya2_Dp7CX7ך74uKۨ{ X\ Nu=2R*G5RqMsMZWMw/*:EGwO:tZiq@cKDM+H1$ N[ϯ^iI{X܏֕yԺO[soYvB*$>dkjej[)g)"*PdU?.Js(1otk9BtFtxdBjg+q[>P5-FT_K [%@13@vJBV?WjRH1w\brU._J@x}u:,A.2x|Jn,t[k\^AnfgTbiY 8Z5KWI|y<\I \7#$wGJmxsþ.+$Ӥmf>chsq0Y~p\97 ܥЉ۰tIqZ qrio+~ ~ξ#i='⧇lٮ^y *.>{&}_ Fx?L7wWbya~LޞEKm?Y'uýJSdtbB#g*0n:(n (Š((((((((((((( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( CKHzP;AZdn1vV4hC ( ( ( ( ()\Zڂnr̎(MC[K~(wC +FFkLWmüH=ܜ[K~(-%ij:Riאy(C.:A,!}̒YGp:Iǵ[-%i}ߴ ;EƗK+}I6w9szTυ^hP)JZe?B_@zmL[yI"x R ai6jbj/B_Gn!/O 9;Czm.a =pYXQG{P4Ǡ (H/f1WjH;_P$~Yt(b[9 ~6V{˻y<yS3O.X\<}ߴ >uA ]WGn!/OysyK(81w\boWͧ* mޘzx4ϲB_@A7^0aϗ,R4nZLjnRXdY g;Y ֯n!/O_P ivi2dim+OuA di4Ȯcw?*HB1 V]!{5nE {ā]<8?²JPo2ZY+$ *c,8]q^Dvy z i(((((((((*hVc;-vuۆXD>eB im6`@_/^3ETÂ̱((}Oc)Q檫i^m=.,sN-Y$H1<>Ȍ G]ǖ Zswm:ϘЬKktQ>r~9[hϘ\mץxRgǾH:},pU'Ͻr7?+]6;k˛)Z|n ( vyUkQAuoM_ 'W~:cwGxs]@5ht, -vaZڊD0: 5n}` \hRjFf7ZamC#.sȯB%-Jg;mJx#W<'o4]}S) (((((((((((((΢((((((((((((((((((((((((((4P2EPxR)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}U0ҧ뫖x  "2$? | |Aq\?a]QEQEQEQEQEQEQEQEQE y\T y\P 't)dgG[$UgKqhV Ey?tnWW<,rd٬U;sѡVF4>o6N*n w9P2\w$iޱgs8o(ٴJ +[-JԵYw+B\MTe+g8J.Vdej"@$vD1_E=JkU1y!cԲ95wgQA-$ 2y&IdriMf+@5>ˌ cciXZms.~Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%bLQZ(1F)hbLQZ(1F)hbLQZ(1F)hbLQZ(1F)hbLQZ(1F)hbLQZ(1E-kb^}@)!+7¿»,Z(#֌Z(#֌Z(#֌Z(#֌Z(#֌Z(#֌Z(#֌Z(#֌Z(#֌Z*=j+m&Q? E :50O!>Px;W@ "9Ee?+<4ຉI'd7F'<*~Xk] ^ |dvGAϋKQ`x7Bomaϖ6̌oc=+S]5&Lա[,E2=&Т7&cʴŞ-ck6ZVi~M,m=G{#cVB^}RU2ynqۯzM[[%%-"9ָwZ]KSO{@Ye2~5sQR}]צ׿gh(xo6cg½ h##7P}AW^Eu5$"F{XQBb ?:Kk sF%eh%ehB%eh%ehB%eh%ehB%eh%ehB .#o4RҢlX-ݼ,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(((((((((((((((((((((((((((((((((((((((((((((((((((((^}ZC\W[\9q\?a]m!Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@A 33)HWU 5su;ju*̰L#qҝZ-%M. Foiw$NCyc {m_L&cq3?[ReΓ,wys߶2w 1 ;{㧹8NSIh1ˢDeS"=8#ךC(Bdi^EduW?.t? w0YG4Kn.1/MORn"αF!3ߊmka_Dcڿ_ʍ[}Z6n6O7,>+w>+/Guk0ob[1܉D,GPEatv;WQ7u aa F/`~cF 3*e%M״Tƕ3@;WQ7uq淋CU%[f$ܽG==Z\vWju*kzծ~GA$5荘lpNp=|=I}GQ-ΰXEtvMo#’ɝޥڿ_ʼkx*}j;*uL& r0}9p-Cz-8ݻoUr=%WC$$FB?/]V=J[Ku,^bdUx oxf (Ig@ k9i =I(mWW'_NSZ-Ldߔ.#z+W򨣚Yd'H%\K[o;Eė A\\uHu(_f4=7ӵ4 jOJ2V-xyp=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@Ҳok]xS@\kº0j3ÌyXHdT_i{G} O=MEC?_أ0h@QP(L/PT?i{E}>5`b=MEC?_أ0h@QP(L/PT?i{E}>5`b=MEC?_أ0h@QP(L/PT?i{E}>KQS՝OVm^%p#g֩kM%P4K> k-2[,_vGNsbO?6(NڃC<j4\iZܛ](mÞZwZi:։_hڌJmIVx,2]ğ ?$mPVw9  ]g×_' cycddu<V[<]im鷓]ù'r?1?w?6(ؓ9Eτ5=K϶Ap)c'ӵt+nyb1g1W$mQ'nrFؓ9Gğ C.ιi^QStc6O]KeH8iUI }eOlnbNpC23@[މyv|7D[iT9#yW?$mQ'nr] e֠b-o/gLf ֬5wˮ̋vDkopp 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\> 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. 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. 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. 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 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 (choose from list of available devices if more than one) 5. Select full backup and full verify 6. Erase the default backup job shown (select and delete, or use the [clear] button) 7. Select the button [file chooser] at the bottom 8. Navigate through the directories and select some files or directories to be copied + double-click a directory to open it and enable selection within that directory + select one or more files/directories, using left-mouse (or shift+left-mouse) + use the [include] button to include all selected items in the backup job + use the [exclude] button to exclude items previously included at a higher level + use the [include] button to include items previously excluded at a higher level + use the buttons at the top to go back up the directory hierarchy + use the [hidden] button to toggle the display of hidden files 9. Select the [done] button 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 - new choices will be appended. Cycle between the editor and file chooser as much 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 are exceptions to 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 File Menu \bopen job Open a previously saved backup job file for re-use (edit, run). The default location for job files is /home/user/.dkopp \bopen 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. \bedit 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. \bshow job List the current backup job data and diagnose any errors. \bsave 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. \brun job The current backup job is executed. Backup and verify modes are taken from the job. \brun 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. \bquit Exit program. Backup Menu \bfull 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. \bincremental 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. \baccumulate Same as incremental, but without DVD/BRD file deletions. Verify Menu \bfull 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. \bincremental 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. \bthorough 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) Report Menu \bget 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. \bdiffs 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. \bdiffs 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. \bdiffs 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. \blist 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. \blist DVD/BRD files All files on the DVD/BRD are listed in alphabetic sequence. \bfind 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. \bview 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. \bsave screen The main window, where messages and reports are written, is saved in a text file. Restore Menu \bsetup 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. \blist 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. \brestore 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. DVD/BRD Menu \bset 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. \bset 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. \bmount 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. \beject DVD/BRD The DVD/BRD is unmounted and ejected. \breset 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. \berase 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. \bformat 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. Help Menu \bcontents Display the help file (this file). \babout Display the dkopp program version and date. Toolbar Buttons \bedit job Shortcut to the backup job editor (same as menu File > edit job) \brun 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. \bpause and resume The currently running job or menu function may be paused and resumed. Use this to inspect output on the fly. \bkill 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. \bclear The main window, where messages and reports are written, is cleared. \bquit Exit the application. If the job file has been edited and not saved, you will be given an opportunity to save the changes. 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. \bFile 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. 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. Technical Notes \bUninstall Debian package: use command: sudo apt remove dkopp Tarball: use command: sudo make uninstall Appimage package: use command: dkopp -uninstall \bMounting 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. \bDVD 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. \bBlank 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. \bFlakey 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. \bMedia 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. \bPrivacy 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. \bCommand 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. \bDeleted 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. \bIncremental 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. \bFile 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. \bRestoring 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. \bSpecial 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. \bSpecial 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. \bKilling 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. \bDuplicate files If job file "include" statements overlap, resulting in duplicate files in the backup set, this is reported and the backup is terminated. \bMicrosoft Windows DVD/BRDs created with dkopp use the standard ISO-9660 file system, which can be read by Windows. \bDVD/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 \bIncremental 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. \bGrowisofs 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. \bLinux 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. \bBackup 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). \bDVD/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. \bgenisoimage 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. \bPhone home Dkopp will occasionally send a message to its web host for counting users. No information is kept that could be associated with a person or location. dkopp/data/widgets.css0000644000175000017500000000101413774024600013723 0ustar micomico/*** This file reduces the size of some GTK widgets by reducing unnecessary padding. You may make changes or comment-out text to restore the standard GTK widgets. Do not delete the file - it would be automatically restored. ***/ /*** disabled modifications window *, menu, box * { color: #000000; background-color: #DDDDDD; } ***/ button, .button { padding: 0px 2px 0px 2px; /* top right bottom left */ } scale { /* slider control */ padding: 6px 6px; } dkopp/debian-control0000644000175000017500000000117313774024600013463 0ustar micomicoPackage: dkopp Version: 7.7 Architecture: amd64 Section: utils Installed-Size: 1484 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.