/* * Program: EBS MARKETEER II Contacts Management Program * Programming Language: 'C' * Programmer: Robert John Morton UK-YE572246C * Date: Wed 13 Aug 2020 - Belo Horizonte-MG Project: Started: Sat 18 Apr 2020 Finished: Wed 13 Aug 2020 From the directory in which mktr.c resides: to compile: gcc mktr.c -L/usr/X11R6/lib -o mktr -lX11 -lm to run: ./mktr This listing contains the monolithiic version of Marketeer II. A GUI-daemon version is implemented [with source code] only as a commissioned project. Within this program I have deliberately used different techniques in different places in order to illustrate their various uses. The program is a compromise between widely used common functions for code economy on the one hand and loc- ally dedicated functions on the other hand in areas where easy maintenance and individual adaptability is preferable. STRUCTURE OF A MARKETEER 512-BYTE PROSPECT RECORD NOTE: if the first byte is negative [ie its high bit is set], this indicates that the record has been deleted and is available for a new name & address. Company Name 32 bytes Name & Address 14 * 32 = 448 bytes 448 Namecode 32 bytes Date fields 2 * 4 = 8 bytes 456 Premises Name 32 bytes Key Values 16 * 16-bit = 32 bytes 488 Street No/Name 32 bytes SIC Number 32 bits = 4 bytes 492 Town Name 32 bytes Knitting Needle Code = 20 bytes 512 County 32 bytes ------------------ Postcode 32-bytes Total: 512 bytes Phone Number 32 bytes Mobile Number 32 bytes Email 32 bytes STRUCTURE OF 32-BYTE INDEX RECORD Web Site 32 bytes AAAAAAAAAAAAAAAA0000000000009999 Contact Name 32 bytes A = 16-byte alphanumeric label Job Title 32 bytes 0 = 12 null characters Cross Ref 32 bytes 9 = 32-bit record number Discourse on this program at: https://robmorton.website/software/mktr.html */ #include // contains the X11 grapical drawing functions #include // input output to/from the console and keyboard #include // standard functions for the 'C' language #include // to initialise char arrays with string literals #include // for elapsed time calculation #include // for Xkb [keyboard input] #include // contains access() check if file exists in openDoc() #define WHITE 0xFFFFFF // colour white for 'selected item' lettering #define GREY 0xBBBBBB // colour grey for 'unselected item' lettering #define DARK 0x444444 // colour for menu item background #define BLACK 0x000000 // colour black for clearing unbackgrounded areas #define AMAREL 0xFFFF00 // bright yellow for messages [Portuguese AMARELO] #define YELLOW 0xDDDD00 // slightly duller yellow for field titles #define GREEN 0x00FF00 // colour green for map and success messages #define RED 0xFF0000 // colour red for error messages #define CYAN 0x66DDCC // colour for contacts META data #define RS 2048 // size [in bytes] of a name & address record #define rs 11 // the above, expressed as a power of 2 for shift multiplies #define AL 32 // total number of bytes in a name & address line record #define XL 32 // total number of bytes in an index record #define KL 32 // total number of bytes in a Key Name or Value record line #define xs 5 // the above, expressed as a power of 2 for shift multiplies #define SL 16 // length [number of characters in] an index search field #define RO 28 // record number off-set within a 32 bit index record #define LM 24 // left margin of application #define SA 327 // left margin of search box titles #define MX 440 // left margin for the post areas annunciator map #define PM 294 // left margin of second column of Profile Tab #define MY 140 // vertical datum for the post areas annunciator map #define BY 340 // vertical datum of the tops of the row of control buttons #define BH 21 // button height [pixels] #define BW 57 // button width + inter-button space [pixels] #define XT 329 // x-coord of start of message text #define YT 378 // y-coord of message text line #define VS 20 // inter-line spacing for search fields #define VN 18 // inter-line spacing for name & address lines #define VV 17 // inter-line spacing for all other Panel Numbers Display *XD; // pointer to the allocated X11 display device Window XW; // reference for the window created for this program GC XG; // X11 graphics context within which to write and draw FILE // FILE POINTERS FOR FILES THAT REMAIN OPEN THROUGHOUT A SESSION *FR, // name & address records file *FN, // namecodes index file *FP, // postcodes index file *FC, // phonenums index file *FD, // deleteds index file *FK, // Key Names and Key Values file *FX, // for any index file [for use in common index handling functions] *FY, // for value of FE used in setting up a field for editing *FI[3], // array of the 3 file pointers for switching between FN, FP, FC *FE[12]; // array of file pointers for setting up field editing int XF[3] = {1,6,7}, // line numbers of the indexed fields within a record /* The following 3 selectors can take any of the following values: -1 = no panel selected 0 = Search Panel 1 = Nane & Address panel, 2 = Key Names panel 3 = Key Values panel 4 = SIC panel 5 = Selector Key panel 6 = Event Dates panel 7 = Diary dates panel 8 = Diary comments panel 9 = Notes panel */ PN = 0, // Panel Number currently selected [initial default 0:search] PE = -1, // Panel number of the line LN most recently Edited KE = -1, // Key Edit mode: 0:no mode, 1:editing names, 2: editing values mX, mY, // x and y coordinates of where the mouse was clicked TB = 0, // number of the currently selected tab KA = 0, // keyboard ASCII value originating from a keyboard event KS = 0, // keyboard Scan Code originating from a keyboard event rd = 1, // Record number of the current name and address record. RD = 0, // number of the last record in the records file DL = 0, // total number of the last entry in the 'deleted records' file rg[18], // line lengths of the regions data // GLOBAL VARIABLES PERTAINING TO INDEX SEARCH ix = 0, // index record number of current name and address record IX = 0, // number of the last record in namecode/postcode/phonenum indexes HL = 0, // Search 'High/Low' flag. 1:search for highest exact match SR = 0, // Search Result: 0=exact match, 1=nearest match, 2=not found // GLOBAL VARIABLES PERTAINING TO THE LINE EDITOR Tx, // x-coord of start of Line Editor's text line Ty, // y-coord of start of Line Editor's text line EB, // horizontal extent of yellow editing box SB, // start byte of field currently being edited LN = -1, // Line Editor: line number of the field within the field-set ln = -1, // line number of the last line to be edited LL = 0, // number of the last line in the field-set, counting from zero LE = 0, // max field length as used by Line Editor's working array ED[] lE = 0, // current length of content in Line Editor's working array ED[] lS = 0, // current length of content in Search Field array SF[] CP = 0, // cursor position within the field currently being edited CR = 0, // 1:new-line caused by carriage-return rather than down-arrow im = 0, // insert mode flag: 1=insert mode on, 0=insert mode off KH = -1, // line number of currently highlighted Key Name NE = 0, // unset KEY NAMES editing mode: 1=on, 0=off // LAST RECORD NUMBERS FOR THE DATABASE FILES NR, // number of last record space in the name & address file NI, // number of last record space in the index files ND, // number of last record space in the 'deleted records' file // GLOBAL VARIABLES FOR THE DIARY TAB [T2] T2B = 0, // Diary Tab's Buttons illumination selector bits // GLOBAL VARIABLES FOR THE DATA TAB [T3] T3B = 0, // Data Tab's Buttons illumination selector bits // GLOBAL VARIABLES FOR THE REGION TAB [T4] T4T = 0, // Region Tab's currently selected target: 0,1,2,3,4 T4R[5], // Region Tab's 'selected regions' selector bits T4B = 0, // Region Tab's bottom buttons selector bits T4S[18], // array for region statistics [% of addresses in each region] // GLOBAL VARIABLES FOR THE TARGETS TAB [T5] T5T = 0, // Target Tab's currently selected target: 0,1,2,3,4,5,6 T5B = 0, // Target Tab's bottom buttons selector bits T5N = 0, // Target Tab's selected key name line number T5F, // Target Tab's field num 0=SIC 1=Sel Code, 2=Strt Date, 3= End Date T5V[16], // array for selected values [bits] for each key // GLOBAL VARIABLES FOR THE LETTERS TAB [T6] T6S[5], // LETTERS Tab's button states T6B = 0, // states of the bottom buttons in Tab 6 T6N = 0, // number of lines of text in the selected letter text file // VARIABLES PERTAINING TO DATES AND ELAPSED TIME rD, // reference day rM, // reference month rY, // reference year sD, // system day sM, // system month sY, // system year ET; // elapsed time between two given dates char ED[65], // the Line Editor's working array SF[33], // Search Field array to contain the current search term XE[33], // array to contain index entry being compared with the above SI[6], // for 5-digit Standard Industrial Classification in TARGET Tab SC[21], // for 20-character Selector Code in TARGET Tab D1[12], // for Start Date in TARGET Tab D2[12], // for End Date in TARGET Tab T6L[13], // Date of the current mailshot letter T6V[20] = "letters/Letter .txt", // letter file name template MO[13][4] = { "MMM", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }, T6O[5][6] = {"bmail","pmail","email","phone"," "}; // GLOBAL VARIABLE PERTAINING TO THE POST AREAS MAP short PC[120], // two-letter codes for the 120 UK post areas PS[120]; // element of PA[] at which each area's plotting data starts unsigned char PL[120]; // number of horizontal line that makes up each post area char CO[1000][2], // the 1000 plot-pairs that make up the UK & Ireland coast line PA[2376][2], // horizontal line plot-pairs that make up each post area RG[18][80]; // 80-char byte array to store region data // ---------------------------- AUXILIARY FUNCTIONS --------------------------- /* CLEAR PANEL. Clears the panel area for a given Panel Number [PN]: x[]=top left coordinate of panel, y[]=bottom left, w[]=x-extent, h[]=y extent. */ void clearPanel(int pn) { // PN = Panel Number number static int x[] = {374,119, 24,134,367,324,294, 24,112,206,374,367,324,377,377}, y[] = {238, 55, 74, 74,203,263,314, 76, 76, 76, 76,203,263,314,330}, w[] = {102,198,100,150, 36,126,182, 78,364,158,102, 36,126, 72, 72}, h[] = { 96,275,276,276, 16, 16, 32,274,274,274,274, 16, 16, 16, 16}; XSetForeground(XD,XG,DARK); XFillRectangle(XD,XW,XG,x[pn],y[pn],w[pn],h[pn]); } /* DISPLAY ERROR & ADVISORY MESSAGES Called from 1 place each in search(), trawl, nextRec, delRec, prvRec(). */ void showMsg(int x) { XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,XT - 1,YT - 12,150,15); if(x-- == 0) return; static char *M[] = { // x = 0 simply clears message area "Exact match.", // x = 1 "Nearest match.", // x = 2 "Hit top of index.", // x = 3 "Hit bottom of index.", // x = 4 "NO RECORDS FOUND", // x = 5 "Previous record deleted.", // x = 6 "INDEX CORRUPT: REBUILD", // x = 7 }; static int m[] = {12,14,17,20,16,24,22}, // message lengths c[] = {GREEN,YELLOW,YELLOW,YELLOW,RED,YELLOW,RED,WHITE}; XSetForeground(XD,XG,*(c + x)); XDrawString(XD,XW,XG,XT,YT,*(M + x),*(m + x)); } /* STRING COMPARATOR: This special version of the standard strcmp() is designed to work specifically with the requirements of this program. It only compares up to the maximum length 'SL' of the indexed field content. This function is called from 5 places in search(), from 1 place in idxRec(), from 2 places in hqs() and 1 place in iInteg(). */ int StrCmp(char *S, char *T) { int j = 0; // prime the return value for(int i = 0; i < lS; i++) { // for each character in the two strings int s = *(S + i), // character from first string to be com t = *(T + i); // pared with character from second string if(s > t) { j = +1; break; } // return +1 if S greater if(s < t) { j = -1; break; } // return -1 if S smaller if(s == 0) break; // if both NULL, return '0' [equal] } return j; } // --------------------- ANCILLARY FORMATTING FUNCTIONS ---------------------- /* COPY THE CONTENTS OF THE LINE EDITOR FIELD ED[] TO THE SEARCH FIELD SF[] Called only once each by shiftIX() and srchED(). */ void EDtoSF() { for(int k = 0; k < lE; k++) // for each character ED[] content SF[k] = ED[k]; // copy it into the corresponding SF[] element SF[SL] = (char)0; // terminate SF[]'s content with a null char lS = lE; // copy ED[]'s current content length to lS } /* CONVERT AN 'int x' TO AN l-DIGIT NUMERIC STRING WITH LEADING ZEROS IN ARRAYS[]: Called from 1 place each in editSI() and openDoc() and from 2 places each in editDT() and ixtotxt(). */ char *showInt(int x, int l) { static char S[33]; // to hold the string version of the number if(l > 11) l = 11; // safety precaution for(int i = 0; i < l; i++) S[i] = '0'; // fill the whole field with zeros S[l] = (char)0; // terminating NULL character if(x > 0) { // if the number is non-zero: int i = l; // total of y possible digits in the number while(x > 0 && // while there are more digita to process i > 0) { // work backwards from [8] to [0] S[--i] = (char) // Store remainder after dividing the number (x % 10 + 48); // by 10 as a numeric character in the array. x /= 10; // divide the number by 10 [unrounded] } // and loop back } // containing the formatted number. return S; // Address of start of char array number } // DISPLAYS THE CURRENT LINE EDITOR ARRAY CONTENT void showLine(int c) { // c = display colour for the field text XSetForeground(XD,XG,c); XDrawString(XD,XW,XG,Tx,Ty,ED,lE); } /* PURGE & REVERSE A TELEPHONE NUMBER PRESENTED IN ARRAY SF[] Called from 2 places in shiftIX(), and 1 place in srchED(). */ void prFone() { int k, c, l = 0; // char and valid char count char S[33]; // temporary array to reverse phone number for(k = 0; k < lS; k++) { // for each character in current field c = SF[k]; // get next character from editor array if(c >= '0' && c <= '9' // then provided it is a number || c == '+') // or a '+', S[l++] = c; // put char in temporary array } for(k = 0; k < l; k++) // Reverse the order of char- SF[l - k - 1] = S[k]; // cters of the phone number. SF[l] = (char)0; // add terminating NULL character lS = l; // length of purged reversed phone number } /* SETS UP THE WINDOW COORDINATES, DIMENSIONS AND FILE START BYTE 'SB' FOR EDITING A TEXT FIELD SPECIFIED BY ITS FIELD-TYPE [PN] & LINE NUMBER [LN]. TX[] & TY[] contain the horizontal and vertical coodinates within the win- dow of the first letter of the first field of a Panel Number. ML[] contains the number of lines, the maximum line length and the inter-line spacing for each field type. NL[] contains the length of each ADDRESS line. The lines [fields] of an ADDRESS have different maximum lengths. Called from 1 place each by editNA(), editSF(), editKN(), editKV(), editSI(), editKC(), editDT(), editDY(). */ void setupField() { static int TX[15] = {377,124, 30,140,370,327,297, 30,118,212,380,370,327,380,380}, TY[15] = {250, 88, 88, 88,215,275,326, 90, 90, 87, 87,215,275,326,342}, ML[15] = { 5, 13, 15, 15, 0, 0, 1, 15, 15, 15, 15, 0, 0, 0, 0}, LF[15] = { 16, 0, 15, 23, 5, 20, 11, 11, 59, 25, 15, 5, 20, 11, 11}, LS[15] = { 20, 18, 17, 17, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17}, NL[14] = {30,SL,30,30,30,30,SL,SL,30,30,30,30,30,SL}; // COMMON VARIABLES SET UP BY THIS FUNCTION Tx = TX[PN]; // x-coord of the beginning of the field Ty = TY[PN] + LN * LS[PN]; // y-coord of the base of the text field LL = ML[PN]; // maximum line number of Panel Number LE = LF[PN]; // maximum line length of Panel Number SB = rd << rs; // start byte of current contact record switch(PN) { // switch on Panel Number: case 0: clearPanel(0); break; // search case 1: SB += (LN << 5); LE = NL[LN]; break; // name & address case 2: SB = LN << 5; break; // key names case 3: SB = (((KH + 1) << 4) + LN) << 5; break; // key values case 4: SB += 488; lE = 5; break; // SIC case 5: SB += 492; lE = 20; break; // Selector Code case 6: SB += 448 + (LN << 2); lE = 11; break; // dates: next & last case 7: SB += 512 + (LN << 6); break; // diary date case 8: SB += 516 + (LN << 6); break; // diary comment case 9: SB = 8704 + (LN << 5); break; // META case 10: SB += 1536 + (LN << 4); break; // DATA case 11: lE = 5; break; // Target SIC case 12: lE = 20; break; // Target Selector Code case 13: lE = 11; break; // Target Start Date case 14: lE = 11; // Target End Date } EB = LE * 6 + 6; // width of the yellow field box } // -------------------- ANCILLARY FILE HANDLING FUNCTIONS -------------------- void helpPage(char *S) { // DISPLAY THE HELP PAGE IN THE DEFAULT WEB BROWSER int i, s = strlen(S); // length of 'mktr.html' file path char C[64] = "xdg-open https://robmorton.website/"; // 35 characters for(i = 0; i < s; i++) // add the file path for 'mktr.html' C[i+35] = S[i]; // onto the end of the base URL C[i+35] = ' '; // space after the end of the command C[i+36] = '&'; // place command in 'background' mode C[i+37] = 0; // string-terminating NULL character printf("%s\n",C); // verification print in terminal window system(C); // issue the command to the system } void getField(FILE *F, int x) { // LOAD A FIELD INTO LINE EDITOR ARRAY fseek(F,SB,SEEK_SET); // seek start byte of name & address if(x) lE = fgetc(F); // get the length of the field content if(lE > LE) lE = LE; // safety net else if(lE < 0) lE = 0; // safety net for(int k = 0; k < lE; k++) // copy the DIARY comment into the ED[k] = fgetc(F); // Line Editor's editing array ED[] ED[lE] = (char)0; } void putField(FILE *F, int x) { // SAVE LINE EDITOR ARRAY CONTENT TO DISK if(PE != PN) return; // exit if field was not modified fseek(F,SB,SEEK_SET); // start byte of name & address field if(lE > LE) lE = LE; // safety measure if(x) fputc(lE,F); // store length of field content for(int k = 0; k < lE; k++) // Copy the field content into the fputc(ED[k],F); // Line Editor's editing array ED[]. fflush(F); // in case of power failure etc. PE = -1; // reset the 'field modified' flag } /* GET AN INT FROM A 4-BYTE TRAIN IN A FILE. Called from a large number of places throughout the program */ int getInt(FILE *F) { unsigned int x // need to shift the sign bit too = fgetc(F) << 24 // shift first byte to left of int | fgetc(F) << 16 // shift second byte to 2nd from left | fgetc(F) << 8 // shift 3rd byte to 2nd from right | fgetc(F); // leave 4th byte where it is at far right return (int)x; // return as a signed integer } /* PUT AN INT TO A 4-BYTE TRAIN IN A FILE: NO FLUSH. Called from a large number of places throughout the program */ void putInt(int X, FILE *F) { unsigned int x = (unsigned int)X; // need to shift the sign bit too fputc(x >> 24 & 0xFF, F); // Store the new number of records fputc(x >> 16 & 0xFF, F); // as a 4 byte int in the first fputc(x >> 8 & 0xFF, F); // 4 bytes of the records file. fputc(x & 0xFF, F); } void putint(int x, FILE *F) { // SAVE AN INT TO FILE AS 4-BYTE TRAIN NO FLUSH fputc((x >> 24) & 0xFF, F); // Store the new number of records fputc((x >> 16) & 0xFF, F); // as a 4 byte int in the first fputc((x >> 8) & 0xFF, F); // 4 bytes of the records file. fputc(x & 0xFF, F); } int getShort(FILE *F) { // GET A 16-BIT SHORT FROM A 2-BYTE PAIR IN A FILE return fgetc(F) << 8 // shift HIGH byte to 2nd from right | fgetc(F); // leave LOW byte where it is at far right } void putShort(int x, FILE *F) { // SAVE A SHORT INT TO A FILE AS 2-BYTE TRAIN fputc((x >> 8) & 0xFF, F); // shift high byte to low byte and store it fputc(x & 0xFF, F); // store the low byte fflush(F); // flush file buffer in case of power failure } // GET THE RECORD NUMBER FROM A NAMECODE, POSTCODE OR PHONE NUMBER INDEX ENTRY int getRecNum() { fseek(FX,(ix << xs) + RO,SEEK_SET); // go to the (n)th entry return getInt(FX); // return the index entry's record number as an integer } // LOAD A NAMECODE/POSTCODE/PHONE INDEX ENTRY INTO THE XE[] ARRAY void getIndexEntry() { // called from 5 places in search(), fseek(FX,ix << xs,SEEK_SET); // set to start of record in index file for(int k = 0; k < SL; k++) // go to the nth record and load XE[k] = fgetc(FX); // the SL namecode bytes into XE[] XE[SL] = (char)0; // terminate string with a null character } // SHIFT A BLOCK OF INDEX ENTRIES ONE NOTCH UP THE INDEX FILE void shiftUp(int Hi, int Lo) { // called from newRec() & shiftIX() int j = Hi << xs, // start byte of the high entry J = Lo << xs; // start byte of the low entry char S[33]; // array to hold current index entry while(j > J) { // for each entry at and above current one: fseek(FX,j - XL,SEEK_SET); // seek start byte of next lower entry for(int k = 0; k < XL; k++) // Put the 32 bytes of this entry S[k] = fgetc(FX); // into the S[] array. fseek(FX,j,SEEK_SET); // seek start byte of current entry for(int k = 0; k < XL; k++) // Put these 32 bytes fputc(S[k], FX); // into the current entry. j -= XL; // move to start byte of previous entry } // end of while() loop } // SHIFT A BLOCK OF INDEX ENTRIES ONE NOTCH DOWN THE INDEX FILE void shiftDn(int Lo, int Hi) { // called by delRec() & shiftIX() int j = Lo << xs, // start byte of the low entry J = Hi << xs; // start byte of the high entry char S[33]; // array to hold current index entry while(j < J) { // for each entry at and above current: fseek(FX,j + XL,SEEK_SET); // seek start byte of next higher entry for(int k = 0; k < XL; k++) // Put the 32 bytes of this entry S[k] = fgetc(FX); // into the S[] array. fseek(FX,j,SEEK_SET); // seek start byte of current entry for(int k = 0; k < XL; k++) // Put these 32 bytes fputc(S[k], FX); // into the current entry. j += XL; // move to start byte of next entry } // end of while() loop } // -------------------------- INDEX SEARCH FUNCTIONS -------------------------- /* SEARCH FOR A GIVEN NAMECODE, POSTCODE OR PHONENUM WITHIN THE RESPECTIVE INDEX: Searches initially by binary slicing. If exact match not found, an inch-up/inch-down is used to home in on the exact [or nearest higher] match. This is what is required for inserting, moving or deleting an in- dex entry. However, for a user-initiated search, the user intuitively expects to be presented with the record pertaining to the first occur- rence of the search term. Therefore, in this case, a further inch-down is done to find the FIRST occurrence of the entered search term. Called by idxRec(), shiftIX(), srchED(), newRec(). */ void search() { for(int i = lS; i <= SL; i++) // Fill out remainder of search field SF[i] = (char)0; // with null chars and terminate search SR = 1; // 0 = exact match, 1 = nearest match, 2 = not found // BINARY SLICE SEARCH LOOP int x; // 3-state string comparison variable ix = IX >> 1; // start with keyword half way up INDEX[] int j = ix; // jump size starts at half INDEX[] size while((j >>= 1) > 0) { // While halved jump size still > 0: getIndexEntry(); // load namecode/postcode into XE[] x = StrCmp(SF,XE); // compare entered word with word in INDEX[] if(x > 0) ix += j; // if 'SF' > 'XE' we must look higher up else if(x < 0) ix -= j; // if 'SF' < 'XE' we must look lower down else { SR = 0; break; } // else keyword found [exact match] } // end of while() // SUPPLEMENTARY INCH-UP/INCH-DOWN SEARCH if(SR > 0) { // if not yet found, do inch-up/down search int U = 0, D = 0; // 1:going-up/going-down respectively while(U == 0 || D == 0) { // while not yet reversed direction getIndexEntry(); // load namecode/postcode into XE[] x = StrCmp(SF,XE); // compare entered word with word in INDEX[] if(x > 0) { // if 'SF' is higher than 'XE' if(++ix > IX) // if moving up overshoots top of INDEX {SR = 2; break;} // set flag to 'ABOVE TOP' and exit while() U = 1; // if not, set that we have moved up a peg } else if(x < 0) { // if 'SF' is lower than 'XE' if(--ix < 1) { // If moving down undershoots bottom of INDEX ix = 1; // then first entry is nearest higher match. SR = 3; // Indicates that we have hit the bottom of break; // the index, so break out of the while(). } D = 1; // if not, set that we have moved down a peg } else {SR = 0; break;} // else exact match has been found } // end of while() } if(SR == 0) { // if an exact match was found: /* When searching for a name & address record using one of the 3 indexes and an EXACT match is found, it is necessary to make sure that it is the FIRST exact match so that the user can intuitively search forwards through the index using only the NEXT button. */ if(HL == 0) { // if the lowest exact match is required: while(ix > 1) { // while not yet tested first index entry getIndexEntry(); // load namecode/postcode/phone num into XE[] if(StrCmp(SF,XE) > 0) // If 'SF' now higher than 'XE' { ix++; break; } // back up to the lowest exact match and exit ix--; // go to the next lower entry and try again } } /* On the other hand, when it is necessary to insert a new entry into an index, and an EXACT match has been found, it is necessary to find the entry after the FINAL exact match so it, plus all the entries above it, can be moved upwards en-bloc and the new entry inserted in its place. */ else { // find entry following highest exact match while(ix <= IX) { // while final index entry not yet tested getIndexEntry(); // load namecode/postcode/phone num into XE[] if(StrCmp(SF,XE) < 0) // if 'SF' now lower than 'XE' this is break; // the first entry above HIGHEST exact match ix++; // go to the next higher entry and try again } // end of while loop } /* When searching for a name & address record using one of the 3 indexes and only a NEAREST match is found, it is necessary to make sure it is the first HIGHER nearest match. This is ideal for browsing by the user and for the insertion of a new entry. */ } else if(SR == 1) { // else if only a nearest match was found while(ix <= IX) { // while not yet reached top of index getIndexEntry(); // load namecode/postcode/phone num into XE[] /* Exit if SF < XE, i.e. if XE is now the next higher nearest match to SF, as is required for inserting or deleting an index entry. Otherwise, move 'XE' one more notch up the index and try again. */ if(StrCmp(SF,XE) < 0) break; ix++; } // end of while() } if(TB == 0) showMsg(SR + 1); // show search result message } /* More than one index entry can exist that matches the search term. For ex- ample, more than one address can have the same postcode. Therefore it is necessary to find the particular exact-match entry that contains the rec- ord number of the record whose index entry is to be moved or deleted. */ int idxRec() { // called from 1 place each in delRec() & shiftIX() HL = 0; search(); // search for the lowest exact match if(SR > 0) { // not exact match showMsg(7); // show the INDEX CORRUPT message ix = 0; return 0; // zero index number indicates error } while(ix <= IX) { // while not yet reached top of index if(rd == getRecNum()) // If this is the index entry of the record being break; // stored or deleted, break out of this while(). if(++ix > IX) { // If already tested final entry in the index showMsg(7); // show the INDEX CORRUPT message and ix = 0; return 0; // exit the index entry removal function. } } getIndexEntry(); // load the (ix)th namecode/postcode/phonenum into XE[] /* If the search term mis-matches, then all the matching entries have been examined without encountering the one containing the required record number. This can only mean that the index file has become corrupted, so display the INDEX CORRUPT message and exit with a zero return value. If you see this msg, go to the MAINT tab to rebuild the index. */ if(StrCmp(SF,XE) != 0) { showMsg(7); return 0; } return ix; } /* SHIFTS A MODIFIED INDEX ENTRY TO ITS NEW POSITION WITHIN ITS INDEX FILE. When the content of an indexed field has been edited, it is unlikely to be still in the correct position [in alphabetical order] within its index file. It must therefore be deleted from its old position and reinserted at its new position within the alphabetical order of its index. The inter- vening block of entries between its old and new positions must be moved down or up by one place according to whether the new position be higher or lower than the old position. Called only from 3 places in 1 function: editNA() case 2: */ void shiftIX(int R) { // R = start byte of the relevant indexed field int k; // Load original version of modified index field into SF[] for search(). fseek(FR,R,SEEK_SET); // start byte of the indexed field being edited lS = fgetc(FR); // length of field's original textual content for(k = 0; k < lS; k++) // Put original content of indexed field before SF[k] = fgetc(FR); // it was edited in the Line Editor array SF[] SF[lS] = (char)0; // in order to find its entry within the index. if(LN == 7) prFone(); // if phone number, purge and reverse it // Search the index for the exact match entry with correct record number. if(idxRec() == 0) // Bail out if unable to find an exact return; // match with the correct record number. int Old = ix; // save it as the ORIGINAL index position // Find position where new modified entry must be inserted in index 'FX': EDtoSF(); // copy Line Editor content into Search Field array if(LN == 7) prFone(); // if phone number, purge and reverse it HL = 1; // Find position [ix] of the entry immediately search(); // above the search term. [Uses 'FX'] int New = ix; // position for the EDITED index entry /* If OLD and NEW are equal, both OLD and NEW are returned by the search() function as exact matches, so don't move anything. */ if(Old == New) return; /* SHIFT THE INTERVENING BLOCK OF INDEX ENTRIES ONE PLACE UP OR DOWN. If the NEW entry is lower down the index than the OLD entry, shift the block of entries UP over the old entry at the top to make way for the NEW entry at the bottom. */ if(Old > New) shiftUp(Old, New); /* If the NEW entry is higher up the index than the OLD entry, then first decrement the NEW entry's index number because the value of ix returned by the search() function is the NEXT POSITION HIGHER than where the edited entry should be put. Then shift the block of entries down over the old entry at the bottom to make way for new entry at the top. */ else { New--; shiftDn(Old, New); } /* Insert the new entry at position 'NEW' in the index: Note that the search() function has already padded out this new entry with NULLs to the 16 characters standard length 'SL'. */ int x = New << xs; // start byte for new index entry fseek(FX,x,SEEK_SET); // seek start byte for new index entry for(k = 0; k < SL; k++) // Store the new entry's SL-byte fputc(SF[k], FX); // edited namecode/postcode/phonenum. fseek(FX,x + RO,SEEK_SET); // seek start byte of entry's record number putInt(rd, FX); // store entry's 4-BYTE record number /* NOTE: The total number of entries in an index does not change as a result of this operation. */ } // T0-------------- FUNCTIONS PERTAINING TO THE ADDRESS TAB ------------------ /* DISPLAY OR ERASE THE YELLOW 'INSERT' MESSAGE Called from MEH(), KEH(), ins(), lfrt(), updn(), editSF() case 0 */ void insMsg(int i) { static int pn[] = {1,0,0,0,1,1,1,1,0,0,0}; int h, // horizontal coordinate of start of text field c = BLACK; // default background colour for INSERT message if(pn[PN]) return; // exit if INSERT function blocked in this panel switch(PN) { // Panel Number case 1: h = 276; c = DARK; break; // ADDRESS case 2: h = 88; break; // KEY NAMES case 3: h = 245; break; // KEY VALUES case 8: h = 434; break; // DIARY COMMENT case 9: h = 325; break; // META case 10: h = 434; break; // DATA } if(i == 0) { // if INSERT mode is being cancelled XSetForeground(XD,XG,c); // erase the word 'INSERT' XFillRectangle(XD,XW,XG,h,60,36,11); } else { // if INSERT mode is being set XSetForeground(XD,XG,GREEN); // show the word 'INSERT' in bright green XDrawString(XD,XW,XG,h,70,"INSERT",6); } im = i; // set/reset insert mode as required } /* WIPE THE NAME AND ADDRESS AREA AND SHOW THE RECORD NUMBER: Called from 1 place each in showNA() and newRec(). */ void clearNA() { char S[33]; clearPanel(1); // display the blank name & address panel XSetForeground(XD,XG,WHITE); // Display record number in white int l = sprintf(S,"%i of %i [%i+%i]",rd,RD,IX,DL); XDrawString(XD,XW,XG,124,70,S,l); } /* DISPLAY THE COASTLINE FOR POST AREAS ANNUNCIATOR MAP. Loads the required coordinate data [from arrays] from which it constructs and displays, in green, the coastal outline of the United Kingdom. */ void coastLine() { int oldx = (int)CO[0][0], // prime with first pair of co-ordinates oldy = (int)CO[0][1]; // in the map outline data XSetForeground(XD,XG,DARK); // paint map background XFillRectangle(XD,XW,XG,MX - 113,MY - 85,149,175); XSetForeground(XD,XG,GREEN); // colour for the map outline for(int k = 1; k < 1000; k++) { // for each point on the coastal outline int x = (int)CO[k][0]; // get x-co-ord of next point on CO if(x < 120) { // 120 is the discontinuity marker int y = (int)CO[k][1]; // get y-co-ordinate XDrawLine(XD,XW,XG, // Draw a line MX + oldx, MY + oldy, // from the previous point MX + x, MY + y); // to the new point. oldx = x; oldy = y; // set old x,y = new x,y for next time } else { // < 120 indicates a discontinuity k++; // in the map outline, so skip over it oldx = (int)CO[k][0]; // set first x, y of new run as old oldy = (int)CO[k][1]; // plot and add in the required biases } } XSetForeground(XD,XG,GREY); // DRAW THE BOX ROUND ORKNEY & SHETLAND int a = -5, b = -26, c = 30, q = -73; XDrawLine(XD,XW,XG,MX + a,MY + b,MX + c,MY + b); XDrawLine(XD,XW,XG,MX + a,MY + q,MX + c,MY + q); XDrawLine(XD,XW,XG,MX + a,MY + b,MX + a,MY + q); XDrawLine(XD,XW,XG,MX + c,MY + b,MX + c,MY + q); } /* SHADE IN THE APPROPRIATE POST AREA ON THE COASTLINE MAP. The first 16 bits of the integer variable 'pa' contain the 8-bit + 8-bit ASCII values of the 2-letter Post Area code. Displays in bright yellow the post area of the address given in name & address record 'rd'. */ void postArea(int pa) { for(int i = 0; i < 120; i++) // for each entry in post area index if(pa == (int)PC[i]) { // if it matches presented post area int j = (int)PS[i]; // start of this post area's plots XSetForeground(XD,XG,AMAREL); // colour for the post area int x = (int)PL[i], x1, x2; // horizontal lines in this post area for(int k = 0; k < x; k += 2) // for each of these lines: if((x1 = (int)PA[j + k][0]) < 28 && (x2 = (int)PA[j + k + 1][0]) < 28) XDrawLine(XD,XW,XG, // draw the line from: MX + x1, MY + (int)PA[j + k][1], // start of horiz scan MX + x2, MY + (int)PA[j + k + 1][1] // to end of horiz scan ); break; // bail out of the for(i) loop } } /* LOAD & DISPLAY A NAME & ADDRESS RECORD. Loads & displays, field-by-field, the name and address record 'rd'. Called from 1 place each in srchED(), nextRec(), trawl(), T0show() and nxtRec(), MET0SF(), and from 2 places in prevRec(). */ void showNA() { clearNA(); // clear the name and address box int R = rd << rs, // off-set of current NA record Q = 0, // byte offset within record pa, // for short int post area representation v = 88; // vertical start position char S[33]; // local scratchpad array XSetForeground(XD,XG,WHITE); // colour for the name & address lines for(int i = 0; i < 14; i++) { // for each field of the record fseek(FR,R + Q,SEEK_SET); // set to start of required record int L = fgetc(FR); // get length of field content from record if(L > 30) break; // record corrupt for(int j = 0; j < L; j++) // for each char in field S[j] = fgetc(FR); // next char of current field if(i == 6) // save post area for annunciator map pa = (int)S[0] << 8 | (int)S[1]; XDrawString(XD,XW,XG,124,v,S,L); v += VN; // advance to next lower line Q += AL; // advance to next field within record [32] } coastLine(); // draw the UK coastline in green postArea(pa); // shade in the appropriate Post Area in bright yellow } /* TRAWL THE RECORDS FILE FOR THE PRESENTED SEARCH TERM Called from 1 place each in editSF() & nxtRec(). */ void trawl() { int Flag = 0; // 'search term captured' flag int I = RS; // start byte of first record char S[33]; // local scratchpad array for(rd = 1; rd <= RD; rd++) { // for each record in records file: sprintf(S,"Trawled %i of %i recs.",rd,RD); showMsg(0); // Clear message area then XSetForeground(XD,XG,WHITE); // display the record count in white. XDrawString(XD,XW,XG,XT,YT,S,strlen(S)); int j, J = 0; // start byte of first field for(j = 0; j < 14; j++) { // for each of the record's 14 fields fseek(FR,I + J,SEEK_SET); // locate start of field int n = fgetc(FR); // get length of field from record if(n < 0 || n > 30) break; // record corrupt for(int k = 0; k < n; k++) // Read the content of the field S[k] = fgetc(FR); // into the S[] character array. S[n] = (char)0; // terminate completed string with a NULL if(strstr(S,ED) != NULL) { // If test string found in field string Flag = 1; break; // set CAPTURED flag; break out of loop. } J += AL; // advance to start of next field } // end of j-loop if(Flag > 0) // if the search term was found in the current record break; // break out of the records search rd-loop I += RS; // advance to start byte of next record } if(Flag > 0) showNA(); // if search term found, show name & address } /* SEARCH THE APPROPRIATE INDEX FOR THE SEARCH TERM PRESENTED IN SF[] Called from 3 places in editSF() case 0. */ void srchED() { EDtoSF(); // copy ED[] content to SF[] for search if(LN == 3) // if doing phone index prFone(); // purge and reverse phone number HL = 0; // Find the first exact match or the next search(); // higher nearest match to the search term. rd = getRecNum(); // get record number from index entry 'ix' showNA(); // display the name and address details } /* GET THE RECORD NUMBER 'rd' OF THE NEXT NON-DELETED RECORD IN THE RECORDS FILE THEN DISPLAY THE RECORD: Called from 1 place each in editSF(), delRec(), nxtRec(). */ void nextRec() { int x = 0; // counter to avoid infinite looping int S = rd << rs; // start byte of current record while(x++ < RD) { // to avoid infinite looping if(rd > RD) { // if reached beyond last of records rd = 1; // loop back to start of index S = RS; // go to start byte of first record } fseek(FR,S,SEEK_SET); // go to start byte of the (rd)th record if((fgetc(FR) & 0x80) == 0) // if first bit of record not set break; // found next non-deleted record rd++; // otherwise try the next record in sequence S += RS; // increment to start byte of next record } // end of while(x++) if(x >= RD) { // if checked all the records on file showMsg(5); // NO RECORDS FOUND message rd = 1; // default record number } showNA(); // display name & address record } /* THREE AUXILIARY FUNCTIONS FOR SETTING UP AND MANGING LINE EDITING drawFld() draws the field background and is also used to de-box a field and clear the cursor. drawBox() draws the yellow box around the selected field to indicate that it has focus for editing. drawCur() draws the green cursor in the prescribed position 'CP' within the field. They all refer only to global variables. All are called from multiple places in each of editNA(), editKN(), editKV(), editSI(), editKC() and editDT(). */ void drawFld() { XSetForeground(XD,XG,DARK); // dark background to Key Values field XFillRectangle(XD,XW,XG,Tx - 3,Ty - 12,EB,16); // de-boxes by default } void drawBox() { XSetForeground(XD,XG,AMAREL); // yellow box for Key Name field XDrawRectangle(XD,XW,XG,Tx - 3,Ty - 12,EB - 2,15); } void drawCur() { if(CP > lE) CP = lE; // cursor must not be beyond content int x = Tx - 1 + CP * 6; // compute horizontal position of cursor XSetForeground(XD,XG,GREEN); // draw the cursor XDrawLine(XD,XW,XG,x,Ty - 11,x,Ty + 1); } /* PREPARES A NAME & ADDRESS FIELD FOR EDITING by setting the parameters that determine the field's position within the appropriate TAB plus the lengths of the field and of its current content. It is called with a switch value that invokes one of 4 cases. In all cases the field's area first is cleared to the DARK background. case 0: Simply loads the original content of the field into the Line Editor's working array ED[]. case 1: Additionally boxes the field in yellow and places the cursor immediately before the character that was clicked or, if clicked beyond the content, after the last character of the content. case 2: Saves the content of the Line Editor's working array to the appropriate name & address record field. If the field being editied is an indexed field, it first recalls the original content of the field, then calls shiftIX() to reposition the index entry within the appropriate index file. case 3: Re-display the yellow box and green cursor + field content without re-loading from disk. In all cases, the field content is finally re-displayed. Called from 1 place in newRec(), 4 places in MET0NA(), 1 place in escape() and 2 places in updn(). */ void editNA(int sw) { // switch: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] PN = 1; // set to name & address panel setupField(); // set up required field for editing drawFld(); // dark background for field switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box the field ready for editing case 0: // load original field content into ED[] getField(FR,1); // get the field content from the file if(sw == 1) drawCur(); // if we entered via case 1: set cursor break; case 2: // save name & address field if(PE == 1) { // if the field has been modified switch(LN) { case 1: FX = FN; shiftIX(SB); break; // namecode case 6: FX = FP; shiftIX(SB); break; // postcode case 7: FX = FC; shiftIX(SB); // phone number } } // NOTE: PE is set by the Line Editor putField(FR,1); // store the field content to the file break; case 3: drawBox(); drawCur(); // display yellow box and green cursor } showLine(WHITE); // display the field's text content in white } /* PREPARES SEARCH FIELD 'LN' FOR EDITING by setting the parameters that det- ermine the field's position within the ADDRESS Tab plus the lengths of the field and of its current content. It is called with a switch value that in- vokes one of 3 cases. In all cases the field's area first is cleared to the DARK background. case 0: Clears the Line Editor's working array ED[] by filling it with NULL characters. Boxes the field in bright yellow. Sets the cursor position to the start of the field. Displays the cursor. case 1: Redisplays the yellow box around the field. Erases the cursor. Invokes the required search. case 2: Simply leaves the search field clear. Called from 1 place each in T0show(), newRec(), xref(), MET0NA(), MET0CB(), escape(), from 2 places in MET0SF() and 3 places in updn(). */ void editSF(int sw) { // switch: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] PN = 0; // set to search panel setupField(); // set up the field for the Line Editor switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box the field ready for editing for(int k = 0; k <= LE; k++) ED[k] = (char)0; // clear the Line Editor's array ED[] lE = 0; // set content length to zero CP = 0; // set cursor to start of field drawCur(); // draw the green cursor break; case 2: // search on the entered search term insMsg(0); // clear possible INSERT message drawBox(); // draw yellow box round field switch(LN) { // switch on search field 0 to 4 inclusive case 0: // record number rd = atoi(ED); // extract the record number 'rd' nextRec(); // retrieve and display the record break; case 1: // namecode FX = FN; // namecode index file pointer srchED(); // search on search term in ED[] break; case 2: // postcode FX = FP; // postcode index file pointer srchED(); // search on search term in ED[] break; case 3: // phone number FX = FC; // phone index file pointer srchED(); // search on search term in ED[] break; case 4: trawl(); // trawler for the search term in ED[] } PE = -1; // set to 'no field has been modified' showLine(YELLOW); // re-display the field content in yellow break; } // end of switch } /* PAINT THE INITIAL CONTENT OF THE ADDRESS TAB Called only from one place within showTab(). */ void T0show() { const char *S[] = { "Record Number", "Company Name ", "SEARCH CODE ", "Premises Name", "Street ", "Town ", "County ", "POSTAL CODE ", "PHONE NUMBER ", "Mobile Phone ", "Email Addr. ", "Web Site ", "Contact Name ", "Job Title ", "Cross Ref. " }; // DISPLAY THE ANNOTATION TEXT FOR EACH OF THE NAME & ADDRESS LINES int v = 70; // vertical coordinate of the first [Record Number] line XSetForeground(XD,XG,YELLOW); // show the line annotations in yellow for( int i = 0; i < 15; i++) { // for each of the 15 name & address lines XDrawString(XD,XW,XG,LM,v,*(S + i),13); // display its text v += VN; // advance vertically downwards to the next line of text } // DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM XSetForeground(XD,XG,DARK); // shade for buttons background for(int i = 0; i < 8; i++) // shade the background area for each button XFillRectangle(XD,XW,XG,24 + i * BW,BY,53,BH); const char // annotate horizontal row of control buttons *A[] = {"DELETE","HELP","NEW","COPY", "XREF","PREV","NEXT","CLEAR"}; const int a[] = {-5, 0, 3, 0, 0, 0, 0,-3}, // horiz offsets for word starts b[] = { 6, 4, 3, 4, 4, 4, 4, 5}; // number of letters in each word XSetForeground(XD,XG,GREY); // colour for the lettering for(int i = 0; i < 8; i++) // for each of the 8 buttons: XDrawString(XD,XW,XG,38 + i * BW + a[i],355,A[i],b[i]); // DISPLAY THE 5 SEARCH FIELDS AT LOWER RIGHT XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,SA,250,"RecNum",6); XDrawString(XD,XW,XG,SA,270,"S Code",6); XDrawString(XD,XW,XG,SA,290,"P Code",6); XDrawString(XD,XW,XG,SA,310,"Phone",5); XDrawString(XD,XW,XG,SA,330,"Trawl",5); showNA(); // display the default name and address record PN = 0; // Panel Number = search field LN = 1; // line number of the namecode [default] search field editSF(1); // highlight the default search field // DRAW THE LIVE LINK WEB SITE ADDRESS FOR THE USER MANUAL XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG,LM,378, "https://robmorton.website/" "software/mktr.html",43); } // -------------------- FIELD CONTENT VALIDATION FUNCTIONS -------------------- /* ALLOW THROUGH ONLY LETTERS A TO Z, a TO z, NUMBERS 0 TO 9 '+' & SPACE Called from 2 places in valSF() and 1 place each in valNA() and KEH(). */ int A9S(int c) { if(c >= 'a' && c <= 'z') c -= 32; // if lower case, make it upper if(!(c >= 'A' && c <= 'Z' // If NOT: a capital letter, || c >='0' && c <= '9' // a number 0 to 9, || c == ' ' || c == '-' // a space, a dash || c == '+')) return -1; // or a '+', then return error code return c; // otherwise, return the character } /* ALLOW THROUGH ONLY CHARACTERS THAT CAN CONSTITUTE A PHONE NUMBER Called from one place each in valSF() and valNA(). */ int fone(int c) { if((c < '0' || c > '9') // if entered character is not a number && c != ' ' // and not a space character ' ' && c != '-' // and not a dash '-' && c != '+') return -1; // and not a '+', then exit return c; // return the valid character } /* VALIDATE THE CONTENT OF A SEARCH FIELD ENTRY Called only fom 1 place in KEH(). */ int valSF() { switch(LN) { // START OF FIELD CASES case 0: // record number if(KA < '0' || KA > '9') // if entered character is not a number return -1; break; case 1: // NAMECODE field if((KA = A9S(KA)) == -1) // exit if not A-Z 0-9 or ' ' return -1; break; case 2: // POSTCODE field if((KA = A9S(KA)) == -1) // exit if not A-Z 0-9 or ' ' return -1; break; case 3: // Phone Number Field if((KA = fone(KA)) == -1) // exit if not 0-9, '-' ' ' '+' return -1; break; } return 0; // return that field is valid } /* VALIDATE THE CONTENT OF A NAME & ADDRESS FIELD Called only fom 1 place in KEH(). */ int valNA() { switch(LN) { // START OF FIELD CASES case 1: // NAMECODE field case 6: // POSTCODE field case 13: // XREF field if((KA = A9S(KA)) == -1) // exit if not A-Z 0-9 or ' ' return -1; break; case 7: // Phone Number Field case 8: // Mobile Number Field if((KA = fone(KA)) == -1) // exit if not 0-9, '-' ' ' '+' return -1; } return 0; // return that field is valid } /* --------------- FUNCTIONS CONCERNING THE EXTENDING OF FILES --------------- EXTEND THE RECORDS, INDEXES AND DELETEDS FILES: Files opened in random mode cannot be extended using only basic 'C' utilities. Thus, when a new record must be added to the END of the records file, it is necessary to close the file and re-open it in 'append' mode, fill a record's worth of space with NULL characters, close the file in 'append' mode and re-open it in 'random access' mode. In reality, I always add 16 records worth of space to avoid having to go through this process too often. GET NUMBER LAST AVAILABLE RECORD SPACE IN THE NAMES & ADDRESSES FILE Called only from 1 place each in newRec() and initLoad(). */ void getNR() { fseek(FR,0,SEEK_END); NR = (int)((ftell(FR) >> rs) - 1); } // EXTEND THE RECORDS FILE: called from only 1 place in newRec() void extendNR() { while(NR < RD) { // while insufficient space for a new name+address record fclose(FR); // close file in 'random access' mode FR = fopen("db/contacts.nad","a"); // re-open it in 'append' mode int J = RS << 4; // extend file by 16 x 2048 bytes for(int j = 0; j < J; j++) // Fill the appended space fputc(0,FR); // with NULL characters. fputc(0,FR); // write EOF character fclose(FR); // close the file in 'append' mode FR = fopen("db/contacts.nad","rb+"); // re-open it in 'random access' mode } getNR(); // get number of records for which there is available space } /* GET NUMBER OF LAST AVAILABLE RECORD SPACE IN THE INDEX ENTRIES FILES Called only from 1 place each in newRec() and initLoad(). */ void getNI() { fseek(FN, 0, SEEK_END); NI = (int)((ftell(FN) >> xs) - 1); } // EXTEND THE 3 INDEX FILES: called from only 1 place in newRec() void extendNI() { static char *S[] = { "db/contacts.name","db/contacts.post","db/contacts.fone" }; while(NI < IX) { // while insufficient space for new index entry for(int i = 0; i < 3; i++) { // for the 3 index files: fclose(*(FI + i)); // close file in 'random' mode FX = fopen(*(S + i),"a"); // re-open it in 'append' mode int J = XL << 4; // extend it by 16 x 32-bytes for(int j = 0; j < J; j++) // Fill the appended space fputc(0,FX); // with NULL characters. fclose(FX); // close file in 'append' mode *(FI + i) = fopen(*(S + i),"rb+"); // re-open it in 'random' mode } } getNI(); // get number of records for which there is available space } /* GET THE NUMBER OF LAST AVAILABLE RECORD SPACE IN 'DELETEDS' FILE Called only from 1 place each in delRec() and initLoad(). */ void getND() { fseek(FD, 0, SEEK_END); ND = (int)((ftell(FD) >> 2) - 1 ); } // EXTEND THE 'DELETEDS' FILE: called only from 1 place in delRec() void extendND() { while(ND < DL) { // while insufficient space for a new 'deletes' entry fclose(FD); // close file in 'random' mode FD = fopen("db/contacts.del","a"); // re-open it in 'append' mode int j, J = 64; // extend file by 16 x 4 bytes for(int j = 0; j < J; j++) // Fill the appended space fputc(0,FD); // with NULL characters. fclose(FD); // close file in 'append' mode FD = fopen("db/contacts.del","rb+"); // re-open it in 'random' mode } getND(); // get number of records for which there is available space } // T0------------ FUNCTIONS PERTAINING TO THE NAME & ADDRESS TAB -------------- /* DELETE A NAME AND ADDRESS RECORD FROM THE RECORDS AND INDEX FILES To delete a record, simply set the most significant bit of its first byte. The rest of the content of the record remains untouched. Then find and remove its entries from each of the 3 index files. */ void delRec() { // called from only one place in MET0CB() // SET THE HIGH BIT OF THE FIRST BYTE OF THE CURRENT RECORD int s = rd << rs; // start byte of current record fseek(FR,s,SEEK_SET); // seek start byte of current record int x = fgetc(FR) | 0x80; // set the high bit of the first byte fseek(FR,s,SEEK_SET); // seek start byte of current record fputc(x, FR); // restore the first byte // IF NECESSARY, REGISTER THE RECORD'S NUMBER IN THE DELETEDS FILE if(rd < RD) { // provided this is not last record of file if(ND <= DL) // If insufficient space for new 'deleteds' extendND(); // entry, extend file space by 16 records. fseek(FD,0,SEEK_SET); // seek the start byte of 'deleteds' file putInt(++DL, FD); // incremented number of deleted records fseek(FD,DL << 2,SEEK_SET); // seek the start byte of new last record putInt(rd, FD); // save number of the deleted record here fflush(FD); // flush deleteds file in case power fails } else { // else, if this is the last record in file fseek(FR,0,SEEK_SET); // seek the start byte of records file putInt(--RD, FR); // store decremented total number of records } fflush(FR); // flush records file in case power fails PE = -1; // reset the 'field modified' flag // EXPUNGE RECORD'S INDEX ENTRY FROM EACH OF THE THREE INDEXES int Flag = 1, i, k; // set Flag to indicate an exact match found for(i = 0; i < 3; i++) { // collapse namecode/postcode/phone indexes: /* Seek the start byte of the indexed field being dealt with on this pass of the i-loop. 's' is the start-byte of the name & address record being deleted. XF[i] is the field number, within the name & address record, of the indexed field being dealt with this pass of the i-loop. 'xs' is the shift that multiplies the line number by 32 to get the start-byte of the index field within the name & addrss record. */ fseek(FR,s + (XF[i] << xs),SEEK_SET); lS = fgetc(FR); // length of field's original textual content for(k = 0; k < lS; k++) // Put original content of indexed field before SF[k] = fgetc(FR); // it was edited in the Line Editor array SF[] SF[lS] = (char)0; // in order to find its entry within the index. if(i == 2) prFone(); // if phone number, purge and reverse it FX = *(FI + i); // get the appropriate index file pointer if(idxRec() > 0) { // if an exact match with correct record number shiftDn(ix, IX); // collapse the index over the deleted entry fseek(FX,0,SEEK_SET); // seek start byte of index file putInt(IX - 1, FX); // decrement the number of index entries fflush(FX); // flush to disk in case of power failure } else Flag = 0; // indicate that an exact match was not found } // end of for(i) if(Flag == 1) { // if exact match with correct record number was found IX--; // decrement the top record number of the index files nextRec(); // get and display the NEXT record in record number order showMsg(6); // show the PREVIOUS RECORD DELETED message } } /* CREATE A NEW NAME & ADDRESS RECORD: If 1 or more deleted record spaces exist in the records file, use one of these for the new record. If not, add a new record to the end of the records file. Clear out any old data from the new record's storage space. Add entries in the 3 index files for the new record's indexed fields. Fill each of these new entries with indicative dummy characters. Called from only one place in MET0CB(). */ void newRec() { char *S; // to hold dummy field entries editSF(0); // clear any highlighted search field PN = 1; // Panel Number = 'name & address' // FIND A NEW AVAILABLE RECORD WITHIN THE RECORDS FILE if(DL > 0) { // if at least 1 spare record available fseek(FD,DL << 2,SEEK_SET); // seek the start byte of the last entry rd = getInt(FD); // get number 'rd' of last spare record } else { // else, need to extend the records file if(NR <= RD) // if insufficient space for new record extendNR(); // extend the file space by 16 records rd = ++RD; // set number of the new record fseek(FR,0,SEEK_SET); // start byte of records file putInt(RD,FR); // store the new total number of records fflush(FR); // flush to secure against power failure } // CLEAR OUT ANY OLD NAME & ADDRESS DATA FROM THE RECORD int R = rd << rs, J = 0, // start bytes of new record and (i)th field i, j; // loop variables for(i = 0; i < 14; i++) { // for each of the 14 fields fseek(FR,R + J,SEEK_SET); // start byte of current field fputc(3,FR); // store length of dummy field = 3 bytes if(i == 7) S = "000"; // dummy phone number else S = "NEW"; // dummy for all other fields fprintf(FR,"%s",S); // print the dummy field entry to file J += AL; // advance to start of next field } // CLEAR OUT ANY OLD PROFILE DATA FROM THE NEW RECORD fseek(FR,R + 448,SEEK_SET); // start byte of profile data for(i = 448; i < 512; i++) // fill the rest of the record fputc(0,FR); // with NULL characters // CHECK THAT THERE IS ENOUGH FREE SPACE IN THE INDEX FILES if(NI <= IX) // if insufficient space for a new index entry extendNI(); // extend the space in the index files by 16 records // INSERT DUMMY INDEX ENTRIES FOR THE NEW RECORD for(i = 0; i < 3; i++) { // for each of the 3 indexes: FX = *(FI + i); // get the appropriate index file pointer if(i == 3) { SF[0] = '0'; SF[1] = '0'; SF[2] = '0'; } else { SF[0] = 'N'; SF[1] = 'E'; SF[2] = 'W'; } SF[3] = (char)0; // string-terminating NULL character lS = 3; // length of search string [vital] HL = 1; // Find the position [ix] of the entry search(); // immediately above the search term. shiftUp(IX + 1, ix); /* Pull all entries, at and above 'ix', one place higher up in the index. Insert the new entry at the appropriate position in the index: Note that search() above has already padded out SF[] with NULLs to SL chars. */ R = ix << xs; fseek(FX,R,SEEK_SET); // seek start byte of new entry position for(j = 0; j < SL; j++) // Put the SL bytes of the new entry in fputc(SF[j], FX); // to the (ix)th position in the index. fseek(FX,R + RO,SEEK_SET); // seek start byte of record number position putInt(rd, FX); // store the entry's 4-byte record number // Increment the total number of index entries for the current index: fseek(FX,0,SEEK_SET); // seek the start byte of the index file putInt(IX + 1, FX); // store incremented number of index entries fflush(FX); // flush to disk in case of power failure } IX++; // increment the total number of index entries showNA(); // display the new blank name & address record LN = 0; // first line of the name & address editNA(1); // Show highlighted field box & load ready for editing } /* GO TO THE CROSS-REFERENCED NAME & ADDRESS RECORD [XREF BUTTON CLICKED] Called from only one place in MET0CB(). */ void xref() { fseek(FR,(rd << rs) + 416,SEEK_SET); // start byte of XREF in record 'rd' int L = fgetc(FR); // length of the current content of the XREF field lE = 0; // zero the length of the search field content for(int i = 0; i < L; i++) { // for each of the up to I chrs of an XREF char c = fgetc(FR); // get char from XREF line in ED[] array if(c >= 'A' && c <= 'Z' // provided it is either a capital letter || c >= '0' && c <= '9') // or a number ED[lE++] = c; // transfer it to the Line Editor array else break; // Otherwise break the loop because we've } // reached end of the valid characters. ED[lE] = (char)0; // terminate search term with a NULL char PN = 0; // set field type to 'search field' LN = 1; // namecode is search field 1 editSF(2); // show the XREF in the namecode search field */ } /* GO TO PREVIOUS NAME & ADDRESS RECORD ACCORDING TO THE CURRENT INDEX TYPE Called only from one place in MET0CB(). */ void prvRec() { switch(LN) { // switch according to which search field case 0: { // Record Number field int x = 0; // tries counter to avoid infinite looping while(x++ < RD) { // while not yet looped round the while file if(--rd < 1) // if reached beginning of records rd = RD; // loop back to end of index fseek(FR,rd << rs,SEEK_SET); // go to start of (rd)th record int v = fgetc(FR) & 0x80; if(v == 0) // if first bit of record not set break; // found next non-deleted record } if(x >= RD) { // if checked all the records on file showMsg(5); // NO RECORDS FOUND message rd = 1; // default record number } showNA(); // display name & address record } break; case 1: // Namecode, postcode and phonenum fields case 2: // require the same action to be taken. case 3: if(--ix < 1) // if reached beginning of index ix = IX; // loop back to end of index rd = getRecNum(); // get record number from index number showNA(); // display the name & address break; } // end of switch(LN) Search Field switch } /* GO TO NEXT NAME & ADDRESS RECORD ACCORDING TO THE CURRENT INDEX TYPE Called from only once place in MET0CB(). */ void nxtRec() { switch(LN) { // switch according to which search field case 0: rd++; // get the next record in record number order nextRec(); // skips over deleted records break; case 1: // Namecode, postcode, phone number fields case 2: // require the same action to be taken. case 3: if(++ix > IX) // if reached end of index ix = 1; // loop back to beginning of index rd = getRecNum(); // get record number from index number showNA(); // display the name & address break; case 4: trawl(); // continue trawling from current record } // end of switch(LN) } /* IF THE MOUSE HAS BEEN CLICKED WITHIN ONE OF THE 5 SEARCH FIELDS Called from only one place in MEH() case 0. */ void MET0SF() { if(mY < 230 || mY > 330) return; // outside search fields area PN = 0; // field type is 'search field' LN = -1; showNA(); // de-box all name & address fields editSF(0); // clear the 5 search fields int x = 238, y = x + 16; // datums for top & bottom of fields for(int i = 0; i < 5; i++) { // for each of the 5 search fields int z = i * VS; // 20 pixels between search fields if(mY < y + z) { // if mouse clicked within this field LN = i; break; // note the field number and exit loop } } editSF(1); // box ready for editing } /* THE MOUSE WAS CLICKED ON ONE OF THE 14 NAME & ADDRESS FIELDS MET0NA = Mouse Event Tab 0 Name & Address, where Tab 0 is the ADDRESS Tab. Called from only one place in MEH() case 0. */ void MET0NA() { int x = -1; // prime the number of the line that was clicked editSF(0); // clear any highlighted search field for(int i = 0; i < 14; i++) { // for each of the 14 name & address lines: int y = VN * i; // top of field offset if(mY > 76 + y // upper Y-limit for each field && mY < 92 + y) // lower Y limit for each field x = i; // note number of selected field } LN = x; // line number of address line being edited if(x > -1) { // if a name & address line has been clicked CP = (mX - 124) / 6; // set cursor to char at which mouse is pointing if(PN == 1) { // if currently editing a name & address line if(x == ln) // if click was on the line we're already editing editNA(3); // merely re-display cursor in new position else { // else we're now on a different line LN = ln; // set line number to original line editNA(2); // save & de-box the original line LN = x; // set line number back to newly clicked line editNA(1); // set up new line for editing ln = LN; // remember which line we're editing } } else { // else if not currently editing a name & address editNA(1); // set up Line Editor to edit this field PN = 1; // set to 'editing a name & address line' ln = LN; // remember which line we're editing } } } /* OPEN THE CONTACT'S NOTES OR DOSSIER FILE: The file is opened by launching the LibreOffice word processor or gedit editor with the file name of the contact's notes or dossier file as its command-line argument. Different word procesors could be used but it would mean editing this function and recompiling the program. Called from 2 places in MET1CB(). */ void openDoc(int x) { // 0=clip file 1=notes file 2=dossier file if(x < 0 || x > 2) return; // protection static char *P[] = { // command line templates for: "gedit clip.txt &", // clipboard file "gedit notes/N000000.txt &", // NOTES file "libreoffice dossiers/D000000.odt &" // dossier file }; static int p[] = {16,25,34}, // number of chars in each of the command templates q[] = { 0,13,22}; // starting positions within templates for record num char C[64], // array in which current command line is assembled D[32], // for the file name E[14][32]; // for the name & address // GET NAME & ADDRESS OF CURRENT CONTACT int R = rd << rs, J = 0, // start byte of the name & address record j, k; // local loop indexes for(j = 0; j < 14; j++) { // for each name & address line fseek(FR,R + J,SEEK_SET); // seek start byte of name & address record int L = fgetc(FR); // get its length if(L > 30) break; // safety precaution for(k = 0; k < L; k++) // for each character of the line E[j][k] = fgetc(FR); // get from record and put in E[][] E[j][L] = (char)0; // string terminator NULL character J += AL; // advance to next name & address line } // GET THE REQUIRED COMMAND LINE TEMPLATE FROM P[] int K = p[x]; // length of required command line template for(k = 0; k < K; k++) C[k] = P[x][k]; // copy required template into command line array C[K] = (char)0; // string terminator NULL character if(x > 0) { // if we're doing a notes file or a dossier file // ASSEMBLE THE COMMAND LINE IN C[] char *S = showInt(rd,6); // Form the 6-digit record number string K = q[x]; // start position of rec num within command template for(k = 0; k < 6; k++) C[K + k] = S[k]; // overwrite the 6 chars of rec num within template // COPY THE FILE NAME FROM THE COMMAND LINE TO D[] K = q[x] - 1; // start position of full name for(k = 0; k < 11; k++) D[k] = C[K + k]; // file name D[11] = (char)0; // CHECK WHETHER OR NOT THE FILE ALREADY EXISTS if(access(D,F_OK)){ // if notes or dossier is new [doesn't yet exist] FX = fopen(D,"wb"); // open it for creating/writing if(x == 1) // if creating a new notes file, write initial line fprintf(FX,"New Diary file for: %s\n",S); else { // else we're creating a dossier file fprintf(FX,"New Dosier file for:\n"); for(k = 0; k < 14; k++) // for each line of the name & address fprintf(FX,"%s\n",E[k]); // print it to the dossier file } fclose(FX); // close the notes or dossier file } } else { // otherwise, if doing a clip file FX = fopen("clip.txt","wb"); // open the clip file for writing for(k = 0; k < 14; k++) // for each line of the name & address fprintf(FX,"%s\n",E[k]); // print it in the clip file fclose(FX); // close the clip file } system(C); // launch the application to show and edit the file } /* IF MOUSE HAS BEEN CLICKED ON ONE OF THE ADDRESS TAB'S 8 CONTROL BUTTONS Called from only one place in MEH() case 0. */ void MET0CB() { int BN; // number of the clicked button for(int i = 0; i < 8; i++) { // for each button in the row of 9 int x = BW * i; // offset of current button from first if(mX > 24 + x && mX < 76 + x) // X-limits of the current button BN = i; // note number of clicked button } switch(BN) { // switch according to number of the clicked button case 0: delRec(); break; // DELETE button case 1: // HELP button helpPage("software/mktr.html#nad"); break; case 2: newRec(); break; // NEW button case 3: openDoc(0); break; // copy name & address to Clip file case 4: xref(); break; // XREF button case 5: prvRec(); break; // PREV button case 6: nxtRec(); break; // NEXT button case 7: // CLEAR button LN = -1; // set to de-box fields showNA(); // de-box all name & address fields PN = 0; // doing a search field Panel Number zero LN = 1; // default search field editSF(1); // de-box all search fields } } void MT0() { // HANDLES MOUSE CLICKS WITHIN THE NAME & ADDRESS TAB insMsg(0); // kill the 'Ins' state if(mY > BY && mY < 360) // vertical limits of control buttons MET0CB(); // click was on one of the control buttons else if(mX > 374 && mX < 475) // X-limits of search fields MET0SF(); // clicked in one of the 5 search fields else if(mX > 118 && mX < 317) // X-limits of name & address fields MET0NA(); /* mouse clicked on http link at bottom, so invoke Firefox web browser to load the Marketeer manual from the 20m server. */ else if(mX > 23 && mX < 287 && mY > 368 && mY < 379) helpPage("software/mktr.html"); } // T1--------------- FUNCTIONS PERTAINING TO THE PROFILE TAB ------------------ /* RETRIEVE AND DISPLAY ALL THE KEY NAMES IN THE PROFILE TAB Called from showKVs() and showSVs(). */ void showKNs(int kn) { clearPanel(2); // display Key Names panel background int Q = 0, // start byte of first Key Name v = 88; // vertical starting reference char S[33]; // local scratchpad array for(int i = 0; i < SL; i++) { // for each of the SL Key Names fseek(FK,Q,SEEK_SET); // start byte number of required key name int N = fgetc(FK); // get its length in bytes if(N < 0 || N > SL) break; // safety net for(int j = 0; j < N; j++) // for each byte of the key name S[j] = fgetc(FK); // store it in the S[] array if(i == kn) XSetForeground(XD,XG,WHITE); // show Key Name in white else XSetForeground(XD,XG,YELLOW); // show Key Name in yellow XDrawString(XD,XW,XG,30,v,S,N); v += VV; // drop to next display line Q += KL; // advance to start of next field's data } } /* RETRIEVE AND DISPLAY ALL THE KEY VALUES PERTAINING TO CLICKED KEY NAME Called from 2 places in MET1KNV() */ void showKVs(int kn, // kn:Key Name whose values are required int kv, // kv:Key Value just toggled [if applicable] int sw) { // sw:switch 1=Key Value just toggled if(kn < 0) return; // clicked in wrong column showKNs(kn); // show highlighted Key Name clearPanel(3); // wipe the Key Values panel int R = (rd << rs) // record number times record size + 456 // + start byte of key selection registers + (kn << 1); // + key number x 2 bytes per short integer fseek(FR,R,SEEK_SET); // start of record's SL-bit selection for this Key int s = getShort(FR); // selector register for this Key Name if(sw == 1) { // if a Key Value has been selected or deselected int x = 1 << kv; // selector bit for this Key Value if((s & x) > 0) // If the selector bit is already set s &= ~x; // then clear it else s |= x; // else set it. } int Q = (KH + 1) << 9, // start byte of Key Name's first Key Value v = 88, // vertical starting reference x = 1; // shiftable test bit char S[33]; // local scratchpad array for(int i = 0; i < SL; i++) { // for each of the SL Key Names fseek(FK,Q,SEEK_SET); // start byte number of key value string int N = fgetc(FK); // get its length in bytes if(N < 0 || N > 24) break; // safety net if(N == 0) // if this key value has no text s &= ~x; // unset it's selection bit else { // else, the key value has text for(int j = 0; j < N; j++) // for each byte of the key name S[j] = fgetc(FK); // store it in the S[] array /* If this key value is selected [ie. it's selection bit is set], display its text in white, otherwise display it in grey. */ if((s & x) > 0) XSetForeground(XD,XG,WHITE); else XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,140,v,S,N); } x <<= 1; // shift the selection bits one place right v += VV; // drop to next display line Q += KL; // advance to start of next field's data } fseek(FR,R,SEEK_SET); // start of record's SL-bit selection for this Key putShort(s,FR); // save the new selection fflush(FR); // push to disk in case of power failure PE = -1; // reset the 'field modified' flag } /* RETRIEVE AND DISPLAY ALL THE CURRENTLY SELECTED KEY VALUES Called from T1show() and MET1KNV(). */ void showSVs() { showKNs(-1); // display the key names int v = 88; // vertical starting reference clearPanel(3); // clear Key Values area to dark int R = (rd << rs) + 456; // start byte of the key selector shorts char S[33]; // local scratchpad array for(int i = 0; i < SL; i++) { // for each of the SL Key Names fseek(FR,R + i + i,SEEK_SET); // start byte of this key's selections int Q = (i + 1) << 9, // start byte of Key Value x = getShort(FR), // this key's SL-bit selection register y = -1, // catch index number of 1st set bit z = 0; // to count how many bits were set for(int j = 0; j < 16; j++) { // for each of the 16 Key Values if((x & 1) == 1) { // if bit zero is set if(y == -1) y = j; // index of first encountered '1' bit z++; // count the '1' bits encountered } x >>= 1; // shift all bits on place left } if(y > -1) { // if at least one of the bits was set fseek(FK,Q + (y << 5),SEEK_SET); // start of this Key Value's text int N = fgetc(FK); // get its length in bytes if(N > 30) break; // corrupt entry for(int j = 0; j < N; j++) // for each byte of the key name S[j] = fgetc(FK); // store it in the S[] array XSetForeground(XD,XG,GREY); // show Key Value in grey XDrawString(XD,XW,XG,140,v,S,N); if(z > 9) S[0] = '+'; // just show '+' if >9 values selected else S[0] = (char)(z + 48); // else show the number of values selected if(z > 1) XDrawString(XD,XW,XG,275,v,S,1); } v += VV; // drop to next display line } } /* TWO FUNCTIONS PREPARE A KEY NAME OR KEY VALUE FIELD FOR EDITING by setting the parameters that determine the field's position within the PROFILE Tab plus the lengths of the field and of its current content. It is called with a switch value that invokes one of 4 cases. [In all cases the field's area first is cleared to the DARK background. case 0: Loads the original content of the field into the Line Editor's array ED[]. case 1: Displays a yellow box around the field. Draws the cursor. Then loads the original content of the field into the Line Editor's array ED[] as in case 0. case 2: Saves the edited Key Name [if modified] to the appropriate name & address record. case 3: Re-draw the box, cursor and Line Editor text content In all cases, the field content is finally re-displayed. Called by from 1 place in edKN()and escape(), 4 places in selKN(), and 2 places in updn(). */ void editKN(int sw) { // switch: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] setupField(); // set up required field for Line Editor drawFld(); // draw the dark grey field background int c = WHITE; // text display colour switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box field in yellow ready for editing case 0: getField(FK,1); // load original content from file if(sw == 1) drawCur(); // display the green cursor c = WHITE; break; case 2: // save and re-display the field text putField(FK,1); // save the Line Editor content to disk c = YELLOW; // display the field text in yellow break; case 3: drawBox(); drawCur(); // display yellow box & green cursor } showLine(c); // display the text content of the Line Editor array } /* Called by from 1 place in edKV()and escape(), 4 places in selKV(), and 2 places in updn(). */ void editKV(int sw) { // switch: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] setupField(); // set up required field for Line Editor int c = WHITE; // text display colour drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box the field is ready for editing case 0: // load original content from the file getField(FK,1); // load the field into ED[] if(sw == 1) drawCur(); // display the green cursor break; case 2: // save the Key Value [if modified] putField(FK,1); // store the field to disk c = GREY; // display the field text break; case 3: drawBox(); drawCur(); // display yellow box & green cursor } showLine(c); // display the text content of the Line Editor array } /* PREPARES THE SIC [STANDARD INDUSTRIAL CLASSIFICATION] NUMBER & THE 'KNIT- TING NEEDLE' GENERAL SELECTOR CODEFIELD FOR EDITING by setting the para- meters that determine the field's position within the PROFILE Tab plus the lengths of the field and of its current content. It is called with a switch value that invokes one of 4 cases. In all cases the field's area first is cleared to the DARK background. case 0: Loads the original content of the field into the Line Editor's array ED[]. case 1: Displays a yellow box around the field. Draws the cursor. Then loads the original content of the field into the Line Editor's array ED[] as in case 0. case 2: Saves the edited SIC field [if modified] to the appropriate name & address record. In all cases, the field content is finally re-displayed. Both these functions are called from 1 place in T1show(), unBox(), escape() and updn() and from 2 places in MET1SIC(). */ void editSI(int sw) { // switch: 0=show, 1=get/edit, 2=put PN = 4; LN = 0; setupField(); // set up required field for Line Editor drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); drawCur(); // display yellow box & green cursor case 0: // load original content from file fseek(FR,SB,SEEK_SET); // seek start byte of SIC number char *S = showInt(getInt(FR),lE); // integer to a 5-digit string for(int k = 0; k < lE; k++) // copy the SIC number into the ED[k] = S[k]; // Line Editor's editing array ED[] break; case 2: // save edited SIC field content if(PE == PN) { // if SIC field was modified fseek(FR,SB,SEEK_SET); // start byte of SIC number ED[lE] = (char)0; // terminate ED content to corrent length putInt(atoi(ED),FR); // convert ED[] to integer and store it PE = -1; // reset the 'field modified' flag } // NOTE: PE is set by the Line Editor break; case 3: drawBox(); drawCur(); // display yellow box & green cursor } showLine(WHITE); // display the text content of the Line Editor array } void editKC(int sw) { // switch: 0=show, 1=get/edit, 2=put PN = 5; LN = 0; setupField(); // set up required field for Line Editor drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); drawCur(); // display yellow box & green cursor case 0: getField(FR,0); break; // load original content from file case 2: putField(FR,0); break; // save the field's content if modified case 3: drawBox(); drawCur(); // display yellow box & green cursor } showLine(WHITE); // display the text content of the Line Editor array } /* COMPUTE THE ELAPSED TIME [IN DAYS] BETWEEN TWO DATES. NOTE: global vari- ables: ET = Elapsed Time in days between the dates given by: rY = reference Year, rM = reference Month, rD = reference Day and sY = system Year, sM = system Month, sD = system Day. The only global variable this function mod- ifies is ET. Called only from 1 place each in showET() and packDate(). */ int getET() { static int dm[] = {31,28,31,30,31,30,31,31,30,31,30,31}; if(rY < 999 || rY > 9999 || rM < 1 || rM > 12 || rD < 1 || rD > 31) return -1; // error return ET = 0; // zero the elapsed time int d = rD, m = rM, y = rY, // local vars for ref day, month, year D = sD, M = sM, Y = sY, // local vars for sys day, month, year r = (y << 16) + (m << 8) + d, // cast ref date as an integer s = (Y << 16) + (M << 8) + D, // cast sys date as an integer f = 0, x; // clear the 'future' flag if(r == s) return 0; // If refence date = system date return zero ET if(r > s) { // if reference date is in the future f = 1; // set the 'future' flag x = D; D = d; d = x; // swap system day and refernce day x = M; M = m; m = x; // swap system month and refernce month x = Y; Y = y; y = x; // swap system year and refernce year } ET = -d; // set elapsed time to -(ref date day number) while(1) { // permanent but breakable loop if(m == M && y == Y) { // If system month & year = reference month & year ET += D; // elapsed time is difference between day numbers. break; // exit because we've reached system date } ET += dm[m - 1]; // add the number of days in this month /* If it's February AND { (the year is divisible by 4 but not by 100) OR (it is divisible by 400) } then it is a leap year so add an extra day. */ if(m == 2 && (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)) ET++; if(++m > 12) { // if incremented month is beyond December m = 1; // reset month to January y++; // and increment the year number } if(ET > 999999) // elapsed time beyond beyond max displayable size { ET = 0; break; } // so exit with ET = 0 } // end of while(1) if(f == 1) ET = -ET; // if reference date is future, invert sign of ET return 0; // success } // COMPUTE AND DISPLAY THE ELAPSED TIME void showET() { // called only from 1 place in editDT() char S[33]; // scratchpad array XSetForeground(XD,XG,DARK); // wipe elapsed time area here XFillRectangle(XD,XW,XG,371,Ty - 12,105,15); XSetForeground(XD,XG,WHITE); // display the field text int pf = 0, y; // 'past/future' flag 0:past 1:future if(getET() == 0) { // if elapsed time is valid if(ET < 0) {ET = -ET; pf = 1;} // if -ve, set ET +ve but 'future' double m = ET / 30.4375; // number of months if(ET < 90) // if elapsed time < 60 days sprintf(S,"%5i days",ET); // display it as a number of days else if(m < 25) // if less than 25 months ago sprintf(S,"> %2i months",(int)m); // display elapsed time in months else // else, its over 2 years sprintf(S,"> %2i years",(int)(m / 12)); // display ET in years int l = strlen(S), // length of elapsed time string x = (l * 6) + 381; // start position of 'ago' or 'hence' XDrawString(XD,XW,XG,375,Ty,S,l); // show ET figure if(pf == 0) XDrawString(XD,XW,XG,x,Ty,"ago",3); // past date else XDrawString(XD,XW,XG,x,Ty,"hence",5); // future date } else XDrawString(XD,XW,XG,375,Ty,"?????",5); // invalid date } /* PACK DISPLAY VERSION OF DATE [EG: 11 MAY 2019] IN ED[] INTO A 4-BYTE TRAIN [YYMD] FOR STORINGE IN THE NAME & ADDRESS RECORD: Converts the date from display format to storage format. Called from only 1 place in editDT(). */ unsigned int packDate() { char S[5]; // local scratchpad array for(int j = 0; j < 4; j++) // for each of the 4 year digits S[j] = ED[j + 7]; // get year number: bytes 7 to 10 S[4] = (char)0; // terminate with a NULL character rY = atoi(S); // 'rY' is used way down the function if(rY < 0 || rY > 9999) rY = 0; // limit valid range of year number for(int j = 0; j < 13; j++) { // for each of the 12 months: if(MO[j][0] == ED[3] // If first letter of month matches && MO[j][1] == ED[4] // and 2nd letter of month matches && MO[j][2] == ED[5]) // and 3rd letter of month matches, { rM = j; break; } // preserve the found number } // and break out of the for(j) loop. if(rM < 0 || rM > 12) rM = 0; // limit valid range of month number S[0] = ED[0]; // 1st digit of day number S[1] = ED[1]; // 2nd digit of day number S[2] = (char)0; // terminate with a NULL character rD = atoi(S); // store day of month in records file if(rD < 0 || rD > 31) rD = 0; // limit valid range of day number return rY << 16 & 0xFFFF0000 // shift to occupy top 16 bits of integer | rM << 8 & 0x0000FF00 // shift to occupy second lowest byte | rD & 0x000000FF; // place in the lowest byte } /* UNPACK A NUMERIC DATE INTO THE LINE EDITOR ARRAY ED[]: Converts a date from storage format [YYMD] to display format [EG: 11 MAY 2019]. Called from only 1 place in editDT(). */ void unpackDate(int d, int m, int y) { char *s; if(y < 0 || y > 9999) y = 0; // limit valid range of year number if(m < 0 || m > 12) m = 0; // limit valid range of month number if(d < 0 || d > 31) d = 0; // limit valid range of day number s = showInt(d,2); // make a 2-digit string of day number ED[0] = *s; // copy 2-digit day number string ED[1] = *(s + 1); // to date field string ED[2] = ' '; // space between day number & month name ED[3] = MO[m][0]; // copy 3-letter month name to date field ED[4] = MO[m][1]; ED[5] = MO[m][2]; ED[6] = ' '; // space between month name & year number s = showInt(y,4); // make a 4-digit string of year number for(int j = 0; j < 4; j++) // copy 4-digit year number string ED[j + 7] = *(s + j); // to date field string ED[11] = (char)0; // terminating NULL char for date string lE = 11; // length of the standard date field } void getDate() { // GET AND UNPACK DATE FROM RECORDS FILE fseek(FR,SB,SEEK_SET); // seek start byte of Date field rY = fgetc(FR) << 8 | fgetc(FR); // year number rM = fgetc(FR); // month number rD = fgetc(FR); // day number unpackDate(rD,rM,rY); // unpack date into Line Editor array } void putDate() { // PACK AND SAVE A DATE TO RECORDS FILE if(PE != PN) return; // exit if the field hasn't been modified fseek(FR,SB,SEEK_SET); // seek start byte of Date field putInt(packDate(),FR); // pack & store modified date to records file PE = -1; } /* PREPARES A DATE FIELD FOR EDITING by setting the parameters that determine the field's position within the PROFILE Tab plus the lengths of the field and of its current content. It is called with a switch value that invokes one of 3 cases. In all cases the field's area first is cleared to the DARK background. case 0: Loads the original content of the field into the Line Editor's array ED[]. case 1: Displays a yellow box around the field. Draws the cursor. Then loads the original content of the field into the Line Editor's array ED[] as in case 0. case 2: Saves the edited date field [if modified] to the appropriate name & address record. In all cases, the field content is finally re-displayed and the elapsed time from the new date is computed and displayed. Called from 1 place in escape(), 2 places each in T1show() and unBox(), and 4 places each in MET1SIC() and updn(). */ void editDT(int sw) { // switch: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] PN = 6; setupField(); // set up required field for Line Editor drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); drawCur(); // field is to be boxed for editing case 0: getDate(); break; // load original date from file case 2: putDate(); // pack & store modified date to file } showLine(WHITE); // display the date showET(); // compute and display the elapsed time } /* SHOW PROFILE TAB'S EDITING BUTTONS Called from 1 place in T1show() and 2 places in MET1CB() Arguments: x = 1 Key Names editing mode active, y = 1 Key Values editing mode active. */ void showT1buts() { char // PROFILE buttons *PB[7] = {"EDIT NAMES","EDIT VALUES","NOTES", "DOSSIER","WEB SITE","EMAIL","HELP"}; int Pb[7] = { 3, 1,10, 4, 2,11,13}, // insets for PROFILE button annotations Bl[7] = {10,11, 5, 7, 8, 5, 4}, // lengths of the button annotations Bw[7] = {78,78,60,60,60,60,60}, // widths of the 7 buttons g = 24, // y-coord of left side of first button h = 27; // y-coord of start of first letter for(int i = 0; i < 7; i++) { // for each of the 6 control buttons char *H = PB[i]; // pointer to start of current annotation int j = Pb[i], // inset for annotation of current button k = Bw[i]; // width of current button XSetForeground(XD,XG,DARK); // display button background XFillRectangle(XD,XW,XG,g,360,k - 4,BH); if(i == 0 // if 'EDIT NAMES' button && KE == 2 // and in Key Names editing mode || i == 1 // OR if 'EDIT VALUES' button && KE == 3) // and in Key Values editing mode XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,j + h,375,H,Bl[i]); g += k; // advance to left side of next button h += k; // advance to start of next button's annotation } } /* DRAW THE INITIAL CONTENT OF THE PROFILE TAB Called from only one place in showTab(). */ void T1show() { PE = -1; // set 'no field modified' PN = -1; // set field focus to null field ln = -1; // no previous line has been edited yet KH = -1; // set no key selected [highlighted] KE = -1; // not in any key editing mode int v = 70; XSetForeground(XD,XG,AMAREL); // display column titles XDrawString(XD,XW,XG, 30, v, "KEY NAMES", 9); XDrawString(XD,XW,XG, 140, v, "KEY VALUES", 10); showSVs(); // show the keys and their currently selected values showT1buts(0, 0); // show the PROFILE Tab's buttons (temp) int N[] = { // Display contact details as follows: 352, // contact name: line number 11 * 32 bytes field length 384, // job title: line number 12 * 32 bytes field length 0, // comapny name: line number 0 * 32 bytes field length 224, // telephone: line number 7 * 32 bytes field length 288 // email: line number 9 * 32 bytes field length }; v = 67; // starting y-coordinate XSetForeground(XD,XG,GREY); // display colour int s = rd << rs; // start byte of current record char S[33]; // local scratchpad array for(int j = 0; j < 5; j++) { // for each of the 5 items fseek(FR,s + N[j],SEEK_SET); // start byte of line data int l = fgetc(FR); // length of content for(int k = 0; k < l; k++) // for each char of content S[k] = fgetc(FR); // copy it into byte array XDrawString(XD,XW,XG,PM,v,S,l); // display it v += 15; } XSetForeground(XD,XG,AMAREL); v = 180; XDrawString(XD,XW,XG,PM + 34,v,"Standard Industrial",19); v += 15; XDrawString(XD,XW,XG,PM + 49,v,"Classification",14); LN = 0; editSI(0); XSetForeground(XD,XG,AMAREL); v += 60; XDrawString(XD,XW,XG,PM + 22,v,"Arbitrary Selector Code",23); LN = 0; editKC(0); LN = 0; editDT(0); // display the NEXT event date and its elapsed time LN = 1; editDT(0); // display the LAST event date and its elapsed time PN = -1; LN = -1; } /* INVOKE THE FIREFOX WEB BROWSER TO OPEN THE CURRENT CONTACT'S WEBSITE Called from only 1 place in met1cb(). */ void WebSite() { char S[65]; // local scratch-pad array for command line strcpy(S,"xdg-open "); // put browser name at start of char array fseek(FR,(rd << rs) + 310,SEEK_SET); // start of web address field int L = fgetc(FR); // get length of web address for(int k = 0; k < L; k++) S[k + 9] = fgetc(FR); // install it in S[] from position 9 S[L + 9] = ' '; // add a delimiting space S[L + 10] = '&'; // set to run browser in background S[L + 11] = (char)0; // terminate command string with NULL char system(S); // launch the web browser } /* INVOKE THE EMAIL CLIENT'S WRITER FUNCTION TO WRITE EMAIL TO CURRENT CONTACT. Called from only 1 place in met1cb(). */ void Email() { char S[65]; // local scratch-pad array for command line strcpy(S,"xdg-open mailto:"); fseek(FR,(rd << rs) + 279,SEEK_SET); // start of email address field int L = fgetc(FR); // get length of email address for(int k = 0; k < L; k++) S[k + 16] = fgetc(FR); // install it in S[] from position 16 S[L + 16] = ' '; // add a delimiting space S[L + 17] = '&'; // set to run email client in background S[L + 18] = (char)0; // terminate command string with NULL char system(S); // launch the email client } /* THE MOUSE WAS CLICKED ON A NEW PROFILE FIELD: Saves and unboxes, if neces- sary, any of the single-line PROFILE fields [SIC, Selection Code or the 2 dates] which may be currenly boxed for editing. Called from 1 place in selKV(), 2 places in selKN() and 4 places in MET1SIC(). */ int unBox() { if(KH > -1) // if a Key Name is currently selected [highlighted] return -1; // can't open SIC, Selector Code or Date field for editing if(PN < 2) // no PROFILE field is currently open for editing, return 0; // so it's OK to open a field for editing switch(PN) { // switch on Panel Number case 2: // can't unblock Key Names editing case 3: return -1; // can't unblock Key Values editing case 4: editSI(2); break; // save and de-box it SIC field case 5: editKC(2); break; // save and de-box it Selection Code case 6: editDT(2); break; // save and de-box event date } PE = -1; // set to 'no field has been modified' PN = -1; // set to 'not editing any field' return 0; } /* SWITCH BETWEEN KEY NAMES 'SELECTION' AND 'EDITING' MODES: Sets button annotation bright for when in editing mode and dim when in selection mode. Called from only one place in MET1CB(). */ void edKN() { if(KE != 2) { // if not currently in KEY NAME editing mode if(PN > 2) return; // another PROFILE field is open for editing KE = 2; // set field focus for editing Key Names } else { // else we're in Key Names editing mode KE = 2 editKN(2); // save it & de-box it if necessary showKNs(-1); // re-display the key names with none highlighted PE = -1; // set 'no field modified' PN = -1; // set field focus to null field KH = -1; // set no key selected [highlighted] KE = -1; // not in any key editing mode } LN = -1; // set for no box highlighted showT1buts(); // re-display the PROFILE Tab's buttons } /* SWITCH BETWEEN KEY VALUES 'SELECTION' AND 'EDITING' MODES: Sets button annotation bright for when in editing mode and dim when in selection mode. Called from only one place in MET1CB(). */ void edKV() { if(KE != 3) { // if not currently in KEY VALUES editing mode if(PN > 3) return; // another PROFILE field is open for editing KE = 3; // set field focus for editing Key Values } else { // else we're in Key Values editing mode KE = 3 if(PE == 3) // if KEY VALUES field modified editKV(2); // save & de-box it showKVs(KH,0,0); // re-display the Key Values of current Key Name PE = -1; // set 'no field modified' PN = -1; // set field focus to null field KE = -1; // not in any key editing mode } LN = -1; // set for no box highlighted showT1buts(); // re-display the PROFILE Tab's buttons } /* ONE OF THE ROW OF 8 BUTTONS AT THE BOTTOM OF THE PROFILE TAB WAS CLICKED The values in the if() statements are the left and right edges of the buttons in pixels. MET1CB = Mouse Event for Tab 1 Control Buttons; Tab 1 = PROFILE Tab. Called from only one place in MEH() case 1: */ void MET1CB() { if(mX > 23 && mX < 96) // if clicked on 'EDIT NAMES' button edKN(); // toggle Key Names edit/select modes else if(mX > 99 && mX < 172) // if clicked on 'EDIT VALUES' button edKV(); // toggle Key Values edit/select modes else if(mX > 179 && mX < 236) // NOTES button clicked openDoc(1); // open the contact's Diary file else if(mX > 239 && mX < 396) // DOSSIER button clicked openDoc(2); // open the contact's Dossier file else if(mX > 299 && mX < 356) // WEB SITE button clicked WebSite(); // open the contact's website else if(mX > 359 && mX < 416) // EMAIL button clicked Email(); // send an email addressed to the contact else if(mX > 419 && mX < 476) // EMAIL button clicked helpPage("software/mktr.html#profile"); } /* MANAGES THE SELECTION OR EDITING OF A KEY NAME: Sets the common variables: KH [Keyname Highlighted: number of the currently highlighted Key Name] CP [Cursor Position] PN [Panel Number] LN [Line Number] Called from only one place in MET1KNV(). */ void selKN(int x) { // x = number of actual Key Name just clicked if(KE == 2) { // if in Key Names editing mode CP = (mX - 29) / 6; // position within field of clicked character LN = x; // line number of Key Name being edited /* We are already editing a Key Name field, so if the click was within the same field, we simply move the cursor to where the mouse was clicked. However, if the click was on another Key Name anywhere within the list, we need to save and de-box the old Key Name field and box the new Key Name and set it up for editing. */ if(PN == 2) { // if already editing a Key Name field if(x == ln) // if the click was on the same Key Name editKN(3); // simply re-display the cursor at its new position else { // else [if the click was on another Key Name] LN = ln; // set line number to the original Key Name editKN(2); // save & de-box the original Key Name LN = x; // set line number to newly clicked Key Name editKN(1); // set up the new Key Name for editing ln = LN; // remember which Key Name we're editing } /* We're not already editing a Key Name field, so we need to set up the currently clicked Key Name for editing. */ } else { // else, if not already editing a Key Name unBox(); // save and unbox any other boxed PROFILE field PN = 2; // set to 'editing a Key Name' editKN(1); // set up Line Editor to edit this Key Name ln = LN; // remember which Key Name we're editing } /* We are NOT editing a Key Name, so we must be simply selecting or de- selecting it. If the Key Name on which the mouse was clicked is already highlighted [selected], then de-select [lowlight or dim] it. Otherwise, the click was on a lowlighted Key Name, so select [highlight] it. */ } else { // else NOT in Key Name editing mode KE != 1 unBox(); // save and unbox any other boxed PROFILE field if(x == KH) { // if the Key Name is already highlighted KH = -1; // lowlight the currently highlighted Key Name showSVs(); // display first selected value for each key } else { // else clicked on unhighlighted Key Name KH = x; // highlight the currently clicked Key Name showKVs(KH,0,0); // display Key Values } } } /* MANAGES THE SELECTION OR EDITING OF A KEY VALUE: Sets the common variables: CP [cursor position] PN [Panel Number] LN [Line Number] Called from only one place in MET1KNV(). */ void selKV(int x) { // line number of actual Key Value jst clicked LN = x; // line number of the Key Value that was clicked if(PN > 3) { // another PROFILE field is still open for editing unBox(); // so save and de-box it return; // then take no further action } /* If NOT in Key Values editing mode, display the Key Values, toggling the selection state [highlighting] of the Key Value that was clicked. */ if(KE != 3) showKVs(KH,x,1); /* Otherwise, we are in Key Values editing mode, so provided a Key NAME is currently highlighted [which means that its values are currently dis- played in the Key Values panel] set cursor position 'CP' to where the mouse was clicked within the Key Value field being edited. */ else if(KH > -1) { // provided a Key Name is highlighted CP = (mX - 140) / 6; // position within field of clicked char /* If a Key Value is already being edited, then, provided the mouse was clicked within the same Key Value field, simply re-display the cursor at the character on which the mouse was clicked. Otherwise, the mouse was clicked somewhere else in the list of Key Values. So, save and de-box the original field that was being edited and set up the new Key Value, upon which the mouse was just clicked, for editing. Note the line number of the Key Value we are now editing.*/ if(PN == 3) { // if currently editing a Key Value if(LN == ln) // provided click was on the same line editKV(3); // merely re-display cursor at new position else { // else clicked on another line LN = ln; // set line number to original line editKV(2); // save & de-box the original line LN = x; // set line number back to newly clicked line editKV(1); // set up new line for editing ln = LN; // remember which line we're editing } } /* Else, we are not currently editing a Key Value. We must therefore box and set up the newly clicked Key Value for editing. */ else { // else not currently editing a Key Value PN = 3; // set that we are now editing a Key Value editKV(1); // set up Line Editor to edit this Key Value ln = LN; // remember which line we're editing } } } /* THE MOUSE WAS CLICKED WITHIN THE PROFILE TAB'S KEY NAMES AND VALUES AREA MET1KNV = Mouse Event on Tab 1 for Key Names or Values. Called from only one place in MEH() case 1: */ void MET1KNV() { int i; // number of the actual line that was clicked on for(i = 0; i < 16; i++) { // for each row of the Key Names/Values int j = i * VV; // vertical row off-set in pixels if(mY > 79 + j // if mouse clicked below top of row && mY < 94 + j) // and also above bottom of row break; // break if mouse clicked on the (i)th row } if(i < 16) { // click was within one of the Key Rows if(mX > 28 && mX < 120) // mouse clicked within Key Names panel selKN(i); // select or edit clicked Key Name else if(mX > 138 && mX < 280) // mouse clicked within Key Values panel selKV(i); // select or edit clicked Key Value } } /* MOUSE WAS CLICKED IN ONE OF THE 4 FIELDS ON THE RIGHT OF THE PROFILE TAB: the SIC number, the Knitting Needle Code or one of the two date fields. Called from only one place in MEH() case 1: */ void MET1SIC() { // SIC [Standard Industrial Classification] FIELD CLICKED if(mX > 368 && mX < 402 && mY > 203 && mY < 217) { CP = (mX - 370) / 6; // position within field of clicked char if(PN == 4) { // if currently editing the SIC field editSI(3); // merely re-position the cursor } else { // else not currently editing SIC field if(unBox() == -1) return; PN = 4; // set field focus to 'SIC number' editSI(1); // load, display and box the SIC number } return; // exit the Mouse Event Handler // KM [Knitting Needle] 30-CHARACTER SELECTOR FIELD CLICKED } else if(mX > 293 && mX < 477 && mY > 263 && mY < 277) { CP = (mX - 328) / 6; // position within field of clicked char if(PN == 5) { // if currently editing the knitting needle editKC(3); // merely re-position the cursor } else { // else if already in 'editing KM' mode if(unBox() == -1) return; PN = 5; // set field focus to Knitting Needle editKC(1); // load, display and box the KM code } return; // exit the Mouse Event Handler // DATE: 11-CHARACTER NEXT DATE FIELD CLICKED } else if(mX > PM && mX < PM + 71 && mY > 313 && mY < 330) { CP = (mX - PM - 2) / 6; // set cursor position if(PN == 6) { // if NEXT date is currently being edited LN = 0; editDT(2); // save the edited NEXT date PN = -1; // set field focus to null field } else { // else NEXT date not currently being edited PN = 6; // set focus to event dates panel LN = 0; // line number for NEXT event date if(unBox() == -1) return; editDT(1); // display and box the NEXT event date } return; // exit the Mouse Event Handler // DATE: 11-CHARACTER LAST DATE FIELD CLICKED } else if(mX > PM && mX < PM + 71 && mY > 330 && mY < 347) { CP = (mX - PM - 2) / 6; // set cursor position if(PN == 7) { // if LAST date is currently being edited LN = 1; editDT(2); // save the edited LAST date PN = -1; // set field focus to null field } else { // else no field is currently being edited PN = 6; // set focus to event dates panel LN = 1; // line number for LAST event date if(unBox() == -1) return; editDT(1); // display and box the LAST event date } return; // exit the Mouse Event Handler } } void MT1() { // HANDLES MOUSE CLICKS WITHIN THE PROFILE TAB if(mY > 359) // click was in the control buttons MET1CB(); // area across the bottom of the tab else if(mX < 284) // click was in Key Names & Values area MET1KNV(); else // clicked on SIC, KH or a DATE field MET1SIC(); } // T2---------------- FUNCTIONS PERTAINING TO THE DIARY TAB ------------------- /* LOAD AND DISPLAY THE CURRENT STATE OF THE DIARY Called from 1 place in T2show(). */ void getDiary() { clearPanel(7); // display the blank DIARY dates panel clearPanel(8); // display the blank DIARY comments panel int r = (rd << rs) + 512; // start byte of this record's diary section for(LL = 0; LL < 16; LL++) { // for each of the 16 diary lines fseek(FR,r,SEEK_SET); // seek start byte of this dirary line rY = getShort(FR); // get year number rM = fgetc(FR); // get month number rD = fgetc(FR); // get day number int L = fgetc(FR); // get length of the one-liner comment if(L == 0) break; // null entry is assumed to be the if(L > 59) L = 59; // safety net int Y = 90 + LL * VV; // set vertical coordinate of line unpackDate(rD,rM,rY); // 4-byte packed format to 11-byte display XSetForeground(XD,XG,YELLOW); // colour for dates XDrawString(XD,XW,XG,30,Y,ED,11); // display the date for(int j = 0; j < L; j++) // for each character of the comment ED[j] = fgetc(FR); // load it into the diary array XSetForeground(XD,XG,GREY); // colour for diary note XDrawString(XD,XW,XG,118,Y,ED,L); // display the one-liner diary comment r += 64; // advance to start byte of next line } } /* PUSH ALL CURRENT DIARY ENTRIES ONE PLACE UP OR DOWN IN THE LIST. The orig- inal first line of the diary [the first diary entry ever written for this contact] is preserved forever as the first displayed line. All subsequent lines [from the second line to the last line] are pushed or pulled accord- ing to whether the Diary Panel is currently full or not. Called from 1 place in MET2DY(). */ void insShunt() { int r = rd << rs; // start byte of current contact record if(LL < 0 || LL > 15 // outside the range of valid Diary line numbers || LL < 0 || LL > 16 // beyond the range of uninhabited line numbers || LN >= LL) { // selected line beyond currently inhabited lines printf("LL=%i LN=%i\n",LL,LN); // print rogue values to terminal return; // and bail out } if(LL > 15) { // if the Diary Panel is full: shunt up r += 576; // start byte of Line 1 [512 + 64] for(int i = 1; i < LN; i++) { fseek(FR,r + 64,SEEK_SET); // seek start byte of next/prev diary line for(int j = 0; j < 64; j++) // pull the whole of the next/prev line's ED[j] = fgetc(FR); // content into ED[] fseek(FR,r,SEEK_SET); // seek start byte of this diary line for(int j = 0; j < 64; j++) // store the whole of the content fputc(ED[j],FR); // into this line r += 64; // advance to start byte of next/prev line } } else { // else, if Diary Panel is NOT YET full: r += 512 + (LL << 6); // start byte of 1st uninhabited line LL for(int i = LL; i > LN; i--) { fseek(FR,r - 64,SEEK_SET); // seek start byte of previous diary line for(int j = 0; j < 64; j++) // pull the whole of the next/prev line's ED[j] = fgetc(FR); // content into ED[] fseek(FR,r,SEEK_SET); // seek start byte of this diary line for(int j = 0; j < 64; j++) // store the whole of the content fputc(ED[j],FR); // into this line r -= 64; // advance to start byte of next/prev line } } fseek(FR,(rd << rs) + 512 + (LN << 6),SEEK_SET); // start byte of Line LN unpackDate(sD,sM,sY); // put today's date in display form in ED[] putInt(packDate(),FR); // pack and save today's date to file for(int j = 4; j < 64; j++) // fill the rest of the last diary fputc(' ',FR); // line with null characters. fflush(FR); // push to disk in case of power failure getDiary(); // re-display the diary in full } /* PREPARES THE DIARY'S DATE & COMMENT FIELDS RESPECTIVELY FOR EDITING by set- ting parameters that determine the field's position within the DIARY Tab plus the lengths of the field and of its current content. It is called with a switch value that invokes one of 3 cases. In all cases the field's area first is cleared to the DARK background. case 0: Loads the original content of the field into the Line Editor's array ED[]. case 1: Displays a yellow box around the field. Draws the cursor. Then loads the original content of the field into the Line Editor's array ED[] as in case 0. case 2: Saves the edited DIARY entry [if modified] to the appropriate name & address record. case 3: Re-display the field content in grey In all cases, the field content is finally re-displayed. Called from 1 place each in MET2DY() & newEntry() 2 places in updn(). */ void editDY(int sw, int p) { // sw: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] if(p) PN = 8; else PN = 7; // set appropriate Panel Number setupField(); // start byte of required field int c = WHITE; // text display colour drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box the field ready for editing case 0: // load original content from the file if(p) getField(FR,1); // load original DIARY comment from the file else getDate(); // load DIARY date into Line Editor array ED[] if(sw == 1) drawCur(); // display the green cursor break; case 2: if(p) putField(FR,1); // save DIARY comment [if modified] else putDate(); // save DIARY date [if modified] case 3: if(p) c = GREY; // show comment in grey else c = YELLOW; // show date in yellow } showLine(c); // display the field's textual content } /* SHOW DIARY TAB'S EDITING BUTTONS Called from 1 place in T1show() and 2 places in MET1CB() */ void showT2buts() { #define bw 76 // button width const int Pb[6] = {6,6,6,14,14,21}, // insets for PROFILE button annotations Bl[6] = {9,9,9, 6, 6, 4}; // lengths of the button annotations const char // PROFILE buttons *PB[6] = {"NEW ENTRY","EDIT DATE","EDIT TEXT","INSERT","DELETE","HELP"}; for(int i = 0; i < 6; i++) { // for each of the 6 control buttons XSetForeground(XD,XG,DARK); // display button backgrounds XFillRectangle(XD,XW,XG,24 + i * bw,360,bw - 4,BH); if(T2B & 1 << i) // if this button is illuminated XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,27 + *(Pb + i) + i * bw,375,*(PB + i),*(Bl + i)); } } /* DRAW THE INITIAL CONTENT OF THE DIARY TAB Called from only one place in showTab(). */ void T2show() { int v = 70; // y-coord of the contact name & company XSetForeground(XD,XG,GREY); // display colour int s = (rd << rs) + 352, // start byte of current record L = 0; // offset to start of field display point char S[33]; // local scratchpad array for(int i = 0; i < 2; i++) { // for each of the 5 items fseek(FR,s,SEEK_SET); // start byte of line data int l = fgetc(FR); // length of content for(int j = 0; j < l; j++) // for each char of content S[j] = fgetc(FR); // copy it into byte array XDrawString(XD,XW,XG,30 + i * 10 + L * 6,v,S,l); // display it L = l; // for biasing next field rightwards s -= 352; // back up to start of name & address record } getDiary(); // load and display current diary entries showT2buts(0,0); // show the DIARY Tab's buttons (temp) } // MAKE SPACE FOR NEW DIARY ENTRY. Called only from newEntry() below. void newShunt() { int r = (rd << rs) + 576; // start byte of second diary field for(int i = 1; i < 15; i++) { // from the second to the 14th line fseek(FR,r + 64,SEEK_SET); // seek start byte of next diary line for(int j = 0; j < 64; j++) // pull the whole of the next/prev line's ED[j] = fgetc(FR); // content into ED[] ED[64] = (char)0; fseek(FR,r,SEEK_SET); // seek start byte of this diary line for(int j = 0; j < 64; j++) // store the whole of the content fputc(ED[j],FR); // into this line r += 64; // advance to start byte of next/prev line } fseek(FR,(rd << rs) + 1472,SEEK_SET); // seek start byte of 15th field unpackDate(sD,sM,sY); // put today's date in display form in ED[] putInt(packDate(),FR); // pack and save today's date to file for(int j = 4; j < 64; j++) // Fill the rest of the last diary fputc((char)0,FR); // line with null characters. fflush(FR); // push to disk in case of power failure getDiary(); // re-display the diary in full LN = 15; // new entry is always on line 15 after a shunt } /* PLACE TODAY'S DATE IN DIARY'S DATE FIELD Called from only 1 place each in newEntry() and MET2DY(). */ void insToday() { rD = sD; // make reference day today's day rM = sM; // make reference month today's day rY = sY; // make reference year today's day unpackDate(rD,rM,rY); // put today's date in display format PE = 7; // mark the date as having been modified editDY(2,0); // save and display today's date PE = -1; // set that comment field has NOT been modified editDY(1,1); // set up diary comment field for the Line Editor } /* TO CREATE AND SAVE A NEW DIARY ENTRY. When the NEW ENTRY button is toggled from dim to bright, todays date is placed on the next clear line and saved to disk. The comment field beside it is boxed ready for the Line Editor to pass and display a typed-in comment. When the NEW ENTRY button is toggled from bright to dim, the comment field is de-boxed and saved to disk. This function is called only from 2 places in DBCL(). */ void newEntry(int x) { // 1=button bright 0=button dim if(x) { // if NEW ENTRY button already illuminated, editDY(2,1); // de-box and save the comment field getDiary(); // re-display the whole diary } else { // else, if NEW ENTRY button NOT illuminated if(LL > 15) // if diary panel full newShunt(); // push diary lines up by one place insToday(); // place today's date in Diary's date field } } /* DIARY BUTTONS CONTROL LOGIC [DBCL]: The first 3 buttons are mutually exclu- sive. The last 3 buttons do not illuminate; each is respectively set to its 'on' state when clicked. Each is later returned to its 'off' state by the function that handles its respective action. The states of the DIARY'S con- trol buttons are kept in the variable T2B as the states of the lower 6 bits, the least significant bit representing the state of the first button [NEW ENTRY]. Called from 3 places in MET2CB(). T2B: 000001 [0X01] = NEW ENTRY on [bits going from right to left repre- 000010 [0X02] = EDIT DATE on sent buttons going from left to right] 000100 [0X04] = EDIT TEXT on */ void DBCL(int x) { // x = button number from left to right int y = 1 << x; // set the bit for button 'x' if(T2B & y) { // if button 'x' is illuminated switch(x) { case 0: newEntry(1); break; // save and unbox new entry case 1: editDY(2,0); break; // save and unbox date field case 2: // save and unbox comment field case 3: editDY(2,1); break; // save and unbox comment field } T2B &= ~y; // dim button 'x' } else { // else, if button 'x' is dim T2B = 0; // dim all buttons if(x == 0) // if doing the NEW ENTRY button newEntry(0); // show today's date, set up comment field for editing T2B |= y; // illuminate button 'x' } showT2buts(); // re-display the buttons } /* ONE OF THE ROW OF 6 BUTTONS AT THE BOTTOM OF THE DIARY TAB WAS CLICKED The values in the if() statements are the left and right edges of the buttons in pixels. MET2CB = Mouse Event for Tab 2 Control Buttons; Tab 2 = DIARY Tab. Called from only one place in MEH() case 2: */ void MET2CB() { if(mX > 23 && mX < 96) // if clicked on 'NEW ENTRY' button DBCL(0); // enable/disable New Entry edit mode else if(mX > 99 && mX < 172) // if clicked on 'EDIT DATE' button DBCL(1); // enable/disable Diary Date edit mode else if(mX > 175 && mX < 248) // if EDIT TEXT button was clicked DBCL(2); // enable/disable EDIT TEXT edit mode else if(mX > 251 && mX < 324) // if the INSERT button was clicked DBCL(3); // set up the mouse clicker for inserting else if(mX > 327 && mX < 400) // if the DELETE button was clicked DBCL(4); // set up the mouse clicker for deleting else if(mX > 403 && mX < 476) // if the HELP button was clicked helpPage("software/mktr.html#diary"); } // DELETE A CLICKED DIARY ENTRY. Called only from 1 place in MET2DY(). void delShunt() { if(LN < 0 || LN > 15 // outside the range of valid Diary line numbers || LL < 0 || LL > 16 // beyond the range of uninhabited line numbers || LN >= LL) { // selected line beyond currently inhabited lines printf("delShunt(): LL=%i LN=%i\n",LL,LN); return; // print rogue values to terminal and bail out } int r = (rd << rs) + 512 + (LN << 6); // start byte of line LN for(int i = LN; i < LL; i++) { // from current line to last inhabited line fseek(FR,r + 64,SEEK_SET); // seek start byte of line LN + 1 for(int j = 0; j < 64; j++) // Pull the whole of the line's ED[j] = fgetc(FR); // content into ED[]. fseek(FR,r,SEEK_SET); // seek start byte of line LN for(int j = 0; j < 64; j++) // store the whole of the content fputc(ED[j],FR); // into line LN r += 64; // advance to start byte of next line } /* Seek the start byte of what has become the new first uninhabited field now that the loop has finished. */ fseek(FR,(rd << rs) + 512 + ((LL - 1) << 6),SEEK_SET); for(int j = 0; j < 64; j++) // fill the whole of this diary fputc((char)0,FR); // line with null characters. fflush(FR); // push to disk in case of power failure getDiary(); // re-display the diary in full } /* SETS UP A CLICKED DIARY DATE FIELD OR DIARY COMMENT FIELD FOR EDITING Called from 2 places in MEH() case 2. */ void MET2DY() { int X = 116, // cursor x-datum x = 1; // x=0 date, x=1 comment // FIND THE LINE 'LN' ON WHICH THE MOUSE WAS CLICKED if(mY > 75 && mY < 350) { // vertical limits of both panels: int y = 76 + VV; // y-coord of bottom of first DATE field for(LN = 0; LN < 16; LN++) { // for each line in the diary if(mY < y) break; // exit if click above bottom of this field y += VV; // otherwise, advance to next lower line } } else return; // Exit because mouse click was outside the Diary Panel if(T2B & 16) { // if the Diary entry DELETE flag is set delShunt(); // shunt diary entries up over the deleted one T2B &= ~16; // dim the DELETE button showT2buts(); // re-display the buttons return; // exit } if(T2B & 8) { // if the Diary entry INSERT flag is set insShunt(); // shunt diary entries to make way for new entry insToday(); // need to insert today's date here as with NEW EDIRY CP = 0; // set cursor to start of line } else { if(mX > 23 && mX < 102) { // the click was in the dates panel if((T2B & 2) == 0) return; // exit if EDIT DATE button dim X = 28; x = 0; // cursor x-datum for date panel } else if(mX > 112 && mX < 475) { // the click was in the comments panel if((T2B & 4) == 0) return; // exit if EDIT TEXT button dim } else return; // click was outside both panels CP = (mX - X) / 6; // set cursor position } editDY(1,x); // set up DATE [x=1] or COMMENT [x=2] for editing } void MT2() { // HANDLES MOUSE CLICKS WITHIN THE DIARY TAB if(mY > 359) // click was in the control buttons area MET2CB(); // across the bottom of the tab else // the click was in the Diary area MET2DY(); } // T3----------------- FUNCTIONS PERTAINING TO THE DATA TAB ------------------- void showT3buts() { #define bw 76 // button width const int Pb[6] = {6,6,6,6,6,21}, // insets for PROFILE button annotations Bl[6] = {9,9,9,9,9, 4}; // lengths of the button annotations const char // PROFILE buttons *PB[6] = { " SPARE ", " SPARE ", " SPARE ", "EDIT META", "EDIT DATA", "HELP", }; for(int i = 0; i < 6; i++) { // for each of the 6 control buttons XSetForeground(XD,XG,DARK); // display button backgrounds XFillRectangle(XD,XW,XG,24 + i * bw,360,bw - 4,BH); if(T3B & 1 << i) // if this button is illuminated XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,27 + *(Pb + i) + i * bw,375,*(PB + i),*(Bl + i)); } } /* DISPLAY THE INITIAL CONTENT OF THE DATA TAB [TAB 3]. CALLED ONLY FROM ONE PLACE IN SHOWtAB(). */ void T3show() { clearPanel(9); // display the blank META panel clearPanel(10); // display the blank DATA panel char S[64]; int N = 0x183D, // selector of the name & address lines to be displayed V = 89, // vertical coord of the first display line R = rd << rs, // start byte of the current contact record L; XSetForeground(XD,XG,AMAREL); XDrawString(XD,XW,XG,24,70,"COMPANY",7); XDrawString(XD,XW,XG,214,70,"META DATA",9); XDrawString(XD,XW,XG,380,70,"DATA",4); XSetForeground(XD,XG,0x00FFFF); XDrawString(XD,XW,XG,24,207,"CONTACT",7); XSetForeground(XD,XG,YELLOW); // display company details in yellow for(int i = 0; i < 14; i++) { // display the company and contact details if(i == 8) // display contact details in cyan XSetForeground(XD,XG,CYAN); fseek(FR,R,SEEK_SET); // start byte of current field if(N & 1) { // if this line must be displayed L = fgetc(FR); // length of field content if(L < 0) break; // corrupt line length if(L > 29) L = 29; // if line too long to display for(int j = 0; j < L; j++) // for each character in the line S[j] = fgetc(FR); // put it in the scratch array XDrawString(XD,XW,XG,24,V,S,L); // display the array content V += 17; // drop to the next line if(i == 5) V += 51; // skip to line for contact details } N >>= 1; // advance to the next line's display switch R += 32; // advance to the start byte of the next line } R = 8704; V = 89; XSetForeground(XD,XG,YELLOW); // display the META DATA titles in yellow for(int i = 0; i < 16; i++) { // display the 16 items of the Meta Data if(i == 8) // beyond line 8, display in cyan XSetForeground(XD,XG,CYAN); fseek(FK,R,SEEK_SET); // start byte of current field within record L = fgetc(FK); // get length of line content if(L < 0) break; // corrupt line length if(L > 25) L = 25;; // if line too long to display for(int j = 0; j < L; j++) // for each character in the line S[j] = fgetc(FK); // put it in the scratch array XDrawString(XD,XW,XG,212,V,S,L); // display the array content R += 32; // advance to the start byte of the next line V += 17; // drop to the next line } R = (rd << rs) + 1536; V = 89; XSetForeground(XD,XG,YELLOW); // display the DATA in yellow for(int i = 0; i < 16; i++) { // display the 16 items of the Meta Data if(i == 8) // beyond line 8, display in cyan XSetForeground(XD,XG,CYAN); fseek(FR,R,SEEK_SET); // start byte of current field within record L = fgetc(FR); // get length of line content if(L < 0) break; // corrupt line length if(L > 15) L = 15; // if line too long to display for(int j = 0; j < L; j++) // for each character in the line S[j] = fgetc(FR); // put it in the scratch array XDrawString(XD,XW,XG,380,V,S,L); // display the array content R += 16; // advance to the start byte of the next line V += 17; // drop to the next line } showT3buts(); // display the control buttons across the bottom of the Tab } void edKN3() {} // dummy void edKV3() {} // dummy void edKW3() {} // dummy /* PREPARES THE META [x=0] OR DATA [x=1] FIELDS FOR EDITING by setting para- meters that determine the field's position within the DATA Tab plus the length of each field and of its current content. It's called with a switch value [sw] that invokes one of 3 cases. In all cases the field's area is first cleared to the DARK background. case 0: Loads the original content of the field into the Line Editor's array ED[]. case 1: Displays a yellow box around the field. Draws the cursor. Then loads the original content of the field into the Line Editor's array ED[] as in case 0. case 2: Saves the edited META or DATA entry [if modified] to the appro- priate name & address record. In all cases, the field content is finally re-displayed. Called from 1 place each in MET3DY() & newEntry() 2 places in updn(). */ void editMD(int sw, int x) { // sw: 0=show, 1=get/edit, 2=put if(LN < 0) return; // outside line range [just for safety] if(x) { PN = 10; FY = FR; } // set appropriate panel number else { PN = 9; FY = FK; } // and file pointer setupField(); // start byte of required field int c = WHITE; // text display colour drawFld(); // draw the dark grey field background switch(sw) { // switch: 0=show, 1=get/edit, 2=put case 1: drawBox(); // box the field ready for editing case 0: // load original content from the file getField(FY,1); // load date into Line Editor array ED[] if(sw == 1) drawCur(); // display the green cursor break; case 2: // save and de-box the META field putField(FY,1); // store the field content to disk case 3: if(LN < 8) c = YELLOW; // if one of the comany Company items else c = CYAN; // else it is one of the Contact fields } showLine(c); // display the textual content of the field } /* SETS UP A CLICKED META OR DATA FIELD FOR EDITING Called from 2 places in MEH() case 3. */ void MET3MD() { int X, x = 0; // x=0 META, x=1 DATA if(mY > 75 && mY < 350) { // vertical limits of both panels: if(mX > 205 && mX < 364) x = 1; // the click was in the META panel if(mX > 373 && mX < 476) x = 2; // the click was in the DATA panel } switch(x) { // switch on which of the two panels case 0: // click was outside both panels return; // so exit break; case 1: // META panel if((T3B & 8) == 0) // EDIT META button dim return; // so exit X = 210; // cursor datum for META panel break; case 2: // DATA panel if((T3B & 16) == 0) // EDIT DATA button dim return; // so exit X = 378; // cursor datum for DATA panel } // COMPUTE LINE NUMBER 'LN' int y = 76 + VV; // bottom of first META field for(LN = 0; LN < 16; LN++) { // for each line in the diary if(mY < y) break; // if click above bottom of this field, finish y += VV; // advance to next lower line } CP = (mX - X) / 6; // set cursor position editMD(1,x - 1); // set up META field for editing } /* UNBOX ANY BOXED FIELD IF BOXED AND SAVE [OR NOT] AS NECESSARY Called only from 4 places in edData() below. */ void saveMD(int x) { if(x) { // we're dealing with a META field if(PE == 9) // if a META field has been modified editMD(2,0); // save and de-box the current META line else // else, the field is boxed but has not been modified editMD(3,0); // so simply de-box the current DATA line } else { // we're dealing with a DATA field if(PE == 10) // if the DATA field has been modified editMD(2,1); // save and de-box the current DATA line else // else, the field is boxed but has not been modified editMD(3,1); // so simply de-box the current DATA line } } /* ONE OF THE DATA TAB'S CONTROL BUTTONS HAS BEEN CLICKED Called from only 2 places in MET3CB(). */ void edData(int x) { switch(x) { // switch on Panel Number case 3: // the EDIT META button has been clicked if(T3B & 8) { // if the EDIT META button is bright T3B &= ~8; // dim it saveMD(1); // unbox and save META field as necessary } else { // else, if EDIT META button is dim T3B |= 8; // illuminate it if(T3B & 16) { // if the EDIT DATA button is bright T3B &= ~16; // dim it saveMD(0); // unbox and save DATA field as necessary } } break; case 4: // the EDIT DATA button has been clicked if(T3B & 16) { // if the EDIT DATA button is bright T3B &= ~16; // dim it saveMD(0); // unbox and save DATA field as necessary } else { // else, if the EDIT DATA button is dim T3B |= 16; // illuminate it if(T3B & 8) { // if the EDIT META button is illuminated T3B &= ~8; // dim it saveMD(1); // unbox and save META field as necessary } } break; } showT3buts(); // re-display the buttons } /* ONE OF THE ROW OF 8 BUTTONS AT THE BOTTOM OF THE DATA TAB WAS CLICKED The values in the if() statements are the left and right edges of the buttons in pixels. MET3CB = Mouse Event for Tab 3 Control Buttons; Tab 3 = DATA Tab. Called from only one place in MEH() case 3: */ void MET3CB() { if(mX > 23 && mX < 96) // if clicked on 'EDIT NAMES' button edKN3(); // toggle Key Names edit/select modes else if(mX > 99 && mX < 172) // if clicked on 'EDIT VALUES' button edKV3(); // toggle Key Values edit/select modes else if(mX > 175 && mX < 248) // DIARY button clicked edKW3(); // open the contact's Diary file else if(mX > 251 && mX < 324) // EDIT DATA button clicked edData(3); // set up or cancel META field editing else if(mX > 327 && mX < 400) // EDIT DATA button clicked edData(4); // set up or cancel DATA field editing else if(mX > 403 && mX < 476) // HELP button clicked helpPage("software/mktr.html#data"); } void MT3() { // HANDLES MOUSE CLICKS WITHIN THE DATA TAB if(mY > 359) // click was in the control buttons area MET3CB(); // area across the bottom of the tab else // clicked in META or DATA tab areas MET3MD(); } // T4--------------- FUNCTIONS PERTAINING TO THE REGIONS TAB ------------------ /* SHOW REGION TAB'S BOTTOM BUTTONS Called from 1 place in T4show() and 2 places in MT4() */ void T4buts() { char // PROFILE buttons *PB[8] = {" LIST ","SHOW ALL","TARGET 1","TARGET 2", "TARGET 3","TARGET 4"," STATIS "," HELP "}; int g = 24, // y-coord of left side of first button h = 25, // y-coord of start of first letter k = BW - 4; // width of shaded button for(int i = 0; i < 8; i++) { // for each of the 6 control buttons char *H = PB[i]; // pointer to start of current annotation int j = 2; // inset for annotation of current button XSetForeground(XD,XG,DARK); // display button background XFillRectangle(XD,XW,XG,g,360,k,BH); if(T4B & 1 << i) // if displaying the selected button XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,j + h,375,H,8); g += BW; // advance to left side of next button h += BW; // advance to start of next button's annotation } } /* DISPLAY THE REGIONS INFORMATION AND MAP WITHIN THE REGIONS TAB WINDOW Called from T4sel(), T4buts() and T4show(). */ void T4data() { int v = 70, // vertical start position of the first region name n = 0, // total number of bytes in the region name + post areas line i,j; // loop variables coastLine(); // draw the British Isles coast line in green XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,LM,61,11,281); for(i = 0; i < 18; i++) { // for each of the 6 control buttons int n = rg[i]; // number of bytes in region string if(T4R[T4T] & 1 << i) { // if this region is illuminated int r = 16, // start byte of post area letters q = n - r; // number of post areas in this region * 3 for(j = 0; j < q; j += 3) // for each post area in this region /* Put the post area's first letter, as an ASCII code, into an 'int', shift it left by 8 bit positions and then put the post area's second letter, as an ASCII code, into the lower 8 bits of the same integer. Pass this integer to the post area paint function postArea() to dis- play this post area on the map on the right of the Regions window. */ postArea((int)RG[i][j + r] << 8 | (int)RG[i][j + r + 1]); XSetForeground(XD,XG,AMAREL); // display in yellow } else // otherwise XSetForeground(XD,XG,GREY); // display in grey char S[3]; sprintf(S,"%2i",T4S[i]); // format the percentage display XDrawString(XD,XW,XG,LM,v,S,2); // region's % of name & address records XDrawString(XD,XW,XG,LM+20,v,RG[i],n); // draw region's name & codes line v += 16; // drop down to the next line } XDrawString(XD,XW,XG,340,294,"Generate target lists",21); XDrawString(XD,XW,XG,340,310,"of names & addresses",20); XDrawString(XD,XW,XG,340,326,"for specific regions.",21); } void T4getStats() { FILE *F = fopen("geo/stats.dat","r"); // open region stats file for(int i = 0; i < 18; i++) T4S[i] = getInt(F); fclose(F); } /* SELECT/DESELECT THE CLICKED REGION NAME. Called from T4but1() and MT4(). */ void T4sel() { int t = 1; // test bit [starts in least significant position /* 62 = vertical coord of top of first region name within window 350 = 62 + 16 x 18 vertical pixel drop of all 18 names 16 = the number of vertical pixels [leading] between region names. */ for(int v = 60; v < 348; v += 16) { // for each of the 18 region names if(mY > v && mY < v + 16) // if mouse within vertical limits of name if(T4R[T4T] & t) // then if the name was already selected T4R[T4T] &= ~t; // deselect it else // otherwise it was not already selected T4R[T4T] |= t; // so select it t <<= 1; // advance test bit to next higher position within the register } T4data(); // redisplay the name highlighting after selection } /* LOAD THE REGION DATA FOR GEOGRAPHIC TARGETING. Called only from initLoad(). */ void T4load() { int f = 0, // declare and reset the 'in comment' flag m = 0, // region number n = 0, // character count within region line c; // to hold the currently inputted character FILE *F = fopen("geo/regions.txt","r"); // Open the file for reading. while(!feof(F)) { // while we haven't yet reached the end of the file c = fgetc(F); // get the next character from the file 'regions.txt' if(f == 1) { // if we are inside a comment line if(c == '\n') { // if we have reached the end of the comment line f = 0; // reset the 'in comment' flag n = 0; // reset the line's character count } } // loop back ignoring the current character else { // otherwise we must be outside a comment line /* If current char is a '#' we are at the beginning of a comment line, so set the 'in comment' flag and loop back to get the next char. */ if(c == '#') f = 1; else { // we are outside a comment line so we must be in a data line if(c == '\n') { // If we have reached the end of a non-comment line if(n > 0) { // then provided it was not a blank line: RG[m][n] = '\0'; // terminate the string in the // RG[][] array with a null character rg[m++] = n; // save the line length } n = 0; // reset the character counter of the line string array } /* else we've not yet reached the end of the data line so add the current character to the line's string array. */ else RG[m][n++] = c; } } } fclose(F); // close the 'regions.txt' data file } /* LOAD THE CURRENT SELECTIONS FOR TARGETS 0 TO 4 When none of the TARGET buttons is illuminated, TARGET 0 is assumed. Called only from initLoad(). */ void T4get() { FILE *F = fopen("geo/geotargs.dat","r"); // open targets file for reading T4T = getInt(F); // currently selected target 0,1,2,3,4 T4R[0] = getInt(F); // currently selected regions for Target 0 T4R[1] = getInt(F); // currently selected regions for Target 1 T4R[2] = getInt(F); // currently selected regions for Target 2 T4R[3] = getInt(F); // currently selected regions for Target 3 T4R[4] = getInt(F); // currently selected regions for Target 4 fclose(F); // close the targets file } /* SAVE THE CURRENT SELECTIONS FOR TARGETS 0 TO 4 When none of the TARGET buttons is illuminated, TARGET 0 is assumed. Called only from T4but0() [see below]. */ void T4put() { FILE *F = fopen("geo/geotargs.dat","w"); // open targets file for reading putint(T4T,F); // currently selected target 0,1,2,3,4 putint(T4R[0],F); // currently selected regions for Target 0 putint(T4R[1],F); // currently selected regions for Target 1 putint(T4R[2],F); // currently selected regions for Target 2 putint(T4R[3],F); // currently selected regions for Target 3 putint(T4R[4],F); // currently selected regions for Target 4 fclose(F); // close the targets file } /* CREATE A LIST OF ALL THE NAME & ADDRESS RECORDS THAT FALL WITHIN THE REGION SPECIFIED WITHIN THE REGIONS TAB WINDOW AND SAVE THESE SELECTIONS IN THE SLECTED TARGET'S 'geolst' FILE: 'geolst0.txt', 'geolst1.txt', 'geolst2.txt', 'geolst3.txt', 'geolst4.txt' and their corresponding '.dat' files. Called only from MT4(). */ void T4but0() { // make regional target list char S[16] = "geo/geolst .txt", T[16] = "geo/geolst .dat"; S[10] = T4T + 48; // insert target number into the filename string T[10] = T4T + 48; // insert target number into the filename string FILE *F = fopen(S,"w"); // open an empty geographic target file if(T4B & 1) { // if the CREATE LIST button is illuminated T4B &= 126; // clear it fprintf(F,"# TARGET LIST EMPTY\n"); fclose(F); // create empty file, close it return; } T4B |= 1; // illuminate the CREATE LIST button T4put(); // save the current target selection data time_t now; // `time_t` is an arithmetic time type time(&now); // get current time fprintf(F,"%s %i %s %s","# TARGET LIST",T4T,"CREATED:",ctime(&now)); fprintf(F,"# SHOWING THE NUMBERS OF THE NAME & ADDRESS RECORDS\n"); fprintf(F,"# THAT APPEAR IN THE SELECTED REGIONS. POST AREAS\n"); fprintf(F,"# WITH NO ADDRESSES ON FILE ARE EXCLUDED.\n"); FX = FP; // set to search the postcode index [required by search()] lS = 2; // always just two letters [required by search()] HL = 0; // lowest exact match required for search() FILE *G = fopen(T,"w"); // open an empty geographic target file int N = 1; // total number of records in geographic target file putInt(N,G); // initially, set total number of records to 1 for(int i = 0; i < 18; i++) { // for each of the 18 regions int n = rg[i]; // number of bytes in region string if(T4R[T4T] & 1 << i) { // if this region is selected fprintf(F,"\n# %s\n",RG[i]); // print details of this included region int r = 16, j; // start byte of post area letters int q = rg[i] - r; // number of post areas in this region * 3 for(j = 0; j < q; j += 3) { // for each post area within this region SF[0] = RG[i][j + r], // 1st character of 2-letter post area SF[1] = RG[i][j + r + 1]; // 2nd character of 2-letter post area search(); /* Find the 1st occurrence, within the 'contacts.post' file of this 2-letter post area code. Note: record num- bers within 'contacts.post' start with 'Record 1'. Record zero is used to store the current number of entries in the postcode index. */ if(SR > 0) continue; // skip if exact post area code not in index int x = getRecNum(); fprintf(F,"%s %06i\n",SF,getRecNum()); // print 1st occurrence entry putInt(x,G); // put the record number in the geographic target list N++; // increment the number of records in the target file ix++; // advance to next record within the postcode index /* While the next record in 'contacts.post' contains the same 2- letter post area code as last time, add an entry to 'geolst.txt' and to 'geolst.dat'. */ while(1) { // while not yet encountered a mismatch getIndexEntry(); // load the 'ix'th postcode into XE[] if(StrCmp(SF,XE) != 0) break; // bail out on mismatch x = getRecNum(); fprintf(F," %06i\n",x); // print next occurrence entry putInt(x,G); N++; ix++; } } // end of for(j = 0; j < q; j += 3) } } fclose(F); fclose(G); // close both files G = fopen(T,"r+"); // re-open geographic target file in random mode putInt(N,G); // save total number of records in record '0' fclose(G); // close the file } void T4but1() { // select all regions if(T4B & 2) { // if the SELECT ALL button is illuminated T4B &= 124; // clear the SELECT ALL and CREATE LIST buttons T4R[T4T] = 0; // clear all regions for selected Target } else { // else SELECT ALL is dark, so T4B &= 124; // clear the SELECT ALL and CREATE LIST buttons T4B |= 2; // illuminate it T4R[T4T] = 0x3FFFF; // select all regions } T4sel(); } void T4but(int x){ // x = target number 1, 2, 3, 4 int y = 2 << x; // y = button bit position if(T4B & y) { // if target button x is already selected T4B &= 64; // deselect all target buttons T4T = 0; // set to TARGET 0 } else { // otherwise it was not already selected T4B &= 66; // de-select all target buttons + CREATE LIST button T4B |= y; // select the TARGET 1 button T4T = x; // set TARGET 1 active } T4data(); // re-display the target selections data } /* GENERATE, DISPLAY AND STORE THE PERCENTAGES OF THE TOTAL NAMES & ADDRESSES IN EACH REGION. Called from only one place in MT4(). */ void T4but6() { if(T4B & 64) return; // exit if the STATS button is illuminated T4B |= 64; // illuminate the STATS button T4buts(); int i,j,k, // loop index variables x, // scratch-pad integer variable T = 0, // accumulator for total number of valid records Q[18][20], // up to 20 post areas per Region W[18]; // number of post areas in each Region char S[32]; for(j = 0; j < 18; j++) { // for each of the Regions T4S[j] = 0; // Clear the j_th Region's address counter int V = rg[j], // number of bytes in j_th post area list w = 0; // counts number of post areas in j_th Region /* Skip the 16 bytes Region name in each Regions data string. Each post area code in the data string occupies 2 bytes + a space, so there are 3 byes for every integer entry in the counters arrat Q[][]. */ for(k = 16; k < V; k += 3) { x = RG[j][k] << 8; // get 1st byte of k_th post area & shift it up x |= RG[j][k + 1]; // OR-in the second byte Q[j][w++] = x; // set this 2-byte post area in the Q-array } // increment k by 3 to skip the space char W[j] = w; // number of post areas in the j_th Region } for(i = 1; i < RD; i++) { // for each address on file int r = i << rs; // start byte of the i_th record fseek(FR,r,SEEK_SET); // seek this record's start byte if(fgetc(FR) & 0x80) // if high first bit of its 1st byte is set continue; // the record is deleted, so skip it ++T; // increment total number of valid records fseek(FR,r + 193,SEEK_SET); // start byte of the i_th record's postcode int p = getShort(FR); // get its 2-letter post area for(j = 0; j < 18; j++) { // for each region int K = W[j]; // number of bytes in the j_th Region's data for(k = 0; k < K; k++) // for each of this Region's post areas if(p == Q[j][k]) { // if the record's post area is one of them ++T4S[j]; // increment region's addresses counter break; // found, so don't need to check the rest } } } if(T > 0) for(j = 0; j < 18; j++) { // CONVERT THE COUNTS TO PERCENTAGES x = T4S[j]; // number of addresses in the j_th Region x = 100 * x / T; // percentage of addresses in the j_th Region if(x > 99) x = 99; // guard against overshoot to 3 digits T4S[j] = x; // store percentage in original array element } FILE *F = fopen("geo/stats.dat","w"); // open region stats file for(int i = 0; i < 18; i++) putInt(T4S[i],F); // save the post area counts per Region fclose(F); T4data(); // show data for current TARGET T4B &= ~64; // extinguish the STATS button T4buts(); // show data for current TARGET } /* AREAS TAB CASE FOR THE INITIAL DRAWING OF THE TAB CONTENT Called from only one place in showTab(). */ void T4show() { switch(T4T) { case 0: // show data for TARGET 0 T4getStats(); // get the current Region Statistics T4data(); // display the Region Data break; case 1: T4but(1); break; // target button 1 case 2: T4but(2); break; // target button 2 case 3: T4but(3); break; // target button 3 case 4: T4but(4); // target button 4 } T4buts(); } void MT4() { // HANDLES MOUSE CLICKS WITHIN THE REGIONS TAB if(mY > 60 && mY < 344 // mouse within vertical limits of region names && mX > 43 && mX < 140) { // and also within horizontal limits T4sel(); // deal with the region selection click T4B &= ~2; // clear the SELECT ALL button T4buts(); // re-display the bottom buttons return; // if a region name was clicked then clearly } // a button cannot have been clicked if(mY < 360 || mY > 380) // outside vertial limits of the buttons return; if(mX > 23 && mX < 77) T4but0(); // create LIST button was clicked else if(mX > 80 && mX < 134) T4but1(); // SHOW ALL button was clicked else if(mX > 137 && mX < 191) T4but(1); // TARGET 1 button was clicked else if(mX > 194 && mX < 248) T4but(2); // TARGET 2 button was clicked else if(mX > 251 && mX < 305) T4but(3); // TARGET 3 button was clicked else if(mX > 308 && mX < 362) T4but(4); // TARGET 4 button was clicked else if(mX > 365 && mX < 419) T4but6(); // STATISTICS button was clicked else if(mX > 422 && mX < 476) // HELP button was clicked helpPage("software/mktr.html#regions"); else return; // exit if click was not on an actual button T4buts(); // re-display the buttons } // T5--------------- FUNCTIONS PERTAINING TO THE TARGETS TAB ------------------ /* LOAD THE CURRENT KEY VALUE SELECTIONS FOR THE CURRENT TARGET NUMBER When none of the TARGET buttons is illuminated, TARGET 0 is assumed. Called only from T5sel() and T5show(). 'x' is the number of the currently selected target. */ void T5get(int x) { char S[17] = "key/keytarg .dat"; // name template for target file S[11] = x + 48; // insert target number as a numeric character FILE *F = fopen(S,"r"); // open target's file for reading for(int i = 0; i < 16; i++) // for each of the 16 keys for this target T5V[i] = getInt(F); // get its currently selected key values for(int i = 0; i < 5; i++) // get the 5-character SIC code SI[i] = fgetc(F); for(int i = 0; i < 20; i++) // get the 20-character Selector Code SC[i] = fgetc(F); for(int i = 0; i < 11; i++) // get the 11-character Start Date D1[i] = fgetc(F); for(int i = 0; i < 11; i++) // get the 11-character End Date D2[i] = fgetc(F); fclose(F); // close the target file } /* SHOW TARGET TAB'S BOTTOM BUTTONS Called from 1 place each in T5but0(), T5sel(), T5show() and MT5(). */ void T5buts() { char // PROFILE buttons *PB[8] = {" LIST "," SAVE ","TARGET 1","TARGET 2", "TARGET 3","TARGET 4"," STATIS "," HELP "}; int g = 24, // y-coord of left side of first button h = 27; // y-coord of start of first letter XSetForeground(XD,XG,DARK); // shade button background for(int i = 0; i < 8; i++) // draw each button XFillRectangle(XD,XW,XG,LM + BW * i,360,53,21); XSetForeground(XD,XG,GREY); // most buttons are grey for(int i = 0; i < 8; i++) { // for each of the 6 control buttons if(T5B & 1 << i) // if displaying a selected button XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,h,375,PB[i],8); g += BW; // advance to left side of next button h += BW; // advance to start of next button's annotation } } /* TEST A NAME & ADDRESS RECORD'S KEY VALUES AGAINST THE TARGET PROFILE Take care when modifying this function: the logic is a bitch. Called only from one place in T5list(). */ int T5checkKVs(int r) { fseek(FR,r,SEEK_SET); // seek start byte of current record if(fgetc(FR) & 0x80) // if high first bit of first byte is set return 1; // the record is deleted, so skip it int e = 0; // assume this record must be included for(int j = 0; j < 16; j++) { // for each of the 16 Keys int a = T5V[j]; // Target's bit pattern of Values for Key 'j'. if(a == 0) // if no bits are set, skip this Key continue; /* move on to next key SEEK WITHIN RECORD 'i' THE START BYTE OF THE j_th KEY'S SELECTOR BITS record number 'i' times the record size = start byte of record | offset of key selection registers | | key number 'j' x 2 bytes per short integer | | | */ fseek(FR,r + 456 + (j << 1),SEEK_SET); if(a & getShort(FR)) // if Target and Record share at least 1 common bit continue; // record can be included, so go check further Keys e = 1; // set the exclusion flag break; // no need to check further Keys } return e; // returns 0 if record to be included, 1 if not } /* CHECK THE 5 DIGITS OF THE SIC NUMBER FROM LEFT TO RIGHT If the first character of the Target's SIC number is a space, the SIC number test is skipped entirely. Otherwise, for all of the 5 characters, if the Target's version is not a space AND it is the same as the name & address record's version, then, as far as the SIC test is concerned, the record is to be included in the Target List. */ int T5checkSIC(int r) { fseek(FR,r + 488,SEEK_SET); // seek start byte of SIC number int e = 0; // assume this record must be included for(int j = 0; j < 5; j++) { // for each of the 5 SIC digit characters /* If the Target's current character is a 'space' then [further] testing of the SIC is irrelevant, so break out of loop with a 0 return value. */ if(SI[j] == ' ') break; /* If the Target's current character is not the same as the corresponding character of the name & address record's version of the SIC, this name and address record must be rejected. So set the exclusion flag and terminate the loop. */ if(SI[j] != fgetc(FR)) { e = 1; break; } } return e; // returns 0 if record to be included, 1 if not } /* CHECK THE 20 CHARACTERS OF THE SELECTOR CODE FROM LEFT TO RIGHT If the first character of the Target's Selector Code is a space, the Sel- ector Code test is skipped entirely. Otherwise, for all of the 20 char- acters, if the Target's version is not a space AND it is the same as the name & address record's version, then, as far as the Selector Code test is concerned, the record is to be included in the Target List. */ int T5checkSel(int r) { int e = 0; // at first assume the record must be included fseek(FR,r + 488,SEEK_SET); // seek start byte of Selector Code if(SC[0] != ' ') // provided the 1st character isn't a space for(int j = 0; j < 20; j++) // for each of the 20 Selector Code chars if(SC[j] != ' ' // if Target's current character isn't a space && SC[j] != fgetc(FR)) { // & it isn't the same as the record's version e = 1; // then set the record exclusion flag break; // no need to test any further characters } return e; // 0=OK, 1=exclude this record } /* TEST IF RECORD'S LAST EVENT DATE IS BETWEEN THE START AND END DATES SPECIFIED IN THE TARGET PROFILE. NOTE: if either of the Target Profile's date fields contains only space characters, the test against that date is ingnored. First seek the start byte of the name & address record's Last Event date and assemble it in an unsigned integer variable 'd'. */ int T5checkDat(int r) { int j; fseek(FR,r + 448,SEEK_SET); // set file pointer to start of Last Event date /* GET THE LAST EVENT DATE FROM THE NAME & ADDRESS RECORD This occupies a 32-bit integer [i.e. 4 bytes of 8 bits each]. */ unsigned int d = fgetc(FR) << 24 // get and shift the upper byte of year number | fgetc(FR) << 16 // get, shift & add in lower byte of year number | fgetc(FR) << 8 // get, shift & add in the month number | fgetc(FR); // add in the day number for(j = 0; j > 11; j++) // put the Target Profile's ED[j] = D1[j]; // Start Date in the ED[] array ED[11] = (char)0; // terminating null character for packDate() /* If the second character of the Target Profile's Start Date isn't a 'space' AND the record's Last Event date is before the Start Date, then this record must be excluded. */ if(ED[1] != ' ' && d < packDate()) return 1; for(j = 0; j > 11; j++) // put the Target Profile's ED[j] = D2[j]; // End Date in the ED[] array ED[11] = (char)0; // terminating null character for packDate() /* If the second character of the Target Profile's End Date isn't a 'space' AND the record's Last Event date is after the End Date, then this record must be excluded. */ if(ED[1] == ' ' && d > packDate()) return 1; return 0; } /* GENERATE THE TARGET LIST RESTRICTED TO THE CURRENT TARGET PROFILE Called only from 1 place in T5but1(). */ void T5list() { // SET UP THE TARGET LIST'S FILENAME OF THE CURRENTLY SELECTED TARGET char S[16] = "key/keylst .txt", // file name template for '.txt' file T[16] = "key/keylst .dat"; // file name template for '.dat' file int c = T5T + 48; // target number as a character S[10] = c; // insert target number character in '.txt' filename T[10] = c; // insert target number character in '.dat' filename FILE *F = fopen(S,"w"); // open target list txt file for writing FILE *G = fopen(T,"w"); // open target list dat file for writing int N = 1; // total number of records in '.dat' file putInt(N,G); // store it in corresponding '.dat' file // INSERT THE TARGET LIST FILE'S TITLE, DATE AND INSTRUCTIONS time_t now; // `time_t` is an arithmetic time type time(&now); // get current time fprintf(F,"%s %i %s %s","# TARGET LIST",T5T,"CREATED:",ctime(&now)); fprintf(F,"# SHOWING THE NUMBERS OF THE NAME & ADDRESS RECORDS\n"); fprintf(F,"# THAT HAVE KEY VALUES WHICH MATCH THOSE SPECIFIED\n"); fprintf(F,"# IN TARGET PROFILE %i.\n\n",T5T); // TEST EACH NAME & ADDRESS RECORD IN TURN AGAINST THE TARGET PROFILE for(int i = 1; i < RD; i++) { // for each name & address record on file int r = i << rs; // start byte of i_th name & address record if(T5checkKVs(r) == 0 // if Key Values check is OK && T5checkSIC(r) == 0 // and SIC number check is OK && T5checkSel(r) == 0 // and Selector Code check is OK && T5checkDat(r) == 0 ) { // and Dates Check is OK int x = getRecNum(); fprintf(F,"%06i\n",i); // store the record number in the Target List putInt(i,G); // store it in the corresponding '.dat' file N++; // increment total number of records written } } fclose(F); fclose(G); // close the Target List file G = fopen(T,"r+"); // open target '.dat' file for random access putInt(N,G); // store it in corresponding '.dat' file fclose(G); // close the Target List '.dat' file } /* COMPILE AND SAVE THE KEYS STATISTICS IN FILE 'KeyStats.txt' Called from only one place in MT5(). */ void T5stat() { int C[16][16], // counters i,j,k; char S[33] = "key/KeyStats .txt"; // file name template S[12] = T5T + 48; // insert target number as a character FILE *F = fopen(S,"w"); // open target list file for reading // INSERT THE TARGET LIST FILE'S TITLE, DATE AND INSTRUCTIONS time_t now; // `time_t` is an arithmetic time type time(&now); // get current time fprintf(F,"%s %i %s %s","# TARGET",T5T,"STATISTICS CREATED:",ctime(&now)); fprintf(F,"# SHOWING THE NAME OF EACH KEY, WITH ITS POSSIBLE VALUES.\n"); fprintf(F,"# AGAINST EACH VALUE IS SHOWN THE NUMBER OF NAME & ADDRESS\n"); fprintf(F,"# RECORDS WITH THAT VALUE SET + THE PERCENTAGE THEREOF.\n\n"); for(i = 0; i < 16; i++) // zero all the set Key Values counters for(j = 0; j < 16; j++) C[i][j] = 0; // COUNT IN HOW MANY NAMES & ADDRESS RECORDS EACH KEY VALUE APPEARS SELECTED int T = 0; // counter of active name & address records for(i = 0; i < RD; i++) { // for each name & address record int r = i << rs; // start byte of the i_th record fseek(FR,r,SEEK_SET); // seek this record's start byte if(fgetc(FR) & 0x80) // if high first bit of its 1st byte is set continue; // the record is deleted, so skip it ++T; // increment total number of valid records r += 456; // start byte of record's key values for(j = 0; j < 16; j++) { // for each key within that record fseek(FR,r,SEEK_SET); // start byte of this key values int x = getShort(FR); // get the bit pattern for this key for(k = 0; k < 16; k++) { // for each value of that key if(x & 1 << k) // if value selected, ++C[j][k]; // increment counter } r += 4; // advance 4 bytes to the start byte of the next key value } } fprintf(F,"%s %i %s","There are",T,"active "); fprintf(F,"name & address records on file.\n\n"); /* PRINT KEY NAMES AND PRINT THEM WITH THEIR CORRESPONDING 16 KEY VALUES + THE NUMBER OF OCCURRENCES OF EACH KEY VALUE BEING SET */ int P = 0, // file pointer for Key Names Q = KL << 4; // file pointer for Key Values for(i = 0; i < 16; i++) { // for each of the 16 Key Names fseek(FK,P,SEEK_SET); // start byte number of required key name int J = fgetc(FK); // get its length in bytes if(J < 0 || J > SL) // safety net break; // exit if name too long for(j = 0; j < J; j++) // for each byte of the key name S[j] = fgetc(FK); // store it in j_th element of the array for(j = J; j < 36; j++) // pad out the Key Name line S[j] = '_'; // with underscore characters S[j] = (char)0; // put terminating null character at end fprintf(F,"%s\n",S); // print the Key Name P += KL; // advance to start of next field's data for(j = 0; j < 16; j++) { // for each of this key's values fseek(FK,Q,SEEK_SET); // start byte number of required key name int N = fgetc(FK); // get its length in bytes if(N < 0 || N > 25) // safety net break; // if Key Value too long if(N > 0) { // provided the Key Value is not blank for(k = 0; k < N; k++) // for each byte of the key name S[k] = fgetc(FK); // store it in 1st element of the S[] array for(k = N; k < 24; k++) // pad out the Key Value to 24 characters S[k] = ' '; // using 'space' characters S[k] = (char)0; // insert terminating null character int x = -1, y = -1; // '-1' signifies 'figures unknown' if(T > 0) { // provided there is at least 1 valid record x = C[i][j]; // count for current Key Value y = 100 * x / T; // this expresses as a percentage if(y > 99) y = 99; // guard against overshoot to 3 digits } fprintf(F,"%s %6i %3i%s\n",S,x,y,"%"); } Q += KL; // advance to start of next field's data } fprintf(F,"\n"); // blank line between Key Name + Values blocks } fclose(F); } /* SAVE THE CURRENT KEY VALUES SELECTIONS FOR THE CURRENT TARGET IN FILE 'keytargN.txt', where 'N' can be 0,1,2,3,4,5,6. Called from 1 place in MT5() when the SAVE button is clicked. */ void T5but1() { if(T5B & 2) // if the SAVE button is illuminated T5B &= ~2; // clear it else { // else, if it is not illuminated T5B |= 2; // illuminate the SAVE button char S[] = "key/keytarg .dat"; // file name template S[11] = T5T + 48; // insert target number as a character FILE *F = fopen(S,"w"); // open targets file for reading for(int i = 0; i < 16; i++) // save the currently selected putint(T5V[i],F); // key values for this Target for(int i = 0; i < 5; i++) // save the 5-character SIC code fputc(SI[i],F); for(int i = 0; i < 20; i++) // save the 20-character Selector Code fputc(SC[i],F); for(int i = 0; i < 11; i++) // save the Start Date fputc(D1[i],F); for(int i = 0; i < 11; i++) // save the End Date fputc(D2[i],F); fclose(F); // close the target file } T5buts(); // show the current states of the TARGET Tab's bottom buttons } /* CREATE A LIST OF NAMES & ADDRESSES OF PEOPLE WHOSE KEY SETTINGS MATCH THE CURRENTLY SELECTED TARGET PROFILE. Called from 1 place in MT5() when the LIST button is clicked. */ void T5but0() { if(T5B & 1) // if the LIST button is illuminated T5B &= ~1; // clear it else { // else, if it is not illuminated T5B |= 1; // illuminate the LIST button } T5list(); // generate the target list for the current target profile T5buts(); // show the current states of the TARGET Tab's bottom buttons } /* RETRIEVE AND DISPLAY ALL THE KEY NAMES IN THE TARGET TAB Called from 1 place each in T5sel(), T5selName(), T5show(). */ void T5KNs(int kn) { clearPanel(2); // display Key Names panel background int Q = 0, // start byte of first Key Name v = 88; // vertical starting reference char S[33]; // local scratchpad array for(int i = 0; i < SL; i++) { // for each of the 16 Key Names fseek(FK,Q,SEEK_SET); // start byte number of required key name int N = fgetc(FK); // get its length in bytes if(N < 0 || N > SL) break; // safety net for(int j = 0; j < N; j++) // for each byte of the key name S[j] = fgetc(FK); // store it in the S[] array if(i == kn) // if this key is the selected one XSetForeground(XD,XG,WHITE); // show Key Name in white else // otherwise XSetForeground(XD,XG,YELLOW); // show Key Name in yellow XDrawString(XD,XW,XG,30,v,S,N); // display the key name v += VV; // drop to next display line Q += KL; // advance to start of next field's data } } /* RETRIEVE AND DISPLAY THE SELECTED KEY'S VALUES IN THE TARGET TAB Called from 1 place each in T5sel(), T5selName(), T5selVal(), T5show(). */ void T5KVs(int kn) { // kn:Key Name whose values are required int Q = (kn + 1) << 9, // start byte of selected key's first Value v = 88, // vertical starting reference x = 1; // shiftable test bit char S[33]; // local scratchpad array clearPanel(3); // clear the Key Values panel for(int i = 0; i < SL; i++) { // for each of the 16 Key Values int x = T5V[kn]; // selector bits for values of selected Key fseek(FK,Q,SEEK_SET); // start byte number of key value string int N = fgetc(FK); // get its length in bytes if(N < 0 || N > 24) break; // safety net if(N > 0) // if this key value has has text for(int j = 0; j < N; j++) // for each byte of the key value text S[j] = fgetc(FK); // store it in the S[] array if(x & 1 << i) // if this key value is selected XSetForeground(XD,XG,WHITE); // display it in white else // otherwise XSetForeground(XD,XG,GREY); // display it in grey XDrawString(XD,XW,XG,140,v,S,N); // display the key value v += VV; // drop to next display line Q += KL; // advance to start of next field's data } } // DISPLAY ONE OF THE 4 FIELDS: SIC, SELECTOR CODE, START DAT OR END DATE void T5showField(int f) { PN = f + 11; // NOTE: PN is a global variable. int x,y,l; char *S; switch(f) { case 0: x = 370, y = 215, S = SI; l = 5; break; case 1: x = 327, y = 275, S = SC; l = 20; break; case 2: x = 380, y = 326, S = D1; l = 11; break; case 3: x = 380, y = 343, S = D2; l = 11; break; } clearPanel(PN); XSetForeground(XD,XG,WHITE); XDrawString(XD,XW,XG,x,y,S,l); } void T5editField(int f) { T5F = f; PN = f + 11; // panel number recognized by the line editor LN = 0; // line number within panel recognized by line editor CP = 0; // set the cursor to the beginning of the field setupField(); // set up required field for Line Editor drawBox(); // display yellow box drawCur(); // display green cursor int k; switch(f) { case 0: for(k = 0; k < 5; k++) ED[k] = SI[k]; break; case 1: for(k = 0; k < 20; k++) ED[k] = SC[k]; break; case 2: for(k = 0; k < 11; k++) ED[k] = D1[k]; break; case 3: for(k = 0; k < 11; k++) ED[k] = D2[k]; break; } } void T5saveField(int f) { int k; switch(f) { case 0: for(k = 0; k < 5; k++) SI[k] = ED[k]; break; case 1: for(k = 0; k < 20; k++) SC[k] = ED[k]; break; case 2: for(k = 0; k < 11; k++) D1[k] = ED[k]; break; case 3: for(k = 0; k < 11; k++) D2[k] = ED[k]; break; } T5showField(f); PE = -1; // reset the 'field modified' flag } /* BUTTON LOGIC FOR THE 6 TARGET SELECTOR BUTTONS: Called from 6 places in MT5(). DEC HEX HELP 1 0 0 0 0 0 0 0 128 80 | TARGET 6 0 1 0 0 0 0 0 0 64 40 | | TARGET 5 0 0 1 0 0 0 0 0 32 20 | | | TARGET 4 0 0 0 1 0 0 0 0 16 10 | | | | TARGET 3 0 0 0 0 1 0 0 0 8 8 | | | | | TARGET 2 0 0 0 0 0 1 0 0 4 4 | | | | | | TARGET 1 0 0 0 0 0 0 1 0 2 2 | | | | | | | SAVE 0 0 0 0 0 0 0 1 1 1 | | | | | | | | 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 BIT NUMBERS */ void T5sel(int x) { // x is the number of the clicked button 1 to 5 int y = 2 << x; // y is the selector bit for the clicked button if(T5B & y) { // if the clicked TARGET button is illuminated T5B &= 0; // clear all target buttons + the SAVE button T5T = 0; // set TARGET 0 active } else { // else a dark button was clicked, so T5B &= 0; // clear all target buttons + the SAVE button T5B |= y; // illuminate the clicked button T5T = x; // set the clicked TARGET active } T5N = 0; // select the first key name for this target T5get(T5T); // load the key value settings for this target T5KNs(T5N); // display the key names with selected key's name highlighted T5KVs(T5N); // display the selected key's values for(int i = 0; i < 4; i++) T5showField(i); // display the SIC, Selector Code and Dates fields T5buts(); // show the current states of the TARGET Tab's bottom buttons } /* HANDLE THE SITUATION WHEN A KEY NAME HAS BEEN CLICKED Called from only one place in MT5(). */ void T5selName() { int v = 74; // upper limit of Key Name field for(int i = 0; i < 16; i++) { // for each of the 16 key names if(mY > v && mY < v + 17) { // if mouse clicked on this key name if(i != T5N) { // if this key name is currently selected T5N = i; // select it T5KNs(i); // display this keys names T5KVs(i); // display this keys values } break; // break loop when clicked key name dealt with } v += 17; // advance down to next key name } } /* HANDLE THE SITUATION WHEN ONE OF A KEY'S VALUES HAS BEEN CLICKED Called from only one place in MT5(). */ void T5selVal() { T5B &= ~3; T5buts(); // kill the LIST and SAVE buttons int v = 74, // upper limit of Key Name field x = T5V[T5N]; // selection bits word for current key name for(int i = 0; i < 16; i++) { // for each of the 16 key names if(mY > v && mY < v + 17) { // if mouse clicked on this key name int y = 1 << i; // bit mask for the 'i'th key value if(x & y) // if this key value is currently selected x &= ~y; // the de-select it else // otherwise it is not currently selected x |= y; // so select it T5V[T5N] = x; // save selections word for current key name T5KVs(T5N); // display key values for current key name break; // break loop when chicked value dealt with } v += 17; // advance down to next key name } } /* TARGET TAB CASE FOR THE INITIAL DRAWING OF THE TAB CONTENT Called from only one place in showTab(). */ void T5show() { int v = 70; // vertical coordinate of column titles XSetForeground(XD,XG,AMAREL); // display column titles XDrawString(XD,XW,XG, 30, v, "KEY NAMES", 9); XDrawString(XD,XW,XG, 140, v, "KEY VALUES", 10); T5get(T5T); // get TARGET 0's key values settings T5KNs(T5T); // show the key names with the first one highlighted T5KVs(T5T); // show its values with the selected ones highlighted T5buts(); // show the initial states of the bottom buttons char *S[] = {"Define target profiles as sets", "of key values, which can then ", "be used to filter out lists of", "names and addresses of people ", "whose keys match the profiles."}; v = 70; for(int i = 0; i < 5; i++) { XDrawString(XD,XW,XG,PM+3,v,S[i],30); v += 17; } XSetForeground(XD,XG,AMAREL); v = 180; XDrawString(XD,XW,XG,PM + 34,v,"Standard Industrial",19); v += 15; XDrawString(XD,XW,XG,PM + 49,v,"Classification",14); XSetForeground(XD,XG,AMAREL); v += 60; XDrawString(XD,XW,XG,PM + 22,v,"Arbitrary Selector Code",23); v += 68; XDrawString(XD,XW,XG,PM + 3,v,"Start Date:",10); v += 17; XDrawString(XD,XW,XG,PM + 3,v,"End Date:",8); for(int i = 0; i < 4; i++) T5showField(i); } void MT5() { // HANDLES MOUSE CLICKS WITHIN THE TARGETS TAB if(mY > 73 && mY < 350) { // vertical limits of Keys Area if(mX > 23 && mX < 124) // in Key Names area T5selName(); // handle a click on a Key Name else if(mX > 133 && mX < 284) // in Key Values area T5selVal(); // handle a click on a Key Value else if(mX > 366 && mX < 403 && mY > 202 && mY < 219) T5editField(0); // edit the SIC field else if(mX > 323 && mY > 261 && mX < 450 && mY < 278) T5editField(1); // edit the Selector Code field else if(mX > 376 && mY > 311 && mX < 449 && mY < 328) T5editField(2); // edit the Start Date field else if(mX > 376 && mY > 328 && mX < 449 && mY < 345) T5editField(3); // edit the End Date field return; } if(mY < 360 || mY > 380) // outside vertial limits of the buttons return; if(mX > 23 && mX < 77) T5but0(); // SAVE button was clicked else if(mX > 80 && mX < 134) T5but1(); // LIST button was clicked else if(mX > 137 && mX < 191) T5sel(1); // TARGET 1 button was clicked else if(mX > 194 && mX < 248) T5sel(2); // TARGET 2 button was clicked else if(mX > 251 && mX < 305) T5sel(3); // TARGET 3 button was clicked else if(mX > 308 && mX < 362) T5sel(4); // TARGET 4 button was clicked else if(mX > 365 && mX < 419) T5stat(); // STATIS button was clicked else if(mX > 422 && mX < 476) // HELP button was clicked helpPage("software/mktr.html#targets"); else return; // exit if click was not on an actual button T5buts(); // re-display the buttons } // T6--------------- FUNCTIONS PERTAINING TO THE LETTERS TAB ----------------- /* DISPLAY THE BUTTONS AND ILLUMINATE ACCORDING TO CURRENT SELECTIONS Called from one place each in T6show(), T6logic() and T6edit(). */ void T6buts() { char *PB[4][5] = { // button labels {"TEXT 0","TEXT 1","TEXT 2","TEXT 3","TEXT 4"}, {"LIST 0","LIST 1","LIST 2","LIST 3","LIST 4"}, {"LIST 0","LIST 1","LIST 2","LIST 3","LIST 4"}, {"BMAIL","PMAIL","EMAIL","PHONE","-----"} }; int pb[4] = {9,9,9,11}, // horiz insets of labels from left edges of buttons pl[4] = {6,6,6, 5}, // number of characters in each button label x = 24, // left edge of the first column of buttons y = 80, // top edge of the top button of each column c; // test bit for(int i = 0; i < 4; i++) { // for each column of buttons int b = T6S[i], // logic states for this column of buttons c = 1; // test bit to determine a button state for(int j = 0; j < 5; j++) { // for each button in current column XSetForeground(XD,XG,DARK); // shade button background XFillRectangle(XD,XW,XG,x,y,53,21); if(b & c) // if displaying a selected button XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,x+pb[i],y+15,PB[i][j],pl[i]); y += 25; // drop down to top edge of next lower button c <<= 1; // shift the test bit one place left to test next button } x += BW; // move across to the left edge of the next column of buttons y = 80; // set back to top edge of top button for next column } char *P[8] = {"EDIT","MERGE"," "," "," "," "," ","HELP"}; int p[8] = {14,11,11,14,14,14,14,14}, // number of pixels label inset from left l[8] = { 4, 5, 5, 4, 4, 4, 4, 4}, // number of characters in button label g = 24; // y-coord of left side of first button XSetForeground(XD,XG,DARK); // shade button background for(int i = 0; i < 8; i++) // draw each button XFillRectangle(XD,XW,XG,LM + BW * i,360,53,21); XSetForeground(XD,XG,GREY); // most buttons are grey for(int i = 0; i < 8; i++) { // for each of the 6 control buttons if(T6B & 1 << i) // if displaying a selected button XSetForeground(XD,XG,WHITE); // display in white else // otherwise XSetForeground(XD,XG,GREY); // default in grey XDrawString(XD,XW,XG,g + p[i],375,P[i],l[i]); g += BW; // advance to left side of next button } } // INITIAL CONTENT OF THE LETTERS TAB. Called only once from showTab(). void T6show() { int x = 30, // horizontal start of first column title y = 95, // start height of file names v = 70, // vertical coordinate of column titles t[5] = {3,3,0,3,3}, l[5] = {6,6,7,6,11}; char T[5][12] = {"LETTER","REGION","PROFILE","MEDIUM","OUTPUT FILE"}; XSetForeground(XD,XG,AMAREL); // display column titles for(int i = 0; i < 5; i++) { XDrawString(XD,XW,XG,x+t[i],v,T[i],l[i]); x += BW; y += 25; } const char *S[7] = { "1) Click the LETTER button for the desired letter or message TEXT. ", "2) Click EDIT if you wish to edit this letter. Click again when finished. ", "3) Click LIST No. for REGION to which you wish to restrict the output. ", "4) Click LIST No. for PROFILE to which you wish to restrict the output. ", "5) Click MERGE button if you want LETTER TEXT integrated with the output. ", "6) Click appropriate MEDIUM button for the type of output you require. ", " Generation of the output file will then commence automatically. " }; XSetForeground(XD,XG,GREY); // grey lettering v = 230; for(int i = 0; i < 7; i++) { // display each instruction line XDrawString(XD,XW,XG,LM,v,*(S + i),74); v += 18; } T6buts(); } /* CONVERT BIT POSITION TO BUTTON NUMBER INPUT 'x' OUTPUT 'y' BUTTON NUMBER FROM LEFT TO RIGHT 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 1 Called from 3 places in T6msg(), 0 0 0 0 0 1 0 0 2 7 places in T6gen(), 0 0 0 0 1 0 0 0 3 2 places in T6logic() 0 0 0 1 0 0 0 0 4 and one place in T6edit(). 0 0 1 0 0 0 0 0 5 0 1 0 0 0 0 0 0 6 1 0 0 0 0 0 0 0 7 */ int T6bitpos(int x) { int y = -1; if(x & 1) y = 0; if(x & 2) y = 1; if(x & 4) y = 2; if(x & 8) y = 3; if(x & 16) y = 4; return y; } /* DISPLAY THE OUTPUT PROGRESS MESSAGE OR NAME OF OUTPUT FILE Called from 2 places in T6logic(). */ void T6msg(int p) { // 1 = output generation in progress; 2 = finished XSetForeground(XD,XG,BLACK); // clear the OUTPUT FILE message area XFillRectangle(XD,XW,XG,252,80,224,121); if(p == 0) return; // just clear the message area char S[4][12] = {"bmailNN.txt","pmailNN.txt","emailNN.txt","phoneNN.txt"}; int z = T6bitpos(T6S[3]); // get button number of selected output medium if(z == -1) return; // exit if no output medium selected int x = T6bitpos(T6S[1]); // number of selected geographic target list if(x != -1) // provided a REGION target is selected S[z][5] = x + 48; // set its number into the output file name int y = T6bitpos(T6S[2]); // number of selected key values target list if(y != -1) // provided a PROFILE target is selected S[z][6] = y + 48; // set its number into the output file name int v = 94 + z * 25; // vertical base line for the message text if(p == 1) { XSetForeground(XD,XG,AMAREL); XDrawString(XD,XW,XG,252,v,"Generating the output file...",29); } else { XSetForeground(XD,XG,GREEN); XDrawString(XD,XW,XG,260,v,S[z],11); XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,335,v,"Click to view.",14); } } /* GET THE nn_th FIELD OF THE r_th NAME AND ADDRESS RECORD Called from one place in putNAD() and 3 places in T6out(). */ int getNAD(int r, int n) { // seek the start byte of the n_th field fseek(FR,(r << rs) + (n << 5),SEEK_SET); int L = fgetc(FR),i; // length of the field's content if(L < 1 || L > 30) // if it's a blank line or it's longer than 30 return -1; // characters, exit with error code for(i = 0; i < L; i++) // for each character in the field SF[i] = fgetc(FR); // put it in the j_th position of the SF[] array SF[i] = (char)0; // add string-terminating null character return L; // return length of field content } /* COUNT THE NUMBER OF LINES OF TEXT IN THE SELECTED LETTER TEXT FILE This is done only once when one of the MEDIUM buttons is pressed. The line count is placed in the global variable T6N from where it is read for each letter that is printed for a given mailshot list. Called from only one place in T6gen(). */ void T6scan(FILE *L) { T6N = 0; // global counter for number of lines of text in letter int c, // current character retrieved from the letter file d = 0; // to remember it for the next pass of the following loop while((c = fgetc(L)) != EOF) { // provided next char not an 'end-of-file' if(c == '\n') // if it is a 'new-line' character ++T6N; // increment number of lines found d = c; // remember this character for next pass } if(d > 0 && // provided at least 1 non 'EOF' character was found d != '\n') // and it is not a 'new-line' character ++T6N; // increment the number of lines for a final line } // that was not terminated by a carriage-return /* GET AND FORMAT THE CURRENT LETTER DATE. The function ctime() returns the date and time in the format 'Fri Mar 11 10:18:44 2022'. This function picks the bits needed to form our letter date in format '11 Mar 2022'. Called once from T6gen(). */ void T6LD() { time_t now; // `time_t` is an arithmetic time type time(&now); // get current time char *S = ctime(&now); // pointer to start of standard 'date' string T6L[ 0] = *(S + 8); // day number digit #1 T6L[ 1] = *(S + 9); // day number digit #2 T6L[ 2] = 32; // space character T6L[ 3] = *(S + 4); // abbreviated month name letter #1 T6L[ 4] = *(S + 5); // abbreviated month name letter #2 T6L[ 5] = *(S + 6); // abbreviated month name letter #3 T6L[ 6] = 32; // space character T6L[ 7] = *(S + 20); // year number digit #1 T6L[ 8] = *(S + 21); // year number digit #2 T6L[ 9] = *(S + 22); // year number digit #3 T6L[10] = *(S + 23); // year number digit #4 T6L[11] = 0; // terminating NULL character } /* INITIAL SALUTATION FOR THE LETTER: 'Dear Fred' or Dear 'Mr Bloggs'. We must look to see if there is a familiar name between () brackets within the Contact Name field, which is in the global character array SF[]. The Contact name has the form 'Mr F Bloggs (Fred)', where (Fred) indicates that in a letter, the person is addressed 'Dear Fred'. If a bracketed familiar name is absent, the person is addressed 'Dear Mr Bloggs'. Called only once from T6bmail(). */ char * T6sal(int L) { static char S[40] = "Dear Sir"; // array to contain the salutation string int c, // for current character picked from cotact name i = 0, // index number of character within contact name j = 4; /* index num of char in salutation string Note that chars 0 to 4 'Dear ' are already in XE[]. */ if(L == 0) return S; // return "Dear Sir" if no contact name for(i = 0; i < L; i++) { // for each character in the Contact Name field c = *(SF + i); // get [next] character from Contact Name array if(j == 4) { // if we've not yet detected an opening bracket if(c == '(') // if current character is an opening bracket j = 5; // set to accepting salutation sharacters } else { // else we have already detected an opening bracket if(c == ')') { // if current character is a closing bracket *(S + j) = 0; // ternimate the salutation string with a NULL break; // and exit the while() loop } else // else we are collecting characters of the familiar *(S + j++) = c; // salutation, so add the character to the salutation } } /* if no brackets () were found in the contact name field, we need to extract the 'Mr' or 'Ms' and then the surname. */ if(c != ')') { // if no closing bracket, there was no familiar salutation /* Starting at the very beginning of the Contact Name field, get the person's title: Mr, Ms, Mrs etc.. */ i = 0; // index num of char in Contact Name array j = 5; // points to first char of person's title while((c = *(SF + j)) != ' ') // while [next] character isn't a space *(S + j++) = c; // set it in the salutation array *(S + j++) = ' '; // space char following the person's title /* Starting from the end of the Contact Name field [i.e. at the end of the contact's surname, step backwards to the 'space' before the beginning of the surname. */ int b = L - 1; /* 'b' starts off as being the index number of the last character of the recipient's surname. */ while((c = *(S + b)) != ' ') { if(b < 0) break; // the zero test is just a safety net --b; // 'b' becomes the index number of the first } // letter of the recipient's surname. /* Working forwards from the first letter of the surname 'I', copy each letter of the surname into the salutation array 'XE'. NOTE: 'j' is now the index number of the first character of the surname within the salutation array 'XE'. */ for(i = b; i < L; i++) *(S + j++) = *(SF + i); *(S + j) = 0; // insert the terminating NULL character } return S; } /* SELECT THE APPROPRIATE FINAL SALUTATION FOR THE LETTER ACCORDING TO THE HASH CODE IN THE CONTACT NAME FIELD OF THE RECIPIENT. Called only once from T6bmail(). */ char * T6sig() { static char *S = "Yours sincerely"; char c = 0; // to hold the current character of the Contact Name field int i = 0; // index number of current character of the Contact Name field while((c = *(SF + i)) != 0) // while we've not yet reached the { // end of the Contact Name field: ++i; // increment index number to point to next character if(c == '#') { // if the current character is a '#' c = *(SF + i); // get the next character, which is the salutation code break; // break out of the while() loop } } if(c == 'F') S = "Yours faithfully"; else if(c == 'T') S = "Yours truly"; else if(c == 'R') S = "Regards"; else if(c == 'B') S = "Best regards"; else if(c == 'K') S = "With kind thoughts"; return S; } /* PRINT THE LETTER TEXT TO THE APPROPRIATE OUTPUT FILE Called from 1 place in T6out(). */ void T6bmail(FILE *L, FILE *H, int r) { int c = 0, i, // number of valid lines acquired X[8] = {11,12,0,2,3,4,5,6}; // numbers of the required fields fprintf(H,"\n%s\n\n",T6L); // print letter date and skip a line for(i = 0; i < 8; i++) { // for each of the 8 required fields if(getNAD(r,X[i]) != -1) { // if the field is valid, then if(c > 0) // if a valid field already been printed fprintf(H,"\n"); // go to a new line fprintf(H,"%s",SF); // print the address line to the file c++; // increment num of valid fields printed } } for(i = c; i < 8; i++) // if less than 8 address lines printed fprintf(H,"\n"); // make up short-fall with blank lines fprintf(H,"\n\n"); // terminate block with a blank line if(!(T6B & 2)) { // if MERGE button is dim fprintf(H,"\n\n"); // terminate block with a blank line return; // and quit without printing the letter text } /* CENTRE THE TEXT OF THE LETTER VERTICALLY ON THE PAGE. A4 page has 66 lines of standard 12-point type. The date, address and the 'Dear ...' take the first 14 lines. So we are now 33 - 14 = 19 lines above the half-way point down the page. So the letter text may contain up to 38 lines of print. So, provided the letter text contains less than 38 lines, compute the number of extra lines to skip before start of text then print that many 'new-line' charaters to the output file. */ if(T6N < 38) { int I = (38 - T6N) >> 1; for(int i = 0; i < I; i++) fputc('\n',H); } else T6N = 38; /* getNAD(r,11) puts the content of the 11th field [the Contact Name field] of the 'r'th name & address record into the global character array SF[] and return the length of its content. T6sal() then returns the appropriate sal- utation 'Dear Fred' or 'Dear Mr Bloggs' from which it is then printed. */ fprintf(H,"\n%s\n\n",T6sal(getNAD(r,11))); /* The file has already been read to find out how many lines it contained so it is necessary to rewind it now. Besides, it needs rewinding after printing each copy of the letter text. */ rewind(L); // return to start of letter file if necessary int d = 0; // to note the previous character /* While we've not yet reached the end of the Letter File, copy the current character to the output file and save it in 'd' for next time. */ while((c = fgetc(L)) != EOF) { fputc(c,H); d = c; } /* If final character of Letter File, before the end-of-file mark, was not a 'new-line' character, then write a final 'new-line' to the Output File. */ if(d != '\n') fputc('\n',H); /* Print the signature block. T6sig() returns 'Yours ...'. After the first call to getNAD(), SF first contains the signatory's name taken from Record 1's Contact Name. After the second call to getNAD() SF contains the sinatory's job title. */ fprintf(H,"\n%s\n\n\n\n\n\n",T6sig()); // salutation getNAD(1,11); fprintf(H,"%s\n",SF); // signatory name getNAD(1,12); fprintf(H,"%s\n\f",SF); // job title } /* PRINT A CSV [COMMA-SEPARATED VALUES] LIST OF THE NAMES AND ADDRESSES OF EVERYBODY ON THE OUTPUT LIST. Called from only one place in T6out().*/ void T6pmail(FILE *L, FILE *H, int r) { int n = 0, // number of valid lines acquired i, j, J, // loop index variables and limit X[8] = {11,12,0,2,3,4,5,6}; // numbers of the required fields for(i = 0; i < 8; i++) { // for each of the 8 required fields if((J = getNAD(r,X[i])) != -1) { // if the field is valid, then if(n > 0) // if a valid field already been printed fprintf(H,","); // print the separating comma if(i == 0) { // if this is the Contact Name line for(j = 0; j < J; j++) { // for each char in the Contact Name int c = SF[j]; // get character from the field string if(c == '(' || // if it is start of the Contact's c == '#') { // familiar name or signatory code if(SF[j - 1] == ' ') // then if previous character was a j--; // space character, then back-up to it SF[j] = 0; // terminate the string at this point break; // and break out of the for() loop } } } fprintf(H,"\"%s\"",SF); // print the address line to the file n++; // increment num of valid fields printed } } fprintf(H,"\n"); // terminate the csv line or block with a blank line } /* PRINT THE EMAIL ADDRESS LIST FOR IMPORT AS CSV FILE TO MOZILLA FIREFOX. CSV line format: "First Name","Last Name","Ref Num & Name","","Primary Email" Called from 1 place in T6out(). */ void T6email(FILE *L, FILE *H, int r) { /* Start by printing two quoted null fields because we don't need a first name or a last name. Then print the 6-digit Marketeer reference number fol- lowed by (familiar name) if it exists, otherwise the formal Contact Name. */ fprintf(H,"\"\",\"\",\"%06i %s\",\"\",",r,T6sal(getNAD(r,11)) + 5); getNAD(r,9); // get the contact's email address fprintf(H,"\"%s\"\n",SF); // and print it in quotes; finish with 'new-line' } /* CONSTRUCT & STORE ONE LINE OR ADDRESS BLOCK OF THE APPROPRIATE OUTPUT FILE L is file handle for the letter file H is the file handle for the output file z is the number 0 to 4 of the MEDIUM button r is the record number of the name & address record Called from 3 places in T6gen(). */ void T6out(FILE *L, FILE *H, int z, int r) { switch(z) { // switch on MEDIUM button number case 0: // PHYSICAL MAIL BLOCK ADDRESS LIST T6bmail(L,H,r); // print name & address block break; // print letter text only if MERGE button is bright case 1: // PHYSICAL MAIL CSV LIST T6pmail(L,H,r); // print the 'CSV' list of addressees break; // the r_th name & address record case 2: // ELECTRONIC MAIL T6email(L,H,r); // print email heading information break; // print email text if MERGE button is bright case 3: // TELEPHONE LIST getNAD(r,7); // get telephone number fprintf(H,"%s\n",SF); // save it as a single item on the line } } /* MERGE REGION LIST AND TARGET LIST AND GENERATE APPROPRIATE OUTPUT FILE. If a name & address record appears both in the selected region and the selected target list, create an entry in the output file. */ void T6gen() { int z = T6bitpos(T6S[3]); // get button number of selected output medium if(z < 0 && z > 3) return; // no button pressed: no medium selected // IF ONE OF THE GEOGRAPHIC TARGETS IS SELECTED, OPEN ITS LIST FILE int x = T6bitpos(T6S[1]); // number of selected geographic target list char S[16] = "geo/geolst .dat"; // set up Region target file name template; FILE *F; // file handle for geographic target list file int f = 0; // first assume no geographic target selected if(x != -1) { // if a geographic target is selected S[10] = x + 48; // put geo target list number in file name F = fopen(S,"r"); // open this file for reading f = 1; // indicate geographic target selected } // IF ONE OF THE KEY VALUES TARGETS IS SELECTED, OPEN ITS LIST FILE int y = T6bitpos(T6S[2]); // number of selected key values target list char T[16] = "key/keylst .dat"; // set up Region target file name template; FILE *G; // file handle for keyvals target list file int g = 0; // first assume no keyvals target selected if(y != -1) { // if a keyvals target is selected T[10] = y + 48; // put key target list number in file name G = fopen(T,"r"); // open this file for reading g = 1; // indicate that a key vals target is selected } // EXIT IF NEITHER TARGET TYPE HAS A BUTTON SELECTED if(f == 0 && g == 0) return; // Note neither file was opened // OPEN THE LETTER TEXT SOURCE FILE IF APPROPRIATE FILE *L; // null handle for LETTER text file int w = T6bitpos(T6S[0]); // number of selected LETTER text int LFO = 0; // letter file not open if((T6B & 2) // if the MERGE button is on && w != -1 // && a valid letter text is selected && (z == 0 || z == 2)) { // && doing block-addressed mail or email shot T6LD(); // get and format the current letter date T6V[14] = w + 48; // insert the LETTER No. digit L = fopen(T6V,"r+"); // open the appropriate letter file LFO = 1; // letter file open T6scan(L); // count number of lines of text in letter } // FORM THE FILE NAME OF THE OUTPUT CSV FILE, THEN OPEN IT FOR WRITING char U[20] = "targets/ mail .txt"; // template for output medium csv file if(f) U[13] = x + 48; // if a geo file was selected, insert its else U[13] = 'N'; // number in the file name, else insert an 'N' if(g) U[14] = y + 48; // if a key file was selected, insert its else U[14] = 'N'; // number in the file name, else insert an 'N' switch(z) { // construct file name according to selected medium case 0: U[8] = 'b'; break; // 'b' for bmail [block addressed mail] case 1: U[8] = 'p'; break; // 'p' for pmail [csv addressed mail] case 2: U[8] = 'e'; break; // 'e' for email case 3: U[8] = 'p'; U[9] = 'h'; U[10] = 'o'; U[11] = 'n'; U[12] = 'e'; } FILE *H = fopen(U,"w"); // open the medium output file // GENERATE THE HEADING BLOCK FOR THE OUTPUT FILE int l = T6bitpos(T6S[3]), // button number of selected MEDIUM button m = T6bitpos(T6S[1]), // button number of selected REGION button n = T6bitpos(T6S[2]); // button number of selected PROFILE button time_t now; // `time_t` is an arithmetic time type time(&now); // get current time switch(z) { // print the appropriate heading for the output file case 0: fprintf(H,"# MAILSHOT LIST GEO %i KEY %i ON %s",m,n,ctime(&now)); break; case 1: fprintf(H,"# MAILING CSV LIST GEO %i KEY %i ON %s",m,n,ctime(&now)); break; case 2: fprintf(H,"# EMAIL CSV LIST GEO %i KEY %i ON %s",m,n,ctime(&now)); break; case 3: fprintf(H,"# PHONE LIST GEO %i KEY %i ON %s",m,n,ctime(&now)); } /* GENERATE THE APPROPRIATE OUTPUT FILE. note that record '0' in both files contains the number of valid records the file contains. So the records con- taining the reference numbers of the members of each Target List are num- bered numbered from '1', not '0'. */ int I,J,i,j; if(f == 1 && g == 0) { // IF ONLY A GEOGRAPHIC TARGET IS SELECTED I = getInt(F); // total number of records in geo '.dat' file for(i = 1; i < I; i++) // for each record in the 'geo' file T6out(L,H,z,getInt(F)); // create an entry in the output fclose(F); // close the geo target list file } else if(g == 1 && f == 0) { // IF ONLY A KEY VALUES TARGET IS SELECTED J = getInt(G); // total number of records in key '.dat' file for(j = 1; j < J; j++) // for each record in the 'key' file T6out(L,H,z,getInt(G)); // create an entry in the output fclose(G); // close the key values target list file } else { // A GEO AND A KEY VALUES TARGET WAS SELECTED I = getInt(F); // total number of records in geo '.dat' file J = getInt(G); // total number of records in key '.dat' file for(i = 1; i < I; i++) { // for each record in the 'geo' file x = getInt(F); // get its next entry fseek(G,4,SEEK_SET); // start of record #1 of key values target list for(j = 1; j < J; j++) // for each record in the 'key' file if(x == getInt(G)) // if same record number appears in both lists T6out(L,H,z,x); // write it to the output file } fclose(F); fclose(G); // close both files } fclose(H); // close the output file if(LFO) fclose(L); // close letter file if it was open } /* BUTTON LOGIC FIR THE 4 COLUMNS OF 5 BUTTONS EACH. Each row comprises a set of 5 logically related buttons. When a dim button is clicked, it brightens and any other bright button is automatically dim- med. If a bright button is clicked, it simply dims. Thus, in any column, only one or none of the buttons can be bright at any given time. That is, one item or no item can be selected at any given time. There's no logical relationship between buttons in different columns. Called only from one place in MT6(); */ void T6logic(int c, int r) { // column number, row number int x = T6S[c], // bits states for buttons in column 'c' y = 1 << r; // test bit for the button in row 'r' if(x & y) // if the bit is set [button is bright] x &= ~y; // unset the bit [dim the button] else { // otherwise x &= 0; // clear all buttons x |= y; // brighten the button } T6S[c] = x; // save the changed bit pattern if(c == 0) // if a button in the LETTERS column was clicked T6B &= ~1; // dim the EDIT button T6buts(); // re-display the buttons if(c == 3 && // if one of the MEDIUM buttons was pressed r != 4) { // and it is not the 4th [unused] button T6msg(1); // display the "progress" message T6gen(); // generate the output file T6msg(2); // display the "finished" message } if(c == 4) { // if an OUTPUT message name was clicked char S[28] = "gedit targets/bmailNN.txt &"; // command line template for(int i = 0; i < 5; i++) // put medium into line command S[i+14] = T6O[r][i]; // e.g. bmail, pmail, email, phone if((x = T6bitpos(T6S[1])) != -1) // if a geographic target was used S[19] = x + 48; // insert its number into the file name if((x = T6bitpos(T6S[2])) != -1) // if a key values target was used S[20] = x + 48; // insert its number into the file name system(S); // invoke gedit to display output file } } /* OPEN TEXT EDITOR TO EDIT ONE OF THE 5 LETTER TEXT FILES Called only from one place in MT6(). */ void T6edit() { int y = T6bitpos(T6S[0]); // number of LETTER that is currently selected if(y == -1) return; // exit if no LETTER is currently selected if(T6B & 1) // if the EDIT button is currently bright T6B &= ~ 1; // then dim it else { // else the EDIT button must be currently dim T6B |= 1; // so brighten the EDIT button char S[28] = "gedit letters/Letter .txt &"; S[20] = 48 + y; // insert LETTER number in name of letter file system(S); // display it in the 'gedit' text editor } T6buts(); // display the current states of the bottom row of buttons } /* PROCESS THE MERGE BUTTON. If the MERGE button is bright, the text of the selected LETTER is merged with each name and address or email address in the output file. If the MERGE button is dim, the output file will contain the list of names & addresses or email addresses only. The state of the MERGE button is tested inside the T6gen() function. Called only from one place in MT6(). */ void T6merge() { if(T6B & 2) // if the MERGE button is currently bright T6B &= ~2; // then dim it else T6B |= 2; // brighten the MERGE button T6buts(); } // HANDLES MOUSE CLICKS WITHIN TAB 6: THE LETTERS TAB void MT6() { // SELECTOR BUTTONS FOR THE LETTERS TAB int c = -1; // column number if(mX > 23 && mX < 77) c = 0; // LETTER buttons if(mX > 80 && mX < 134) c = 1; // REGION butons if(mX > 137 && mX < 191) c = 2; // TARGET buttons if(mX > 194 && mX < 248) c = 3; // MEDIUM buttons if(mX > 251 && mX < 476) c = 4; // output filename message int r = -1; // row number if(mY > 79 && mY < 101) r = 0; if(mY > 104 && mY < 126) r = 1; if(mY > 129 && mY < 151) r = 2; if(mY > 154 && mY < 176) r = 3; if(mY > 179 && mY < 201) r = 4; if(c >= 0 && c < 5 && r >= 0 && r < 5) T6logic(c,r); else if(mX > 23 && mX < 77) T6edit(); // EDIT button was clicked else if(mX > 80 && mX < 134) T6merge(); // MERGE button was clicked /*else if(mX > 137 && mX < 191) T6(1); // PRINT button was clicked else if(mX > 194 && mX < 248) T6(2); // SEND button was clicked else if(mX > 251 && mX < 305) T6(3); // CALL button was clicked else if(mX > 308 && mX < 362) T6(4); // NEXT button was clicked else if(mX > 365 && mX < 419) T6(); // PREV button was clicked*/ else if(mX > 422 && mX < 476) // HELP button was clicked helpPage("software/mktr.html#letters"); else return; // exit if click was not on an actual button } // T7------------- FUNCTIONS PERTAINING TO THE MAINTENANCE TAB ---------------- /* MAINTENANCE TAB CASE FOR THE INITIAL DRAWING OF THE TAB CONTENT Called from only one place in showTab(). */ void T7show() { const char *S[10] = { "INTEGRITY Test if all indexes are in alphanumeric order. ", " ERROR Open the Integrity Errors file in text editor. ", " REBUILD Invoke the re-building of all three indexes. ", " TO TEXT Create readable text versions of the 3 indexes.", "SRCH CODE Open the Search Code index in the text editor. ", "POST CODE Open the Post Code index in the text editor. ", "PHONE NUM Open the Phone Number index in the text editor.", " DELETED Open the 'Deleteds' file in the text editor. ", " RECORDS Create and open 'Records' file in text editor. ", " HELP Display the web-based HELP for the MAINT Tab. " }; XSetForeground(XD,XG,DARK); // shade button background int v = 55; for(int i = 0; i < 10; i++) { // draw each button XFillRectangle(XD,XW,XG,24,v,60,15); v += 20; } XSetForeground(XD,XG,GREY); // buttons have grey lettering v = 67; for(int i = 0; i < 10; i++) { // annotate each button XDrawString(XD,XW,XG,27,v,*(S + i),59); v += 20; } } /* STORE INDEX ELEMENT TEXT 'S' & RECORD NO. 'R' IN ELEMENT 'E' OF INDEX 'FX'. Called twice from the Hoare Sort and also 3 times from the indexes re- building program iRebuild() [case 7 of the MAINTenance tab]. */ void putEntry(char *S, int R, int E) { int Q = E << xs; // record number times index record size fseek(FX,Q,SEEK_SET); // set pointer to start of record 'E' for(int k = 0; k < SL; k++) // Put each of the SL characters of the text fputc(S[k],FX); // to the appropriate entry in the index file fseek(FX,Q + RO,SEEK_SET); // set pointer to recnum in record 'E' putInt(R,FX); // then put the record number after it } /* READ THE INDEX ELEMENT 'E' OF INDEX 'FX' INTO THE GIVEN ARRAY Called only from 2 places in Hoare Quick Sort hqs(). */ void getText(char *S, int E) { fseek(FX,E << xs,SEEK_SET); // set pointer to start of record 'E' for(int k = 0; k < SL; k++) // Put each of the SL characters of the text S[k] = fgetc(FX); // part of index entry into output array } // don't need a NULL terminator /* READ THE INDEX ELEMENT TEXT 'E' OF INDEX 'FX' Called only from 2 places in Hoare Quick Sort hqs(). */ char *getEnT(int E) { static char S[29]; // output array SL chard + NULL char fseek(FX,E << xs,SEEK_SET); // set pointer to start of record 'E' for(int k = 0; k < SL; k++) // Put each of the SL characters of the text S[k] = fgetc(FX); // part of index entry into output array S[SL] = (char)0; // & terminate string with a NULL character return S; // return pointer to output array S[] } /* This function embodies C A R Hoare's Quick Sort algorithm. Note that it is a highly re-entrant function: it calls itself indefinitely. This version works directly with the index files rather than loading the indexes into gigantic arrays in order to sort them. StrCmp() as used in this function, is not the strcmp() from but my own version designed to accom- modate the specific requirements and limits of this program. It is defined towards the beginning of this listing. Explanation of the expression (LO + HI >> 1) << xs, which determines the start byte of the mid element of the current sort range within the index file. The parentheses as shown are the minimum necessary and sufficient for correct processing under the operator precedences of the C programming language: '+' takes precedence over '<<' which takes precedence over' >>'. ( LO + HI The sum of the high element and the low element numbers, >> 1 ) divide the above by 2 by right-shifting the integer 1 bit, << xs shift-multiply all by the number of bytes per index entry. */ void hqs(int LO, int HI) { // called twice by itself & iRebuild() int lo = LO; // set moving lo to LO end of partition int hi = HI; // set moving hi to HI end of partition char S[33]; // to contain entry currently being compared if(HI > LO) { // if the partition contains anything fseek(FX,(LO + HI >> 1) << xs,SEEK_SET); // see header text above for(int k = 0; k < SL; k++) // get content of partition's mid element S[k] = fgetc(FX); // text part only [excludes record number] S[SL] = (char)0; // terminating NULL for call to StrCmp() while(lo <= hi) { // loop through index until indices cross while(lo < HI && StrCmp(getEnT(lo),S) < 0) lo++; // StrCmp() is while(hi > LO && StrCmp(getEnT(hi),S) > 0) hi--; // defined above if(lo <= hi) { //IF LOW INDEX <= HIGH INDEX SWAP THEIR 'CONTENTS' char loT[SL]; // SL-char Lo string getText(loT, lo); // get the low element's text ix = lo; int loR = getRecNum(); // get low element's record number char hiT[SL]; // SL-char Hi string getText(hiT, hi); // get the high element's text ix = hi; int hiR = getRecNum(); // get high element's record number putEntry(hiT, hiR, lo); // put them in the low element putEntry(loT, loR, hi); // put orig low element in high element lo++; // push lower sort boundary up by one element hi--; // pull upper sort boundary down by one element } } if(LO < hi) hqs(LO, hi); // re-enter with the lowered 'hi' if(lo < HI) hqs(lo, HI); // re-enter with the raised 'lo' } } /* MAINTENANCE TAB: INDEX INTEGRITY CHECK: Verifies whether or not all the entries in each of the 3 index files are in strict alphabetical order; or, more strictly, in order of ascending ASCII number. Writes any errors it encounters to the file 'idxerr.txt'. Called from only one place in MEH() case 7: */ void iInteg() { char *I[] = { "Namecode", "Postcode", "Phone No" }, S[29], T[29]; // scratch arrays FILE *F7 = fopen("reports/idxerr.txt","w"); // open errors file for(int i = 0; i < 3; i++) { // for each of the 3 indexes: int Flag = 0; // clear the error detection flag FX = *(FI + i); // pointer for current index file fseek(FX,0,SEEK_SET); // Set to start of index file to get the int IX = getInt(FX); // total number of entries in this index. for(int j = 0; j < SL; j++) // Intitialise the previous entry S[j] = (char)0; // array by filling it with nulls. int s = XL; // start byte of current index entry for(int j = 1; j <= IX; j++) { // for each entry in the index fseek(FX,s,SEEK_SET); // go to first byte of current index entry for(int k = 0; k < SL; k++) // for each character in current entry T[k] = fgetc(FX); // put the entry text into T[] if(StrCmp(T,S) < 0) { // if entries not in alphabetical order fprintf(F7, "%s Entry %i \"%s\" out of order.\n", I[i], j, T); Flag = 1; // print details to error file } // and set 'error detected' flag for(int k = 0; k < SL; k++) // put the current entry S[k] = T[k]; // in the previous entry s += XL; // increment to start byte of next entry } // end of for(j) if(Flag == 0) { fprintf(F7, "%ss Index: %i entries OK.\n",I[i], IX); XSetForeground(XD,XG,GREEN); XDrawString(XD,XW,XG,390,67,"OK",2); } else { XSetForeground(XD,XG, RED); XDrawString(XD,XW,XG,390,67,"ERRORS",6); } XSetForeground(XD,XG,BLACK); // wipe any other 'DONE' messages XFillRectangle(XD,XW,XG,390,75,25,60); } // end of for(i) fclose(F7); // close the errors file } /* MAINTENANCE TAB: AUTOMATICALLY RE-BUILD ALL THREE INDEXES: Skips over deleted records so that they are not included in the re-built indexes. Called from only one place in MEH() case 7: */ void iRebuild() { char S[33], T[33]; // working arrays to hold an index entry IX = 0; DL = 0; // start with a clear index file fseek(FR,0,SEEK_SET); // go to start of records file int RD = getInt(FR); // get the total number of records for(int i = 0; i < 3; i++) { FX = *(FI + i); // Store the total number of records fseek(FX,0,SEEK_SET); // as a 4-byte integer in record zero putInt(RD,FX); // of curent index file. } int R = RS, // start byte of record number one I = XL, // start byte of index entry number one D = 0; // start byte of the deleteds file for(int i = 1; i <= RD; i++) { // for each record in the records file: fseek(FR,R,SEEK_SET); // go to start of [next] record int x = fgetc(FR); // If the first bit of the record is set, if((x & 0x80) > 0) { // the record is deleted, so DL++; // increment the number of deleted records D += 4; // start byte of next entry in deleteds file fseek(FD,D,SEEK_SET); // save its record number in the putInt(i, FD); // 'deleteds' file and skip it. R += RS; // advance to start byte of next record continue; // and skip this deleted record } for(int j = 0; j < 3; j++) { // for each of the 3 indexed fields /* Set the file pointer to the start byte [within the name & address record] of the particular indexed field being dealt with on this pass of the for(j) loop. This is the start byte of the current record [num- ber 'R'] plus the field number XF[j] of the indexed field within the record, multiplied by the length of a standard field of a name & address record. The multiply is effected by a left shift of 'xs' bits. 'xs' is the length of a name & address field expressed as a power of 2. */ fseek(FR, R + (XF[j] << xs), SEEK_SET); int L = fgetc(FR), // get its content length z = 0, c, k; for(k = 0; k < L; k++) { // for each char in the indexed field c = fgetc(FR); // get next character from records file if(j == 2) { // if doing the phonenum field if(c >= '0' && c <= '9' // provided it is a number 0 to 9 || c == '+') // or a '+' sign T[z++] = c; // put chararacter in the temporary array } else S[z++] = c; // otherwise put it in the normal array } if(j == 2) // if we're doing the phone index for(k = 0; k < z; k++) // Reverse the order of the S[z - k - 1] = T[k]; // characters of the phone number. for(k = z; k < SL; k++) // Pad out the index entry S[k] = (char)0; // with trailing NULL characters. FX = *(FI + j); // get the appropriate index file pointer fseek(FX,I,SEEK_SET); // go to start of index record[i] putEntry(S,i,i); // copy index content to index record } IX++; // increment number of index entries I += XL; // advance to start byte of next index entry R += RS; // advance to start byte of next record } // end of for(i) // store the value of IX in each index file's Record Zero then flush file: fseek(FN,0,SEEK_SET); putInt(IX,FN); fflush(FN); fseek(FP,0,SEEK_SET); putInt(IX,FP); fflush(FP); fseek(FC,0,SEEK_SET); putInt(IX,FC); fflush(FC); fseek(FD,0,SEEK_SET); putInt(DL,FD); fflush(FD); for(int i = 0; i < 3; i++) { // for each of the three index files FX = *(FI + i); // set file pointer for appropriate index hqs(1,RD); // sort all index entries into casting order fflush(FX); // flush each index file in case power fails } XSetForeground(XD,XG,BLACK); // wipe all 'DONE' messages XFillRectangle(XD,XW,XG,390,55,25,80); XSetForeground(XD,XG,GREEN); // display the 'DONE' message in green XDrawString(XD,XW,XG,390,107,"DONE",4); } /* MAKE A TEXT FILE VERSIONS OF THE INDEX FILES: Provides a means of visually checking the content of an index file. Useful for diagnosing why in the case of an index becoming corrupted due to such things as memory corrup- tion or disk malfunction. Called only from one place in MEH() case 7: */ void ixtotxt() { FILE *F7; // file handle for(int i = 0; i < 3; i++) { // for each of the three indexes: switch(i) { case 0: F7 = fopen("reports/namecode.txt","wb"); break; case 1: F7 = fopen("reports/postcode.txt","wb"); break; case 2: F7 = fopen("reports/phonenum.txt","wb"); } FX = *(FI + i); // pointer for current index file fseek(FX,0,SEEK_SET); // set to start of index file int IX = getInt(FX); // get number of entries in the file fprintf(F7,"%s\n",showInt(IX,6)); int m = XL; // start byte of first record for(int j = 1; j <= IX; j++) { // for each record in records file: fseek(FX,m,SEEK_SET); // go to start of this index record for(int k = 0; k < SL; k++) { // for each char in current field char c = fgetc(FX); // get character from index file if(c == (char)0) // replace null characters with spaces c = ' '; fputc(c,F7); // put character to index file } fseek(FX,m + RO,SEEK_SET); // go to start of this index's record num // store record number with leading zeros fprintf(F7,"%s\n",showInt(getInt(FX),6)); m += XL; // move to start byte of next record } fclose(F7); // close index text file } F7 = fopen("reports/deleteds.txt", "w"); fseek(FD,0,SEEK_SET); // set to start of deleteds file DL = getInt(FD); // get number of entries in the file fprintf(F7, "Deleteds: %i\n", DL); // store number of deleteds for(int j = 1; j <= DL; j++) { // for each record in records file: fseek(FD,j << 2,SEEK_SET); // go to start of this deleteds record fprintf(F7, " %i\n", getInt(FD)); // put rec num in index text entry } fclose(F7); // close index text file XSetForeground(XD,XG,GREEN); // display green 'DONE' message XDrawString(XD,XW,XG,390,127,"DONE",4); } /* EXPORT ALL RECORDS TO A TEXT FILE 'records.txt': Produces a human-readable listing of all the names and addresses in the records file. This can be imported as source information to other programs by writing a simple con- vertion program. Called from only one place in MEH() case 7: */ void expRecs() { FILE *F7 = fopen("reports/records.txt", "wb"); fseek(FR,0,SEEK_SET); // seek first byte of records file int N = getInt(FR); // get the total number of records fprintf(F7, "Number of Records: %i\n\n", N++); fprintf(F7, "----------------------------------------------------\n"); int R = RS; // start byte of record number one for(int i = 1; i < N; i++) { // for each record in the records file int Q = 0; for(int j = 0; j < 14; j++) { // for each field except dates fields fseek(FR,R + Q,SEEK_SET); // seek first byte of the Nth record int L = fgetc(FR); // get length of field for(int k = 0; k < L; k++) // for each character in the field fputc(fgetc(FR), F7); // store character in output text file fputc('\n', F7); // terminate field line with a CR Q += AL; // advance offset pointer to next field } fseek(FR,R + 448,SEEK_SET); // seek first byte of the date fields for(int j = 0; j < 2; j++) { // for each of the two dates: int y = fgetc(FR) << 8 // pull the 2-byte year number | fgetc(FR); int m = fgetc(FR); // pull the 1-byte month number if(m < 0 || m > 12) // if it is out of range m = 0; // set it to zero int d = fgetc(FR); // pull the 1-byte day number if(d < 0 || d > 31) // if it is out of range d = 0; // set it to zero if(d < 10) // if day number less than 10 fprintf(F7, "0"); // put in a leading zero fprintf(F7, "%i", d); // print the day number fprintf(F7, " %s ", MO[m]); // print the month name if(y < 1000) fprintf(F7, "0"); // put in leading zeros on year if(y < 100) fprintf(F7, "0"); if(y < 10) fprintf(F7, "0"); fprintf(F7, "%i\n", y); // print the year number } R += RS; // advance to the start byte of the next record fprintf(F7, "----------------------------------------------------\n"); } // 52 dashes [can be used as an inter-record marker] fclose(F7); // close the output file XSetForeground(XD,XG,GREEN); // display green 'DONE' message XDrawString(XD,XW,XG,390,227,"DONE",4); if(N < 5000) system("gedit records.txt &"); else { XSetForeground(XD,XG,RED); // display green 'DONE' message XDrawString(XD,XW,XG,33,247, "RECORDS FILE CREATED BUT TOO BIG TO DISPLAY IN TEXT EDITOR",58); } } void MT7() { // HANDLES MOUSE CLICKS WITHIN THE MAINT TAB if(mX < 24 || mX > 83) // exit immediately if mouse is outside return; // horizontal bounds of the MAINT buttons int i, y = 55; for(i = 0; i < 10; i++) { // find which button mouse was clicked on if(mY > y && mY < y + 15) // if clicked on button number 'i' break; // break out of loop y += 20; // step to the next button down } switch(i) { // Switch for the 5 MAINT cases case 0: iInteg(); break; // check integrity of indexes case 1: system("gedit idxerr.txt &"); break; // show 'errors' file case 2: iRebuild(); break; // automatically re-build all 3 indexes case 3: ixtotxt(); break; // make text files from indexes case 4: system("gedit namecode.txt &"); break; case 5: system("gedit postcode.txt &"); break; case 6: system("gedit phonenum.txt &"); break; case 7: system("gedit deleteds.txt &"); break; case 8: expRecs(); break; // export records file to a text file case 9: // HELP helpPage("software/mktr.html#maint"); } } // ------------------- ANCILLARY KEYBOARD EVENT HANDLERS --------------------- /* [RE]DISPLAY LINE EDITOR'S CURSOR: called from 1 place each in showED() and lfrt() and from 2 places each in bksp() and hEnd(). */ void doCursor(int sw) { int x = Tx - 1 + CP * 6, // compute horizontal position of cursor c = GREEN; // bright green to show cursor if(sw == 0) c = DARK; // dark colour to erase cursor else if(CP > lE) CP = lE; // make cursor not beyond content length XSetForeground(XD,XG,c); // draw the cursor XDrawLine(XD,XW,XG,x,Ty - 11,x,Ty + 1); } /* [RE]DISPLAY TEXTUAL CONTENT OF LINE EDITOR FIELD Called from 1 place each in bksp(), Del(), KEH(). */ void showED() { XSetForeground(XD,XG,DARK); // clear box XFillRectangle(XD,XW,XG,Tx - 2,Ty - 11,LE * 6 + 2,13); XSetForeground(XD,XG,WHITE); XDrawString(XD,XW,XG,Tx,Ty,ED,lE); // display text doCursor(1); } /* WHEN THE LEFT-ARROW OR THE RIGHT-ARROW KEY IS PRESSED Called only from one place in KEH(). */ void lfrt() { insMsg(0); // clear the INSERT message if present doCursor(0); // erase the cursor from its old position if(KS == 113) { // if it is a left-arrow if(--CP < 0) // then loop from begining CP = lE; // back to end of line-content } else { // else it must be a right-arrow if(++CP > lE) // so loop forwards CP = 0; // to beginning of line content } doCursor(1); // show the cursor in its new position } // WHEN THE 'Backspace' KEY IS PRESSED: Called only fom 1 place in KEH(). void bkSp() { PE = PN; // name & address line/record modified doCursor(0); // if cursor not at start of field if(CP > 0 && CP <= lE) { // AND there is content at or beyond it: CP--; // move cursor one place left for(int i = CP; i < lE; i++) ED[i] = ED[i + 1]; // pull all characters one place left lE--; // decrement the field's content length ED[lE] = (char)0; // terminate shortened field content string showED(); // display the field's textual content } } // WHEN THE 'Delete' KEY IS PRESSED: Called only fom 1 place in KEH(). void Del() { PE = PN; // name & address line/record modified if(CP < lE) { // if cursor position < field content length for(int i = CP; i < lE; i++) ED[i] = ED[i + 1]; // pull all characters one place left ED[lE] = '\0'; // terminate shortened field content string lE--; // decrement the field's content length showED(); // display textual content of ED[] field } } // WHEN THE 'Insert' KEY IS PRESSED: Called only fom 1 place in KEH(). void ins() { static int pn[] = {1,0,0,0,1,1,1,1,0,0,0}; if(pn[PN]) return; // exit if insert function does not apply to panel if(im) insMsg(0); // if 'insert' mode set clear it else insMsg(1); // else set it } // WHEN THE 'Home' or 'End' KEY IS PRESSED: Called only from 2 places in KEH(). void hEnd(int x) { doCursor(0); // erase the cursor at its old position CP = x; // set new position of cursor doCursor(1); // display it in its new position } /* ESCAPE KEY PRESSED WAS PRESSED: de-box the field and re-display its original content. Called only from 1 place in KEH(). */ void escape() { if(PN < 0 || PN > 14) return; insMsg(0); switch(PN) { case 0: editSF(0); break; // a search field case 1: editNA(0); break; // a name & address field case 2: editKN(0); break; // a key names field case 3: editKV(0); break; // a key values field case 4: editSI(0); break; // the SIC field case 5: editKC(0); break; // the Selector Code field case 6: editDT(0); break; // the NEXT/LAST date field case 7: editDY(0,0); break; // the Diary's Dates Panel case 8: editDY(0,1); break; // the Diary's Comments Panel case 9: editMD(0,0); break; // the Meta Data Panel case 10: editMD(0,1); break; // the Data Panel case 11: T5showField(0); break; // Target Tab's SIC case 12: T5showField(1); break; // Target Tab's Selector Code case 13: T5showField(2); break; // Target Tab's Start Date case 14: T5showField(3); // Target Tab's End Date } PE = -1; // set to 'no field has been modified' PN = -1; // set to 'not editing any field' } /* WHEN UP-ARROW, DOWN-ARROW OR 'ENTER' KEY IS PRESSED Called from only one place in KEH. */ void updn() { insMsg(0); // clear INSERT message if(CR == 1) { // if the 'Enter' key was pressed CR = 0; // reset the 'Enter' flag if(TB == 5) { // if we are in the Target Tab T5saveField(T5F); return; } if(PN == 0) { // If we're in a search field, editSF(2); // go do the search return; // then exit. } CP = 0; // set the cursor to the beginning of the field } switch(PN) { // on Panel Number number: case 0: editSF(0); break; // clear the current search field case 1: editNA(2); break; // put & de-box current name & address line case 2: editKN(2); break; // put Key Name case 3: editKV(2); break; // put Key Value case 4: editSI(2); return; // put SIC field case 5: editKC(2); return; // put Selector Code field case 6: editDT(2); break; // put the date field case 7: // updn() has no effect for the case 8: return; // DIARY date or comment fields case 9: editMD(2,0); break; // put the META field case 10: editMD(2,1); // put the DATA field } if(KS == 111) { // If it is an up-arrow then if(--LN < 0) LN = LL; // loop backwards from first to last field } else { // else it must be a down-arrow if(++LN > LL) LN = 0; // so loop forwards from last to first field } switch(PN) { // on Panel Number number: case 0: editSF(1); break; // clear and box ready for editing case 1: editNA(1); break; // get the current name & address line case 2: editKN(1); break; // get Key Name case 3: editKV(1); break; // get Key Value case 6: editDT(1); break; // set up date for editing case 9: editMD(1,0); break; // get META case 10: editMD(1,1); // get DATA } ln = LN; // remember which line we are now on } // --------------- THE FOLLOWING FUNCTIONS ARE CALLED BY MAIN() --------------- /* DISPLAY ALL THE TITLES, BUTTONS AND DEFAULT IN-FILL FOR CURRENT TAB which is specified by the numeric value of the global variable 'TB': 1=ADDRESS Tab, 2=PROFILE Tab, 3=DIARY Tab etc.. Called from only 1 place each in main() and MEH(). */ void showTab() { PN = 0, // Panel Number currently selected [initial default 0:search] PE = -1, // Field type number of the field last Modified by the Line Editor KE = -1, // Key Edit mode: 0:no mode, 1:editing names, 2: editing values LN = -1; // reset number of line being edited XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,24,55,453,380); // clear Tab area const char *BA[8] = { "ADDRESS", "PROFILE", "DIARY", "DATA", "REGIONS", "TARGETS", "LETTERS", "MAINT" }; const int NC[8] = {7,7,5,4,7,7,7,5}, // number of chars in each annotation NB[8] = {0,0,5,8,0,0,0,5}; // x-bias for each annotation XSetForeground(XD,XG, DARK); // shade button background for(int i = 0; i < 8; i++) // draw each button XFillRectangle(XD,XW,XG,LM + BW * i,17,53,21); XSetForeground(XD,XG, GREY); // most buttons are grey for(int i = 0; i < 8; i++) { // annotate each button if(TB == i) XSetForeground(XD,XG,WHITE); else XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,30 + BW * i + *(NB + i),32,*(BA + i),*(NC + i)); } switch(TB) { // switch on tab number case 0: T0show(); break; // paint the ADDRESS tab case 1: T1show(); break; // paint the PROFILE tab case 2: T2show(); break; // paint the DIARY tab case 3: T3show(); break; // paint the DATA tab case 4: T4show(); break; // paint the REGIONS tab case 5: T5show(); break; // paint the TARGETS tab case 6: T6show(); break; // paint the LETTERS tab case 7: T7show(); break; // paint the MAINTENANCE tab } } /* THE KEYBOAD EVENTS HANDLER: A key press event generates two values: a key- board Scan Code and an ASCII value. All key-press events generate a Scan Code but an ASCII value is given only for printable characters. For each key-press event, a Scan Code and an ASCII value [where applicable] are put in the global variables KS and KA, after which main() calls this function to act upon the values presented. Called only from 1 place in main(). */ void KEH() { // DEAL WITH NON-PRINTABLE CONTROL CHARACTERS switch(KS) { // KEYBOARD SCAN CODES SWITCH case 50: return; // block the shift code case 9: escape(); return; // if 'Esc' key pressed. case 36: CR = 1; // carriage return case 111: // up-arrow key case 116: updn(); return; // down-arrow case 113: // if LEFTWARDS arrow key pressed case 114: lfrt(); return; // if RIGHTWARDS-arrow key pressed case 110: hEnd(0); return; // 'Home' key case 115: hEnd(lE); return; // 'End' key case 118: ins(); return; // if 'Insert' key pressed case 22: bkSp(); return; // if 'Backspace' key pressed case 119: Del(); return; // if 'Delete' key pressed } if(KA < 32 || KA > 127) return; // non-printable ASCII character // VALIDATE PRINTABLE CHARACTERS ACCORDING TO FIELD-TYPE AND LINE NUMBER switch(PN) { // PN = Panel Number case 0: // search field if(valSF() < 0) return; // validate according to line number LN break; case 1: // name & address field if(valNA() < 0) return; // validate according to line number LN break; case 4: // SIC field if(KA < '0' || KA > '9') // only allow numbers 0 to 9 return; break; case 5: // Selector Code case 6: // event dates case 7: // diary dates if((KA = A9S(KA)) == -1) // only characters A-Z or 0-9 return; } /* LINE EDITOR: All printable characters are received by this Line Editor which places each, as it is received, in its appropriate place within the Line Editor's character array ED[]. */ if(im == 1) { // if in 'Insert' mode if(lE < LE) { // if still space in the field for(int i = lE; i >= CP; i--) // Working backwards, shift all chars ED[i + 1] = ED[i]; // from cursor position 1 place right. lE++; // increment length of field content } else insMsg(0); // else cancel 'insert' mode } if(CP < LE) { // if cursor not yet reached field boundary ED[CP] = KA; // put the new character at cursor position if(CP >= lE) { // if currently at end of textual content ED[CP + 1] = (char)0; // make character beyond into a NULL char lE++; // increment number of characters of content } if(CP < LE) CP++; // advance cursor if not at field boundary showED(); // display textual content of ED[] array } PE = PN; // indicates that content of a field Type PN has been Modified } // END OF THE KEYBOARD EVENTS HANDLER KEH() /* THE MOUSE EVENTS HANDLER: Every time the mouse's left button is pressed, main() puts the current position of the mouse [its horizontal and vertical pixel coordinates within the program's window] into the global variables 'mX' and 'mY' respectively, then calls this function to process the mouse event according to these coordinates. None of the other mouse buttons are currently used by this program. Called from only 1 place in main(). */ void MEH() { if(mY > 16 && mY < 38) { // if mouse's Y-coord within Tab buttons: for(TB = 0; TB < 8; TB++) { // for each possible Tab button int B = BW * TB; if(mX > 23 + B && mX < 77 + B) break; } if(TB < 8) // If click was inside an actual tab button, highlight showTab(); // the appropriate tab button and display tab content. return; } switch(TB) { // Tab Switch for each of the Tabs case 0: MT0(); break; // Name & Address Tab case 1: MT1(); break; // Profile Tab case 2: MT2(); break; // Diary Tab case 3: MT3(); break; // Notes Tab case 4: MT4(); break; // Areas Tab case 5: MT5(); break; // Targets Tab case 6: MT6(); break; // Statistics Tab case 7: MT7(); // Maintenance Tab } } /* OPEN ALL SESSION FILES AND LOAD SESSION DATA Called from only one place in main(). */ void initLoad() { // LOAD THE POSTCODE MAP DATA INTO THE APPROPRIATE DATA ARRAYS FX = fopen("maps/uk.map","rb"); // open the map data file for(int i = 0; i < 120; i++) // for each of the 120 post areas: PC[i] = (short)(fgetc(FX) << 8 | fgetc(FX)); // get 2-letter area code for(int i = 0; i < 120; i++) // for each of the 120 post areas: PS[i] = (short)(fgetc(FX) << 8 | fgetc(FX)); // get 16-bit start element for(int i = 0; i < 120; i++) // for each of the 120 post areas: PL[i] = (unsigned char)fgetc(FX); // get number of horizontal lines for(int i = 0; i < 1000; i++) // for each of the 1000 coastal line plots for(int j = 0; j < 2; j++) // for start plot and end plot CO[i][j] = (char)fgetc(FX); // get the 1-byte plot for(int i = 0; i < 2376; i++) // for the 2376 post area lines for(int j = 0; j < 2; j++) // for each of the two plots per line PA[i][j] = (char)fgetc(FX); // get the 1-byte plot fclose(FX); // close the map data file T4load(); // load the standard target regions data T4get(); // load the currently selected regional target data // OPEN ALL MARKETEER'S SESSION FILES FR = fopen("db/contacts.nad","rb+"); // open the names & addresses file RD = getInt(FR); // get the total number of records FN = fopen("db/contacts.name","rb+"); // open the namecodes index file IX = getInt(FN); // get total number of namecode records FX = FN; // set file pointer to namecodes [default] FP = fopen("db/contacts.post","rb+"); // open the postcode index file FC = fopen("db/contacts.fone","rb+"); // open the phone index file FD = fopen("db/contacts.del","rb+"); // open the deleteds index file DL = getInt(FD); // get total number of deleted records FI[0] = FN; // put pointer to namecodes file in switching array FI[1] = FP; // put pointer to postcodes file in switching array FI[2] = FC; // put pointer to phonenums file in switching array FK = fopen("db/contacts.keys","rb+"); // open the profile keys file FE[0] = NULL; // no file involved in setting up search field FE[1] = FR; // file for setting up name & address field for editing FE[2] = FK; // file for setting up key names field for editing FE[3] = FK; // file for setting up key values field for editing FE[4] = FR; // file for setting up SIC field for editing FE[5] = FR; // file for setting up Selector Code field for editing FE[6] = FR; // file for setting up NEXT/LAST dates fields for editing FE[7] = FR; // file for setting up diary date field for editing FE[8] = FR; // file for setting up diary comment field for editing getNR(); // get number NR of records for which there is available space getNI(); // get number NI of index recs for which there is available space getND(); // get number ND of deleteds for which there is available space } // ---------------------------------- MAIN() ---------------------------------- /* MAIN FUNCTION FROM WHICH ALL THE ABOVE APPLICATION FUNCTIONS ARE CALLED Creates mouse event and X-window objects, opens all session files, then establishes a permanent loop which can be interrupted by window exposure, keyboard, mouse and window control events. Closes all files and the X- window then terminates the program in response to a window close event. Calls only 3 functions: the exposure display/redisplay function showTab() plus the mouse & keyboard event handlers MEH() & KEH(). */ int main(int argc, char *argv[]) { time_t t = time(NULL); // GET SYSTEM DAT struct tm tm = *localtime(&t); sY = (int)(tm.tm_year + 1900); // integral year number sM = (int)(tm.tm_mon + 1); // integral month number sD = (int)(tm.tm_mday); // integral day number XEvent e; // reference to any event from the created window int s, // number of the current default screen mouseB, // number of the mouse button that was pressed 0, 1, 2, 3, 4 upper; // 1:upper case /* Check that you can actually open the X-display by getting a non-NULL reference to a display. Else send an error message to the terminal. */ XD = XOpenDisplay(NULL); if(XD == NULL) { fprintf(stderr,"Cannot open display\n"); exit(1); } s = DefaultScreen(XD); // get the number of the default screen XG = DefaultGC(XD,s); // set the graphics context to use XW = XCreateSimpleWindow( // create an application window XD, // within the default display RootWindow(XD, s), // within the root window [desktop] 10, 10, // position of top right corner 500, 400, // width and height of the window 1, // border width in pixels BlackPixel(XD, s), // colour of window's border BlackPixel(XD, s) // colour of window's background ); // set window title XStoreName(XD,XW,"EBS MARKETEER II by Robert John Morton UK-YE572246C"); /* Make input possible from the keyboard, window controls and mouse then display the new window on the screen. Not all these bit masks are needed so comment out those that are not needed. */ XSelectInput( XD, XW, ExposureMask | ButtonPressMask | KeyPressMask // | KeyReleaseMask // | PointerMotionMask // | ButtonReleaseMask // | StructureNotifyMask ); XMapWindow(XD,XW); XFlush(XD); /* Define a unique indentifier WM_DELETE_WINDOW (referred to as an atom) that will invoke the X11 protocol that closes windows upon the display 'XD' we have just created. [At least I think this is what it means] */ Atom WM_DELETE_WINDOW = XInternAtom(XD,"WM_DELETE_WINDOW",False); /* Make 'delete window' events from our particular window visible as a ClientMessage event.*/ XSetWMProtocols(XD,XW,&WM_DELETE_WINDOW,1); initLoad(); // open all session files and load session data /* The event-handling [or 'run'] loop [a permanent loop] broken only by the 'break' function in response to an external event. */ while(1) { // permanent loop interruptible by X11 events time_t st = clock(); // start time for execution of this pass if(XPending(XD) > 0) { // Provided there exist events on event queue, XNextEvent(XD,&e); // go fetch the first one. if(e.type == Expose) { // if an 'expose' event has occurred LN = -1; // to avoid highlighting a name & address field showTab(); // draw all titles and butons in window } else if(e.type == ButtonPress) { // if a mouse button was clicked: if (e.xbutton.button == Button1) { // if Mouse Button 1 was clicked mX = e.xbutton.x; // x-coord of mouse click mY = e.xbutton.y; // y-coord of mouse click MEH(); // Mouse Event Handler [MEH] } } else if(e.type == KeyPress) { // if a key has been pressed: KS = e.xkey.keycode; // keyboard scan code of pressed key KA = (int)XkbKeycodeToKeysym ( // ASCII code of the scanned key XD, // window display pointer KS, // key scan [position] code 0, // key level e.xkey.state & ShiftMask ? 1 : 0 // upper or lower case ); KEH(); // Keyboard Event Handler [KEH] } /* Else if a box control has been clicked, break out of the permanent while() loop, close the window and exit....Should also check here for other client message types; but as the only protocol that is reg- istered above is WM_DELETE_WINDOW, it is safe for this program. */ else if(e.type == ClientMessage) break; // break the permanent while() loop } // end of if(Xpending(XD) > 0) usleep(100000 - clock() + st); // sleep for the rest of the 100ms cycle } // end of permanent while() loop fclose(FR); // close the names & addresses file fclose(FN); // close the namecodes index file fclose(FP); // close the postcode index file fclose(FC); // close the phone numbers index file fclose(FD); // close the deleteds index file fclose(FK); // close the deleteds index file XCloseDisplay(XD); // clear this window from the X11 display 'XD' return 0; // return that everything went OK } // end of main()